@pithy-sh/core 0.12.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 (362) 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/audit/composed.d.ts +18 -0
  10. package/dist/audit/composed.d.ts.map +1 -0
  11. package/dist/audit/composed.js +28 -0
  12. package/dist/capability/bindings.d.ts +22 -0
  13. package/dist/capability/bindings.d.ts.map +1 -1
  14. package/dist/capability/bindings.js +32 -2
  15. package/dist/capability/capability.d.ts +133 -15
  16. package/dist/capability/capability.d.ts.map +1 -1
  17. package/dist/capability/capability.js +17 -2
  18. package/dist/capability/erasure.d.ts +75 -0
  19. package/dist/capability/erasure.d.ts.map +1 -0
  20. package/dist/capability/erasure.js +2 -0
  21. package/dist/capability/manifest.d.ts +5 -0
  22. package/dist/capability/manifest.d.ts.map +1 -1
  23. package/dist/capability/personColumns.d.ts +28 -0
  24. package/dist/capability/personColumns.d.ts.map +1 -0
  25. package/dist/capability/personColumns.js +66 -0
  26. package/dist/capability/personRows.d.ts +62 -0
  27. package/dist/capability/personRows.d.ts.map +1 -0
  28. package/dist/capability/personRows.js +59 -0
  29. package/dist/capability/purge.d.ts +113 -4
  30. package/dist/capability/purge.d.ts.map +1 -1
  31. package/dist/capability/purge.js +138 -5
  32. package/dist/capability/settings.d.ts +10 -0
  33. package/dist/capability/settings.d.ts.map +1 -1
  34. package/dist/capability/vectorIndex.d.ts +36 -0
  35. package/dist/capability/vectorIndex.d.ts.map +1 -0
  36. package/dist/capability/vectorIndex.js +25 -0
  37. package/dist/cli/commandCatalog.generated.d.ts +434 -4
  38. package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
  39. package/dist/cli/commandCatalog.generated.js +455 -2
  40. package/dist/cli/commandFacts.d.ts +8 -5
  41. package/dist/cli/commandFacts.d.ts.map +1 -1
  42. package/dist/cli/commandFacts.js +12 -3
  43. package/dist/consent/purpose.d.ts +70 -0
  44. package/dist/consent/purpose.d.ts.map +1 -0
  45. package/dist/consent/purpose.js +97 -0
  46. package/dist/consent/reader.d.ts +99 -0
  47. package/dist/consent/reader.d.ts.map +1 -0
  48. package/dist/consent/reader.js +33 -0
  49. package/dist/controlPlane/audit/actions.d.ts +15 -4
  50. package/dist/controlPlane/audit/actions.d.ts.map +1 -1
  51. package/dist/controlPlane/audit/actions.js +16 -5
  52. package/dist/controlPlane/capability.d.ts +10 -0
  53. package/dist/controlPlane/capability.d.ts.map +1 -1
  54. package/dist/controlPlane/capability.js +56 -8
  55. package/dist/controlPlane/context.d.ts +4 -0
  56. package/dist/controlPlane/context.d.ts.map +1 -1
  57. package/dist/controlPlane/context.js +3 -2
  58. package/dist/controlPlane/data/connection.d.ts +21 -2
  59. package/dist/controlPlane/data/connection.d.ts.map +1 -1
  60. package/dist/controlPlane/data/connection.js +13 -4
  61. package/dist/controlPlane/data/connections.d.ts +94 -0
  62. package/dist/controlPlane/data/connections.d.ts.map +1 -0
  63. package/dist/controlPlane/data/connections.js +151 -0
  64. package/dist/controlPlane/data/hold.d.ts +46 -0
  65. package/dist/controlPlane/data/hold.d.ts.map +1 -0
  66. package/dist/controlPlane/data/hold.js +36 -0
  67. package/dist/controlPlane/data/oidcTrust.d.ts +122 -0
  68. package/dist/controlPlane/data/oidcTrust.d.ts.map +1 -0
  69. package/dist/controlPlane/data/oidcTrust.js +173 -0
  70. package/dist/controlPlane/data/tables.d.ts +4 -0
  71. package/dist/controlPlane/data/tables.d.ts.map +1 -1
  72. package/dist/controlPlane/data/tables.js +9 -4
  73. package/dist/controlPlane/data/trustFirstCall.d.ts +16 -0
  74. package/dist/controlPlane/data/trustFirstCall.d.ts.map +1 -0
  75. package/dist/controlPlane/data/trustFirstCall.js +25 -0
  76. package/dist/controlPlane/discovery/adminRoute.d.ts +165 -2
  77. package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -1
  78. package/dist/controlPlane/discovery/adminRoute.js +32 -8
  79. package/dist/controlPlane/discovery/routeInput.d.ts +23 -0
  80. package/dist/controlPlane/discovery/routeInput.d.ts.map +1 -0
  81. package/dist/controlPlane/discovery/routeInput.js +80 -0
  82. package/dist/controlPlane/discovery/toolName.d.ts +21 -0
  83. package/dist/controlPlane/discovery/toolName.d.ts.map +1 -0
  84. package/dist/controlPlane/discovery/toolName.js +25 -0
  85. package/dist/controlPlane/erasure/attest.d.ts +78 -0
  86. package/dist/controlPlane/erasure/attest.d.ts.map +1 -0
  87. package/dist/controlPlane/erasure/attest.js +164 -0
  88. package/dist/controlPlane/erasure/read.d.ts +39 -0
  89. package/dist/controlPlane/erasure/read.d.ts.map +1 -0
  90. package/dist/controlPlane/erasure/read.js +140 -0
  91. package/dist/controlPlane/error/errors.d.ts +61 -2
  92. package/dist/controlPlane/error/errors.d.ts.map +1 -1
  93. package/dist/controlPlane/error/errors.js +13 -0
  94. package/dist/controlPlane/evidence/derive.d.ts +124 -0
  95. package/dist/controlPlane/evidence/derive.d.ts.map +1 -0
  96. package/dist/controlPlane/evidence/derive.js +343 -0
  97. package/dist/controlPlane/evidence/mapping.d.ts +85 -0
  98. package/dist/controlPlane/evidence/mapping.d.ts.map +1 -0
  99. package/dist/controlPlane/evidence/mapping.js +114 -0
  100. package/dist/controlPlane/evidence/pack.d.ts +46 -0
  101. package/dist/controlPlane/evidence/pack.d.ts.map +1 -0
  102. package/dist/controlPlane/evidence/pack.js +124 -0
  103. package/dist/controlPlane/http/connections.d.ts +96 -0
  104. package/dist/controlPlane/http/connections.d.ts.map +1 -0
  105. package/dist/controlPlane/http/connections.js +109 -0
  106. package/dist/controlPlane/http/guard.d.ts +23 -2
  107. package/dist/controlPlane/http/guard.d.ts.map +1 -1
  108. package/dist/controlPlane/http/guard.js +103 -7
  109. package/dist/controlPlane/http/handlers.d.ts +212 -10
  110. package/dist/controlPlane/http/handlers.d.ts.map +1 -1
  111. package/dist/controlPlane/http/handlers.js +117 -26
  112. package/dist/controlPlane/http/holds.d.ts +57 -0
  113. package/dist/controlPlane/http/holds.d.ts.map +1 -0
  114. package/dist/controlPlane/http/holds.js +98 -0
  115. package/dist/controlPlane/http/presence.d.ts +27 -0
  116. package/dist/controlPlane/http/presence.d.ts.map +1 -0
  117. package/dist/controlPlane/http/presence.js +67 -0
  118. package/dist/controlPlane/http/processing.d.ts +73 -0
  119. package/dist/controlPlane/http/processing.d.ts.map +1 -0
  120. package/dist/controlPlane/http/processing.js +17 -0
  121. package/dist/controlPlane/http/responses.d.ts +927 -3
  122. package/dist/controlPlane/http/responses.d.ts.map +1 -1
  123. package/dist/controlPlane/http/responses.js +283 -7
  124. package/dist/controlPlane/http/routes.d.ts +12 -2
  125. package/dist/controlPlane/http/routes.d.ts.map +1 -1
  126. package/dist/controlPlane/http/routes.js +182 -5
  127. package/dist/controlPlane/http/schemas.d.ts +63 -0
  128. package/dist/controlPlane/http/schemas.d.ts.map +1 -1
  129. package/dist/controlPlane/http/schemas.js +39 -1
  130. package/dist/controlPlane/http/verify.d.ts.map +1 -1
  131. package/dist/controlPlane/http/verify.js +13 -1
  132. package/dist/controlPlane/http/verifyTrust.d.ts +76 -0
  133. package/dist/controlPlane/http/verifyTrust.d.ts.map +1 -0
  134. package/dist/controlPlane/http/verifyTrust.js +75 -0
  135. package/dist/controlPlane/migrations/0004_connection_trusts.d.ts +15 -0
  136. package/dist/controlPlane/migrations/0004_connection_trusts.d.ts.map +1 -0
  137. package/dist/controlPlane/migrations/0004_connection_trusts.js +23 -0
  138. package/dist/controlPlane/migrations/0005_holds.d.ts +12 -0
  139. package/dist/controlPlane/migrations/0005_holds.d.ts.map +1 -0
  140. package/dist/controlPlane/migrations/0005_holds.js +26 -0
  141. package/dist/controlPlane/processing/derive.d.ts +77 -0
  142. package/dist/controlPlane/processing/derive.d.ts.map +1 -0
  143. package/dist/controlPlane/processing/derive.js +206 -0
  144. package/dist/controlPlane/processing/read.d.ts +10 -0
  145. package/dist/controlPlane/processing/read.d.ts.map +1 -0
  146. package/dist/controlPlane/processing/read.js +89 -0
  147. package/dist/controlPlane/scope/kind.d.ts +46 -0
  148. package/dist/controlPlane/scope/kind.d.ts.map +1 -0
  149. package/dist/controlPlane/scope/kind.js +52 -0
  150. package/dist/controlPlane/scope/scope.d.ts +58 -3
  151. package/dist/controlPlane/scope/scope.d.ts.map +1 -1
  152. package/dist/controlPlane/scope/scope.js +59 -4
  153. package/dist/controlPlane/token/jws.js +1 -1
  154. package/dist/controlPlane/token/mint.js +1 -1
  155. package/dist/controlPlane/wire.d.ts +23 -0
  156. package/dist/controlPlane/wire.d.ts.map +1 -1
  157. package/dist/controlPlane/wire.js +24 -1
  158. package/dist/createBackend.d.ts +13 -0
  159. package/dist/createBackend.d.ts.map +1 -1
  160. package/dist/createBackend.js +11 -3
  161. package/dist/createEntrypoint.d.ts +14 -1
  162. package/dist/createEntrypoint.d.ts.map +1 -1
  163. package/dist/createEntrypoint.js +19 -5
  164. package/dist/error/http.d.ts +216 -0
  165. package/dist/error/http.d.ts.map +1 -1
  166. package/dist/error/payload.d.ts +432 -0
  167. package/dist/error/payload.d.ts.map +1 -1
  168. package/dist/error/payload.js +144 -0
  169. package/dist/error/pithyError.d.ts +22 -0
  170. package/dist/error/pithyError.d.ts.map +1 -1
  171. package/dist/error/pithyError.js +31 -1
  172. package/dist/http/routeSpec.d.ts +46 -1
  173. package/dist/http/routeSpec.d.ts.map +1 -1
  174. package/dist/http/routeSpec.js +39 -5
  175. package/dist/http/validation.d.ts +11 -2
  176. package/dist/http/validation.d.ts.map +1 -1
  177. package/dist/http/validation.js +20 -3
  178. package/dist/http/verification.d.ts.map +1 -1
  179. package/dist/http/verification.js +1 -1
  180. package/dist/i18n/translator.d.ts +2 -1
  181. package/dist/i18n/translator.d.ts.map +1 -1
  182. package/dist/index.d.ts +1 -1
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.js +3 -3
  185. package/dist/logger/record.d.ts +126 -0
  186. package/dist/logger/record.d.ts.map +1 -1
  187. package/dist/mcp/annotations.d.ts +20 -0
  188. package/dist/mcp/annotations.d.ts.map +1 -0
  189. package/dist/mcp/annotations.js +23 -0
  190. package/dist/mcp/tools.d.ts +75 -0
  191. package/dist/mcp/tools.d.ts.map +1 -0
  192. package/dist/mcp/tools.js +118 -0
  193. package/dist/openapi/document.d.ts.map +1 -1
  194. package/dist/openapi/document.js +8 -26
  195. package/dist/openapi/operations.d.ts +35 -0
  196. package/dist/openapi/operations.d.ts.map +1 -0
  197. package/dist/openapi/operations.js +37 -0
  198. package/dist/presence/channel.d.ts +11 -0
  199. package/dist/presence/channel.d.ts.map +1 -0
  200. package/dist/presence/channel.js +65 -0
  201. package/dist/presence/declaration.d.ts +154 -0
  202. package/dist/presence/declaration.d.ts.map +1 -0
  203. package/dist/presence/declaration.js +164 -0
  204. package/dist/presence/durableObject.d.ts +61 -0
  205. package/dist/presence/durableObject.d.ts.map +1 -0
  206. package/dist/presence/durableObject.js +257 -0
  207. package/dist/presence/frames.d.ts +166 -0
  208. package/dist/presence/frames.d.ts.map +1 -0
  209. package/dist/presence/frames.js +90 -0
  210. package/dist/presence/http/guard.d.ts +11 -0
  211. package/dist/presence/http/guard.d.ts.map +1 -0
  212. package/dist/presence/http/guard.js +20 -0
  213. package/dist/presence/http/responses.d.ts +31 -0
  214. package/dist/presence/http/responses.d.ts.map +1 -0
  215. package/dist/presence/http/responses.js +34 -0
  216. package/dist/presence/http/routes.d.ts +24 -0
  217. package/dist/presence/http/routes.d.ts.map +1 -0
  218. package/dist/presence/http/routes.js +127 -0
  219. package/dist/presence/http/schemas.d.ts +29 -0
  220. package/dist/presence/http/schemas.d.ts.map +1 -0
  221. package/dist/presence/http/schemas.js +28 -0
  222. package/dist/presence/names.d.ts +23 -0
  223. package/dist/presence/names.d.ts.map +1 -0
  224. package/dist/presence/names.js +22 -0
  225. package/dist/presence/room.d.ts +95 -0
  226. package/dist/presence/room.d.ts.map +1 -0
  227. package/dist/presence/room.js +105 -0
  228. package/dist/presence/ticket.d.ts +55 -0
  229. package/dist/presence/ticket.d.ts.map +1 -0
  230. package/dist/presence/ticket.js +109 -0
  231. package/dist/privacy/classify.js +1 -1
  232. package/dist/privacy/encrypted.js +1 -1
  233. package/dist/processing/activity.d.ts +134 -0
  234. package/dist/processing/activity.d.ts.map +1 -0
  235. package/dist/processing/activity.js +161 -0
  236. package/dist/processing/composed.d.ts +18 -0
  237. package/dist/processing/composed.d.ts.map +1 -0
  238. package/dist/processing/composed.js +28 -0
  239. package/dist/processing/vocabulary.d.ts +26 -0
  240. package/dist/processing/vocabulary.d.ts.map +1 -0
  241. package/dist/processing/vocabulary.js +35 -0
  242. package/dist/queue/consumer.d.ts +59 -0
  243. package/dist/queue/consumer.d.ts.map +1 -0
  244. package/dist/queue/consumer.js +46 -0
  245. package/dist/retention/composed.d.ts +37 -0
  246. package/dist/retention/composed.d.ts.map +1 -0
  247. package/dist/retention/composed.js +66 -0
  248. package/dist/retention/holds.d.ts +61 -0
  249. package/dist/retention/holds.d.ts.map +1 -0
  250. package/dist/retention/holds.js +107 -0
  251. package/dist/retention/holdsPurge.d.ts +22 -0
  252. package/dist/retention/holdsPurge.d.ts.map +1 -0
  253. package/dist/retention/holdsPurge.js +50 -0
  254. package/dist/retention/policy.d.ts +132 -0
  255. package/dist/retention/policy.d.ts.map +1 -0
  256. package/dist/retention/policy.js +197 -0
  257. package/dist/session/rotated.d.ts +34 -0
  258. package/dist/session/rotated.d.ts.map +1 -0
  259. package/dist/session/rotated.js +25 -0
  260. package/dist/test-utils/connectionFixtures.d.ts +13 -0
  261. package/dist/test-utils/connectionFixtures.d.ts.map +1 -0
  262. package/dist/test-utils/connectionFixtures.js +58 -0
  263. package/dist/test-utils/interleave.d.ts +12 -0
  264. package/dist/test-utils/interleave.d.ts.map +1 -0
  265. package/dist/test-utils/interleave.js +26 -0
  266. package/dist/testWorker.d.ts +8 -0
  267. package/dist/testWorker.d.ts.map +1 -0
  268. package/dist/testWorker.js +9 -0
  269. package/dist/version.generated.d.ts +1 -1
  270. package/dist/version.generated.js +1 -1
  271. package/dist/workflow/dispatchRoute.js +1 -1
  272. package/package.json +1 -1
  273. package/src/audit/auditEvent.ts +1 -1
  274. package/src/audit/chain.ts +551 -0
  275. package/src/audit/citations.ts +63 -0
  276. package/src/audit/composed.ts +30 -0
  277. package/src/capability/bindings.ts +61 -0
  278. package/src/capability/capability.ts +166 -18
  279. package/src/capability/erasure.ts +82 -0
  280. package/src/capability/personColumns.ts +83 -0
  281. package/src/capability/personRows.ts +125 -0
  282. package/src/capability/purge.ts +208 -8
  283. package/src/capability/settings.ts +7 -0
  284. package/src/capability/vectorIndex.ts +60 -0
  285. package/src/cli/commandCatalog.generated.ts +488 -2
  286. package/src/cli/commandFacts.ts +12 -3
  287. package/src/cloudflare-test.d.ts +3 -1
  288. package/src/consent/purpose.ts +160 -0
  289. package/src/consent/reader.ts +134 -0
  290. package/src/controlPlane/audit/actions.ts +15 -4
  291. package/src/controlPlane/capability.ts +99 -20
  292. package/src/controlPlane/context.ts +10 -2
  293. package/src/controlPlane/data/connection.ts +16 -2
  294. package/src/controlPlane/data/connections.ts +231 -0
  295. package/src/controlPlane/data/hold.ts +52 -0
  296. package/src/controlPlane/data/oidcTrust.ts +249 -0
  297. package/src/controlPlane/data/tables.ts +7 -0
  298. package/src/controlPlane/data/trustFirstCall.ts +37 -0
  299. package/src/controlPlane/discovery/adminRoute.ts +71 -9
  300. package/src/controlPlane/discovery/routeInput.ts +86 -0
  301. package/src/controlPlane/discovery/toolName.ts +23 -0
  302. package/src/controlPlane/erasure/attest.ts +205 -0
  303. package/src/controlPlane/erasure/read.ts +169 -0
  304. package/src/controlPlane/error/errors.ts +69 -2
  305. package/src/controlPlane/evidence/derive.ts +426 -0
  306. package/src/controlPlane/evidence/mapping.ts +118 -0
  307. package/src/controlPlane/evidence/pack.ts +190 -0
  308. package/src/controlPlane/http/connections.ts +129 -0
  309. package/src/controlPlane/http/guard.ts +178 -10
  310. package/src/controlPlane/http/handlers.ts +172 -40
  311. package/src/controlPlane/http/holds.ts +108 -0
  312. package/src/controlPlane/http/presence.ts +71 -0
  313. package/src/controlPlane/http/processing.ts +19 -0
  314. package/src/controlPlane/http/responses.ts +590 -7
  315. package/src/controlPlane/http/routes.ts +302 -3
  316. package/src/controlPlane/http/schemas.ts +126 -0
  317. package/src/controlPlane/http/verify.ts +19 -2
  318. package/src/controlPlane/http/verifyTrust.ts +176 -0
  319. package/src/controlPlane/migrations/0004_connection_trusts.ts +30 -0
  320. package/src/controlPlane/migrations/0005_holds.ts +40 -0
  321. package/src/controlPlane/processing/derive.ts +359 -0
  322. package/src/controlPlane/processing/read.ts +102 -0
  323. package/src/controlPlane/scope/kind.ts +63 -0
  324. package/src/controlPlane/scope/scope.ts +65 -3
  325. package/src/controlPlane/wire.ts +26 -0
  326. package/src/createBackend.ts +31 -6
  327. package/src/createEntrypoint.ts +34 -2
  328. package/src/error/payload.ts +302 -0
  329. package/src/error/pithyError.ts +42 -0
  330. package/src/http/routeSpec.ts +75 -5
  331. package/src/http/validation.ts +35 -3
  332. package/src/http/verification.ts +3 -1
  333. package/src/i18n/translator.ts +2 -1
  334. package/src/index.ts +2 -0
  335. package/src/mcp/annotations.ts +38 -0
  336. package/src/mcp/tools.ts +220 -0
  337. package/src/openapi/document.ts +8 -28
  338. package/src/openapi/operations.ts +62 -0
  339. package/src/presence/channel.ts +46 -0
  340. package/src/presence/declaration.ts +283 -0
  341. package/src/presence/durableObject.ts +287 -0
  342. package/src/presence/frames.ts +153 -0
  343. package/src/presence/http/guard.ts +23 -0
  344. package/src/presence/http/responses.ts +63 -0
  345. package/src/presence/http/routes.ts +165 -0
  346. package/src/presence/http/schemas.ts +39 -0
  347. package/src/presence/names.ts +45 -0
  348. package/src/presence/room.ts +145 -0
  349. package/src/presence/ticket.ts +145 -0
  350. package/src/processing/activity.ts +289 -0
  351. package/src/processing/composed.ts +33 -0
  352. package/src/processing/vocabulary.ts +43 -0
  353. package/src/queue/consumer.ts +104 -0
  354. package/src/retention/composed.ts +75 -0
  355. package/src/retention/holds.ts +175 -0
  356. package/src/retention/holdsPurge.ts +50 -0
  357. package/src/retention/policy.ts +305 -0
  358. package/src/session/rotated.ts +53 -0
  359. package/src/test-utils/connectionFixtures.ts +58 -0
  360. package/src/test-utils/interleave.ts +31 -0
  361. package/src/testWorker.ts +13 -0
  362. package/src/version.generated.ts +1 -1
