mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-02 07:13:17 +00:00
Some checks are pending
deploy / deploy (push) Waiting to run
* Phase 0b slice: monorepo scaffold + shared core + integration packages 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). * Complete Cognito auth provider + Phase 1 build brief Finish the WIP CognitoAuthProvider (client_id allow-list as audience boundary, finance TTL ceiling, deny-list, scope-prefix stripping) with its test suite, and check in docs/build-plan-phase-1.md so the Phase 1 work has its governing brief in-tree (design.md §2.5). * ci: disable cdk synth for Phase 0b (no CDK app yet) The reusable ci-typescript-cdk workflow defaults run-cdk-synth: true, but the Phase 0b package scaffold has no cdk.json or stacks, so cdk synth fails with '--app is required'. Disable it here; Phase 1 re-enables it with the server CDK stubs.
126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
/**
|
||
* QBO client interface and implementation.
|
||
*
|
||
* The real QuickBooks Online API is reached via OAuth 2.0 with a server-held
|
||
* refresh token (stored in AWS Secrets Manager). The implementation below
|
||
* stubs the actual HTTP call so no real network I/O happens at import time.
|
||
*
|
||
* TODO (DEFERRED — auth layer, phase 3): Replace the stub in QboClientImpl with
|
||
* a real intuit-oauth + node-quickbooks (or raw fetch) call that:
|
||
* 1. Reads the refresh token from Secrets Manager at
|
||
* arn:aws:secretsmanager:us-east-1:328440206208:secret:sh-mcp/qbo-oauth
|
||
* 2. Exchanges it for a short-lived access token on demand (and rotates the
|
||
* stored refresh token when QBO returns a new one).
|
||
* 3. Issues the GET /v3/company/{realmId}/query?query=... request.
|
||
* 4. Never logs or returns the access/refresh tokens.
|
||
* The QboClientInterface below is the stable contract; the implementation is
|
||
* injected, so tests and the real server each provide their own.
|
||
*/
|
||
|
||
/** A vendor record as returned by QBO's vendor query endpoint. */
|
||
export interface QboVendor {
|
||
/** QBO internal vendor ID */
|
||
id: string;
|
||
/** Display name of the vendor */
|
||
displayName: string;
|
||
/** Primary contact email, if present */
|
||
email?: string;
|
||
/** Primary phone number, if present */
|
||
phone?: string;
|
||
/** Whether the vendor is currently active */
|
||
active: boolean;
|
||
/** Vendor balance (amount owed), expressed as a number */
|
||
balance?: number;
|
||
/**
|
||
* Tax identification number. Treated as sensitive — callers MUST pass this
|
||
* through redact() before including it in a tool response.
|
||
*/
|
||
taxId?: string;
|
||
}
|
||
|
||
/** Parameters forwarded to the QBO vendor query. */
|
||
export interface SearchVendorsParams {
|
||
/** Free-text search term matched against vendor display name */
|
||
query: string;
|
||
/** Maximum number of results to return (1–100, default 20) */
|
||
maxResults?: number;
|
||
}
|
||
|
||
/** Result envelope returned by the QBO vendor query. */
|
||
export interface SearchVendorsResult {
|
||
vendors: QboVendor[];
|
||
/** Total matching vendors in QBO (may exceed vendors.length if paginated) */
|
||
totalCount: number;
|
||
}
|
||
|
||
/**
|
||
* The interface every consumer codes against.
|
||
* The real implementation, a test mock, and any future adapters all satisfy
|
||
* this shape — nothing in src/ imports a concrete HTTP library directly.
|
||
*/
|
||
export interface QboClientInterface {
|
||
/**
|
||
* Search QBO vendors by display name.
|
||
*
|
||
* @throws {QboThrottleError} when QBO returns HTTP 429.
|
||
* @throws {QboApiError} for any other non-2xx response.
|
||
*/
|
||
searchVendors(params: SearchVendorsParams): Promise<SearchVendorsResult>;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Error types
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export class QboApiError extends Error {
|
||
constructor(
|
||
message: string,
|
||
public readonly statusCode: number,
|
||
) {
|
||
super(message);
|
||
this.name = 'QboApiError';
|
||
}
|
||
}
|
||
|
||
export class QboThrottleError extends Error {
|
||
constructor(
|
||
/** Seconds to wait before retrying, if provided by QBO */
|
||
public readonly retryAfterSeconds?: number,
|
||
) {
|
||
super('QBO rate limit exceeded');
|
||
this.name = 'QboThrottleError';
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Thin implementation (real call stubbed — see TODO above)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Thin wrapper around the QuickBooks Online Accounting API.
|
||
*
|
||
* STUBBED: the actual HTTP call is replaced with a NotImplementedError so
|
||
* this class can be imported without triggering real network I/O or requiring
|
||
* AWS credentials. Inject a mock (see test/qbo.test.ts) in unit tests.
|
||
*/
|
||
export class QboClientImpl implements QboClientInterface {
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
async searchVendors(_params: SearchVendorsParams): Promise<SearchVendorsResult> {
|
||
// TODO (DEFERRED — auth layer): implement real QBO API call.
|
||
// Steps:
|
||
// 1. Retrieve OAuth refresh token from Secrets Manager.
|
||
// 2. Exchange for QBO access token (rotate stored refresh token if renewed).
|
||
// 3. Build QBO SQL query:
|
||
// SELECT * FROM Vendor WHERE DisplayName LIKE '%{query}%'
|
||
// STARTPOSITION 1 MAXRESULTS {maxResults}
|
||
// 4. GET https://quickbooks.api.intuit.com/v3/company/{realmId}/query
|
||
// with Authorization: Bearer {accessToken}
|
||
// 5. Map QBO QueryResponse.Vendor[] → SearchVendorsResult.
|
||
// 6. On HTTP 429 → throw QboThrottleError(retryAfterSeconds).
|
||
// 7. On other non-2xx → throw QboApiError(message, statusCode).
|
||
throw new Error(
|
||
'QboClientImpl.searchVendors is not yet implemented. ' +
|
||
'Inject a mock QboClientInterface for tests, or implement the real call (see TODO above).',
|
||
);
|
||
}
|
||
}
|