@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
@@ -2,24 +2,36 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
4
  import type { Context } from "hono";
5
- import type { PithyHonoEnv } from "../../capability/capability";
5
+ import type { Capability, PithyHonoEnv } from "../../capability/capability";
6
+ import { commandName } from "../../cli/command";
6
7
  import { causeMessage } from "../../error/cause";
8
+ import type { RouteGroup } from "../../openapi/document";
9
+ import type { PresenceDeclaration } from "../../presence/declaration";
7
10
  import { privacyAttestation, privacyPosture } from "../../privacy/posture";
8
11
  import type { PrivacyInventory } from "../../privacy/report";
9
12
  import type { RetirementVerdict } from "../../privacy/retirement";
13
+ import type { EffectiveRetention } from "../../retention/policy";
10
14
  import { workerVersionMetadata } from "../../worker/identity";
11
15
  import { ControlPlaneAuditActions, safeEmit } from "../audit/actions";
12
16
  import type { ControlPlaneConfig } from "../config/config";
13
17
  import type { ControlPlaneContext } from "../context";
14
- import { ControlPlaneConnection, type RegisteredKey } from "../data/connection";
18
+ import type { RegisteredKey } from "../data/connection";
19
+ import { type ConnectionForWrite, readConnectionForWrite, writeKeys } from "../data/connections";
15
20
  import { appendKey, expireKey, pruneKeys } from "../data/keyLifecycle";
16
- import { CONTROL_PLANE_CONNECTIONS_TABLE, type ControlPlaneDatabase } from "../data/tables";
21
+ import type { OidcTrust } from "../data/oidcTrust";
22
+ import type { ControlPlaneDatabase } from "../data/tables";
17
23
  import type { CapabilityDeclaration, ControlPlaneManifestWire } from "../discovery/adminRoute";
18
24
  import { type CapabilityHealthSource, readCapabilityHealth } from "../discovery/health";
19
25
  import { healthWire } from "../discovery/healthSummary";
26
+ import { routeInputs, withRouteInputs } from "../discovery/routeInput";
27
+ import { readErasurePeriod, readErasureSubject } from "../erasure/read";
20
28
  import { ControlPlaneInvalidCredentialError, ControlPlaneKeyConflictError } from "../error/errors";
29
+ import { readEvidencePack } from "../evidence/pack";
21
30
  import { type ComposedMigrationDatabase, readDatabaseMigrations } from "../migrationState/migrationState";
