From 8a8aeaedd792821696e74985efe8b95f306458c5 Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 12 Oct 2026 22:32:21 +0200 Subject: [PATCH] u --- API.mcp.md | 177 +++++++++++++++++++++++++++ API.md | 11 ++ scripts/test-mcp.js | 94 ++++++++++++++ scripts/test-shipment.js | 12 ++ src/index.js | 23 ++++ src/providers/mssql/queries/mcp.js | 90 ++++++++++++++ src/providers/mssql/schema.js | 58 +++++++++ src/providers/sqlite3/index.js | 16 ++- src/providers/sqlite3/queries/mcp.js | 16 +++ src/providers/sqlite3/schema.js | 47 +++++++ src/store/mcp/consumeAuthCode.js | 16 +++ src/store/mcp/createAuthCode.js | 43 +++++++ src/store/mcp/createRefreshToken.js | 34 +++++ src/store/mcp/deleteClient.js | 16 +++ src/store/mcp/getAuthCode.js | 16 +++ src/store/mcp/getClient.js | 16 +++ src/store/mcp/getRefreshToken.js | 16 +++ src/store/mcp/listClients.js | 15 +++ src/store/mcp/registerClient.js | 55 +++++++++ src/store/mcp/revokeRefreshToken.js | 16 +++ src/store/numbers/peekNumber.js | 3 +- 21 files changed, 786 insertions(+), 4 deletions(-) create mode 100644 API.mcp.md create mode 100644 scripts/test-mcp.js create mode 100644 src/providers/mssql/queries/mcp.js create mode 100644 src/providers/sqlite3/queries/mcp.js create mode 100644 src/store/mcp/consumeAuthCode.js create mode 100644 src/store/mcp/createAuthCode.js create mode 100644 src/store/mcp/createRefreshToken.js create mode 100644 src/store/mcp/deleteClient.js create mode 100644 src/store/mcp/getAuthCode.js create mode 100644 src/store/mcp/getClient.js create mode 100644 src/store/mcp/getRefreshToken.js create mode 100644 src/store/mcp/listClients.js create mode 100644 src/store/mcp/registerClient.js create mode 100644 src/store/mcp/revokeRefreshToken.js diff --git a/API.mcp.md b/API.mcp.md new file mode 100644 index 0000000..d4f8fcc --- /dev/null +++ b/API.mcp.md @@ -0,0 +1,177 @@ +# mcp + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `mcp.consumeAuthCode` + +*Source: `src/store/mcp/consumeAuthCode.js`* + +Marks an authorization code as used. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `code` | `string` | The authorization code to mark used. | + +**Returns** + +`Promise` — True once marked. + +## `mcp.createAuthCode` + +*Source: `src/store/mcp/createAuthCode.js`* + +Stores an OAuth 2.1 authorization code with PKCE parameters. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | | +| `input.code` | `string` | The generated authorization code. | +| `input.clientId` | `string` | The client ID. | +| `input.googleAuthId` | `number` | The ID of the authenticated user. | +| `input.redirectUri` | `string` | The redirect URI used in the authorize request. | +| `input.codeChallenge` | `string` | The PKCE code challenge. | +| `input.codeChallengeMethod` | `string` | The PKCE method (e.g. S256). | +| `[input.scope]` | `string|null` | Granted scope. | +| `input.expiresAt` | `string|Date` | Expiration ISO string or Date. | + +**Returns** + +`Promise` — The authorization code. + +## `mcp.createRefreshToken` + +*Source: `src/store/mcp/createRefreshToken.js`* + +Stores an OAuth 2.1 refresh token. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | | +| `input.refreshToken` | `string` | The refresh token string. | +| `input.clientId` | `string` | The client ID. | +| `input.googleAuthId` | `number` | The user ID. | +| `[input.scope]` | `string|null` | Scopes granted. | +| `input.expiresAt` | `string|Date` | Expiration Date or ISO string. | + +**Returns** + +`Promise` — Created token record or id. + +## `mcp.deleteClient` + +*Source: `src/store/mcp/deleteClient.js`* + +Deletes a registered MCP client by client ID. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `clientId` | `string` | The client identifier to delete. | + +**Returns** + +`Promise` — True if deleted. + +## `mcp.getAuthCode` + +*Source: `src/store/mcp/getAuthCode.js`* + +Retrieves an authorization code record. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `code` | `string` | The authorization code. | + +**Returns** + +`Promise` — The auth code record or null. + +## `mcp.getClient` + +*Source: `src/store/mcp/getClient.js`* + +Retrieves an MCP client by client ID. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `clientId` | `string` | The client identifier. | + +**Returns** + +`Promise` — The client record or null if not found. + +## `mcp.getRefreshToken` + +*Source: `src/store/mcp/getRefreshToken.js`* + +Retrieves an OAuth 2.1 refresh token. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `refreshToken` | `string` | The refresh token. | + +**Returns** + +`Promise` — The refresh token record or null. + +## `mcp.listClients` + +*Source: `src/store/mcp/listClients.js`* + +Lists all registered MCP clients. + +**Returns** + +`Promise>` — List of client records. + +## `mcp.registerClient` + +*Source: `src/store/mcp/registerClient.js`* + +Registers a new OAuth MCP client. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | | +| `input.clientId` | `string` | Unique client identifier. | +| `[input.clientSecret]` | `string|null` | Optional client secret for confidential clients. | +| `input.clientName` | `string` | Human-readable client name. | +| `input.redirectUris` | `string|Array` | JSON string or array of redirect URIs. | +| `[input.grantTypes]` | `string|Array` | Supported grant types. | +| `[input.responseTypes]` | `string|Array` | Supported response types. | +| `[input.scope]` | `string|null` | Allowed scope. | + +**Returns** + +`Promise` — The created client record. + +## `mcp.revokeRefreshToken` + +*Source: `src/store/mcp/revokeRefreshToken.js`* + +Revokes an OAuth 2.1 refresh token. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `refreshToken` | `string` | The refresh token to revoke. | + +**Returns** + +`Promise` — True once revoked. diff --git a/API.md b/API.md index 83bfc85..cf9ae12 100644 --- a/API.md +++ b/API.md @@ -307,6 +307,17 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `delete(googleAuthId: number, categoryId: number, propertyId: number, tx?: object): Promise` - `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise>` - `setByCategoryId(googleAuthId: number, categoryId: number, properties: Array<{propertyId: number, value: string, sort?: number}>, tx?: object): Promise>` +- [mcp](API.mcp.md) + - `consumeAuthCode(code: string): Promise` + - `createAuthCode(input: {code: string, clientId: string, googleAuthId: number, redirectUri: string, codeChallenge: string, codeChallengeMethod: string, scope?: string|null, expiresAt: string|Date}): Promise` + - `createRefreshToken(input: {refreshToken: string, clientId: string, googleAuthId: number, scope?: string|null, expiresAt: string|Date}): Promise` + - `deleteClient(clientId: string): Promise` + - `getAuthCode(code: string): Promise` + - `getClient(clientId: string): Promise` + - `getRefreshToken(refreshToken: string): Promise` + - `listClients(): Promise>` + - `registerClient(input: {clientId: string, clientSecret?: string|null, clientName: string, redirectUris: string|Array, grantTypes?: string|Array, responseTypes?: string|Array, scope?: string|null}): Promise` + - `revokeRefreshToken(refreshToken: string): Promise` - [modelconversation](API.modelconversation.md) - `create(googleAuthId: number, input: {provider: string, model: string, started_by?: number|null}, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null}>` - `delete(googleAuthId: number, conversationId: number, tx?: object): Promise` diff --git a/scripts/test-mcp.js b/scripts/test-mcp.js new file mode 100644 index 0000000..59cf37a --- /dev/null +++ b/scripts/test-mcp.js @@ -0,0 +1,94 @@ +import assert from 'node:assert'; +import LibDashStore from '../src/index.js'; +import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js'; + +async function main() { + console.log('Testing MCP store methods on in-memory SQLite...'); + const provider = new BetterSqlite3Provider({ path: ':memory:' }); + await provider.initialize(); + const store = await new LibDashStore(provider); + + // 1. Create a google_auth user + const googleAuthId = await store.google_auth.add('mcp-tester@example.com'); + assert(googleAuthId > 0, 'googleAuthId should be positive integer'); + + // 2. Register client + const client = await store.mcp.registerClient({ + clientId: 'test-client-123', + clientSecret: 'secret-456', + clientName: 'Test Client', + redirectUris: ['http://localhost:3000/callback'], + grantTypes: ['authorization_code', 'refresh_token'], + responseTypes: ['code'], + scope: 'mcp:tools', + }); + console.log('Registered client:', client); + assert.strictEqual(client.clientId, 'test-client-123'); + assert.strictEqual(client.clientName, 'Test Client'); + + // 3. Get client + const fetched = await store.mcp.getClient('test-client-123'); + assert(fetched, 'Should find client'); + assert.strictEqual(fetched.clientId, 'test-client-123'); + + // 4. List clients + const clients = await store.mcp.listClients(); + assert.strictEqual(clients.length, 1); + + // 5. Create auth code + const code = 'auth-code-xyz'; + await store.mcp.createAuthCode({ + code, + clientId: 'test-client-123', + googleAuthId, + redirectUri: 'http://localhost:3000/callback', + codeChallenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM', + codeChallengeMethod: 'S256', + scope: 'mcp:tools', + expiresAt: new Date(Date.now() + 300_000), + }); + + // 6. Get auth code + const codeRow = await store.mcp.getAuthCode(code); + assert(codeRow, 'Should find auth code'); + assert.strictEqual(codeRow.clientId, 'test-client-123'); + assert.strictEqual(codeRow.used, 0); + + // 7. Consume auth code + await store.mcp.consumeAuthCode(code); + const consumedRow = await store.mcp.getAuthCode(code); + assert.strictEqual(consumedRow.used, 1); + + // 8. Create refresh token + const refreshToken = 'refresh-token-abc'; + await store.mcp.createRefreshToken({ + refreshToken, + clientId: 'test-client-123', + googleAuthId, + scope: 'mcp:tools', + expiresAt: new Date(Date.now() + 86400_000), + }); + + // 9. Get refresh token + const tokenRow = await store.mcp.getRefreshToken(refreshToken); + assert(tokenRow, 'Should find refresh token'); + assert.strictEqual(tokenRow.revoked, 0); + + // 10. Revoke refresh token + await store.mcp.revokeRefreshToken(refreshToken); + const revokedRow = await store.mcp.getRefreshToken(refreshToken); + assert.strictEqual(revokedRow.revoked, 1); + + // 11. Delete client + await store.mcp.deleteClient('test-client-123'); + const deleted = await store.mcp.getClient('test-client-123'); + assert.strictEqual(deleted, null); + + await provider.close(); + console.log('✓ All MCP store tests passed successfully!'); +} + +main().catch((err) => { + console.error('Test failed:', err); + process.exit(1); +}); diff --git a/scripts/test-shipment.js b/scripts/test-shipment.js index 5a86bcf..3d2ad4d 100644 --- a/scripts/test-shipment.js +++ b/scripts/test-shipment.js @@ -214,6 +214,18 @@ async function run() { assert.equal(shipmentsAfterDel.length, 1); console.log('PASS: Unshipped shipment 2 successfully deleted'); + // 9. Verify peek next number works for entity and other types without error + const nextEntity = await store.entity.peekNextNumber(googleAuthId); + assert.ok(typeof nextEntity === 'string' && nextEntity.length > 0); + console.log('PASS: store.entity.peekNextNumber succeeded:', nextEntity); + + const numList = await store.numbers.list(googleAuthId); + assert.ok(Array.isArray(numList) && numList.length > 0); + const shipmentSeq = numList.find((n) => n.type === 'shipment'); + assert.ok(shipmentSeq, 'Expected shipment sequence in numbers.list'); + assert.equal(shipmentSeq.prefix, '-L-'); + console.log('PASS: store.numbers.list includes shipment with prefix -L-'); + console.log('--- ALL TESTS PASSED SUCCESSFULLY! ---'); } finally { try { diff --git a/src/index.js b/src/index.js index 8e8cbdc..ec7d64d 100644 --- a/src/index.js +++ b/src/index.js @@ -230,6 +230,16 @@ import articlelistGetItems from './store/articlelist/getItemsByArticleSku.js'; import articlelistSetItems from './store/articlelist/setItemsByArticleSku.js'; import optionslistGetOptions from './store/optionslist/getOptionsByArticleSku.js'; import optionslistSetOptions from './store/optionslist/setOptionsByArticleSku.js'; +import mcpRegisterClient from './store/mcp/registerClient.js'; +import mcpGetClient from './store/mcp/getClient.js'; +import mcpListClients from './store/mcp/listClients.js'; +import mcpDeleteClient from './store/mcp/deleteClient.js'; +import mcpCreateAuthCode from './store/mcp/createAuthCode.js'; +import mcpGetAuthCode from './store/mcp/getAuthCode.js'; +import mcpConsumeAuthCode from './store/mcp/consumeAuthCode.js'; +import mcpCreateRefreshToken from './store/mcp/createRefreshToken.js'; +import mcpGetRefreshToken from './store/mcp/getRefreshToken.js'; +import mcpRevokeRefreshToken from './store/mcp/revokeRefreshToken.js'; export default class LibDashStore { /** @@ -619,6 +629,19 @@ export default class LibDashStore { setOptionsByArticleSku: optionslistSetOptions(provider), }; + this.mcp = { + registerClient: mcpRegisterClient(provider), + getClient: mcpGetClient(provider), + listClients: mcpListClients(provider), + deleteClient: mcpDeleteClient(provider), + createAuthCode: mcpCreateAuthCode(provider), + getAuthCode: mcpGetAuthCode(provider), + consumeAuthCode: mcpConsumeAuthCode(provider), + createRefreshToken: mcpCreateRefreshToken(provider), + getRefreshToken: mcpGetRefreshToken(provider), + revokeRefreshToken: mcpRevokeRefreshToken(provider), + }; + this.purge = storePurge(provider); this.initialize = storeInitialize(provider); diff --git a/src/providers/mssql/queries/mcp.js b/src/providers/mssql/queries/mcp.js new file mode 100644 index 0000000..9bf3abb --- /dev/null +++ b/src/providers/mssql/queries/mcp.js @@ -0,0 +1,90 @@ +export const INSERT_CLIENT = ` +INSERT INTO mcp_client (client_id, client_secret, client_name, redirect_uris, grant_types, response_types, scope) +OUTPUT INSERTED.id, INSERTED.client_id, INSERTED.client_secret, INSERTED.client_name, INSERTED.redirect_uris, INSERTED.grant_types, INSERTED.response_types, INSERTED.scope, INSERTED.created_at +VALUES (@clientId, @clientSecret, @clientName, @redirectUris, @grantTypes, @responseTypes, @scope) +`; + +export const GET_CLIENT = ` +SELECT + id, + client_id AS clientId, + client_secret AS clientSecret, + client_name AS clientName, + redirect_uris AS redirectUris, + grant_types AS grantTypes, + response_types AS responseTypes, + scope, + created_at AS createdAt +FROM mcp_client +WHERE client_id = @clientId +`; + +export const LIST_CLIENTS = ` +SELECT + id, + client_id AS clientId, + client_name AS clientName, + redirect_uris AS redirectUris, + grant_types AS grantTypes, + response_types AS responseTypes, + scope, + created_at AS createdAt +FROM mcp_client +ORDER BY created_at DESC +`; + +export const DELETE_CLIENT = ` +DELETE FROM mcp_client +WHERE client_id = @clientId +`; + +export const INSERT_AUTH_CODE = ` +INSERT INTO mcp_auth_code (code, client_id, google_auth_id, redirect_uri, code_challenge, code_challenge_method, scope, expires_at) +VALUES (@code, @clientId, @googleAuthId, @redirectUri, @codeChallenge, @codeChallengeMethod, @scope, @expiresAt) +`; + +export const GET_AUTH_CODE = ` +SELECT + code, + client_id AS clientId, + google_auth_id AS googleAuthId, + redirect_uri AS redirectUri, + code_challenge AS codeChallenge, + code_challenge_method AS codeChallengeMethod, + scope, + expires_at AS expiresAt, + used +FROM mcp_auth_code +WHERE code = @code +`; + +export const CONSUME_AUTH_CODE = ` +UPDATE mcp_auth_code +SET used = 1 +WHERE code = @code +`; + +export const INSERT_REFRESH_TOKEN = ` +INSERT INTO mcp_token (refresh_token, client_id, google_auth_id, scope, expires_at) +OUTPUT INSERTED.id +VALUES (@refreshToken, @clientId, @googleAuthId, @scope, @expiresAt) +`; + +export const GET_REFRESH_TOKEN = ` +SELECT + id, + refresh_token AS refreshToken, + client_id AS clientId, + google_auth_id AS googleAuthId, + scope, + expires_at AS expiresAt, + revoked +FROM mcp_token +WHERE refresh_token = @refreshToken +`; + +export const REVOKE_REFRESH_TOKEN = ` +UPDATE mcp_token +SET revoked = 1 +WHERE refresh_token = @refreshToken +`; diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index bb923d2..5bfd728 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -1177,6 +1177,61 @@ BEGIN END `; +export const INIT_MCP_CLIENT = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'mcp_client') +BEGIN + CREATE TABLE mcp_client ( + id INT IDENTITY(1,1) PRIMARY KEY, + client_id NVARCHAR(100) NOT NULL, + client_secret NVARCHAR(255) NULL, + client_name NVARCHAR(255) NOT NULL, + redirect_uris NVARCHAR(MAX) NOT NULL, + grant_types NVARCHAR(255) NULL, + response_types NVARCHAR(255) NULL, + scope NVARCHAR(255) NULL, + created_at DATETIME2 NOT NULL CONSTRAINT DF_mcp_client_created_at DEFAULT SYSUTCDATETIME(), + CONSTRAINT UQ_mcp_client_client_id UNIQUE (client_id) + ); +END +`; + +export const INIT_MCP_AUTH_CODE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'mcp_auth_code') +BEGIN + CREATE TABLE mcp_auth_code ( + code NVARCHAR(128) PRIMARY KEY, + client_id NVARCHAR(100) NOT NULL, + google_auth_id INT NOT NULL, + redirect_uri NVARCHAR(2048) NOT NULL, + code_challenge NVARCHAR(255) NOT NULL, + code_challenge_method NVARCHAR(32) NOT NULL, + scope NVARCHAR(255) NULL, + expires_at DATETIME2 NOT NULL, + used TINYINT NOT NULL CONSTRAINT DF_mcp_auth_code_used DEFAULT 0, + created_at DATETIME2 NOT NULL CONSTRAINT DF_mcp_auth_code_created_at DEFAULT SYSUTCDATETIME(), + CONSTRAINT FK_mcp_auth_code_google_auth FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE + ); +END +`; + +export const INIT_MCP_TOKEN = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'mcp_token') +BEGIN + CREATE TABLE mcp_token ( + id INT IDENTITY(1,1) PRIMARY KEY, + refresh_token NVARCHAR(128) NOT NULL, + client_id NVARCHAR(100) NOT NULL, + google_auth_id INT NOT NULL, + scope NVARCHAR(255) NULL, + expires_at DATETIME2 NOT NULL, + revoked TINYINT NOT NULL CONSTRAINT DF_mcp_token_revoked DEFAULT 0, + created_at DATETIME2 NOT NULL CONSTRAINT DF_mcp_token_created_at DEFAULT SYSUTCDATETIME(), + CONSTRAINT UQ_mcp_token_refresh UNIQUE (refresh_token), + CONSTRAINT FK_mcp_token_google_auth FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE + ); +END +`; + /** DDL scripts in dependency order for schema init. */ export const SCHEMA_SCRIPTS = [ INIT_META, @@ -1244,4 +1299,7 @@ export const SCHEMA_SCRIPTS = [ INIT_PAYMENT, INIT_SHIPPING, INIT_PACKAGE, + INIT_MCP_CLIENT, + INIT_MCP_AUTH_CODE, + INIT_MCP_TOKEN, ]; diff --git a/src/providers/sqlite3/index.js b/src/providers/sqlite3/index.js index 994293b..43a659f 100644 --- a/src/providers/sqlite3/index.js +++ b/src/providers/sqlite3/index.js @@ -19,13 +19,23 @@ function normalizeParamValue(value) { return value; } -function bindParams(params) { - return Object.fromEntries( +function bindParams(params, sql = null) { + const bound = Object.fromEntries( Object.entries(params).map(([key, value]) => [ key.replace(/^[@:$]/, ''), normalizeParamValue(value), ]), ); + if (typeof sql === 'string') { + const matches = sql.matchAll(/[@:$]([a-zA-Z0-9_]+)/g); + for (const match of matches) { + const name = match[1]; + if (!(name in bound)) { + bound[name] = null; + } + } + } + return bound; } function normalizeInsertId(value) { @@ -227,7 +237,7 @@ export class BetterSqlite3Executor { /** @returns {Promise} */ async query(sql, params = {}) { const statement = this.db.prepare(sql); - const bindings = bindParams(params); + const bindings = bindParams(params, sql); if (statement.reader) { const rows = statement.all(bindings); diff --git a/src/providers/sqlite3/queries/mcp.js b/src/providers/sqlite3/queries/mcp.js new file mode 100644 index 0000000..5b3fb69 --- /dev/null +++ b/src/providers/sqlite3/queries/mcp.js @@ -0,0 +1,16 @@ +import * as source from '../../mssql/queries/mcp.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'mcp'); +export const { + INSERT_CLIENT, + GET_CLIENT, + LIST_CLIENTS, + DELETE_CLIENT, + INSERT_AUTH_CODE, + GET_AUTH_CODE, + CONSUME_AUTH_CODE, + INSERT_REFRESH_TOKEN, + GET_REFRESH_TOKEN, + REVOKE_REFRESH_TOKEN, +} = queries; diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index 17c8a93..f01f912 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -845,6 +845,50 @@ CREATE TABLE IF NOT EXISTS package ( ); `; +export const INIT_MCP_CLIENT = ` +CREATE TABLE IF NOT EXISTS mcp_client ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id TEXT NOT NULL UNIQUE, + client_secret TEXT, + client_name TEXT NOT NULL, + redirect_uris TEXT NOT NULL, + grant_types TEXT, + response_types TEXT, + scope TEXT, + created_at TEXT NOT NULL DEFAULT (${UTC_NOW}) +); +`; + +export const INIT_MCP_AUTH_CODE = ` +CREATE TABLE IF NOT EXISTS mcp_auth_code ( + code TEXT PRIMARY KEY, + client_id TEXT NOT NULL, + google_auth_id INTEGER NOT NULL, + redirect_uri TEXT NOT NULL, + code_challenge TEXT NOT NULL, + code_challenge_method TEXT NOT NULL, + scope TEXT, + expires_at TEXT NOT NULL, + used INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE +); +`; + +export const INIT_MCP_TOKEN = ` +CREATE TABLE IF NOT EXISTS mcp_token ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + refresh_token TEXT NOT NULL UNIQUE, + client_id TEXT NOT NULL, + google_auth_id INTEGER NOT NULL, + scope TEXT, + expires_at TEXT NOT NULL, + revoked INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE +); +`; + /** DDL scripts in dependency order for SQLite schema initialization. */ export const SCHEMA_SCRIPTS = [ INIT_META, @@ -909,4 +953,7 @@ export const SCHEMA_SCRIPTS = [ INIT_PAYMENT, INIT_SHIPPING, INIT_PACKAGE, + INIT_MCP_CLIENT, + INIT_MCP_AUTH_CODE, + INIT_MCP_TOKEN, ]; diff --git a/src/store/mcp/consumeAuthCode.js b/src/store/mcp/consumeAuthCode.js new file mode 100644 index 0000000..8d096b7 --- /dev/null +++ b/src/store/mcp/consumeAuthCode.js @@ -0,0 +1,16 @@ +import { CONSUME_AUTH_CODE as MSSQL_CONSUME } from '../../providers/mssql/queries/mcp.js'; +import { CONSUME_AUTH_CODE as SQLITE_CONSUME } from '../../providers/sqlite3/queries/mcp.js'; + +export default function consumeAuthCode(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_CONSUME : MSSQL_CONSUME; + /** + * Marks an authorization code as used. + * + * @param {string} code - The authorization code to mark used. + * @returns {Promise} True once marked. + */ + return async (code) => { + await provider.query(sql, { code }); + return true; + }; +} diff --git a/src/store/mcp/createAuthCode.js b/src/store/mcp/createAuthCode.js new file mode 100644 index 0000000..702c921 --- /dev/null +++ b/src/store/mcp/createAuthCode.js @@ -0,0 +1,43 @@ +import { INSERT_AUTH_CODE as MSSQL_INSERT } from '../../providers/mssql/queries/mcp.js'; +import { INSERT_AUTH_CODE as SQLITE_INSERT } from '../../providers/sqlite3/queries/mcp.js'; + +export default function createAuthCode(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_INSERT : MSSQL_INSERT; + /** + * Stores an OAuth 2.1 authorization code with PKCE parameters. + * + * @param {object} input + * @param {string} input.code - The generated authorization code. + * @param {string} input.clientId - The client ID. + * @param {number} input.googleAuthId - The ID of the authenticated user. + * @param {string} input.redirectUri - The redirect URI used in the authorize request. + * @param {string} input.codeChallenge - The PKCE code challenge. + * @param {string} input.codeChallengeMethod - The PKCE method (e.g. S256). + * @param {string|null} [input.scope] - Granted scope. + * @param {string|Date} input.expiresAt - Expiration ISO string or Date. + * @returns {Promise} The authorization code. + */ + return async ({ + code, + clientId, + googleAuthId, + redirectUri, + codeChallenge, + codeChallengeMethod = 'S256', + scope = 'mcp:tools', + expiresAt, + }) => { + const exp = expiresAt instanceof Date ? expiresAt.toISOString() : expiresAt; + await provider.query(sql, { + code, + clientId, + googleAuthId, + redirectUri, + codeChallenge, + codeChallengeMethod, + scope, + expiresAt: exp, + }); + return code; + }; +} diff --git a/src/store/mcp/createRefreshToken.js b/src/store/mcp/createRefreshToken.js new file mode 100644 index 0000000..7d0db7c --- /dev/null +++ b/src/store/mcp/createRefreshToken.js @@ -0,0 +1,34 @@ +import { INSERT_REFRESH_TOKEN as MSSQL_INSERT } from '../../providers/mssql/queries/mcp.js'; +import { INSERT_REFRESH_TOKEN as SQLITE_INSERT } from '../../providers/sqlite3/queries/mcp.js'; + +export default function createRefreshToken(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_INSERT : MSSQL_INSERT; + /** + * Stores an OAuth 2.1 refresh token. + * + * @param {object} input + * @param {string} input.refreshToken - The refresh token string. + * @param {string} input.clientId - The client ID. + * @param {number} input.googleAuthId - The user ID. + * @param {string|null} [input.scope] - Scopes granted. + * @param {string|Date} input.expiresAt - Expiration Date or ISO string. + * @returns {Promise} Created token record or id. + */ + return async ({ + refreshToken, + clientId, + googleAuthId, + scope = 'mcp:tools', + expiresAt, + }) => { + const exp = expiresAt instanceof Date ? expiresAt.toISOString() : expiresAt; + const result = await provider.query(sql, { + refreshToken, + clientId, + googleAuthId, + scope, + expiresAt: exp, + }); + return result.rows[0]?.id ?? result.insertId; + }; +} diff --git a/src/store/mcp/deleteClient.js b/src/store/mcp/deleteClient.js new file mode 100644 index 0000000..a8a6e4d --- /dev/null +++ b/src/store/mcp/deleteClient.js @@ -0,0 +1,16 @@ +import { DELETE_CLIENT as MSSQL_DELETE } from '../../providers/mssql/queries/mcp.js'; +import { DELETE_CLIENT as SQLITE_DELETE } from '../../providers/sqlite3/queries/mcp.js'; + +export default function deleteClient(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE; + /** + * Deletes a registered MCP client by client ID. + * + * @param {string} clientId - The client identifier to delete. + * @returns {Promise} True if deleted. + */ + return async (clientId) => { + await provider.query(sql, { clientId }); + return true; + }; +} diff --git a/src/store/mcp/getAuthCode.js b/src/store/mcp/getAuthCode.js new file mode 100644 index 0000000..d77c33b --- /dev/null +++ b/src/store/mcp/getAuthCode.js @@ -0,0 +1,16 @@ +import { GET_AUTH_CODE as MSSQL_GET } from '../../providers/mssql/queries/mcp.js'; +import { GET_AUTH_CODE as SQLITE_GET } from '../../providers/sqlite3/queries/mcp.js'; + +export default function getAuthCode(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET; + /** + * Retrieves an authorization code record. + * + * @param {string} code - The authorization code. + * @returns {Promise} The auth code record or null. + */ + return async (code) => { + const result = await provider.query(sql, { code }); + return result.rows[0] ?? null; + }; +} diff --git a/src/store/mcp/getClient.js b/src/store/mcp/getClient.js new file mode 100644 index 0000000..e7beb3d --- /dev/null +++ b/src/store/mcp/getClient.js @@ -0,0 +1,16 @@ +import { GET_CLIENT as MSSQL_GET } from '../../providers/mssql/queries/mcp.js'; +import { GET_CLIENT as SQLITE_GET } from '../../providers/sqlite3/queries/mcp.js'; + +export default function getClient(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET; + /** + * Retrieves an MCP client by client ID. + * + * @param {string} clientId - The client identifier. + * @returns {Promise} The client record or null if not found. + */ + return async (clientId) => { + const result = await provider.query(sql, { clientId }); + return result.rows[0] ?? null; + }; +} diff --git a/src/store/mcp/getRefreshToken.js b/src/store/mcp/getRefreshToken.js new file mode 100644 index 0000000..d3f49ea --- /dev/null +++ b/src/store/mcp/getRefreshToken.js @@ -0,0 +1,16 @@ +import { GET_REFRESH_TOKEN as MSSQL_GET } from '../../providers/mssql/queries/mcp.js'; +import { GET_REFRESH_TOKEN as SQLITE_GET } from '../../providers/sqlite3/queries/mcp.js'; + +export default function getRefreshToken(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET; + /** + * Retrieves an OAuth 2.1 refresh token. + * + * @param {string} refreshToken - The refresh token. + * @returns {Promise} The refresh token record or null. + */ + return async (refreshToken) => { + const result = await provider.query(sql, { refreshToken }); + return result.rows[0] ?? null; + }; +} diff --git a/src/store/mcp/listClients.js b/src/store/mcp/listClients.js new file mode 100644 index 0000000..5ca47a9 --- /dev/null +++ b/src/store/mcp/listClients.js @@ -0,0 +1,15 @@ +import { LIST_CLIENTS as MSSQL_LIST } from '../../providers/mssql/queries/mcp.js'; +import { LIST_CLIENTS as SQLITE_LIST } from '../../providers/sqlite3/queries/mcp.js'; + +export default function listClients(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST; + /** + * Lists all registered MCP clients. + * + * @returns {Promise>} List of client records. + */ + return async () => { + const result = await provider.query(sql); + return result.rows ?? []; + }; +} diff --git a/src/store/mcp/registerClient.js b/src/store/mcp/registerClient.js new file mode 100644 index 0000000..7b2c94d --- /dev/null +++ b/src/store/mcp/registerClient.js @@ -0,0 +1,55 @@ +import { INSERT_CLIENT as MSSQL_INSERT } from '../../providers/mssql/queries/mcp.js'; +import { INSERT_CLIENT as SQLITE_INSERT } from '../../providers/sqlite3/queries/mcp.js'; + +export default function registerClient(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_INSERT : MSSQL_INSERT; + /** + * Registers a new OAuth MCP client. + * + * @param {object} input + * @param {string} input.clientId - Unique client identifier. + * @param {string|null} [input.clientSecret] - Optional client secret for confidential clients. + * @param {string} input.clientName - Human-readable client name. + * @param {string|Array} input.redirectUris - JSON string or array of redirect URIs. + * @param {string|Array} [input.grantTypes] - Supported grant types. + * @param {string|Array} [input.responseTypes] - Supported response types. + * @param {string|null} [input.scope] - Allowed scope. + * @returns {Promise} The created client record. + */ + return async ({ + clientId, + clientSecret = null, + clientName, + redirectUris, + grantTypes = 'authorization_code,refresh_token', + responseTypes = 'code', + scope = 'mcp:tools', + }) => { + const urisStr = Array.isArray(redirectUris) ? JSON.stringify(redirectUris) : redirectUris; + const grantsStr = Array.isArray(grantTypes) ? grantTypes.join(',') : grantTypes; + const resTypesStr = Array.isArray(responseTypes) ? responseTypes.join(',') : responseTypes; + + const result = await provider.query(sql, { + clientId, + clientSecret, + clientName, + redirectUris: urisStr, + grantTypes: grantsStr, + responseTypes: resTypesStr, + scope, + }); + + const row = result.rows[0]; + return { + id: row?.id ?? result.insertId, + clientId: row?.clientId ?? row?.client_id ?? clientId, + clientSecret: row?.clientSecret ?? row?.client_secret ?? clientSecret, + clientName: row?.clientName ?? row?.client_name ?? clientName, + redirectUris: row?.redirectUris ?? row?.redirect_uris ?? urisStr, + grantTypes: row?.grantTypes ?? row?.grant_types ?? grantsStr, + responseTypes: row?.responseTypes ?? row?.response_types ?? resTypesStr, + scope: row?.scope ?? scope, + createdAt: row?.createdAt ?? row?.created_at, + }; + }; +} diff --git a/src/store/mcp/revokeRefreshToken.js b/src/store/mcp/revokeRefreshToken.js new file mode 100644 index 0000000..9954001 --- /dev/null +++ b/src/store/mcp/revokeRefreshToken.js @@ -0,0 +1,16 @@ +import { REVOKE_REFRESH_TOKEN as MSSQL_REVOKE } from '../../providers/mssql/queries/mcp.js'; +import { REVOKE_REFRESH_TOKEN as SQLITE_REVOKE } from '../../providers/sqlite3/queries/mcp.js'; + +export default function revokeRefreshToken(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_REVOKE : MSSQL_REVOKE; + /** + * Revokes an OAuth 2.1 refresh token. + * + * @param {string} refreshToken - The refresh token to revoke. + * @returns {Promise} True once revoked. + */ + return async (refreshToken) => { + await provider.query(sql, { refreshToken }); + return true; + }; +} diff --git a/src/store/numbers/peekNumber.js b/src/store/numbers/peekNumber.js index c0a76f8..c936ca5 100644 --- a/src/store/numbers/peekNumber.js +++ b/src/store/numbers/peekNumber.js @@ -34,7 +34,8 @@ export default function peekNumber(provider) { */ return async (companyId, type, tx) => { const doPeek = async (runner) => { - await runner.query(ensureSql, { companyId, type }); + const prefix = type === 'shipment' ? '-L-' : ''; + await runner.query(ensureSql, { companyId, type, prefix }); const res = await runner.query(getSql, { companyId, type }); const row = res.rows[0]; if (!row) {