@pithy-sh/core 0.13.0 → 0.15.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 (354) hide show
  1. package/README.md +54 -0
  2. package/dist/audit/auditEvent.js +1 -1
  3. package/dist/audit/chain.d.ts +289 -0
  4. package/dist/audit/chain.d.ts.map +1 -0
  5. package/dist/audit/chain.js +400 -0
  6. package/dist/audit/citations.d.ts +52 -0
  7. package/dist/audit/citations.d.ts.map +1 -0
  8. package/dist/audit/citations.js +13 -0
  9. package/dist/capability/bindings.d.ts +22 -0
  10. package/dist/capability/bindings.d.ts.map +1 -1
  11. package/dist/capability/bindings.js +32 -2
  12. package/dist/capability/capability.d.ts +123 -16
  13. package/dist/capability/capability.d.ts.map +1 -1
  14. package/dist/capability/capability.js +17 -2
  15. package/dist/capability/erasure.d.ts +75 -0
  16. package/dist/capability/erasure.d.ts.map +1 -0
  17. package/dist/capability/erasure.js +2 -0
  18. package/dist/capability/manifest.d.ts +5 -0
  19. package/dist/capability/manifest.d.ts.map +1 -1
  20. package/dist/capability/personColumns.d.ts +28 -0
  21. package/dist/capability/personColumns.d.ts.map +1 -0
  22. package/dist/capability/personColumns.js +66 -0
  23. package/dist/capability/personRows.d.ts +62 -0
  24. package/dist/capability/personRows.d.ts.map +1 -0
  25. package/dist/capability/personRows.js +59 -0
  26. package/dist/capability/purge.d.ts +86 -19
  27. package/dist/capability/purge.d.ts.map +1 -1
  28. package/dist/capability/purge.js +83 -5
  29. package/dist/capability/settings.d.ts +10 -0
  30. package/dist/capability/settings.d.ts.map +1 -1
  31. package/dist/capability/vectorIndex.d.ts +36 -0
  32. package/dist/capability/vectorIndex.d.ts.map +1 -0
  33. package/dist/capability/vectorIndex.js +25 -0
  34. package/dist/cli/commandCatalog.generated.d.ts +438 -3
  35. package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
  36. package/dist/cli/commandCatalog.generated.js +454 -2
  37. package/dist/cli/commandFacts.d.ts +8 -5
  38. package/dist/cli/commandFacts.d.ts.map +1 -1
  39. package/dist/cli/commandFacts.js +12 -3
  40. package/dist/consent/purpose.d.ts +70 -0
  41. package/dist/consent/purpose.d.ts.map +1 -0
  42. package/dist/consent/purpose.js +97 -0
  43. package/dist/consent/reader.d.ts +99 -0
  44. package/dist/consent/reader.d.ts.map +1 -0
  45. package/dist/consent/reader.js +33 -0
  46. package/dist/controlPlane/audit/actions.d.ts +15 -4
  47. package/dist/controlPlane/audit/actions.d.ts.map +1 -1
  48. package/dist/controlPlane/audit/actions.js +16 -5
  49. package/dist/controlPlane/capability.d.ts +10 -0
  50. package/dist/controlPlane/capability.d.ts.map +1 -1
  51. package/dist/controlPlane/capability.js +56 -8
  52. package/dist/controlPlane/context.d.ts +4 -0
  53. package/dist/controlPlane/context.d.ts.map +1 -1
  54. package/dist/controlPlane/context.js +3 -2
  55. package/dist/controlPlane/data/connection.d.ts +21 -2
  56. package/dist/controlPlane/data/connection.d.ts.map +1 -1
  57. package/dist/controlPlane/data/connection.js +13 -4
  58. package/dist/controlPlane/data/connections.d.ts +94 -0
  59. package/dist/controlPlane/data/connections.d.ts.map +1 -0
  60. package/dist/controlPlane/data/connections.js +151 -0
  61. package/dist/controlPlane/data/hold.d.ts +46 -0
  62. package/dist/controlPlane/data/hold.d.ts.map +1 -0
  63. package/dist/controlPlane/data/hold.js +36 -0
  64. package/dist/controlPlane/data/oidcTrust.d.ts +122 -0
  65. package/dist/controlPlane/data/oidcTrust.d.ts.map +1 -0
  66. package/dist/controlPlane/data/oidcTrust.js +173 -0
  67. package/dist/controlPlane/data/tables.d.ts +4 -0
  68. package/dist/controlPlane/data/tables.d.ts.map +1 -1
  69. package/dist/controlPlane/data/tables.js +9 -4
  70. package/dist/controlPlane/data/trustFirstCall.d.ts +16 -0
  71. package/dist/controlPlane/data/trustFirstCall.d.ts.map +1 -0
  72. package/dist/controlPlane/data/trustFirstCall.js +25 -0
  73. package/dist/controlPlane/discovery/adminRoute.d.ts +165 -2
  74. package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -1
  75. package/dist/controlPlane/discovery/adminRoute.js +32 -8
  76. package/dist/controlPlane/discovery/routeInput.d.ts +23 -0
  77. package/dist/controlPlane/discovery/routeInput.d.ts.map +1 -0
  78. package/dist/controlPlane/discovery/routeInput.js +80 -0
  79. package/dist/controlPlane/discovery/toolName.d.ts +21 -0
  80. package/dist/controlPlane/discovery/toolName.d.ts.map +1 -0
  81. package/dist/controlPlane/discovery/toolName.js +25 -0
  82. package/dist/controlPlane/erasure/attest.d.ts +78 -0
  83. package/dist/controlPlane/erasure/attest.d.ts.map +1 -0
  84. package/dist/controlPlane/erasure/attest.js +164 -0
  85. package/dist/controlPlane/erasure/read.d.ts +39 -0
  86. package/dist/controlPlane/erasure/read.d.ts.map +1 -0
  87. package/dist/controlPlane/erasure/read.js +140 -0
  88. package/dist/controlPlane/error/errors.d.ts +61 -2
  89. package/dist/controlPlane/error/errors.d.ts.map +1 -1
  90. package/dist/controlPlane/error/errors.js +13 -0
  91. package/dist/controlPlane/evidence/derive.d.ts +124 -0
  92. package/dist/controlPlane/evidence/derive.d.ts.map +1 -0
  93. package/dist/controlPlane/evidence/derive.js +343 -0
  94. package/dist/controlPlane/evidence/mapping.d.ts +85 -0
  95. package/dist/controlPlane/evidence/mapping.d.ts.map +1 -0
  96. package/dist/controlPlane/evidence/mapping.js +114 -0
  97. package/dist/controlPlane/evidence/pack.d.ts +46 -0
  98. package/dist/controlPlane/evidence/pack.d.ts.map +1 -0
  99. package/dist/controlPlane/evidence/pack.js +124 -0
  100. package/dist/controlPlane/http/connections.d.ts +96 -0
  101. package/dist/controlPlane/http/connections.d.ts.map +1 -0
  102. package/dist/controlPlane/http/connections.js +109 -0
  103. package/dist/controlPlane/http/guard.d.ts +23 -2
  104. package/dist/controlPlane/http/guard.d.ts.map +1 -1
  105. package/dist/controlPlane/http/guard.js +69 -12
  106. package/dist/controlPlane/http/handlers.d.ts +212 -10
  107. package/dist/controlPlane/http/handlers.d.ts.map +1 -1
  108. package/dist/controlPlane/http/handlers.js +117 -26
  109. package/dist/controlPlane/http/holds.d.ts +57 -0
  110. package/dist/controlPlane/http/holds.d.ts.map +1 -0
  111. package/dist/controlPlane/http/holds.js +98 -0
  112. package/dist/controlPlane/http/presence.d.ts +27 -0
  113. package/dist/controlPlane/http/presence.d.ts.map +1 -0
  114. package/dist/controlPlane/http/presence.js +67 -0
  115. package/dist/controlPlane/http/processing.d.ts +73 -0
  116. package/dist/controlPlane/http/processing.d.ts.map +1 -0
  117. package/dist/controlPlane/http/processing.js +17 -0
  118. package/dist/controlPlane/http/responses.d.ts +927 -3
  119. package/dist/controlPlane/http/responses.d.ts.map +1 -1
  120. package/dist/controlPlane/http/responses.js +283 -7
  121. package/dist/controlPlane/http/routes.d.ts +12 -2
  122. package/dist/controlPlane/http/routes.d.ts.map +1 -1
  123. package/dist/controlPlane/http/routes.js +182 -5
  124. package/dist/controlPlane/http/schemas.d.ts +63 -0
  125. package/dist/controlPlane/http/schemas.d.ts.map +1 -1
  126. package/dist/controlPlane/http/schemas.js +39 -1
  127. package/dist/controlPlane/http/verify.d.ts.map +1 -1
  128. package/dist/controlPlane/http/verify.js +13 -1
  129. package/dist/controlPlane/http/verifyTrust.d.ts +76 -0
  130. package/dist/controlPlane/http/verifyTrust.d.ts.map +1 -0
  131. package/dist/controlPlane/http/verifyTrust.js +75 -0
  132. package/dist/controlPlane/migrations/0004_connection_trusts.d.ts +15 -0
  133. package/dist/controlPlane/migrations/0004_connection_trusts.d.ts.map +1 -0
  134. package/dist/controlPlane/migrations/0004_connection_trusts.js +23 -0
  135. package/dist/controlPlane/migrations/0005_holds.d.ts +12 -0
  136. package/dist/controlPlane/migrations/0005_holds.d.ts.map +1 -0
  137. package/dist/controlPlane/migrations/0005_holds.js +26 -0
  138. package/dist/controlPlane/processing/derive.d.ts +77 -0
  139. package/dist/controlPlane/processing/derive.d.ts.map +1 -0
  140. package/dist/controlPlane/processing/derive.js +206 -0
  141. package/dist/controlPlane/processing/read.d.ts +10 -0
  142. package/dist/controlPlane/processing/read.d.ts.map +1 -0
  143. package/dist/controlPlane/processing/read.js +89 -0
  144. package/dist/controlPlane/scope/kind.d.ts +46 -0
  145. package/dist/controlPlane/scope/kind.d.ts.map +1 -0
  146. package/dist/controlPlane/scope/kind.js +52 -0
  147. package/dist/controlPlane/scope/scope.d.ts +58 -3
  148. package/dist/controlPlane/scope/scope.d.ts.map +1 -1
  149. package/dist/controlPlane/scope/scope.js +59 -4
  150. package/dist/controlPlane/token/jws.js +1 -1
  151. package/dist/controlPlane/token/mint.js +1 -1
  152. package/dist/controlPlane/wire.d.ts +23 -0
  153. package/dist/controlPlane/wire.d.ts.map +1 -1
  154. package/dist/controlPlane/wire.js +24 -1
  155. package/dist/createBackend.d.ts +13 -0
  156. package/dist/createBackend.d.ts.map +1 -1
  157. package/dist/createBackend.js +11 -3
  158. package/dist/createEntrypoint.d.ts +7 -0
  159. package/dist/createEntrypoint.d.ts.map +1 -1
  160. package/dist/createEntrypoint.js +4 -0
  161. package/dist/error/http.d.ts +168 -0
  162. package/dist/error/http.d.ts.map +1 -1
  163. package/dist/error/payload.d.ts +336 -0
  164. package/dist/error/payload.d.ts.map +1 -1
  165. package/dist/error/payload.js +112 -0
  166. package/dist/error/pithyError.d.ts +22 -0
  167. package/dist/error/pithyError.d.ts.map +1 -1
  168. package/dist/error/pithyError.js +31 -1
  169. package/dist/http/routeSpec.d.ts +46 -1
  170. package/dist/http/routeSpec.d.ts.map +1 -1
  171. package/dist/http/routeSpec.js +39 -5
  172. package/dist/http/verification.d.ts.map +1 -1
  173. package/dist/http/verification.js +1 -1
  174. package/dist/i18n/translator.d.ts +2 -1
  175. package/dist/i18n/translator.d.ts.map +1 -1
  176. package/dist/index.d.ts +1 -1
  177. package/dist/index.d.ts.map +1 -1
  178. package/dist/index.js +3 -3
  179. package/dist/logger/record.d.ts +98 -0
  180. package/dist/logger/record.d.ts.map +1 -1
  181. package/dist/mcp/annotations.d.ts +20 -0
  182. package/dist/mcp/annotations.d.ts.map +1 -0
  183. package/dist/mcp/annotations.js +23 -0
  184. package/dist/mcp/tools.d.ts +75 -0
  185. package/dist/mcp/tools.d.ts.map +1 -0
  186. package/dist/mcp/tools.js +118 -0
  187. package/dist/openapi/document.d.ts.map +1 -1
  188. package/dist/openapi/document.js +8 -26
  189. package/dist/openapi/operations.d.ts +35 -0
  190. package/dist/openapi/operations.d.ts.map +1 -0
  191. package/dist/openapi/operations.js +37 -0
  192. package/dist/presence/channel.d.ts +11 -0
  193. package/dist/presence/channel.d.ts.map +1 -0
  194. package/dist/presence/channel.js +65 -0
  195. package/dist/presence/declaration.d.ts +154 -0
  196. package/dist/presence/declaration.d.ts.map +1 -0
  197. package/dist/presence/declaration.js +164 -0
  198. package/dist/presence/durableObject.d.ts +61 -0
  199. package/dist/presence/durableObject.d.ts.map +1 -0
  200. package/dist/presence/durableObject.js +257 -0
  201. package/dist/presence/frames.d.ts +166 -0
  202. package/dist/presence/frames.d.ts.map +1 -0
  203. package/dist/presence/frames.js +90 -0
  204. package/dist/presence/http/guard.d.ts +11 -0
  205. package/dist/presence/http/guard.d.ts.map +1 -0
  206. package/dist/presence/http/guard.js +20 -0
  207. package/dist/presence/http/responses.d.ts +31 -0
  208. package/dist/presence/http/responses.d.ts.map +1 -0
  209. package/dist/presence/http/responses.js +34 -0
  210. package/dist/presence/http/routes.d.ts +24 -0
  211. package/dist/presence/http/routes.d.ts.map +1 -0
  212. package/dist/presence/http/routes.js +127 -0
  213. package/dist/presence/http/schemas.d.ts +29 -0
  214. package/dist/presence/http/schemas.d.ts.map +1 -0
  215. package/dist/presence/http/schemas.js +28 -0
  216. package/dist/presence/names.d.ts +23 -0
  217. package/dist/presence/names.d.ts.map +1 -0
  218. package/dist/presence/names.js +22 -0
  219. package/dist/presence/room.d.ts +95 -0
  220. package/dist/presence/room.d.ts.map +1 -0
  221. package/dist/presence/room.js +105 -0
  222. package/dist/presence/ticket.d.ts +55 -0
  223. package/dist/presence/ticket.d.ts.map +1 -0
  224. package/dist/presence/ticket.js +109 -0
  225. package/dist/privacy/classify.js +1 -1
  226. package/dist/privacy/encrypted.js +1 -1
  227. package/dist/processing/activity.d.ts +134 -0
  228. package/dist/processing/activity.d.ts.map +1 -0
  229. package/dist/processing/activity.js +161 -0
  230. package/dist/processing/composed.d.ts +18 -0
  231. package/dist/processing/composed.d.ts.map +1 -0
  232. package/dist/processing/composed.js +28 -0
  233. package/dist/processing/vocabulary.d.ts +26 -0
  234. package/dist/processing/vocabulary.d.ts.map +1 -0
  235. package/dist/processing/vocabulary.js +35 -0
  236. package/dist/queue/consumer.d.ts +59 -0
  237. package/dist/queue/consumer.d.ts.map +1 -0
  238. package/dist/queue/consumer.js +46 -0
  239. package/dist/retention/composed.d.ts +37 -0
  240. package/dist/retention/composed.d.ts.map +1 -0
  241. package/dist/retention/composed.js +66 -0
  242. package/dist/retention/holds.d.ts +61 -0
  243. package/dist/retention/holds.d.ts.map +1 -0
  244. package/dist/retention/holds.js +107 -0
  245. package/dist/retention/holdsPurge.d.ts +22 -0
  246. package/dist/retention/holdsPurge.d.ts.map +1 -0
  247. package/dist/retention/holdsPurge.js +50 -0
  248. package/dist/retention/policy.d.ts +132 -0
  249. package/dist/retention/policy.d.ts.map +1 -0
  250. package/dist/retention/policy.js +197 -0
  251. package/dist/session/rotated.d.ts +34 -0
  252. package/dist/session/rotated.d.ts.map +1 -0
  253. package/dist/session/rotated.js +25 -0
  254. package/dist/test-utils/connectionFixtures.d.ts +13 -0
  255. package/dist/test-utils/connectionFixtures.d.ts.map +1 -0
  256. package/dist/test-utils/connectionFixtures.js +58 -0
  257. package/dist/test-utils/interleave.d.ts +12 -0
  258. package/dist/test-utils/interleave.d.ts.map +1 -0
  259. package/dist/test-utils/interleave.js +26 -0
  260. package/dist/testWorker.d.ts +8 -0
  261. package/dist/testWorker.d.ts.map +1 -0
  262. package/dist/testWorker.js +9 -0
  263. package/dist/version.generated.d.ts +1 -1
  264. package/dist/version.generated.js +1 -1
  265. package/dist/workflow/dispatchRoute.js +1 -1
  266. package/package.json +1 -1
  267. package/src/audit/auditEvent.ts +1 -1
  268. package/src/audit/chain.ts +551 -0
  269. package/src/audit/citations.ts +63 -0
  270. package/src/capability/bindings.ts +61 -0
  271. package/src/capability/capability.ts +156 -19
  272. package/src/capability/erasure.ts +82 -0
  273. package/src/capability/personColumns.ts +83 -0
  274. package/src/capability/personRows.ts +125 -0
  275. package/src/capability/purge.ts +158 -21
  276. package/src/capability/settings.ts +7 -0
  277. package/src/capability/vectorIndex.ts +60 -0
  278. package/src/cli/commandCatalog.generated.ts +487 -2
  279. package/src/cli/commandFacts.ts +12 -3
  280. package/src/cloudflare-test.d.ts +3 -1
  281. package/src/consent/purpose.ts +160 -0
  282. package/src/consent/reader.ts +134 -0
  283. package/src/controlPlane/audit/actions.ts +15 -4
  284. package/src/controlPlane/capability.ts +99 -20
  285. package/src/controlPlane/context.ts +10 -2
  286. package/src/controlPlane/data/connection.ts +16 -2
  287. package/src/controlPlane/data/connections.ts +231 -0
  288. package/src/controlPlane/data/hold.ts +52 -0
  289. package/src/controlPlane/data/oidcTrust.ts +249 -0
  290. package/src/controlPlane/data/tables.ts +7 -0
  291. package/src/controlPlane/data/trustFirstCall.ts +37 -0
  292. package/src/controlPlane/discovery/adminRoute.ts +71 -9
  293. package/src/controlPlane/discovery/routeInput.ts +86 -0
  294. package/src/controlPlane/discovery/toolName.ts +23 -0
  295. package/src/controlPlane/erasure/attest.ts +205 -0
  296. package/src/controlPlane/erasure/read.ts +169 -0
  297. package/src/controlPlane/error/errors.ts +69 -2
  298. package/src/controlPlane/evidence/derive.ts +426 -0
  299. package/src/controlPlane/evidence/mapping.ts +118 -0
  300. package/src/controlPlane/evidence/pack.ts +190 -0
  301. package/src/controlPlane/http/connections.ts +129 -0
  302. package/src/controlPlane/http/guard.ts +118 -9
  303. package/src/controlPlane/http/handlers.ts +172 -40
  304. package/src/controlPlane/http/holds.ts +108 -0
  305. package/src/controlPlane/http/presence.ts +71 -0
  306. package/src/controlPlane/http/processing.ts +19 -0
  307. package/src/controlPlane/http/responses.ts +590 -7
  308. package/src/controlPlane/http/routes.ts +302 -3
  309. package/src/controlPlane/http/schemas.ts +126 -0
  310. package/src/controlPlane/http/verify.ts +19 -2
  311. package/src/controlPlane/http/verifyTrust.ts +176 -0
  312. package/src/controlPlane/migrations/0004_connection_trusts.ts +30 -0
  313. package/src/controlPlane/migrations/0005_holds.ts +40 -0
  314. package/src/controlPlane/processing/derive.ts +359 -0
  315. package/src/controlPlane/processing/read.ts +102 -0
  316. package/src/controlPlane/scope/kind.ts +63 -0
  317. package/src/controlPlane/scope/scope.ts +65 -3
  318. package/src/controlPlane/wire.ts +26 -0
  319. package/src/createBackend.ts +31 -6
  320. package/src/createEntrypoint.ts +17 -0
  321. package/src/error/payload.ts +234 -0
  322. package/src/error/pithyError.ts +42 -0
  323. package/src/http/routeSpec.ts +75 -5
  324. package/src/http/verification.ts +3 -1
  325. package/src/i18n/translator.ts +2 -1
  326. package/src/index.ts +2 -0
  327. package/src/mcp/annotations.ts +38 -0
  328. package/src/mcp/tools.ts +220 -0
  329. package/src/openapi/document.ts +8 -28
  330. package/src/openapi/operations.ts +62 -0
  331. package/src/presence/channel.ts +46 -0
  332. package/src/presence/declaration.ts +283 -0
  333. package/src/presence/durableObject.ts +287 -0
  334. package/src/presence/frames.ts +153 -0
  335. package/src/presence/http/guard.ts +23 -0
  336. package/src/presence/http/responses.ts +63 -0
  337. package/src/presence/http/routes.ts +165 -0
  338. package/src/presence/http/schemas.ts +39 -0
  339. package/src/presence/names.ts +45 -0
  340. package/src/presence/room.ts +145 -0
  341. package/src/presence/ticket.ts +145 -0
  342. package/src/processing/activity.ts +289 -0
  343. package/src/processing/composed.ts +33 -0
  344. package/src/processing/vocabulary.ts +43 -0
  345. package/src/queue/consumer.ts +104 -0
  346. package/src/retention/composed.ts +75 -0
  347. package/src/retention/holds.ts +175 -0
  348. package/src/retention/holdsPurge.ts +50 -0
  349. package/src/retention/policy.ts +305 -0
  350. package/src/session/rotated.ts +53 -0
  351. package/src/test-utils/connectionFixtures.ts +58 -0
  352. package/src/test-utils/interleave.ts +31 -0
  353. package/src/testWorker.ts +13 -0
  354. package/src/version.generated.ts +1 -1
