@pithy-sh/core 0.13.0 → 0.14.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 +433 -3
  35. package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
  36. package/dist/cli/commandCatalog.generated.js +449 -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 +482 -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
@@ -213,6 +213,20 @@ const AuthAccountDeletedPublic = z
213
213
  })
214
214
  .describe("The account is deleted and awaiting its purge (403).");
215
215
 
216
+ const AuthHandoffInvalidPublic = z
217
+ .object({
218
+ code: z
219
+ .literal("auth/handoff_invalid")
220
+ .describe(
221
+ "The one-time code a native app trades for its session after a redirect sign-in is spent, expired, was never issued, or came with the wrong verifier. One code for all four, so the answer says nothing about which codes exist or why a trade failed. The remedy is the same for each: sign in again.",
222
+ ),
223
+ status: z
224
+ .literal(400)
225
+ .describe("Bad Request — the code presented cannot be traded. Not 401: no credential was presented to refuse."),
226
+ ...publicFields,
227
+ })
228
+ .describe("A sign-in handoff code that is spent, expired, unknown or presented with the wrong verifier (400).");
229
+
216
230
  const NotFoundPublic = z
217
231
  .object({
218
232
  code: z.literal("core/not_found").describe("The requested resource does not exist."),
@@ -339,6 +353,30 @@ const WebhookUnverifiedPublic = z
339
353
  })
340
354
  .describe("An inbound webhook failed its signature check (401).");
341
355
 
356
+ // --- @pithy-sh/core: presence rooms ---
357
+
358
+ const PresenceTicketInvalidPublic = z
359
+ .object({
360
+ code: z
361
+ .literal("core/presence_ticket_invalid")
362
+ .describe(
363
+ "A presence socket was opened without a ticket this Worker accepts. Deliberately one code for every failing step — no ticket, a bad signature, an expired one, one already spent, or one minted for another room — so a caller holding a stolen ticket learns nothing about which check it tripped. `detail` names the step for the log, and the codec strips it.",
364
+ ),
365
+ status: z.literal(401).describe("Unauthorized — mint a fresh ticket and open the socket with it."),
366
+ ...publicFields,
367
+ })
368
+ .describe("A presence socket upgrade carried no valid ticket (401).");
369
+
370
+ const PresenceRoomFullPublic = z
371
+ .object({
372
+ code: z
373
+ .literal("core/presence_room_full")
374
+ .describe("A presence room already holds as many participants as its room kind allows."),
375
+ status: z.literal(409).describe("Conflict — the room is at its participant cap. Try again when someone leaves."),
376
+ ...publicFields,
377
+ })
378
+ .describe("A presence room is at its participant cap (409).");
379
+
342
380
  // --- @pithy-sh/cloudflare: out-of-Worker CF REST client codes ---
343
381
 
344
382
  const CloudflareNotConfiguredPublic = z
@@ -855,6 +893,42 @@ const LedgerInvalidAmountPublic = z
855
893
  })
856
894
  .describe("An amount is not a positive integer (400).");
857
895
 
896
+ const LedgerEntryNotFoundPublic = z
897
+ .object({
898
+ code: z
899
+ .literal("ledger/entry_not_found")
900
+ .describe(
901
+ "No ledger entry with that reference exists on the account named. An entry on another account is answered the same way, so a reversal can never reach past the account its path names.",
902
+ ),
903
+ status: z.literal(404).describe("Not Found."),
904
+ ...publicFields,
905
+ })
906
+ .describe("A referenced ledger entry does not exist on that account (404).");
907
+
908
+ const LedgerEntryNotReversiblePublic = z
909
+ .object({
910
+ code: z
911
+ .literal("ledger/entry_not_reversible")
912
+ .describe(
913
+ "Only a credit or a debit reverses, and never one that is itself a reversal. A hold has release; a capture spends escrow and a transfer touches a second account, so both are refused. `params.kind` names the entry's kind, and `params.reverses` the entry a reversal reversed. Nothing was written.",
914
+ ),
915
+ status: z.literal(409).describe("Conflict — the entry's kind does not reverse."),
916
+ ...publicFields,
917
+ })
918
+ .describe("A ledger entry of a kind that does not reverse (409).");
919
+
920
+ const LedgerRefConflictPublic = z
921
+ .object({
922
+ code: z
923
+ .literal("ledger/ref_conflict")
924
+ .describe(
925
+ "The ref a control-plane write would be written under is already held by a different entry, so the write cannot be recorded and is not a replay either: a credit's or a debit's key already used for another account, the other act, another amount or a hold, or the derived ref of a reversal or a hold's resolution held by an unrelated entry. Nothing moved; the entry stays unreversed, or the hold stays open.",
926
+ ),
927
+ status: z.literal(409).describe("Conflict — the ref belongs to another entry."),
928
+ ...publicFields,
929
+ })
930
+ .describe("A write whose ref another entry already holds (409).");
931
+
858
932
  // --- @pithy-sh/rating: skill-rating + experience tracker codes ---
