@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
@@ -5,6 +5,7 @@ import { commandName } from "../../cli/command";
5
5
  import { PithyError } from "../../error/pithyError";
6
6
  import type { MessageParams } from "../../i18n/catalog";
7
7
  import type { UnmarkedString } from "../../privacy/types";
8
+ import type { ControlPlaneActorKind } from "../data/connection";
8
9
 
9
10
  /**
10
11
  * The `control-plane` strategy's throw sugar. The `controlplane/*` codes live in core's closed
@@ -53,12 +54,39 @@ export class ControlPlaneNotConnectedError extends PithyError {
53
54
  }
54
55
  }
55
56
 
57
+ /**
58
+ * What a call under an OIDC trust carried when its claims did not match (#722), for the audit row and nobody
59
+ * else.
60
+ *
61
+ * Set only once the token's signature, issuer and audience have verified: by then the caller demonstrably
62
+ * holds a genuine token for this audience, so naming the claims it sent leaks nothing it does not already
63
+ * hold — and those claims are what an operator picks a matcher from. Before that the single-error discipline
64
+ * holds, and no refusal carries this.
65
+ */
66
+ export interface ControlPlaneRefusedTrust {
67
+ /** The connection whose trust was named. */
68
+ readonly connectionId: string;
69
+ /** The trust the call named. */
70
+ readonly trustId: string;
71
+ /** The workload's claims, as strings, capped — `observedClaims` in `../data/oidcTrust`. */
72
+ readonly observedClaims: Readonly<Record<string, string>>;
73
+ }
74
+
56
75
  /**
57
76
  * The credential failed verification. One error for every step — pass the step in `detail` so the log
58
77
  * can say which, and the caller cannot.
59
78
  */
