mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-07 09:19:13 +00:00
The 0a-INDEPENDENT code slice (one-shot via af-0b-package-slice workflow: Haiku scaffold + Sonnet packages, Sonnet fix-to-green). Nothing deploys; no CDK/servers. - Monorepo scaffold: npm workspaces, strict TS (NodeNext), vitest (80% gate), eslint 9 flat config, prettier; ci.yaml/deploy.yaml callers (Node 24, enable-qemu). - @sh-mcp/shared: transport-agnostic core — Scope/AuthContext/ToolDef, ToolRegistry, redact()+maskValue() (PII), OpenAPI 3.1 generator. AUTH STUBBED behind an AuthProvider interface (TODO auth-layer-0a); JWT/aud/client_id/JWKS/deny-list deferred per design.md §2. - 9 integration packages (qbo, google-maps, internal-data, payments, knowledge-base, gmail, calendar, tasks, reminders): tools against shared, external deps mocked behind injected client interfaces; finance handlers call redact(). Verified green: tsc -b clean, vitest 245/245, eslint 0 errors. Auth mechanism intentionally deferred until the 0a spike resolves it (G16/§0.4).
124 lines
4.8 KiB
TypeScript
124 lines
4.8 KiB
TypeScript
/**
|
|
* TasksClient — external-dependency interface for the tasks package.
|
|
*
|
|
* All code in tools.ts codes against the TasksClient interface, never against a
|
|
* concrete AWS SDK import. The real implementation (DynamoDBTasksClient) stubs
|
|
* the actual DynamoDB call so no AWS credentials or network are needed at import
|
|
* time or in tests.
|
|
*
|
|
* Tests inject a MockTasksClient (see test/tasks.test.ts).
|
|
*/
|
|
|
|
export interface Task {
|
|
taskId: string;
|
|
sub: string; // owner — partition key, enforced ABAC (dynamodb:LeadingKeys)
|
|
title: string;
|
|
description?: string;
|
|
completed: boolean;
|
|
createdAt: string; // ISO-8601
|
|
completedAt?: string; // ISO-8601
|
|
}
|
|
|
|
export interface CreateTaskInput {
|
|
sub: string;
|
|
title: string;
|
|
description?: string;
|
|
}
|
|
|
|
export interface ListTasksInput {
|
|
sub: string;
|
|
includeCompleted?: boolean;
|
|
}
|
|
|
|
export interface CompleteTaskInput {
|
|
sub: string;
|
|
taskId: string;
|
|
}
|
|
|
|
export interface DeleteTaskInput {
|
|
sub: string;
|
|
taskId: string;
|
|
}
|
|
|
|
/**
|
|
* The interface every caller (tools.ts, jobs, tests) depends on.
|
|
* The DynamoDB table is partitioned by `sub`; callers always pass their own sub
|
|
* so the ABAC LeadingKeys condition on the IAM policy matches.
|
|
*/
|
|
export interface TasksClient {
|
|
createTask(input: CreateTaskInput): Promise<Task>;
|
|
listTasks(input: ListTasksInput): Promise<Task[]>;
|
|
completeTask(input: CompleteTaskInput): Promise<Task>;
|
|
deleteTask(input: DeleteTaskInput): Promise<void>;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Real (DynamoDB) implementation
|
|
// ---------------------------------------------------------------------------
|
|
// The real AWS SDK import is lazy and guard-wrapped so that:
|
|
// 1. Importing this file at test time does NOT instantiate a real SDK client.
|
|
// 2. A real deployment provides TABLE_NAME and AWS credentials via the
|
|
// Lambda execution environment.
|
|
//
|
|
// TODO (DEFERRED — auth layer): Once the real JWT/aud/client_id validation
|
|
// layer is in place, ensure the DynamoDB client is constructed with a role that
|
|
// only has `dynamodb:GetItem`, `dynamodb:PutItem`, `dynamodb:UpdateItem`,
|
|
// `dynamodb:DeleteItem`, `dynamodb:Query` on the tasks table, scoped to
|
|
// `dynamodb:LeadingKeys` = `${cognito-identity.amazonaws.com:sub}` so a
|
|
// compromised server cannot read another user's tasks.
|
|
|
|
const TABLE_NAME = process.env['TASKS_TABLE_NAME'] ?? 'sh-mcp-tasks';
|
|
|
|
export class DynamoDBTasksClient implements TasksClient {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
private ddb: any; // typed as `any` to avoid importing @aws-sdk/client-dynamodb at the top level
|
|
|
|
constructor() {
|
|
if (process.env['NODE_ENV'] === 'test') {
|
|
throw new Error(
|
|
'DynamoDBTasksClient must not be instantiated in tests. Inject a mock TasksClient instead.',
|
|
);
|
|
}
|
|
// Lazy import — only reached in a real Lambda execution environment.
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const { DynamoDBDocumentClient } = require('@aws-sdk/lib-dynamodb');
|
|
this.ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
|
// Accessed once so TypeScript does not flag the field as write-only.
|
|
// Remove when the real DynamoDB calls are wired in the methods below.
|
|
void this.ddb;
|
|
}
|
|
|
|
async createTask(input: CreateTaskInput): Promise<Task> {
|
|
// TODO: replace this stub with a real `PutCommand` against TABLE_NAME.
|
|
// Stub guards against accidental real calls during development.
|
|
throw new Error(
|
|
`DynamoDBTasksClient.createTask not yet implemented. Table: ${TABLE_NAME}, input: ${JSON.stringify(input)}`,
|
|
);
|
|
}
|
|
|
|
async listTasks(input: ListTasksInput): Promise<Task[]> {
|
|
// TODO: replace with a real `QueryCommand` (KeyConditionExpression: 'sub = :sub',
|
|
// optionally FilterExpression: 'completed = :completed').
|
|
throw new Error(
|
|
`DynamoDBTasksClient.listTasks not yet implemented. Table: ${TABLE_NAME}, input: ${JSON.stringify(input)}`,
|
|
);
|
|
}
|
|
|
|
async completeTask(input: CompleteTaskInput): Promise<Task> {
|
|
// TODO: replace with a real `UpdateCommand` setting completed = true, completedAt = now.
|
|
// Enforce ownership: ConditionExpression: 'sub = :sub' so a user cannot complete another
|
|
// user's task even if they guess the taskId.
|
|
throw new Error(
|
|
`DynamoDBTasksClient.completeTask not yet implemented. Table: ${TABLE_NAME}, input: ${JSON.stringify(input)}`,
|
|
);
|
|
}
|
|
|
|
async deleteTask(input: DeleteTaskInput): Promise<void> {
|
|
// TODO: replace with a real `DeleteCommand` with the same sub-ownership condition.
|
|
throw new Error(
|
|
`DynamoDBTasksClient.deleteTask not yet implemented. Table: ${TABLE_NAME}, input: ${JSON.stringify(input)}`,
|
|
);
|
|
}
|
|
}
|