859
933
 
860
934
  const RatingUnknownAlgorithmPublic = z
@@ -921,6 +995,96 @@ const RatingRecordForbiddenPublic = z
921
995
  })
922
996
  .describe("The caller lacks the scope to record a rated outcome (403).");
923
997
 
998
+ // --- @pithy-sh/consent: purposes, grants and withdrawals ---
999
+
1000
+ const ConsentUnknownPurposePublic = z
1001
+ .object({
1002
+ code: z
1003
+ .literal("consent/unknown_purpose")
1004
+ .describe("No consent purpose with that id is declared by a composed capability or in `consent({ purposes })`."),
1005
+ status: z.literal(404).describe("Not Found."),
1006
+ ...publicFields,
1007
+ })
1008
+ .describe("A consent purpose that is not declared (404).");
1009
+
1010
+ const ConsentStaleNoticePublic = z
1011
+ .object({
1012
+ code: z
1013
+ .literal("consent/stale_notice")
1014
+ .describe(
1015
+ "The grant names a notice version that is not the purpose's current one. A person agrees to the notice in force, so the screen showing an older one has to be reloaded.",
1016
+ ),
1017
+ status: z.literal(409).describe("Conflict — the notice has moved on."),
1018
+ ...publicFields,
1019
+ })
1020
+ .describe("A grant against a notice version that is not current (409).");
1021
+
1022
+ const ConsentMethodBelowMinimumPublic = z
1023
+ .object({
1024
+ code: z
1025
+ .literal("consent/method_below_minimum")
1026
+ .describe(
1027
+ "The grant was given by a method below the purpose's minimum — a click where the purpose needs a typed name, a drawn signature or a provider's envelope.",
1028
+ ),
1029
+ status: z.literal(400).describe("Bad Request — the method is below the purpose's minimum."),
1030
+ ...publicFields,
1031
+ })
1032
+ .describe("A grant below the purpose's minimum method (400).");
1033
+
1034
+ const ConsentWithdrawalBelowGrantPublic = z
1035
+ .object({
1036
+ code: z
1037
+ .literal("consent/withdrawal_below_grant")
1038
+ .describe(
1039
+ "The purpose requires a withdrawal at least at the grade of the grant it withdraws, as a HIPAA revocation must be in writing, and this one is below it.",
1040
+ ),
1041
+ status: z.literal(400).describe("Bad Request — the withdrawal is below the grant's method."),
1042
+ ...publicFields,
1043
+ })
1044
+ .describe("A withdrawal below the grade its purpose requires (400).");
1045
+
1046
+ const ConsentEvidenceMissingPublic = z
1047
+ .object({
1048
+ code: z
1049
+ .literal("consent/evidence_missing")
1050
+ .describe(
1051
+ "The method names evidence the request did not carry: a typed name for `typed`, a drawn image for `drawn`, a provider and envelope for `provider`, or the document a signed grant is given against.",
1052
+ ),
1053
+ status: z.literal(400).describe("Bad Request — the evidence for the method is missing."),
1054
+ ...publicFields,
1055
+ })
1056
+ .describe("A grant or withdrawal missing the evidence its method needs (400).");
1057
+
1058
+ const ConsentExpiryPassedPublic = z
1059
+ .object({
1060
+ code: z
1061
+ .literal("consent/expiry_passed")
1062
+ .describe("The grant's expiry is not in the future, so it would answer no from the moment it was recorded."),
1063
+ status: z.literal(400).describe("Bad Request — the expiry has already passed."),
1064
+ ...publicFields,
1065
+ })
1066
+ .describe("A grant whose expiry has already passed (400).");
1067
+
1068
+ const ConsentDocumentNotFoundPublic = z
1069
+ .object({
1070
+ code: z.literal("consent/document_not_found").describe("No signed document with that content hash is kept."),
1071
+ status: z.literal(404).describe("Not Found."),
1072
+ ...publicFields,
1073
+ })
1074
+ .describe("A signed document that is not kept (404).");
1075
+
1076
+ const ConsentEvidenceStoreMissingPublic = z
1077
+ .object({
1078
+ code: z
1079
+ .literal("consent/evidence_store_missing")
1080
+ .describe(
1081
+ "A drawn signature or a provider's certificate is stored in R2, and the Worker has no `CONSENT_EVIDENCE` bucket bound. A binding we never got, so the answer is in this Worker's wiring.",
1082
+ ),
1083
+ status: z.literal(500).describe("Internal Server Error — the evidence bucket is not bound."),
1084
+ ...publicFields,
1085
+ })
1086
+ .describe("The evidence bucket is not bound (500).");
1087
+
924
1088
  // --- @pithy-sh/matchmaking: pairing + presence capability codes ---
