@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
@@ -229,6 +229,45 @@ const TARGET = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
229
229
  */
230
230
  const NAMED_RESOURCE_KINDS = new Set<string>(FEATURE_RESOURCE_KINDS);
231
231
 
232
+ /**
233
+ * **How a Worker consumes a queue it also produces to (#824).**
234
+ *
235
+ * A consumer is half of the queue's own configuration — Cloudflare reads it off the `queues.consumers` entry, not
236
+ * off the code — so it is declared beside the producer binding and written with it, by the same writer. The
237
+ * dead-letter queue is named by **binding**, never by queue name: the name is `<project>-<env>-<thing>`, composed
238
+ * per environment by the same namer that names the producer's, so a spec that stated it would be one environment's.
239
+ *
240
+ * `maxRetries` is bounded by what Cloudflare Queues accepts (0 to 100), and the capability's handler reads the same
241
+ * number to know a message's last attempt, which is the moment it can count one as dead-lettered.
242
+ */
243
+ export const QueueConsumerSpec = z
244
+ .object({
245
+ deadLetter: z
246
+ .string()
247
+ .min(1)
248
+ // A binding name, and narrowed to one: a manifest is third-party data, and this string is composed into the
249
+ // dead-letter queue's name in the adopter's `wrangler.jsonc`.
250
+ .regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "A dead-letter queue is named by its binding: letters, digits and `_`.")
251
+ .describe(
252
+ "The binding of the queue a message goes to once its retries are spent. Declared as a `queue` binding of its own, so `pithy provision` creates it; its queue name is composed the same way as this one's.",
253
+ ),
254
+ maxRetries: z
255
+ .number()
256
+ .int()
257
+ .min(0)
258
+ .max(100)
259
+ .describe("How many times a failed message is retried before it is dead-lettered. Cloudflare allows 0 to 100."),
260
+ maxBatchSize: z
261
+ .number()
262
+ .int()
263
+ .min(1)
264
+ .max(100)
265
+ .optional()
266
+ .describe("The most messages one invocation of the handler receives. Absent leaves Cloudflare's default."),
267
+ })
268
+ .describe("How a Worker consumes a queue it produces to: where exhausted messages go, and how often one is retried.");
269
+ export type QueueConsumerSpec = z.infer<typeof QueueConsumerSpec>;
270
+
232
271
  /**
233
272
  * Declares a Cloudflare binding a capability requires in the Worker env. The authoring
234
273
  * shape lets `optional` be omitted (defaults false); `createBackend` normalizes via parse.
@@ -308,6 +347,9 @@ export const BindingSpec = z
308
347
  .describe(
309
348
  "The `<thing>` segment of this resource's name, when it is not the kebabbed binding name. Absent is the ordinary case and means the binding name is the segment (`EMAIL_SUPPRESSIONS` → `email-suppressions`). Declare it where the two must differ: `SUPPORT_BUCKET` backs `<project>-global-support`, because the binding keeps `_BUCKET` for whoever reads `env.SUPPORT_BUCKET` in a Worker while the resource drops it — `r2` is already the composed name's kind, so the suffix repeats it. Meaningful only for a `d1`, `kv`, `r2` or `queue` binding.",
310
349
  ),
350
+ consumer: QueueConsumerSpec.optional().describe(
351
+ "That this Worker also **consumes** the queue it produces to, and how: the queue its exhausted messages go to and how many times one is retried first. Written as the `queues.consumers` entry beside the producer, so the queue's handler is the Worker's own `queue` export (`Capability.queue`). Absent, the Worker only sends. Meaningful only for a `queue` binding.",
352
+ ),
311
353
  })
312
354
  .describe("Declares a Cloudflare binding a capability requires in the Worker env.")
313
355
  .check((ctx) => {
@@ -376,6 +418,25 @@ export const BindingSpec = z
376
418
  message: `Binding "${ctx.value.name}" needs a target — the ${ctx.value.type} resource it addresses, written as \`${target.field}\`.`,
377
419
  });
378
420
  }
421
+ // A consumer is the queue's configuration, so on any other kind it describes nothing a writer emits.
422
+ const consumer = ctx.value.consumer;
423
+ if (consumer !== undefined && ctx.value.type !== "queue") {
424
+ ctx.issues.push({
425
+ code: "custom",
426
+ input: ctx.value,
427
+ path: ["consumer"],
428
+ message: `Binding "${ctx.value.name}" declares a consumer, which only a queue binding has — this one is a ${ctx.value.type}.`,
429
+ });
430
+ }
431
+ // Dead-lettering into the queue a message just failed out of is a retry loop with no end, so it is refused.
432
+ if (consumer !== undefined && consumer.deadLetter === ctx.value.name) {
433
+ ctx.issues.push({
434
+ code: "custom",
435
+ input: ctx.value,
436
+ path: ["consumer", "deadLetter"],
437
+ message: `Queue "${ctx.value.name}" names itself as its dead-letter queue. Declare a second queue binding for it.`,
438
+ });
439
+ }
379
440
  if (target === undefined && ctx.value.target !== undefined) {
380
441
  ctx.issues.push({
381
442
  code: "custom",
@@ -5,9 +5,12 @@ import type { ExecutionContext, ForwardableEmailMessage } from "@cloudflare/work
5
5
  import type { Hono, MiddlewareHandler } from "hono";
6
6
  import type { z } from "zod";
7
7
  import type { ActingOrganizationResolver } from "../access/acting";
8
+ import type { AuditCitationReader } from "../audit/citations";
8
9
  import type { AuditEmit } from "../audit/recorder";
10
+ import { type ConsentPurpose, parseConsentPurposes } from "../consent/purpose";
11
+ import type { ConsentGrantedListener, ConsentReader, ConsentWithdrawnListener } from "../consent/reader";
9
12
  import type { ControlPlaneContext } from "../controlPlane/context";
10
- import type { AdminRoute } from "../controlPlane/discovery/adminRoute";
13
+ import type { AdminRouteDeclaration } from "../controlPlane/discovery/adminRoute";
11
14
  import type { CapabilityManifestConfig } from "../controlPlane/discovery/configuration";
12
15
  import type { CapabilityHealth } from "../controlPlane/discovery/health";
13
16
  import type { ControlPlaneVerifier } from "../controlPlane/http/guard";
@@ -22,12 +25,28 @@ import type { Translator } from "../i18n/translator";
22
25
  import type { KvNamespaceSpecMap } from "../kv/namespaces";
23
26
  import type { Logger } from "../logger/logger";
24
27
  import { declareRetained } from "../migrations/retainedDeclaration";
28
+ import { presenceRoomsSocket } from "../presence/channel";
29
+ import {
30
+ assertPresenceDeclaration,
31
+ PRESENCE_ROOMS_BINDING_SPEC,
32
+ type PresenceDeclaration,
33
+ } from "../presence/declaration";
34
+ import {
35
+ type ProcessingActivity,
36
+ type ProcessingActivityInput,
37
+ parseProcessingActivities,
38
+ } from "../processing/activity";
39
+ import type { CapabilityQueueHandler } from "../queue/consumer";
40
+ import type { PurgeWindowDefault } from "../retention/policy";
25
41
  import type { SeedSet } from "../seed/seed";
42
+ import type { SessionRotatedListener } from "../session/rotated";
26
43
  import type { WorkflowSpecMap } from "../workflow/spec";
27
44
  import { BindingSpec, type BindingSpecInput } from "./bindings";
28
45
  import type { ClientProjection, ClientProjectionContext } from "./client";
46
+ import type { ErasureRecords } from "./erasure";
29
47
  import { assertPurgeContributions, type PurgeContribution } from "./purge";
30
48
  import type { CapabilitySettings } from "./settings";
49
+ import { assertVectorIndexContributions, type VectorIndexContribution } from "./vectorIndex";
31
50
 
32
51
  /** Hono `Variables` every capability's routes are typed against. `createBackend` seeds these per request. */
