@pithy-sh/core 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +54 -0
  2. package/dist/audit/auditEvent.js +1 -1
  3. package/dist/audit/chain.d.ts +289 -0
  4. package/dist/audit/chain.d.ts.map +1 -0
  5. package/dist/audit/chain.js +400 -0
  6. package/dist/audit/citations.d.ts +52 -0
  7. package/dist/audit/citations.d.ts.map +1 -0
  8. package/dist/audit/citations.js +13 -0
  9. package/dist/capability/bindings.d.ts +22 -0
  10. package/dist/capability/bindings.d.ts.map +1 -1
  11. package/dist/capability/bindings.js +32 -2
  12. package/dist/capability/capability.d.ts +123 -16
  13. package/dist/capability/capability.d.ts.map +1 -1
  14. package/dist/capability/capability.js +17 -2
  15. package/dist/capability/erasure.d.ts +75 -0
  16. package/dist/capability/erasure.d.ts.map +1 -0
  17. package/dist/capability/erasure.js +2 -0
  18. package/dist/capability/manifest.d.ts +5 -0
  19. package/dist/capability/manifest.d.ts.map +1 -1
  20. package/dist/capability/personColumns.d.ts +28 -0
  21. package/dist/capability/personColumns.d.ts.map +1 -0
  22. package/dist/capability/personColumns.js +66 -0
  23. package/dist/capability/personRows.d.ts +62 -0
  24. package/dist/capability/personRows.d.ts.map +1 -0
  25. package/dist/capability/personRows.js +59 -0
  26. package/dist/capability/purge.d.ts +86 -19
  27. package/dist/capability/purge.d.ts.map +1 -1
  28. package/dist/capability/purge.js +83 -5
  29. package/dist/capability/settings.d.ts +10 -0
  30. package/dist/capability/settings.d.ts.map +1 -1
  31. package/dist/capability/vectorIndex.d.ts +36 -0
  32. package/dist/capability/vectorIndex.d.ts.map +1 -0
  33. package/dist/capability/vectorIndex.js +25 -0
  34. package/dist/cli/commandCatalog.generated.d.ts +438 -3
  35. package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
  36. package/dist/cli/commandCatalog.generated.js +454 -2
  37. package/dist/cli/commandFacts.d.ts +8 -5
  38. package/dist/cli/commandFacts.d.ts.map +1 -1
  39. package/dist/cli/commandFacts.js +12 -3
  40. package/dist/consent/purpose.d.ts +70 -0
  41. package/dist/consent/purpose.d.ts.map +1 -0
  42. package/dist/consent/purpose.js +97 -0
  43. package/dist/consent/reader.d.ts +99 -0
  44. package/dist/consent/reader.d.ts.map +1 -0
  45. package/dist/consent/reader.js +33 -0
  46. package/dist/controlPlane/audit/actions.d.ts +15 -4
  47. package/dist/controlPlane/audit/actions.d.ts.map +1 -1
  48. package/dist/controlPlane/audit/actions.js +16 -5
  49. package/dist/controlPlane/capability.d.ts +10 -0
  50. package/dist/controlPlane/capability.d.ts.map +1 -1
  51. package/dist/controlPlane/capability.js +56 -8
  52. package/dist/controlPlane/context.d.ts +4 -0
  53. package/dist/controlPlane/context.d.ts.map +1 -1
  54. package/dist/controlPlane/context.js +3 -2
  55. package/dist/controlPlane/data/connection.d.ts +21 -2
  56. package/dist/controlPlane/data/connection.d.ts.map +1 -1
  57. package/dist/controlPlane/data/connection.js +13 -4
  58. package/dist/controlPlane/data/connections.d.ts +94 -0
  59. package/dist/controlPlane/data/connections.d.ts.map +1 -0
  60. package/dist/controlPlane/data/connections.js +151 -0
  61. package/dist/controlPlane/data/hold.d.ts +46 -0
  62. package/dist/controlPlane/data/hold.d.ts.map +1 -0
  63. package/dist/controlPlane/data/hold.js +36 -0
  64. package/dist/controlPlane/data/oidcTrust.d.ts +122 -0
  65. package/dist/controlPlane/data/oidcTrust.d.ts.map +1 -0
  66. package/dist/controlPlane/data/oidcTrust.js +173 -0
  67. package/dist/controlPlane/data/tables.d.ts +4 -0
  68. package/dist/controlPlane/data/tables.d.ts.map +1 -1
  69. package/dist/controlPlane/data/tables.js +9 -4
  70. package/dist/controlPlane/data/trustFirstCall.d.ts +16 -0
  71. package/dist/controlPlane/data/trustFirstCall.d.ts.map +1 -0
  72. package/dist/controlPlane/data/trustFirstCall.js +25 -0
  73. package/dist/controlPlane/discovery/adminRoute.d.ts +165 -2
  74. package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -1
  75. package/dist/controlPlane/discovery/adminRoute.js +32 -8
  76. package/dist/controlPlane/discovery/routeInput.d.ts +23 -0
  77. package/dist/controlPlane/discovery/routeInput.d.ts.map +1 -0
  78. package/dist/controlPlane/discovery/routeInput.js +80 -0
  79. package/dist/controlPlane/discovery/toolName.d.ts +21 -0
  80. package/dist/controlPlane/discovery/toolName.d.ts.map +1 -0
  81. package/dist/controlPlane/discovery/toolName.js +25 -0
  82. package/dist/controlPlane/erasure/attest.d.ts +78 -0
  83. package/dist/controlPlane/erasure/attest.d.ts.map +1 -0
  84. package/dist/controlPlane/erasure/attest.js +164 -0
  85. package/dist/controlPlane/erasure/read.d.ts +39 -0
  86. package/dist/controlPlane/erasure/read.d.ts.map +1 -0
  87. package/dist/controlPlane/erasure/read.js +140 -0
  88. package/dist/controlPlane/error/errors.d.ts +61 -2
  89. package/dist/controlPlane/error/errors.d.ts.map +1 -1
  90. package/dist/controlPlane/error/errors.js +13 -0
  91. package/dist/controlPlane/evidence/derive.d.ts +124 -0
  92. package/dist/controlPlane/evidence/derive.d.ts.map +1 -0
  93. package/dist/controlPlane/evidence/derive.js +343 -0
  94. package/dist/controlPlane/evidence/mapping.d.ts +85 -0
  95. package/dist/controlPlane/evidence/mapping.d.ts.map +1 -0
  96. package/dist/controlPlane/evidence/mapping.js +114 -0
  97. package/dist/controlPlane/evidence/pack.d.ts +46 -0
  98. package/dist/controlPlane/evidence/pack.d.ts.map +1 -0
  99. package/dist/controlPlane/evidence/pack.js +124 -0
  100. package/dist/controlPlane/http/connections.d.ts +96 -0
  101. package/dist/controlPlane/http/connections.d.ts.map +1 -0
  102. package/dist/controlPlane/http/connections.js +109 -0
  103. package/dist/controlPlane/http/guard.d.ts +23 -2
  104. package/dist/controlPlane/http/guard.d.ts.map +1 -1
  105. package/dist/controlPlane/http/guard.js +69 -12
  106. package/dist/controlPlane/http/handlers.d.ts +212 -10
  107. package/dist/controlPlane/http/handlers.d.ts.map +1 -1
  108. package/dist/controlPlane/http/handlers.js +117 -26
  109. package/dist/controlPlane/http/holds.d.ts +57 -0
  110. package/dist/controlPlane/http/holds.d.ts.map +1 -0
  111. package/dist/controlPlane/http/holds.js +98 -0
  112. package/dist/controlPlane/http/presence.d.ts +27 -0
  113. package/dist/controlPlane/http/presence.d.ts.map +1 -0
  114. package/dist/controlPlane/http/presence.js +67 -0
  115. package/dist/controlPlane/http/processing.d.ts +73 -0
  116. package/dist/controlPlane/http/processing.d.ts.map +1 -0
  117. package/dist/controlPlane/http/processing.js +17 -0
  118. package/dist/controlPlane/http/responses.d.ts +927 -3
  119. package/dist/controlPlane/http/responses.d.ts.map +1 -1
  120. package/dist/controlPlane/http/responses.js +283 -7
  121. package/dist/controlPlane/http/routes.d.ts +12 -2
  122. package/dist/controlPlane/http/routes.d.ts.map +1 -1
  123. package/dist/controlPlane/http/routes.js +182 -5
  124. package/dist/controlPlane/http/schemas.d.ts +63 -0
  125. package/dist/controlPlane/http/schemas.d.ts.map +1 -1
  126. package/dist/controlPlane/http/schemas.js +39 -1
  127. package/dist/controlPlane/http/verify.d.ts.map +1 -1
  128. package/dist/controlPlane/http/verify.js +13 -1
  129. package/dist/controlPlane/http/verifyTrust.d.ts +76 -0
  130. package/dist/controlPlane/http/verifyTrust.d.ts.map +1 -0
  131. package/dist/controlPlane/http/verifyTrust.js +75 -0
  132. package/dist/controlPlane/migrations/0004_connection_trusts.d.ts +15 -0
  133. package/dist/controlPlane/migrations/0004_connection_trusts.d.ts.map +1 -0
  134. package/dist/controlPlane/migrations/0004_connection_trusts.js +23 -0
  135. package/dist/controlPlane/migrations/0005_holds.d.ts +12 -0
  136. package/dist/controlPlane/migrations/0005_holds.d.ts.map +1 -0
  137. package/dist/controlPlane/migrations/0005_holds.js +26 -0
  138. package/dist/controlPlane/processing/derive.d.ts +77 -0
  139. package/dist/controlPlane/processing/derive.d.ts.map +1 -0
  140. package/dist/controlPlane/processing/derive.js +206 -0
  141. package/dist/controlPlane/processing/read.d.ts +10 -0
  142. package/dist/controlPlane/processing/read.d.ts.map +1 -0
  143. package/dist/controlPlane/processing/read.js +89 -0
  144. package/dist/controlPlane/scope/kind.d.ts +46 -0
  145. package/dist/controlPlane/scope/kind.d.ts.map +1 -0
  146. package/dist/controlPlane/scope/kind.js +52 -0
  147. package/dist/controlPlane/scope/scope.d.ts +58 -3
  148. package/dist/controlPlane/scope/scope.d.ts.map +1 -1
  149. package/dist/controlPlane/scope/scope.js +59 -4
  150. package/dist/controlPlane/token/jws.js +1 -1
  151. package/dist/controlPlane/token/mint.js +1 -1
  152. package/dist/controlPlane/wire.d.ts +23 -0
  153. package/dist/controlPlane/wire.d.ts.map +1 -1
  154. package/dist/controlPlane/wire.js +24 -1
  155. package/dist/createBackend.d.ts +13 -0
  156. package/dist/createBackend.d.ts.map +1 -1
  157. package/dist/createBackend.js +11 -3
  158. package/dist/createEntrypoint.d.ts +7 -0
  159. package/dist/createEntrypoint.d.ts.map +1 -1
  160. package/dist/createEntrypoint.js +4 -0
  161. package/dist/error/http.d.ts +168 -0
  162. package/dist/error/http.d.ts.map +1 -1
  163. package/dist/error/payload.d.ts +336 -0
  164. package/dist/error/payload.d.ts.map +1 -1
  165. package/dist/error/payload.js +112 -0
  166. package/dist/error/pithyError.d.ts +22 -0
  167. package/dist/error/pithyError.d.ts.map +1 -1
  168. package/dist/error/pithyError.js +31 -1
  169. package/dist/http/routeSpec.d.ts +46 -1
  170. package/dist/http/routeSpec.d.ts.map +1 -1
  171. package/dist/http/routeSpec.js +39 -5
  172. package/dist/http/verification.d.ts.map +1 -1
  173. package/dist/http/verification.js +1 -1
  174. package/dist/i18n/translator.d.ts +2 -1
  175. package/dist/i18n/translator.d.ts.map +1 -1
  176. package/dist/index.d.ts +1 -1
  177. package/dist/index.d.ts.map +1 -1
  178. package/dist/index.js +3 -3
  179. package/dist/logger/record.d.ts +98 -0
  180. package/dist/logger/record.d.ts.map +1 -1
  181. package/dist/mcp/annotations.d.ts +20 -0
  182. package/dist/mcp/annotations.d.ts.map +1 -0
  183. package/dist/mcp/annotations.js +23 -0
  184. package/dist/mcp/tools.d.ts +75 -0
  185. package/dist/mcp/tools.d.ts.map +1 -0
  186. package/dist/mcp/tools.js +118 -0
  187. package/dist/openapi/document.d.ts.map +1 -1
  188. package/dist/openapi/document.js +8 -26
  189. package/dist/openapi/operations.d.ts +35 -0
  190. package/dist/openapi/operations.d.ts.map +1 -0
  191. package/dist/openapi/operations.js +37 -0
  192. package/dist/presence/channel.d.ts +11 -0
  193. package/dist/presence/channel.d.ts.map +1 -0
  194. package/dist/presence/channel.js +65 -0
  195. package/dist/presence/declaration.d.ts +154 -0
  196. package/dist/presence/declaration.d.ts.map +1 -0
  197. package/dist/presence/declaration.js +164 -0
  198. package/dist/presence/durableObject.d.ts +61 -0
  199. package/dist/presence/durableObject.d.ts.map +1 -0
  200. package/dist/presence/durableObject.js +257 -0
  201. package/dist/presence/frames.d.ts +166 -0
  202. package/dist/presence/frames.d.ts.map +1 -0
  203. package/dist/presence/frames.js +90 -0
  204. package/dist/presence/http/guard.d.ts +11 -0
  205. package/dist/presence/http/guard.d.ts.map +1 -0
  206. package/dist/presence/http/guard.js +20 -0
  207. package/dist/presence/http/responses.d.ts +31 -0
  208. package/dist/presence/http/responses.d.ts.map +1 -0
  209. package/dist/presence/http/responses.js +34 -0
  210. package/dist/presence/http/routes.d.ts +24 -0
  211. package/dist/presence/http/routes.d.ts.map +1 -0
  212. package/dist/presence/http/routes.js +127 -0
  213. package/dist/presence/http/schemas.d.ts +29 -0
  214. package/dist/presence/http/schemas.d.ts.map +1 -0
  215. package/dist/presence/http/schemas.js +28 -0
  216. package/dist/presence/names.d.ts +23 -0
  217. package/dist/presence/names.d.ts.map +1 -0
  218. package/dist/presence/names.js +22 -0
  219. package/dist/presence/room.d.ts +95 -0
  220. package/dist/presence/room.d.ts.map +1 -0
  221. package/dist/presence/room.js +105 -0
  222. package/dist/presence/ticket.d.ts +55 -0
  223. package/dist/presence/ticket.d.ts.map +1 -0
  224. package/dist/presence/ticket.js +109 -0
  225. package/dist/privacy/classify.js +1 -1
  226. package/dist/privacy/encrypted.js +1 -1
  227. package/dist/processing/activity.d.ts +134 -0
  228. package/dist/processing/activity.d.ts.map +1 -0
  229. package/dist/processing/activity.js +161 -0
  230. package/dist/processing/composed.d.ts +18 -0
  231. package/dist/processing/composed.d.ts.map +1 -0
  232. package/dist/processing/composed.js +28 -0
  233. package/dist/processing/vocabulary.d.ts +26 -0
  234. package/dist/processing/vocabulary.d.ts.map +1 -0
  235. package/dist/processing/vocabulary.js +35 -0
  236. package/dist/queue/consumer.d.ts +59 -0
  237. package/dist/queue/consumer.d.ts.map +1 -0
  238. package/dist/queue/consumer.js +46 -0
  239. package/dist/retention/composed.d.ts +37 -0
  240. package/dist/retention/composed.d.ts.map +1 -0
  241. package/dist/retention/composed.js +66 -0
  242. package/dist/retention/holds.d.ts +61 -0
  243. package/dist/retention/holds.d.ts.map +1 -0
  244. package/dist/retention/holds.js +107 -0
  245. package/dist/retention/holdsPurge.d.ts +22 -0
  246. package/dist/retention/holdsPurge.d.ts.map +1 -0
  247. package/dist/retention/holdsPurge.js +50 -0
  248. package/dist/retention/policy.d.ts +132 -0
  249. package/dist/retention/policy.d.ts.map +1 -0
  250. package/dist/retention/policy.js +197 -0
  251. package/dist/session/rotated.d.ts +34 -0
  252. package/dist/session/rotated.d.ts.map +1 -0
  253. package/dist/session/rotated.js +25 -0
  254. package/dist/test-utils/connectionFixtures.d.ts +13 -0
  255. package/dist/test-utils/connectionFixtures.d.ts.map +1 -0
  256. package/dist/test-utils/connectionFixtures.js +58 -0
  257. package/dist/test-utils/interleave.d.ts +12 -0
  258. package/dist/test-utils/interleave.d.ts.map +1 -0
  259. package/dist/test-utils/interleave.js +26 -0
  260. package/dist/testWorker.d.ts +8 -0
  261. package/dist/testWorker.d.ts.map +1 -0
  262. package/dist/testWorker.js +9 -0
  263. package/dist/version.generated.d.ts +1 -1
  264. package/dist/version.generated.js +1 -1
  265. package/dist/workflow/dispatchRoute.js +1 -1
  266. package/package.json +1 -1
  267. package/src/audit/auditEvent.ts +1 -1
  268. package/src/audit/chain.ts +551 -0
  269. package/src/audit/citations.ts +63 -0
  270. package/src/capability/bindings.ts +61 -0
  271. package/src/capability/capability.ts +156 -19
  272. package/src/capability/erasure.ts +82 -0
  273. package/src/capability/personColumns.ts +83 -0
  274. package/src/capability/personRows.ts +125 -0
  275. package/src/capability/purge.ts +158 -21
  276. package/src/capability/settings.ts +7 -0
  277. package/src/capability/vectorIndex.ts +60 -0
  278. package/src/cli/commandCatalog.generated.ts +487 -2
  279. package/src/cli/commandFacts.ts +12 -3
  280. package/src/cloudflare-test.d.ts +3 -1
  281. package/src/consent/purpose.ts +160 -0
  282. package/src/consent/reader.ts +134 -0
  283. package/src/controlPlane/audit/actions.ts +15 -4
  284. package/src/controlPlane/capability.ts +99 -20
  285. package/src/controlPlane/context.ts +10 -2
  286. package/src/controlPlane/data/connection.ts +16 -2
  287. package/src/controlPlane/data/connections.ts +231 -0
  288. package/src/controlPlane/data/hold.ts +52 -0
  289. package/src/controlPlane/data/oidcTrust.ts +249 -0
  290. package/src/controlPlane/data/tables.ts +7 -0
  291. package/src/controlPlane/data/trustFirstCall.ts +37 -0
  292. package/src/controlPlane/discovery/adminRoute.ts +71 -9
  293. package/src/controlPlane/discovery/routeInput.ts +86 -0
  294. package/src/controlPlane/discovery/toolName.ts +23 -0
  295. package/src/controlPlane/erasure/attest.ts +205 -0
  296. package/src/controlPlane/erasure/read.ts +169 -0
  297. package/src/controlPlane/error/errors.ts +69 -2
  298. package/src/controlPlane/evidence/derive.ts +426 -0
  299. package/src/controlPlane/evidence/mapping.ts +118 -0
  300. package/src/controlPlane/evidence/pack.ts +190 -0
  301. package/src/controlPlane/http/connections.ts +129 -0
  302. package/src/controlPlane/http/guard.ts +118 -9
  303. package/src/controlPlane/http/handlers.ts +172 -40
  304. package/src/controlPlane/http/holds.ts +108 -0
  305. package/src/controlPlane/http/presence.ts +71 -0
  306. package/src/controlPlane/http/processing.ts +19 -0
  307. package/src/controlPlane/http/responses.ts +590 -7
  308. package/src/controlPlane/http/routes.ts +302 -3
  309. package/src/controlPlane/http/schemas.ts +126 -0
  310. package/src/controlPlane/http/verify.ts +19 -2
  311. package/src/controlPlane/http/verifyTrust.ts +176 -0
  312. package/src/controlPlane/migrations/0004_connection_trusts.ts +30 -0
  313. package/src/controlPlane/migrations/0005_holds.ts +40 -0
  314. package/src/controlPlane/processing/derive.ts +359 -0
  315. package/src/controlPlane/processing/read.ts +102 -0
  316. package/src/controlPlane/scope/kind.ts +63 -0
  317. package/src/controlPlane/scope/scope.ts +65 -3
  318. package/src/controlPlane/wire.ts +26 -0
  319. package/src/createBackend.ts +31 -6
  320. package/src/createEntrypoint.ts +17 -0
  321. package/src/error/payload.ts +234 -0
  322. package/src/error/pithyError.ts +42 -0
  323. package/src/http/routeSpec.ts +75 -5
  324. package/src/http/verification.ts +3 -1
  325. package/src/i18n/translator.ts +2 -1
  326. package/src/index.ts +2 -0
  327. package/src/mcp/annotations.ts +38 -0
  328. package/src/mcp/tools.ts +220 -0
  329. package/src/openapi/document.ts +8 -28
  330. package/src/openapi/operations.ts +62 -0
  331. package/src/presence/channel.ts +46 -0
  332. package/src/presence/declaration.ts +283 -0
  333. package/src/presence/durableObject.ts +287 -0
  334. package/src/presence/frames.ts +153 -0
  335. package/src/presence/http/guard.ts +23 -0
  336. package/src/presence/http/responses.ts +63 -0
  337. package/src/presence/http/routes.ts +165 -0
  338. package/src/presence/http/schemas.ts +39 -0
  339. package/src/presence/names.ts +45 -0
  340. package/src/presence/room.ts +145 -0
  341. package/src/presence/ticket.ts +145 -0
  342. package/src/processing/activity.ts +289 -0
  343. package/src/processing/composed.ts +33 -0
  344. package/src/processing/vocabulary.ts +43 -0
  345. package/src/queue/consumer.ts +104 -0
  346. package/src/retention/composed.ts +75 -0
  347. package/src/retention/holds.ts +175 -0
  348. package/src/retention/holdsPurge.ts +50 -0
  349. package/src/retention/policy.ts +305 -0
  350. package/src/session/rotated.ts +53 -0
  351. package/src/test-utils/connectionFixtures.ts +58 -0
  352. package/src/test-utils/interleave.ts +31 -0
  353. package/src/testWorker.ts +13 -0
  354. package/src/version.generated.ts +1 -1