925
1089
 
926
1090
  const MatchmakingRoomNotFoundPublic = z
@@ -1840,6 +2004,7 @@ export const KitPublicErrorPayload = z
1840
2004
  AuthSessionNotFreshPublic,
1841
2005
  AuthAccountSuspendedPublic,
1842
2006
  AuthAccountDeletedPublic,
2007
+ AuthHandoffInvalidPublic,
1843
2008
  NotFoundPublic,
1844
2009
  ConflictPublic,
1845
2010
  RateLimitPublic,
@@ -1851,6 +2016,8 @@ export const KitPublicErrorPayload = z
1851
2016
  UnknownWorkflowPublic,
1852
2017
  WorkflowFailedPublic,
1853
2018
  WebhookUnverifiedPublic,
2019
+ PresenceTicketInvalidPublic,
2020
+ PresenceRoomFullPublic,
1854
2021
  CloudflareNotConfiguredPublic,
1855
2022
  CloudflareRequestFailedPublic,
1856
2023
  CloudflareInvalidResponsePublic,
@@ -1898,12 +2065,23 @@ export const KitPublicErrorPayload = z
1898
2065
  LedgerInsufficientFundsPublic,
1899
2066
  LedgerHoldNotOpenPublic,
1900
2067
  LedgerInvalidAmountPublic,
2068
+ LedgerEntryNotFoundPublic,
2069
+ LedgerEntryNotReversiblePublic,
2070
+ LedgerRefConflictPublic,
1901
2071
  RatingUnknownAlgorithmPublic,
1902
2072
  RatingUnsupportedPlayerCountPublic,
1903
2073
  RatingInvalidParamsPublic,
1904
2074
  RatingGameNotFoundPublic,
1905
2075
  RatingPoolNotFoundPublic,
1906
2076
  RatingRecordForbiddenPublic,
2077
+ ConsentUnknownPurposePublic,
2078
+ ConsentStaleNoticePublic,
2079
+ ConsentMethodBelowMinimumPublic,
2080
+ ConsentWithdrawalBelowGrantPublic,
2081
+ ConsentEvidenceMissingPublic,
2082
+ ConsentExpiryPassedPublic,
2083
+ ConsentDocumentNotFoundPublic,
2084
+ ConsentEvidenceStoreMissingPublic,
1907
2085
  MatchmakingRoomNotFoundPublic,
1908
2086
  MatchmakingRoomFullPublic,
1909
2087
  MatchmakingInvalidCodePublic,
@@ -2006,6 +2184,9 @@ const AuthAccountSuspended = AuthAccountSuspendedPublic.extend(internalFields).d
2006
2184
  const AuthAccountDeleted = AuthAccountDeletedPublic.extend(internalFields).describe(
2007
2185
  AuthAccountDeletedPublic.description ?? "",
2008
2186
  );
2187
+ const AuthHandoffInvalid = AuthHandoffInvalidPublic.extend(internalFields).describe(
2188
+ AuthHandoffInvalidPublic.description ?? "",
2189
+ );
2009
2190
  const NotFound = NotFoundPublic.extend(internalFields).describe(NotFoundPublic.description ?? "");
2010
2191
  const Conflict = ConflictPublic.extend(internalFields).describe(ConflictPublic.description ?? "");
2011
2192
  const RateLimit = RateLimitPublic.extend(internalFields).describe(RateLimitPublic.description ?? "");