@@ -5,17 +5,30 @@ import type { Hono } from "hono";
5
5
  import type { PithyHonoEnv } from "../../capability/capability";
6
6
  import { describeRoute } from "../../http/routeSpec";
7
7
  import { validate } from "../../http/validation";
8
- import { type AdminRoute, ControlPlaneManifest } from "../discovery/adminRoute";
8
+ import { PresenceTicket } from "../../presence/http/responses";
9
+ import { ControlPlanePresenceTicketRequest } from "../../presence/http/schemas";
10
+ import { type AdminRouteDeclaration, ControlPlaneManifest } from "../discovery/adminRoute";
9
11
  import {
10
12
  ANY_VERIFIED_CALLER,
13
+ CONNECTIONS_READ_SCOPE,
14
+ CONNECTIONS_REVOKE_SCOPE,
15
+ ERASURE_READ_SCOPE,
16
+ EVIDENCE_READ_SCOPE,
11
17
  KEYS_ROTATE_SCOPE,
12
18
  MANIFEST_READ_SCOPE,
13
19
  MIGRATIONS_READ_SCOPE,
14
20
  PRIVACY_READ_SCOPE,
21
+ PROCESSING_READ_SCOPE,
22
+ RETENTION_HOLD_SCOPE,
23
+ RETENTION_READ_SCOPE,
15
24
  } from "../scope/scope";
