@softure-ai/mcp-access 0.2.9 → 0.3.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 +55 -0
  2. package/README.md +231 -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 +61 -0
  22. package/dist/jobs/operation-tasks.d.ts.map +1 -0
  23. package/dist/jobs/operation-tasks.js +161 -0
  24. package/dist/jobs/operation-tasks.js.map +1 -0
  25. package/dist/jobs/operations.d.ts +68 -0
  26. package/dist/jobs/operations.d.ts.map +1 -0
  27. package/dist/jobs/operations.js +110 -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 +41 -2
  106. package/src/jobs/operation-state.ts +119 -0
  107. package/src/jobs/operation-tasks.ts +229 -0
  108. package/src/jobs/operations.ts +213 -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/CHANGELOG.md CHANGED
@@ -4,6 +4,61 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
4
4
  production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
5
5
  "Definition of done"). Versions before the first one below are described in their GitHub Releases (`mcp-access@x.y.z`).
6
6
 
7
+ ## 0.3.0
8
+
9
+ - Long operations as MCP Tasks (#977), in `@softure-ai/mcp-access/jobs`: `startOperation` takes `taskRequest` (the
10
+ tool handler's request context) and `taskTtlSeconds`; when the client declared the Tasks extension
11
+ (`io.modelcontextprotocol/tasks`, revision 2026-07-28) on that request, the tool answers a task instead of an
12
+ operation id. `registerOperationTasks(server, { ctx, identity, readResult?, poll? })` declares the extension and
13
+ serves `tasks/get` (working, completed with the `get_operation` answer, `isError` for a failed operation, cancelled),
14
+ `tasks/update` and `tasks/cancel` (write access only), scoped to the account. Task ids are random and live in the
15
+ new table `mcp.operation_tasks` (migration `0006`), pruned past their expiry by `pruneMcpAccess` (which now also
16
+ counts `operationTasks`, and `softure-mcp prune` prints it) and deleted with the account. Every other client keeps the
17
+ polling tools. Migrate: run `softure migrate`; to offer tasks, pass the handler's second argument as `taskRequest`
18
+ and call `registerOperationTasks` on the same server (README "As MCP Tasks").
19
+ - Long operations as tools (#952), in `@softure-ai/mcp-access/jobs` on top of `@softure-ai/jobs` (with the progress
20
+ and running-job cancel of jobs #951): `startOperation(ctx, identity, { queue, payload, dedupKey? })` enqueues and
21
+ answers `{ operationId, status: "queued", pollAfterSeconds, message }` at once; `registerOperationStatusTool`
22
+ (`get_operation`) answers the status, the handler's progress, a growing `pollAfterSeconds`, the app's `result`
23
+ (`readResult`) and the `errorCode`; `registerOperationCancelTool` (`cancel_operation`, write access only) cancels a
24
+ queued operation and asks a running one to stop. Operations are scoped to the caller's account; no migration.
25
+ Migrate: an app with its own "start returns an id" and "status" tools can replace them with these (README "Long
26
+ operations").
27
+ - Personal tokens for the app's plain HTTP routes (#961): the option `apiTokens` (`prefix`, default `sftapi_`;
28
+ `scopes` with `read`/`write` access and a description; `examples` for the one-time reveal) turns on a second token
29
+ kind. `requireApiToken(ctx, request, { scopes })` checks a route's request: client address counted (bucket `api`),
30
+ Bearer token verified, token counted (bucket `api-token`), scopes and the write switch checked; refusals are values
31
+ that `apiTokenErrorResponse` answers (400/401/403/429/503). The new entry `@softure-ai/mcp-access/tokens` exports
32
+ it with `issueApiToken`, `verifyApiToken`, list, revoke and prune, and loads no MCP SDK. An API token never opens
33
+ the MCP endpoint and an MCP token never passes the check. The token page lists the app's scopes, adds the form
34
+ "New token for other apps" (`kind=api`, `scope:<name>` fields), shows the token once with the app's examples
35
+ (form state `api-ok`, `IssuedApiTokenView`) and names each token's kind. `AccessTokenView` gains `kind` and
36
+ `scopes`; the gate input `kind` and `scopes`; the privacy export `kind` and `scopes`. Both kinds share
37
+ `maxTokensPerUser`. Migration `0005_add_api_tokens.sql` adds `kind` and `scopes` (existing rows stay MCP tokens).
38
+ Migrate: nothing for an app without `apiTokens`, apart from running the migration; an app that compares
39
+ `listAccessTokens` or the privacy export field by field gains the two fields. To use it, add `apiTokens`, spread
40
+ `API_TOKEN_RATE_LIMIT_BUCKETS` into `security({ buckets })` and call `requireApiToken` in the routes.
41
+ - Confirmed tool calls (#953): `withConfirmation(handler, { ctx, identity, tool, estimate, ttlSeconds?,
42
+ runWithoutConfirmationWhen?, secret? })` (`/server`) makes a priced or irreversible tool estimate first and answer
43
+ `{ needsConfirmation: true, estimate, confirmation, expiresAt, message }`; the second call with the same arguments
44
+ and the token does the work. The token is an HMAC bound to the tool, the canonical arguments, the account and access
45
+ token, the estimate's total and the expiry, single use (stored by hash in the new `mcp.used_confirmations`), and a
46
+ mismatch is refused as `input.confirmation_invalid`, `_other_tool`, `_other_account`, `_expired`,
47
+ `_arguments_changed`, `_estimate_changed` or `_used`. `confirmationInput` is the schema field. `ToolCallRecord`
48
+ gains `confirmed: boolean | null` (`null` for other tools), and `pruneMcpAccess` counts `usedConfirmations`.
49
+ Migrate: run `softure migrate` (file 4). To confirm a tool, set `MCP_CONFIRMATION_SECRET` (32+ characters), add
50
+ `confirmation: confirmationInput` to its input schema and wrap its callback; drop an app-level `confirm: true`
51
+ boolean the assistant could set itself. A test that builds a `ToolCallRecord` by hand adds `confirmed: null`.
52
+ - Strict tool input lists a required nullable scalar as required (#919): a `z.number().nullable()` or
53
+ `z.boolean().nullable()` field that the parse requires is in `required` of the listed schema again, and a scalar
54
+ field's `default` (or `prefault`) is listed. The parse rules are unchanged. Migrate: an app that copied the inner
55
+ schema's `optin` onto its own text step, or put a `default` inside it, can drop that wrapper.
56
+ - A tool registered without an input schema on a strict server gets the strict empty object (#919): `tools/call`
57
+ with any argument is refused with the unknown-fields text, and the handler is still called with the context only.
58
+ The listing gains `additionalProperties: false`, so the measured tool list version changes once.
59
+ - `renamedFields` also takes a rule `(field) => newName | undefined` (#919), e.g. for a rename by pattern across many
60
+ fields. `RenamedFields` is exported from `/server`.
61
+
7
62
  ## 0.2.9
8
63
 
9
64
  - A runtime kill switch for writes (#904): the manifest switch `mcp-access.writes_enabled` (`WRITES_ENABLED_SWITCH`)
package/README.md CHANGED
@@ -42,6 +42,9 @@ limit check made race-free.
42
42
  - one grant per account and client, listed on the token page as "connected apps"; revoking it
43
43
  deletes its access tokens at once (`grant_id`, `ON DELETE CASCADE`). OAuth access tokens do not
44
44
  count against `maxTokensPerUser` and are not listed with the hand-issued ones.
45
+ - **Tokens for the app's own HTTP routes** (`apiTokens`, off by default): personal tokens with the app's prefix and
46
+ scopes (`recipes:write`) for a phone Shortcut, an automation tool or a script, checked in a route handler with
47
+ `requireApiToken` from `@softure-ai/mcp-access/tokens`, an entry that loads no MCP SDK (§4 "Tokens for HTTP routes").
45
48
  - Tokens an app issued before adopting the module keep working through `legacyTokenPattern`.
46
49
  - Server functions for scripts and other hosts, and a health check for `GET /api/health`.
47
50
  - For the app's own server: tool result helpers with a safe error boundary and server actions as write tools
@@ -49,7 +52,9 @@ limit check made race-free.
49
52
  (`createMcpServerFactory`, `measureToolList`, `/server`), strict tool input that refuses unknown and renamed
50
53
  fields (`withStrictToolInput`, `/server`), an opt-in `get_server_info` tool that tells which build answers
51
54
  (`serverInfoTool`, `/server`), a stdio entry for local assistants (`/stdio`), a catalog parity check (`/testing`), a gate before a
52
- token is issued (`issueToken`, `/next`) and the `softure-mcp prune` command for a daily cleanup (§4, §5).
55
+ token is issued (`issueToken`, `/next`), long operations as tools over `@softure-ai/jobs` (`startOperation`,
56
+ `get_operation`, `cancel_operation`, `/jobs`) or as MCP Tasks for clients that declare them (`registerOperationTasks`)
57
+ and the `softure-mcp prune` command for a daily cleanup (§4, §5).
53
58
 
54
59
  ## 2. Installation
55
60
 
@@ -193,7 +198,7 @@ input schema registered on the server from then on, or pass `strictToolInput: tr
193
198
 
194
199
  ```ts
195
200
  const server = withStrictToolInput(new McpServer({ name: "acme", version }), {
196
- renamedFields: { notes: "note" }, // old name → new name
201
+ renamedFields: { notes: "note" }, // old name → new name, or a rule: (field) => newName | undefined
197
202
  coerceTextScalars: true, // default
198
203
  messages: getMcpAccessMessages(config).toolInput, // default: the English dictionary
199
204
  });
@@ -206,10 +211,16 @@ const server = withStrictToolInput(new McpServer({ name: "acme", version }), {
206
211
  `.catchall(x)`) keeps it; refinements and the object's own error messages stay.
207
212
  - **Renamed fields:** a field sent under an old name where the object accepts the new one is refused with the
208
213
  stable code `input.field_renamed` and the new name, so a session on an older tool list learns it in one call.
214
+ `renamedFields` is a map or a rule, e.g. `(field) => field.includes("Pln") ? field.replace("Pln", "Home") :
215
+ undefined` for a rename across many fields; a name the rule leaves alone is an unknown field.
209
216
  - **Text scalars:** a client on an older tool list sends a field it does not know as text, so a boolean field also
210
217
  takes the exact text `"true"`/`"false"` and a number field a JSON number written as text, converted before the
211
218
  field's own rules (`"-1"` still fails `min(0)`). Any other text is refused. The listed JSON Schema keeps `boolean`
212
- and `number` and gains `additionalProperties: false`.
219
+ and `number`, the fields' `required` and the `default` of a scalar field, and gains `additionalProperties: false`.
220
+ A required `z.number().nullable()` stays required. (zod lists no `default` on an object or array that holds a
221
+ scalar field, since its input is transformed.)
222
+ - **No input schema:** a tool registered without one gets the strict empty object, so `tools/call` with any argument
223
+ is refused; its handler is still called with the request context only.
213
224
  - The SDK validates before the handler runs, so a refused call saves nothing. A raw shape is wrapped in
214
225
  `z.object` first; a schema that is not zod passes unchanged. `toStrictToolInput(schema, options?)` gives the
215
226
  strict schema on its own.
@@ -296,6 +307,128 @@ export async function createServer({ userId, canWrite, tokenId }: McpServerIdent
296
307
  }
297
308
  ```
298
309
 
310
+ ### Long operations: start, status, cancel
311
+
312
+ A tool whose work takes minutes (a render, an import) cannot hold the call open: the endpoint answers in JSON mode
313
+ and drops mid-call notifications, a proxy may cut a long response (Cloudflare at 100 seconds), and assistants time out
314
+ tool calls. `@softure-ai/mcp-access/jobs` turns such work into an operation on top of `@softure-ai/jobs`: the tool
315
+ starts a job and answers an id at once, the assistant asks `get_operation` until it ends, and may stop it with
316
+ `cancel_operation`.
317
+
318
+ ```ts
319
+ import { registerOperationCancelTool, registerOperationStatusTool, startOperation } from "@softure-ai/mcp-access/jobs";
320
+
321
+ export async function createServer(identity: McpServerIdentity) {
322
+ server.registerTool("render_video", { inputSchema: z.object({ videoId: z.string() }) }, (args) =>
323
+ withToolErrors(() => startOperation(ctx, identity, { queue: "video.render", payload: args, dedupKey: `video:${args.videoId}` })),
324
+ );
325
+ registerOperationStatusTool(server, { ctx, identity, readResult: (job) => findRenderUrl(db, job.id) });
326
+ registerOperationCancelTool(server, { ctx, identity }); // registers nothing without identity.canWrite
327
+ }
328
+ ```
329
+
330
+ - `startOperation(ctx, identity, { queue, payload, dedupKey?, statusToolName?, poll? })` enqueues the job and answers
331
+ `{ operationId, status: "queued", progress: null, pollAfterSeconds, message }`; the message tells the assistant to
332
+ call the status tool and not to start again. With a `dedupKey` (at most 150 characters), starting again while the
333
+ account's operation of that queue runs joins it: the answer carries its id and status.
334
+ - `get_operation` (`registerOperationStatusTool`, read only) answers `{ operationId, status, progress,
335
+ pollAfterSeconds?, cancelRequested?, result?, errorCode?, message }`. `status` is `queued`, `running` (also while a
336
+ failed attempt waits for its retry), `succeeded`, `failed` or `cancelled`; `progress` is what the handler reports
337
+ with `context.reportProgress` (codes and counts only); `pollAfterSeconds` is there while the operation runs and
338
+ grows with its age (a quarter of its seconds, between `poll.minSeconds` 2 and `poll.maxSeconds` 30); `result` is
339
+ what `readResult(job)` reads from the app's tables once it succeeded (jobs stores no result); `errorCode` is the
340
+ failure's code (`jobs.cancelled` for a cancelled one).
341
+ - `cancel_operation` (`registerOperationCancelTool`, a write) ends a queued operation at once and asks a running one
342
+ to stop (`requestJobCancel`: the handler's `signal` aborts on its next heartbeat); the answer says
343
+ `cancelRequested: true` until it has ended. An ended one answers "already ended" as a tool error.
344
+ - Scoped to the account: an operation's job has origin `mcp` and the dedup key `mcp:<userId>:<key>` (a random key
345
+ without `dedupKey`). Another account's operation, a job that is not an operation and a malformed id all answer
346
+ "No operation with this id belongs to this account." No table of its own: an operation lives as long as its job
347
+ (`retention` of jobs).
348
+ - `getOperation` and `cancelOperation` are the same reads as functions; the texts are under `operations` in the
349
+ module dictionary, and `name`/`description` rename a tool.
350
+
351
+ #### As MCP Tasks
352
+
353
+ A client that speaks the Tasks extension (`io.modelcontextprotocol/tasks`, protocol revision 2026-07-28) can follow an
354
+ operation as a task instead of calling `get_operation`. It is opt-in per tool and needs two things on the same server:
355
+ pass the tool handler's request context as `taskRequest`, and register the task handlers.
356
+
357
+ ```ts
358
+ import { registerOperationTasks, startOperation } from "@softure-ai/mcp-access/jobs";
359
+
360
+ server.registerTool("render_video", { inputSchema: z.object({ videoId: z.string() }) }, (args, request) =>
361
+ withToolErrors(() => startOperation(ctx, identity, { queue: "video.render", payload: args, taskRequest: request })),
362
+ );
363
+ registerOperationStatusTool(server, { ctx, identity, readResult }); // still there for every other client
364
+ registerOperationTasks(server, { ctx, identity, readResult });
365
+ ```
366
+
367
+ - Only a request whose client declared the extension in its per-request capabilities gets a task (`resultType:
368
+ "task"`, `status: "working"`, `ttlMs`, `pollIntervalMs`). A client that did not, including every 2025-era client,
369
+ gets the operation id exactly as above, so the polling tools stay the default.
370
+ - `registerOperationTasks(server, { ctx, identity, readResult?, poll? })` declares the extension in `server/discover`
371
+ and answers `tasks/get`, `tasks/update` and `tasks/cancel` for the request's account:
372
+ - `tasks/get`: `working` while the operation is queued or running (`statusMessage` says which, or that a stop was
373
+ asked; `pollIntervalMs` is `pollAfterSeconds` in milliseconds); `completed` once it ended with a `result` that is
374
+ the `get_operation` answer as a tool result (with the app's `readResult`), marked `isError: true` when the
375
+ operation failed (the spec keeps the `failed` status for protocol errors); `cancelled` when it was cancelled.
376
+ - `tasks/cancel`: like `cancel_operation`, a write: a read-only token gets `-32600`. An operation that has ended is
377
+ acknowledged all the same (cancelling is cooperative in the spec).
378
+ - `tasks/update`: operations never ask for input, so an owned task is acknowledged.
379
+ - An unknown task, another account's, or one whose job is gone is `-32602` "Task not found"; an expired one `-32602`
380
+ "Task has expired"; a request without the declared extension `-32021`.
381
+ - A task id is random (32 bytes, base64url) and stored in `mcp.operation_tasks` (§5) with its owner, its job and an
382
+ expiry: `taskTtlSeconds` on `startOperation`, a minute to thirty days, default a day. The same account starting the
383
+ same running operation again (same `dedupKey`) gets the same task. Progress counts stay with `get_operation`.
384
+
385
+ ### Confirmed calls
386
+
387
+ A tool that spends money (a paid provider call) or cannot be undone follows the `--commit` rule over MCP:
388
+ `withConfirmation` (`/server`) makes the first call estimate only, the person agrees in the chat, and only a second
389
+ call with the same input does the work. The assistant cannot agree on its own: the token comes only from an estimate,
390
+ is single use and bound to that exact input.
391
+
392
+ ```ts
393
+ import { confirmationInput, withConfirmation } from "@softure-ai/mcp-access/server";
394
+
395
+ server.registerTool(
396
+ "render_film",
397
+ { inputSchema: { scene: z.string(), seconds: z.number(), confirmation: confirmationInput } },
398
+ withConfirmation((args) => renderFilm(ctx, userId, args), {
399
+ ctx, // the module context, as for the app's queries
400
+ identity, // the factory's McpServerIdentity: the token is bound to its userId and tokenId
401
+ tool: "render_film",
402
+ estimate: (args) => estimateFilm(ctx, userId, args), // { total: 12, currency: "credits", ... }
403
+ runWithoutConfirmationWhen: (estimate) => estimate.total === 0, // everything was cached
404
+ }),
405
+ );
406
+ ```
407
+
408
+ - **First call** (no `confirmation`): runs `estimate(args)` only and answers
409
+ `{ needsConfirmation: true, estimate, confirmation, expiresAt, message }`; `message` is the fixed sentence
410
+ `confirmation.ask` of the dictionary, which tells the assistant to show the estimate and get the person's yes.
411
+ `estimate` returns an object with a finite numeric `total`; every other field reaches the assistant as it is.
412
+ When `runWithoutConfirmationWhen(estimate)` is true the handler runs at once instead.
413
+ - **The token** is an HMAC-SHA256 signed with `MCP_CONFIRMATION_SECRET` (§6, or the `secret` option) over the tool
414
+ name, a digest of the canonical JSON of the arguments without `confirmation` (`toCanonicalJson`: keys sorted,
415
+ `undefined` left out), a digest of the account and the access token, the estimate's total and the expiry
416
+ (`ttlSeconds`, default 900).
417
+ - **Second call** (same arguments and `confirmation`): the estimate runs again, then the checks, each refused with its
418
+ own code and the text of `confirmation.*` in the dictionary (an app's `hints` replace them):
419
+ `input.confirmation_invalid` (not signed by this secret, malformed), `input.confirmation_other_tool`,
420
+ `input.confirmation_other_account` (another account or access token), `input.confirmation_expired`,
421
+ `input.confirmation_arguments_changed`, `input.confirmation_estimate_changed` (the total changed after the yes)
422
+ and `input.confirmation_used`. Only when all pass is the token stored by its hash and the handler run with the
423
+ arguments without `confirmation`; two parallel calls with one token run once. A refused call never uses up a valid
424
+ token; a handler that fails after the check needs a new estimate.
425
+ - **The record**: `onToolCall` records gain `confirmed`: `true` for a call that ran on a valid confirmation, `false`
426
+ for an estimate, a refusal or a call run through `runWithoutConfirmationWhen`, `null` for tools without
427
+ confirmation. `argumentNames` leave `confirmation` out.
428
+ - `confirmationInput` is the field for the input schema (an optional string), so a strict server lists and accepts
429
+ it. Without `MCP_CONFIRMATION_SECRET` the call fails as a bug, with one log line naming the variable: it never runs
430
+ without asking.
431
+
299
432
  ### Stdio
300
433
 
301
434
  A local assistant on the machine that holds the database can run the same factory over stdio, without a token:
@@ -491,6 +624,67 @@ An app that composes its own routes from `/server` builds the context with
491
624
  resolves origins with `resolveMcpOrigins(config, request)` and passes them to the URL builders,
492
625
  whose last argument they are; `OAuthClientRow` is exported from the package root.
493
626
 
627
+ ### Tokens for HTTP routes
628
+
629
+ A person who calls the app from outside the browser (an iPhone Shortcut, Make or Zapier, a script) gets a personal
630
+ token for the app's plain HTTP routes. Declare the scopes the routes ask for, and the buckets:
631
+
632
+ ```ts
633
+ // softure.config.ts
634
+ security({ buckets: { ...AUTH_RATE_LIMIT_BUCKETS, ...MCP_RATE_LIMIT_BUCKETS, ...API_TOKEN_RATE_LIMIT_BUCKETS } }),
635
+ mcpAccess({
636
+ serverName: "acme",
637
+ allowWrites: true,
638
+ apiTokens: {
639
+ prefix: "acme_", // default "sftapi_"; never one of the module's own sftmc* prefixes
640
+ scopes: [
641
+ { name: "recipes:read", access: "read", description: { en: "Reads your recipes." } },
642
+ { name: "recipes:write", access: "write", description: { en: "Adds and changes recipes." } },
643
+ ],
644
+ // Shown once with a new token; {token} and {appOrigin} are filled in.
645
+ examples: [{ title: { en: "curl" }, text: { en: "curl -H 'Authorization: Bearer {token}' {appOrigin}/api/recipes" } }],
646
+ },
647
+ }),
648
+ ```
649
+
650
+ ```ts
651
+ // app/api/recipes/route.ts
652
+ import { getSoftureConfig } from "@softure-ai/core/next";
653
+ import { apiTokenErrorResponse, createMcpAccessContext, requireApiToken } from "@softure-ai/mcp-access/tokens";
654
+
655
+ export async function POST(request: Request): Promise<Response> {
656
+ const ctx = await createMcpAccessContext(getSoftureConfig());
657
+ const caller = await requireApiToken(ctx, request, { scopes: ["recipes:write"] });
658
+ if (!caller.ok) return apiTokenErrorResponse(caller);
659
+ // caller.value: { userId, tokenId, scopes }; scope every query to userId.
660
+ return Response.json(await addRecipe(caller.value.userId, await request.json()));
661
+ }
662
+ ```
663
+
664
+ - **The two kinds stay apart.** An API token (`kind = 'api'`, the app's prefix) never opens `POST /api/mcp`, and an
665
+ MCP token (`sftmcp_`, `mcp:read`/`mcp:write`) never passes `requireApiToken`. Both share the table, the hashing,
666
+ `tokenLifetimeDays`, `maxTokensPerUser` (one count for both), the revoke action, privacy and prune.
667
+ - **Order of the check:** the client address is counted in `api` before any lookup, the `Authorization: Bearer`
668
+ token is verified (expiry in the query, last use recorded), the token is counted in `api-token`, then the scopes.
669
+ - **Refusals are values** for `apiTokenErrorResponse`: `400 client_unidentified`; `401 api_token_missing` and
670
+ `api_token_invalid` (unknown, revoked, expired, an MCP token) with `WWW-Authenticate: Bearer`; `403
671
+ api_scope_missing` with `missingScopes` and `error="insufficient_scope"`; `403 api_writes_disabled` when the token
672
+ holds the write scope but writing is off; `429 rate_limited` with `Retry-After`; `503 api_unavailable` (logged)
673
+ when the count or the lookup fails. A scope the app does not declare, or missing buckets, throws: a setup bug.
674
+ - **Writes:** a `write` scope is granted at issue only with `allowWrites`, and counts on a request only while
675
+ writing is enabled (`allowWrites` and the `mcp-access.writes_enabled` switch, §7).
676
+ - **The token page** lists the app's scopes and offers a second form, "New token for other apps", that posts to the
677
+ same issue action with `kind=api` and a `scope:<name>` checkbox per offered scope. The new token is shown once
678
+ with its header and the app's `examples`; the action answers `{ status: "api-ok", issued }`
679
+ (`IssuedApiTokenView`), so an app reading `status === "ok"` for MCP tokens is unchanged. The list names each
680
+ token's kind ("AI assistant", "Other apps") and an API token's scopes. A `beforeIssue` gate gets `kind` and
681
+ `scopes`, with `canWrite` true when a write scope is asked.
682
+ - **Without MCP:** an app that has no MCP server enables `mcpAccess` with `apiTokens`, mounts only the token page
683
+ and its routes, and imports `@softure-ai/mcp-access/tokens` (which loads no `@modelcontextprotocol/*` module). The
684
+ npm peer list still names the SDK.
685
+ - Server functions: `issueApiToken`, `verifyApiToken`, `requireApiToken`, `apiTokenErrorResponse`,
686
+ `isApiTokenShape`, plus `listAccessTokens`, `revokeAccessToken`, `pruneAccessTokens` (`/tokens`, also in `/server`).
687
+
494
688
  ### Closed registration
495
689
 
496
690
  Registration is public by default: claude.ai and ChatGPT register a client before anyone signs in, and a client
@@ -546,9 +740,25 @@ expired (with their access tokens) and clients registered over a day ago that ho
546
740
  (`id = 1`) with `opened_at` and `open_until` (`open_until > opened_at`), the window of "Closed registration" (§4). It
547
741
  holds no personal data.
548
742
 
743
+ `migrations/0004_create_used_confirmations.sql` creates `mcp.used_confirmations`: the sha256 of each confirmation
744
+ token that has been used (`token_hash`, the primary key), `used_at` and the token's `expires_at`, which makes a
745
+ confirmation single use ("Confirmed calls", §4). It holds no personal data; `pruneUsedConfirmations(ctx)` deletes the
746
+ rows past `expires_at`.
747
+
748
+ `migrations/0005_add_api_tokens.sql` adds `access_tokens.kind` (`mcp` or `api`, default `mcp`) and
749
+ `access_tokens.scopes` (`text[]`, default empty), with one check: an `mcp` row has no scopes, an `api` row has 1 to
750
+ 32 scopes and no grant. Existing rows become MCP tokens through the defaults. An API token's `token_hash` is the
751
+ sha256 of its random part after the prefix (the prefix is checked by shape before any lookup). Rollback: in the migration header.
752
+
753
+ `migrations/0006_create_operation_tasks.sql` creates `mcp.operation_tasks` for long operations as MCP Tasks (§4):
754
+ `task_id` (random, 43 base64url characters, the primary key), `user_id` (`REFERENCES auth.users ON DELETE CASCADE`),
755
+ `job_id` (the operation's job in `@softure-ai/jobs`, no foreign key since jobs is optional), `created_at` and
756
+ `expires_at`, unique per account and job. `pruneOperationTasks(ctx)` (`/server`) deletes the rows past `expires_at`;
757
+ the jobs themselves follow the `retention` of jobs.
758
+
549
759
  ### Scheduled pruning
550
760
 
551
- `pruneMcpAccess(ctx)` (`/server`) runs both prunes and counts what each removed. The `softure-mcp prune` command
761
+ `pruneMcpAccess(ctx)` (`/server`) runs the prunes of tokens, OAuth records, used confirmations and operation tasks and counts what each removed. The `softure-mcp prune` command
552
762
  runs it with the app's config (loaded like `softure migrate`, or from an app script through `runMcpAccessCli` from
553
763
  `/cli`) and prints one line. Run it daily, e.g. as a `maintain` hook of `@softure-ai/deploy`:
554
764
 
@@ -593,7 +803,11 @@ migrations after the baseline.
593
803
 
594
804
  ## 6. Environment variables
595
805
 
596
- None of its own. The endpoint URL comes from `appOrigin` in `softure.config.ts`.
806
+ The endpoint URL comes from `appOrigin` in `softure.config.ts`.
807
+
808
+ | Variable | Required | Meaning |
809
+ | --- | --- | --- |
810
+ | `MCP_CONFIRMATION_SECRET` | only for confirmed calls | Signs and verifies the tokens of `withConfirmation` (§4), at least 32 characters, read on every call. Changing it voids the tokens in flight (the person is asked again). |
597
811
 
598
812
  ## 7. Switches
599
813
 
@@ -663,11 +877,12 @@ The MCP server factory passed to `createMcpRoute` (and to `serveMcpStdio`) and t
663
877
  The rows hold no personal data beyond the owner's id and the token names. The module contributes
664
878
  to `@softure-ai/privacy` (`privacy` flags on):
665
879
 
666
- - **Export** (`exportMcpAccessUserData`): `accessTokens`, each hand-issued token's name, write
667
- flag and created, expiry and last-use dates, and `connectedApps`, each grant's client name, write
880
+ - **Export** (`exportMcpAccessUserData`): `accessTokens`, each hand-issued token's name, kind
881
+ (`mcp` or `api`), scopes, write flag and created, expiry and last-use dates, and `connectedApps`, each grant's client name, write
668
882
  flag, created and last-use dates. Never a hash.
669
- - **Deletion** (`deleteMcpAccessUserData`): the user's authorization codes, grants and tokens,
670
- before auth deletes the account (the foreign keys' cascade would remove them too). Registered
883
+ - **Deletion** (`deleteMcpAccessUserData`): the user's authorization codes, grants, tokens and MCP tasks,
884
+ before auth deletes the account (the foreign keys' cascade would remove them too). Tasks hold a job id and dates
885
+ only, so the export leaves them out. Registered
671
886
  clients hold no personal data and are pruned when unused.
672
887
 
673
888
  ## 12. Limitations / known gaps
@@ -677,5 +892,10 @@ to `@softure-ai/privacy` (`privacy` flags on):
677
892
  app that wants `jwks_uri` adds it through `oauth.metadata` and serves the empty set itself).
678
893
  - Only `S256` PKCE and the `authorization_code` and `refresh_token` grants.
679
894
  - Write access is per token, not per tool; finer scopes would be a new option.
680
- - The rate limit is per client address, not per token.
681
- - `responseMode: "json"` drops mid-call notifications; a streaming tool would need a different mode.
895
+ - The MCP endpoint's rate limit is per client address, not per token (API tokens are counted per token as well).
896
+ - One API token kind per app (one prefix); the `kind` column takes more kinds without a migration of the rows.
897
+ - `responseMode: "json"` drops mid-call notifications; a streaming tool would need a different mode. Long work is an
898
+ operation instead (start, `get_operation`, `cancel_operation`).
899
+ - MCP Tasks follow the extension of revision 2026-07-28 only; the 2025-11-25 core Tasks (task-augmented requests,
900
+ `tasks/result`, `tasks/list`) are not served, and those clients use the polling tools. Tasks carry no input
901
+ requests and no `notifications/tasks`.
package/dist/cli/run.d.ts CHANGED
@@ -18,7 +18,7 @@ export interface RunMcpAccessCliOptions {
18
18
  export declare const EXIT_OK = 0;
19
19
  export declare const EXIT_FAILED = 1;
20
20
  export declare const EXIT_USAGE = 2;
21
- export declare const MCP_ACCESS_USAGE = "Usage:\n softure-mcp prune deletes expired access tokens, authorization codes, grants and abandoned clients";
21
+ export declare const MCP_ACCESS_USAGE = "Usage:\n softure-mcp prune deletes expired access tokens, authorization codes, grants, abandoned clients and used confirmations";
22
22
  /** Runs one command and answers its exit code. */
23
23
  export declare function runMcpAccessCli(options: RunMcpAccessCliOptions): Promise<number>;
24
24
  //# sourceMappingURL=run.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"AAQA,OAAO,EAA8B,KAAK,KAAK,EAAE,KAAK,aAAa,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC1H,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG3E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,sBAAsB;IACrC,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B,+BAA+B;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,oFAAoF;IACpF,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,qBAAqB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;CACvF;AAED,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,eAAO,MAAM,gBAAgB,oHAC2E,CAAC;AAOzG,kDAAkD;AAClD,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAatF"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"AAQA,OAAO,EAA8B,KAAK,KAAK,EAAE,KAAK,aAAa,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC1H,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG3E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,sBAAsB;IACrC,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B,+BAA+B;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,oFAAoF;IACpF,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,qBAAqB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;CACvF;AAED,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,eAAO,MAAM,gBAAgB,wIAC+F,CAAC;AAO7H,kDAAkD;AAClD,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAatF"}
package/dist/cli/run.js CHANGED
@@ -13,7 +13,7 @@ export const EXIT_OK = 0;
13
13
  export const EXIT_FAILED = 1;
14
14
  export const EXIT_USAGE = 2;
15
15
  export const MCP_ACCESS_USAGE = `Usage:
16
- softure-mcp prune deletes expired access tokens, authorization codes, grants and abandoned clients`;
16
+ softure-mcp prune deletes expired access tokens, authorization codes, grants, abandoned clients and used confirmations`;
17
17
  const consoleOutput = {
18
18
  log: (line) => console.log(line),
19
19
  error: (line) => console.error(line),
@@ -43,7 +43,7 @@ async function prune(options, output) {
43
43
  try {
44
44
  opened = await (options.openDatabase ?? ((settings) => openCommandDatabase(settings, { max: 1 })))(database);
45
45
  const removed = await pruneMcpAccess({ db: opened.handle.db, clock: options.clock ?? systemClock, config: options.config });
46
- output.log(`softure-mcp prune: removed ${String(removed.accessTokens)} access tokens, ${String(removed.codes)} authorization codes, ${String(removed.grants)} grants, ${String(removed.clients)} clients`);
46
+ output.log(`softure-mcp prune: removed ${String(removed.accessTokens)} access tokens, ${String(removed.codes)} authorization codes, ${String(removed.grants)} grants, ${String(removed.clients)} clients, ${String(removed.usedConfirmations)} used confirmations, ${String(removed.operationTasks)} operation tasks`);
47
47
  return EXIT_OK;
48
48
  }
49
49
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,24 @@
1
1
  import type { CoreErrorCode, Err } from "@softure-ai/core";
2
- export type McpAccessErrorCode = "mcp-access.name_required" | "mcp-access.name_too_long" | "mcp-access.token_limit_reached" | "mcp-access.token_not_found" | "mcp-access.grant_not_found";
2
+ export type McpAccessErrorCode = "mcp-access.name_required" | "mcp-access.name_too_long" | "mcp-access.token_limit_reached" | "mcp-access.token_not_found" | "mcp-access.grant_not_found" | "mcp-access.api_disabled" | "mcp-access.scope_required" | "mcp-access.scope_unknown";
3
+ /** Which door a token opens: the MCP endpoint, or the app's HTTP routes through `requireApiToken`. */
4
+ export type AccessTokenKind = "mcp" | "api";
5
+ /** Why `requireApiToken` refused a request; `apiTokenErrorResponse` turns each into its HTTP answer. */
6
+ export type ApiTokenErrorCode = "mcp-access.api_token_missing" | "mcp-access.api_token_invalid" | "mcp-access.api_scope_missing" | "mcp-access.api_writes_disabled" | "mcp-access.api_unavailable" | "security.client_unidentified" | "security.rate_limited";
7
+ /** A refusal of `requireApiToken`. */
8
+ export type ApiTokenRefusal = Err<Exclude<ApiTokenErrorCode, "security.rate_limited" | "mcp-access.api_scope_missing" | "mcp-access.api_writes_disabled">> | (Err<"mcp-access.api_scope_missing" | "mcp-access.api_writes_disabled"> & {
9
+ readonly missingScopes: readonly string[];
10
+ }) | (Err<"security.rate_limited"> & {
11
+ readonly retryAfterSeconds: number;
12
+ });
13
+ /** Who called a route with a valid API token. */
14
+ export interface ApiTokenIdentity {
15
+ /** The auth user id of the token's owner: scope every query of the route to it. */
16
+ readonly userId: string;
17
+ /** The token's id, for the route's own logs. Never the token. */
18
+ readonly tokenId: string;
19
+ /** The token's effective scopes: write scopes only while the app allows writes. */
20
+ readonly scopes: readonly string[];
21
+ }
3
22
  /** What an app's `beforeIssue` gate may answer instead of letting a token be issued. */
4
23
  export type McpIssueRefusalCode = "mcp-access.issue_refused" | "mcp-access.write_refused";
5
24
  /**
@@ -24,6 +43,9 @@ export interface McpServerIdentity {
24
43
  export interface AccessTokenView {
25
44
  readonly id: string;
26
45
  readonly name: string;
46
+ readonly kind: AccessTokenKind;
47
+ /** The app scopes of an API token; empty for an MCP token. */
48
+ readonly scopes: readonly string[];
27
49
  /** Issued with write access (it still writes only while the app allows writes). */
28
50
  readonly canWrite: boolean;
29
51
  readonly createdAt: Date;
@@ -59,7 +81,7 @@ export interface McpClientSetup {
59
81
  /** The `Authorization` header value, for clients with a header field. */
60
82
  readonly authorizationHeader: string;
61
83
  }
62
- /** A token right after it was issued: the only moment its plaintext exists outside the client. */
84
+ /** An MCP token right after it was issued: the only moment its plaintext exists outside the client. */
63
85
  export interface IssuedToken {
64
86
  readonly id: string;
65
87
  readonly name: string;
@@ -70,6 +92,25 @@ export interface IssuedToken {
70
92
  readonly expiresAt: string;
71
93
  readonly setup: McpClientSetup;
72
94
  }
95
+ /** A snippet the app gave for API tokens (`apiTokens.examples`), filled in with the new token. */
96
+ export interface ApiTokenExample {
97
+ readonly title: string;
98
+ readonly text: string;
99
+ }
100
+ /** An API token right after it was issued, for the one-time reveal. */
101
+ export interface IssuedApiTokenView {
102
+ readonly id: string;
103
+ readonly name: string;
104
+ /** The scopes granted (write scopes only with `allowWrites`). */
105
+ readonly scopes: readonly string[];
106
+ readonly expiresText: string;
107
+ readonly expiresAt: string;
108
+ /** The plaintext, shown once. */
109
+ readonly token: string;
110
+ /** `Bearer <token>`. */
111
+ readonly authorizationHeader: string;
112
+ readonly examples: readonly ApiTokenExample[];
113
+ }
73
114
  /** What the issue action returns to its form (`useActionState`). */
74
115
  export type IssueTokenFormState = {
75
116
  readonly status: "idle";
@@ -77,6 +118,11 @@ export type IssueTokenFormState = {
77
118
  readonly status: "ok";
78
119
  readonly issued: IssuedToken;
79
120
  }
121
+ /** An API token was issued (`kind=api` in the form): its one-time reveal. */
122
+ | {
123
+ readonly status: "api-ok";
124
+ readonly issued: IssuedApiTokenView;
125
+ }
80
126
  /** `message`: the app's own refusal text from the `beforeIssue` gate, shown instead of the code's copy. */
81
127
  | {
82
128
  readonly status: "error";
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}