33
52
  export interface PithyVars {
@@ -180,7 +199,7 @@ export interface SecretRegistryEntrySeam {
180
199
  readonly devValue?: string;
181
200
  /**
182
201
  * The Worker binding this value exists to reach, or absent when it reaches none — the constant half
183
- * of *can this composition use this secret* (#541).
202
+ * of *can this composition use this secret*.
184
203
  *
185
204
  * Read here and never interpreted, exactly like {@link devValue}: the CLI joins it against a Worker's
186
205
  * `declinedBindings` so a credential whose bucket the project declined stops being listed as
@@ -206,7 +225,7 @@ export type SecretRegistrySeam = Record<string, SecretRegistryEntrySeam>;
206
225
  * **It is declared, not matched by name.** `PaymentsRailToggles.paddle` and
207
226
  * `PaymentsProviderCredentials.paddle` happen to be spelled the same today, and a CLI that keyed off
208
227
  * that would be right by coincidence: nothing makes a config flag and a schema key the same thing, and
209
- * two of them one line apart cannot be told apart by convention (#513). The capability already knows
228
+ * two of them one line apart cannot be told apart by convention. The capability already knows
210
229
  * the answer at construction — it has both halves in hand — so it states it, and the CLI reads a fact
211
230
  * instead of inferring one.
212
231
  *
@@ -228,8 +247,8 @@ export type SecretBranchSeam = Record<string, readonly string[]>;
228
247
  * The neighbor of {@link SecretBranchSeam}, one level up. That one says which *blocks* of a bundle
229
248
  * apply when the bundle itself does; this says that a whole secret does not. `@pithy-sh/auth` declares
230
249
  * one credential per OAuth provider, so a project running Google and GitHub has two secrets that no
231
- * code path will ever read — and until #541 `pithy doctor` listed both as outstanding work under
232
- * *"fine to leave until you need it"*, which is not true of a credential the configuration has refused.
250
+ * code path will ever read, and `pithy doctor` does not list them as outstanding work: the configuration
251
+ * has refused them.
233
252
  *
234
253
  * **Declared, never inferred**, for {@link SecretBranchSeam}'s reason: `auth-apple-credentials` and
235
254
  * `providers.apple` are spelled alike today and nothing makes a secret name and a config key the same
@@ -307,7 +326,7 @@ export interface CapabilityComposeContext {
307
326
  * message (an untouched message is dropped by the runtime).
308
327
  *
309
328
  * **`ForwardableEmailMessage` and `ExecutionContext` are imported rather than assumed ambient**, and
310
- * that is the whole of #315's second half. This module is reachable from a client: a browser program
329
+ * that is deliberate. This module is reachable from a client: a browser program
311
330
  * that names a control-plane scope through a capability's guards lands here, and a browser program
312
331
  * has no Workers globals. Read off the global scope, the two names resolved only for a consumer that
313
332
  * had `@cloudflare/workers-types` in its `types` — so an adopter's DOM program failed on two errors
@@ -400,7 +419,7 @@ export interface Capability<
400
419
  * The CF permissions this capability's **deployed Workers** call Cloudflare with, as short catalog keys
401
420
  * (`secrets:write`, `images:write`, `analytics:read`). The runtime counterpart of {@link ciPermissions}.
402
421
  *
403
- * **A deployed application holds one Cloudflare API token, and this is the only way into it (#728).**
422
+ * **A deployed application holds one Cloudflare API token, and this is the only way into it.**
404
423
  * Every composed capability's keys union into the project-level `runtime` profile, minted per
405
424
  * environment into the Secrets Store and read by every Worker of that environment through one binding,
406
425
  * the way every capability's `ciPermissions` union into the one `ci-system` token. A capability cannot
@@ -427,10 +446,10 @@ export interface Capability<
427
446
  * the router that actually mounted; a stale declaration is worse than none, because a client
428
447
  * believes it.
429
448
  */
430
- adminRoutes?: readonly AdminRoute[];
449
+ adminRoutes?: readonly AdminRouteDeclaration[];
431
450
  /**
432
451
  * The bounded health summary this capability contributes to its own manifest entry — a small, fixed
433
- * set of scalars a management client can render beside a rail without a second round trip (#317).
452
+ * set of scalars a management client can render beside a rail without a second round trip.
434
453
  * Omit it when a capability has nothing to say, which is the normal case.
435
454
  *
436
455
  * **Declared alongside the routes, and only through `defineCapabilityHealth`.** The type is branded,
@@ -446,7 +465,7 @@ export interface Capability<
446
465
  health?: CapabilityHealth;
447
466
  /**
448
467
  * The configured facts this capability states into its own manifest entry — the decisions an adopter
449
- * already made that a management client has to respect to call anything (#422). Omit it when nothing
468
+ * already made that a management client has to respect to call anything. Omit it when nothing
450
469
  * this capability is configured with changes how a client calls it, which is the normal case.
451
470
  *
452
471
  * **A third field, because the two obvious names are taken and neither is this.**
@@ -467,7 +486,7 @@ export interface Capability<
467
486
  manifestConfig?: CapabilityManifestConfig;
468
487
  /**
469
488
  * How this capability checks that its own **settings work**, as opposed to being merely present — the
470
- * seam `pithy doctor` runs for every capability every Worker composes (#411). Omit it when a capability
489
+ * seam `pithy doctor` runs for every capability every Worker composes. Omit it when a capability
471
490
  * has nothing worth checking, which is the normal case and is silent rather than a fault.
472
491
  *
473
492
  * Declared here, on the instance, and **never keyed off `pithy.manifest.json`**: `@pithy-sh/matchmaking`
@@ -505,7 +524,7 @@ export interface Capability<
505
524
  * The npm **package** that supplies this capability, or `null` where there is none.
506
525
  *
507
526
  * The join key {@link Capability.version} is useless without. A version joins against a release feed
508
- * by package name, and this manifest reported the version alone until #626 — so a client had to guess
527
+ * by package name, and without this a client has to guess
509
528
  * `@pithy-sh/${name}`, which is right for most capabilities and wrong for the one that matters:
510
529
  * `controlplane` ships inside `@pithy-sh/core`, so the guess reaches a package that has never been
511
530
  * published and the join comes back empty, which is indistinguishable from "up to date".
@@ -544,7 +563,7 @@ export interface Capability<
544
563
  * *load*, because the adopter's own toolchain has to keep working on the project while they fix it.
545
564
  * `@pithy-sh/payments` is the case it exists for: `pithy add` scaffolds an unimplemented
546
565
  * `resolveSubject`, every CLI command keeps loading that config, and the Worker refuses to boot until
547
- * the adopter has written it (#500). A check that can be made at composition still belongs at
566
+ * the adopter has written it. A check that can be made at composition still belongs at
548
567
  * composition — earlier is better, and a refusal the CLI can raise is a refusal an adopter sees sooner.
549
568
  */
550
569
  boot?: (context: CapabilityComposeContext) => void;
@@ -556,6 +575,18 @@ export interface Capability<
556
575
  * parsed with. A capability with no socket leaves it unset.
557
576
  */
558
577
  sockets?: readonly SocketChannel[];
578
+ /**
579
+ * The presence room kinds this capability serves (`../presence/declaration.ts`): who else has one of its
580
+ * records open, live. Omit it when a capability has no record more than one person acts on, which is the
581
+ * normal case.
582
+ *
583
+ * **Declaring it is the whole of the wiring.** `defineCapability` adds the `PRESENCE_ROOMS` Durable Object
584
+ * binding and the room socket to this capability's {@link requiredBindings} and {@link sockets};
585
+ * `createBackend` mounts `{basePath}/presence/tickets` and `{basePath}/presence/rooms/:kind/:recordId`
586
+ * beside its routes; the control-plane seam answers `POST /control-plane/presence/tickets` for every kind
587
+ * and lists them in the manifest. Build it from the resolved config, like {@link adminRoutes}.
588
+ */
589
+ presence?: PresenceDeclaration;
559
590
  /**
560
591
  * Composable middleware (e.g. turnstile(), requireAuth()). Runs on every request, the control-plane
561
592
  * surface included. A refusal that protects a route — a rate limit, authentication, a CSRF check —
@@ -594,7 +625,7 @@ export interface Capability<
594
625
  /**
595
626
  * Whether this capability fills `AuthContext.scopes` — whether anything will ever put a string in the
596
627
  * array the scope gates read. `@pithy-sh/auth` declares it when an adopter supplies `resolveScopes`, and
597
- * not otherwise (#560).
628
+ * not otherwise.
598
629
  *
599
630
  * The same reason {@link providesEntitlements} exists, one seam over. Every scope gate fails closed, so a
600
631
  * Worker composing `leaderboard` with no resolver is not broken: it refuses every caller on the gated
@@ -633,9 +664,16 @@ export interface Capability<
633
664
  * capability that declares one (e.g. `@pithy-sh/email`'s bounce/complaint handler).
634
665
  */
635
666
  email?: CapabilityEmailHandler;
667
+ /**
668
+ * Queue consumer. The entrypoint (`createEntrypoint`) exposes the Worker's one `queue` export when any
669
+ * capability declares one, and hands each the messages of a batch whose body names it — see
670
+ * `../queue/consumer.ts` for why the body, not the queue name, decides. The queue itself is declared as a
671
+ * `queue` binding with a `consumer`, which is what writes the `queues.consumers` entry that delivers here.
672
+ */
673
+ queue?: CapabilityQueueHandler;
636
674
  /**
637
675
  * The audit recorder this capability provides **for code that has no request** — a Workflow step, which the
638
- * runtime constructs with `env` and nothing else (#622). `@pithy-sh/audit` declares it; nothing else does.
676
+ * runtime constructs with `env` and nothing else. `@pithy-sh/audit` declares it; nothing else does.
639
677
  *
640
678
  * A route records through `c.var.emit`, which the same capability's middleware fills, and keeps doing that.
641
679
  * This is the other door, for the purge that erases a deleted account: it runs in a Workflow and still owes
@@ -644,6 +682,59 @@ export interface Capability<
644
682
  * from the env per call, like the request recorder is built per request. Non-fatal by the same contract.
645
683
  */
646
684
  auditRecorder?: (env: Record<string, unknown>) => AuditEmit;
685
+ /**
686
+ * The trail's citation reader: which recorded events name a resource, and where each sits in its chain.
687
+ * `@pithy-sh/audit` declares it; nothing else does. The erasure attestation cites a purge's events through it,
688
+ * found among the composed capabilities by `../audit/citations.ts`, so the control plane imports no audit package.
689
+ */
690
+ auditCitations?: (env: Record<string, unknown>) => AuditCitationReader;
691
+ /**
692
+ * The purposes this capability processes under consent — `@pithy-sh/email` declares `email.marketing`
693
+ * and `email.tracking`. Omit it when nothing here needs consent, which is the ordinary case.
694
+ *
695
+ * **Only under this capability's own `name`**, as `<name>.<purpose>`: `defineCapability` refuses anything else,
696
+ * the rule tables and error codes follow. The consent capability composes every declaration into the catalog its
697
+ * `hasConsent` answers from, and `pithy doctor` names a composition that declares purposes and records no consent.
698
+ */
699
+ consentPurposes?: readonly ConsentPurpose[];
700
+ /**
701
+ * The processing activities this capability performs on personal data — `@pithy-sh/auth` declares `auth.account`.
702
+ * Each names a purpose, the data classes it uses and, where the capability can say, a default legal basis. Only
703
+ * under this capability's own `name`, as `<name>.<activity>`. `GET /control-plane/processing` derives the GDPR
704
+ * Article 30 record from them; see `../processing/activity.ts`. Build it from the resolved config, so a recipient
705
+ * that is switched off is not named.
706
+ */
707
+ processing?: readonly ProcessingActivity[];
708
+ /**
709
+ * The consent seam's provider: a reader built over this run's env. `@pithy-sh/consent` declares it;
710
+ * nothing else does. A capability asks it through `composedConsent` in `../consent/reader.ts`, never by
711
+ * importing the consent package.
712
+ */
713
+ consentReader?: (env: Record<string, unknown>) => ConsentReader;
714
+ /**
715
+ * Hear of a withdrawal from one of this capability's own purposes, to keep its own state in step — email
716
+ * suppresses an address for marketing when `email.marketing` is withdrawn. The consent capability calls
717
+ * it after the withdrawal is recorded, only for purposes under this capability's name.
718
+ */
719
+ consentWithdrawn?: ConsentWithdrawnListener;
720
+ /**
721
+ * Hear of a grant to one of this capability's own purposes once it stands — the other half of
722
+ * {@link Capability.consentWithdrawn}. Email lifts the marketing suppression a withdrawal wrote when
723
+ * `email.marketing` is granted again, so the suppression list and the record never disagree either way.
724
+ */
725
+ consentGranted?: ConsentGrantedListener;
726
+ /**
727
+ * Hear of a session replaced by its successor, to carry state keyed on the old session id to the new one —
728
+ * `@pithy-sh/organization` moves the acting selection. The capability that owns sessions announces it through
729
+ * `announceSessionRotated` in `../session/rotated.ts`, so neither side imports the other.
730
+ */
731
+ sessionRotated?: SessionRotatedListener;
732
+ /**
733
+ * An account's email address, by user id, or `null` when it has none — the capability that owns accounts
734
+ * declares it (`@pithy-sh/auth`), so a capability holding state by address finds one without importing auth.
735
+ * Read on demand, never carried.
736
+ */
737
+ addressOf?: (env: Record<string, unknown>, userId: string) => Promise<string | null>;
647
738
  /**
648
739
  * Named D1 databases this capability contributes tables to (database name → {@link DatabaseSpec}).
649
740
  * `createBackend` merges every capability's slices per database (via `composeDatabases`) and serves
@@ -681,6 +772,24 @@ export interface Capability<
681
772
  * on their own `app` capability for their own tables, with the same types.
682
773
  */
683
774
  purge?: readonly PurgeContribution[];
775
+ /**
776
+ * How this capability's record of a purge is read back — declared by the capability that owns a purge
777
+ * subject and runs its purge: `@pithy-sh/auth` for a user, `@pithy-sh/organization` for an organization. The
778
+ * control plane's erasure attestation reads it. See `./erasure.ts`.
779
+ */
780
+ erasureRecords?: ErasureRecords;
781
+ /**
782
+ * This capability's own soft-delete window — declared by the capability that owns a deletion, from its
783
+ * own `retention.purgeAfterDays`. A Worker's `retention` block may replace it; `GET /control-plane/manifest`
784
+ * and `pithy doctor` report which window takes effect and where it came from. See `../retention/policy.ts`.
785
+ */
786
+ purgeWindow?: PurgeWindowDefault;
787
+ /**
788
+ * The audit actions that record this capability's lifecycle transitions beyond its erasure — a suspension, a
789
+ * reinstatement, a restore. The retention floor covers each, beside the actions `erasureRecords` names, so an
790
+ * operator lifting a suspension is kept as long as the deletion it reversed. See `../retention/policy.ts`.
791
+ */
792
+ lifecycleAuditActions?: readonly string[];
684
793
  /**
685
794
  * The role powers this capability's own routes gate on, through the acting-organization seam —
686
795
  * `media:manage`, `storage:manage`. Omit it when a capability asks no power, which is the normal case.
@@ -692,6 +801,12 @@ export interface Capability<
692
801
  * importing any of them.
693
802
  */
694
803
  powers?: readonly string[];
804
+ /**
805
+ * The Vectorize indexes this capability owns and re-embeds in a Workflow of its own — see `./vectorIndex.ts`.
806
+ * `pithy vector reprocess` reads them beside `vector()`'s own indexes, so one command re-embeds every
807
+ * index in the project. Omit it when a capability owns no index, which is the ordinary case.
808
+ */
809
+ vectorIndexes?: readonly VectorIndexContribution[];
695
810
  /** Bindings this capability needs in the env (normalized — `optional` is always set). */
696
811
  requiredBindings: BindingSpec[];
697
812
  }
@@ -702,8 +817,9 @@ export type CapabilityInput<
702
817
  Namespaces extends KvNamespaceSpecMap = Record<never, never>,
703
818
  Name extends string = string,
704
819
  Workflows extends WorkflowSpecMap = Record<never, never>,
705
- > = Omit<Capability<Databases, Namespaces, Name, Workflows>, "requiredBindings"> & {
820
+ > = Omit<Capability<Databases, Namespaces, Name, Workflows>, "requiredBindings" | "processing"> & {
706
821
  requiredBindings: BindingSpecInput[];
822
+ processing?: readonly ProcessingActivityInput[];
707
823
  };
708
824
 
709
825
  /**
@@ -719,16 +835,37 @@ export function defineCapability<
719
835
  const Workflows extends WorkflowSpecMap = Record<never, never>,
720
836
  >(input: CapabilityInput<Databases, Namespaces, Name, Workflows>): Capability<Databases, Namespaces, Name, Workflows> {
721
837
  // A retained table is declared on the `down`s that could drop it the moment the capability exists, so a
722
- // migration the runner is handed carries it however its provider was built (#588).
838
+ // migration the runner is handed carries it however its provider was built.
723
839
  for (const spec of Object.values(input.databases ?? {})) {
724
840
  if (spec.migrations && spec.retained) declareRetained(spec.migrations, spec.retained);
725
841
  }
726
842
  // A purge contribution no purge could run — a subject the kit does not know, or two for one — is refused
727
843
  // where it is written, naming the capability, rather than on the day somebody is erased.
728
844
  if (input.purge) assertPurgeContributions(input.name, input.purge);
845
+ // An index no reprocess could re-embed is refused the same way, naming the capability.
846
+ if (input.vectorIndexes) {
847
+ assertVectorIndexContributions(input.name, input.vectorIndexes, Object.keys(input.workflows ?? {}));
848
+ }
849
+ // A consent purpose outside the capability's own domain, or declared twice, is refused where it is written.
850
+ const consentPurposes = input.consentPurposes
851
+ ? parseConsentPurposes(input.name, input.consentPurposes, input.name)
852
+ : undefined;
853
+ const processing = input.processing ? parseProcessingActivities(input.name, input.processing, input.name) : undefined;
854
+ // A capability that declares room kinds needs the room's binding and serves the room's socket, so both are
855
+ // added here, once, rather than spelled by every capability that declares one and drifting from it.
856
+ const presence = input.presence && input.presence.rooms.length > 0 ? input.presence : undefined;
857
+ if (presence) assertPresenceDeclaration(input.name, presence, input.adminRoutes ?? []);
858
+ const requiredBindings =
859
+ presence && !input.requiredBindings.some((binding) => binding.name === PRESENCE_ROOMS_BINDING_SPEC.name)
860
+ ? [...input.requiredBindings, PRESENCE_ROOMS_BINDING_SPEC]
861
+ : input.requiredBindings;
862
+ const { processing: _declared, ...declared } = input;
729
863
  return {
730
- ...input,
731
- requiredBindings: input.requiredBindings.map((binding) => BindingSpec.parse(binding)),
864
+ ...declared,
865
+ ...(consentPurposes ? { consentPurposes } : {}),
866
+ ...(processing ? { processing } : {}),
867
+ ...(presence ? { sockets: [...(input.sockets ?? []), presenceRoomsSocket(input.name, presence)] } : {}),
868
+ requiredBindings: requiredBindings.map((binding) => BindingSpec.parse(binding)),
732
869
  };
733
870
  }
734
871
 
@@ -0,0 +1,82 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { PurgeSubject } from "./purge";
5
+
6
+ /**
7
+ * **The erasure record (#823): what a capability that runs a purge kept about it, read back.**
8
+ *
9
+ * A purge (#620, #623) records its progress in the lifecycle table of the capability that owns the subject:
10
+ * `pithy_auth_user_lifecycle` for a user, `pithy_organization_lifecycle` for an organization. Each holds the
11
+ * deletion, one row per step (erased, or retained with its basis under #814), a failure when the purge gave up, and
12
+ * the `purged` row at the end, keyed by the subject's id and nothing else.
13
+ *
14
+ * The control plane's erasure attestation assembles those rows into a record an auditor can be handed. It cannot
15
+ * import auth or organization (principle 4), so the capability that owns a subject declares how its rows are read,
16
+ * here, and which audit actions its purge records — so the attestation can cite them in the chain (#822).
17
+ *
18
+ * **Nothing about the person crosses this seam** beyond the id the trail already keeps: no address, no name.
19
+ */
20
+
21
+ /** What happened, as the lifecycle tables name it. Only the rows an erasure is made of. */
22
+ export type ErasureHistoryAction = "deleted" | "restored" | "purge_step" | "purge_retained" | "purge_failed" | "purged";
23
+
24
+ /** How a deletion was asked for: with a window (`soft`) or none (`hard`). */
25
+ export type ErasureDeletionMode = "soft" | "hard";
26
+
27
+ /** One row of a subject's lifecycle, as the attestation reads it. Oldest first, in the order it was written. */
28
+ export interface ErasureHistoryRow {
29
+ /** The subject's id. */
30
+ readonly subjectId: string;
31
+ /** What happened. */
32
+ readonly action: ErasureHistoryAction;
33
+ /** Who did it: `control-plane`, `user` or `system`. */
34
+ readonly actorType: string;
35
+ /** Why, in whoever acted's words. A step's summary, a retained step's basis. Null when nobody had to say. */
36
+ readonly reason: string | null;
37
+ /** The purge step a step, retained or failed row names — the capability, or the purge's own step. */
38
+ readonly step: string | null;
39
+ /** On a `deleted` row written since #823, how the deletion was asked for. Null before. */
40
+ readonly mode: ErasureDeletionMode | null;
41
+ /** On a `deleted` row written since #823, when its purge comes due. Null before. */
42
+ readonly purgeAfter: Date | null;
43
+ /** When it happened. */
44
+ readonly at: Date;
45
+ }
46
+
47
+ /** The audit actions one subject's purge records, so the attestation cites them. */
48
+ export interface ErasureAuditActions {
49
+ /** Every action a deletion of the subject is recorded under — soft, hard, by its owner. */
50
+ readonly deleted: readonly string[];
51
+ /** One purge step: erased or retained, its `metadata.step` naming which. */
52
+ readonly step: string;
53
+ /** The purge giving up, its `metadata.step` naming where. */
54
+ readonly failed: string;
55
+ /** The purge finishing. */
56
+ readonly purged: string;
57
+ }
58
+
59
+ /** Reading one kind of subject's erasure rows, over one env. */
60
+ export interface ErasureHistoryReader {
61
+ /** Every erasure row for these subjects, oldest first. A subject with none is absent. */
62
+ history(subjectIds: readonly string[]): Promise<ErasureHistoryRow[]>;
63
+ /**
64
+ * Every subject with a purge in the window: a step, a failure or a completion written in it, a deletion whose
65
+ * purge came due in it, or one still outstanding in it — due by its end (or recorded before a due instant was) and
66
+ * neither purged nor restored before it opened. The last is the purge that came due earlier and has written nothing
67
+ * since, which is the one a period read exists to name. Inclusive at both ends.
68
+ */
69
+ subjectsIn(since: Date, until: Date): Promise<string[]>;
70
+ }
71
+
72
+ /** What a capability that owns a purge subject declares: which subject, its trail, and how its rows are read. */
73
+ export interface ErasureRecords {
74
+ /** The subject this capability's purge erases. One capability per subject. */
75
+ readonly subject: PurgeSubject;
76
+ /** The `resourceType` its audit events are recorded against. */
77
+ readonly resourceType: string;
78
+ /** The audit actions its purge records. */
79
+ readonly auditActions: ErasureAuditActions;
80
+ /** The reader, over one env. */
81
+ readonly read: (env: Record<string, unknown>) => ErasureHistoryReader;
82
+ }
@@ -0,0 +1,83 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { z } from "zod";
5
+ import type { DatabaseSpecMap } from "../data/databases";
6
+
7
+ /**
8
+ * **Every capability that holds rows about a person answers for them** — erases them on a purge, or keeps them and
9
+ * says why, and in both cases exports them (#814). This is the check that holds a capability to it, from its own
10
+ * schema rather than from a list somebody maintains.
11
+ *
12
+ * A table carries a row about a person when a column names one: an id (`userId`, `ownerId`, `subjectId`, `actorId`,
13
+ * any `…UserId`, the two sides of an invite or a friendship) or an address (`email`, `recipient`, any `…Address`).
14
+ * A capability whose schema carries one must declare a `user` purge contribution with an `export`. `retain` requires
15
+ * one already; an erasing contribution may omit it, and for an adopter's own capability that is the honest
16
+ * `not_exported` — but no kit capability may.
17
+ *
18
+ * Over-reaching is cheap here and under-reaching is the defect: a column that names an operator or a sender rather
19
+ * than the person still costs only a contribution that finds nothing, while a person column the rule missed is rows
20
+ * an access request never answers for.
21
+ */
22
+
23
+ /** Column names that name a person outright. */
24
+ const PERSON_COLUMNS: ReadonlySet<string> = new Set([
25
+ "userId",
26
+ "ownerId",
27
+ "subjectId",
28
+ "actorId",
29
+ "inviterId",
30
+ "inviteeId",
31
+ "userA",
32
+ "userB",
33
+ "email",
34
+ "recipient",
35
+ ]);
36
+
37
+ /** Whether a column, by its camelCase name, names a person: an id of one, or an address. */
38
+ export function isPersonColumn(column: string): boolean {
39
+ return PERSON_COLUMNS.has(column) || column.endsWith("UserId") || column.endsWith("Address");
40
+ }
41
+
42
+ /** What the check reads off a capability — its name, its tables and its purge contributions. */
43
+ export interface PersonRowsSubject {
44
+ readonly name: string;
45
+ readonly databases?: DatabaseSpecMap;
46
+ readonly purge?: readonly { readonly subject: string; readonly export?: unknown }[];
47
+ }
48
+
49
+ /** One capability that holds rows about a person and does not answer for them. */
50
+ export interface PersonRowGap {
51
+ /** The capability. */
52
+ readonly capability: string;
53
+ /** Its person columns, as `table.column`. */
54
+ readonly columns: readonly string[];
55
+ /** `no_contribution` — no `user` purge contribution at all; `no_export` — one that does not export. */
56
+ readonly gap: "no_contribution" | "no_export";
57
+ }
58
+
59
+ /** Every person column in a capability's tables, as `table.column`, in declaration order. */
60
+ export function personColumnsOf(capability: PersonRowsSubject): string[] {
61
+ const found: string[] = [];
62
+ for (const spec of Object.values(capability.databases ?? {})) {
63
+ for (const [table, schema] of Object.entries(spec.tables)) {
64
+ const shape = (schema as z.ZodObject).shape ?? {};
65
+ for (const column of Object.keys(shape)) if (isPersonColumn(column)) found.push(`${table}.${column}`);
66
+ }
67
+ }
68
+ return found;
69
+ }
70
+
71
+ /** Every capability among `capabilities` that holds rows about a person and does not answer for them. */
72
+ export function personRowGaps(capabilities: readonly PersonRowsSubject[]): PersonRowGap[] {
73
+ const gaps: PersonRowGap[] = [];
74
+ for (const capability of capabilities) {
75
+ const columns = personColumnsOf(capability);
76
+ if (columns.length === 0) continue;
77
+ const contribution = capability.purge?.find((candidate) => candidate.subject === "user");
78
+ if (!contribution) gaps.push({ capability: capability.name, columns, gap: "no_contribution" });
79
+ else if (typeof contribution.export !== "function")
80
+ gaps.push({ capability: capability.name, columns, gap: "no_export" });
81
+ }
82
+ return gaps;
83
+ }