@pithy-sh/core 0.1.2 → 0.1.4

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 (393) hide show
  1. package/dist/address/address.d.ts +57 -0
  2. package/dist/address/address.d.ts.map +1 -0
  3. package/dist/address/address.js +71 -0
  4. package/dist/audit/auditEvent.d.ts +89 -0
  5. package/dist/audit/auditEvent.d.ts.map +1 -0
  6. package/dist/audit/auditEvent.js +69 -0
  7. package/dist/audit/recorder.d.ts +18 -0
  8. package/dist/audit/recorder.d.ts.map +1 -0
  9. package/dist/audit/recorder.js +8 -0
  10. package/dist/capability/bindings.d.ts +62 -0
  11. package/dist/capability/bindings.d.ts.map +1 -0
  12. package/dist/capability/bindings.js +97 -0
  13. package/dist/capability/capability.d.ts +501 -0
  14. package/dist/capability/capability.d.ts.map +1 -0
  15. package/dist/capability/capability.js +16 -0
  16. package/dist/capability/client.d.ts +70 -0
  17. package/dist/capability/client.d.ts.map +1 -0
  18. package/dist/capability/client.js +86 -0
  19. package/dist/capability/compose.d.ts +25 -0
  20. package/dist/capability/compose.d.ts.map +1 -0
  21. package/dist/capability/compose.js +54 -0
  22. package/dist/capability/composition.d.ts +26 -0
  23. package/dist/capability/composition.d.ts.map +1 -0
  24. package/dist/capability/composition.js +84 -0
  25. package/dist/capability/config.d.ts +7 -0
  26. package/dist/capability/config.d.ts.map +1 -0
  27. package/dist/capability/config.js +14 -0
  28. package/dist/capability/devSecret.d.ts +32 -0
  29. package/dist/capability/devSecret.d.ts.map +1 -0
  30. package/dist/capability/devSecret.js +28 -0
  31. package/dist/capability/manifest.d.ts +298 -0
  32. package/dist/capability/manifest.d.ts.map +1 -0
  33. package/dist/capability/manifest.js +356 -0
  34. package/dist/capability/secretOrigin.d.ts +121 -0
  35. package/dist/capability/secretOrigin.d.ts.map +1 -0
  36. package/dist/capability/secretOrigin.js +134 -0
  37. package/dist/capability/settings.d.ts +119 -0
  38. package/dist/capability/settings.d.ts.map +1 -0
  39. package/dist/capability/settings.js +66 -0
  40. package/dist/capability/validateBindings.d.ts +4 -0
  41. package/dist/capability/validateBindings.d.ts.map +1 -0
  42. package/dist/capability/validateBindings.js +34 -0
  43. package/dist/capability/vanishingKey.d.ts +31 -0
  44. package/dist/capability/vanishingKey.d.ts.map +1 -0
  45. package/dist/capability/vanishingKey.js +79 -0
  46. package/dist/controlPlane/audit/actions.d.ts +70 -0
  47. package/dist/controlPlane/audit/actions.d.ts.map +1 -0
  48. package/dist/controlPlane/audit/actions.js +76 -0
  49. package/dist/controlPlane/capability.d.ts +47 -0
  50. package/dist/controlPlane/capability.d.ts.map +1 -0
  51. package/dist/controlPlane/capability.js +138 -0
  52. package/dist/controlPlane/config/config.d.ts +19 -0
  53. package/dist/controlPlane/config/config.d.ts.map +1 -0
  54. package/dist/controlPlane/config/config.js +85 -0
  55. package/dist/controlPlane/context.d.ts +27 -0
  56. package/dist/controlPlane/context.d.ts.map +1 -0
  57. package/dist/controlPlane/context.js +29 -0
  58. package/dist/controlPlane/data/connection.d.ts +79 -0
  59. package/dist/controlPlane/data/connection.d.ts.map +1 -0
  60. package/dist/controlPlane/data/connection.js +55 -0
  61. package/dist/controlPlane/data/keyLifecycle.d.ts +62 -0
  62. package/dist/controlPlane/data/keyLifecycle.d.ts.map +1 -0
  63. package/dist/controlPlane/data/keyLifecycle.js +126 -0
  64. package/dist/controlPlane/data/replay.d.ts +18 -0
  65. package/dist/controlPlane/data/replay.d.ts.map +1 -0
  66. package/dist/controlPlane/data/replay.js +20 -0
  67. package/dist/controlPlane/data/tables.d.ts +34 -0
  68. package/dist/controlPlane/data/tables.d.ts.map +1 -0
  69. package/dist/controlPlane/data/tables.js +37 -0
  70. package/dist/controlPlane/discovery/adminRoute.d.ts +313 -0
  71. package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -0
  72. package/dist/controlPlane/discovery/adminRoute.js +136 -0
  73. package/dist/controlPlane/discovery/configuration.d.ts +108 -0
  74. package/dist/controlPlane/discovery/configuration.d.ts.map +1 -0
  75. package/dist/controlPlane/discovery/configuration.js +187 -0
  76. package/dist/controlPlane/discovery/drift.d.ts +64 -0
  77. package/dist/controlPlane/discovery/drift.d.ts.map +1 -0
  78. package/dist/controlPlane/discovery/drift.js +58 -0
  79. package/dist/controlPlane/discovery/health.d.ts +99 -0
  80. package/dist/controlPlane/discovery/health.d.ts.map +1 -0
  81. package/dist/controlPlane/discovery/health.js +151 -0
  82. package/dist/controlPlane/discovery/healthSummary.d.ts +236 -0
  83. package/dist/controlPlane/discovery/healthSummary.d.ts.map +1 -0
  84. package/dist/controlPlane/discovery/healthSummary.js +278 -0
  85. package/dist/controlPlane/error/errors.d.ts +67 -0
  86. package/dist/controlPlane/error/errors.d.ts.map +1 -0
  87. package/dist/controlPlane/error/errors.js +79 -0
  88. package/dist/controlPlane/http/cors.d.ts +112 -0
  89. package/dist/controlPlane/http/cors.d.ts.map +1 -0
  90. package/dist/controlPlane/http/cors.js +192 -0
  91. package/dist/controlPlane/http/guard.d.ts +77 -0
  92. package/dist/controlPlane/http/guard.d.ts.map +1 -0
  93. package/dist/controlPlane/http/guard.js +142 -0
  94. package/dist/controlPlane/http/handlers.d.ts +179 -0
  95. package/dist/controlPlane/http/handlers.d.ts.map +1 -0
  96. package/dist/controlPlane/http/handlers.js +245 -0
  97. package/dist/controlPlane/http/responses.d.ts +99 -0
  98. package/dist/controlPlane/http/responses.d.ts.map +1 -0
  99. package/dist/controlPlane/http/responses.js +63 -0
  100. package/dist/controlPlane/http/routes.d.ts +40 -0
  101. package/dist/controlPlane/http/routes.d.ts.map +1 -0
  102. package/dist/controlPlane/http/routes.js +83 -0
  103. package/dist/controlPlane/http/schemas.d.ts +39 -0
  104. package/dist/controlPlane/http/schemas.d.ts.map +1 -0
  105. package/dist/controlPlane/http/schemas.js +31 -0
  106. package/dist/controlPlane/http/verify.d.ts +84 -0
  107. package/dist/controlPlane/http/verify.d.ts.map +1 -0
  108. package/dist/controlPlane/http/verify.js +65 -0
  109. package/dist/controlPlane/migrations/0001_init.d.ts +51 -0
  110. package/dist/controlPlane/migrations/0001_init.d.ts.map +1 -0
  111. package/dist/controlPlane/migrations/0001_init.js +65 -0
  112. package/dist/controlPlane/replay/d1Guard.d.ts +29 -0
  113. package/dist/controlPlane/replay/d1Guard.d.ts.map +1 -0
  114. package/dist/controlPlane/replay/d1Guard.js +39 -0
  115. package/dist/controlPlane/replay/guard.d.ts +52 -0
  116. package/dist/controlPlane/replay/guard.d.ts.map +1 -0
  117. package/dist/controlPlane/replay/guard.js +0 -0
  118. package/dist/controlPlane/replay/kvGuard.d.ts +99 -0
  119. package/dist/controlPlane/replay/kvGuard.d.ts.map +1 -0
  120. package/dist/controlPlane/replay/kvGuard.js +101 -0
  121. package/dist/controlPlane/scope/scope.d.ts +68 -0
  122. package/dist/controlPlane/scope/scope.d.ts.map +1 -0
  123. package/dist/controlPlane/scope/scope.js +63 -0
  124. package/dist/controlPlane/token/base64url.d.ts +10 -0
  125. package/dist/controlPlane/token/base64url.d.ts.map +1 -0
  126. package/dist/controlPlane/token/base64url.js +48 -0
  127. package/dist/controlPlane/token/claims.d.ts +83 -0
  128. package/dist/controlPlane/token/claims.d.ts.map +1 -0
  129. package/dist/controlPlane/token/claims.js +86 -0
  130. package/dist/controlPlane/token/digest.d.ts +44 -0
  131. package/dist/controlPlane/token/digest.d.ts.map +1 -0
  132. package/dist/controlPlane/token/digest.js +56 -0
  133. package/dist/controlPlane/token/jws.d.ts +52 -0
  134. package/dist/controlPlane/token/jws.d.ts.map +1 -0
  135. package/dist/controlPlane/token/jws.js +65 -0
  136. package/dist/controlPlane/token/mint.d.ts +56 -0
  137. package/dist/controlPlane/token/mint.d.ts.map +1 -0
  138. package/dist/controlPlane/token/mint.js +55 -0
  139. package/dist/controlPlane/wire.d.ts +122 -0
  140. package/dist/controlPlane/wire.d.ts.map +1 -0
  141. package/dist/controlPlane/wire.js +123 -0
  142. package/dist/createBackend.d.ts +58 -0
  143. package/dist/createBackend.d.ts.map +1 -0
  144. package/dist/createBackend.js +170 -0
  145. package/dist/createEntrypoint.d.ts +53 -0
  146. package/dist/createEntrypoint.d.ts.map +1 -0
  147. package/dist/createEntrypoint.js +52 -0
  148. package/dist/data/boundParameters.d.ts +83 -0
  149. package/dist/data/boundParameters.d.ts.map +1 -0
  150. package/dist/data/boundParameters.js +162 -0
  151. package/dist/data/codecs.d.ts +34 -0
  152. package/dist/data/codecs.d.ts.map +1 -0
  153. package/dist/data/codecs.js +135 -0
  154. package/dist/data/cursor.d.ts +81 -0
  155. package/dist/data/cursor.d.ts.map +1 -0
  156. package/dist/data/cursor.js +104 -0
  157. package/dist/data/databases.d.ts +55 -0
  158. package/dist/data/databases.d.ts.map +1 -0
  159. package/dist/data/databases.js +32 -0
  160. package/dist/data/db.d.ts +42 -0
  161. package/dist/data/db.d.ts.map +1 -0
  162. package/dist/data/db.js +24 -0
  163. package/dist/data/withD1Retry.d.ts +91 -0
  164. package/dist/data/withD1Retry.d.ts.map +1 -0
  165. package/dist/data/withD1Retry.js +152 -0
  166. package/dist/entitlement/entitlement.d.ts +118 -0
  167. package/dist/entitlement/entitlement.d.ts.map +1 -0
  168. package/dist/entitlement/entitlement.js +59 -0
  169. package/dist/entitlement/gateScan.d.ts +26 -0
  170. package/dist/entitlement/gateScan.d.ts.map +1 -0
  171. package/dist/entitlement/gateScan.js +96 -0
  172. package/dist/entitlement/require.d.ts +12 -0
  173. package/dist/entitlement/require.d.ts.map +1 -0
  174. package/dist/entitlement/require.js +123 -0
  175. package/dist/env/ambient.d.ts +52 -0
  176. package/dist/env/ambient.d.ts.map +1 -0
  177. package/dist/env/ambient.js +35 -0
  178. package/dist/env/ci.d.ts +36 -0
  179. package/dist/env/ci.d.ts.map +1 -0
  180. package/dist/env/ci.js +40 -0
  181. package/dist/env/stem.d.ts +23 -0
  182. package/dist/env/stem.d.ts.map +1 -0
  183. package/dist/env/stem.js +29 -0
  184. package/dist/error/cause.d.ts +143 -0
  185. package/dist/error/cause.d.ts.map +1 -0
  186. package/dist/error/cause.js +192 -0
  187. package/dist/error/client.d.ts +35 -0
  188. package/dist/error/client.d.ts.map +1 -0
  189. package/dist/error/client.js +38 -0
  190. package/dist/error/extend.d.ts +101 -0
  191. package/dist/error/extend.d.ts.map +1 -0
  192. package/dist/error/extend.js +76 -0
  193. package/dist/error/http.d.ts +1516 -0
  194. package/dist/error/http.d.ts.map +1 -0
  195. package/dist/error/http.js +72 -0
  196. package/dist/error/payload.d.ts +3034 -0
  197. package/dist/error/payload.d.ts.map +1 -0
  198. package/dist/error/payload.js +1110 -0
  199. package/dist/error/pithyError.d.ts +154 -0
  200. package/dist/error/pithyError.d.ts.map +1 -0
  201. package/dist/error/pithyError.js +197 -0
  202. package/dist/error/terminal.d.ts +28 -0
  203. package/dist/error/terminal.d.ts.map +1 -0
  204. package/dist/error/terminal.js +33 -0
  205. package/dist/http/authContext.d.ts +21 -0
  206. package/dist/http/authContext.d.ts.map +1 -0
  207. package/dist/http/authContext.js +23 -0
  208. package/dist/http/routeContract.d.ts +39 -0
  209. package/dist/http/routeContract.d.ts.map +1 -0
  210. package/dist/http/routeContract.js +82 -0
  211. package/dist/http/sameOrigin.d.ts +47 -0
  212. package/dist/http/sameOrigin.d.ts.map +1 -0
  213. package/dist/http/sameOrigin.js +24 -0
  214. package/dist/http/signedWebhook.d.ts +225 -0
  215. package/dist/http/signedWebhook.d.ts.map +1 -0
  216. package/dist/http/signedWebhook.js +247 -0
  217. package/dist/http/validation.d.ts +37 -0
  218. package/dist/http/validation.d.ts.map +1 -0
  219. package/dist/http/validation.js +14 -0
  220. package/dist/http/verification.d.ts +10 -0
  221. package/dist/http/verification.d.ts.map +1 -0
  222. package/dist/http/verification.js +17 -0
  223. package/dist/i18n/acceptLanguage.d.ts +22 -0
  224. package/dist/i18n/acceptLanguage.d.ts.map +1 -0
  225. package/dist/i18n/acceptLanguage.js +55 -0
  226. package/dist/i18n/catalog.d.ts +39 -0
  227. package/dist/i18n/catalog.d.ts.map +1 -0
  228. package/dist/i18n/catalog.js +57 -0
  229. package/dist/i18n/locale.d.ts +74 -0
  230. package/dist/i18n/locale.d.ts.map +1 -0
  231. package/dist/i18n/locale.js +109 -0
  232. package/dist/i18n/localeMarker.d.ts +29 -0
  233. package/dist/i18n/localeMarker.d.ts.map +1 -0
  234. package/dist/i18n/localeMarker.js +112 -0
  235. package/dist/i18n/match.d.ts +26 -0
  236. package/dist/i18n/match.d.ts.map +1 -0
  237. package/dist/i18n/match.js +88 -0
  238. package/dist/i18n/registry.d.ts +25 -0
  239. package/dist/i18n/registry.d.ts.map +1 -0
  240. package/dist/i18n/registry.js +51 -0
  241. package/dist/i18n/translator.d.ts +107 -0
  242. package/dist/i18n/translator.d.ts.map +1 -0
  243. package/dist/i18n/translator.js +80 -0
  244. package/dist/index.d.ts +32 -0
  245. package/dist/index.d.ts.map +1 -0
  246. package/dist/index.js +17 -0
  247. package/dist/kv/kv.d.ts +166 -0
  248. package/dist/kv/kv.d.ts.map +1 -0
  249. package/dist/kv/kv.js +261 -0
  250. package/dist/kv/namespaces.d.ts +58 -0
  251. package/dist/kv/namespaces.d.ts.map +1 -0
  252. package/dist/kv/namespaces.js +40 -0
  253. package/dist/logger/local.d.ts +41 -0
  254. package/dist/logger/local.d.ts.map +1 -0
  255. package/dist/logger/local.js +45 -0
  256. package/dist/logger/logger.d.ts +56 -0
  257. package/dist/logger/logger.d.ts.map +1 -0
  258. package/dist/logger/logger.js +83 -0
  259. package/dist/logger/record.d.ts +919 -0
  260. package/dist/logger/record.d.ts.map +1 -0
  261. package/dist/logger/record.js +79 -0
  262. package/dist/logger/worker.d.ts +74 -0
  263. package/dist/logger/worker.d.ts.map +1 -0
  264. package/dist/logger/worker.js +54 -0
  265. package/dist/migrations/batch.d.ts +10 -0
  266. package/dist/migrations/batch.d.ts.map +1 -0
  267. package/dist/migrations/batch.js +193 -0
  268. package/dist/migrations/bookkeeping.d.ts +24 -0
  269. package/dist/migrations/bookkeeping.d.ts.map +1 -0
  270. package/dist/migrations/bookkeeping.js +57 -0
  271. package/dist/migrations/owner.d.ts +41 -0
  272. package/dist/migrations/owner.d.ts.map +1 -0
  273. package/dist/migrations/owner.js +105 -0
  274. package/dist/migrations/registry.d.ts +36 -0
  275. package/dist/migrations/registry.d.ts.map +1 -0
  276. package/dist/migrations/registry.js +70 -0
  277. package/dist/migrations/runner.d.ts +67 -0
  278. package/dist/migrations/runner.d.ts.map +1 -0
  279. package/dist/migrations/runner.js +218 -0
  280. package/dist/naming/domains.d.ts +108 -0
  281. package/dist/naming/domains.d.ts.map +1 -0
  282. package/dist/naming/domains.js +156 -0
  283. package/dist/naming/environment.d.ts +118 -0
  284. package/dist/naming/environment.d.ts.map +1 -0
  285. package/dist/naming/environment.js +190 -0
  286. package/dist/naming/feature.d.ts +68 -0
  287. package/dist/naming/feature.d.ts.map +1 -0
  288. package/dist/naming/feature.js +106 -0
  289. package/dist/naming/limits.d.ts +229 -0
  290. package/dist/naming/limits.d.ts.map +1 -0
  291. package/dist/naming/limits.js +187 -0
  292. package/dist/naming/provisionScope.d.ts +91 -0
  293. package/dist/naming/provisionScope.d.ts.map +1 -0
  294. package/dist/naming/provisionScope.js +65 -0
  295. package/dist/naming/resource.d.ts +169 -0
  296. package/dist/naming/resource.d.ts.map +1 -0
  297. package/dist/naming/resource.js +220 -0
  298. package/dist/naming/resourceNames.d.ts +83 -0
  299. package/dist/naming/resourceNames.d.ts.map +1 -0
  300. package/dist/naming/resourceNames.js +102 -0
  301. package/dist/naming/segment.d.ts +29 -0
  302. package/dist/naming/segment.d.ts.map +1 -0
  303. package/dist/naming/segment.js +31 -0
  304. package/dist/projection/asRead.d.ts +72 -0
  305. package/dist/projection/asRead.d.ts.map +1 -0
  306. package/dist/projection/asRead.js +169 -0
  307. package/dist/projection/published.d.ts +95 -0
  308. package/dist/projection/published.d.ts.map +1 -0
  309. package/dist/projection/published.js +114 -0
  310. package/dist/schema/describedness.d.ts +55 -0
  311. package/dist/schema/describedness.d.ts.map +1 -0
  312. package/dist/schema/describedness.js +180 -0
  313. package/dist/seed/compose.d.ts +41 -0
  314. package/dist/seed/compose.d.ts.map +1 -0
  315. package/dist/seed/compose.js +51 -0
  316. package/dist/seed/devLogin.d.ts +50 -0
  317. package/dist/seed/devLogin.d.ts.map +1 -0
  318. package/dist/seed/devLogin.js +52 -0
  319. package/dist/seed/exampleIdentities.d.ts +58 -0
  320. package/dist/seed/exampleIdentities.d.ts.map +1 -0
  321. package/dist/seed/exampleIdentities.js +26 -0
  322. package/dist/seed/metadata.d.ts +20 -0
  323. package/dist/seed/metadata.d.ts.map +1 -0
  324. package/dist/seed/metadata.js +27 -0
  325. package/dist/seed/seed.d.ts +264 -0
  326. package/dist/seed/seed.d.ts.map +1 -0
  327. package/dist/seed/seed.js +37 -0
  328. package/dist/seed/seededRows.d.ts +26 -0
  329. package/dist/seed/seededRows.d.ts.map +1 -0
  330. package/dist/seed/seededRows.js +20 -0
  331. package/dist/seed/writeD1.d.ts +43 -0
  332. package/dist/seed/writeD1.d.ts.map +1 -0
  333. package/dist/seed/writeD1.js +73 -0
  334. package/dist/seed/writeKv.d.ts +29 -0
  335. package/dist/seed/writeKv.d.ts.map +1 -0
  336. package/dist/seed/writeKv.js +63 -0
  337. package/dist/semver/semver.d.ts +62 -0
  338. package/dist/semver/semver.d.ts.map +1 -0
  339. package/dist/semver/semver.js +137 -0
  340. package/dist/text/comments.d.ts +62 -0
  341. package/dist/text/comments.d.ts.map +1 -0
  342. package/dist/text/comments.js +161 -0
  343. package/dist/version.generated.d.ts +5 -0
  344. package/dist/version.generated.d.ts.map +1 -0
  345. package/dist/version.generated.js +7 -0
  346. package/dist/worker/health.d.ts +39 -0
  347. package/dist/worker/health.d.ts.map +1 -0
  348. package/dist/worker/health.js +41 -0
  349. package/dist/worker/identity.d.ts +191 -0
  350. package/dist/worker/identity.d.ts.map +1 -0
  351. package/dist/worker/identity.js +162 -0
  352. package/dist/workflow/bindings.d.ts +41 -0
  353. package/dist/workflow/bindings.d.ts.map +1 -0
  354. package/dist/workflow/bindings.js +53 -0
  355. package/dist/workflow/dispatch.d.ts +98 -0
  356. package/dist/workflow/dispatch.d.ts.map +1 -0
  357. package/dist/workflow/dispatch.js +148 -0
  358. package/dist/workflow/dispatchRoute.d.ts +80 -0
  359. package/dist/workflow/dispatchRoute.d.ts.map +1 -0
  360. package/dist/workflow/dispatchRoute.js +134 -0
  361. package/dist/workflow/faults.d.ts +97 -0
  362. package/dist/workflow/faults.d.ts.map +1 -0
  363. package/dist/workflow/faults.js +111 -0
  364. package/dist/workflow/host.d.ts +176 -0
  365. package/dist/workflow/host.d.ts.map +1 -0
  366. package/dist/workflow/host.js +116 -0
  367. package/dist/workflow/hostEntry.d.ts +50 -0
  368. package/dist/workflow/hostEntry.d.ts.map +1 -0
  369. package/dist/workflow/hostEntry.js +24 -0
  370. package/dist/workflow/hostEnv.d.ts +143 -0
  371. package/dist/workflow/hostEnv.d.ts.map +1 -0
  372. package/dist/workflow/hostEnv.js +170 -0
  373. package/dist/workflow/loopback.d.ts +52 -0
  374. package/dist/workflow/loopback.d.ts.map +1 -0
  375. package/dist/workflow/loopback.js +76 -0
  376. package/dist/workflow/naming.d.ts +79 -0
  377. package/dist/workflow/naming.d.ts.map +1 -0
  378. package/dist/workflow/naming.js +138 -0
  379. package/dist/workflow/register.d.ts +19 -0
  380. package/dist/workflow/register.d.ts.map +1 -0
  381. package/dist/workflow/register.js +40 -0
  382. package/dist/workflow/schemas.d.ts +44 -0
  383. package/dist/workflow/schemas.d.ts.map +1 -0
  384. package/dist/workflow/schemas.js +43 -0
  385. package/dist/workflow/spec.d.ts +81 -0
  386. package/dist/workflow/spec.d.ts.map +1 -0
  387. package/dist/workflow/spec.js +0 -0
  388. package/dist/workflow/stepMessage.d.ts +122 -0
  389. package/dist/workflow/stepMessage.d.ts.map +1 -0
  390. package/dist/workflow/stepMessage.js +152 -0
  391. package/package.json +20 -9
  392. package/src/capability/manifest.ts +21 -0
  393. package/src/version.generated.ts +1 -1