@@ -0,0 +1,122 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { z } from "zod";
4
+ /**
5
+ * An OIDC trust: the second credential kind a connection may hold, beside a registered key
6
+ * (`docs/AGENT-ACCESS.md` §6.2, #722).
7
+ *
8
+ * A key is a bearer: one client holds the private half. A trust is a **policy**: a trusted issuer, the
9
+ * audience its tokens must name, and a matcher over the claims that say which workload a token was minted
10
+ * for. Nobody holds a secret. The workload asks its own identity provider for a short-lived token, and the
11
+ * Worker checks it against the issuer's published keys with the verifier the webhooks already use,
12
+ * `core/src/http/oidcWebhook.ts`.
13
+ *
14
+ * ## The claims become data here, and that is a deliberate departure
15
+ *
16
+ * In a webhook the claims check is a predicate the **route** holds, because whoever wrote the route knows
17
+ * the legitimate sender. A connection's trust is per adopter, decided at registration by the same person who
18
+ * grants the scopes, so the matcher is stored with the trust and validated on the way in. That is the whole
19
+ * reason {@link OidcClaimMatch} exists rather than a function.
20
+ *
21
+ * ## The claims check is the boundary, not the signature
22
+ *
23
+ * An identity provider signs for everybody with the same keys. GitHub signs a token for every repository on
24
+ * the planet. A trust whose matcher constrains nothing accepts all of them, so {@link OidcClaimMatch} refuses
25
+ * one at registration — structurally, not as a warning — and refuses the claims that name the token rather
26
+ * than the workload ({@link RESERVED_OIDC_CLAIMS}), since a matcher made of those constrains nothing either.
27
+ *
28
+ * ## Named claims, not the `sub` string
29
+ *
30
+ * A `sub` is a concatenation whose composition belongs to the provider and to the adopter's own template.
31
+ * The claims beneath it — GitHub's `repository_id`, `environment`, `ref`; GitLab's `project_id` — are
32
+ * published individually and are stable. So the matcher is a map of claim names to values, and `sub` is one
33
+ * claim among the others rather than a special case.
34
+ *
35
+ * ## No rotation, and revocation changes shape
36
+ *
37
+ * There is no key to append, prove and expire. A token cannot be revoked; the trust is, and every token
38
+ * already minted stays live until it expires — {@link TRUST_REVOCATION_LATENCY} is the sentence that says
39
+ * so at the moment it matters.
40
+ *
41
+ * Nothing in this module reads a clock, a binding or D1. `now` is injected wherever time matters.
42
+ */
43
+ /**
44
+ * Claims that name the token rather than the workload: who minted it, for whom, and when. A matcher built
45
+ * from these alone would accept every workload the issuer serves, so none of them may be registered.
46
+ * `iss` and `aud` are checked anyway, by the trust's own issuer and audience.
47
+ */
48
+ export declare const RESERVED_OIDC_CLAIMS: readonly ["iss", "aud", "exp", "nbf", "iat", "jti"];
49
+ /** The most claims one trust may match on. Far above any real constraint; a bound on a stored row. */
50
+ export declare const MAX_TRUST_CLAIMS = 16;
51
+ /** The most claims a refusal records, so a token carrying a hundred cannot fill the audit trail. */
52
+ export declare const MAX_OBSERVED_CLAIMS = 32;
53
+ /** The longest claim value a matcher stores or a refusal records. */
54
+ export declare const MAX_CLAIM_VALUE = 256;
55
+ /**
56
+ * One claim name in a matcher. A name, not a path: nested claims are not matched, because a matcher that
57
+ * walks a structure is a matcher with more ways to be wrong on the check the whole trust rests on.
58
+ */
59
+ export declare const OidcClaimName: z.ZodString;
60
+ export type OidcClaimName = z.infer<typeof OidcClaimName>;
61
+ /** One claim value in a matcher: compared as a string against the token's claim of the same name. */
62
+ export declare const OidcClaimValue: z.ZodString;
63
+ export type OidcClaimValue = z.infer<typeof OidcClaimValue>;
64
+ /**
65
+ * Which workloads a trust accepts: every named claim must equal its value.
66
+ *
67
+ * **At least one, and that is the refusal the whole credential kind rests on.** An issuer with no claim
68
+ * constraint accepts every workload it serves.
69
+ */
70
+ export declare const OidcClaimMatch: z.ZodRecord<z.ZodString, z.ZodString>;
71
+ export type OidcClaimMatch = z.infer<typeof OidcClaimMatch>;
72
+ /**
73
+ * One registered OIDC trust, an element of the connection's `trusts` column.
74
+ *
75
+ * Stored beside `keys` rather than inside it: the two kinds share nothing but the connection they grant
76
+ * through, and a union array would make every key reader learn about trusts.
77
+ */
78
+ export declare const OidcTrust: z.ZodObject<{
79
+ trustId: z.ZodString;
80
+ issuer: z.ZodURL;
81
+ jwksUrl: z.ZodURL;
82
+ audience: z.ZodString;
83
+ claims: z.ZodRecord<z.ZodString, z.ZodString>;
84
+ authorizedBy: z.ZodEmail;
85
+ validFrom: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
86
+ revokedAt: z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>>;
87
+ firstCallAt: z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>>;
88
+ }, z.core.$strip>;
89
+ export type OidcTrust = z.output<typeof OidcTrust>;
90
+ /**
91
+ * What a caller should expect after a trust is revoked, said at the moment it is revoked rather than
92
+ * discovered afterwards.
93
+ */
94
+ export declare const TRUST_REVOCATION_LATENCY = "A token cannot be revoked, only the trust. Every call naming this trust is refused from now on, but a token already minted stays in its holder's hands until it expires \u2014 the issuer's own lifetime, typically minutes.";
95
+ /** Every trust that may verify a call at `now`: not revoked, and started. */
96
+ export declare function activeTrusts(trusts: readonly OidcTrust[], now: Date): OidcTrust[];
97
+ /** The trust a call names, if it is live at `now`, or null. The revocation check lives here so no caller forgets it. */
98
+ export declare function findVerifyingTrust(trusts: readonly OidcTrust[], trustId: string, now: Date): OidcTrust | null;
99
+ /** Register a trust beside the others. A duplicate id is refused: the id is what a call addresses. */
100
+ export declare function appendTrust(trusts: readonly OidcTrust[], trust: OidcTrust): OidcTrust[];
101
+ /**
102
+ * Revoke one trust at `now`, or answer null when no trust answers to that id. A trust already revoked keeps
103
+ * the instant it was first revoked: the window a call was refused under must not move.
104
+ */
105
+ export declare function revokeTrust(trusts: readonly OidcTrust[], trustId: string, now: Date): OidcTrust[] | null;
106
+ /**
107
+ * Whether a token's claims satisfy a trust's matcher: every registered claim present and equal.
108
+ *
109
+ * An empty matcher matches nothing. Registration refuses one, and this refuses it again rather than letting
110
+ * a hand-edited row vacuously admit every workload.
111
+ */
112
+ export declare function claimsMatch(match: Readonly<Record<string, string>>, claims: Readonly<Record<string, unknown>>): boolean;
113
+ /**
114
+ * The claims a refused token carried, as a refusal records them: the workload's claims, as strings, capped.
115
+ *
116
+ * This is what makes a custom issuer workable and a profiled one self-correcting — register, let the
117
+ * workload run once, read what it actually sent. Only ever called once the signature, issuer and audience
118
+ * have verified, so the caller demonstrably holds a genuine token for this audience and naming its claims
119
+ * leaks nothing it does not already hold.
120
+ */
121
+ export declare function observedClaims(claims: Readonly<Record<string, unknown>>): Record<string, string>;
122
+ //# sourceMappingURL=oidcTrust.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidcTrust.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/oidcTrust.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,YAAI,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAU,CAAC;AAExF,sGAAsG;AACtG,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,qEAAqE;AACrE,eAAO,MAAM,eAAe,MAAM,CAAC;AAUnC;;;GAGG;AACH,eAAO,MAAM,aAAa,aAQvB,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,qGAAqG;AACrG,eAAO,MAAM,cAAc,aAOxB,CAAC;AACJ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D;;;;;GAKG;AACH,eAAO,MAAM,cAAc,uCAUxB,CAAC;AACJ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAK5D;;;;;GAKG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;iBAuCnB,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC;AAEnD;;;GAGG;AACH,eAAO,MAAM,wBAAwB,iOACsL,CAAC;AAE5N,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,GAAG,EAAE,IAAI,GAAG,SAAS,EAAE,CAEjF;AAED,wHAAwH;AACxH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,SAAS,GAAG,IAAI,CAE7G;AAED,sGAAsG;AACtG,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,GAAG,SAAS,EAAE,CASvF;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,SAAS,EAAE,GAAG,IAAI,CAKxG;AAUD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACvC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACxC,OAAO,CAIT;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUhG"}
@@ -0,0 +1,173 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { ControlPlaneKeyConflictError } from "../error/errors.js";
4
+ import { JsonDate } from "../../data/codecs.js";
5
+ import { z } from "zod";
6
+ //#region src/controlPlane/data/oidcTrust.ts
7
+ /**
8
+ * An OIDC trust: the second credential kind a connection may hold, beside a registered key
9
+ * (`docs/AGENT-ACCESS.md` §6.2, #722).
10
+ *
11
+ * A key is a bearer: one client holds the private half. A trust is a **policy**: a trusted issuer, the
12
+ * audience its tokens must name, and a matcher over the claims that say which workload a token was minted
13
+ * for. Nobody holds a secret. The workload asks its own identity provider for a short-lived token, and the
14
+ * Worker checks it against the issuer's published keys with the verifier the webhooks already use,
15
+ * `core/src/http/oidcWebhook.ts`.
16
+ *
17
+ * ## The claims become data here, and that is a deliberate departure
18
+ *
19
+ * In a webhook the claims check is a predicate the **route** holds, because whoever wrote the route knows
20
+ * the legitimate sender. A connection's trust is per adopter, decided at registration by the same person who
21
+ * grants the scopes, so the matcher is stored with the trust and validated on the way in. That is the whole
22
+ * reason {@link OidcClaimMatch} exists rather than a function.
23
+ *
24
+ * ## The claims check is the boundary, not the signature
25
+ *
26
+ * An identity provider signs for everybody with the same keys. GitHub signs a token for every repository on
27
+ * the planet. A trust whose matcher constrains nothing accepts all of them, so {@link OidcClaimMatch} refuses
28
+ * one at registration — structurally, not as a warning — and refuses the claims that name the token rather
29
+ * than the workload ({@link RESERVED_OIDC_CLAIMS}), since a matcher made of those constrains nothing either.
30
+ *
31
+ * ## Named claims, not the `sub` string
32
+ *
33
+ * A `sub` is a concatenation whose composition belongs to the provider and to the adopter's own template.
34
+ * The claims beneath it — GitHub's `repository_id`, `environment`, `ref`; GitLab's `project_id` — are
35
+ * published individually and are stable. So the matcher is a map of claim names to values, and `sub` is one
36
+ * claim among the others rather than a special case.
37
+ *
38
+ * ## No rotation, and revocation changes shape
39
+ *
40
+ * There is no key to append, prove and expire. A token cannot be revoked; the trust is, and every token
41
+ * already minted stays live until it expires — {@link TRUST_REVOCATION_LATENCY} is the sentence that says
42
+ * so at the moment it matters.
43
+ *
44
+ * Nothing in this module reads a clock, a binding or D1. `now` is injected wherever time matters.
45
+ */
46
+ /**
47
+ * Claims that name the token rather than the workload: who minted it, for whom, and when. A matcher built
48
+ * from these alone would accept every workload the issuer serves, so none of them may be registered.
49
+ * `iss` and `aud` are checked anyway, by the trust's own issuer and audience.
50
+ */
51
+ const RESERVED_OIDC_CLAIMS = [
52
+ "iss",
53
+ "aud",
54
+ "exp",
55
+ "nbf",
56
+ "iat",
57
+ "jti"
58
+ ];
59
+ /** The most claims one trust may match on. Far above any real constraint; a bound on a stored row. */
60
+ const MAX_TRUST_CLAIMS = 16;
61
+ /** The most claims a refusal records, so a token carrying a hundred cannot fill the audit trail. */
62
+ const MAX_OBSERVED_CLAIMS = 32;
63
+ /** The longest claim value a matcher stores or a refusal records. */
64
+ const MAX_CLAIM_VALUE = 256;
65
+ /** A claim name: the plain alphabet providers use, plus `.`, `:` and `-` for a customized template. */
66
+ const CLAIM_NAME = /^[A-Za-z0-9_.:-]{1,64}$/;
67
+ /** Every C0 and C1 control, the line separators, and the bidi controls: a value is one line of plain text. */
68
+ const UNRENDERABLE = /[\p{Cc}\p{Bidi_Control}\u2028\u2029]/u;
69
+ const reserved = new Set(RESERVED_OIDC_CLAIMS);
70
+ /**
71
+ * One claim name in a matcher. A name, not a path: nested claims are not matched, because a matcher that
72
+ * walks a structure is a matcher with more ways to be wrong on the check the whole trust rests on.
73
+ */
74
+ const OidcClaimName = z.string().regex(CLAIM_NAME, "A claim name is letters, digits, `_`, `.`, `:` or `-`, at most 64.").refine((name) => !reserved.has(name), { message: `${RESERVED_OIDC_CLAIMS.join(", ")} name the token, not the workload, so they cannot constrain a trust.` }).describe("A claim the token must carry, by name — `repository_id`, `environment`, `project_id`, `sub`. Never one of the claims that name the token itself.");
75
+ /** One claim value in a matcher: compared as a string against the token's claim of the same name. */
76
+ const OidcClaimValue = z.string().min(1, "A claim value cannot be empty.").max(256, `A claim value is 256 characters at most.`).refine((value) => !UNRENDERABLE.test(value), "A claim value is one line of plain text.").describe("The exact value that claim must have. A numeric or boolean claim is compared by its string form, so `1262274726` matches a `repository_id` whichever way the provider typed it.");
77
+ /**
78
+ * Which workloads a trust accepts: every named claim must equal its value.
79
+ *
80
+ * **At least one, and that is the refusal the whole credential kind rests on.** An issuer with no claim
81
+ * constraint accepts every workload it serves.
82
+ */
83
+ const OidcClaimMatch = z.record(OidcClaimName, OidcClaimValue).refine((claims) => Object.keys(claims).length > 0, { message: "A trust needs at least one claim constraint. An issuer alone accepts every workload it serves." }).refine((claims) => Object.keys(claims).length <= 16, { message: `A trust matches on 16 claims at most.` }).describe("The subject constraint: every named claim must equal its value. At least one, because an issuer alone accepts every workload it serves.");
84
+ /** An https URL. A trust's issuer and key endpoint are fetched from, so plain http is refused. */
85
+ const HttpsUrl = z.url({ protocol: /^https$/ });
86
+ /**
87
+ * One registered OIDC trust, an element of the connection's `trusts` column.
88
+ *
89
+ * Stored beside `keys` rather than inside it: the two kinds share nothing but the connection they grant
90
+ * through, and a union array would make every key reader learn about trusts.
91
+ */
92
+ const OidcTrust = z.object({
93
+ trustId: z.string().min(1).max(64).describe("The trust's id, named by a call on the `pithy-control-plane-trust` header the way a token's `kid` names a key. Not a secret."),
94
+ issuer: HttpsUrl.describe("The exact `iss` this trust accepts — `https://token.actions.githubusercontent.com`, `https://gitlab.com`. A token from any other issuer is refused before its claims are read."),
95
+ jwksUrl: HttpsUrl.describe("Where the issuer publishes the keys that sign its tokens, read from its discovery document at registration. Public, so configuration and not a secret."),
96
+ audience: z.string().min(1).max(512).describe("The `aud` a token must name. Defaults to this environment's own Worker origin, which is what stops a staging token being replayed against production."),
97
+ claims: OidcClaimMatch,
98
+ authorizedBy: z.email().max(254).describe("Who sanctioned this trust: the signed-in session's email at registration. Never who may use it — a trust's users are whoever its claims admit."),
99
+ validFrom: JsonDate.describe("When the trust was registered and began to be honored."),
100
+ revokedAt: JsonDate.nullable().describe("When the trust was revoked, or null. Takes effect on the next call; a token already minted is refused from then on, but stays in its holder's hands until it expires."),
101
+ firstCallAt: JsonDate.nullable().describe("When a call first verified under this trust, or null while it is awaiting a first call. Nothing can prove a trust at registration — there is no private key here and no workload present — so this is the only proof there is.")
102
+ }).describe("One OIDC trust: a trusted issuer, the audience its tokens must name, and the claims that say which workloads it admits.");
103
+ /**
104
+ * What a caller should expect after a trust is revoked, said at the moment it is revoked rather than
105
+ * discovered afterwards.
106
+ */
107
+ const TRUST_REVOCATION_LATENCY = "A token cannot be revoked, only the trust. Every call naming this trust is refused from now on, but a token already minted stays in its holder's hands until it expires — the issuer's own lifetime, typically minutes.";
108
+ /** Every trust that may verify a call at `now`: not revoked, and started. */
109
+ function activeTrusts(trusts, now) {
110
+ return trusts.filter((trust) => trust.revokedAt === null && trust.validFrom.getTime() <= now.getTime());
111
+ }
112
+ /** The trust a call names, if it is live at `now`, or null. The revocation check lives here so no caller forgets it. */
113
+ function findVerifyingTrust(trusts, trustId, now) {
114
+ return activeTrusts(trusts, now).find((trust) => trust.trustId === trustId) ?? null;
115
+ }
116
+ /** Register a trust beside the others. A duplicate id is refused: the id is what a call addresses. */
117
+ function appendTrust(trusts, trust) {
118
+ if (trusts.some((existing) => existing.trustId === trust.trustId)) throw new ControlPlaneKeyConflictError({
119
+ message: "That trust id is already registered on this connection.",
120
+ action: "Register the trust under a new id.",
121
+ detail: `duplicate trustId ${trust.trustId}`
122
+ });
123
+ return [...trusts, trust];
124
+ }
125
+ /**
126
+ * Revoke one trust at `now`, or answer null when no trust answers to that id. A trust already revoked keeps
127
+ * the instant it was first revoked: the window a call was refused under must not move.
128
+ */
129
+ function revokeTrust(trusts, trustId, now) {
130
+ if (!trusts.some((trust) => trust.trustId === trustId)) return null;
131
+ return trusts.map((trust) => trust.trustId === trustId && trust.revokedAt === null ? {
132
+ ...trust,
133
+ revokedAt: now
134
+ } : trust);
135
+ }
136
+ /** A claim's string form, or undefined for a value that is not a string, number or boolean. */
137
+ function claimString(value) {
138
+ if (typeof value === "string") return value;
139
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
140
+ if (typeof value === "boolean") return String(value);
141
+ }
142
+ /**
143
+ * Whether a token's claims satisfy a trust's matcher: every registered claim present and equal.
144
+ *
145
+ * An empty matcher matches nothing. Registration refuses one, and this refuses it again rather than letting
146
+ * a hand-edited row vacuously admit every workload.
147
+ */
148
+ function claimsMatch(match, claims) {
149
+ const entries = Object.entries(match);
150
+ if (entries.length === 0) return false;
151
+ return entries.every(([name, expected]) => claimString(claims[name]) === expected);
152
+ }
153
+ /**
154
+ * The claims a refused token carried, as a refusal records them: the workload's claims, as strings, capped.
155
+ *
156
+ * This is what makes a custom issuer workable and a profiled one self-correcting — register, let the
157
+ * workload run once, read what it actually sent. Only ever called once the signature, issuer and audience
158
+ * have verified, so the caller demonstrably holds a genuine token for this audience and naming its claims
159
+ * leaks nothing it does not already hold.
160
+ */
161
+ function observedClaims(claims) {
162
+ const recorded = {};
163
+ for (const [name, value] of Object.entries(claims)) {
164
+ if (Object.keys(recorded).length >= 32) break;
165
+ if (reserved.has(name) || !CLAIM_NAME.test(name)) continue;
166
+ const text = claimString(value);
167
+ if (text === void 0) continue;
168
+ recorded[name] = text.slice(0, 256);
169
+ }
170
+ return recorded;
171
+ }
172
+ //#endregion
173
+ export { MAX_CLAIM_VALUE, MAX_OBSERVED_CLAIMS, MAX_TRUST_CLAIMS, OidcClaimMatch, OidcClaimName, OidcClaimValue, OidcTrust, RESERVED_OIDC_CLAIMS, TRUST_REVOCATION_LATENCY, activeTrusts, appendTrust, claimsMatch, findVerifyingTrust, observedClaims, revokeTrust };
@@ -5,6 +5,7 @@ import type { Kysely } from "kysely";
5
5
  import type { z } from "zod";
