@softure-ai/mcp-access 0.2.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +241 -11
  3. package/dist/cli/run.d.ts +1 -1
  4. package/dist/cli/run.d.ts.map +1 -1
  5. package/dist/cli/run.js +2 -2
  6. package/dist/cli/run.js.map +1 -1
  7. package/dist/contract.d.ts +48 -2
  8. package/dist/contract.d.ts.map +1 -1
  9. package/dist/index.d.ts +88 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +19 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/jobs/index.d.ts +2 -0
  14. package/dist/jobs/index.d.ts.map +1 -1
  15. package/dist/jobs/index.js +5 -2
  16. package/dist/jobs/index.js.map +1 -1
  17. package/dist/jobs/operation-state.d.ts +58 -0
  18. package/dist/jobs/operation-state.d.ts.map +1 -0
  19. package/dist/jobs/operation-state.js +68 -0
  20. package/dist/jobs/operation-state.js.map +1 -0
  21. package/dist/jobs/operation-tasks.d.ts +68 -0
  22. package/dist/jobs/operation-tasks.d.ts.map +1 -0
  23. package/dist/jobs/operation-tasks.js +163 -0
  24. package/dist/jobs/operation-tasks.js.map +1 -0
  25. package/dist/jobs/operations.d.ts +89 -0
  26. package/dist/jobs/operations.d.ts.map +1 -0
  27. package/dist/jobs/operations.js +131 -0
  28. package/dist/jobs/operations.js.map +1 -0
  29. package/dist/messages/en.d.ts +55 -0
  30. package/dist/messages/en.d.ts.map +1 -1
  31. package/dist/messages/en.js +55 -0
  32. package/dist/messages/en.js.map +1 -1
  33. package/dist/messages/index.d.ts +54 -0
  34. package/dist/messages/index.d.ts.map +1 -1
  35. package/dist/next/issue-token.d.ts +11 -2
  36. package/dist/next/issue-token.d.ts.map +1 -1
  37. package/dist/next/issue-token.js +56 -7
  38. package/dist/next/issue-token.js.map +1 -1
  39. package/dist/next/token-manager-props.d.ts +3 -2
  40. package/dist/next/token-manager-props.d.ts.map +1 -1
  41. package/dist/next/token-manager-props.js +12 -2
  42. package/dist/next/token-manager-props.js.map +1 -1
  43. package/dist/options.d.ts +47 -0
  44. package/dist/options.d.ts.map +1 -1
  45. package/dist/options.js +53 -0
  46. package/dist/options.js.map +1 -1
  47. package/dist/schema.d.ts +214 -0
  48. package/dist/schema.d.ts.map +1 -1
  49. package/dist/schema.js +21 -3
  50. package/dist/schema.js.map +1 -1
  51. package/dist/server/api-tokens.d.ts +64 -0
  52. package/dist/server/api-tokens.d.ts.map +1 -0
  53. package/dist/server/api-tokens.js +176 -0
  54. package/dist/server/api-tokens.js.map +1 -0
  55. package/dist/server/confirmation.d.ts +69 -0
  56. package/dist/server/confirmation.d.ts.map +1 -0
  57. package/dist/server/confirmation.js +220 -0
  58. package/dist/server/confirmation.js.map +1 -0
  59. package/dist/server/health.d.ts.map +1 -1
  60. package/dist/server/health.js +3 -1
  61. package/dist/server/health.js.map +1 -1
  62. package/dist/server/index.d.ts +4 -2
  63. package/dist/server/index.d.ts.map +1 -1
  64. package/dist/server/index.js +3 -1
  65. package/dist/server/index.js.map +1 -1
  66. package/dist/server/privacy.d.ts +4 -0
  67. package/dist/server/privacy.d.ts.map +1 -1
  68. package/dist/server/privacy.js +5 -1
  69. package/dist/server/privacy.js.map +1 -1
  70. package/dist/server/prune.d.ts +5 -1
  71. package/dist/server/prune.d.ts.map +1 -1
  72. package/dist/server/prune.js +14 -4
  73. package/dist/server/prune.js.map +1 -1
  74. package/dist/server/strict-input.d.ts +3 -1
  75. package/dist/server/strict-input.d.ts.map +1 -1
  76. package/dist/server/strict-input.js +54 -8
  77. package/dist/server/strict-input.js.map +1 -1
  78. package/dist/server/tokens.d.ts +46 -11
  79. package/dist/server/tokens.d.ts.map +1 -1
  80. package/dist/server/tokens.js +46 -24
  81. package/dist/server/tokens.js.map +1 -1
  82. package/dist/server/tools.d.ts +8 -0
  83. package/dist/server/tools.d.ts.map +1 -1
  84. package/dist/server/tools.js +1 -0
  85. package/dist/server/tools.js.map +1 -1
  86. package/dist/tokens/index.d.ts +6 -0
  87. package/dist/tokens/index.d.ts.map +1 -0
  88. package/dist/tokens/index.js +8 -0
  89. package/dist/tokens/index.js.map +1 -0
  90. package/dist/ui/index.d.ts +1 -1
  91. package/dist/ui/index.d.ts.map +1 -1
  92. package/dist/ui/index.js.map +1 -1
  93. package/dist/ui/token-manager.d.ts +18 -2
  94. package/dist/ui/token-manager.d.ts.map +1 -1
  95. package/dist/ui/token-manager.js +28 -3
  96. package/dist/ui/token-manager.js.map +1 -1
  97. package/migrations/0004_create_used_confirmations.sql +13 -0
  98. package/migrations/0005_add_api_tokens.sql +15 -0
  99. package/migrations/0006_create_operation_tasks.sql +18 -0
  100. package/module.json +9 -3
  101. package/package.json +7 -2
  102. package/src/cli/run.ts +2 -2
  103. package/src/contract.ts +60 -2
  104. package/src/index.ts +31 -3
  105. package/src/jobs/index.ts +43 -2
  106. package/src/jobs/operation-state.ts +119 -0
  107. package/src/jobs/operation-tasks.ts +234 -0
  108. package/src/jobs/operations.ts +258 -0
  109. package/src/messages/en.ts +56 -0
  110. package/src/next/issue-token.ts +64 -9
  111. package/src/next/token-manager-props.ts +15 -4
  112. package/src/options.ts +62 -0
  113. package/src/schema.ts +23 -3
  114. package/src/server/api-tokens.ts +224 -0
  115. package/src/server/confirmation.ts +316 -0
  116. package/src/server/health.ts +3 -1
  117. package/src/server/index.ts +32 -2
  118. package/src/server/privacy.ts +9 -1
  119. package/src/server/prune.ts +17 -4
  120. package/src/server/strict-input.ts +62 -13
  121. package/src/server/tokens.ts +73 -27
  122. package/src/server/tools.ts +9 -0
  123. package/src/tokens/index.ts +22 -0
  124. package/src/ui/index.ts +1 -0
  125. package/src/ui/token-manager.tsx +140 -3