@@ -0,0 +1,161 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { ValidationError, fromZodError } from "../error/pithyError.js";
4
+ import { PrivacyClassName } from "../privacy/classes.js";
5
+ import { ConsentPurposeId } from "../consent/purpose.js";
6
+ import { LegalBasis } from "./vocabulary.js";
7
+ import { z } from "zod";
8
+ //#region src/processing/activity.ts
9
+ /**
10
+ * Processing activities: what a Worker does with personal data, why, and on what legal basis — the declarations the
11
+ * GDPR Article 30 record is derived from.
12
+ *
13
+ * ## Declared where the processing is
14
+ *
15
+ * A capability states the activities it performs, under its own `name` (`auth.account`), the rule consent purposes,
16
+ * tables and error codes follow. The adopter adds their own in the `processing` block of a Worker's
17
+ * `pithy.config.ts`, and may replace a capability's legal basis there.
18
+ *
19
+ * ## A missing basis is a gap, never a guess
20
+ *
21
+ * `legalBasis` is optional. An activity without one is reported as a gap in the record, and nothing fills it in.
22
+ */
23
+ /** An activity id: `<domain>.<name>`, lowercase, the domain being the declaring capability's name. */
24
+ const ProcessingActivityId = z.string().regex(/^[a-z][a-z0-9-]*\.[a-z][a-z0-9_.-]*$/, "A processing activity id is `<domain>.<name>`: lowercase, the domain being the declaring capability's name.").max(128).describe("A processing activity id, `<domain>.<name>` — `auth.account`. The domain is the declaring capability's name, or the adopter's own domain for an activity declared in `pithy.config.ts`.");
25
+ /** Someone outside the Worker's own code who receives personal data for an activity. */
26
+ const ProcessingRecipient = z.object({
27
+ name: z.string().min(1).max(120).describe("Who receives it, as an auditor would name them — `Stripe`, `Cloudflare Email Service`."),
28
+ receives: z.string().min(1).max(500).describe("What they receive, in one sentence — `The buyer's email address and what they bought.`"),
29
+ classes: z.array(PrivacyClassName).default([]).describe("The data classes in what they receive — `pii`, `phi`, or an adopter's own.")
30
+ }).describe("One recipient of personal data: who it is, what it receives, and the classes in it.");
31
+ /** A consent purpose belongs with a consent basis and nowhere else. */
32
+ function consentPurposeNeedsConsent(value, ctx) {
33
+ if (value.consentPurpose !== void 0 && value.legalBasis !== "consent") ctx.addIssue({
34
+ code: "custom",
35
+ path: ["consentPurpose"],
36
+ message: "`consentPurpose` names the purpose a `consent` basis rests on. Set `legalBasis` to `consent`, or drop it."
37
+ });
38
+ }
39
+ /** One processing activity, as a capability or the adopter declares it. */
40
+ const ProcessingActivity = z.object({
41
+ id: ProcessingActivityId,
42
+ purpose: z.string().min(1).max(500).describe("Why the data is processed, in one sentence an auditor reads — `Keep an account and sign its holder in.`"),
43
+ classes: z.array(PrivacyClassName).default([]).describe("The data classes the activity uses — `pii`, `phi`, or an adopter's own. Cross-checked against the classes the Worker's schemas store."),
44
+ legalBasis: LegalBasis.optional().describe("The lawful basis it rests on. Absent is reported as a gap in the processing record, never filled in."),
45
+ consentPurpose: ConsentPurposeId.optional().describe("For a `consent` basis, the consent purpose the activity rests on — `email.marketing`. The record cites its grant counts."),
46
+ recipients: z.array(ProcessingRecipient).default([]).describe("Who receives the data beyond Cloudflare, which the record always lists — a payment rail, a mail provider. Built from the resolved config, so a rail that is off is not named.")
47
+ }).superRefine(consentPurposeNeedsConsent).describe("One processing activity: its purpose, the data classes it uses, its legal basis and its recipients.");
48
+ /** The adopter's replacement for a capability's legal basis. */
49
+ const ProcessingOverride = z.object({
50
+ legalBasis: LegalBasis.describe("The basis that replaces the capability's own."),
51
+ consentPurpose: ConsentPurposeId.optional().describe("For a `consent` basis, the consent purpose it rests on. Left out, a capability's own purpose is kept.")
52
+ }).strict().superRefine(consentPurposeNeedsConsent).describe("A legal basis the adopter states for a capability's activity, replacing the one it declares.");
53
+ /** The `processing` block, as a Worker's `pithy.config.ts` writes it. */
54
+ const ProcessingPolicy = z.object({
55
+ activities: z.array(ProcessingActivity).optional().describe("Your own processing activities, beside the ones composed capabilities declare. Each under your own domain."),
56
+ overrides: z.record(ProcessingActivityId, ProcessingOverride).optional().describe("A legal basis per capability activity, replacing the one it declares: `{ \"auth.account\": { legalBasis: \"legitimate-interests\" } }`.")
57
+ }).strict().describe("What this Worker does with personal data beyond what its capabilities declare, and the bases you state.");
58
+ /** The domain half of an activity id: `auth` for `auth.account`. */
59
+ function activityDomain(id) {
60
+ const dot = id.indexOf(".");
61
+ return dot === -1 ? id : id.slice(0, dot);
62
+ }
63
+ /**
64
+ * Parse one declarer's activities and refuse what the record could not attribute: an invalid activity, one under a
65
+ * domain that is not the declarer's, or two with one id. `domain` is the declaring capability's name, or `null` for
66
+ * the adopter's own, which may use any domain but a reserved one.
67
+ */
68
+ function parseProcessingActivities(declarer, activities, domain, reserved = /* @__PURE__ */ new Set()) {
69
+ const seen = /* @__PURE__ */ new Set();
70
+ return activities.map((input) => {
71
+ const parsed = ProcessingActivity.safeParse(input);
72
+ if (!parsed.success) throw new ValidationError({
73
+ message: `${declarer} declares a processing activity that is not valid: ${parsed.error.issues[0]?.message ?? "invalid"}.`,
74
+ action: "Give it an id of the form `<domain>.<name>` and a purpose; name a consent purpose only under a consent basis."
75
+ });
76
+ const activity = parsed.data;
77
+ const own = activityDomain(activity.id);
78
+ if (domain !== null && own !== domain) throw new ValidationError({
79
+ message: `The ${declarer} capability declares the processing activity "${activity.id}" outside its own domain.`,
80
+ action: `Name it under ${domain}. — a capability declares activities only under its own name.`
81
+ });
82
+ if (domain === null && reserved.has(own)) throw new ValidationError({
83
+ message: `The processing activity "${activity.id}" is under ${own}., which a composed capability owns.`,
84
+ action: "Name your own activities under your own domain, such as your app's name."
85
+ });
86
+ if (seen.has(activity.id)) throw new ValidationError({
87
+ message: `${declarer} declares the processing activity "${activity.id}" twice.`,
88
+ action: "Declare each activity once."
89
+ });
90
+ seen.add(activity.id);
91
+ return activity;
92
+ });
93
+ }
94
+ /** Parse a `processing` block, or refuse it naming what is wrong. `undefined` is the empty policy. */
95
+ function parseProcessingPolicy(input) {
96
+ if (input === void 0) return {};
97
+ const parsed = ProcessingPolicy.safeParse(input);
98
+ if (!parsed.success) throw fromZodError(parsed.error, {
99
+ message: "The processing block in pithy.config.ts does not parse.",
100
+ action: "Fix the field named, then deploy again."
101
+ });
102
+ return parsed.data;
103
+ }
104
+ /** One declared activity, before any override. */
105
+ function resolved(activity, declaredBy, source) {
106
+ return {
107
+ id: activity.id,
108
+ purpose: activity.purpose,
109
+ classes: [...activity.classes],
110
+ legalBasis: activity.legalBasis ?? null,
111
+ basisSource: activity.legalBasis === void 0 ? null : source,
112
+ consentPurpose: activity.consentPurpose ?? null,
113
+ recipients: activity.recipients.map((recipient) => ({
114
+ ...recipient,
115
+ classes: [...recipient.classes]
116
+ })),
117
+ declaredBy
118
+ };
119
+ }
120
+ /**
121
+ * Every activity as it takes effect over a composition: each capability's, in composition order, then the adopter's,
122
+ * with the block's overrides applied. Refuses an adopter activity under a composed capability's domain, an id declared
123
+ * twice, and an override naming an activity no composed capability declares. Called when a Worker assembles.
124
+ */
125
+ function resolveProcessing(policy, capabilities) {
126
+ const reserved = new Set(capabilities.map((capability) => capability.name));
127
+ const own = parseProcessingActivities("pithy.config.ts", policy.activities ?? [], null, reserved);
128
+ const declared = capabilities.flatMap((capability) => (capability.processing ?? []).map((activity) => resolved(activity, capability.name, "capability")));
129
+ const ids = /* @__PURE__ */ new Set();
130
+ for (const activity of declared) {
131
+ if (ids.has(activity.id)) throw new ValidationError({
132
+ message: `The processing activity "${activity.id}" is declared twice.`,
133
+ action: "Declare each activity once."
134
+ });
135
+ ids.add(activity.id);
136
+ }
137
+ const overrides = policy.overrides ?? {};
138
+ for (const id of Object.keys(overrides)) {
139
+ if (own.some((activity) => activity.id === id)) throw new ValidationError({
140
+ message: `The processing block overrides "${id}", which is declared in the same block.`,
141
+ action: `State its legal basis on the activity itself.`
142
+ });
143
+ if (!ids.has(id)) throw new ValidationError({
144
+ message: `The processing block overrides "${id}", which no composed capability declares.`,
145
+ action: "Name an activity a composed capability declares. `GET /control-plane/processing` lists them."
146
+ });
147
+ }
148
+ return [...declared.map((activity) => {
149
+ const override = overrides[activity.id];
150
+ if (override === void 0) return activity;
151
+ const consentPurpose = override.legalBasis === "consent" ? override.consentPurpose ?? activity.consentPurpose : null;
152
+ return {
153
+ ...activity,
154
+ legalBasis: override.legalBasis,
155
+ basisSource: "config",
156
+ consentPurpose
157
+ };
158
+ }), ...own.map((activity) => resolved(activity, "config", "config"))];
159
+ }
160
+ //#endregion
161
+ export { ProcessingActivity, ProcessingActivityId, ProcessingOverride, ProcessingPolicy, ProcessingRecipient, activityDomain, parseProcessingActivities, parseProcessingPolicy, resolveProcessing };
@@ -0,0 +1,18 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { Capability } from "../capability/capability";
4
+ import { type ResolvedActivity } from "./activity";
5
+ /**
6
+ * The processing activities as this Worker resolved them at assembly, reachable from module scope, so the control
7
+ * plane reads the same record `createBackend` refused or accepted. One Worker assembles one backend; a second assembly
8
+ * in one isolate replaces the first wholesale, as `../retention/composed.ts` does for the retention policy.
9
+ */
10
+ /** Parse and resolve a Worker's `processing` block over what it composes, or refuse it. */
11
+ export declare function effectiveProcessing(block: unknown, capabilities: readonly Capability[]): ResolvedActivity[];
12
+ /** Record the resolved activities. Called once by `createBackend`. */
13
+ export declare function recordProcessing(activities: readonly ResolvedActivity[]): void;
14
+ /** Forget them. For a test that needs the un-assembled state back. */
15
+ export declare function forgetProcessing(): void;
16
+ /** The resolved activities, or `null` before any backend has been assembled in this isolate. */
17
+ export declare function composedProcessing(): readonly ResolvedActivity[] | null;
18
+ //# sourceMappingURL=composed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composed.d.ts","sourceRoot":"","sources":["../../src/processing/composed.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAyB,KAAK,gBAAgB,EAAqB,MAAM,YAAY,CAAC;AAE7F;;;;GAIG;AAEH,2FAA2F;AAC3F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,UAAU,EAAE,GAAG,gBAAgB,EAAE,CAE3G;AAID,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAE9E;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED,gGAAgG;AAChG,wBAAgB,kBAAkB,IAAI,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAEvE"}
@@ -0,0 +1,28 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { parseProcessingPolicy, resolveProcessing } from "./activity.js";
4
+ //#region src/processing/composed.ts
5
+ /**
6
+ * The processing activities as this Worker resolved them at assembly, reachable from module scope, so the control
7
+ * plane reads the same record `createBackend` refused or accepted. One Worker assembles one backend; a second assembly
8
+ * in one isolate replaces the first wholesale, as `../retention/composed.ts` does for the retention policy.
9
+ */
10
+ /** Parse and resolve a Worker's `processing` block over what it composes, or refuse it. */
11
+ function effectiveProcessing(block, capabilities) {
12
+ return resolveProcessing(parseProcessingPolicy(block), capabilities);
13
+ }
14
+ let recorded = null;
15
+ /** Record the resolved activities. Called once by `createBackend`. */
16
+ function recordProcessing(activities) {
17
+ recorded = activities;
18
+ }
19
+ /** Forget them. For a test that needs the un-assembled state back. */
20
+ function forgetProcessing() {
21
+ recorded = null;
22
+ }
23
+ /** The resolved activities, or `null` before any backend has been assembled in this isolate. */
24
+ function composedProcessing() {
25
+ return recorded;
26
+ }
27
+ //#endregion
28
+ export { composedProcessing, effectiveProcessing, forgetProcessing, recordProcessing };
@@ -0,0 +1,26 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { z } from "zod";
4
+ /** The processing record's closed vocabularies, apart from the walk that fills them, so a browser may import them. */
5
+ /** What kind of gap the record names. */
6
+ export declare const PROCESSING_GAP_KINDS: readonly ["no-legal-basis", "no-consent-purpose", "class-not-stored", "class-unclaimed", "phi-not-covered", "no-standing-grant"];
7
+ export type ProcessingGapKindId = (typeof PROCESSING_GAP_KINDS)[number];
8
+ /** Where a recipient came from. */
9
+ export declare const RECIPIENT_SOURCES: readonly ["platform", "binding", "declared"];
10
+ export type RecipientSource = (typeof RECIPIENT_SOURCES)[number];
11
+ /** Where a legal basis came from: the declaring capability, or the `processing` block. */
12
+ export declare const BASIS_SOURCES: readonly ["capability", "config"];
13
+ export type BasisSource = (typeof BASIS_SOURCES)[number];
14
+ /** The six lawful bases of GDPR Article 6(1), (a) to (f), in that order. */
15
+ export declare const LEGAL_BASES: readonly ["consent", "contract", "legal-obligation", "vital-interests", "public-task", "legitimate-interests"];
16
+ /** One lawful basis for processing. */
17
+ export declare const LegalBasis: z.ZodEnum<{
18
+ consent: "consent";
19
+ contract: "contract";
20
+ "legal-obligation": "legal-obligation";
21
+ "legitimate-interests": "legitimate-interests";
22
+ "public-task": "public-task";
23
+ "vital-interests": "vital-interests";
24
+ }>;
25
+ export type LegalBasis = z.infer<typeof LegalBasis>;
26
+ //# sourceMappingURL=vocabulary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vocabulary.d.ts","sourceRoot":"","sources":["../../src/processing/vocabulary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,sHAAsH;AAEtH,yCAAyC;AACzC,eAAO,MAAM,oBAAoB,YAC/B,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,CACX,CAAC;AACX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,mCAAmC;AACnC,eAAO,MAAM,iBAAiB,YAAI,UAAU,EAAE,SAAS,EAAE,UAAU,CAAU,CAAC;AAC9E,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,0FAA0F;AAC1F,eAAO,MAAM,aAAa,YAAI,YAAY,EAAE,QAAQ,CAAU,CAAC;AAC/D,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,4EAA4E;AAC5E,eAAO,MAAM,WAAW,YACtB,SAAS,EACT,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,sBAAsB,CACd,CAAC;AAEX,uCAAuC;AACvC,eAAO,MAAM,UAAU;;;;;;;EAIpB,CAAC;AACJ,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC"}
@@ -0,0 +1,35 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { z } from "zod";
4
+ //#region src/processing/vocabulary.ts
5
+ /** The processing record's closed vocabularies, apart from the walk that fills them, so a browser may import them. */
6
+ /** What kind of gap the record names. */
7
+ const PROCESSING_GAP_KINDS = [
8
+ "no-legal-basis",
9
+ "no-consent-purpose",
10
+ "class-not-stored",
11
+ "class-unclaimed",
12
+ "phi-not-covered",
13
+ "no-standing-grant"
14
+ ];
15
+ /** Where a recipient came from. */
16
+ const RECIPIENT_SOURCES = [
17
+ "platform",
18
+ "binding",
19
+ "declared"
20
+ ];
21
+ /** Where a legal basis came from: the declaring capability, or the `processing` block. */
22
+ const BASIS_SOURCES = ["capability", "config"];
23
+ /** The six lawful bases of GDPR Article 6(1), (a) to (f), in that order. */
24
+ const LEGAL_BASES = [
25
+ "consent",
26
+ "contract",
27
+ "legal-obligation",
28
+ "vital-interests",
29
+ "public-task",
30
+ "legitimate-interests"
31
+ ];
32
+ /** One lawful basis for processing. */
33
+ const LegalBasis = z.enum(LEGAL_BASES).describe("The lawful basis of GDPR Article 6(1): `consent` (a), `contract` (b), `legal-obligation` (c), `vital-interests` (d), `public-task` (e) or `legitimate-interests` (f). A `consent` basis names the consent purpose it rests on.");
34
+ //#endregion
35
+ export { BASIS_SOURCES, LEGAL_BASES, LegalBasis, PROCESSING_GAP_KINDS, RECIPIENT_SOURCES };
@@ -0,0 +1,59 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { ExecutionContext } from "@cloudflare/workers-types";
4
+ import type { Logger } from "../logger/logger";
5
+ /**
6
+ * **A capability's queue consumer, and how one Worker's `queue` export finds it (#824).**
7
+ *
8
+ * A Worker has one `queue` handler for every queue it consumes, the way it has one `email` and one `scheduled`.
9
+ * Which capability a message belongs to cannot be read off the batch: `batch.queue` is the provisioned name,
10
+ * `<project>-<env>-<thing>`, which no capability knows at runtime and which a long name truncates. So the message
11
+ * says it: every body a capability sends carries its own name under {@link QUEUE_CAPABILITY_FIELD}, built with
12
+ * {@link queueBody}, and the dispatcher hands each capability exactly the messages that name it.
13
+ *
14
+ * **A message no composed capability claims is acknowledged and logged**, never retried. Retrying cannot make a
15
+ * capability appear, so a retry would only walk the message to its dead-letter queue and count it as a delivery
16
+ * failure it never was.
17
+ *
18
+ * **A handler that throws costs its own messages, and nothing else.** Every other capability's messages are still
19
+ * handed over; the first failure is rethrown after the pass, so the invocation is recorded as failed and the runtime
20
+ * retries what was neither acknowledged nor retried.
21
+ */
22
+ /**
23
+ * One delivered message, as much of the runtime's as a consumer touches. Stated here rather than imported, so the
24
+ * Workers types' `Message` — a word half the kit uses for other things — never becomes a name the kit reaches for.
25
+ */
26
+ export interface QueueMessage {
27
+ /** The runtime's id for this delivery. */
28
+ readonly id: string;
29
+ /** The body as sent. */
30
+ readonly body: unknown;
31
+ /** How many times this message has been delivered, this one included. Starts at 1. */
32
+ readonly attempts: number;
33
+ /** Settle it: delivered, never to be seen again. */
34
+ ack(): void;
35
+ /** Settle it: deliver it again after the delay, or to the dead-letter queue once retries are spent. */
36
+ retry(options?: {
37
+ delaySeconds?: number;
38
+ }): void;
39
+ }
40
+ /** One batch the runtime hands a Worker's `queue` export. */
41
+ export interface QueueBatch {
42
+ /** The provisioned queue's name. */
43
+ readonly queue: string;
44
+ /** Its messages. */
45
+ readonly messages: readonly QueueMessage[];
46
+ }
47
+ /** The body field that names the capability a queue message belongs to. */
48
+ export declare const QUEUE_CAPABILITY_FIELD = "capability";
49
+ /** A capability's queue handler: the messages that name it, from one batch. */
50
+ export type CapabilityQueueHandler = (messages: readonly QueueMessage[], env: Record<string, unknown>, ctx: ExecutionContext) => Promise<void>;
51
+ /** A message body, stamped with the capability it belongs to so the Worker's one `queue` export can route it. */
52
+ export declare function queueBody<const Capability extends string, const Body extends Record<string, unknown>>(capability: Capability, body: Body): Body & {
53
+ capability: Capability;
54
+ };
55
+ /** The capability a message body names, or null when it names none. */
56
+ export declare function queueMessageCapability(body: unknown): string | null;
57
+ /** Hand each composed capability the messages of one batch that name it. */
58
+ export declare function dispatchQueueBatch(batch: QueueBatch, handlers: ReadonlyMap<string, CapabilityQueueHandler>, env: Record<string, unknown>, ctx: ExecutionContext, log?: Pick<Logger, "warn">): Promise<void>;
59
+ //# sourceMappingURL=consumer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../../src/queue/consumer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;;;;;;;;;;;;;;GAgBG;AAEH;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,wBAAwB;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oDAAoD;IACpD,GAAG,IAAI,IAAI,CAAC;IACZ,uGAAuG;IACvG,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAClD;AAED,6DAA6D;AAC7D,MAAM,WAAW,UAAU;IACzB,oCAAoC;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oBAAoB;IACpB,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;CAC5C;AAED,2EAA2E;AAC3E,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAEnD,+EAA+E;AAC/E,MAAM,MAAM,sBAAsB,GAAG,CACnC,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,iHAAiH;AACjH,wBAAgB,SAAS,CAAC,KAAK,CAAC,UAAU,SAAS,MAAM,EAAE,KAAK,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnG,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,IAAI,GACT,IAAI,GAAG;IAAE,UAAU,EAAE,UAAU,CAAA;CAAE,CAEnC;AAED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAInE;AAED,4EAA4E;AAC5E,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,sBAAsB,CAAC,EACrD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,GAAG,EAAE,gBAAgB,EACrB,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,GACzB,OAAO,CAAC,IAAI,CAAC,CAsBf"}
@@ -0,0 +1,46 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ //#region src/queue/consumer.ts
4
+ /** The body field that names the capability a queue message belongs to. */
5
+ const QUEUE_CAPABILITY_FIELD = "capability";
6
+ /** A message body, stamped with the capability it belongs to so the Worker's one `queue` export can route it. */
7
+ function queueBody(capability, body) {
8
+ return {
9
+ ...body,
10
+ capability
11
+ };
12
+ }
13
+ /** The capability a message body names, or null when it names none. */
14
+ function queueMessageCapability(body) {
15
+ if (typeof body !== "object" || body === null) return null;
16
+ const value = body[QUEUE_CAPABILITY_FIELD];
17
+ return typeof value === "string" && value.length > 0 ? value : null;
18
+ }
19
+ /** Hand each composed capability the messages of one batch that name it. */
20
+ async function dispatchQueueBatch(batch, handlers, env, ctx, log) {
21
+ const claimed = /* @__PURE__ */ new Map();
22
+ for (const message of batch.messages) {
23
+ const capability = queueMessageCapability(message.body);
24
+ if (capability === null || !handlers.has(capability)) {
25
+ log?.warn("queue message unclaimed", {
26
+ queue: batch.queue,
27
+ messageId: message.id,
28
+ capability
29
+ });
30
+ message.ack();
31
+ continue;
32
+ }
33
+ const list = claimed.get(capability) ?? [];
34
+ list.push(message);
35
+ claimed.set(capability, list);
36
+ }
37
+ const failures = [];
38
+ for (const [capability, messages] of claimed) try {
39
+ await handlers.get(capability)?.(messages, env, ctx);
40
+ } catch (error) {
41
+ failures.push(error);
42
+ }
43
+ if (failures[0] !== void 0) throw failures[0];
44
+ }
45
+ //#endregion
46
+ export { QUEUE_CAPABILITY_FIELD, dispatchQueueBatch, queueBody, queueMessageCapability };
@@ -0,0 +1,37 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { Capability } from "../capability/capability";
4
+ import type { PurgeSubject } from "../capability/purge";
5
+ import { type EffectiveRetention, type RetentionComposition } from "./policy";
6
+ /**
7
+ * **The retention policy as this Worker resolved it, reachable from module scope** — so a purge, a prune and
8
+ * an inactivity pass, each running in a Workflow the runtime constructs with `env` alone, read the same policy the
9
+ * routes do. The reasoning, and the cost, are `../capability/composition.ts`'s: `createBackend` runs at module load,
10
+ * one Worker assembles one backend, and a second assembly in one isolate replaces the first wholesale.
11
+ */
12
+ /**
13
+ * What resolving reads off a composition: each capability's window, and every lifecycle action it audits — the
14
+ * transitions it declares, then the ones its erasure records.
15
+ */
16
+ export declare function retentionComposition(capabilities: readonly Capability[]): RetentionComposition;
17
+ /**
18
+ * Parse and resolve a Worker's `retention` block over what it composes, or refuse it. What `createBackend` calls at
19
+ * assembly and `pithy doctor` calls on the same config, so both report — or refuse — one policy.
20
+ */
21
+ export declare function effectiveRetention(block: unknown, capabilities: readonly Capability[]): EffectiveRetention;
22
+ /** Record the resolved policy. Called once by `createBackend`, beside `recordComposition`. */
23
+ export declare function recordRetention(retention: EffectiveRetention): void;
24
+ /** Forget it. For a test that needs the un-assembled state back. */
25
+ export declare function forgetRetention(): void;
26
+ /**
27
+ * The resolved policy — or, before any assembly, `null`. A caller with a capability's own default in hand falls back
28
+ * to it, which is exactly what an empty block means.
29
+ */
30
+ export declare function composedRetention(): EffectiveRetention | null;
31
+ /**
32
+ * The privacy classes a subject's rows hold: every class marked on a schema of a capability that erases that
33
+ * subject in its purge — the capabilities that hold rows keyed to the subject. What the class ages are read against:
34
+ * an account holds `pii` when a capability holding its rows marks a field `pii`.
35
+ */
36
+ export declare function subjectClasses(capabilities: readonly Capability[], subject: PurgeSubject): string[];
37
+ //# sourceMappingURL=composed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composed.d.ts","sourceRoot":"","sources":["../../src/retention/composed.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAE,KAAK,kBAAkB,EAAwB,KAAK,oBAAoB,EAAoB,MAAM,UAAU,CAAC;AAEtH;;;;;GAKG;AAEH;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,SAAS,UAAU,EAAE,GAAG,oBAAoB,CAS9F;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,UAAU,EAAE,GAAG,kBAAkB,CAE1G;AAKD,8FAA8F;AAC9F,wBAAgB,eAAe,CAAC,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAEnE;AAED,oEAAoE;AACpE,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,kBAAkB,GAAG,IAAI,CAE7D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,EAAE,CAUnG"}
@@ -0,0 +1,66 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { privacyInventory } from "../privacy/inventory.js";
4
+ import { parseRetentionPolicy, resolveRetention } from "./policy.js";
5
+ //#region src/retention/composed.ts
6
+ /**
7
+ * **The retention policy as this Worker resolved it, reachable from module scope** — so a purge, a prune and
8
+ * an inactivity pass, each running in a Workflow the runtime constructs with `env` alone, read the same policy the
9
+ * routes do. The reasoning, and the cost, are `../capability/composition.ts`'s: `createBackend` runs at module load,
10
+ * one Worker assembles one backend, and a second assembly in one isolate replaces the first wholesale.
11
+ */
12
+ /**
13
+ * What resolving reads off a composition: each capability's window, and every lifecycle action it audits — the
14
+ * transitions it declares, then the ones its erasure records.
15
+ */
16
+ function retentionComposition(capabilities) {
17
+ return {
18
+ purgeWindows: capabilities.flatMap((capability) => capability.purgeWindow ? [capability.purgeWindow] : []),
19
+ lifecycleActions: capabilities.flatMap((capability) => {
20
+ const actions = capability.erasureRecords?.auditActions;
21
+ const erasure = actions ? [
22
+ ...actions.deleted,
23
+ actions.step,
24
+ actions.failed,
25
+ actions.purged
26
+ ] : [];
27
+ return [...capability.lifecycleAuditActions ?? [], ...erasure];
28
+ })
29
+ };
30
+ }
31
+ /**
32
+ * Parse and resolve a Worker's `retention` block over what it composes, or refuse it. What `createBackend` calls at
33
+ * assembly and `pithy doctor` calls on the same config, so both report — or refuse — one policy.
34
+ */
35
+ function effectiveRetention(block, capabilities) {
36
+ return resolveRetention(parseRetentionPolicy(block), retentionComposition(capabilities));
37
+ }
38
+ /** The resolved policy, or null before any backend has been assembled in this isolate. */
39
+ let recorded = null;
40
+ /** Record the resolved policy. Called once by `createBackend`, beside `recordComposition`. */
41
+ function recordRetention(retention) {
42
+ recorded = retention;
43
+ }
44
+ /** Forget it. For a test that needs the un-assembled state back. */
45
+ function forgetRetention() {
46
+ recorded = null;
47
+ }
48
+ /**
49
+ * The resolved policy — or, before any assembly, `null`. A caller with a capability's own default in hand falls back
50
+ * to it, which is exactly what an empty block means.
51
+ */
52
+ function composedRetention() {
53
+ return recorded;
54
+ }
55
+ /**
56
+ * The privacy classes a subject's rows hold: every class marked on a schema of a capability that erases that
57
+ * subject in its purge — the capabilities that hold rows keyed to the subject. What the class ages are read against:
58
+ * an account holds `pii` when a capability holding its rows marks a field `pii`.
59
+ */
60
+ function subjectClasses(capabilities, subject) {
61
+ const holders = new Set(capabilities.filter((capability) => capability.purge?.some((contribution) => contribution.subject === subject)).map((capability) => capability.name));
62
+ const classes = privacyInventory(capabilities).schemas.filter((schema) => holders.has(schema.capability)).flatMap((schema) => schema.fields.map((field) => field.class));
63
+ return [...new Set(classes)].sort();
64
+ }
65
+ //#endregion
66
+ export { composedRetention, effectiveRetention, forgetRetention, recordRetention, retentionComposition, subjectClasses };
@@ -0,0 +1,61 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { type HoldSubjectType, LegalHold } from "../controlPlane/data/hold";
4
+ import { type ControlPlaneDatabase } from "../controlPlane/data/tables";
5
+ /**
6
+ * **Legal holds: placing, releasing and reading them, and the one question every purge asks.**
7
+ *
8
+ * The holds live in the control plane's table in the app database, `DB`. A capability that erases — auth's purge,
9
+ * organization's purge, audit's prune, auth's inactivity pass — cannot import the control plane, so it asks through
10
+ * {@link legalHolds}, over the env its Workflow was handed, before every step that would touch a subject.
11
+ *
12
+ * **A hold that cannot be read is not an absent hold.** Only one answer reads as "nothing is held" without a row: a
13
+ * database that has no hold table, because no control plane ever migrated it, so nothing could have placed one. Any
14
+ * other failure throws, and the step that asked fails and retries rather than erasing past a hold it could not see.
15
+ */
16
+ /** Who places or releases a hold: the management client's own user id. */
17
+ export interface HoldActor {
18
+ readonly subject: string;
19
+ }
20
+ /** Place a hold. Holds stack: a second hold on one subject is its own row, released on its own. */
21
+ export declare function placeHold(db: ControlPlaneDatabase, input: {
22
+ subjectType: HoldSubjectType;
23
+ subjectId: string | null;
24
+ reason: string;
25
+ actor: HoldActor;
26
+ at: Date;
27
+ }): Promise<LegalHold>;
28
+ /**
29
+ * Release a hold, with a reason. A hold already released is a conflict, never a second release: the row keeps the
30
+ * first one's who and why. An unknown id is a 404.
31
+ */
32
+ export declare function releaseHold(db: ControlPlaneDatabase, input: {
33
+ id: string;
34
+ reason: string;
35
+ actor: HoldActor;
36
+ at: Date;
37
+ }): Promise<LegalHold>;
38
+ /** One hold by id, or null. */
39
+ export declare function readHold(db: ControlPlaneDatabase, id: string): Promise<LegalHold | null>;
40
+ /** Every hold, active first, newest first within each, at most `limit`. */
41
+ export declare function listHolds(db: ControlPlaneDatabase, options: {
42
+ state?: "active" | "released";
43
+ limit: number;
44
+ }): Promise<LegalHold[]>;
45
+ /** How many holds stand. What the manifest carries. */
46
+ export declare function countActiveHolds(db: ControlPlaneDatabase): Promise<number>;
47
+ /** The question every purge, prune and inactivity pass asks before it touches a subject. */
48
+ export interface LegalHolds {
49
+ /** Whether an active hold stands on this subject. A `null` id is the untenanted audit trail. */
50
+ held(subjectType: HoldSubjectType, subjectId: string | null): Promise<boolean>;
51
+ }
52
+ /** The app database the control plane keeps its holds in. Fixed, as the seam's own binding is. */
53
+ export declare const HOLDS_BINDING = "DB";
54
+ /** The holds over one env, read from {@link HOLDS_BINDING}. See the file header for what an unreadable read does. */
55
+ export declare function legalHolds(env: Record<string, unknown>): LegalHolds;
56
+ /**
57
+ * How many holds stand in this env's app database — zero where no control plane ever migrated it, since nothing
58
+ * could have placed one there. Any other failure throws, and the manifest reports the summary unavailable.
59
+ */
60
+ export declare function standingHolds(env: Record<string, unknown>): Promise<number>;
61
+ //# sourceMappingURL=holds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"holds.d.ts","sourceRoot":"","sources":["../../src/retention/holds.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,eAAe,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAEL,KAAK,oBAAoB,EAE1B,MAAM,6BAA6B,CAAC;AAIrC;;;;;;;;;;GAUG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,mGAAmG;AACnG,wBAAsB,SAAS,CAC7B,EAAE,EAAE,oBAAoB,EACxB,KAAK,EAAE;IAAE,WAAW,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GAC5G,OAAO,CAAC,SAAS,CAAC,CAepB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,oBAAoB,EACxB,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GAChE,OAAO,CAAC,SAAS,CAAC,CA6BpB;AAED,+BAA+B;AAC/B,wBAAsB,QAAQ,CAAC,EAAE,EAAE,oBAAoB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAG9F;AAED,2EAA2E;AAC3E,wBAAsB,SAAS,CAC7B,EAAE,EAAE,oBAAoB,EACxB,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACxD,OAAO,CAAC,SAAS,EAAE,CAAC,CAUtB;AAED,uDAAuD;AACvD,wBAAsB,gBAAgB,CAAC,EAAE,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAOhF;AAED,4FAA4F;AAC5F,MAAM,WAAW,UAAU;IACzB,gGAAgG;IAChG,IAAI,CAAC,WAAW,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAChF;AAED,kGAAkG;AAClG,eAAO,MAAM,aAAa,OAAO,CAAC;AAQlC,qHAAqH;AACrH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,CAsBnE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CASjF"}