6
6
  import { type DatabaseSchema } from "../../data/db";
7
7
  import { ControlPlaneConnection } from "./connection";
8
+ import { LegalHold } from "./hold";
8
9
  import { ControlPlaneReplay } from "./replay";
9
10
  /**
10
11
  * The connections table. `CamelCasePlugin` snake-cases it to `pithy_controlplane_connections` — the
@@ -19,6 +20,8 @@ export declare const CONTROL_PLANE_CONNECTIONS_TABLE = "pithyControlplaneConnect
19
20
  * makes the claim a primary-key insert and therefore strongly consistent. See `../replay/guard.ts`.
20
21
  */
21
22
  export declare const CONTROL_PLANE_REPLAYS_TABLE = "pithyControlplaneReplays";
23
+ /** Legal holds, kept after release as the record that one stood. */
24
+ export declare const CONTROL_PLANE_HOLDS_TABLE = "pithyControlplaneHolds";
22
25
  /**
23
26
  * The control-plane tables map. Both are always present: a Worker that composes the seam and has never
24
27
  * been connected still needs somewhere for the answer "nobody is connected" to come from, and the
@@ -29,6 +32,7 @@ export declare function controlPlaneTables(): Record<string, z.ZodObject>;
29
32
  export type ControlPlaneTables = {
30
33
  [CONTROL_PLANE_CONNECTIONS_TABLE]: typeof ControlPlaneConnection;
31
34
  [CONTROL_PLANE_REPLAYS_TABLE]: typeof ControlPlaneReplay;
35
+ [CONTROL_PLANE_HOLDS_TABLE]: typeof LegalHold;
32
36
  };
33
37
  export type ControlPlaneDatabase = Kysely<DatabaseSchema<ControlPlaneTables>>;
34
38
  /** Build the control-plane database from the `DB` binding (CamelCasePlugin installed). */