@@ -2023,6 +2204,12 @@ const WorkflowFailed = WorkflowFailedPublic.extend(internalFields).describe(Work
2023
2204
  const WebhookUnverified = WebhookUnverifiedPublic.extend(internalFields).describe(
2024
2205
  WebhookUnverifiedPublic.description ?? "",
2025
2206
  );
2207
+ const PresenceTicketInvalid = PresenceTicketInvalidPublic.extend(internalFields).describe(
2208
+ PresenceTicketInvalidPublic.description ?? "",
2209
+ );
2210
+ const PresenceRoomFull = PresenceRoomFullPublic.extend(internalFields).describe(
2211
+ PresenceRoomFullPublic.description ?? "",
2212
+ );
2026
2213
  const CloudflareNotConfigured = CloudflareNotConfiguredPublic.extend(internalFields).describe(
2027
2214
  CloudflareNotConfiguredPublic.description ?? "",
2028
2215
  );
@@ -2150,6 +2337,15 @@ const LedgerHoldNotOpen = LedgerHoldNotOpenPublic.extend(internalFields).describ
2150
2337
  const LedgerInvalidAmount = LedgerInvalidAmountPublic.extend(internalFields).describe(
2151
2338
  LedgerInvalidAmountPublic.description ?? "",
2152
2339
  );
2340
+ const LedgerEntryNotFound = LedgerEntryNotFoundPublic.extend(internalFields).describe(
2341
+ LedgerEntryNotFoundPublic.description ?? "",
2342
+ );
2343
+ const LedgerEntryNotReversible = LedgerEntryNotReversiblePublic.extend(internalFields).describe(
2344
+ LedgerEntryNotReversiblePublic.description ?? "",
2345
+ );
2346
+ const LedgerRefConflict = LedgerRefConflictPublic.extend(internalFields).describe(
2347
+ LedgerRefConflictPublic.description ?? "",
2348
+ );
2153
2349
  const RatingUnknownAlgorithm = RatingUnknownAlgorithmPublic.extend(internalFields).describe(
2154
2350
  RatingUnknownAlgorithmPublic.description ?? "",
2155
2351
  );
@@ -2168,6 +2364,30 @@ const RatingPoolNotFound = RatingPoolNotFoundPublic.extend(internalFields).descr
2168
2364
  const RatingRecordForbidden = RatingRecordForbiddenPublic.extend(internalFields).describe(
2169
2365
  RatingRecordForbiddenPublic.description ?? "",
2170
2366
  );
2367
+ const ConsentUnknownPurpose = ConsentUnknownPurposePublic.extend(internalFields).describe(
2368
+ ConsentUnknownPurposePublic.description ?? "",
2369
+ );
2370
+ const ConsentStaleNotice = ConsentStaleNoticePublic.extend(internalFields).describe(
2371
+ ConsentStaleNoticePublic.description ?? "",
2372
+ );
2373
+ const ConsentMethodBelowMinimum = ConsentMethodBelowMinimumPublic.extend(internalFields).describe(
2374
+ ConsentMethodBelowMinimumPublic.description ?? "",
2375
+ );
2376
+ const ConsentWithdrawalBelowGrant = ConsentWithdrawalBelowGrantPublic.extend(internalFields).describe(
2377
+ ConsentWithdrawalBelowGrantPublic.description ?? "",
2378
+ );
2379
+ const ConsentEvidenceMissing = ConsentEvidenceMissingPublic.extend(internalFields).describe(
2380
+ ConsentEvidenceMissingPublic.description ?? "",
2381
+ );
2382
+ const ConsentExpiryPassed = ConsentExpiryPassedPublic.extend(internalFields).describe(
2383
+ ConsentExpiryPassedPublic.description ?? "",
2384
+ );
2385
+ const ConsentDocumentNotFound = ConsentDocumentNotFoundPublic.extend(internalFields).describe(
2386
+ ConsentDocumentNotFoundPublic.description ?? "",
2387
+ );
2388
+ const ConsentEvidenceStoreMissing = ConsentEvidenceStoreMissingPublic.extend(internalFields).describe(
2389
+ ConsentEvidenceStoreMissingPublic.description ?? "",
2390
+ );
2171
2391
  const MatchmakingRoomNotFound = MatchmakingRoomNotFoundPublic.extend(internalFields).describe(
2172
2392
  MatchmakingRoomNotFoundPublic.description ?? "",
2173
2393
  );
@@ -2417,6 +2637,7 @@ export const KitErrorPayload = z
2417
2637
  AuthSessionNotFresh,
2418
2638
  AuthAccountSuspended,
2419
2639
  AuthAccountDeleted,
2640
+ AuthHandoffInvalid,
2420
2641
  NotFound,
2421
2642
  Conflict,
2422
2643
  RateLimit,
@@ -2428,6 +2649,8 @@ export const KitErrorPayload = z
2428
2649
  UnknownWorkflow,
2429
2650
  WorkflowFailed,
2430
2651
  WebhookUnverified,
2652
+ PresenceTicketInvalid,
2653
+ PresenceRoomFull,
2431
2654
  CloudflareNotConfigured,
2432
2655
  CloudflareRequestFailed,
2433
2656
  CloudflareInvalidResponse,
@@ -2475,12 +2698,23 @@ export const KitErrorPayload = z
2475
2698
  LedgerInsufficientFunds,
2476
2699
  LedgerHoldNotOpen,
2477
2700
  LedgerInvalidAmount,
2701
+ LedgerEntryNotFound,
2702
+ LedgerEntryNotReversible,
2703
+ LedgerRefConflict,
2478
2704
  RatingUnknownAlgorithm,
2479
2705
  RatingUnsupportedPlayerCount,
2480
2706
  RatingInvalidParams,
2481
2707
  RatingGameNotFound,
2482
2708
  RatingPoolNotFound,
2483
2709
  RatingRecordForbidden,
2710
+ ConsentUnknownPurpose,
2711
+ ConsentStaleNotice,
2712
+ ConsentMethodBelowMinimum,
2713
+ ConsentWithdrawalBelowGrant,
2714
+ ConsentEvidenceMissing,
2715
+ ConsentExpiryPassed,
2716
+ ConsentDocumentNotFound,
2717
+ ConsentEvidenceStoreMissing,
2484
2718
  MatchmakingRoomNotFound,
2485
2719
  MatchmakingRoomFull,
2486
2720
  MatchmakingInvalidCode,
@@ -192,6 +192,48 @@ export class WebhookUnverifiedError extends PithyError {
192
192
  }
193
193
  }