22
31
  import type {
32
+ ControlPlaneErasurePeriodResponse,
33
+ ControlPlaneErasureSubjectResponse,
34
+ ControlPlaneEvidenceResponse,
23
35
  ControlPlaneKeysResponse,
24
36
  ControlPlaneMigrationsResponse,
25
37
  ControlPlanePingResponse,
@@ -27,8 +39,15 @@ import type {
27
39
  ExpireKeyResponse,
28
40
  PublicKeyView,
29
41
  RegisterKeyResponse,
42
+ TrustView,
30
43
  } from "./responses";
31
- import type { ExpireKeyParams, ExpireKeyRequest, RegisterKeyRequest } from "./schemas";
44
+ import type {
45
+ ErasurePeriodQuery,
46
+ ErasureSubjectParams,
47
+ ExpireKeyParams,
48
+ ExpireKeyRequest,
49
+ RegisterKeyRequest,
50
+ } from "./schemas";
32
51
 
33
52
  /**
34
53
  * The seam's own route handlers.
@@ -77,25 +96,45 @@ export interface ControlPlaneHandlerDeps {
77
96
  composedHealth: () => ReadonlyMap<string, CapabilityHealthSource>;
78
97
  /**
79
98
  * Every database this Worker binds, with the provider this build composes for it — captured by the same
80
- * `compose` hook through `collectMigrationSets`, the function `pithy migrate` composes with (#729).
99
+ * `compose` hook through `collectMigrationSets`, the function `pithy migrate` composes with.
81
100
  */
82
101
  composedMigrations: () => readonly ComposedMigrationDatabase[];
83
102
  /**
84
- * The privacy inventory of every storage schema this Worker composed (#132), walked on the first call and
103
+ * The privacy inventory of every storage schema this Worker composed, walked on the first call and
85
104
  * cached for the isolate. A thunk, so a Worker whose control plane is never called never walks.
86
105
  */
87
106
  privacyInventory: () => PrivacyInventory;
88
107
  /**
89
- * The field key's retirement verdict (#390): the guard over this Worker's encrypted columns, read through its own
108
+ * The field key's retirement verdict: the guard over this Worker's encrypted columns, read through its own
90
109
  * bindings. Never throws: whatever it cannot read is incomplete coverage.
91
110
  */
92
111
  retirement: (env: Record<string, unknown>) => Promise<RetirementVerdict>;
112
+ /**
113
+ * Every presence room kind this Worker composed, by kind, with the declaration that serves it — captured
114
+ * by the same `compose` hook. A thunk for the same reason as {@link composedCapabilities}.
115
+ */
116
+ composedPresence: () => ReadonlyMap<string, PresenceDeclaration>;
117
+ /**
118
+ * Every capability this Worker composed, as the objects themselves — the evidence pack reads which one records
119
+ * audit events and the bindings it writes through. A thunk for the same reason as the rest.
120
+ */
121
+ composition: () => readonly Capability[];
122
+ /**
123
+ * The route table `createBackend` recorded for the app these routes mounted on, or `undefined` for an app it did
124
+ * not assemble. Read per call, after every capability's routes have mounted, so the evidence pack sees them all.
125
+ */
126
+ routeGroups: () => readonly RouteGroup[] | undefined;
127
+ /**
128
+ * The retention block as this Worker resolved it at assembly, or null when no backend recorded one. The
129
+ * manifest reports it.
130
+ */
131
+ retention?: () => EffectiveRetention | null;
93
132
  /** The clock, injected so a test can stand at any instant. */
94
133
  now: () => Date;
95
134
  }
96
135
 
97
136
  /** The verified caller. Never null here — `requireControlPlane` ran first, or this code is unreachable. */