@@ -1 +1 @@
1
- {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/tables.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,iCAAiC,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,6BAA6B,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAKhE;AAED,+DAA+D;AAC/D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,CAAC,+BAA+B,CAAC,EAAE,OAAO,sBAAsB,CAAC;IACjE,CAAC,2BAA2B,CAAC,EAAE,OAAO,kBAAkB,CAAC;CAC1D,CAAC;AACF,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAE9E,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,UAAU,GAAG,oBAAoB,CAKzE"}
1
+ {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/tables.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,iCAAiC,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,6BAA6B,CAAC;AAEtE,oEAAoE;AACpE,eAAO,MAAM,yBAAyB,2BAA2B,CAAC;AAElE;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAMhE;AAED,+DAA+D;AAC/D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,CAAC,+BAA+B,CAAC,EAAE,OAAO,sBAAsB,CAAC;IACjE,CAAC,2BAA2B,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACzD,CAAC,yBAAyB,CAAC,EAAE,OAAO,SAAS,CAAC;CAC/C,CAAC;AACF,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAE9E,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,UAAU,GAAG,oBAAoB,CAMzE"}
@@ -1,7 +1,8 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
- import { ControlPlaneConnection } from "./connection.js";
3
+ import { LegalHold } from "./hold.js";
4
4
  import { createDatabase } from "../../data/db.js";
5
+ import { ControlPlaneConnection } from "./connection.js";
5
6
  import { ControlPlaneReplay } from "./replay.js";
6
7
  //#region src/controlPlane/data/tables.ts
7
8
  /**
@@ -17,6 +18,8 @@ const CONTROL_PLANE_CONNECTIONS_TABLE = "pithyControlplaneConnections";
17
18
  * makes the claim a primary-key insert and therefore strongly consistent. See `../replay/guard.ts`.
18
19
  */
19
20
  const CONTROL_PLANE_REPLAYS_TABLE = "pithyControlplaneReplays";
21
+ /** Legal holds, kept after release as the record that one stood. */
22
+ const CONTROL_PLANE_HOLDS_TABLE = "pithyControlplaneHolds";
20
23
  /**
21
24
  * The control-plane tables map. Both are always present: a Worker that composes the seam and has never
22
25
  * been connected still needs somewhere for the answer "nobody is connected" to come from, and the
@@ -25,15 +28,17 @@ const CONTROL_PLANE_REPLAYS_TABLE = "pithyControlplaneReplays";
25
28
  function controlPlaneTables() {
26
29
  return {
27
30
  [CONTROL_PLANE_CONNECTIONS_TABLE]: ControlPlaneConnection,
28
- [CONTROL_PLANE_REPLAYS_TABLE]: ControlPlaneReplay
31
+ [CONTROL_PLANE_REPLAYS_TABLE]: ControlPlaneReplay,
32
+ [CONTROL_PLANE_HOLDS_TABLE]: LegalHold
29
33
  };
30
34
  }
31
35
  /** Build the control-plane database from the `DB` binding (CamelCasePlugin installed). */
32
36
  function controlPlaneDatabase(d1) {
33
37
  return createDatabase(d1, {
34
38
  [CONTROL_PLANE_CONNECTIONS_TABLE]: ControlPlaneConnection,
35
- [CONTROL_PLANE_REPLAYS_TABLE]: ControlPlaneReplay
39
+ [CONTROL_PLANE_REPLAYS_TABLE]: ControlPlaneReplay,
40
+ [CONTROL_PLANE_HOLDS_TABLE]: LegalHold
36
41
  });
37
42
  }
38
43
  //#endregion
39
- export { CONTROL_PLANE_CONNECTIONS_TABLE, CONTROL_PLANE_REPLAYS_TABLE, controlPlaneDatabase, controlPlaneTables };
44
+ export { CONTROL_PLANE_CONNECTIONS_TABLE, CONTROL_PLANE_HOLDS_TABLE, CONTROL_PLANE_REPLAYS_TABLE, controlPlaneDatabase, controlPlaneTables };
@@ -0,0 +1,16 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { type ControlPlaneDatabase } from "./tables";
4
+ /**
5
+ * Record the first call a trust answered (#722), so a listing stops saying *awaiting a first call*.
6
+ *
7
+ * **One statement that touches one field, never a read-modify-write of the column.** The verifier read the row
8
+ * moments ago, and an operator may have revoked the trust since. Writing back the whole `trusts` array as the
9
+ * verifier saw it would resurrect that trust — a revocation undone by the very call it was meant to stop. So the
10
+ * write is `json_set` on the matching element's `firstCallAt` alone, and only while that element is still
11
+ * unrevoked and still uncalled: a revoked trust is left exactly as revoked, and a second first call changes nothing.
12
+ *
13
+ * The instant is written as ISO-8601, the shape `JsonDate` stores inside the column.
14
+ */
15
+ export declare function recordTrustFirstCall(db: ControlPlaneDatabase, connectionId: string, trustId: string, at: Date): Promise<void>;
16
+ //# sourceMappingURL=trustFirstCall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trustFirstCall.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/trustFirstCall.ts"],"names":[],"mappings":"AAIA,OAAO,EAAmC,KAAK,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,oBAAoB,EACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,IAAI,GACP,OAAO,CAAC,IAAI,CAAC,CAcf"}
@@ -0,0 +1,25 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { CONTROL_PLANE_CONNECTIONS_TABLE } from "./tables.js";
4
+ import { sql } from "kysely";
5
+ //#region src/controlPlane/data/trustFirstCall.ts
6
+ /**
7
+ * Record the first call a trust answered (#722), so a listing stops saying *awaiting a first call*.
8
+ *
9
+ * **One statement that touches one field, never a read-modify-write of the column.** The verifier read the row
10
+ * moments ago, and an operator may have revoked the trust since. Writing back the whole `trusts` array as the
11
+ * verifier saw it would resurrect that trust — a revocation undone by the very call it was meant to stop. So the
12
+ * write is `json_set` on the matching element's `firstCallAt` alone, and only while that element is still
13
+ * unrevoked and still uncalled: a revoked trust is left exactly as revoked, and a second first call changes nothing.
14
+ *
15
+ * The instant is written as ISO-8601, the shape `JsonDate` stores inside the column.
16
+ */
17
+ async function recordTrustFirstCall(db, connectionId, trustId, at) {
18
+ const trusts = sql.ref("trusts");
19
+ const uncalled = (alias) => sql`json_extract(${sql.ref(`${alias}.value`)}, '$.trustId') = ${trustId}
20
+ and json_extract(${sql.ref(`${alias}.value`)}, '$.revokedAt') is null
21
+ and json_extract(${sql.ref(`${alias}.value`)}, '$.firstCallAt') is null`;
22
+ await db.updateTable(CONTROL_PLANE_CONNECTIONS_TABLE).set({ trusts: sql`json_set(${trusts}, '$[' || (select ${sql.ref("t.key")} from json_each(${trusts}) as t where ${uncalled("t")} limit 1) || '].firstCallAt', ${at.toISOString()})` }).where("id", "=", connectionId).where(sql`exists (select 1 from json_each(${trusts}) as u where ${uncalled("u")})`).execute();
23
+ }
24
+ //#endregion
25
+ export { recordTrustFirstCall };