@@ -0,0 +1,84 @@
1
+ import type { ControlPlaneConfig } from "../config/config";
2
+ import { ControlPlaneContext } from "../context";
3
+ import type { ControlPlaneConnection } from "../data/connection";
4
+ import type { ReplayGuard } from "../replay/guard";
5
+ import { type ControlPlaneRequirement } from "../scope/scope";
6
+ /**
7
+ * The verification pipeline — the one place a control-plane call is decided.
8
+ *
9
+ * Written against a dependency object rather than a Hono context so the whole decision is testable
10
+ * without a Worker: every step below is exercised in `verify.test.ts` with a fake connection loader and
11
+ * a fake replay guard. The middleware in `guard.ts` is the thin part that reads those dependencies off
12
+ * the request.
13
+ *
14
+ * ## The header is not `Authorization`, and that is deliberate
15
+ *
16
+ * `@pithy-sh/auth` installs a global middleware that, for **any** request carrying an `authorization`
17
+ * header, builds a Better Auth instance and resolves a session against D1. A control-plane token on
18
+ * that header would therefore hit the auth stack on every management call — wasted work at best, and
19
+ * an unnecessary adjacency between two strategies that must stay separate. On {@link CONTROL_PLANE_HEADER}
20
+ * the seam is invisible to it: no Better Auth instance is ever constructed for a control-plane call,
21
+ * which is the structural fact behind "a control-plane call creates no user and no session".
22
+ *
23
+ * ## Order is a security property, not a style choice
24
+ *
25
+ * Nothing with a side effect happens before the signature verifies. In particular the `jti` is claimed
26
+ * **last**: claim it earlier and an unauthenticated caller could burn the ids of tokens it merely
27
+ * observed, denying a legitimate management client by replaying its own forgeries. Everything before
28
+ * step 7 is a lookup or a comparison, and grants nothing on its own.
29
+ *
30
+ * ## One error for every failure, on purpose
31
+ *
32
+ * Every step but the scope check raises the same {@link ControlPlaneInvalidCredentialError}. A caller
33
+ * cannot tell an unknown key from a bad signature from a replayed token, and so cannot use the response
34
+ * to learn how far a forgery got. The step goes in `detail`, which the HTTP codec strips and the log
35
+ * keeps. The scope failure is distinguished because by then the caller is proven legitimate — telling
36
+ * them which grant they lack is actionable and leaks nothing they do not already hold.
37
+ */
38
+ /**
39
+ * The header names live in `../wire`, which imports nothing so that a browser can hold them — the far
40
+ * end of this seam calls the adopter's Worker directly, and importing them from here drags WebCrypto
41
+ * into a DOM-typed build.
42
+ *
43
+ * Re-exported because this path is already published: every caller outside the repo names
44
+ * `@pithy-sh/core/src/controlPlane/http/verify`, and moving a wire constant must not break the
45
+ * programs that speak the wire. Write new imports against `../wire`; everything in this repo already
46
+ * does.
47
+ */
48
+ export { CONTROL_PLANE_HEADER, CONTROL_PLANE_VERSION_HEADER } from "../wire";
49
+ /** What the pipeline needs from the outside world. Every one is injectable, so every step is testable. */
50
+ export interface ControlPlaneVerifyDeps {
51
+ /** Load the connection a token addresses, by id. Null when no such connection exists here. */
52
+ loadConnection: (connectionId: string) => Promise<ControlPlaneConnection | null>;
53
+ /**
54
+ * How many connections this environment has at all. Called **only** when the lookup above missed, to
55
+ * separate "you have never connected anything" — worth saying plainly — from "that connection id is
56
+ * not one of ours", which must stay indistinguishable from every other credential failure or it
57
+ * becomes an oracle for enumerating connection ids.
58
+ */
59
+ countConnections: () => Promise<number>;
60
+ /** The single-use gate over `jti`. */
61
+ replay: ReplayGuard;
62
+ /** This Worker's environment name, checked against the connection's. */
63
+ environment: string;
64
+ /** The resolved seam config — the token and window bounds this Worker enforces. */
65
+ config: ControlPlaneConfig;
66
+ /** The clock, injected so a test can stand at any instant. */
67
+ now: () => Date;
68
+ }
69
+ /** One inbound call, reduced to what verification actually needs. */
70
+ export interface ControlPlaneCall {
71
+ /** The compact JWS from {@link CONTROL_PLANE_HEADER}, or undefined when the caller sent none. */
72
+ token: string | undefined;
73
+ /** The raw request body bytes. Empty for a request that carries no body. */
74
+ body: Uint8Array;
75
+ /** What the route demands: one named scope, or merely a verified caller. */
76
+ requirement: ControlPlaneRequirement;
77
+ }
78
+ /**
79
+ * Verify one control-plane call, or throw. Returns the context the handler runs under.
80
+ *
81
+ * The steps are numbered to match `docs/CONTROL-PLANE.md` §9.
82
+ */
83
+ export declare function verifyControlPlaneCall(call: ControlPlaneCall, deps: ControlPlaneVerifyDeps): Promise<ControlPlaneContext>;
84
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/verify.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAOjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,KAAK,uBAAuB,EAAe,MAAM,gBAAgB,CAAC;AAI3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH;;;;;;;;;GASG;AACH,OAAO,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AAE7E,0GAA0G;AAC1G,MAAM,WAAW,sBAAsB;IACrC,8FAA8F;IAC9F,cAAc,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;IACjF;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,sCAAsC;IACtC,MAAM,EAAE,WAAW,CAAC;IACpB,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,MAAM,EAAE,kBAAkB,CAAC;IAC3B,8DAA8D;IAC9D,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,iGAAiG;IACjG,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,EAAE,UAAU,CAAC;IACjB,4EAA4E;IAC5E,WAAW,EAAE,uBAAuB,CAAC;CACtC;AAOD;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CA2E9B"}
@@ -0,0 +1,65 @@
1
+ import { scopeCovers } from "../scope/scope.js";
2
+ import { ControlPlaneInsufficientScopeError, ControlPlaneInvalidCredentialError, ControlPlaneNotConnectedError } from "../error/errors.js";
3
+ import { CONTROL_PLANE_HEADER, CONTROL_PLANE_VERSION_HEADER } from "../wire.js";
4
+ import { ControlPlaneContext } from "../context.js";
5
+ import { findVerifyingKey } from "../data/keyLifecycle.js";
6
+ import { sha256Base64Url, timingSafeEqual } from "../token/digest.js";
7
+ import { parseCompactJws, verifyEd25519 } from "../token/jws.js";
8
+ //#region src/controlPlane/http/verify.ts
9
+ /** Raise the one credential error, naming the failing step for the log and nobody else. */
10
+ function deny(step) {
11
+ throw new ControlPlaneInvalidCredentialError({ detail: step });
12
+ }
13
+ /**
14
+ * Verify one control-plane call, or throw. Returns the context the handler runs under.
15
+ *
16
+ * The steps are numbered to match `docs/CONTROL-PLANE.md` §9.
17
+ */
18
+ async function verifyControlPlaneCall(call, deps) {
19
+ if (!call.token) deny("no control-plane credential presented");
20
+ const { header, claims, signingInput, signature } = parseCompactJws(call.token);
21
+ const connection = await deps.loadConnection(claims.aud);
22
+ if (!connection) {
23
+ if (await deps.countConnections() === 0) throw new ControlPlaneNotConnectedError({ detail: `no connection registered in environment ${deps.environment}` });
24
+ deny(`aud ${claims.aud} names no connection here`);
25
+ }
26
+ if (connection.environment !== deps.environment) deny(`connection ${connection.id} is bound to ${connection.environment}, not ${deps.environment}`);
27
+ if (claims.iss !== connection.issuer) deny(`iss ${claims.iss} is not the issuer connection ${connection.id} trusts`);
28
+ const key = findVerifyingKey(connection.keys, header.kid, deps.now());
29
+ if (!key) deny(`kid ${header.kid} is not a live key on connection ${connection.id}`);
30
+ if (!await verifyEd25519(signingInput, signature, key.publicKey)) deny(`signature does not verify under kid ${header.kid}`);
31
+ const lifetime = claims.exp - claims.iat;
32
+ if (lifetime <= 0 || lifetime > deps.config.maxTokenLifetimeSeconds) deny(`token lifetime ${lifetime}s exceeds the ${deps.config.maxTokenLifetimeSeconds}s maximum`);
33
+ const nowSeconds = Math.floor(deps.now().getTime() / 1e3);
34
+ const skew = deps.config.clockSkewSeconds;
35
+ if (claims.exp <= nowSeconds - skew) deny(`token expired at ${claims.exp}, now ${nowSeconds}`);
36
+ if (claims.iat > nowSeconds + skew) deny(`token issued at ${claims.iat} is in the future, now ${nowSeconds}`);
37
+ await verifyBodyDigest(call.body, claims.bodySha256);
38
+ if (!scopeCovers(call.requirement, claims.scope, connection.scopes)) throw new ControlPlaneInsufficientScopeError({ detail: `required ${String(call.requirement)}; token carried ${claims.scope}; connection grants [${connection.scopes.join(", ")}]` });
39
+ if (!await deps.replay.claim(claims.jti, connection.id)) deny(`jti ${claims.jti} was already spent`);
40
+ return ControlPlaneContext.parse({
41
+ connectionId: connection.id,
42
+ environment: connection.environment,
43
+ issuer: connection.issuer,
44
+ subject: claims.sub,
45
+ scope: claims.scope,
46
+ grantedScopes: connection.scopes,
47
+ keyId: key.keyId,
48
+ tokenId: claims.jti
49
+ });
50
+ }
51
+ /**
52
+ * The body digest check. `bodySha256` is null exactly when the body is empty — a bijection, so there is
53
+ * no third state a caller could steer into. A token minted for an empty body cannot be presented with
54
+ * one, and a token minted for a body cannot be presented without it.
55
+ */
56
+ async function verifyBodyDigest(body, claimed) {
57
+ if (body.byteLength === 0) {
58
+ if (claimed !== null) deny("token claims a body digest but the request carries no body");
59
+ return;
60
+ }
61
+ if (claimed === null) deny("request carries a body but the token claims no digest");
62
+ if (!timingSafeEqual(await sha256Base64Url(body), claimed)) deny("body digest does not match the token");
63
+ }
64
+ //#endregion
65
+ export { CONTROL_PLANE_HEADER, CONTROL_PLANE_VERSION_HEADER, verifyControlPlaneCall };
@@ -0,0 +1,51 @@
1
+ import type { Migration } from "kysely/migration";
2
+ /**
3
+ * The control-plane seam's tables: which management client may call this Worker, and which of its tokens
4
+ * have already been spent.
5
+ *
6
+ * This is the inbound seam — the hosted dashboard calling *into* the adopter's Worker. It is not
7
+ * Cloudflare's control plane, which is the outbound provisioning REST API and shares nothing with these
8
+ * tables.
9
+ *
10
+ * camelCase identifiers; `CamelCasePlugin` snake-cases them to `pithy_controlplane_*` in the DDL. `down`
11
+ * drops each index before its table, and is tested.
12
+ *
13
+ * ## `pithy_controlplane_connections`
14
+ *
15
+ * Two shapes are deliberate.
16
+ *
17
+ * `id` is text, not an autoincrementing integer. It is the token's `aud`, so it leaves the Worker on
18
+ * every call; a sequential id would tell a caller how many connections exist and let them guess the
19
+ * next one.
20
+ *
21
+ * `scopes` and `keys` are JSON columns rather than child tables. Keys especially: a rotation is then
22
+ * one `UPDATE` that cannot half-apply, and verification is one primary-key read with no join on a
23
+ * Worker hot path. SQLite cannot `CHECK` inside a JSON string, so the Zod schemas on
24
+ * `ControlPlaneConnection` are the only gate on their contents — `NOT NULL` here is the most the
25
+ * database itself can say.
26
+ *
27
+ * `basePath` records where the seam is mounted on the Worker this connection addresses. It is stored
28
+ * beside `workerUrl` because the two together fully determine the manifest address — and it is the one
29
+ * part a client cannot discover, since it *is* the manifest's own address. Everything else already
30
+ * solves this: `AdminRoute.path` carries the fully mounted path, so no client hardcodes a capability's
31
+ * mount point. Without it a client must assume `/control-plane`, and an adopter who moved the mount
32
+ * registers cleanly, passes the `ping` — called at that same assumed path — and then 404s on every call,
33
+ * with the operator diagnosing the wrong problem.
34
+ *
35
+ * ## `pithy_controlplane_replays`
36
+ *
37
+ * **`jti` is the primary key, and that is the entire replay mechanism.** The claim is
38
+ * `INSERT … ON CONFLICT DO NOTHING RETURNING`, so SQLite's uniqueness decides which of N concurrent
39
+ * presentations wins — no read-then-write, no window between checking and recording. The KV guard this
40
+ * replaced could not offer that: KV has no compare-and-set, so two colocations could both see a miss.
41
+ *
42
+ * The key is `jti` alone, deliberately, and **not** `(jti, connectionId)`. A composite would let a token
43
+ * captured from one connection be spent again against another, which is the property the guard exists to
44
+ * deny; `connectionId` is carried as a plain column, for the incident rather than the decision.
45
+ *
46
+ * `expiresAt` exists so the table can be pruned. A replay table that only grows is a slow leak in every
47
+ * adopter's database — the one thing KV gave for nothing, since its entries expired themselves. Its index
48
+ * is what makes the prune a range scan rather than a full table scan on an administrator-paced write path.
49
+ */
50
+ export declare const controlplane_0001_init: Migration;
51
+ //# sourceMappingURL=0001_init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0001_init.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/migrations/0001_init.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAkDpC,CAAC"}
@@ -0,0 +1,65 @@
1
+ //#region src/controlPlane/migrations/0001_init.ts
2
+ /**
3
+ * The control-plane seam's tables: which management client may call this Worker, and which of its tokens
4
+ * have already been spent.
5
+ *
6
+ * This is the inbound seam — the hosted dashboard calling *into* the adopter's Worker. It is not
7
+ * Cloudflare's control plane, which is the outbound provisioning REST API and shares nothing with these
8
+ * tables.
9
+ *
10
+ * camelCase identifiers; `CamelCasePlugin` snake-cases them to `pithy_controlplane_*` in the DDL. `down`
11
+ * drops each index before its table, and is tested.
12
+ *
13
+ * ## `pithy_controlplane_connections`
14
+ *
15
+ * Two shapes are deliberate.
16
+ *
17
+ * `id` is text, not an autoincrementing integer. It is the token's `aud`, so it leaves the Worker on
18
+ * every call; a sequential id would tell a caller how many connections exist and let them guess the
19
+ * next one.
20
+ *
21
+ * `scopes` and `keys` are JSON columns rather than child tables. Keys especially: a rotation is then
22
+ * one `UPDATE` that cannot half-apply, and verification is one primary-key read with no join on a
23
+ * Worker hot path. SQLite cannot `CHECK` inside a JSON string, so the Zod schemas on
24
+ * `ControlPlaneConnection` are the only gate on their contents — `NOT NULL` here is the most the
25
+ * database itself can say.
26
+ *
27
+ * `basePath` records where the seam is mounted on the Worker this connection addresses. It is stored
28
+ * beside `workerUrl` because the two together fully determine the manifest address — and it is the one
29
+ * part a client cannot discover, since it *is* the manifest's own address. Everything else already
30
+ * solves this: `AdminRoute.path` carries the fully mounted path, so no client hardcodes a capability's
31
+ * mount point. Without it a client must assume `/control-plane`, and an adopter who moved the mount
32
+ * registers cleanly, passes the `ping` — called at that same assumed path — and then 404s on every call,
33
+ * with the operator diagnosing the wrong problem.
34
+ *
35
+ * ## `pithy_controlplane_replays`
36
+ *
37
+ * **`jti` is the primary key, and that is the entire replay mechanism.** The claim is
38
+ * `INSERT … ON CONFLICT DO NOTHING RETURNING`, so SQLite's uniqueness decides which of N concurrent
39
+ * presentations wins — no read-then-write, no window between checking and recording. The KV guard this
40
+ * replaced could not offer that: KV has no compare-and-set, so two colocations could both see a miss.
41
+ *
42
+ * The key is `jti` alone, deliberately, and **not** `(jti, connectionId)`. A composite would let a token
43
+ * captured from one connection be spent again against another, which is the property the guard exists to
44
+ * deny; `connectionId` is carried as a plain column, for the incident rather than the decision.
45
+ *
46
+ * `expiresAt` exists so the table can be pruned. A replay table that only grows is a slow leak in every
47
+ * adopter's database — the one thing KV gave for nothing, since its entries expired themselves. Its index
48
+ * is what makes the prune a range scan rather than a full table scan on an administrator-paced write path.
49
+ */
50
+ const controlplane_0001_init = {
51
+ up: async (db) => {
52
+ await db.schema.createTable("pithyControlplaneConnections").addColumn("id", "text", (c) => c.primaryKey()).addColumn("environment", "text", (c) => c.notNull()).addColumn("issuer", "text", (c) => c.notNull()).addColumn("workerUrl", "text", (c) => c.notNull()).addColumn("basePath", "text", (c) => c.notNull().defaultTo("/control-plane")).addColumn("scopes", "text", (c) => c.notNull()).addColumn("keys", "text", (c) => c.notNull()).addColumn("createdAt", "integer", (c) => c.notNull()).addColumn("updatedAt", "integer", (c) => c.notNull()).execute();
53
+ await db.schema.createIndex("pithyControlplaneConnectionsEnvironmentIdx").on("pithyControlplaneConnections").columns(["environment"]).execute();
54
+ await db.schema.createTable("pithyControlplaneReplays").addColumn("jti", "text", (c) => c.primaryKey()).addColumn("connectionId", "text", (c) => c.notNull()).addColumn("expiresAt", "integer", (c) => c.notNull()).execute();
55
+ await db.schema.createIndex("pithyControlplaneReplaysExpiresAtIdx").on("pithyControlplaneReplays").columns(["expiresAt"]).execute();
56
+ },
57
+ down: async (db) => {
58
+ await db.schema.dropIndex("pithyControlplaneReplaysExpiresAtIdx").execute();
59
+ await db.schema.dropTable("pithyControlplaneReplays").execute();
60
+ await db.schema.dropIndex("pithyControlplaneConnectionsEnvironmentIdx").execute();
61
+ await db.schema.dropTable("pithyControlplaneConnections").execute();
62
+ }
63
+ };
64
+ //#endregion
65
+ export { controlplane_0001_init };
@@ -0,0 +1,29 @@
1
+ import { type ControlPlaneDatabase } from "../data/tables";
2
+ import type { ReplayGuard } from "./guard";
3
+ /**
4
+ * The D1-backed {@link ReplayGuard} — the seam's default.
5
+ *
6
+ * `INSERT … ON CONFLICT DO NOTHING RETURNING` is the whole thing. The insert either wins the `jti`
7
+ * primary key and returns a row, or conflicts and returns nothing; there is no read-then-write, so there
8
+ * is no window between deciding and recording. Of N concurrent presentations of one token, SQLite admits
9
+ * exactly one, and it does so identically whichever colocation each request landed in — which is
10
+ * precisely what the KV guard could not promise.
11
+ *
12
+ * It is the same move `@pithy-sh/auth` makes on the other side, consuming a refresh token with a
13
+ * conditional delete so that of N presentations exactly one wins. Single-use-by-constraint is the house
14
+ * pattern; KV was the outlier.
15
+ */
16
+ /** What the guard needs from its caller. Injected, so nothing here reaches for a clock or a global. */
17
+ export interface D1ReplayGuardOptions {
18
+ /** The clock. Injected so a test can stand at any instant. */
19
+ now: () => Date;
20
+ /**
21
+ * How long a spent `jti` is remembered — `jtiTtlSeconds` from the seam config, which is itself
22
+ * cross-checked at assembly to outlive the widest window a token can be accepted in. A row's
23
+ * `expiresAt` is stamped from it, and the prune below is the only thing that reads it back.
24
+ */
25
+ ttlSeconds: number;
26
+ }
27
+ /** A {@link ReplayGuard} over the seam's own D1 table — strongly consistent, and the default. */
28
+ export declare function d1ReplayGuard(db: ControlPlaneDatabase, options: D1ReplayGuardOptions): ReplayGuard;
29
+ //# sourceMappingURL=d1Guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1Guard.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/replay/d1Guard.ts"],"names":[],"mappings":"AAIA,OAAO,EAA+B,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACxF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;;;;;;;;;;GAYG;AAEH,uGAAuG;AACvG,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AA4BD,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,EAAE,EAAE,oBAAoB,EAAE,OAAO,EAAE,oBAAoB,GAAG,WAAW,CA0BlG"}
@@ -0,0 +1,39 @@
1
+ import { ControlPlaneReplay } from "../data/replay.js";
2
+ import { CONTROL_PLANE_REPLAYS_TABLE } from "../data/tables.js";
3
+ //#region src/controlPlane/replay/d1Guard.ts
4
+ /**
5
+ * Delete rows whose token could no longer be accepted under any clock this Worker honors.
6
+ *
7
+ * **Pruning is the one thing KV gave for nothing** — its entries expired themselves, and a table does
8
+ * not. A replay table that only grows is a slow leak in every adopter's database, so this is deliberate
9
+ * rather than left to a future sweep.
10
+ *
11
+ * It runs **only after a successful claim**, which is the load-bearing detail. Pruning on every call
12
+ * would let an attacker replaying one token in a loop drive an unbounded `DELETE` per attempt; a refused
13
+ * claim now costs exactly one conflicting insert and nothing else. And a successful claim is already a
14
+ * write, so the prune rides along on a path that was never read-only, on an administrator-paced route.
15
+ *
16
+ * A failure here is swallowed. Pruning is housekeeping: a claim that has already been decided must not
17
+ * be reported as failed because a cleanup that had nothing to do with the decision could not run.
18
+ */
19
+ async function prune(db, now) {
20
+ try {
21
+ await db.deleteFrom(CONTROL_PLANE_REPLAYS_TABLE).where("expiresAt", "<", ControlPlaneReplay.shape.expiresAt.encode(now)).execute();
22
+ } catch {}
23
+ }
24
+ /** A {@link ReplayGuard} over the seam's own D1 table — strongly consistent, and the default. */
25
+ function d1ReplayGuard(db, options) {
26
+ return { async claim(jti, connectionId) {
27
+ const now = options.now();
28
+ const record = ControlPlaneReplay.encode({
29
+ jti,
30
+ connectionId,
31
+ expiresAt: new Date(now.getTime() + options.ttlSeconds * 1e3)
32
+ });
33
+ if (await db.insertInto("pithyControlplaneReplays").values(record).onConflict((oc) => oc.column("jti").doNothing()).returning("jti").executeTakeFirst() === void 0) return false;
34
+ await prune(db, now);
35
+ return true;
36
+ } };
37
+ }
38
+ //#endregion
39
+ export { d1ReplayGuard };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The replay guard: a control-plane token is spendable exactly once.
3
+ *
4
+ * Every other check asks what the call *is* — signature, issuer, audience, environment, scope, body
5
+ * digest, expiry. None of them notices the same valid call arriving twice. {@link ReplayGuard.claim} is
6
+ * what notices: it records the token's `jti` and reports whether this caller was the first to do so. A
7
+ * second arrival is refused, and the caller denies.
8
+ *
9
+ * ## Two implementations, and D1 is the default
10
+ *
11
+ * `d1ReplayGuard` (`./d1Guard`) is the one the seam composes. It claims with
12
+ * `INSERT … ON CONFLICT DO NOTHING RETURNING`, so the primary key decides the winner and the decision is
13
+ * strongly consistent — of N concurrent presentations of one token, exactly one row is inserted and
14
+ * exactly one caller is told it won, wherever the requests landed.
15
+ *
16
+ * `kvReplayGuard` (`./kvGuard`) remains available and selectable, and is **best-effort by construction**.
17
+ * Workers KV has no compare-and-set and is eventually consistent across colocations, so a read-then-write
18
+ * claim admits a genuine race: two copies of one token presented in two PoPs inside the propagation
19
+ * window can both read a miss and both claim. That is why it is no longer the default. It is kept because
20
+ * the trade is legitimate for an adopter who would rather not pay a D1 write on this path and whose admin
21
+ * operations are all idempotent — but it is now an explicit choice with the cost written down, rather
22
+ * than the silent default it used to be.
23
+ *
24
+ * ## Why the default moved
25
+ *
26
+ * The exploitable surface is narrow: same connection, same scope, same body digest, inside a 60-second
27
+ * expiry. What can be replayed is *one exact call*. Most admin operations shrug that off. Not all do — a
28
+ * nudge sends real people a second email, a key registration appends, and anything that enqueues work
29
+ * enqueues it again.
30
+ *
31
+ * And the cost of closing it is close to nothing **here specifically**. A control-plane hot path is an
32
+ * administrator clicking something: low volume, high privilege. Trading a few milliseconds for
33
+ * correctness is the obvious side of that bargain, which is a different calculation from a per-request
34
+ * user path. The seam already owns a D1 namespace, so this is a second migration in an existing one
35
+ * rather than new infrastructure — and single-use-by-unique-constraint is the house pattern besides:
36
+ * `@pithy-sh/auth` consumes a refresh token exactly this way, and `@pithy-sh/payments` keys idempotency
37
+ * off a `UNIQUE`. KV was the outlier.
38
+ */
39
+ /**
40
+ * The single-use gate over a token's `jti`. One method, so the storage decision behind it stays
41
+ * replaceable — the interface is the substitution point, and it is why moving the default from KV to D1
42
+ * touched no call site.
43
+ */
44
+ export interface ReplayGuard {
45
+ /**
46
+ * Claim `jti` for `connectionId`. Returns true when this call was the first to spend it, and
47
+ * **false when it was already spent — the caller must then deny.** The false case is not an error to
48
+ * log and continue past; it is the replay.
49
+ */
50
+ claim(jti: string, connectionId: string): Promise<boolean>;
51
+ }
52
+ //# sourceMappingURL=guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/replay/guard.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5D"}
File without changes
@@ -0,0 +1,99 @@
1
+ import { z } from "zod";
2
+ import type { TypedKv } from "../../kv/kv";
3
+ import type { ReplayGuard } from "./guard";
4
+ /**
5
+ * The KV-backed {@link ReplayGuard} — still selectable, no longer the default.
6
+ *
7
+ * See `./guard.ts` for what the guard is for and why D1 now holds the default. The part that belongs
8
+ * here is the caveat that is a property of *this* store and not of the seam:
9
+ *
10
+ * **Workers KV has no compare-and-set and is eventually consistent across colocations.** A `put` in one
11
+ * PoP is not immediately visible in another, so two copies of one token presented in two locations
12
+ * inside the propagation window can both read a miss and both claim. Read-then-write is the strongest
13
+ * claim KV offers; it is best-effort, not an atomic single-use gate.
14
+ *
15
+ * Three things bound the exposure for an adopter who chooses it anyway.
16
+ *
17
+ * 1. **The window is KV's propagation delay** — seconds, and independent of the 60-second token. The
18
+ * write is issued the moment the first call lands; once it has converged the jti is spent for a full
19
+ * {@link CONTROL_PLANE_JTI_TTL_SECONDS}, which deliberately outlives the token that carried it. So
20
+ * there is no window at the end of the token's life, only at the very start of it.
21
+ * 2. **The token is not a general capability.** It is bound to one connection, one scope, one request
22
+ * body digest, and that expiry. What can survive the window is a replay of *the exact same call* —
23
+ * not a forged one, not a broader one, not a later one.
24
+ * 3. **`d1ReplayGuard` is one config key away**, and closes the race outright. Choosing this one is
25
+ * choosing to skip a D1 write on an administrator-paced path, with the race as the stated price.
26
+ */
27
+ /**
28
+ * The KV binding the seam's replay set lives in.
29
+ *
30
+ * **Not Cloudflare's control plane.** That is the outbound provisioning REST API this project reaches
31
+ * through `@pithy-sh/cloudflare`. This is the inbound, adopter-authenticated admin seam, and it owns
32
+ * its own namespace rather than sharing `SESSIONS` — a junk-drawer KV would put user sessions and
33
+ * management-token receipts behind one binding, and they have nothing to do with each other.
34
+ */
35
+ export declare const CONTROL_PLANE_KV_BINDING = "CONTROL_PLANE";
36
+ /**
37
+ * The fixed first key segment for everything the seam stores in KV.
38
+ *
39
+ * One word, no hyphen: `assertValidConfig` rejects a prefix containing the key separator, and the
40
+ * namespace tokens across this capability (migration namespace, table prefix, error domain) are all
41
+ * the single word `controlplane` for the same family of reasons. The `jti` segment below is what makes
42
+ * the physical key read `controlplane:jti:<id>` — a scope segment in the key, not in the prefix, so a
43
+ * second kind of seam entry can join the namespace without colliding.
44
+ */
45
+ export declare const CONTROL_PLANE_KV_PREFIX = "controlplane";
46
+ /** A spent token's receipt: which connection burned it, and when. */
47
+ export declare const SeenJti: z.ZodObject<{
48
+ connectionId: z.ZodString;
49
+ seenAt: z.ZodNumber;
50
+ }, z.core.$strip>;
51
+ export type SeenJti = z.infer<typeof SeenJti>;
52
+ /** The key for one receipt. Physical key: `controlplane:jti:<jti>`. */
53
+ export declare const SeenJtiKey: z.ZodObject<{
54
+ scope: z.ZodLiteral<"jti">;
55
+ jti: z.ZodString;
56
+ }, z.core.$strip>;
57
+ export type SeenJtiKey = z.infer<typeof SeenJtiKey>;
58
+ /**
59
+ * The KV namespaces the control-plane capability registers, served on `c.var.kv.controlplane.jtis`.
60
+ *
61
+ * The TTL is the store's, not the caller's: an entry that expired while the token that spent it was
62
+ * still valid would silently reopen the replay window, so it is declared once here and every write
63
+ * inherits it.
64
+ *
65
+ * **A function of the config, not a constant.** It used to be a module-level literal pinned to
66
+ * {@link CONTROL_PLANE_JTI_TTL_SECONDS}, which made `jtiTtlSeconds` dead config: an adopter who
67
+ * lengthened `maxTokenLifetimeSeconds` and lengthened the replay memory to match — the pairing the
68
+ * config's own cross-field check tells them to make — still got a 180-second store, and their tokens
69
+ * outlived the memory of them. The one setting that governs this store now actually reaches it.
70
+ */
71
+ export declare function controlPlaneKvNamespaces(jtiTtlSeconds?: number): {
72
+ controlplane: {
73
+ binding: string;
74
+ stores: {
75
+ jtis: {
76
+ prefix: string;
77
+ key: z.ZodObject<{
78
+ scope: z.ZodLiteral<"jti">;
79
+ jti: z.ZodString;
80
+ }, z.core.$strip>;
81
+ value: z.ZodObject<{
82
+ connectionId: z.ZodString;
83
+ seenAt: z.ZodNumber;
84
+ }, z.core.$strip>;
85
+ ttlSeconds: number;
86
+ };
87
+ };
88
+ };
89
+ };
90
+ /** The shape {@link controlPlaneKvNamespaces} produces — what `c.var.kv` is narrowed to. */
91
+ export type ControlPlaneKvNamespaces = ReturnType<typeof controlPlaneKvNamespaces>;
92
+ /**
93
+ * A {@link ReplayGuard} over a typed KV store — the shipped implementation.
94
+ *
95
+ * Read-then-write, because KV offers nothing better. A stored receipt that fails validation throws
96
+ * rather than resolving, which fails closed: a poisoned entry denies the call instead of admitting it.
97
+ */
98
+ export declare function kvReplayGuard<M extends z.ZodType>(store: TypedKv<typeof SeenJti, typeof SeenJtiKey, M>): ReplayGuard;
99
+ //# sourceMappingURL=kvGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kvGuard.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/replay/kvGuard.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AAExD;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAEtD,qEAAqE;AACrE,eAAO,MAAM,OAAO;;;iBAkBjB,CAAC;AACJ,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAC;AAE9C,uEAAuE;AACvE,eAAO,MAAM,UAAU;;;iBAegF,CAAC;AACxG,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CAAC,aAAa,GAAE,MAAsC;;;;;;;;;;;;;;;;;;EAc7F;AAED,4FAA4F;AAC5F,MAAM,MAAM,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEnF;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,OAAO,EAAE,OAAO,UAAU,EAAE,CAAC,CAAC,GAAG,WAAW,CASpH"}
@@ -0,0 +1,101 @@
1
+ import "../token/claims.js";
2
+ import { z } from "zod";
3
+ //#region src/controlPlane/replay/kvGuard.ts
4
+ /**
5
+ * The KV-backed {@link ReplayGuard} — still selectable, no longer the default.
6
+ *
7
+ * See `./guard.ts` for what the guard is for and why D1 now holds the default. The part that belongs
8
+ * here is the caveat that is a property of *this* store and not of the seam:
9
+ *
10
+ * **Workers KV has no compare-and-set and is eventually consistent across colocations.** A `put` in one
11
+ * PoP is not immediately visible in another, so two copies of one token presented in two locations
12
+ * inside the propagation window can both read a miss and both claim. Read-then-write is the strongest
13
+ * claim KV offers; it is best-effort, not an atomic single-use gate.
14
+ *
15
+ * Three things bound the exposure for an adopter who chooses it anyway.
16
+ *
17
+ * 1. **The window is KV's propagation delay** — seconds, and independent of the 60-second token. The
18
+ * write is issued the moment the first call lands; once it has converged the jti is spent for a full
19
+ * {@link CONTROL_PLANE_JTI_TTL_SECONDS}, which deliberately outlives the token that carried it. So
20
+ * there is no window at the end of the token's life, only at the very start of it.
21
+ * 2. **The token is not a general capability.** It is bound to one connection, one scope, one request
22
+ * body digest, and that expiry. What can survive the window is a replay of *the exact same call* —
23
+ * not a forged one, not a broader one, not a later one.
24
+ * 3. **`d1ReplayGuard` is one config key away**, and closes the race outright. Choosing this one is
25
+ * choosing to skip a D1 write on an administrator-paced path, with the race as the stated price.
26
+ */
27
+ /**
28
+ * The KV binding the seam's replay set lives in.
29
+ *
30
+ * **Not Cloudflare's control plane.** That is the outbound provisioning REST API this project reaches
31
+ * through `@pithy-sh/cloudflare`. This is the inbound, adopter-authenticated admin seam, and it owns
32
+ * its own namespace rather than sharing `SESSIONS` — a junk-drawer KV would put user sessions and
33
+ * management-token receipts behind one binding, and they have nothing to do with each other.
34
+ */
35
+ const CONTROL_PLANE_KV_BINDING = "CONTROL_PLANE";
36
+ /**
37
+ * The fixed first key segment for everything the seam stores in KV.
38
+ *
39
+ * One word, no hyphen: `assertValidConfig` rejects a prefix containing the key separator, and the
40
+ * namespace tokens across this capability (migration namespace, table prefix, error domain) are all
41
+ * the single word `controlplane` for the same family of reasons. The `jti` segment below is what makes
42
+ * the physical key read `controlplane:jti:<id>` — a scope segment in the key, not in the prefix, so a
43
+ * second kind of seam entry can join the namespace without colliding.
44
+ */
45
+ const CONTROL_PLANE_KV_PREFIX = "controlplane";
46
+ /** A spent token's receipt: which connection burned it, and when. */
47
+ const SeenJti = z.object({
48
+ connectionId: z.string().min(1).describe("The connection whose token claimed this jti. Recorded for forensics only — a replay is refused whatever connection presents it, because the jti alone is the key."),
49
+ seenAt: z.number().int().nonnegative().describe("When the jti was claimed, as a ms-epoch number. A plain number rather than a date codec: nothing decodes this back into app types, and it exists to make a raw KV dump readable during an incident.")
50
+ }).describe("The receipt written when a control-plane token is spent. Its presence is the whole signal; the fields are for the incident, not the decision.");
51
+ /** The key for one receipt. Physical key: `controlplane:jti:<jti>`. */
52
+ const SeenJtiKey = z.object({
53
+ scope: z.literal("jti").describe("The fixed second key segment, naming what kind of entry this is. Carried in the key rather than the prefix so the namespace can hold another kind of seam entry later without a migration."),
54
+ jti: z.string().min(1).max(128).describe("The token's unique id, minted by the management client. Bounded because it arrives from a caller and becomes a KV key — an unbounded id would be an unbounded write.")
55
+ }).describe("The KV key identifying one spent token: the `jti` scope segment and the token id itself.");
56
+ /**
57
+ * The KV namespaces the control-plane capability registers, served on `c.var.kv.controlplane.jtis`.
58
+ *
59
+ * The TTL is the store's, not the caller's: an entry that expired while the token that spent it was
60
+ * still valid would silently reopen the replay window, so it is declared once here and every write
61
+ * inherits it.
62
+ *
63
+ * **A function of the config, not a constant.** It used to be a module-level literal pinned to
64
+ * {@link CONTROL_PLANE_JTI_TTL_SECONDS}, which made `jtiTtlSeconds` dead config: an adopter who
65
+ * lengthened `maxTokenLifetimeSeconds` and lengthened the replay memory to match — the pairing the
66
+ * config's own cross-field check tells them to make — still got a 180-second store, and their tokens
67
+ * outlived the memory of them. The one setting that governs this store now actually reaches it.
68
+ */
69
+ function controlPlaneKvNamespaces(jtiTtlSeconds = 300) {
70
+ return { controlplane: {
71
+ binding: CONTROL_PLANE_KV_BINDING,
72
+ stores: { jtis: {
73
+ prefix: CONTROL_PLANE_KV_PREFIX,
74
+ key: SeenJtiKey,
75
+ value: SeenJti,
76
+ ttlSeconds: jtiTtlSeconds
77
+ } }
78
+ } };
79
+ }
80
+ /**
81
+ * A {@link ReplayGuard} over a typed KV store — the shipped implementation.
82
+ *
83
+ * Read-then-write, because KV offers nothing better. A stored receipt that fails validation throws
84
+ * rather than resolving, which fails closed: a poisoned entry denies the call instead of admitting it.
85
+ */
86
+ function kvReplayGuard(store) {
87
+ return { async claim(jti, connectionId) {
88
+ const key = {
89
+ scope: "jti",
90
+ jti
91
+ };
92
+ if (await store.get(key) !== null) return false;
93
+ await store.put(key, {
94
+ connectionId,
95
+ seenAt: Date.now()
96
+ });
97
+ return true;
98
+ } };
99
+ }
100
+ //#endregion
101
+ export { CONTROL_PLANE_KV_BINDING, CONTROL_PLANE_KV_PREFIX, SeenJti, SeenJtiKey, controlPlaneKvNamespaces, kvReplayGuard };