98
- function caller(c: Context<PithyHonoEnv>): ControlPlaneContext {
137
+ export function caller(c: Context<PithyHonoEnv>): ControlPlaneContext {
99
138
  const context = c.var.controlPlane;
100
139
  if (!context) {
101
140
  // Defense in depth against a route line assembled without the gate. It cannot happen through
@@ -106,37 +145,33 @@ function caller(c: Context<PithyHonoEnv>): ControlPlaneContext {
106
145
  return context;
107
146
  }
108
147
 
109
- /** Load the connection this call authenticated against. It verified moments ago, so it exists. */
110
- async function loadConnection(
111
- db: ControlPlaneDatabase,
112
- connectionId: string,
113
- ): Promise<{ connection: ControlPlaneConnection }> {
114
- const row = await db
115
- .selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE)
116
- .selectAll()
117
- .where("id", "=", connectionId)
118
- .executeTakeFirst();
119
- if (!row) {
148
+ /** Load the connection this call authenticated against, as stored, for a key write. It verified moments ago. */
149
+ async function loadConnection(db: ControlPlaneDatabase, connectionId: string): Promise<ConnectionForWrite> {
150
+ const loaded = await readConnectionForWrite(db, connectionId);
151
+ if (loaded === null) {
120
152
  // The row was deleted between verification and here — a revocation landing mid-request. Denying is
121
153
  // the correct answer: revocation is meant to be immediate.
122
154
  throw new ControlPlaneInvalidCredentialError({ detail: `connection ${connectionId} disappeared mid-request` });
123
155
  }
124
- return { connection: ControlPlaneConnection.parse(row) };
156
+ return loaded;
125
157
  }
126
158
 
127
- /** Write a connection's key array back, stamping `updatedAt`. One UPDATE — a rotation cannot half-apply. */
159
+ /**
160
+ * Write a connection's key array back, stamping `updatedAt`. One UPDATE, so a rotation cannot half-apply, and only
161
+ * while the keys are still what `loaded` read: a revocation landing between the read and this write is never undone.
162
+ */
128
163
  async function saveKeys(
129
164
  db: ControlPlaneDatabase,
130
- connectionId: string,
165
+ loaded: ConnectionForWrite,
131
166
  keys: readonly RegisteredKey[],
132
167
  now: Date,
133
168
  ): Promise<void> {
134
- const encoded = ControlPlaneConnection.shape.keys.encode([...keys]);
135
- await db
136
- .updateTable(CONTROL_PLANE_CONNECTIONS_TABLE)
137
- .set({ keys: encoded, updatedAt: ControlPlaneConnection.shape.updatedAt.encode(now) })
138
- .where("id", "=", connectionId)
139
- .execute();
169
+ if (await writeKeys(db, loaded, keys, now)) return;
170
+ throw new ControlPlaneKeyConflictError({
171
+ message: "The connection's keys changed while this call ran.",
172
+ action: "Make the call again.",
173
+ detail: `connection ${loaded.connection.id} had its keys written between this call's read and its write`,
174
+ });
140
175
  }
141
176
 
142
177
  /**
@@ -156,6 +191,37 @@ function publicKeyView(key: RegisteredKey): PublicKeyView {
156
191
  };
157
192
  }
158
193
 
194
+ /** The public view of one OIDC trust: its issuer, audience and claims — the population it admits. */
195
+ function trustView(trust: OidcTrust): TrustView {
196
+ return {
197
+ trustId: trust.trustId,
198
+ issuer: trust.issuer,
199
+ audience: trust.audience,
200
+ claims: { ...trust.claims },
201
+ authorizedBy: trust.authorizedBy,
202
+ validFrom: trust.validFrom.toISOString(),
203
+ revokedAt: trust.revokedAt?.toISOString() ?? null,
204
+ firstCallAt: trust.firstCallAt?.toISOString() ?? null,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Refuse a key write to a caller that verified under an OIDC trust.
210
+ *
211
+ * A trust exists so that no key is held on the workload's host. Letting a trust's caller register a key would
212
+ * turn a short-lived identity into a standing credential at the first call — an exchange by another name, and
213
+ * the custody the trust was registered to avoid. Granting `keys:rotate` to a connection is a statement about
214
+ * its keys, never a license for a workload to mint one.
215
+ */
216
+ function refuseTrustCaller(context: ControlPlaneContext, what: string): void {
217
+ if (context.credentialKind !== "trust") return;
218
+ throw new ControlPlaneKeyConflictError({
219
+ message: "A call under a workload trust cannot change this connection's keys.",
220
+ action: `Register or expire a key from a client that holds one, or with ${commandName("dashboard connect")}.`,
221
+ detail: `${what} refused: trust ${context.keyId} verified this call`,
222
+ });
223
+ }
224
+
159
225
  /**
160
226
  * `GET /control-plane/ping` — connectivity and key proof.
161
227
  *
@@ -186,11 +252,11 @@ export function pingHandler(deps: ControlPlaneHandlerDeps) {
186
252
  * Discovery over configuration. A client builds its navigation from this rather than from settings
187
253
  * someone maintains, so a Worker with no payments capability has no purchases pane as a matter of fact.
188
254
  *
189
- * **And the numbers a client would otherwise pay a second round trip for** (#317). Each capability's
255
+ * **And the numbers a client would otherwise pay a second round trip for**. Each capability's
190
256
  * bounded summary is resolved here, behind the scope that capability's own read is behind: a connection
191
257
  * without it gets no number rather than a zero, and never costs the adopter's Worker the query.
192
258
  *
193
- * **One capability's failure costs one capability's number** (#350). A producer that throws, or that
259
+ * **One capability's failure costs one capability's number**. A producer that throws, or that
194
260
  * reports something its declaration cannot name, lands on `unavailable` for that entry alone —
195
261
  * `readCapabilityHealth` catches it, so nothing rejects, so `Promise.all` still resolves and every
196
262
  * sibling still reports. This route is what the whole management surface is built from; a manifest that
@@ -198,13 +264,21 @@ export function pingHandler(deps: ControlPlaneHandlerDeps) {
198
264
  * name.
199
265
  */
200
266
  export function manifestHandler(deps: ControlPlaneHandlerDeps) {
267
+ // Each admin route's input, converted once per route table rather than once per read. Keyed on the
268
+ // table itself, so an app whose groups were recorded late is converted when it first has them.
269
+ let converted: { groups: readonly RouteGroup[]; inputs: ReturnType<typeof routeInputs> } | undefined;
270
+ const inputsFor = (groups: readonly RouteGroup[] | undefined) => {
271
+ if (groups === undefined) return undefined;
272
+ if (converted?.groups !== groups) converted = { groups, inputs: routeInputs(groups) };
273
+ return converted.inputs;
274
+ };
201
275
  return async (c: Context<PithyHonoEnv>) => {
202
276
  const context = caller(c);
203
277
  const sources = deps.composedHealth();
204
278
  // In parallel: each producer is bounded by its own declaration, and a Worker composing several has
205
279
  // no reason to serialize numbers that do not depend on each other.
206
280
  const capabilities = await Promise.all(
207
- deps.composedCapabilities().map(async (declaration) => ({
281
+ withRouteInputs(deps.composedCapabilities(), inputsFor(deps.routeGroups())).map(async (declaration) => ({
208
282
  ...declaration,
209
283
  // Spread, so the values and the failure flag are written together or not at all. A handler that
210
284
  // set one and forgot the other is the shape of defect the four-state value exists to prevent.
@@ -220,24 +294,32 @@ export function manifestHandler(deps: ControlPlaneHandlerDeps) {
220
294
  environment: context.environment,
221
295
  connectionId: context.connectionId,
222
296
  version: build.id,
223
- // Beside the id, because the id alone cannot tell a deploy from a rollback (#132): a rollback
297
+ // Beside the id, because the id alone cannot tell a deploy from a rollback: a rollback
224
298
  // reactivates a version already seen, and this is the one field that moves backwards when it does.
225
299
  versionCreatedAt: build.createdAt,
226
300
  // Classes and counts, never field names — those are `GET /privacy`'s, behind their own scope. A project
227
301
  // that declared nothing walks nothing; otherwise the walk is the isolate's cached one.
228
- // The retirement verdict beside it (#390), so a dashboard reads the guard rather than inferring it.
302
+ // The retirement verdict beside it, so a dashboard reads the guard rather than inferring it.
229
303
  privacy: {
230
304
  ...privacyPosture(privacyAttestation(c.env), deps.privacyInventory),
231
305
  retirement: await deps.retirement(c.env as Record<string, unknown>),
232
306
  },
233
307
  capabilities,
234
308
  grantedScopes: [...context.grantedScopes],
309
+ // How long this Worker keeps what it holds, as it takes effect: policy, not data, so on every manifest.
310
+ ...retentionOf(deps),
235
311
  } satisfies ControlPlaneManifestWire);
236
312
  };
237
313
  }
238
314
 
315
+ /** The resolved retention for the manifest, or nothing where this Worker recorded none. */
316
+ function retentionOf(deps: ControlPlaneHandlerDeps): { retention?: EffectiveRetention } {
317
+ const retention = deps.retention?.() ?? null;
318
+ return retention === null ? {} : { retention };
319
+ }
320
+
239
321
  /**
240
- * `GET /control-plane/migrations` — where each database this Worker binds stands (#729).
322
+ * `GET /control-plane/migrations` — where each database this Worker binds stands.
241
323
  *
242
324
  * Read-only: it reports, and nothing over the seam runs, reverses or resets a migration.
243
325
  *
@@ -274,10 +356,11 @@ export function listKeysHandler(deps: ControlPlaneHandlerDeps) {
274
356
  environment: connection.environment,
275
357
  keys: connection.keys.map(publicKeyView),
276
358
  // The review a credential listing exists for needs these, and it is the adopter's to perform —
277
- // on their own side, from their own row (#721).
359
+ // on their own side, from their own row.
278
360
  authorizedBy: connection.authorizedBy,
279
361
  actorKind: connection.actorKind,
280
362
  purpose: connection.purpose,
363
+ trusts: (connection.trusts ?? []).map(trustView),
281
364
  } satisfies ControlPlaneKeysResponse);
282
365
  };
283
366
  }
@@ -295,9 +378,11 @@ export function listKeysHandler(deps: ControlPlaneHandlerDeps) {
295
378
  export function registerKeyHandler(deps: ControlPlaneHandlerDeps) {
296
379
  return async (c: Context<PithyHonoEnv>, body: RegisterKeyRequest) => {
297
380
  const context = caller(c);
381
+ refuseTrustCaller(context, "key registration");
298
382
  const db = deps.database(c);
299
383
  const now = deps.now();
300
- const { connection } = await loadConnection(db, context.connectionId);
384
+ const loaded = await loadConnection(db, context.connectionId);
385
+ const { connection } = loaded;
301
386
 
302
387
  const appended = appendKey(
303
388
  connection.keys,
@@ -309,7 +394,7 @@ export function registerKeyHandler(deps: ControlPlaneHandlerDeps) {
309
394
  retentionMs: deps.config.keyRetentionDays * MS_PER_DAY,
310
395
  maxKeys: deps.config.maxKeys,
311
396
  });
312
- await saveKeys(db, connection.id, pruned, now);
397
+ await saveKeys(db, loaded, pruned, now);
313
398
 
314
399
  await safeEmit(
315
400
  c.var.emit,
@@ -365,9 +450,11 @@ export function registerKeyHandler(deps: ControlPlaneHandlerDeps) {
365
450
  export function expireKeyHandler(deps: ControlPlaneHandlerDeps) {
366
451
  return async (c: Context<PithyHonoEnv>, params: ExpireKeyParams, body: ExpireKeyRequest) => {
367
452
  const context = caller(c);
453
+ refuseTrustCaller(context, "key expiry");
368
454
  const db = deps.database(c);
369
455
  const now = deps.now();
370
- const { connection } = await loadConnection(db, context.connectionId);
456
+ const loaded = await loadConnection(db, context.connectionId);
457
+ const { connection } = loaded;
371
458
 
372
459
  if (body.provenKeyId !== context.keyId) {
373
460
  throw new ControlPlaneKeyConflictError({
@@ -378,7 +465,7 @@ export function expireKeyHandler(deps: ControlPlaneHandlerDeps) {
378
465
  }
379
466
 
380
467
  const expired = expireKey(connection.keys, params.keyId, { provenKeyId: body.provenKeyId, now });
381
- await saveKeys(db, connection.id, expired, now);
468
+ await saveKeys(db, loaded, expired, now);
382
469
 
383
470
  await safeEmit(
384
471
  c.var.emit,
@@ -412,7 +499,7 @@ export function expireKeyHandler(deps: ControlPlaneHandlerDeps) {
412
499
  }
413
500
 
414
501
  /**
415
- * `GET /control-plane/privacy` — the full privacy inventory (#132).
502
+ * `GET /control-plane/privacy` — the full privacy inventory.
416
503
  *
417
504
  * Every storage schema this Worker composed that holds a marked field, each field's path and class, and where it
418
505
  * is stored — derived from the running code, so unmarking a field removes it on the next deploy with nothing in
@@ -438,3 +525,48 @@ export function privacyHandler(deps: ControlPlaneHandlerDeps) {
438
525
  } satisfies ControlPlanePrivacyResponse);
439
526
  };
440
527
  }
528
+
529
+ /**
530
+ * `GET /control-plane/evidence` — the evidence pack.
531
+ *
532
+ * SOC 2 and ISO 27001 evidence derived from what this Worker composes, records and declares: each control's
533
+ * references, the facts read and their sources, and `evidenced`, `gap` or `not-derivable` with the reason. It
534
+ * proves controls present and performs nothing. Behind `evidence:read`, and it never widens another scope: what
535
+ * a capability keeps behind its own read stays there. Read-only.
536
+ */
537
+ export function evidenceHandler(deps: ControlPlaneHandlerDeps) {
538
+ return async (c: Context<PithyHonoEnv>) => {
539
+ const context = caller(c);
540
+ return c.json((await readEvidencePack(c, deps, context)) satisfies ControlPlaneEvidenceResponse);
541
+ };
542
+ }
543
+
544
+ /**
545
+ * `GET /control-plane/erasure/:subjectType/:subjectId` — one subject's erasure, attested.
546
+ *
547
+ * Read from the purge's own lifecycle rows, through the capability that owns the subject, and cited in the audit
548
+ * chain through the composed audit. A subject with no deletion in force is a 404. Read-only.
549
+ */
550
+ export function erasureSubjectHandler(deps: ControlPlaneHandlerDeps) {
551
+ return async (c: Context<PithyHonoEnv>, params: ErasureSubjectParams) => {
552
+ caller(c);
553
+ const env = (c.env ?? {}) as Record<string, unknown>;
554
+ return c.json(
555
+ (await readErasureSubject(
556
+ env,
557
+ deps,
558
+ params.subjectType,
559
+ params.subjectId,
560
+ )) satisfies ControlPlaneErasureSubjectResponse,
561
+ );
562
+ };
563
+ }
564
+
565
+ /** `GET /control-plane/erasure?since&until` — every purge in a period, counted. Read-only. */
566
+ export function erasurePeriodHandler(deps: ControlPlaneHandlerDeps) {
567
+ return async (c: Context<PithyHonoEnv>, query: ErasurePeriodQuery) => {
568
+ caller(c);
569
+ const env = (c.env ?? {}) as Record<string, unknown>;
570
+ return c.json((await readErasurePeriod(env, deps, query)) satisfies ControlPlaneErasurePeriodResponse);
571
+ };
572
+ }
@@ -0,0 +1,108 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { Context } from "hono";
5
+ import type { PithyHonoEnv } from "../../capability/capability";
6
+ import { countActiveHolds, listHolds, placeHold, releaseHold } from "../../retention/holds";
7
+ import { ControlPlaneAuditActions, safeEmit } from "../audit/actions";
8
+ import type { LegalHold } from "../data/hold";
9
+ import { type ControlPlaneHandlerDeps, caller } from "./handlers";
10
+ import type { ControlPlaneHoldResponse, ControlPlaneHoldsResponse, LegalHoldView } from "./responses";
11
+ import type { HoldListQuery, HoldParams, PlaceHoldRequest, ReleaseHoldRequest } from "./schemas";
12
+
13
+ /**
14
+ * The legal-hold routes: place, release, list.
15
+ *
16
+ * Placing and releasing are each audited under `controlplane/*`, which the retention floor keeps at least a year,
17
+ * with the subject, the hold and the reason. A hold suspends every purge, prune and inactivity deletion touching its
18
+ * subject; the capabilities that erase read it through `retention/holds.ts`. **Nothing here shortens a retention**:
19
+ * a hold can only preserve, and its release returns the subject to the policy the Worker was deployed with.
20
+ */
21
+
22
+ /** The default page of `GET /control-plane/holds`. */
23
+ const DEFAULT_HOLD_LIMIT = 100;
24
+
25
+ /** A hold, as the routes answer it. */
26
+ export function holdView(hold: LegalHold): LegalHoldView {
27
+ return {
28
+ id: hold.id,
29
+ subjectType: hold.subjectType,
30
+ subjectId: hold.subjectId,
31
+ reason: hold.reason,
32
+ placedBy: hold.placedBy,
33
+ placedAt: hold.placedAt.toISOString(),
34
+ state: hold.state,
35
+ releasedAt: hold.releasedAt?.toISOString() ?? null,
36
+ releasedBy: hold.releasedBy,
37
+ releaseReason: hold.releaseReason,
38
+ };
39
+ }
40
+
41
+ /** The audit event for a placement or a release: the subject as the resource, the hold and the reason beside it. */
42
+ async function auditHold(c: Context<PithyHonoEnv>, action: string, hold: LegalHold, reason: string): Promise<void> {
43
+ const context = caller(c);
44
+ await safeEmit(
45
+ c.var.emit,
46
+ {
47
+ action,
48
+ outcome: "success",
49
+ severity: "warning",
50
+ actorType: "control-plane",
51
+ actorId: context.subject,
52
+ resourceType: hold.subjectType === "audit" ? "audit_trail" : hold.subjectType,
53
+ resourceId: hold.subjectId,
54
+ requestId: c.req.header("cf-ray"),
55
+ ip: c.req.header("cf-connecting-ip"),
56
+ userAgent: c.req.header("user-agent"),
57
+ metadata: { holdId: hold.id, subjectType: hold.subjectType, reason },
58
+ },
59
+ c.var.log,
60
+ );
61
+ }
62
+
63
+ /** `POST /control-plane/holds` — place a hold. */
64
+ export function placeHoldHandler(deps: ControlPlaneHandlerDeps) {
65
+ return async (c: Context<PithyHonoEnv>, body: PlaceHoldRequest) => {
66
+ const context = caller(c);
67
+ const hold = await placeHold(deps.database(c), {
68
+ subjectType: body.subjectType,
69
+ subjectId: body.subjectId,
70
+ reason: body.reason,
71
+ actor: { subject: context.subject },
72
+ at: deps.now(),
73
+ });
74
+ await auditHold(c, ControlPlaneAuditActions.holdPlaced, hold, body.reason);
75
+ return c.json({ hold: holdView(hold) } satisfies ControlPlaneHoldResponse, 201);
76
+ };
77
+ }
78
+
79
+ /** `POST /control-plane/holds/:holdId/release` — release a hold, with a reason. */
80
+ export function releaseHoldHandler(deps: ControlPlaneHandlerDeps) {
81
+ return async (c: Context<PithyHonoEnv>, params: HoldParams, body: ReleaseHoldRequest) => {
82
+ const context = caller(c);
83
+ const hold = await releaseHold(deps.database(c), {
84
+ id: params.holdId,
85
+ reason: body.reason,
86
+ actor: { subject: context.subject },
87
+ at: deps.now(),
88
+ });
89
+ await auditHold(c, ControlPlaneAuditActions.holdReleased, hold, body.reason);
90
+ return c.json({ hold: holdView(hold) } satisfies ControlPlaneHoldResponse);
91
+ };
92
+ }
93
+
94
+ /** `GET /control-plane/holds` — every hold, standing first. Read-only. */
95
+ export function listHoldsHandler(deps: ControlPlaneHandlerDeps) {
96
+ return async (c: Context<PithyHonoEnv>, query: HoldListQuery) => {
97
+ caller(c);
98
+ const db = deps.database(c);
99
+ const holds = await listHolds(db, {
100
+ ...(query.state ? { state: query.state } : {}),
101
+ limit: query.limit ?? DEFAULT_HOLD_LIMIT,
102
+ });
103
+ return c.json({
104
+ active: await countActiveHolds(db),
105
+ holds: holds.map(holdView),
106
+ } satisfies ControlPlaneHoldsResponse);
107
+ };
108
+ }
@@ -0,0 +1,71 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { Context, MiddlewareHandler } from "hono";
5
+ import type { PithyHonoEnv } from "../../capability/capability";
6
+ import { NotFoundError } from "../../error/pithyError";
7
+ import type { PresenceTicket } from "../../presence/http/responses";
8
+ import { issuePresenceTicket, roomKind } from "../../presence/http/routes";
9
+ import type { ControlPlanePresenceTicketRequest } from "../../presence/http/schemas";
10
+ import { ANY_VERIFIED_CALLER, type ControlPlaneRequirement } from "../scope/scope";
11
+ import { type ControlPlaneRequirementResolver, requireControlPlaneFor } from "./guard";
12
+ import type { ControlPlaneHandlerDeps } from "./handlers";
13
+
14
+ /**
15
+ * `POST /control-plane/presence/tickets` — a ticket into any room this Worker composes, for a management
16
+ * client.
17
+ *
18
+ * **The scope is the room kind's, and the gate checks it.** Which scope a call needs is named in its body, so
19
+ * {@link presenceTicketRequirement} reads the kind from a copy of the body before the gate verifies, and the
20
+ * gate then verifies against that kind's scope exactly as it would against a route's own: one allowed row
21
+ * when it covers, one denial row naming the kind's scope when it does not, never both. Presence adds no scope
22
+ * of its own: a connection that may read a record may see who else has it open.
23
+ *
24
+ * A body that names no declared kind needs only a verified caller, so an unverified one is refused before any
25
+ * kind is looked at, and a verified one then gets the handler's 404 — the manifest lists every kind, so its
26
+ * existence is no secret.
27
+ */
28
+ export function presenceTicketRequirement(deps: ControlPlaneHandlerDeps): ControlPlaneRequirementResolver {
29
+ return async (c) => {
30
+ const kind = await bodyKind(c.req.raw);
31
+ const presence = kind === undefined ? undefined : deps.composedPresence().get(kind);
32
+ if (kind === undefined || !presence) return ANY_VERIFIED_CALLER;
33
+ return roomKind(presence, kind).scope as ControlPlaneRequirement;
34
+ };
35
+ }
36
+
37
+ /** The gate the route wears: {@link requireControlPlaneFor} over the room kind's scope. */
38
+ export function presenceTicketGate(deps: ControlPlaneHandlerDeps): MiddlewareHandler<PithyHonoEnv> {
39
+ return requireControlPlaneFor(presenceTicketRequirement(deps));
40
+ }
41
+
42
+ /**
43
+ * The body's `kind`, read from a copy so the route's validator still reads the original. Only to choose a
44
+ * scope: the validator is what parses the body, and anything unreadable here is simply no kind.
45
+ */
46
+ async function bodyKind(request: Request): Promise<string | undefined> {
47
+ try {
48
+ const body: unknown = await request.clone().json();
49
+ const kind = typeof body === "object" && body !== null ? (body as { kind?: unknown }).kind : undefined;
50
+ return typeof kind === "string" ? kind : undefined;
51
+ } catch {
52
+ return undefined;
53
+ }
54
+ }
55
+
56
+ /** The handler, behind {@link presenceTicketGate}: the kind's scope is already verified, so it mints. */
57
+ export function presenceTicketHandler(deps: ControlPlaneHandlerDeps) {
58
+ return async (c: Context<PithyHonoEnv>, body: ControlPlanePresenceTicketRequest): Promise<Response> => {
59
+ const presence = deps.composedPresence().get(body.kind);
60
+ if (!presence) throw new NotFoundError({ message: "That room kind does not exist.", params: { kind: body.kind } });
61
+ const room = roomKind(presence, body.kind);
62
+ const ticket = await issuePresenceTicket(c, presence, {
63
+ kind: room.kind,
64
+ recordId: body.recordId,
65
+ ref: body.participant.ref,
66
+ name: body.participant.name,
67
+ origin: "control-plane",
68
+ });
69
+ return c.json(ticket satisfies PresenceTicket, 201);
70
+ };
71
+ }
@@ -0,0 +1,19 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { Context } from "hono";
5
+ import type { PithyHonoEnv } from "../../capability/capability";
6
+ import { readProcessingRecord } from "../processing/read";
7
+ import { type ControlPlaneHandlerDeps, caller } from "./handlers";
8
+ import type { ControlPlaneProcessingResponse } from "./responses";
9
+
10
+ /**
11
+ * `GET /control-plane/processing` — the record of processing activities, GDPR Article 30. Each activity's purpose,
12
+ * classes, legal basis, recipients, retention and measures, and every gap named. Read-only.
13
+ */
14
+ export function processingHandler(deps: ControlPlaneHandlerDeps) {
15
+ return async (c: Context<PithyHonoEnv>) => {
16
+ const context = caller(c);
17
+ return c.json((await readProcessingRecord(c, deps, context)) satisfies ControlPlaneProcessingResponse);
18
+ };
19
+ }