package/dist/schema.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // Drizzle view of the module's tables (migrations/0001_create_access_tokens.sql,
2
- // 0002_create_oauth_grants.sql and 0003_create_oauth_registration_window.sql). The migrations are the
3
- // source of truth; this file only types the queries.
2
+ // 0002_create_oauth_grants.sql, 0003_create_oauth_registration_window.sql, 0004_create_used_confirmations.sql,
3
+ // 0005_add_api_tokens.sql and 0006_create_operation_tasks.sql). The migrations are the source of truth; this file only types the queries.
4
4
  import { users } from "@softure-ai/auth";
5
- import { boolean, jsonb, pgSchema, smallint, text, timestamp, uuid } from "drizzle-orm/pg-core";
5
+ import { bigint, boolean, jsonb, pgSchema, smallint, text, timestamp, uuid } from "drizzle-orm/pg-core";
6
6
  export const mcpSchema = pgSchema("mcp");
7
7
  export const oauthClients = mcpSchema.table("oauth_clients", {
8
8
  id: uuid("id").primaryKey().defaultRandom(),
@@ -56,10 +56,28 @@ export const accessTokens = mcpSchema.table("access_tokens", {
56
56
  expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
57
57
  lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
58
58
  grantId: uuid("grant_id").references(() => oauthGrants.id, { onDelete: "cascade" }),
59
+ /** `mcp` opens the MCP endpoint; `api` passes `requireApiToken` with its `scopes` (migration 0005). */
60
+ kind: text("kind").$type().notNull().default("mcp"),
61
+ scopes: text("scopes").array().$type().notNull().default([]),
59
62
  });
60
63
  export const oauthRegistrationWindow = mcpSchema.table("oauth_registration_window", {
61
64
  id: smallint("id").primaryKey().default(1),
62
65
  openedAt: timestamp("opened_at", { withTimezone: true }).notNull(),
63
66
  openUntil: timestamp("open_until", { withTimezone: true }).notNull(),
64
67
  });
68
+ export const usedConfirmations = mcpSchema.table("used_confirmations", {
69
+ tokenHash: text("token_hash").primaryKey(),
70
+ usedAt: timestamp("used_at", { withTimezone: true }).notNull(),
71
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
72
+ });
73
+ /** MCP Tasks over long operations (#977): a random task id per account and job, until `expiresAt`. */
74
+ export const operationTasks = mcpSchema.table("operation_tasks", {
75
+ taskId: text("task_id").primaryKey(),
76
+ userId: uuid("user_id")
77
+ .notNull()
78
+ .references(() => users.id, { onDelete: "cascade" }),
79
+ jobId: bigint("job_id", { mode: "bigint" }).notNull(),
80
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
81
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
82
+ });
65
83
  //# sourceMappingURL=schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ import { type Err, type Ok } from "@softure-ai/core";
2
+ import type { ApiTokenIdentity, ApiTokenRefusal } from "../contract.js";
3
+ import { type ApiTokenOptions } from "../options.js";
4
+ import { type McpAccessContext } from "./tokens.js";
5
+ /** The bucket counted per client address before the token is looked up. */
6
+ export declare const API_RATE_LIMIT_BUCKET = "api";
7
+ /** The bucket counted per verified token. */
8
+ export declare const API_TOKEN_RATE_LIMIT_BUCKET = "api-token";
9
+ /** The app's API token settings, or null while `apiTokens` is unset. */
10
+ export declare function getApiTokenOptions(ctx: Pick<McpAccessContext, "config">): ApiTokenOptions | null;
11
+ /** Whether a presented value has the shape of one of the app's API tokens: its prefix and 32 base64url bytes. */
12
+ export declare function isApiTokenShape(options: ApiTokenOptions, token: string): boolean;
13
+ export interface IssueApiTokenInput {
14
+ /** The auth user id of the owner, from the session. */
15
+ readonly userId: string;
16
+ readonly name: string;
17
+ /** Scope names from the form: any values; unknown ones are refused. */
18
+ readonly scopes: readonly string[];
19
+ }
20
+ export interface IssuedApiToken {
21
+ readonly id: string;
22
+ /** The plaintext: shown to the owner once, stored nowhere. */
23
+ readonly token: string;
24
+ readonly name: string;
25
+ /** The scopes granted, in the order the app declares them. */
26
+ readonly scopes: readonly string[];
27
+ readonly expiresAt: Date;
28
+ }
29
+ export type IssueApiTokenResult = Ok<IssuedApiToken> | Err<"mcp-access.name_required" | "mcp-access.name_too_long" | "mcp-access.token_limit_reached" | "mcp-access.api_disabled" | "mcp-access.scope_required" | "mcp-access.scope_unknown">;
30
+ /**
31
+ * Issues an API token with the asked scopes. A scope the app does not declare is refused; a write
32
+ * scope is granted only while `allowWrites` is on (left out otherwise), and a token needs at least
33
+ * one scope left. Shares the per-account limit and lock with MCP tokens. Database failures propagate.
34
+ */
35
+ export declare function issueApiToken(ctx: McpAccessContext, input: IssueApiTokenInput): Promise<IssueApiTokenResult>;
36
+ /** The identity behind a valid API token. */
37
+ export interface VerifiedApiToken extends ApiTokenIdentity {
38
+ readonly expiresAt: Date;
39
+ }
40
+ /**
41
+ * The identity behind an API token, or null for anything unknown, revoked, expired, an MCP token or
42
+ * any value without the app's prefix. Write scopes count only while writing is enabled (`allowWrites`
43
+ * and the `mcp-access.writes_enabled` switch). Records the use like an MCP token. Database failures propagate.
44
+ */
45
+ export declare function verifyApiToken(ctx: McpAccessContext, token: string): Promise<VerifiedApiToken | null>;
46
+ export interface RequireApiTokenOptions {
47
+ /** Every scope the route needs; each must be one the app declares in `apiTokens.scopes`. */
48
+ readonly scopes: readonly string[];
49
+ }
50
+ /**
51
+ * Checks a route's request for a valid API token with every scope in `scopes`, in this order:
52
+ * the client is identified, its request counted per address (before any lookup, so a flood of
53
+ * guesses costs nothing), the `Authorization: Bearer` token verified, the request counted per token,
54
+ * the scopes checked. Expected refusals are values, for `apiTokenErrorResponse`; a failed count or
55
+ * lookup is logged and answered `mcp-access.api_unavailable`. A scope the app does not declare, or a
56
+ * missing bucket, is a setup bug and throws.
57
+ */
58
+ export declare function requireApiToken(ctx: McpAccessContext, request: Request, { scopes: required }: RequireApiTokenOptions): Promise<Ok<ApiTokenIdentity> | ApiTokenRefusal>;
59
+ /**
60
+ * The HTTP answer to a refusal of `requireApiToken`: JSON `{ error }` (the code without its module), with
61
+ * `WWW-Authenticate` on 401 and 403 (RFC 6750 §3), `Retry-After` on 429, and never cached.
62
+ */
63
+ export declare function apiTokenErrorResponse(refusal: ApiTokenRefusal): Response;
64
+ //# sourceMappingURL=api-tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-tokens.d.ts","sourceRoot":"","sources":["../../src/server/api-tokens.ts"],"names":[],"mappings":"AAKA,OAAO,EAAqC,KAAK,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAGxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EAA8B,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAEjF,OAAO,EAAgD,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGlG,2EAA2E;AAC3E,eAAO,MAAM,qBAAqB,QAAQ,CAAC;AAC3C,6CAA6C;AAC7C,eAAO,MAAM,2BAA2B,cAAc,CAAC;AAKvD,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,GAAG,eAAe,GAAG,IAAI,CAEhG;AAED,iHAAiH;AACjH,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAGhF;AAWD,MAAM,WAAW,kBAAkB;IACjC,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;CAC1B;AAED,MAAM,MAAM,mBAAmB,GAC3B,EAAE,CAAC,cAAc,CAAC,GAClB,GAAG,CACC,0BAA0B,GAC1B,0BAA0B,GAC1B,gCAAgC,GAChC,yBAAyB,GACzB,2BAA2B,GAC3B,0BAA0B,CAC7B,CAAC;AAEN;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAiBlH;AAED,6CAA6C;AAC7C,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;IACxD,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAE3G;AAiBD,MAAM,WAAW,sBAAsB;IACrC,4FAA4F;IAC5F,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE,OAAO,EAChB,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,sBAAsB,GAC3C,OAAO,CAAC,EAAE,CAAC,gBAAgB,CAAC,GAAG,eAAe,CAAC,CAmCjD;AAYD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,QAAQ,CAaxE"}
@@ -0,0 +1,176 @@
1
+ // Personal tokens for an app's plain HTTP routes (#961): a phone Shortcut, an automation tool or a
2
+ // script sends `Authorization: Bearer <prefix>…` and the route calls `requireApiToken`. They share the
3
+ // table, hashing, expiry, limit, privacy and prune with the MCP tokens (`tokens.ts`); the `kind`
4
+ // column keeps the two doors apart. Nothing here imports the MCP SDK: `@softure-ai/mcp-access/tokens`
5
+ // is for an app that has no MCP server.
6
+ import { err, errorLogLabel, getModule, ok } from "@softure-ai/core";
7
+ import { randomBytes } from "node:crypto";
8
+ import { consumeRateLimit, identifyClient } from "@softure-ai/security/server";
9
+ import { MAX_PRESENTED_TOKEN_LENGTH } from "../options.js";
10
+ import { getMcpAccessOptions } from "./options.js";
11
+ import { findValidToken, hashAccessToken, insertToken } from "./tokens.js";
12
+ import { isWritingEnabled } from "./writes-switch.js";
13
+ /** The bucket counted per client address before the token is looked up. */
14
+ export const API_RATE_LIMIT_BUCKET = "api";
15
+ /** The bucket counted per verified token. */
16
+ export const API_TOKEN_RATE_LIMIT_BUCKET = "api-token";
17
+ const BEARER_PREFIX = /^Bearer[ \t]+/i;
18
+ const NO_STORE = "no-store";
19
+ /** The app's API token settings, or null while `apiTokens` is unset. */
20
+ export function getApiTokenOptions(ctx) {
21
+ return getMcpAccessOptions(ctx.config).apiTokens ?? null;
22
+ }
23
+ /** Whether a presented value has the shape of one of the app's API tokens: its prefix and 32 base64url bytes. */
24
+ export function isApiTokenShape(options, token) {
25
+ if (token.length > MAX_PRESENTED_TOKEN_LENGTH || !token.startsWith(options.prefix))
26
+ return false;
27
+ return /^[A-Za-z0-9_-]{43}$/.test(token.slice(options.prefix.length));
28
+ }
29
+ /**
30
+ * A new API token: the app's prefix and 32 random bytes. The stored sha256 covers the random part
31
+ * alone (the secret); the prefix is a label that the shape check requires before any lookup.
32
+ */
33
+ function createApiSecret(prefix) {
34
+ const random = randomBytes(32).toString("base64url");
35
+ return { secret: `${prefix}${random}`, secretHash: hashAccessToken(random) };
36
+ }
37
+ /**
38
+ * Issues an API token with the asked scopes. A scope the app does not declare is refused; a write
39
+ * scope is granted only while `allowWrites` is on (left out otherwise), and a token needs at least
40
+ * one scope left. Shares the per-account limit and lock with MCP tokens. Database failures propagate.
41
+ */
42
+ export async function issueApiToken(ctx, input) {
43
+ const apiOptions = getApiTokenOptions(ctx);
44
+ if (apiOptions === null)
45
+ return err("mcp-access.api_disabled");
46
+ const asked = new Set(input.scopes);
47
+ const declared = new Set(apiOptions.scopes.map((scope) => scope.name));
48
+ if ([...asked].some((name) => !declared.has(name)))
49
+ return err("mcp-access.scope_unknown");
50
+ const { allowWrites } = getMcpAccessOptions(ctx.config);
51
+ const granted = apiOptions.scopes.filter((scope) => asked.has(scope.name) && (scope.access === "read" || allowWrites));
52
+ if (granted.length === 0)
53
+ return err("mcp-access.scope_required");
54
+ const scopes = granted.map((scope) => scope.name);
55
+ const canWrite = granted.some((scope) => scope.access === "write");
56
+ const result = await insertToken(ctx, { userId: input.userId, name: input.name, kind: "api", canWrite, scopes, secret: createApiSecret(apiOptions.prefix) });
57
+ if (!result.ok)
58
+ return result;
59
+ const { id, token, name, expiresAt } = result.value;
60
+ return ok({ id, token, name, scopes, expiresAt });
61
+ }
62
+ /**
63
+ * The identity behind an API token, or null for anything unknown, revoked, expired, an MCP token or
64
+ * any value without the app's prefix. Write scopes count only while writing is enabled (`allowWrites`
65
+ * and the `mcp-access.writes_enabled` switch). Records the use like an MCP token. Database failures propagate.
66
+ */
67
+ export async function verifyApiToken(ctx, token) {
68
+ return (await checkApiToken(ctx, token))?.verified ?? null;
69
+ }
70
+ /** A verified token and the write scopes it holds that the writes switch keeps from it now. */
71
+ async function checkApiToken(ctx, token) {
72
+ const apiOptions = getApiTokenOptions(ctx);
73
+ if (apiOptions === null || !isApiTokenShape(apiOptions, token))
74
+ return null;
75
+ const row = await findValidToken(ctx, hashAccessToken(token.slice(apiOptions.prefix.length)), "api");
76
+ if (row === null)
77
+ return null;
78
+ const isWriteAllowed = await isWritingEnabled(ctx);
79
+ const writeScopes = new Set(apiOptions.scopes.filter((scope) => scope.access === "write").map((scope) => scope.name));
80
+ const withheld = isWriteAllowed ? [] : row.scopes.filter((scope) => writeScopes.has(scope));
81
+ return {
82
+ verified: { userId: row.userId, tokenId: row.id, scopes: row.scopes.filter((scope) => !withheld.includes(scope)), expiresAt: row.expiresAt },
83
+ withheld,
84
+ };
85
+ }
86
+ /**
87
+ * Checks a route's request for a valid API token with every scope in `scopes`, in this order:
88
+ * the client is identified, its request counted per address (before any lookup, so a flood of
89
+ * guesses costs nothing), the `Authorization: Bearer` token verified, the request counted per token,
90
+ * the scopes checked. Expected refusals are values, for `apiTokenErrorResponse`; a failed count or
91
+ * lookup is logged and answered `mcp-access.api_unavailable`. A scope the app does not declare, or a
92
+ * missing bucket, is a setup bug and throws.
93
+ */
94
+ export async function requireApiToken(ctx, request, { scopes: required }) {
95
+ const apiOptions = getApiTokenOptions(ctx);
96
+ if (apiOptions === null) {
97
+ throw new Error("@softure-ai/mcp-access: requireApiToken needs apiTokens in mcpAccess({ ... })");
98
+ }
99
+ assertDeclaredScopes(apiOptions, required);
100
+ assertRateLimitBuckets(ctx);
101
+ const client = identifyClient(ctx, request.headers);
102
+ if (!client.ok)
103
+ return client;
104
+ try {
105
+ const byAddress = await consumeRateLimit(ctx, { bucket: API_RATE_LIMIT_BUCKET, key: client.value });
106
+ if (!byAddress.ok)
107
+ return { ...err("security.rate_limited"), retryAfterSeconds: byAddress.retryAfterSeconds };
108
+ const header = request.headers.get("authorization");
109
+ if (header === null || !BEARER_PREFIX.test(header))
110
+ return err("mcp-access.api_token_missing");
111
+ const checked = await checkApiToken(ctx, header.replace(BEARER_PREFIX, "").trim());
112
+ if (checked === null)
113
+ return err("mcp-access.api_token_invalid");
114
+ const { verified, withheld } = checked;
115
+ const byToken = await consumeRateLimit(ctx, { bucket: API_TOKEN_RATE_LIMIT_BUCKET, key: `token:${verified.tokenId}` });
116
+ if (!byToken.ok)
117
+ return { ...err("security.rate_limited"), retryAfterSeconds: byToken.retryAfterSeconds };
118
+ const missingScopes = required.filter((scope) => !verified.scopes.includes(scope));
119
+ if (missingScopes.length > 0) {
120
+ // Write scopes the token holds while writing is off: say so, the token itself is fine.
121
+ const code = missingScopes.every((scope) => withheld.includes(scope)) ? "mcp-access.api_writes_disabled" : "mcp-access.api_scope_missing";
122
+ return { ...err(code), missingScopes };
123
+ }
124
+ return ok({ userId: verified.userId, tokenId: verified.tokenId, scopes: verified.scopes });
125
+ }
126
+ catch (error) {
127
+ // The token is never logged; the operation and the failure label are.
128
+ console.error(`@softure-ai/mcp-access: checking an API token failed: ${errorLogLabel(error)}`);
129
+ return err("mcp-access.api_unavailable");
130
+ }
131
+ }
132
+ const STATUS_BY_CODE = {
133
+ "security.client_unidentified": 400,
134
+ "mcp-access.api_token_missing": 401,
135
+ "mcp-access.api_token_invalid": 401,
136
+ "mcp-access.api_scope_missing": 403,
137
+ "mcp-access.api_writes_disabled": 403,
138
+ "security.rate_limited": 429,
139
+ "mcp-access.api_unavailable": 503,
140
+ };
141
+ /**
142
+ * The HTTP answer to a refusal of `requireApiToken`: JSON `{ error }` (the code without its module), with
143
+ * `WWW-Authenticate` on 401 and 403 (RFC 6750 §3), `Retry-After` on 429, and never cached.
144
+ */
145
+ export function apiTokenErrorResponse(refusal) {
146
+ const status = STATUS_BY_CODE[refusal.error];
147
+ const error = refusal.error.slice(refusal.error.indexOf(".") + 1);
148
+ const headers = new Headers({ "cache-control": NO_STORE });
149
+ if (status === 401) {
150
+ headers.set("www-authenticate", refusal.error === "mcp-access.api_token_missing" ? "Bearer" : 'Bearer error="invalid_token"');
151
+ }
152
+ if ("missingScopes" in refusal) {
153
+ headers.set("www-authenticate", `Bearer error="insufficient_scope", scope="${refusal.missingScopes.join(" ")}"`);
154
+ return Response.json({ error, missingScopes: refusal.missingScopes }, { status, headers });
155
+ }
156
+ if ("retryAfterSeconds" in refusal)
157
+ headers.set("retry-after", String(refusal.retryAfterSeconds));
158
+ return Response.json({ error }, { status, headers });
159
+ }
160
+ function assertDeclaredScopes(apiOptions, required) {
161
+ const declared = new Set(apiOptions.scopes.map((scope) => scope.name));
162
+ const unknown = required.filter((scope) => !declared.has(scope));
163
+ if (unknown.length > 0) {
164
+ throw new Error(`@softure-ai/mcp-access: requireApiToken asks for scopes apiTokens.scopes does not declare: ${unknown.join(", ")}`);
165
+ }
166
+ }
167
+ /** A missing bucket is a setup bug: thrown, so it reaches the log by name instead of a quiet 503. */
168
+ function assertRateLimitBuckets(ctx) {
169
+ const options = getModule(ctx.config, "security")?.options;
170
+ for (const bucket of [API_RATE_LIMIT_BUCKET, API_TOKEN_RATE_LIMIT_BUCKET]) {
171
+ if (options?.buckets?.[bucket] === undefined) {
172
+ throw new Error(`@softure-ai/mcp-access: the security module has no "${bucket}" bucket; spread API_TOKEN_RATE_LIMIT_BUCKETS into security({ buckets })`);
173
+ }
174
+ }
175
+ }
176
+ //# sourceMappingURL=api-tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-tokens.js","sourceRoot":"","sources":["../../src/server/api-tokens.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import type { McpServerIdentity } from "../contract.js";
3
+ import type { McpAccessContext } from "./tokens.js";
4
+ import { type ToolCaller, type ToolCallHook, type ToolErrorHints } from "./tools.js";
5
+ import type { CallToolResult } from "@modelcontextprotocol/server";
6
+ /** The argument that carries the token on the confirmed call. */
7
+ export declare const CONFIRMATION_FIELD = "confirmation";
8
+ /** Signs and verifies confirmation tokens. */
9
+ export declare const CONFIRMATION_SECRET_ENV = "MCP_CONFIRMATION_SECRET";
10
+ /** A shorter secret counts as missing: a guessable key lets anyone mint a confirmation. */
11
+ export declare const MIN_CONFIRMATION_SECRET_LENGTH = 32;
12
+ /** How long a confirmation stays valid by default: fifteen minutes. */
13
+ export declare const DEFAULT_CONFIRMATION_TTL_SECONDS = 900;
14
+ /** The `confirmation` field for the tool's input schema, so a strict server lists and accepts it. */
15
+ export declare const confirmationInput: z.ZodOptional<z.ZodString>;
16
+ /** The stable codes of a refused confirmed call. */
17
+ export type ConfirmationErrorCode = "input.confirmation_invalid" | "input.confirmation_other_tool" | "input.confirmation_other_account" | "input.confirmation_expired" | "input.confirmation_arguments_changed" | "input.confirmation_estimate_changed" | "input.confirmation_used";
18
+ /** What a call would do and cost: `total` is bound to the token; every other field is shown to the person as it is. */
19
+ export interface ConfirmationEstimate {
20
+ readonly total: number;
21
+ }
22
+ /** The answer of a call that needs the person's yes first. */
23
+ export interface ConfirmationRequest<Estimate extends ConfirmationEstimate = ConfirmationEstimate> {
24
+ readonly needsConfirmation: true;
25
+ readonly estimate: Estimate;
26
+ readonly confirmation: string;
27
+ /** ISO 8601. */
28
+ readonly expiresAt: string;
29
+ /** The fixed sentence that tells the assistant to ask the person before calling again. */
30
+ readonly message: string;
31
+ }
32
+ /** The tool's arguments as the SDK passes them, `confirmation` included. */
33
+ export type ConfirmedToolArgs<Args> = Args & {
34
+ readonly confirmation?: string | undefined;
35
+ };
36
+ export interface WithConfirmationOptions<Args, Estimate extends ConfirmationEstimate> {
37
+ /** The module context: the database for the used tokens, the clock for the expiry, the config for the copy. */
38
+ readonly ctx: McpAccessContext;
39
+ /** The request's identity, as the server factory got it: the token is bound to its account and access token. */
40
+ readonly identity: Pick<McpServerIdentity, "userId" | "tokenId">;
41
+ /** The tool's registered name, also its label in the log line and the `onToolCall` record. */
42
+ readonly tool: string;
43
+ /** What the call would do and cost, without doing it. Runs on both calls; the second must give the same total. */
44
+ readonly estimate: (args: Args) => Estimate | Promise<Estimate>;
45
+ /** How long the person has to agree. Default: 900 seconds. */
46
+ readonly ttlSeconds?: number;
47
+ /** Runs the handler at once, without asking, when this answers true, e.g. `(estimate) => estimate.total === 0`. */
48
+ readonly runWithoutConfirmationWhen?: (estimate: Estimate) => boolean;
49
+ /** Default: `process.env.MCP_CONFIRMATION_SECRET`, read on every call. */
50
+ readonly secret?: string;
51
+ /** Texts per error code, as in `withToolErrors`; they also replace the refusal texts of the dictionary. */
52
+ readonly hints?: ToolErrorHints;
53
+ /** Called once after each call with its record (`confirmed` included), as in `withToolErrors`. */
54
+ readonly onToolCall?: ToolCallHook;
55
+ /** Who called, for the record, as in `withToolErrors`. */
56
+ readonly caller?: ToolCaller;
57
+ }
58
+ /**
59
+ * The tool callback for `server.registerTool(name, { inputSchema: { ...fields, confirmation: confirmationInput } },
60
+ * withConfirmation(handler, options))`. Without `confirmation` it runs `estimate` only and answers a
61
+ * `ConfirmationRequest` (or runs the handler when `runWithoutConfirmationWhen` says so); with a valid one it runs
62
+ * the handler with the arguments without `confirmation`. A refusal answers its `input.confirmation_*` text.
63
+ */
64
+ export declare function withConfirmation<Args extends Readonly<Record<string, unknown>>, Estimate extends ConfirmationEstimate>(handler: (args: Args) => unknown, options: WithConfirmationOptions<Args, Estimate>): (args: ConfirmedToolArgs<Args>) => Promise<CallToolResult>;
65
+ /** Deletes the used tokens past their expiry; a daily job through `pruneMcpAccess`. */
66
+ export declare function pruneUsedConfirmations(ctx: McpAccessContext): Promise<number>;
67
+ /** JSON with every object's keys sorted and `undefined` fields left out, so equal arguments give equal text. */
68
+ export declare function toCanonicalJson(value: unknown): string;
69
+ //# sourceMappingURL=confirmation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confirmation.d.ts","sourceRoot":"","sources":["../../src/server/confirmation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,220 @@
1
+ // Confirmed tool calls: the `--commit` rule over MCP for a tool that spends money or cannot be undone. A call
2
+ // without `confirmation` only runs the estimate and answers it with a confirmation token; the assistant shows the
3
+ // estimate, the person agrees in the chat, and a second call with the same arguments and the token does the work.
4
+ //
5
+ // server.registerTool("render_film", { inputSchema: { scene: z.string(), confirmation: confirmationInput } },
6
+ // withConfirmation((args) => renderFilm(ctx, identity.userId, args), {
7
+ // ctx, identity, tool: "render_film",
8
+ // estimate: (args) => estimateFilm(ctx, identity.userId, args), // { total: 12, currency: "credits", ... }
9
+ // }));
10
+ //
11
+ // The token is a stateless HMAC (`MCP_CONFIRMATION_SECRET`) over the tool, a digest of the canonical JSON of the
12
+ // arguments, a digest of the account and access token, the estimate's total and an expiry. Its payload carries
13
+ // those digests in the clear, so each mismatch gets its own code after the signature has been checked. It is
14
+ // single use: the confirmed call stores its hash (`mcp.used_confirmations`) before the work runs.
15
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
16
+ import { formatMessage } from "@softure-ai/core";
17
+ import { lte } from "drizzle-orm";
18
+ import { z } from "zod";
19
+ import { en } from "../messages/en.js";
20
+ import { usedConfirmations } from "../schema.js";
21
+ import { getMcpAccessMessages } from "./options.js";
22
+ import { withToolErrors } from "./tools.js";
23
+ /** The argument that carries the token on the confirmed call. */
24
+ export const CONFIRMATION_FIELD = "confirmation";
25
+ /** Signs and verifies confirmation tokens. */
26
+ export const CONFIRMATION_SECRET_ENV = "MCP_CONFIRMATION_SECRET";
27
+ /** A shorter secret counts as missing: a guessable key lets anyone mint a confirmation. */
28
+ export const MIN_CONFIRMATION_SECRET_LENGTH = 32;
29
+ /** How long a confirmation stays valid by default: fifteen minutes. */
30
+ export const DEFAULT_CONFIRMATION_TTL_SECONDS = 900;
31
+ /** The longest token the check looks at; a real one is about 300 characters. */
32
+ const MAX_CONFIRMATION_LENGTH = 2048;
33
+ /** The `confirmation` field for the tool's input schema, so a strict server lists and accepts it. */
34
+ export const confirmationInput = z.string().max(MAX_CONFIRMATION_LENGTH).optional().describe(en.confirmation.fieldDescription);
35
+ /**
36
+ * Domain separation: the same key signing anything else can never produce a valid confirmation. Changing it voids
37
+ * every token in flight, exactly like changing the key.
38
+ */
39
+ const SIGNED_PREFIX = "softure.mcp-access.confirmation.v1:";
40
+ const TOKEN_PATTERN = /^([A-Za-z0-9_-]{1,2000})\.([A-Za-z0-9_-]{43})$/;
41
+ const DIGEST_PATTERN = /^[A-Za-z0-9_-]{43}$/;
42
+ const payloadSchema = z.strictObject({
43
+ v: z.literal(1),
44
+ t: z.string().min(1).max(128),
45
+ a: z.string().regex(DIGEST_PATTERN),
46
+ c: z.string().regex(DIGEST_PATTERN),
47
+ n: z.number().finite(),
48
+ e: z.number().int().positive(),
49
+ r: z.string().regex(/^[A-Za-z0-9_-]{16,64}$/),
50
+ });
51
+ /**
52
+ * The tool callback for `server.registerTool(name, { inputSchema: { ...fields, confirmation: confirmationInput } },
53
+ * withConfirmation(handler, options))`. Without `confirmation` it runs `estimate` only and answers a
54
+ * `ConfirmationRequest` (or runs the handler when `runWithoutConfirmationWhen` says so); with a valid one it runs
55
+ * the handler with the arguments without `confirmation`. A refusal answers its `input.confirmation_*` text.
56
+ */
57
+ export function withConfirmation(handler, options) {
58
+ return async (input) => {
59
+ const { [CONFIRMATION_FIELD]: confirmation, ...rest } = input;
60
+ const args = rest;
61
+ let confirmed = false;
62
+ const recordOptions = {
63
+ label: options.tool,
64
+ args: rest,
65
+ hints: { ...getRefusalTexts(options.ctx), ...options.hints },
66
+ // Read after the work, when the call has decided whether it ran on a confirmation.
67
+ get confirmed() {
68
+ return confirmed;
69
+ },
70
+ ...(options.onToolCall === undefined ? {} : { onToolCall: options.onToolCall }),
71
+ ...(options.caller === undefined ? {} : { caller: options.caller }),
72
+ };
73
+ return withToolErrors(async () => {
74
+ const secret = readConfirmationSecret(options);
75
+ const estimate = await runEstimate(options, args);
76
+ if (confirmation === undefined) {
77
+ if (options.runWithoutConfirmationWhen?.(estimate) === true)
78
+ return handler(args);
79
+ return requestConfirmation(options, args, estimate, secret);
80
+ }
81
+ const refusal = await checkConfirmation(options, { token: confirmation, args, estimate, secret });
82
+ if (refusal !== null)
83
+ return { ok: false, error: refusal };
84
+ confirmed = true;
85
+ return handler(args);
86
+ }, recordOptions);
87
+ };
88
+ }
89
+ /** Deletes the used tokens past their expiry; a daily job through `pruneMcpAccess`. */
90
+ export async function pruneUsedConfirmations(ctx) {
91
+ const removed = await ctx.db
92
+ .delete(usedConfirmations)
93
+ .where(lte(usedConfirmations.expiresAt, ctx.clock.now()))
94
+ .returning({ tokenHash: usedConfirmations.tokenHash });
95
+ return removed.length;
96
+ }
97
+ function readConfirmationSecret(options) {
98
+ const secret = options.secret ?? process.env[CONFIRMATION_SECRET_ENV];
99
+ if (secret !== undefined && secret.length >= MIN_CONFIRMATION_SECRET_LENGTH)
100
+ return secret;
101
+ // The boundary logs a thrown error by its kind only, so the cause is named here once.
102
+ console.error(`@softure-ai/mcp-access: tool ${options.tool} needs ${CONFIRMATION_SECRET_ENV} (at least ${String(MIN_CONFIRMATION_SECRET_LENGTH)} characters)`);
103
+ throw new Error(`@softure-ai/mcp-access: ${CONFIRMATION_SECRET_ENV} is missing or shorter than ${String(MIN_CONFIRMATION_SECRET_LENGTH)} characters`);
104
+ }
105
+ async function runEstimate(options, args) {
106
+ const estimate = await options.estimate(args);
107
+ if (typeof estimate !== "object" || estimate === null || typeof estimate.total !== "number" || !Number.isFinite(estimate.total)) {
108
+ throw new Error(`@softure-ai/mcp-access: the estimate of tool ${options.tool} has no finite numeric total`);
109
+ }
110
+ return estimate;
111
+ }
112
+ function requestConfirmation(options, args, estimate, secret) {
113
+ const ttlSeconds = options.ttlSeconds ?? DEFAULT_CONFIRMATION_TTL_SECONDS;
114
+ const expiresAtSeconds = Math.floor(options.ctx.clock.now().getTime() / 1000) + Math.max(1, Math.floor(ttlSeconds));
115
+ const payload = {
116
+ v: 1,
117
+ t: options.tool,
118
+ a: digestArguments(args),
119
+ c: digestCaller(options.identity),
120
+ n: estimate.total,
121
+ e: expiresAtSeconds,
122
+ r: randomBytes(16).toString("base64url"),
123
+ };
124
+ const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
125
+ const expiresAt = new Date(expiresAtSeconds * 1000).toISOString();
126
+ return {
127
+ needsConfirmation: true,
128
+ estimate,
129
+ confirmation: `${encoded}.${sign(encoded, secret)}`,
130
+ expiresAt,
131
+ message: formatMessage(getMcpAccessMessages(options.ctx.config).confirmation.ask, { expiresAt }),
132
+ };
133
+ }
134
+ /**
135
+ * The refusal code, or null when the call may run. The signature is checked first, so no field of the payload is
136
+ * trusted before it; the token is consumed last, so a refused call never uses up a valid confirmation.
137
+ */
138
+ async function checkConfirmation(options, check) {
139
+ const payload = readSignedPayload(check.token, check.secret);
140
+ if (payload === null)
141
+ return "input.confirmation_invalid";
142
+ if (payload.t !== options.tool)
143
+ return "input.confirmation_other_tool";
144
+ if (payload.c !== digestCaller(options.identity))
145
+ return "input.confirmation_other_account";
146
+ const now = options.ctx.clock.now();
147
+ if (now.getTime() >= payload.e * 1000)
148
+ return "input.confirmation_expired";
149
+ if (payload.a !== digestArguments(check.args))
150
+ return "input.confirmation_arguments_changed";
151
+ if (payload.n !== check.estimate.total)
152
+ return "input.confirmation_estimate_changed";
153
+ const inserted = await options.ctx.db
154
+ .insert(usedConfirmations)
155
+ .values({ tokenHash: createHash("sha256").update(check.token).digest("hex"), usedAt: now, expiresAt: new Date(payload.e * 1000) })
156
+ .onConflictDoNothing()
157
+ .returning({ tokenHash: usedConfirmations.tokenHash });
158
+ return inserted.length === 0 ? "input.confirmation_used" : null;
159
+ }
160
+ /** The payload of a token this secret signed, else null. Never throws. */
161
+ function readSignedPayload(token, secret) {
162
+ if (token.length > MAX_CONFIRMATION_LENGTH)
163
+ return null;
164
+ const match = TOKEN_PATTERN.exec(token);
165
+ if (match === null)
166
+ return null;
167
+ const [, encoded = "", signature = ""] = match;
168
+ // Both are 43 characters of base64url (the pattern), so the buffers have the same length.
169
+ if (!timingSafeEqual(Buffer.from(sign(encoded, secret)), Buffer.from(signature)))
170
+ return null;
171
+ let decoded;
172
+ try {
173
+ decoded = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
174
+ }
175
+ catch {
176
+ // Signed by this secret but unreadable: only a key shared with another signer could produce that.
177
+ return null;
178
+ }
179
+ const parsed = payloadSchema.safeParse(decoded);
180
+ return parsed.success ? parsed.data : null;
181
+ }
182
+ function sign(encoded, secret) {
183
+ return createHmac("sha256", secret).update(`${SIGNED_PREFIX}${encoded}`).digest("base64url");
184
+ }
185
+ function digestArguments(args) {
186
+ return createHash("sha256").update(toCanonicalJson(args)).digest("base64url");
187
+ }
188
+ function digestCaller(identity) {
189
+ return createHash("sha256").update(JSON.stringify([identity.userId, identity.tokenId])).digest("base64url");
190
+ }
191
+ /** JSON with every object's keys sorted and `undefined` fields left out, so equal arguments give equal text. */
192
+ export function toCanonicalJson(value) {
193
+ return JSON.stringify(sortKeys(value)) ?? "null";
194
+ }
195
+ function sortKeys(value) {
196
+ if (Array.isArray(value))
197
+ return value.map((item) => (item === undefined ? null : sortKeys(item)));
198
+ if (value instanceof Date)
199
+ return value.toISOString();
200
+ if (typeof value !== "object" || value === null)
201
+ return value;
202
+ const entries = Object.entries(value)
203
+ .filter(([, item]) => item !== undefined)
204
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
205
+ return Object.fromEntries(entries.map(([key, item]) => [key, sortKeys(item)]));
206
+ }
207
+ /** The dictionary's refusal texts per code, with the app's overrides. */
208
+ function getRefusalTexts(ctx) {
209
+ const texts = getMcpAccessMessages(ctx.config).confirmation;
210
+ return {
211
+ "input.confirmation_invalid": texts.invalid,
212
+ "input.confirmation_other_tool": texts.otherTool,
213
+ "input.confirmation_other_account": texts.otherAccount,
214
+ "input.confirmation_expired": texts.expired,
215
+ "input.confirmation_arguments_changed": texts.argumentsChanged,
216
+ "input.confirmation_estimate_changed": texts.estimateChanged,
217
+ "input.confirmation_used": texts.used,
218
+ };
219
+ }
220
+ //# sourceMappingURL=confirmation.js.map