60
79
  export class ControlPlaneInvalidCredentialError extends PithyError {
61
- constructor(args: ControlPlaneErrorArgs = {}, options?: { cause?: unknown }) {
80
+ /**
81
+ * A claim mismatch on an otherwise verified OIDC token, for the denial row. A property of the vehicle, never of
82
+ * `payload`, so the refusal a caller receives is the same one every other step answers. Null everywhere else.
83
+ */
84
+ readonly refusedTrust: ControlPlaneRefusedTrust | null;
85
+
86
+ constructor(
87
+ args: ControlPlaneErrorArgs & { refusedTrust?: ControlPlaneRefusedTrust } = {},
88
+ options?: { cause?: unknown },
89
+ ) {
62
90
  super(
63
91
  {
64
92
  code: "controlplane/invalid_credential",
@@ -72,12 +100,50 @@ export class ControlPlaneInvalidCredentialError extends PithyError {
72
100
  },
73
101
  options,
74
102
  );
103
+ this.refusedTrust = args.refusedTrust ?? null;
75
104
  }
76
105
  }
77
106
 
107
+ /**
108
+ * Who a scope refusal refused, as verification proved it (#812).
109
+ *
110
+ * By the scope step the signature, the key, the lifetime and the body digest have all verified, so these are facts
111
+ * rather than claims. The gate writes them onto the denial row, which is what lets a known credential probing
112
+ * for scopes it lacks be traced back to it.
113
+ */
114
+ export interface ControlPlaneRefusedCaller {
115
+ /** The connection the verified token addressed. */
116
+ readonly connectionId: string;
117
+ /** The token's `sub`, proven by the signature. */
118
+ readonly subject: string;
119
+ /** The registered key that verified the signature — or, for a call under an OIDC trust, the trust. */
120
+ readonly keyId: string;
121
+ /**
122
+ * Said only for a call under an OIDC trust (#722), in which case `keyId` names the trust. Absent reads as a key, as
123
+ * it does on the allowed row, so a trust's refusals are not recorded as a key's with an id no key has.
124
+ */
125
+ readonly credentialKind?: "trust";
126
+ /**
127
+ * The kind of client the connection was registered as, from the adopter's row and never the token — the same value
128
+ * the allowed row is stamped with, so a trail filtered on kind holds a client's refusals beside its calls. Null for a
129
+ * connection registered before kinds were recorded.
130
+ */
131
+ readonly actorKind: ControlPlaneActorKind | null;
132
+ }
133
+
78
134
  /** The credential verified, but nothing granted it this operation. */
79
135
  export class ControlPlaneInsufficientScopeError extends PithyError {
80
- constructor(args: ControlPlaneErrorArgs = {}, options?: { cause?: unknown }) {
136
+ /**
137
+ * The verified caller, for the audit row and nobody else. A property of the vehicle, never of `payload`: the HTTP
138
+ * codec encodes the payload alone, so the refusal a caller receives is unchanged. Null when a throw site has no
139
+ * verified caller to name.
140
+ */
141
+ readonly caller: ControlPlaneRefusedCaller | null;
142
+
143
+ constructor(
144
+ args: ControlPlaneErrorArgs & { caller?: ControlPlaneRefusedCaller } = {},
145
+ options?: { cause?: unknown },
146
+ ) {
81
147
  super(
82
148
  {
83
149
  code: "controlplane/insufficient_scope",
@@ -91,6 +157,7 @@ export class ControlPlaneInsufficientScopeError extends PithyError {
91
157
  },
92
158
  options,
93
159
  );
160
+ this.caller = args.caller ?? null;
94
161
  }
95
162
  }
96
163
 
@@ -0,0 +1,426 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { DescribedOperation } from "../../openapi/operations";
5
+ import type { PrivacyAttestation } from "../../privacy/posture";
6
+ import type { PrivacyInventory } from "../../privacy/report";
7
+ import type { RetirementVerdict } from "../../privacy/retirement";
8
+ import type { ControlPlaneConnection, RegisteredKey } from "../data/connection";
9
+ import { type CapabilityHealthReport, type HealthSummaryKey, standingOf } from "../discovery/healthSummary";
10
+ import type { EvidenceEntry, EvidenceFact, MigrationDatabaseView } from "../http/responses";
11
+ import { MIGRATIONS_READ_SCOPE } from "../scope/scope";
12
+ import {
13
+ AUDIT_PACKAGE,
14
+ EVIDENCE_CONTROLS,
15
+ type EvidenceControlId,
16
+ type EvidenceState,
17
+ SECRETS_CAPABILITY,
18
+ SECRETS_PACKAGE,
19
+ } from "./mapping";
20
+
21
+ /**
22
+ * The derivation behind each control in the evidence pack (#821): plain inputs in, facts and a state out.
23
+ *
24
+ * **Every function here is pure.** The reads — a D1 query, a health producer, the route table — happen in
25
+ * `./pack.ts`, which hands each one what it found or the fact that it could not look. So every rule for a state
26
+ * is tested here directly, without a Worker, and the rules cannot drift by being restated at a read site.
27
+ *
28
+ * **What decides a state.** A gap is something positively missing. Not-derivable is the Worker being unable to
29
+ * say. Evidenced is neither. A control with both reports `gap`, and lists the gaps first and the unknowns after,
30
+ * because something known to be missing is the more actionable fact and the unknowns are still there to read.
31
+ * Nothing here says more than the facts do: an unknown is never folded into `evidenced`.
32
+ */
33
+
34
+ /** What a control's derivation produced, before the mapping table's references are attached. */
35
+ export interface Derived {
36
+ state: EvidenceState;
37
+ reasons: string[];
38
+ facts: EvidenceFact[];
39
+ }
40
+
41
+ /** Settle a control's state from what was missing and what could not be read. See the module doc. */
42
+ export function settle(gaps: readonly string[], unknowns: readonly string[], facts: EvidenceFact[]): Derived {
43
+ const state: EvidenceState = gaps.length > 0 ? "gap" : unknowns.length > 0 ? "not-derivable" : "evidenced";
44
+ return { state, reasons: [...gaps, ...unknowns], facts };
45
+ }
46
+
47
+ /** One entry of the pack: the mapping table's row for the control, beside what was derived. */
48
+ export function evidenceEntry(control: EvidenceControlId, derived: Derived): EvidenceEntry {
49
+ const row = EVIDENCE_CONTROLS[control];
50
+ return {
51
+ control,
52
+ title: row.title,
53
+ soc2: [...row.soc2],
54
+ iso27001: [...row.iso27001],
55
+ state: derived.state,
56
+ reasons: derived.reasons,
57
+ facts: derived.facts,
58
+ };
59
+ }
60
+
61
+ /** How many undescribed routes a reason names before it counts the rest. */
62
+ const NAMED_ROUTE_LIMIT = 10;
63
+
64
+ /** What the audit-logging control reads. */
65
+ export interface AuditLoggingInput {
66
+ /** The composed capability declaring an audit recorder, with each binding it writes through, or null for none. */
67
+ recorder: { capability: string; bindings: readonly { binding: string; present: boolean }[] } | null;
68
+ /** Whether this very request's emit seam is a recorder rather than the no-op: the guard wrote through it. */
69
+ emitting: boolean;
70
+ }
71
+
72
+ /**
73
+ * Audit logging in force: a composed recorder, the binding it writes through, and this request's emit seam
74
+ * actually recording. The seam's guard emits on every control-plane call, allowed or denied, so a recorder on
75
+ * the seam is a record of every management call — including the one reading this pack.
76
+ */
77
+ export function deriveAuditLogging(input: AuditLoggingInput): Derived {
78
+ const facts: EvidenceFact[] = [
79
+ { fact: "auditRecorder", value: input.recorder?.capability ?? null, source: "composition" },
80
+ ];
81
+ const gaps: string[] = [];
82
+ if (!input.recorder) {
83
+ gaps.push(
84
+ `No composed capability records audit events, so control-plane calls and every other audited action go unrecorded. Compose ${AUDIT_PACKAGE}.`,
85
+ );
86
+ return settle(gaps, [], facts);
87
+ }
88
+ for (const { binding, present } of input.recorder.bindings) {
89
+ facts.push({ fact: "bindingPresent", subject: binding, value: present, source: "bindings" });
90
+ if (!present) {
91
+ gaps.push(
92
+ `The \`${binding}\` binding \`${input.recorder.capability}\` records through is missing from this Worker, so its events are dropped.`,
93
+ );
94
+ }
95
+ }
96
+ facts.push({ fact: "controlPlaneCallsRecorded", value: input.emitting, source: "auditRecorder" });
97
+ if (!input.emitting) {
98
+ gaps.push(
99
+ `This request's emit seam is not \`${input.recorder.capability}\`'s recorder, so control-plane calls go unrecorded.`,
100
+ );
101
+ }
102
+ return settle(gaps, [], facts);
103
+ }
104
+
105
+ /** The strategies a spec names, as a list. */
106
+ function strategiesOf(operation: DescribedOperation): readonly string[] {
107
+ const verification = operation.spec?.verification;
108
+ if (verification === undefined) return [];
109
+ return typeof verification === "string" ? [verification] : verification;
110
+ }
111
+
112
+ /** Why a control reading the route table cannot: an app `createBackend` did not assemble has no recorded groups. */
113
+ const NO_ROUTE_TABLE = "This Worker's route table was not assembled by `createBackend`, so it cannot be read.";
114
+
115
+ /**
116
+ * Every route declares how a caller is verified: `describeRoute`'s `verification`, read off the composed route
117
+ * table. A route with no `describeRoute` declares nothing, and is named.
118
+ */
119
+ export function deriveRouteVerification(operations: readonly DescribedOperation[] | undefined): Derived {
120
+ if (operations === undefined) return settle([], [NO_ROUTE_TABLE], []);
121
+ const undescribed = operations.filter((operation) => operation.spec === undefined);
122
+ const counts = new Map<string, number>();
123
+ for (const operation of operations) {
124
+ for (const strategy of strategiesOf(operation)) counts.set(strategy, (counts.get(strategy) ?? 0) + 1);
125
+ }
126
+ const facts: EvidenceFact[] = [
127
+ { fact: "routes", value: operations.length, source: "routeSpec" },
128
+ { fact: "undescribedRoutes", value: undescribed.length, source: "routeSpec" },
129
+ ...[...counts].map(
130
+ ([strategy, count]): EvidenceFact => ({
131
+ fact: "routesVerifiedBy",
132
+ subject: strategy,
133
+ value: count,
134
+ source: "routeSpec",
135
+ }),
136
+ ),
137
+ ];
138
+ const gaps = undescribed
139
+ .slice(0, NAMED_ROUTE_LIMIT)
140
+ .map((operation) => `\`${operation.method} ${operation.path}\` declares no verification.`);
141
+ const rest = undescribed.length - NAMED_ROUTE_LIMIT;
142
+ if (rest > 0) gaps.push(`${rest} more ${rest === 1 ? "route declares" : "routes declare"} no verification.`);
143
+ return settle(gaps, [], facts);
144
+ }
145
+
146
+ /** What the management-access and key-rotation controls read off one connection row. Never key material. */
147
+ export type ConnectionRecord = Pick<
148
+ ControlPlaneConnection,
149
+ "id" | "authorizedBy" | "actorKind" | "purpose" | "scopes"
150
+ > & {
151
+ keys: readonly Pick<RegisteredKey, "validFrom" | "validUntil" | "revokedAt">[];
152
+ };
153
+
154
+ /** Why a control reading the connections table cannot. */
155
+ const CONNECTIONS_UNREADABLE = "The control-plane connections table could not be read on this call.";
156
+
157
+ /**
158
+ * Management access attributed and scoped: every connection in this environment, with the principal who
159
+ * authorized it, its actor kind, its purpose (#721) and its granted scopes. The principal is the email the
160
+ * connection records and nothing else; no audit row's actor is copied in.
161
+ */
162
+ export function deriveManagementAccess(connections: readonly ConnectionRecord[] | null): Derived {
163
+ if (connections === null) return settle([], [CONNECTIONS_UNREADABLE], []);
164
+ const facts: EvidenceFact[] = [{ fact: "connections", value: connections.length, source: "connection" }];
165
+ const gaps: string[] = [];
166
+ for (const connection of connections) {
167
+ facts.push(
168
+ { fact: "authorizedBy", subject: connection.id, value: connection.authorizedBy, source: "connection" },
169
+ { fact: "actorKind", subject: connection.id, value: connection.actorKind, source: "connection" },
170
+ { fact: "purpose", subject: connection.id, value: connection.purpose, source: "connection" },
171
+ { fact: "scopes", subject: connection.id, value: [...connection.scopes], source: "connection" },
172
+ );
173
+ const missing = [
174
+ connection.authorizedBy === null ? "principal" : null,
175
+ connection.actorKind === null ? "actor kind" : null,
176
+ connection.purpose === null ? "purpose" : null,
177
+ ].filter((name): name is string => name !== null);
178
+ if (missing.length > 0) {
179
+ gaps.push(
180
+ `Connection ${connection.id} records no ${missing.join(", ")}: it was registered before they were recorded. Registering it again records them.`,
181
+ );
182
+ }
183
+ }
184
+ return settle(gaps, [], facts);
185
+ }
186
+
187
+ /** A day, in ms. Key ages are reported in whole days, which is how a rotation cadence is reasoned about. */
188
+ const MS_PER_DAY = 86_400_000;
189
+
190
+ /** Whether a key verifies right now: started, not past its window, not revoked. */
191
+ function isLive(key: ConnectionRecord["keys"][number], now: Date): boolean {
192
+ if (key.revokedAt !== null) return false;
193
+ if (key.validFrom.getTime() > now.getTime()) return false;
194
+ return key.validUntil === null || key.validUntil.getTime() > now.getTime();
195
+ }
196
+
197
+ /** What secrets' rotation status was on this call: its health report and the keys that declare it, or absent. */
198
+ export type SecretsRotationInput =
199
+ | { state: "not-composed" }
200
+ | { state: "composed"; keys: readonly HealthSummaryKey[]; report: CapabilityHealthReport };
201
+
202
+ /** What the key-rotation control reads. */
203
+ export interface KeyRotationInput {
204
+ connections: readonly ConnectionRecord[] | null;
205
+ now: Date;
206
+ secrets: SecretsRotationInput;
207
+ retirement: RetirementVerdict;
208
+ }
209
+
210
+ /** Secrets' facts, gaps and unknowns from its declared health — by standing for a state, never by its word. */
211
+ function secretsRotation(input: SecretsRotationInput, facts: EvidenceFact[], gaps: string[], unknowns: string[]): void {
212
+ if (input.state === "not-composed") {
213
+ unknowns.push(`${SECRETS_PACKAGE} is not composed, so no secret's rotation cadence is declared here.`);
214
+ return;
215
+ }
216
+ const report = input.report;
217
+ if (report.state === "withheld") {
218
+ const scopes = [...new Set(input.keys.map((key) => `\`${key.scope}\``))].join(", ");
219
+ unknowns.push(
220
+ `This connection is not granted ${scopes}, the scope \`${SECRETS_CAPABILITY}\` reports rotation status under.`,
221
+ );
222
+ return;
223
+ }
224
+ if (report.state === "unavailable") {
225
+ unknowns.push(`\`${SECRETS_CAPABILITY}\` could not report its rotation status on this call.`);
226
+ return;
227
+ }
228
+ if (report.state === "undeclared") {
229
+ unknowns.push(`\`${SECRETS_CAPABILITY}\` reports no rotation status: it predates the health summary.`);
230
+ return;
231
+ }
232
+ for (const key of input.keys) {
233
+ const value = report.values[key.key];
234
+ if (value === undefined) continue;
235
+ const standing = standingOf(key, value);
236
+ // A count is a number and says what it is. A state is reported by its standing, so the capability's own word
237
+ // for an outcome never becomes the pack's: the pack carries evidence, and leaves the outcome to its route.
238
+ facts.push({
239
+ fact: key.key,
240
+ subject: SECRETS_CAPABILITY,
241
+ value: key.kind === "count" ? value : standing,
242
+ source: "capabilityHealth",
243
+ });
244
+ if (standing === "attention") {
245
+ gaps.push(
246
+ `\`${SECRETS_CAPABILITY}\` reports \`${key.key}\` wanting attention. Its status route says which and why.`,
247
+ );
248
+ }
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Keys and secrets rotate: each connection's live keys and the age of the oldest, secrets' declared cadence and
254
+ * what is past it, and the field key's retirement guard (#390). Ages are facts and never graded here: the seam
255
+ * declares no cadence for control-plane keys, and inventing one would be the pack's opinion.
256
+ */
257
+ export function deriveKeyRotation(input: KeyRotationInput): Derived {
258
+ const facts: EvidenceFact[] = [];
259
+ const gaps: string[] = [];
260
+ const unknowns: string[] = [];
261
+ if (input.connections === null) {
262
+ unknowns.push(`${CONNECTIONS_UNREADABLE} Key ages are unknown.`);
263
+ } else {
264
+ for (const connection of input.connections) {
265
+ const live = connection.keys.filter((key) => isLive(key, input.now));
266
+ const oldest = live.reduce<number | null>(
267
+ (min, key) => (min === null ? key.validFrom.getTime() : Math.min(min, key.validFrom.getTime())),
268
+ null,
269
+ );
270
+ facts.push(
271
+ { fact: "liveKeys", subject: connection.id, value: live.length, source: "connectionKeys" },
272
+ {
273
+ fact: "oldestLiveKeyAgeDays",
274
+ subject: connection.id,
275
+ value: oldest === null ? null : Math.floor((input.now.getTime() - oldest) / MS_PER_DAY),
276
+ source: "connectionKeys",
277
+ },
278
+ );
279
+ }
280
+ }
281
+ secretsRotation(input.secrets, facts, gaps, unknowns);
282
+ facts.push(
283
+ { fact: "fieldKeyRetirementBlocked", value: input.retirement.blocked, source: "fieldKeyRetirement" },
284
+ { fact: "fieldKeyRetirementReasons", value: [...input.retirement.reasons], source: "fieldKeyRetirement" },
285
+ { fact: "liveFieldKeyVersions", value: input.retirement.liveVersions, source: "fieldKeyRetirement" },
286
+ );
287
+ if (input.retirement.blocked) {
288
+ gaps.push(`The field key's oldest live version is held from retirement: ${input.retirement.reasons.join(", ")}.`);
289
+ } else if (input.retirement.reasons.includes("incomplete-coverage")) {
290
+ // The guard reports `blocked: false` when it could not resolve the key set or derive the columns: nothing is
291
+ // due because nothing was read. That is the Worker unable to say, never a clean guard.
292
+ unknowns.push("The field key's retirement guard could not read every source on this call.");
293
+ }
294
+ return settle(gaps, unknowns, facts);
295
+ }
296
+
297
+ /** What the change-history control reads. */
298
+ export interface ChangeHistoryInput {
299
+ /** Whether this connection holds `migrations:read`, the scope the seam keeps migration state behind. */
300
+ granted: boolean;
301
+ /** Each bound database's ledger, read only when granted. */
302
+ databases: readonly MigrationDatabaseView[];
303
+ }
304
+
305
+ /**
306
+ * Change history per environment: each bound database's applied migrations, the last group and when it applied
307
+ * (#729). Behind `migrations:read`, as the same numbers are on the manifest, so the pack never widens it.
308
+ */
309
+ export function deriveChangeHistory(input: ChangeHistoryInput): Derived {
310
+ if (!input.granted) {
311
+ return settle(
312
+ [],
313
+ [`This connection is not granted \`${MIGRATIONS_READ_SCOPE}\`, the scope migration state is read under.`],
314
+ [],
315
+ );
316
+ }
317
+ const facts: EvidenceFact[] = [];
318
+ const gaps: string[] = [];
319
+ const unknowns: string[] = [];
320
+ for (const database of input.databases) {
321
+ if (database.state === "unreadable") {
322
+ unknowns.push(`The migration ledger of database \`${database.database}\` could not be read on this call.`);
323
+ continue;
324
+ }
325
+ const subject = database.database;
326
+ facts.push(
327
+ { fact: "applied", subject, value: database.applied.length, source: "migrationLedger" },
328
+ { fact: "pending", subject, value: database.pending.length, source: "migrationLedger" },
329
+ { fact: "undeclared", subject, value: database.undeclared.length, source: "migrationLedger" },
330
+ { fact: "lastGroup", subject, value: database.lastGroup?.group ?? null, source: "migrationLedger" },
331
+ { fact: "lastGroupAppliedAt", subject, value: database.lastGroup?.appliedAt ?? null, source: "migrationLedger" },
332
+ );
333
+ if (database.pending.length > 0) {
334
+ gaps.push(
335
+ `Database \`${subject}\` has ${database.pending.length} declared ${database.pending.length === 1 ? "migration" : "migrations"} this build ships and has not applied.`,
336
+ );
337
+ }
338
+ }
339
+ return settle(gaps, unknowns, facts);
340
+ }
341
+
342
+ /** What the data-protection control reads. */
343
+ export interface DataProtectionInput {
344
+ /** The privacy inventory of every stored schema, or null where the walk could not run. */
345
+ inventory: PrivacyInventory | null;
346
+ /** What the last `pithy deploy` attested the project declares, or null where none reached this Worker. */
347
+ attestation: PrivacyAttestation | null;
348
+ }
349
+
350
+ /**
351
+ * Data classified and encrypted at rest: the classes the stored schemas hold (#132) beside the classes the
352
+ * project declared, and the fields marked `encrypted()` (#390). Classes and counts only, never a field's name:
353
+ * those are `GET /privacy`'s, behind their own scope.
354
+ */
355
+ export function deriveDataProtection(input: DataProtectionInput): Derived {
356
+ if (input.inventory === null) {
357
+ return settle([], ["This Worker's stored schemas could not be walked for privacy markers on this call."], []);
358
+ }
359
+ const held = input.inventory;
360
+ const facts: EvidenceFact[] = [
361
+ { fact: "classesHeld", value: [...held.observed], source: "privacyMarkers" },
362
+ ...held.observed.map(
363
+ (name): EvidenceFact => ({
364
+ fact: "markedFields",
365
+ subject: name,
366
+ value: held.counts[name] ?? 0,
367
+ source: "privacyMarkers",
368
+ }),
369
+ ),
370
+ { fact: "schemasWalked", value: held.schemasWalked, source: "privacyMarkers" },
371
+ { fact: "encryptedFields", value: held.encryptedFields, source: "encryptedMarkers" },
372
+ { fact: "unrotatableEncryptedFields", value: held.unrotatable, source: "encryptedMarkers" },
373
+ {
374
+ fact: "classesDeclared",
375
+ value: input.attestation === null ? null : [...input.attestation.classes],
376
+ source: "privacyAttestation",
377
+ },
378
+ ];
379
+ if (input.attestation === null) {
380
+ return settle(
381
+ [],
382
+ [
383
+ "No privacy attestation reached this Worker, so what the project declares cannot be read. A deploy through the CLI writes one.",
384
+ ],
385
+ facts,
386
+ );
387
+ }
388
+ const declared = new Set(input.attestation.classes);
389
+ const observed = new Set(held.observed);
390
+ const gaps = [
391
+ ...held.observed
392
+ .filter((name) => !declared.has(name))
393
+ .map((name) => `Stored schemas hold \`${name}\` fields the project never declared.`),
394
+ ...input.attestation.classes
395
+ .filter((name) => !observed.has(name))
396
+ .map((name) => `The project declares \`${name}\` and no stored field is marked with it.`),
397
+ ];
398
+ return settle(gaps, [], facts);
399
+ }
400
+
401
+ /** What the CSRF control reads. */
402
+ export interface CsrfInput {
403
+ operations: readonly DescribedOperation[] | undefined;
404
+ /** Whether a composed capability published a same-origin policy on this request. */
405
+ sameOriginPublished: boolean;
406
+ }
407
+
408
+ /**
409
+ * CSRF wherever cookie mode is: the routes that accept a session cookie, and whether a composed capability
410
+ * publishes the same-origin policy `requireSameOrigin()` enforces. No route taking a cookie needs none.
411
+ */
412
+ export function deriveCsrf(input: CsrfInput): Derived {
413
+ if (input.operations === undefined) return settle([], [NO_ROUTE_TABLE], []);
414
+ const sessionRoutes = input.operations.filter((operation) => strategiesOf(operation).includes("session")).length;
415
+ const facts: EvidenceFact[] = [
416
+ { fact: "sessionRoutes", value: sessionRoutes, source: "routeSpec" },
417
+ { fact: "sameOriginPolicy", value: input.sameOriginPublished, source: "sameOriginGate" },
418
+ ];
419
+ const gaps =
420
+ sessionRoutes > 0 && !input.sameOriginPublished
421
+ ? [
422
+ `${sessionRoutes} ${sessionRoutes === 1 ? "route accepts" : "routes accept"} a session cookie and no composed capability publishes a same-origin policy. Compose \`@pithy-sh/auth\`.`,
423
+ ]
424
+ : [];
425
+ return settle(gaps, [], facts);
426
+ }
@@ -0,0 +1,118 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { z } from "zod";
5
+
6
+ /**
7
+ * The evidence pack's mapping table (#821): which controls the Worker derives evidence for, and the SOC 2 and
8
+ * ISO/IEC 27001:2022 references each one answers.
9
+ *
10
+ * **Evidence, never a verdict.** Each control the pack reports carries the facts it read, where each came from,
11
+ * and one of three states. None of them is a judgment about a framework: an auditor decides that, and nothing
12
+ * here or on the wire uses the vocabulary of one.
13
+ *
14
+ * **Versioned, and the version travels with every pack.** A reference moving, a control joining or leaving, or
15
+ * a rule for a state changing is a new {@link EVIDENCE_MAPPING_VERSION}, so a pack read last quarter and one read
16
+ * today can be told apart by the number on them. A control needing a declaration the kit does not hold is a
17
+ * later version of this table, never a guess in this one.
18
+ *
19
+ * Zod-only, so a management client can import the table and the vocabulary without the Worker's runtime.
20
+ */
21
+
22
+ /** The version of this table. Bumped whenever a control, a reference, or a state rule changes. */
23
+ export const EVIDENCE_MAPPING_VERSION = 1;
24
+
25
+ /** The seven controls, by id. Stable: a client keys on these, and the title may be reworded. */
26
+ export const EVIDENCE_CONTROL_IDS = [
27
+ "audit-logging",
28
+ "route-verification",
29
+ "management-access",
30
+ "key-rotation",
31
+ "change-history",
32
+ "data-protection",
33
+ "csrf",
34
+ ] as const;
35
+
36
+ /** One control's id. */
37
+ export const EvidenceControlId = z
38
+ .enum(EVIDENCE_CONTROL_IDS)
39
+ .describe("The control this entry reports on, as a stable id. Keyed on by clients; the title may be reworded.");
40
+ export type EvidenceControlId = z.infer<typeof EvidenceControlId>;
41
+
42
+ /**
43
+ * What the Worker could say about a control. Three states, and none of them a verdict.
44
+ *
45
+ * - `evidenced`: the facts are present.
46
+ * - `gap`: something is missing, and the entry names what.
47
+ * - `not-derivable`: the Worker cannot say, and the entry names why.
48
+ */
49
+ export const EvidenceState = z
50
+ .enum(["evidenced", "gap", "not-derivable"])
51
+ .describe(
52
+ "What the Worker could say about this control: `evidenced` when the facts are present, `gap` when something is missing and the entry names what, `not-derivable` when the Worker cannot say and the entry names why. Evidence for an auditor, never a judgment.",
53
+ );
54
+ export type EvidenceState = z.infer<typeof EvidenceState>;
55
+
56
+ /**
57
+ * Where a fact came from: the declaration or record the Worker read it off. Every fact names one, so an auditor
58
+ * can follow each back to the code or the row it is about.
59
+ */
60
+ export const EvidenceSource = z
61
+ .enum([
62
+ "composition",
63
+ "auditRecorder",
64
+ "bindings",
65
+ "routeSpec",
66
+ "connection",
67
+ "connectionKeys",
68
+ "capabilityHealth",
69
+ "fieldKeyRetirement",
70
+ "migrationLedger",
71
+ "privacyMarkers",
72
+ "encryptedMarkers",
73
+ "privacyAttestation",
74
+ "sameOriginGate",
75
+ ])
76
+ .describe(
77
+ "The declaration a fact was read from: `composition` (the capabilities this Worker composes), `auditRecorder` (the audit emit seam), `bindings` (this Worker's bindings), `routeSpec` (each route's `describeRoute`), `connection` (a control-plane connection's row), `connectionKeys` (its registered keys), `capabilityHealth` (a capability's declared health summary), `fieldKeyRetirement` (the field key's retirement guard), `migrationLedger` (a database's migration ledger), `privacyMarkers` (privacy-class markers on stored schemas), `encryptedMarkers` (`encrypted()` markers), `privacyAttestation` (the classes the last `pithy deploy` declared), or `sameOriginGate` (the same-origin policy a capability publishes).",
78
+ );
79
+ export type EvidenceSource = z.infer<typeof EvidenceSource>;
80
+
81
+ /** One row of the table: what a control is called and the framework references it answers. */
82
+ export interface EvidenceControl {
83
+ readonly title: string;
84
+ readonly soc2: readonly string[];
85
+ readonly iso27001: readonly string[];
86
+ }
87
+
88
+ /**
89
+ * The table itself, keyed by control so every id has exactly one row by construction. The pack reports them in
90
+ * {@link EVIDENCE_CONTROL_IDS} order. SOC 2 references are the AICPA Trust Services Criteria; ISO references are
91
+ * ISO/IEC 27001:2022 Annex A.
92
+ */
93
+ export const EVIDENCE_CONTROLS: Readonly<Record<EvidenceControlId, EvidenceControl>> = {
94
+ "audit-logging": { title: "Audit logging in force", soc2: ["CC7.2"], iso27001: ["A.8.15"] },
95
+ "route-verification": {
96
+ title: "Every route declares how a caller is verified",
97
+ soc2: ["CC6.1"],
98
+ iso27001: ["A.5.15"],
99
+ },
100
+ "management-access": {
101
+ title: "Management access attributed and scoped",
102
+ soc2: ["CC6.2", "CC6.3"],
103
+ iso27001: ["A.5.18"],
104
+ },
105
+ "key-rotation": { title: "Keys and secrets rotate", soc2: ["CC6.1"], iso27001: ["A.8.24"] },
106
+ "change-history": { title: "Change history per environment", soc2: ["CC8.1"], iso27001: ["A.8.32"] },
107
+ "data-protection": { title: "Data classified and encrypted at rest", soc2: ["C1.1"], iso27001: ["A.8.24"] },
108
+ csrf: { title: "CSRF wherever cookie mode is", soc2: ["CC6.6"], iso27001: ["A.8.26"] },
109
+ };
110
+
111
+ /** The capability whose composition is the audit-logging control's subject. Named in the gap it reports. */
112
+ export const AUDIT_PACKAGE = "@pithy-sh/audit";
113
+
114
+ /** The capability whose declared health carries secrets' rotation cadence and what is overdue. */
115
+ export const SECRETS_CAPABILITY = "secrets";
116
+
117
+ /** Its package, named in a reason. */
118
+ export const SECRETS_PACKAGE = "@pithy-sh/secrets";