@pithy-sh/core 0.1.1 → 0.1.3

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,24 @@
1
+ import { ForbiddenError } from "../error/pithyError.js";
2
+ //#region src/http/sameOrigin.ts
3
+ /**
4
+ * Require this request to come from an origin the Worker trusts — the CSRF gate a cookie-authenticated
5
+ * mutating route wears, beside the strategy that says who the caller is.
6
+ *
7
+ * app.post("/organizations", requireAuth(), requireSameOrigin(), zValidator(…), handler)
8
+ *
9
+ * A bearer request carries no ambient credential and is CSRF-exempt; that exemption belongs to the
10
+ * published gate, since the capability that owns the credential model is the one that knows.
11
+ */
12
+ function requireSameOrigin() {
13
+ return async (c, next) => {
14
+ const gate = c.var.sameOrigin;
15
+ if (!gate) throw new ForbiddenError({
16
+ message: "Cross-origin request rejected.",
17
+ action: "Compose a capability that resolves this Worker's trusted origins (auth), then retry.",
18
+ detail: "no same-origin policy on the request — no composed capability published one"
19
+ });
20
+ return await gate(c, next);
21
+ };
22
+ }
23
+ //#endregion
24
+ export { requireSameOrigin };
@@ -0,0 +1,225 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ import type { PithyHonoEnv } from "../capability/capability";
3
+ /**
4
+ * The `signed-webhook` strategy, for any sender. Secret, header name, tolerance and the exact received
5
+ * bytes in; verified, or a `PithyError`, out.
6
+ *
7
+ * `signed-webhook` has been a first-class strategy in {@link ./verification.VerificationStrategy} since the
8
+ * contract landed, but the only implementation was `@pithy-sh/payments`' webhook guard — welded to a rail
9
+ * catalog, a D1 table and a dedup insert, because it also records deliveries. So an adopter declaring the
10
+ * strategy on their own route had the word and nothing to put behind it. This is the word's implementation,
11
+ * and `@pithy-sh/payments`' Stripe rail is now one of its callers rather than a second copy of it.
12
+ *
13
+ * **Dedup and persistence stay the caller's**: at-least-once delivery is a property of the sender, not of the
14
+ * signature, and a verifier that also owned a table could not be composed by anyone whose table differs. Say
15
+ * the consequence plainly, because it is a security property and not only an availability one: this proves a
16
+ * delivery is authentic and fresh, never that it is *new*. Inside the tolerance a captured delivery replays
17
+ * as many times as it is sent. A handler that grants, charges, or deletes needs its own uniqueness key —
18
+ * payments spends a `UNIQUE (rail, providerEventId)` insert on exactly this.
19
+ *
20
+ * ## The scheme
21
+ *
22
+ * One header carries both halves of the proof: `<header>: t=1785931200,v1=5257a8…` — the timestamp the
23
+ * delivery claims, then one hex HMAC-SHA256 per signature, each computed over `<timestamp>.<body>`. Stripe's
24
+ * format, deliberately, because it is the one every other sender copied and because it gets the four things
25
+ * below right. The key names are options — a sender writing `v0` costs one line — but the signed payload is
26
+ * fixed. A caller-supplied template would be the one part of the scheme that must be byte-identical at both
27
+ * ends, and getting it wrong yields a verifier that verifies nothing while still returning.
28
+ *
29
+ * ## Why the timestamp is inside the signed payload
30
+ *
31
+ * A signature with no freshness window is valid forever: whoever captures one delivery can replay it for as
32
+ * long as the secret lives. The window only closes that if it cannot be stepped around — and it cannot,
33
+ * because the timestamp is part of what was signed. Re-dating a captured delivery to escape the window
34
+ * invalidates its own signature. A timestamp in a second header, or alongside the signature but outside it,
35
+ * would be a suggestion.
36
+ *
37
+ * The window is checked in **both** directions. A delivery dated far in the future is a clock problem or a
38
+ * crafted one, and neither is a delivery to act on; accepting it would also hand a forger an unbounded replay
39
+ * window the moment a secret leaks.
40
+ *
41
+ * ## Why the comparison is `crypto.subtle.verify`
42
+ *
43
+ * Comparing HMACs with `===` leaks how many leading bytes matched, and that leak is enough to forge a
44
+ * signature one byte at a time given enough attempts. WebCrypto's `verify` does the comparison itself, in
45
+ * constant time, in the platform — so the requirement is met by not hand-writing the compare at all.
46
+ * (`timingSafeEqual` in `../controlPlane/token/digest` is the string equivalent, for values WebCrypto will
47
+ * not compare for you.) Every listed signature is tried, because a sender rotating its secret signs with each
48
+ * active one, and refusing a delivery whose second signature matched would drop every delivery for the length
49
+ * of the rotation.
50
+ *
51
+ * ## What a refusal says
52
+ *
53
+ * `core/webhook_unverified`, 401, one code for every failing step. The step goes in `detail`, which the HTTP
54
+ * codec strips — so an operator reading a log learns which check refused it and the sender learns only that
55
+ * something did. **Never the secret, never a signature, never the body.** A refusal is the one message a
56
+ * hostile caller is guaranteed to receive, so it is the last place to echo anything it supplied or anything
57
+ * it is trying to guess.
58
+ *
59
+ * A caller that must word its own refusal — a rail whose own code is part of its contract — calls
60
+ * {@link checkSignedWebhook}, which reports rather than throws. {@link verifySignedWebhook} is that plus the
61
+ * kit's error.
62
+ */
63
+ /** How far a delivery's own timestamp may be from now. Stripe's default, and generous against clock skew. */
64
+ export declare const SIGNED_WEBHOOK_TOLERANCE_SECONDS = 300;
65
+ /**
66
+ * How many candidate signatures one delivery may spend, after the unusable ones are dropped.
67
+ *
68
+ * The list is written by whoever sent the request, and every entry on it costs a full HMAC over the whole
69
+ * body, once per configured secret. Uncapped, a 16 KiB header holds a few hundred well-formed candidates and
70
+ * one anonymous POST buys hundreds of times the CPU a real delivery does — billed CPU, on a runtime with a
71
+ * burst limit. A sender rotating its secret lists two, occasionally three. Eight is far past generous and
72
+ * turns an unbounded multiplier into a constant.
73
+ */
74
+ export declare const SIGNED_WEBHOOK_MAX_CANDIDATES = 8;
75
+ /** What the two ends of the scheme must agree on, beyond the secret. */
76
+ export interface SignedWebhookScheme {
77
+ /**
78
+ * The header carrying the proof, lower case — that is how Hono presents a header name. Required and
79
+ * never defaulted: a default would be a header some sender happens not to use, read silently.
80
+ */
81
+ header: string;
82
+ /** The header key holding the timestamp. Defaults to `t`. */
83
+ timestampKey?: string;
84
+ /** The header key holding a signature. Defaults to `v1`. */
85
+ signatureKey?: string;
86
+ /** The freshness window, in seconds, either side of now. Defaults to {@link SIGNED_WEBHOOK_TOLERANCE_SECONDS}. */
87
+ toleranceSeconds?: number;
88
+ }
89
+ /**
90
+ * The secret(s) this endpoint accepts. An array is the receiver's own rotation overlap: both the outgoing and
91
+ * the incoming secret verify until the sender is switched over. `secretsStore(…).getVersions(NAME)` is what
92
+ * feeds it, through `Object.values(secret.versions)` — it returns `{ currentVersion, versions }`, not an array,
93
+ * and only a `valueType: "text"` secret yields strings to pass here. (The sender's rotation is the other
94
+ * axis — several signatures in one header — and needs nothing from the caller.)
95
+ *
96
+ * An entry that resolved to nothing is dropped rather than tried: a blank version is a configuration fault,
97
+ * and WebCrypto refuses a zero-length key with a `DOMException` that would otherwise escape as a 500 whose
98
+ * text names nothing. An endpoint left with no usable secret is reported as the configuration fault it is.
99
+ */
100
+ export type SignedWebhookSecret = string | readonly string[];
101
+ /** Everything {@link verifySignedWebhook} needs beyond the bytes and the header value. */
102
+ export interface VerifySignedWebhookOptions extends SignedWebhookScheme {
103
+ /** The signing secret, or every secret still honored during a rotation. */
104
+ secret: SignedWebhookSecret;
105
+ /** The clock. Injectable so the freshness window is deterministic in tests rather than wall-clock dependent. */
106
+ now?: Date;
107
+ }
108
+ /** Everything {@link requireSignedWebhook} needs, as a route wears it. */
109
+ export interface SignedWebhookGuardOptions extends SignedWebhookScheme {
110
+ /**
111
+ * The secret, or a resolver reading it from the Worker env at the point of need. A guard is built at
112
+ * module scope and `env` only exists per request, so a secret read through `@pithy-sh/secrets` can only
113
+ * arrive as the second form — and reading it per request is also what makes a rotation take effect
114
+ * without a deploy.
115
+ */
116
+ secret: SignedWebhookSecret | ((env: Record<string, unknown>) => SignedWebhookSecret | Promise<SignedWebhookSecret>);
117
+ /** The clock, for the freshness window. Injected so tests are deterministic. */
118
+ now?: () => Date;
119
+ }
120
+ /** A parsed proof header: when the delivery says it was signed, and with what. */
121
+ export interface SignedWebhookHeader {
122
+ /** The timestamp value, in seconds since the epoch. Part of the signed payload as well as of the header. */
123
+ timestamp: number;
124
+ /** Every signature value, in the order listed. One per secret the sender currently signs with. */
125
+ signatures: readonly string[];
126
+ }
127
+ /**
128
+ * Read a proof header, or `undefined` when it is not one.
129
+ *
130
+ * Undefined rather than a throw per unreadable shape, because the caller turns them all into one refusal:
131
+ * distinguishing "no timestamp" from "no signature" would describe our parser to a sender, and neither is
132
+ * actionable by anyone but an operator reading the endpoint's configuration.
133
+ *
134
+ * Unknown keys are dropped rather than refused. A sender may list a scheme this build does not know — Stripe
135
+ * sends `v0` beside `v1` — and a delivery whose known signature is good is authentic whatever else it
136
+ * carries. A header carrying *only* unknown keys has proved nothing, so it yields undefined.
137
+ *
138
+ * The timestamp is the exception: it is read strictly, once, and only in the exact spelling it was signed in.
139
+ * Two of them is a header that has not said when it was signed, and taking the last is how a parser and a
140
+ * sender come to disagree about what was covered. A non-canonical one (`00178…`) could never verify either,
141
+ * because the bytes signed are the digits the sender wrote — refusing it here names the digit rather than
142
+ * sending an operator after a rotated secret.
143
+ */
144
+ export declare function parseSignedWebhookHeader(header: string, timestampKey: string, signatureKey: string): SignedWebhookHeader | undefined;
145
+ /**
146
+ * Why a delivery did not verify, for a caller that words its own refusal.
147
+ *
148
+ * Three reasons, because there are three checks. They are for the caller's `detail` and an operator's log,
149
+ * never for the response body: a sender that learns which check refused it learns how close it got.
150
+ */
151
+ export type SignedWebhookRefusal =
152
+ /** No header, or one carrying no timestamp and signature this scheme can read. */
153
+ {
154
+ reason: "unreadable";
155
+ }
156
+ /** Read, but dated outside the freshness window — a clock, or a replay. */
157
+ | {
158
+ reason: "stale";
159
+ skew: number;
160
+ tolerance: number;
161
+ }
162
+ /**
163
+ * Read and fresh, and nothing in it verified.
164
+ *
165
+ * `compared` is how many listed values were actually put to a secret — the candidates left after the ones
166
+ * that cannot be an HMAC-SHA256 are dropped and the cap is applied. Zero is a different finding from the
167
+ * rest: no comparison ran at all, so the answer is the sender's signature format, not the endpoint's
168
+ * secret. A refusal that does not carry the number cannot tell an operator which of the two it is.
169
+ */
170
+ | {
171
+ reason: "unmatched";
172
+ compared: number;
173
+ };
174
+ /**
175
+ * Check one delivery, and **report** rather than throw: `undefined` when a holder of the secret signed these
176
+ * exact bytes inside the window, a {@link SignedWebhookRefusal} when nobody did.
177
+ *
178
+ * This is the seam a rail composes. `@pithy-sh/payments` must keep raising `payments/verification_failed`
179
+ * with its own Stripe-worded `detail`, and that is a contract of its own — so the scheme lives here once and
180
+ * each caller keeps its vocabulary, rather than the whole verifier existing twice with two sets of comments
181
+ * to keep in step. Most callers want {@link verifySignedWebhook}.
182
+ *
183
+ * It still throws for **our** failure: an endpoint holding no usable secret, or handed a clock that is not a
184
+ * clock, is a configuration fault rather than a refusal, and every caller reports that the same way.
185
+ *
186
+ * `body` must be the **exact received bytes**. A parsed-and-re-serialized object is different bytes — key
187
+ * order alone changes it — and would never verify.
188
+ */
189
+ export declare function checkSignedWebhook(body: string, header: string | null, options: VerifySignedWebhookOptions): Promise<SignedWebhookRefusal | undefined>;
190
+ /**
191
+ * Verify one delivery. Resolves when a holder of the secret signed these exact bytes inside the window, and
192
+ * throws `core/webhook_unverified` otherwise.
193
+ */
194
+ export declare function verifySignedWebhook(body: string, header: string | null, options: VerifySignedWebhookOptions): Promise<void>;
195
+ /**
196
+ * The `signed-webhook` gate, as a route wears it: `app.post(path, requireSignedWebhook({…}), zValidator("json",
197
+ * Body, validationHook), handler)`.
198
+ *
199
+ * ## Why the header is read before anything else
200
+ *
201
+ * The proof header is the cheapest thing to check and the commonest thing to be missing, so it is checked
202
+ * first. A delivery carrying no readable one is refused whatever the body holds — so resolving the secret (a
203
+ * D1 read and a decrypt) and buffering the body before looking would be unauthenticated work bought by an
204
+ * anonymous POST, on the one route whose whole purpose is to refuse unauthenticated callers.
205
+ *
206
+ * The header is then parsed a second time inside the full check rather than threaded through it. Parsing a
207
+ * short header twice costs nothing; two entry points into the verification, one of them holding a
208
+ * half-verified state, would cost the property that there is exactly one way through this scheme.
209
+ *
210
+ * One consequence, and it is the right one: an endpoint whose secret never resolved answers a proof-less POST
211
+ * with the same 401 every other endpoint gives it, and reports the configuration fault to the first delivery
212
+ * that actually carries a proof. A caller with nothing to verify learns nothing about our configuration.
213
+ *
214
+ * ## Why it reads the body itself, and with `c.req.text()`
215
+ *
216
+ * The proof covers the exact received bytes, so the gate must see what arrived rather than a reconstruction.
217
+ * That is the sanctioned use the Biome plugin leaves open under `src/http/**`: `c.req.json()` is banned,
218
+ * `c.req.text()` is not.
219
+ *
220
+ * `c.req.text()` rather than `c.req.raw.text()`, and the distinction is load-bearing. Hono caches its own
221
+ * body reads and serves `c.req.json()` out of the cached text, so a `zValidator("json", …)` after this gate
222
+ * parses identical bytes. Reading `c.req.raw` would consume the stream and leave the validator with nothing.
223
+ */
224
+ export declare function requireSignedWebhook(options: SignedWebhookGuardOptions): MiddlewareHandler<PithyHonoEnv>;
225
+ //# sourceMappingURL=signedWebhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signedWebhook.d.ts","sourceRoot":"","sources":["../../src/http/signedWebhook.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AAEH,6GAA6G;AAC7G,eAAO,MAAM,gCAAgC,MAAM,CAAC;AAcpD;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kHAAkH;IAClH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;AAE7D,0FAA0F;AAC1F,MAAM,WAAW,0BAA2B,SAAQ,mBAAmB;IACrE,2EAA2E;IAC3E,MAAM,EAAE,mBAAmB,CAAC;IAC5B,gHAAgH;IAChH,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,0EAA0E;AAC1E,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACpE;;;;;OAKG;IACH,MAAM,EAAE,mBAAmB,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC;IACrH,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,kFAAkF;AAClF,MAAM,WAAW,mBAAmB;IAClC,4GAA4G;IAC5G,SAAS,EAAE,MAAM,CAAC;IAClB,kGAAkG;IAClG,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,GACnB,mBAAmB,GAAG,SAAS,CAuBjC;AAED;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB;AAC9B,kFAAkF;AAChF;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE;AAC1B,2EAA2E;GACzE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AACtD;;;;;;;GAOG;GACD;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAkE3C;AAkCD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,yBAAyB,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAyBxG"}
@@ -0,0 +1,247 @@
1
+ import { InternalError, WebhookUnverifiedError } from "../error/pithyError.js";
2
+ //#region src/http/signedWebhook.ts
3
+ /**
4
+ * The `signed-webhook` strategy, for any sender. Secret, header name, tolerance and the exact received
5
+ * bytes in; verified, or a `PithyError`, out.
6
+ *
7
+ * `signed-webhook` has been a first-class strategy in {@link ./verification.VerificationStrategy} since the
8
+ * contract landed, but the only implementation was `@pithy-sh/payments`' webhook guard — welded to a rail
9
+ * catalog, a D1 table and a dedup insert, because it also records deliveries. So an adopter declaring the
10
+ * strategy on their own route had the word and nothing to put behind it. This is the word's implementation,
11
+ * and `@pithy-sh/payments`' Stripe rail is now one of its callers rather than a second copy of it.
12
+ *
13
+ * **Dedup and persistence stay the caller's**: at-least-once delivery is a property of the sender, not of the
14
+ * signature, and a verifier that also owned a table could not be composed by anyone whose table differs. Say
15
+ * the consequence plainly, because it is a security property and not only an availability one: this proves a
16
+ * delivery is authentic and fresh, never that it is *new*. Inside the tolerance a captured delivery replays
17
+ * as many times as it is sent. A handler that grants, charges, or deletes needs its own uniqueness key —
18
+ * payments spends a `UNIQUE (rail, providerEventId)` insert on exactly this.
19
+ *
20
+ * ## The scheme
21
+ *
22
+ * One header carries both halves of the proof: `<header>: t=1785931200,v1=5257a8…` — the timestamp the
23
+ * delivery claims, then one hex HMAC-SHA256 per signature, each computed over `<timestamp>.<body>`. Stripe's
24
+ * format, deliberately, because it is the one every other sender copied and because it gets the four things
25
+ * below right. The key names are options — a sender writing `v0` costs one line — but the signed payload is
26
+ * fixed. A caller-supplied template would be the one part of the scheme that must be byte-identical at both
27
+ * ends, and getting it wrong yields a verifier that verifies nothing while still returning.
28
+ *
29
+ * ## Why the timestamp is inside the signed payload
30
+ *
31
+ * A signature with no freshness window is valid forever: whoever captures one delivery can replay it for as
32
+ * long as the secret lives. The window only closes that if it cannot be stepped around — and it cannot,
33
+ * because the timestamp is part of what was signed. Re-dating a captured delivery to escape the window
34
+ * invalidates its own signature. A timestamp in a second header, or alongside the signature but outside it,
35
+ * would be a suggestion.
36
+ *
37
+ * The window is checked in **both** directions. A delivery dated far in the future is a clock problem or a
38
+ * crafted one, and neither is a delivery to act on; accepting it would also hand a forger an unbounded replay
39
+ * window the moment a secret leaks.
40
+ *
41
+ * ## Why the comparison is `crypto.subtle.verify`
42
+ *
43
+ * Comparing HMACs with `===` leaks how many leading bytes matched, and that leak is enough to forge a
44
+ * signature one byte at a time given enough attempts. WebCrypto's `verify` does the comparison itself, in
45
+ * constant time, in the platform — so the requirement is met by not hand-writing the compare at all.
46
+ * (`timingSafeEqual` in `../controlPlane/token/digest` is the string equivalent, for values WebCrypto will
47
+ * not compare for you.) Every listed signature is tried, because a sender rotating its secret signs with each
48
+ * active one, and refusing a delivery whose second signature matched would drop every delivery for the length
49
+ * of the rotation.
50
+ *
51
+ * ## What a refusal says
52
+ *
53
+ * `core/webhook_unverified`, 401, one code for every failing step. The step goes in `detail`, which the HTTP
54
+ * codec strips — so an operator reading a log learns which check refused it and the sender learns only that
55
+ * something did. **Never the secret, never a signature, never the body.** A refusal is the one message a
56
+ * hostile caller is guaranteed to receive, so it is the last place to echo anything it supplied or anything
57
+ * it is trying to guess.
58
+ *
59
+ * A caller that must word its own refusal — a rail whose own code is part of its contract — calls
60
+ * {@link checkSignedWebhook}, which reports rather than throws. {@link verifySignedWebhook} is that plus the
61
+ * kit's error.
62
+ */
63
+ /** How far a delivery's own timestamp may be from now. Stripe's default, and generous against clock skew. */
64
+ const SIGNED_WEBHOOK_TOLERANCE_SECONDS = 300;
65
+ /** The header key naming the timestamp, unless a sender names it otherwise. */
66
+ const DEFAULT_TIMESTAMP_KEY = "t";
67
+ /** The header key naming a signature, unless a sender names it otherwise. */
68
+ const DEFAULT_SIGNATURE_KEY = "v1";
69
+ /** HMAC-SHA256 — the algorithm this scheme is. Pinned as a literal, never read from the delivery. */
70
+ const HMAC_SHA256 = {
71
+ name: "HMAC",
72
+ hash: "SHA-256"
73
+ };
74
+ /** The byte length of an HMAC-SHA256 signature. A candidate of any other length cannot be one. */
75
+ const SIGNATURE_BYTES = 32;
76
+ /**
77
+ * How many candidate signatures one delivery may spend, after the unusable ones are dropped.
78
+ *
79
+ * The list is written by whoever sent the request, and every entry on it costs a full HMAC over the whole
80
+ * body, once per configured secret. Uncapped, a 16 KiB header holds a few hundred well-formed candidates and
81
+ * one anonymous POST buys hundreds of times the CPU a real delivery does — billed CPU, on a runtime with a
82
+ * burst limit. A sender rotating its secret lists two, occasionally three. Eight is far past generous and
83
+ * turns an unbounded multiplier into a constant.
84
+ */
85
+ const SIGNED_WEBHOOK_MAX_CANDIDATES = 8;
86
+ /**
87
+ * Read a proof header, or `undefined` when it is not one.
88
+ *
89
+ * Undefined rather than a throw per unreadable shape, because the caller turns them all into one refusal:
90
+ * distinguishing "no timestamp" from "no signature" would describe our parser to a sender, and neither is
91
+ * actionable by anyone but an operator reading the endpoint's configuration.
92
+ *
93
+ * Unknown keys are dropped rather than refused. A sender may list a scheme this build does not know — Stripe
94
+ * sends `v0` beside `v1` — and a delivery whose known signature is good is authentic whatever else it
95
+ * carries. A header carrying *only* unknown keys has proved nothing, so it yields undefined.
96
+ *
97
+ * The timestamp is the exception: it is read strictly, once, and only in the exact spelling it was signed in.
98
+ * Two of them is a header that has not said when it was signed, and taking the last is how a parser and a
99
+ * sender come to disagree about what was covered. A non-canonical one (`00178…`) could never verify either,
100
+ * because the bytes signed are the digits the sender wrote — refusing it here names the digit rather than
101
+ * sending an operator after a rotated secret.
102
+ */
103
+ function parseSignedWebhookHeader(header, timestampKey, signatureKey) {
104
+ let timestamp;
105
+ const signatures = [];
106
+ for (const part of header.split(",")) {
107
+ const separator = part.indexOf("=");
108
+ if (separator < 0) continue;
109
+ const key = part.slice(0, separator).trim();
110
+ const value = part.slice(separator + 1).trim();
111
+ if (key === timestampKey) {
112
+ if (timestamp !== void 0 || !/^(?:0|[1-9]\d*)$/.test(value)) return void 0;
113
+ timestamp = Number(value);
114
+ } else if (key === signatureKey) signatures.push(value);
115
+ }
116
+ if (timestamp === void 0 || signatures.length === 0) return void 0;
117
+ return {
118
+ timestamp,
119
+ signatures
120
+ };
121
+ }
122
+ /**
123
+ * Check one delivery, and **report** rather than throw: `undefined` when a holder of the secret signed these
124
+ * exact bytes inside the window, a {@link SignedWebhookRefusal} when nobody did.
125
+ *
126
+ * This is the seam a rail composes. `@pithy-sh/payments` must keep raising `payments/verification_failed`
127
+ * with its own Stripe-worded `detail`, and that is a contract of its own — so the scheme lives here once and
128
+ * each caller keeps its vocabulary, rather than the whole verifier existing twice with two sets of comments
129
+ * to keep in step. Most callers want {@link verifySignedWebhook}.
130
+ *
131
+ * It still throws for **our** failure: an endpoint holding no usable secret, or handed a clock that is not a
132
+ * clock, is a configuration fault rather than a refusal, and every caller reports that the same way.
133
+ *
134
+ * `body` must be the **exact received bytes**. A parsed-and-re-serialized object is different bytes — key
135
+ * order alone changes it — and would never verify.
136
+ */
137
+ async function checkSignedWebhook(body, header, options) {
138
+ const secrets = (typeof options.secret === "string" ? [options.secret] : options.secret).filter((secret) => secret.length > 0);
139
+ if (secrets.length === 0) throw new InternalError({
140
+ message: "This webhook endpoint is not configured.",
141
+ action: `Give the ${options.header} guard a signing secret.`,
142
+ detail: `The signed-webhook guard on ${options.header} resolved no signing secret, so it can verify nothing.`
143
+ });
144
+ const timestampKey = options.timestampKey ?? DEFAULT_TIMESTAMP_KEY;
145
+ const signatureKey = options.signatureKey ?? DEFAULT_SIGNATURE_KEY;
146
+ const parsed = header === null ? void 0 : parseSignedWebhookHeader(header, timestampKey, signatureKey);
147
+ if (parsed === void 0) return { reason: "unreadable" };
148
+ const tolerance = options.toleranceSeconds ?? 300;
149
+ const nowSeconds = Math.floor((options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
150
+ if (!Number.isFinite(nowSeconds)) throw new InternalError({
151
+ message: "This webhook endpoint is not configured.",
152
+ action: `Give the ${options.header} guard a valid clock.`,
153
+ detail: `The signed-webhook guard on ${options.header} was handed a clock that is not a valid Date, so no delivery's freshness can be judged.`
154
+ });
155
+ const skew = Math.abs(nowSeconds - parsed.timestamp);
156
+ if (skew > tolerance) return {
157
+ reason: "stale",
158
+ skew,
159
+ tolerance
160
+ };
161
+ const signed = new TextEncoder().encode(`${parsed.timestamp}.${body}`);
162
+ const candidates = parsed.signatures.map(hexBytes).filter((bytes) => bytes !== void 0 && bytes.length === SIGNATURE_BYTES).slice(0, 8);
163
+ for (const secret of secrets) {
164
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), HMAC_SHA256, false, ["verify"]);
165
+ for (const candidate of candidates) if (await crypto.subtle.verify(HMAC_SHA256.name, key, candidate, signed)) return;
166
+ }
167
+ return {
168
+ reason: "unmatched",
169
+ compared: candidates.length
170
+ };
171
+ }
172
+ /**
173
+ * The kit's wording for each refusal, in one place — so the guard's early exit says exactly what the full
174
+ * check would have said, rather than a second sentence about the same finding.
175
+ *
176
+ * Every string states what was actually determined and nothing more. `detail` is what an operator reads to
177
+ * decide where to look, and a refusal that asserts a comparison it never ran sends them somewhere else.
178
+ */
179
+ function webhookRefusal(refusal, scheme) {
180
+ const timestampKey = scheme.timestampKey ?? DEFAULT_TIMESTAMP_KEY;
181
+ const signatureKey = scheme.signatureKey ?? DEFAULT_SIGNATURE_KEY;
182
+ switch (refusal.reason) {
183
+ case "unreadable": return new WebhookUnverifiedError({ detail: `The delivery carries no readable ${scheme.header} header with a ${timestampKey}= timestamp and a ${signatureKey}= HMAC.` });
184
+ case "stale": return new WebhookUnverifiedError({ detail: `The delivery is dated ${refusal.skew}s from now, outside the ${refusal.tolerance}s tolerance. Check this Worker's clock, or a replayed delivery.` });
185
+ case "unmatched": return new WebhookUnverifiedError({ detail: refusal.compared === 0 ? `No ${signatureKey}= value in the ${scheme.header} header is 32 bytes of hex, so no signature was compared. Check the sender's signature format.` : `Compared ${refusal.compared} signature${refusal.compared === 1 ? "" : "s"} from the ${scheme.header} header against every configured signing secret; none matches these bytes. Check that the secret belongs to this endpoint and this environment.` });
186
+ }
187
+ }
188
+ /**
189
+ * Verify one delivery. Resolves when a holder of the secret signed these exact bytes inside the window, and
190
+ * throws `core/webhook_unverified` otherwise.
191
+ */
192
+ async function verifySignedWebhook(body, header, options) {
193
+ const refusal = await checkSignedWebhook(body, header, options);
194
+ if (refusal !== void 0) throw webhookRefusal(refusal, options);
195
+ }
196
+ /**
197
+ * The `signed-webhook` gate, as a route wears it: `app.post(path, requireSignedWebhook({…}), zValidator("json",
198
+ * Body, validationHook), handler)`.
199
+ *
200
+ * ## Why the header is read before anything else
201
+ *
202
+ * The proof header is the cheapest thing to check and the commonest thing to be missing, so it is checked
203
+ * first. A delivery carrying no readable one is refused whatever the body holds — so resolving the secret (a
204
+ * D1 read and a decrypt) and buffering the body before looking would be unauthenticated work bought by an
205
+ * anonymous POST, on the one route whose whole purpose is to refuse unauthenticated callers.
206
+ *
207
+ * The header is then parsed a second time inside the full check rather than threaded through it. Parsing a
208
+ * short header twice costs nothing; two entry points into the verification, one of them holding a
209
+ * half-verified state, would cost the property that there is exactly one way through this scheme.
210
+ *
211
+ * One consequence, and it is the right one: an endpoint whose secret never resolved answers a proof-less POST
212
+ * with the same 401 every other endpoint gives it, and reports the configuration fault to the first delivery
213
+ * that actually carries a proof. A caller with nothing to verify learns nothing about our configuration.
214
+ *
215
+ * ## Why it reads the body itself, and with `c.req.text()`
216
+ *
217
+ * The proof covers the exact received bytes, so the gate must see what arrived rather than a reconstruction.
218
+ * That is the sanctioned use the Biome plugin leaves open under `src/http/**`: `c.req.json()` is banned,
219
+ * `c.req.text()` is not.
220
+ *
221
+ * `c.req.text()` rather than `c.req.raw.text()`, and the distinction is load-bearing. Hono caches its own
222
+ * body reads and serves `c.req.json()` out of the cached text, so a `zValidator("json", …)` after this gate
223
+ * parses identical bytes. Reading `c.req.raw` would consume the stream and leave the validator with nothing.
224
+ */
225
+ function requireSignedWebhook(options) {
226
+ return async (c, next) => {
227
+ const { secret: declared, now, ...scheme } = options;
228
+ const proof = c.req.header(scheme.header) ?? null;
229
+ if (!(proof !== null && parseSignedWebhookHeader(proof, scheme.timestampKey ?? DEFAULT_TIMESTAMP_KEY, scheme.signatureKey ?? DEFAULT_SIGNATURE_KEY) !== void 0)) throw webhookRefusal({ reason: "unreadable" }, scheme);
230
+ const secret = typeof declared === "function" ? await declared(c.env) : declared;
231
+ await verifySignedWebhook(await c.req.text(), proof, {
232
+ ...scheme,
233
+ secret,
234
+ now: now?.() ?? /* @__PURE__ */ new Date()
235
+ });
236
+ await next();
237
+ };
238
+ }
239
+ /** Decode lower- or upper-case hex, or `undefined` when it is not hex at all. */
240
+ function hexBytes(value) {
241
+ if (value.length === 0 || value.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(value)) return void 0;
242
+ const bytes = new Uint8Array(value.length / 2);
243
+ for (let index = 0; index < bytes.length; index += 1) bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
244
+ return bytes;
245
+ }
246
+ //#endregion
247
+ export { SIGNED_WEBHOOK_MAX_CANDIDATES, SIGNED_WEBHOOK_TOLERANCE_SECONDS, checkSignedWebhook, parseSignedWebhookHeader, requireSignedWebhook, verifySignedWebhook };
@@ -0,0 +1,37 @@
1
+ import type { z } from "zod";
2
+ /**
3
+ * How a route declares what it accepts. Its sibling {@link ./verification.VerificationStrategy}
4
+ * declares how a caller is verified; this declares the shape the caller may send. Both are
5
+ * mandatory, and both belong on the route line — reading a route tells you what it takes without
6
+ * opening the handler.
7
+ *
8
+ * Routes use `zValidator(target, Schema, validationHook)` from `@hono/zod-validator`. The library
9
+ * owns the middleware and the `c.req.valid(target)` typing; this file owns exactly one thing —
10
+ * what happens when the parse fails. Without the hook, zod-validator answers with its own JSON
11
+ * body, which is neither a `PithyError` payload nor routed through `pithyErrorHandler`. With it,
12
+ * a malformed request is indistinguishable from every other failure the app can produce.
13
+ *
14
+ * The hook is deliberately not a wrapper around `zValidator`. A wrapper would be a second name for
15
+ * a library we do not re-export (CLAUDE.md §Toolchain), and it would hide the target from the route
16
+ * line — the one thing the migration exists to make visible.
17
+ */
18
+ /**
19
+ * The outcome `@hono/zod-validator` hands a hook. Declared structurally rather than by importing
20
+ * the library's `Hook` generic: `Hook` is parameterized by env, path, target and schema, so a
21
+ * single shared hook value could not name it without pinning all four. Every real outcome is
22
+ * assignable to this.
23
+ */
24
+ export type ValidationOutcome = {
25
+ success: true;
26
+ } | {
27
+ success: false;
28
+ error: z.core.$ZodError;
29
+ };
30
+ /**
31
+ * The one validation-failure hook every provided route passes to `zValidator`. Maps the `ZodError`
32
+ * through `fromZodError`, so the client sees the same `validation/invalid_input` 400 with the same
33
+ * `issues[]` that the hand-rolled parse helpers produced before — the migration changes where
34
+ * validation is declared, not what a caller sees when it fails.
35
+ */
36
+ export declare function validationHook(outcome: ValidationOutcome): void;
37
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/http/validation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B;;;;;;;;;;;;;;;GAeG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAA;CAAE,CAAC;AAEhG;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAG/D"}
@@ -0,0 +1,14 @@
1
+ import { fromZodError } from "../error/pithyError.js";
2
+ //#region src/http/validation.ts
3
+ /**
4
+ * The one validation-failure hook every provided route passes to `zValidator`. Maps the `ZodError`
5
+ * through `fromZodError`, so the client sees the same `validation/invalid_input` 400 with the same
6
+ * `issues[]` that the hand-rolled parse helpers produced before — the migration changes where
7
+ * validation is declared, not what a caller sees when it fails.
8
+ */
9
+ function validationHook(outcome) {
10
+ if (outcome.success) return;
11
+ throw fromZodError(outcome.error);
12
+ }
13
+ //#endregion
14
+ export { validationHook };
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * How a route verifies the caller's identity/authenticity. Every route declares one —
4
+ * there is no implicit auth. `@pithy-sh/auth` implements `bearer`/`session`; others map to
5
+ * their own checks. (Turnstile is a humanity check applied as middleware, not an identity
6
+ * strategy, so it is deliberately not listed here.)
7
+ */
8
+ export declare const VerificationStrategy: z.ZodUnion<readonly [z.ZodLiteral<"bearer">, z.ZodLiteral<"session">, z.ZodLiteral<"signed-webhook">, z.ZodLiteral<"control-plane">, z.ZodLiteral<"public">]>;
9
+ export type VerificationStrategy = z.infer<typeof VerificationStrategy>;
10
+ //# sourceMappingURL=verification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification.d.ts","sourceRoot":"","sources":["../../src/http/verification.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,+JAYiE,CAAC;AACnG,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ //#region src/http/verification.ts
3
+ /**
4
+ * How a route verifies the caller's identity/authenticity. Every route declares one —
5
+ * there is no implicit auth. `@pithy-sh/auth` implements `bearer`/`session`; others map to
6
+ * their own checks. (Turnstile is a humanity check applied as middleware, not an identity
7
+ * strategy, so it is deliberately not listed here.)
8
+ */
9
+ const VerificationStrategy = z.union([
10
+ z.literal("bearer").describe("Short-lived access token via Authorization: Bearer; validated by @pithy-sh/auth."),
11
+ z.literal("session").describe("Cookie-based web session, CSRF-protected; validated by @pithy-sh/auth."),
12
+ z.literal("signed-webhook").describe("Inbound webhook, authenticated by the sender's own proof over the exact received bytes — one name over several mechanisms (an HMAC, a signed JWS chain, an OIDC token), because the sender chooses it. `requireSignedWebhook` (./signedWebhook) implements the timestamped-HMAC form; a rail whose sender proves it differently implements its own."),
13
+ z.literal("control-plane").describe("M2M admin via a customer-issued scoped credential; default-denied."),
14
+ z.literal("public").describe("No authentication — the route is open. Must be a deliberate choice.")
15
+ ]).describe("How a route verifies the caller's identity/authenticity. There is no implicit auth.");
16
+ //#endregion
17
+ export { VerificationStrategy };
@@ -0,0 +1,22 @@
1
+ /** One entry of an `Accept-Language` header: the tag the caller asked for, and how much they meant it. */
2
+ export interface LanguageRange {
3
+ /** The language range, lower-cased. `*` survives as itself — it is a valid range, not a tag. */
4
+ readonly range: string;
5
+ /** The entry's q-weight, `0` through `1`. Absent weights are `1`, per RFC 9110. */
6
+ readonly quality: number;
7
+ }
8
+ /**
9
+ * An `Accept-Language` header as q-weighted ranges, most-wanted first.
10
+ *
11
+ * **Tokenize, strip q-weights, then validate — in that order, and all of it before anything reaches
12
+ * `Intl`.** The header is caller-supplied and its real-world contents include `*`, `en_US`, empty
13
+ * tokens, and tags still carrying `;q=0.9`; `new Intl.Locale()` throws a `RangeError` on every one of
14
+ * them. A header that cannot be read is an empty list, never a throw, so the resolver chain falls
15
+ * through to its next link instead of 500-ing.
16
+ *
17
+ * The **full** weighted list is returned, not the first entry: `pt-PT;q=1.0, es;q=0.8, en;q=0.5` from a
18
+ * reader who has no Portuguese is a request for Spanish, and reading only the head answers English.
19
+ * Equal weights keep header order, which is what a browser means by listing them that way.
20
+ */
21
+ export declare function parseAcceptLanguage(header: string | null | undefined): LanguageRange[];
22
+ //# sourceMappingURL=acceptLanguage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acceptLanguage.d.ts","sourceRoot":"","sources":["../../src/i18n/acceptLanguage.ts"],"names":[],"mappings":"AAiBA,0GAA0G;AAC1G,MAAM,WAAW,aAAa;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,mFAAmF;IACnF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAUD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,aAAa,EAAE,CAuBtF"}