25
+ import { listConnectionsHandler, revokeConnectionHandler } from "./connections";
16
26
  import { requireControlPlane } from "./guard";
17
27
  import {
18
28
  type ControlPlaneHandlerDeps,
29
+ erasurePeriodHandler,
30
+ erasureSubjectHandler,
31
+ evidenceHandler,
19
32
  expireKeyHandler,
20
33
  listKeysHandler,
21
34
  manifestHandler,
@@ -24,15 +37,39 @@ import {
24
37
  privacyHandler,
25
38
  registerKeyHandler,
26
39
  } from "./handlers";
40
+ import { listHoldsHandler, placeHoldHandler, releaseHoldHandler } from "./holds";
41
+ import { presenceTicketGate, presenceTicketHandler } from "./presence";
42
+ import { processingHandler } from "./processing";
27
43
  import {
44
+ ControlPlaneConnectionsResponse,
45
+ ControlPlaneErasurePeriodResponse,
46
+ ControlPlaneErasureSubjectResponse,
47
+ ControlPlaneEvidenceResponse,
48
+ ControlPlaneHoldResponse,
49
+ ControlPlaneHoldsResponse,
28
50
  ControlPlaneKeysResponse,
29
51
  ControlPlaneMigrationsResponse,
30
52
  ControlPlanePingResponse,
31
53
  ControlPlanePrivacyResponse,
54
+ ControlPlaneProcessingResponse,
32
55
  ExpireKeyResponse,
33
56
  RegisterKeyResponse,
57
+ RevokeConnectionResponse,
34
58
  } from "./responses";
35
- import { ExpireKeyParams, ExpireKeyRequest, RegisterKeyRequest } from "./schemas";
59
+ import {
60
+ ConnectionListQuery,
61
+ ConnectionParams,
62
+ ErasurePeriodQuery,
63
+ ErasureSubjectParams,
64
+ ExpireKeyParams,
65
+ ExpireKeyRequest,
66
+ HoldListQuery,
67
+ HoldParams,
68
+ PlaceHoldRequest,
69
+ RegisterKeyRequest,
70
+ ReleaseHoldRequest,
71
+ RevokeConnectionRequest,
72
+ } from "./schemas";
36
73
 
37
74
  /**
38
75
  * The seam's own routes, distinct from the admin routes capabilities contribute.
@@ -42,8 +79,18 @@ import { ExpireKeyParams, ExpireKeyRequest, RegisterKeyRequest } from "./schemas
42
79
  * GET /control-plane/keys → the registration state (keys:rotate)
43
80
  * POST /control-plane/keys → register a key (keys:rotate)
44
81
  * POST /control-plane/keys/:keyId/expire → retire a superseded key (keys:rotate)
82
+ * GET /control-plane/connections → every connection, by principal (connections:read)
83
+ * POST /control-plane/connections/:id/revoke → revoke another connection (connections:revoke)
45
84
  * GET /control-plane/migrations → where each database stands (migrations:read)
46
85
  * GET /control-plane/privacy → what each schema holds (privacy:read)
86
+ * GET /control-plane/evidence → SOC 2 and ISO 27001 evidence (evidence:read)
87
+ * GET /control-plane/processing → the Article 30 record (processing:read)
88
+ * GET /control-plane/erasure → every purge in a period (erasure:read)
89
+ * GET /control-plane/erasure/:type/:id → one subject's purge, attested (erasure:read)
90
+ * GET /control-plane/holds → every legal hold (retention:read)
91
+ * POST /control-plane/holds → place a legal hold (retention:hold)
92
+ * POST /control-plane/holds/:id/release → release one, with a reason (retention:hold)
93
+ * POST /control-plane/presence/tickets → a ticket into a presence room (the room kind's scope)
47
94
  *
48
95
  * Every one is `control-plane` and default-denied — with no connection registered they all answer 403,
49
96
  * and that is the shipped state of a Worker nobody has connected.
@@ -70,52 +117,155 @@ import { ExpireKeyParams, ExpireKeyRequest, RegisterKeyRequest } from "./schemas
70
117
  * `ping` carries a null scope. It requires a verified caller and no authorization, and saying so is
71
118
  * how a client knows it can always prove a key — including on a connection granted nothing at all.
72
119
  */
73
- export function controlPlaneRouteDescriptors(basePath: string): AdminRoute[] {
120
+ export function controlPlaneRouteDescriptors(basePath: string): AdminRouteDeclaration[] {
74
121
  return [
75
122
  {
76
123
  method: "GET",
124
+ operation: "ping",
77
125
  path: `${basePath}/ping`,
78
126
  scope: null,
79
127
  summary: "Prove connectivity and which key answered. Always available to a verified caller.",
80
128
  },
81
129
  {
82
130
  method: "GET",
131
+ operation: "manifest.read",
83
132
  path: `${basePath}/manifest`,
84
133
  scope: MANIFEST_READ_SCOPE,
134
+ kind: "reads",
85
135
  summary: "What this Worker composes, and how to call each capability's admin surface.",
86
136
  },
87
137
  {
88
138
  method: "GET",
139
+ operation: "keys.list",
89
140
  path: `${basePath}/keys`,
90
141
  scope: KEYS_ROTATE_SCOPE,
142
+ kind: "credentials",
91
143
  summary: "The registered keys, their validity windows, and their ages.",
92
144
  },
93
145
  {
94
146
  method: "POST",
147
+ operation: "keys.register",
95
148
  path: `${basePath}/keys`,
96
149
  scope: KEYS_ROTATE_SCOPE,
150
+ kind: "credentials",
97
151
  summary: "Register a successor public key. Signed with the key it replaces; appends only.",
98
152
  },
99
153
  {
100
154
  method: "POST",
155
+ operation: "keys.expire",
101
156
  path: `${basePath}/keys/:keyId/expire`,
102
157
  scope: KEYS_ROTATE_SCOPE,
158
+ kind: "credentials",
103
159
  summary: "Retire a superseded key. Must be signed with the successor it names as proven.",
104
160
  },
105
161
  {
106
162
  method: "GET",
163
+ operation: "connections.list",
164
+ path: `${basePath}/connections`,
165
+ scope: CONNECTIONS_READ_SCOPE,
166
+ kind: "reads",
167
+ summary:
168
+ "Every connection bound to this Worker's environment: who authorized it, what kind of client it is for and why, its credentials, grant and trust constraints, and when it was registered or revoked. Filterable by principal. No key material.",
169
+ },
170
+ {
171
+ method: "POST",
172
+ operation: "connections.revoke",
173
+ path: `${basePath}/connections/:connectionId/revoke`,
174
+ scope: CONNECTIONS_REVOKE_SCOPE,
175
+ kind: "credentials",
176
+ summary:
177
+ "Revoke every key and trust another connection in this environment holds, with a reason. Never the calling connection, which ends through disconnect.",
178
+ },
179
+ {
180
+ method: "GET",
181
+ operation: "migrations.read",
107
182
  path: `${basePath}/migrations`,
108
183
  scope: MIGRATIONS_READ_SCOPE,
184
+ kind: "reads",
109
185
  summary:
110
186
  "What each bound database has applied, what is pending, what is undeclared, and which run put it there. Undeclared is relative to this Worker.",
111
187
  },
112
188
  {
113
189
  method: "GET",
190
+ operation: "privacy.read",
114
191
  path: `${basePath}/privacy`,
115
192
  scope: PRIVACY_READ_SCOPE,
193
+ kind: "reads",
116
194
  summary:
117
195
  "Every stored schema holding a privacy-marked field, each field's path and class, and where it lives. Derived from the running code.",
118
196
  },
197
+ {
198
+ method: "GET",
199
+ operation: "evidence.read",
200
+ path: `${basePath}/evidence`,
201
+ scope: EVIDENCE_READ_SCOPE,
202
+ kind: "reads",
203
+ summary:
204
+ "SOC 2 and ISO 27001 evidence derived from what this Worker composes and records: each control's facts, their sources, and what could be said. Evidence for an auditor, never a judgment.",
205
+ },
206
+ {
207
+ method: "GET",
208
+ operation: "processing.read",
209
+ path: `${basePath}/processing`,
210
+ scope: PROCESSING_READ_SCOPE,
211
+ kind: "reads",
212
+ summary:
213
+ "The record of processing activities, GDPR Article 30: each purpose, its data classes, legal basis, recipients, retention and measures, and every gap named.",
214
+ },
215
+ {
216
+ method: "GET",
217
+ operation: "erasure.list",
218
+ path: `${basePath}/erasure`,
219
+ scope: ERASURE_READ_SCOPE,
220
+ kind: "reads",
221
+ summary:
222
+ "Every purge in a period: how many completed, are in progress or failed, and each failed or overdue one with the step it stopped at.",
223
+ },
224
+ {
225
+ method: "GET",
226
+ operation: "erasure.read",
227
+ path: `${basePath}/erasure/:subjectType/:subjectId`,
228
+ scope: ERASURE_READ_SCOPE,
229
+ kind: "reads",
230
+ summary:
231
+ "One user's or organization's erasure: the deletion, every step erased or kept with its reason, the state, and the audit events that record each.",
232
+ },
233
+ {
234
+ method: "GET",
235
+ operation: "holds.list",
236
+ path: `${basePath}/holds`,
237
+ scope: RETENTION_READ_SCOPE,
238
+ kind: "reads",
239
+ summary:
240
+ "Every legal hold, standing first: its subject, who placed it and why, and when and why it was released.",
241
+ },
242
+ {
243
+ method: "POST",
244
+ operation: "holds.place",
245
+ path: `${basePath}/holds`,
246
+ scope: RETENTION_HOLD_SCOPE,
247
+ kind: "writes",
248
+ summary:
249
+ "Place a legal hold on a user, an organization or a tenant's audit trail, with a reason. Every purge, prune and inactivity deletion touching it stops until it is released.",
250
+ },
251
+ {
252
+ method: "POST",
253
+ operation: "holds.release",
254
+ path: `${basePath}/holds/:holdId/release`,
255
+ scope: RETENTION_HOLD_SCOPE,
256
+ kind: "writes",
257
+ summary: "Release a legal hold, with a reason. The subject returns to the retention the Worker deploys with.",
258
+ },
259
+ {
260
+ method: "POST",
261
+ operation: "presence.tickets.mint",
262
+ path: `${basePath}/presence/tickets`,
263
+ // Null because no one scope covers it: the call needs the scope of the room kind its body names, which
264
+ // each kind states in the manifest. Its gate resolves that scope from the body and verifies against it.
265
+ scope: null,
266
+ summary:
267
+ "Mint a single-use ticket into one presence room. Needs the room kind's own scope, which the manifest lists beside each kind.",
268
+ },
119
269
  ];
120
270
  }
121
271
 
@@ -187,6 +337,37 @@ export function registerControlPlaneRoutes(
187
337
  (c) => expireKeyHandler(deps)(c, c.req.valid("param"), c.req.valid("json")),
188
338
  );
189
339
 
340
+ app.get(
341
+ `${basePath}/connections`,
342
+ describeRoute({
343
+ summary: "List every connection in this Worker's environment.",
344
+ description:
345
+ "Every connection bound to this Worker's environment, the caller's own among them; a database shared with another environment never lists that environment's rows. Each connection's id, environment, actor kind, purpose, authorizing principal, credential kind, granted scopes, every trust's subject constraint, whether it is live, and when it was registered and revoked. `principal` narrows to the connections one email authorized; connections registered before principals were recorded are listed apart in `unattributed` and never match. No key material and no secret. Read-only.",
346
+ verification: "control-plane",
347
+ scopes: [CONNECTIONS_READ_SCOPE],
348
+ responses: { 200: ControlPlaneConnectionsResponse },
349
+ }),
350
+ requireControlPlane(CONNECTIONS_READ_SCOPE),
351
+ validate("query", ConnectionListQuery),
352
+ (c) => listConnectionsHandler(deps)(c, c.req.valid("query")),
353
+ );
354
+
355
+ app.post(
356
+ `${basePath}/connections/:connectionId/revoke`,
357
+ describeRoute({
358
+ summary: "Revoke another connection.",
359
+ description:
360
+ "Revokes every key and trust on the connection not yet revoked, at once, with a reason: a live one, and one whose window has not opened yet. A token already minted under a trust stays usable until it expires. Refused on the calling connection, which ends through `pithy dashboard disconnect`, naming its environment with `--env`. A connection bound to another environment answers 404. `pithy dashboard connect` keeps one connection per environment, replacing the one stored, so this route has a target only where the database holds more than one connection for this environment. Audited as `controlplane/connection_revoked`.",
361
+ verification: "control-plane",
362
+ scopes: [CONNECTIONS_REVOKE_SCOPE],
363
+ responses: { 200: RevokeConnectionResponse },
364
+ }),
365
+ requireControlPlane(CONNECTIONS_REVOKE_SCOPE),
366
+ validate("param", ConnectionParams),
367
+ validate("json", RevokeConnectionRequest),
368
+ (c) => revokeConnectionHandler(deps)(c, c.req.valid("param"), c.req.valid("json")),
369
+ );
370
+
190
371
  app.get(
191
372
  `${basePath}/migrations`,
192
373
  describeRoute({
@@ -214,4 +395,122 @@ export function registerControlPlaneRoutes(
214
395
  requireControlPlane(PRIVACY_READ_SCOPE),
215
396
  privacyHandler(deps),
216
397
  );
398
+
399
+ app.get(
400
+ `${basePath}/evidence`,
401
+ describeRoute({
402
+ summary: "Report the evidence this Worker derives for SOC 2 and ISO 27001.",
403
+ description:
404
+ "Seven controls, each with its SOC 2 and ISO/IEC 27001:2022 references, the facts read, the declaration each came from, and `evidenced`, `gap` or `not-derivable` with the reason. Derived from what this Worker composes and records. Evidence for an auditor to weigh, never a judgment. A fact another read keeps behind its own scope stays behind it. Read-only.",
405
+ verification: "control-plane",
406
+ scopes: [EVIDENCE_READ_SCOPE],
407
+ responses: { 200: ControlPlaneEvidenceResponse },
408
+ }),
409
+ requireControlPlane(EVIDENCE_READ_SCOPE),
410
+ evidenceHandler(deps),
411
+ );
412
+
413
+ app.get(
414
+ `${basePath}/processing`,
415
+ describeRoute({
416
+ summary: "Report the record of processing activities.",
417
+ description:
418
+ "Every processing activity the composed capabilities and the `processing` block declare: its purpose, the data classes it uses cross-checked against the privacy inventory, its legal basis or a gap, a consent basis's purpose and grant counts, its recipients with what each receives, its effective retention, and the evidence pack's controls. Every gap is named and none is filled in. It describes the processing and changes none of it. Read-only.",
419
+ verification: "control-plane",
420
+ scopes: [PROCESSING_READ_SCOPE],
421
+ responses: { 200: ControlPlaneProcessingResponse },
422
+ }),
423
+ requireControlPlane(PROCESSING_READ_SCOPE),
424
+ processingHandler(deps),
425
+ );
426
+
427
+ app.get(
428
+ `${basePath}/erasure`,
429
+ describeRoute({
430
+ summary: "Report every purge in a period.",
431
+ description:
432
+ "Every user and organization purge that came due, ran a step, failed or completed in the window: counts of scheduled, in progress, complete, failed and overdue, and each failed or overdue one named with the step it stopped at. The window defaults to the 90 days before `until`, and is at most 366 days. Read-only.",
433
+ verification: "control-plane",
434
+ scopes: [ERASURE_READ_SCOPE],
435
+ responses: { 200: ControlPlaneErasurePeriodResponse },
436
+ }),
437
+ requireControlPlane(ERASURE_READ_SCOPE),
438
+ validate("query", ErasurePeriodQuery),
439
+ (c) => erasurePeriodHandler(deps)(c, c.req.valid("query")),
440
+ );
441
+
442
+ app.get(
443
+ `${basePath}/erasure/:subjectType/:subjectId`,
444
+ describeRoute({
445
+ summary: "Attest one subject's erasure.",
446
+ description:
447
+ "For one user or organization: when it was deleted, by which kind of actor and why, soft or hard, every purge step with its capability, whether it erased or kept the rows and why, and when it finished, the purge's state, and the audit events that record each step by event id and chain position. Nothing about the subject beyond its id. Read-only.",
448
+ verification: "control-plane",
449
+ scopes: [ERASURE_READ_SCOPE],
450
+ responses: { 200: ControlPlaneErasureSubjectResponse },
451
+ }),
452
+ requireControlPlane(ERASURE_READ_SCOPE),
453
+ validate("param", ErasureSubjectParams),
454
+ (c) => erasureSubjectHandler(deps)(c, c.req.valid("param")),
455
+ );
456
+
457
+ app.get(
458
+ `${basePath}/holds`,
459
+ describeRoute({
460
+ summary: "List the legal holds.",
461
+ description:
462
+ "Every hold, standing first and newest first within each: the subject, who placed it and why, and when, by whom and why it was released. `active` counts the holds standing. Read-only.",
463
+ verification: "control-plane",
464
+ scopes: [RETENTION_READ_SCOPE],
465
+ responses: { 200: ControlPlaneHoldsResponse },
466
+ }),
467
+ requireControlPlane(RETENTION_READ_SCOPE),
468
+ validate("query", HoldListQuery),
469
+ (c) => listHoldsHandler(deps)(c, c.req.valid("query")),
470
+ );
471
+
472
+ app.post(
473
+ `${basePath}/holds`,
474
+ describeRoute({
475
+ summary: "Place a legal hold.",
476
+ description:
477
+ "Holds one user, one organization, or one tenant's audit trail, with a reason. While it stands no purge, prune or inactivity deletion touches the subject, and a purge already running stops at its next step. Audited. A hold can only preserve: nothing here shortens a retention.",
478
+ verification: "control-plane",
479
+ scopes: [RETENTION_HOLD_SCOPE],
480
+ responses: { 201: ControlPlaneHoldResponse },
481
+ }),
482
+ requireControlPlane(RETENTION_HOLD_SCOPE),
483
+ validate("json", PlaceHoldRequest),
484
+ (c) => placeHoldHandler(deps)(c, c.req.valid("json")),
485
+ );
486
+
487
+ app.post(
488
+ `${basePath}/holds/:holdId/release`,
489
+ describeRoute({
490
+ summary: "Release a legal hold.",
491
+ description:
492
+ "Ends one standing hold, with a reason, and keeps it as a record. The subject returns to the Worker's retention; a purge it stopped resumes on the next daily pass. Audited.",
493
+ verification: "control-plane",
494
+ scopes: [RETENTION_HOLD_SCOPE],
495
+ responses: { 200: ControlPlaneHoldResponse },
496
+ }),
497
+ requireControlPlane(RETENTION_HOLD_SCOPE),
498
+ validate("param", HoldParams),
499
+ validate("json", ReleaseHoldRequest),
500
+ (c) => releaseHoldHandler(deps)(c, c.req.valid("param"), c.req.valid("json")),
501
+ );
502
+
503
+ app.post(
504
+ `${basePath}/presence/tickets`,
505
+ describeRoute({
506
+ summary: "Mint a presence ticket for a management client.",
507
+ description:
508
+ "Admits the participant the body names to one room of any kind this Worker composes. Sign the call with the room kind's own scope, as the manifest lists it; presence adds no scope. Single use, for 60 seconds. Open the socket at `socketPath`, offering `protocol` and `pithy.ticket.<ticket>` in `Sec-WebSocket-Protocol`.",
509
+ verification: "control-plane",
510
+ responses: { 201: PresenceTicket },
511
+ }),
512
+ presenceTicketGate(deps),
513
+ validate("json", ControlPlanePresenceTicketRequest),
514
+ (c) => presenceTicketHandler(deps)(c, c.req.valid("json")),
515
+ );
217
516
  }
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { z } from "zod";
5
5
  import { Ed25519PublicJwk } from "../data/connection";
6
+ import { HoldState, HoldSubjectType } from "../data/hold";
6
7
 
7
8
  /**
8
9
  * The request contract for the seam's own routes. Every input surface is validated here and read in a
@@ -68,3 +69,128 @@ export const ExpireKeyRequest = z
68
69
  "A request to expire one superseded key. The successor must be named and must already be live — append, verify, then expire, never replace.",
69
70
  );
70
71
  export type ExpireKeyRequest = z.infer<typeof ExpireKeyRequest>;
72
+
73
+ /** `GET /control-plane/erasure/:subjectType/:subjectId` — which subject. */
74
+ export const ErasureSubjectParams = z
75
+ .object({
76
+ subjectType: z.enum(["user", "organization"]).describe("The kind of subject: `user` or `organization`."),
77
+ subjectId: z.string().min(1).max(255).describe("The subject's id."),
78
+ })
79
+ .describe("The subject whose erasure to attest.");
80
+ export type ErasureSubjectParams = z.infer<typeof ErasureSubjectParams>;
81
+
82
+ /** `GET /control-plane/erasure?since&until` — the period. */
83
+ export const ErasurePeriodQuery = z
84
+ .object({
85
+ since: z.iso
86
+ .datetime({ offset: true })
87
+ .optional()
88
+ .describe("The window's inclusive start, ISO-8601. Defaults to 90 days before `until`."),
89
+ until: z.iso
90
+ .datetime({ offset: true })
91
+ .optional()
92
+ .describe("The window's inclusive end, ISO-8601. Defaults to now."),
93
+ })
94
+ .describe("The period to attest: at most 366 days.");
95
+ export type ErasurePeriodQuery = z.infer<typeof ErasurePeriodQuery>;
96
+
97
+ /**
98
+ * Place a legal hold. The subject and a reason, and nothing else: no field shortens a retention, because a hold
99
+ * can only preserve.
100
+ */
101
+ export const PlaceHoldRequest = z
102
+ .object({
103
+ subjectType: HoldSubjectType,
104
+ subjectId: z
105
+ .string()
106
+ .min(1)
107
+ .max(255)
108
+ .nullable()
109
+ .describe(
110
+ "The user's or organization's id, or the tenant whose audit trail to hold. Null only for `audit`, which then holds the untenanted trail.",
111
+ ),
112
+ reason: z
113
+ .string()
114
+ .trim()
115
+ .min(1)
116
+ .max(500)
117
+ .describe(
118
+ "Why the hold is placed, in one or two sentences. Required, and recorded with the hold and its audit event.",
119
+ ),
120
+ })
121
+ .strict()
122
+ .refine((body) => body.subjectId !== null || body.subjectType === "audit", {
123
+ message: "Only an audit hold may name no subject: a user or organization hold names its id.",
124
+ path: ["subjectId"],
125
+ })
126
+ .describe("A legal hold to place: the subject, and why.");
127
+ export type PlaceHoldRequest = z.infer<typeof PlaceHoldRequest>;
128
+
129
+ /** The path parameter of the release route. */
130
+ export const HoldParams = z
131
+ .object({ holdId: z.string().min(1).max(64).describe("The hold's id, as placing it returned.") })
132
+ .describe("The hold to release.");
133
+ export type HoldParams = z.infer<typeof HoldParams>;
134
+
135
+ /** Release a legal hold, with a reason. */
136
+ export const ReleaseHoldRequest = z
137
+ .object({
138
+ reason: z
139
+ .string()
140
+ .trim()
141
+ .min(1)
142
+ .max(500)
143
+ .describe("Why the hold is released. Required, and recorded with the release and its audit event."),
144
+ })
145
+ .strict()
146
+ .describe("A legal hold's release: why it ends.");
147
+ export type ReleaseHoldRequest = z.infer<typeof ReleaseHoldRequest>;
148
+
149
+ /** Which holds to list. */
150
+ export const HoldListQuery = z
151
+ .object({
152
+ state: HoldState.optional().describe("Only holds in this state. Absent lists both, standing first."),
153
+ limit: z.coerce
154
+ .number()
155
+ .int()
156
+ .min(1)
157
+ .max(500)
158
+ .optional()
159
+ .describe("How many holds to list, at most 500. Defaults to 100."),
160
+ })
161
+ .describe("Which legal holds to list.");
162
+ export type HoldListQuery = z.infer<typeof HoldListQuery>;
163
+
164
+ /** Which connections to list. */
165
+ export const ConnectionListQuery = z
166
+ .object({
167
+ principal: z
168
+ .email()
169
+ .max(254)
170
+ .optional()
171
+ .describe(
172
+ "Only connections this email authorized, compared without regard to case. Connections with no recorded principal are listed apart either way and never match.",
173
+ ),
174
+ })
175
+ .describe("Which connections to list.");
176
+ export type ConnectionListQuery = z.infer<typeof ConnectionListQuery>;
177
+
178
+ /** The path parameter of the revoke route. */
179
+ export const ConnectionParams = z
180
+ .object({ connectionId: z.uuid().describe("The connection's id, as the listing reports it.") })
181
+ .describe("The connection to revoke.");
182
+ export type ConnectionParams = z.infer<typeof ConnectionParams>;
183
+
184
+ /** Revoke a connection, with a reason. */
185
+ export const RevokeConnectionRequest = z
186
+ .object({
187
+ reason: z
188
+ .string()
189
+ .trim()
190
+ .min(1)
191
+ .max(500)
192
+ .describe("Why the connection is revoked. Required, and recorded on the revocation's audit event."),
193
+ })
194
+ .strict()
195
+ .describe("A connection's revocation: why it ends.");
196
+ export type RevokeConnectionRequest = z.infer<typeof RevokeConnectionRequest>;
@@ -158,13 +158,30 @@ export async function verifyControlPlaneCall(
158
158
  await verifyBodyDigest(call.body, claims.bodySha256);
159
159
 
160
160
  // 11. Scope. Both sides must agree: what the adopter granted, and what this one token was minted for.
161
+ // A signature proves who minted the token, not who presented it, and a token binds no path: one captured
162
+ // token can be presented at every route it does not cover. So the refusal spends the jti, and names the
163
+ // caller only when this presentation was the first (#812). A replay gets the same refusal and names nobody.
164
+ // Spending costs the caller nothing — the call is refused either way — and it still happens only after
165
+ // step 7, so a forgery cannot burn a jti it merely observed.
161
166
  if (!scopeCovers(call.requirement, claims.scope, connection.scopes)) {
167
+ const firstPresentation = await deps.replay.claim(claims.jti, connection.id);
168
+ const spent = firstPresentation ? "" : `; jti ${claims.jti} was already spent`;
162
169
  throw new ControlPlaneInsufficientScopeError({
163
- detail: `required ${String(call.requirement)}; token carried ${claims.scope}; connection grants [${connection.scopes.join(", ")}]`,
170
+ detail: `required ${String(call.requirement)}; token carried ${claims.scope}; connection grants [${connection.scopes.join(", ")}]${spent}`,
171
+ ...(firstPresentation
172
+ ? {
173
+ caller: {
174
+ connectionId: connection.id,
175
+ subject: claims.sub,
176
+ keyId: key.keyId,
177
+ actorKind: connection.actorKind,
178
+ },
179
+ }
180
+ : {}),
164
181
  });
165
182
  }
166
183
 
167
- // 12. Spend the token. Last, because it is the only step that writes anything.
184
+ // 12. Spend the token. Last, because it is the only step that writes anything a granted call depends on.
168
185
  if (!(await deps.replay.claim(claims.jti, connection.id))) {
169
186
  deny(`jti ${claims.jti} was already spent`);
170
187
  }