194
194
 
195
+ /**
196
+ * One code for every way a presence socket's ticket fails — missing, forged, expired, spent, or minted for
197
+ * another room. The step goes in `detail`, which the codec strips: a caller holding somebody else's ticket
198
+ * learns only that it no longer works.
199
+ */
200
+ export class PresenceTicketInvalidError extends PithyError {
201
+ declare readonly payload: Extract<ErrorPayload, { code: "core/presence_ticket_invalid" }>;
202
+
203
+ constructor(args: ErrorArgs = {}, options?: { cause?: unknown }) {
204
+ super(
205
+ {
206
+ code: "core/presence_ticket_invalid",
207
+ status: 401,
208
+ message: args.message ?? "That presence ticket is not valid.",
209
+ action: args.action ?? "Mint a fresh ticket and open the socket within 60 seconds.",
210
+ detail: args.detail,
211
+ params: args.params,
212
+ },
213
+ options,
214
+ );
215
+ }
216
+ }
217
+
218
+ /** A presence room is at the participant cap its room kind declares. */
219
+ export class PresenceRoomFullError extends PithyError {
220
+ declare readonly payload: Extract<ErrorPayload, { code: "core/presence_room_full" }>;
221
+
222
+ constructor(args: ErrorArgs = {}, options?: { cause?: unknown }) {
223
+ super(
224
+ {
225
+ code: "core/presence_room_full",
226
+ status: 409,
227
+ message: args.message ?? "That room is full.",
228
+ action: args.action,
229
+ detail: args.detail,
230
+ params: args.params,
231
+ },
232
+ options,
233
+ );
234
+ }
235
+ }
236
+
195
237
  export class ForbiddenError extends PithyError {
196
238
  declare readonly payload: Extract<ErrorPayload, { code: "auth/forbidden" }>;
197
239
 
@@ -3,6 +3,8 @@
3
3
 
4
4
  import type { MiddlewareHandler } from "hono";
5
5
  import type { z } from "zod";
6
+ import type { PithyHonoEnv } from "../capability/capability";
7
+ import { ForbiddenError, UnauthorizedError } from "../error/pithyError";
6
8
  import type { VerificationStrategy } from "./verification";
7
9
 
8
10
  /**
@@ -43,6 +45,24 @@ export interface RouteResponse {
43
45
  /** The HTTP methods an `app.all` route is documented under. OpenAPI has no "any method". */
44
46
  export type DocumentedMethod = "get" | "post" | "put" | "patch" | "delete" | "head" | "options";
45
47
 
48
+ /**
49
+ * What a tool does beyond reading, which every non-`GET` tool must say (`docs/AGENT-ACCESS.md` §4).
50
+ *
51
+ * **A route is a tool only when it says so, and a non-`GET` says so louder.** One that sends mail, charges a card or
52
+ * deletes something is a tool an agent can call in a loop, so the effect is not inferred from the method: it is
53
+ * declared, it sets the tool's MCP annotations, it is written into the tool's description, and it is counted on the
54
+ * consent screen beside its capability.
55
+ */
56
+ export const TOOL_EFFECTS = ["writes", "sends", "charges", "deletes"] as const;
57
+ export type ToolEffect = (typeof TOOL_EFFECTS)[number];
58
+
59
+ /**
60
+ * A route's opt-in to the app's MCP endpoint. `true` is a read, and only a `GET` may say it; anything else declares
61
+ * its {@link ToolEffect}. `createBackend` refuses a composition where either half is wrong — `describeRoute` cannot
62
+ * see the method it is mounted under, so the composition is the first place the pair can be checked.
63
+ */
64
+ export type RouteTool = true | { readonly effect: ToolEffect };
65
+
46
66
  /** What a route is for, how its caller is verified, and what it returns. */
47
67
  export interface RouteSpec {
48
68
  /** One sentence, in the brand voice. The operation's title in the contract. */
@@ -64,14 +84,64 @@ export interface RouteSpec {
64
84
  readonly responses: Readonly<Record<number, z.ZodType | RouteResponse>>;
65
85
  /** For an `app.all` route only: the methods it is documented under. */
66
86
  readonly methods?: readonly DocumentedMethod[];
87
+ /**
88
+ * The power a caller's role must hold to reach this route at all. **Declaring it installs the gate**: the
89
+ * same middleware that carries this spec refuses a caller whose acting organization's role does not hold it, so
90
+ * what the MCP endpoint filters `tools/list` on and what the route enforces are one value and cannot drift.
91
+ *
92
+ * A route whose access depends on the record — its owner may, a manager may — declares the power that lets a
93
+ * member reach it at all, and keeps its in-handler check. Absent, the route is gated by its other guards alone,
94
+ * and a tool for it is offered to every signed-in user the endpoint admits.
95
+ */
96
+ readonly power?: string;
97
+ /**
98
+ * The route's permanent name within its capability, e.g. `threads.reply`. Its MCP tool is
99
+ * `<capability>_<operation>`, through the one normalizer the control-plane server's tools use. Unique within a
100
+ * capability, and never renamed once published: an agent references a tool by name.
101
+ */
102
+ readonly operation?: string;
103
+ /**
104
+ * The route's opt-in to the app's MCP endpoint. Absent, the route is not a tool. See {@link RouteTool}.
105
+ */
106
+ readonly tool?: RouteTool;
67
107
  }
68
108
 
69
- /** Declare what a route is for, how its caller is verified, and what it returns. See {@link RouteSpec}. */
109
+ /**
110
+ * Declare what a route is for, how its caller is verified, and what it returns. See {@link RouteSpec}.
111
+ *
112
+ * With a {@link RouteSpec.power} it is also the route's power gate, read off the acting-organization seam the way
113
+ * every capability outside tenancy asks it. It sits first on the route line, so it answers an anonymous caller
114
+ * itself rather than assuming a guard below it will: a 401, because signing in might help, where a role that does
115
+ * not hold the power is a 403.
116
+ */
70
117
  export function describeRoute(spec: RouteSpec): MiddlewareHandler {
71
- const middleware: MiddlewareHandler = async (_c, next) => {
72
- await next();
73
- };
74
- return Object.assign(middleware, { [ROUTE_SPEC]: spec });
118
+ const { power } = spec;
119
+ const middleware: MiddlewareHandler<PithyHonoEnv> =
120
+ power === undefined
121
+ ? async (_c, next) => {
122
+ await next();
123
+ }
124
+ : async (c, next) => {
125
+ if (!c.var.auth) {
126
+ throw new UnauthorizedError({
127
+ message: "Authentication required.",
128
+ action: "Sign in and retry with a valid session or bearer token.",
129
+ detail: `describeRoute(power: ${power}) ran with no signed-in caller`,
130
+ });
131
+ }
132
+ const acting = await c.var.actingOrganization();
133
+ if (acting === null || !acting.roleAllows(power)) {
134
+ throw new ForbiddenError({
135
+ message: `Your role does not allow \`${power}\`.`,
136
+ detail:
137
+ acting === null
138
+ ? `no organization is in force for user ${c.var.auth.userId}, so ${power} is not held`
139
+ : `the role in organization ${acting.organizationId} does not hold ${power}`,
140
+ });
141
+ }
142
+ await next();
143
+ };
144
+ return Object.assign(middleware as MiddlewareHandler, { [ROUTE_SPEC]: spec });
75
145
  }
76
146
 
77
147
  /** The spec a {@link describeRoute} middleware carries, or `undefined` for any other handler. */
@@ -11,7 +11,9 @@ import { z } from "zod";
11
11
  */
12
12
  export const VerificationStrategy = z
13
13
  .union([
14
- z.literal("bearer").describe("Short-lived access token via Authorization: Bearer; validated by @pithy-sh/auth."),
14
+ z
15
+ .literal("bearer")
16
+ .describe("A session token via Authorization: Bearer, rotated before it expires; validated by @pithy-sh/auth."),
15
17
  z.literal("session").describe("Cookie-based web session, CSRF-protected; validated by @pithy-sh/auth."),
16
18
  z
17
19
  .literal("signed-webhook")
@@ -90,7 +90,8 @@ export interface TranslatorInput {
90
90
  readonly formattingLocale?: string;
91
91
  /**
92
92
  * The catalogs to walk, most-specific first. `@pithy-sh/i18n` passes adopter-locale, adopter-default,
93
- * kit-locale, kit-default; a copied screen passes its own baked English and nothing else.
93
+ * kit-locale, composed-locale, composed-default, composed English; a copied screen passes its own baked
94
+ * English and nothing else.
94
95
  */
95
96
  readonly layers: readonly (MessageCatalog | undefined)[];
96
97
  }
package/src/index.ts CHANGED
@@ -99,6 +99,8 @@ export {
99
99
  ANY_VERIFIED_CALLER,
100
100
  type ControlPlaneRequirement,
101
101
  ControlPlaneScope,
102
+ ERASURE_READ_SCOPE,
103
+ EVIDENCE_READ_SCOPE,
102
104
  KEYS_ROTATE_SCOPE,
103
105
  MANIFEST_READ_SCOPE,
104
106
  MIGRATIONS_READ_SCOPE,
@@ -0,0 +1,38 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { ScopeKind } from "../controlPlane/scope/kind";
5
+
6
+ /**
7
+ * What an MCP tool says about itself, from the kind of thing it does. One mapping for both servers: the app's own
8
+ * tools, whose effect is a kind without `reads` or `credentials`, and the control-plane tools `pithy mcp serve`
9
+ * offers, whose route declares its scope's kind.
10
+ */
11
+
12
+ /** The MCP annotations a tool carries, set by its kind. `idempotentHint` is claimed for a read and nothing else. */
13
+ export interface ToolAnnotations {
14
+ readonly readOnlyHint: boolean;
15
+ readonly destructiveHint: boolean;
16
+ readonly idempotentHint?: true;
17
+ readonly openWorldHint: boolean;
18
+ }
19
+
20
+ /** What each kind says in a tool's description, and on the consent screen. One sentence each. */
21
+ export const KIND_SENTENCES: Readonly<Record<ScopeKind, string>> = {
22
+ reads: "Reads data and changes nothing.",
23
+ writes: "Changes data in this application.",
24
+ sends: "Sends a message to somebody.",
25
+ charges: "Charges money.",
26
+ deletes: "Deletes data, and it cannot be undone.",
27
+ credentials: "Mints, rotates or revokes a credential.",
28
+ };
29
+
30
+ /** The MCP annotations for a kind. */
31
+ export function kindAnnotations(kind: ScopeKind): ToolAnnotations {
32
+ return {
33
+ readOnlyHint: kind === "reads",
34
+ destructiveHint: kind === "charges" || kind === "deletes" || kind === "credentials",
35
+ ...(kind === "reads" ? { idempotentHint: true as const } : {}),
36
+ openWorldHint: kind === "sends" || kind === "charges",
37
+ };
38
+ }