@slopware/sloppy-darwin-arm64 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +5 -0
  3. package/bin/sloppy +0 -0
  4. package/bin/sloppyc +0 -0
  5. package/docs/KNOWN_LIMITATIONS.md +16 -0
  6. package/docs/LICENSES.md +6 -0
  7. package/docs/NOTICE.md +8 -0
  8. package/examples/README.md +140 -0
  9. package/examples/auth-api/README.md +20 -0
  10. package/examples/auth-api/app.js +61 -0
  11. package/examples/auth-api/appsettings.json +7 -0
  12. package/examples/auth-api/sloppy.json +5 -0
  13. package/examples/cache-basic/README.md +9 -0
  14. package/examples/cache-basic/app.js +32 -0
  15. package/examples/cache-hybrid-postgres/README.md +10 -0
  16. package/examples/cache-hybrid-postgres/app.js +27 -0
  17. package/examples/cache-output-api/README.md +10 -0
  18. package/examples/cache-output-api/app.js +35 -0
  19. package/examples/codec-base64-hex/README.md +14 -0
  20. package/examples/codec-base64-hex/app.js +15 -0
  21. package/examples/codec-checksums/README.md +15 -0
  22. package/examples/codec-checksums/app.js +8 -0
  23. package/examples/codec-compression/README.md +13 -0
  24. package/examples/codec-compression/app.js +9 -0
  25. package/examples/codec-streaming-compression/README.md +19 -0
  26. package/examples/codec-streaming-compression/app.js +16 -0
  27. package/examples/codec-text-binary/README.md +16 -0
  28. package/examples/codec-text-binary/app.js +17 -0
  29. package/examples/compiler-hello/README.md +71 -0
  30. package/examples/compiler-hello/app.js +7 -0
  31. package/examples/compiler-hello/expected/app.js +8 -0
  32. package/examples/compiler-hello/expected/app.js.map +53 -0
  33. package/examples/compiler-hello/expected/app.plan.json +229 -0
  34. package/examples/compiler-hello/expected/routes.slrt +0 -0
  35. package/examples/config-basic/README.md +13 -0
  36. package/examples/config-basic/app.js +13 -0
  37. package/examples/config-basic/appsettings.json +7 -0
  38. package/examples/config-secrets-redaction/README.md +9 -0
  39. package/examples/config-secrets-redaction/app.js +9 -0
  40. package/examples/config-secrets-redaction/appsettings.json +5 -0
  41. package/examples/config-strict-mode/README.md +7 -0
  42. package/examples/config-strict-mode/app.js +10 -0
  43. package/examples/config-strict-mode/appsettings.json +7 -0
  44. package/examples/configured-api/README.md +38 -0
  45. package/examples/configured-api/app.js +12 -0
  46. package/examples/configured-api/appsettings.Development.json +5 -0
  47. package/examples/configured-api/appsettings.json +6 -0
  48. package/examples/configured-api/sloppy.json +5 -0
  49. package/examples/core-config-secrets/README.md +10 -0
  50. package/examples/core-config-secrets/app.js +15 -0
  51. package/examples/core-fs-time-codec/README.md +9 -0
  52. package/examples/core-fs-time-codec/app.js +8 -0
  53. package/examples/core-network-time-codec/README.md +11 -0
  54. package/examples/core-network-time-codec/app.js +20 -0
  55. package/examples/core-policy-audit/README.md +7 -0
  56. package/examples/core-policy-audit/app.js +22 -0
  57. package/examples/core-process-time-codec/README.md +8 -0
  58. package/examples/core-process-time-codec/app.js +28 -0
  59. package/examples/core-worker-time/README.md +8 -0
  60. package/examples/core-worker-time/app.js +17 -0
  61. package/examples/crypto-hash-hmac/README.md +17 -0
  62. package/examples/crypto-hash-hmac/app.js +29 -0
  63. package/examples/crypto-password/README.md +21 -0
  64. package/examples/crypto-password/app.js +12 -0
  65. package/examples/crypto-random-token/README.md +16 -0
  66. package/examples/crypto-random-token/app.js +12 -0
  67. package/examples/crypto-secret-constant-time/README.md +21 -0
  68. package/examples/crypto-secret-constant-time/app.js +15 -0
  69. package/examples/data-foundation/README.md +39 -0
  70. package/examples/data-foundation/app.js +63 -0
  71. package/examples/dependency-graph/README.md +19 -0
  72. package/examples/dependency-graph/fixtures/graph-helper/index.js +3 -0
  73. package/examples/dependency-graph/fixtures/graph-helper/package.json +6 -0
  74. package/examples/dependency-graph/package.json +7 -0
  75. package/examples/dependency-graph/public/message.txt +1 -0
  76. package/examples/dependency-graph/sloppy.json +9 -0
  77. package/examples/dependency-graph/src/main.ts +8 -0
  78. package/examples/dogfood/README.md +23 -0
  79. package/examples/dogfood/dogfood.json +136 -0
  80. package/examples/dynamic-module-include/README.md +20 -0
  81. package/examples/dynamic-module-include/public/readme.txt +1 -0
  82. package/examples/dynamic-module-include/sloppy.json +12 -0
  83. package/examples/dynamic-module-include/src/main.ts +6 -0
  84. package/examples/dynamic-module-include/src/plugins/alpha.js +3 -0
  85. package/examples/dynamic-module-include/src/plugins/beta.js +3 -0
  86. package/examples/ergonomics/README.md +42 -0
  87. package/examples/ergonomics/app.js +38 -0
  88. package/examples/framework-controller/README.md +12 -0
  89. package/examples/framework-controller/app.js +31 -0
  90. package/examples/framework-di-services/README.md +17 -0
  91. package/examples/framework-di-services/app.ts +40 -0
  92. package/examples/framework-explicit-binding/README.md +12 -0
  93. package/examples/framework-explicit-binding/app.ts +34 -0
  94. package/examples/framework-hello/README.md +16 -0
  95. package/examples/framework-hello/app.ts +16 -0
  96. package/examples/framework-postgres-crud/README.md +73 -0
  97. package/examples/framework-postgres-crud/app.ts +64 -0
  98. package/examples/framework-sqlite-crud/README.md +52 -0
  99. package/examples/framework-sqlite-crud/app.ts +90 -0
  100. package/examples/framework-sqlite-crud/appsettings.json +11 -0
  101. package/examples/framework-sqlserver-crud/README.md +73 -0
  102. package/examples/framework-sqlserver-crud/app.ts +64 -0
  103. package/examples/framework-validation-errors/README.md +12 -0
  104. package/examples/framework-validation-errors/app.ts +16 -0
  105. package/examples/fs-basic/README.md +24 -0
  106. package/examples/fs-basic/app.js +12 -0
  107. package/examples/fs-roots-policy/README.md +14 -0
  108. package/examples/fs-roots-policy/app.js +4 -0
  109. package/examples/fs-streams/README.md +18 -0
  110. package/examples/fs-streams/app.js +11 -0
  111. package/examples/fs-watch/README.md +19 -0
  112. package/examples/fs-watch/app.js +11 -0
  113. package/examples/hello/README.md +63 -0
  114. package/examples/hello/app.js +19 -0
  115. package/examples/hello-minimal/README.md +51 -0
  116. package/examples/hello-minimal/sloppy.json +5 -0
  117. package/examples/hello-minimal/src/main.ts +9 -0
  118. package/examples/http-client-basic/README.md +11 -0
  119. package/examples/http-client-basic/app.js +46 -0
  120. package/examples/http-client-generated/README.md +22 -0
  121. package/examples/http-client-generated/openapi.json +45 -0
  122. package/examples/http-client-resilience/README.md +4 -0
  123. package/examples/http-client-resilience/app.js +38 -0
  124. package/examples/http-client-runtime-loopback/README.md +24 -0
  125. package/examples/http-client-testhost/README.md +4 -0
  126. package/examples/http-client-testhost/app.js +27 -0
  127. package/examples/http-client-testhost-package-mock/README.md +26 -0
  128. package/examples/http-client-typed/README.md +5 -0
  129. package/examples/http-client-typed/app.js +33 -0
  130. package/examples/modules-api/README.md +30 -0
  131. package/examples/modules-api/app.js +9 -0
  132. package/examples/modules-api/modules/routes.js +16 -0
  133. package/examples/modules-api/sloppy.json +5 -0
  134. package/examples/modules-basic/README.md +32 -0
  135. package/examples/modules-basic/app.js +41 -0
  136. package/examples/net-deadline-cancel/README.md +13 -0
  137. package/examples/net-deadline-cancel/app.js +34 -0
  138. package/examples/net-local-ipc/README.md +12 -0
  139. package/examples/net-local-ipc/app.js +46 -0
  140. package/examples/net-policy-strict/README.md +12 -0
  141. package/examples/net-policy-strict/app.js +34 -0
  142. package/examples/net-tcp-client/README.md +10 -0
  143. package/examples/net-tcp-client/app.js +23 -0
  144. package/examples/net-tcp-echo/README.md +11 -0
  145. package/examples/net-tcp-echo/app.js +45 -0
  146. package/examples/net-tcp-server/README.md +10 -0
  147. package/examples/net-tcp-server/app.js +28 -0
  148. package/examples/node-compat-path-events/README.md +15 -0
  149. package/examples/node-compat-path-events/sloppy.json +6 -0
  150. package/examples/node-compat-path-events/src/main.ts +15 -0
  151. package/examples/ops-compiler/README.md +9 -0
  152. package/examples/ops-compiler/app.js +26 -0
  153. package/examples/ops-health-metrics-management/README.md +14 -0
  154. package/examples/ops-health-metrics-management/app.js +24 -0
  155. package/examples/orm-basic/README.md +17 -0
  156. package/examples/orm-basic/app.js +82 -0
  157. package/examples/orm-cursor-export/README.md +16 -0
  158. package/examples/orm-cursor-export/app.js +28 -0
  159. package/examples/orm-migrations/README.md +14 -0
  160. package/examples/orm-migrations/migrations/.gitkeep +1 -0
  161. package/examples/orm-migrations/sloppy.json +9 -0
  162. package/examples/orm-migrations/src/app.ts +34 -0
  163. package/examples/orm-relations-includes/README.md +10 -0
  164. package/examples/orm-relations-includes/app.js +47 -0
  165. package/examples/orm-testservices/README.md +37 -0
  166. package/examples/orm-testservices/test.mjs +32 -0
  167. package/examples/os-runtime-api/README.md +11 -0
  168. package/examples/os-runtime-api/app.js +44 -0
  169. package/examples/package-zod-like/README.md +28 -0
  170. package/examples/package-zod-like/fixtures/zod-like/index.js +48 -0
  171. package/examples/package-zod-like/fixtures/zod-like/package.json +12 -0
  172. package/examples/package-zod-like/package.json +7 -0
  173. package/examples/package-zod-like/sloppy.json +6 -0
  174. package/examples/package-zod-like/src/main.ts +16 -0
  175. package/examples/postgres-basic/README.md +31 -0
  176. package/examples/postgres-basic/app.js +50 -0
  177. package/examples/prealpha-control-plane/README.md +50 -0
  178. package/examples/prealpha-control-plane/appsettings.Development.json +11 -0
  179. package/examples/prealpha-control-plane/appsettings.json +15 -0
  180. package/examples/prealpha-control-plane/sloppy.json +5 -0
  181. package/examples/prealpha-control-plane/src/db/schema.js +7 -0
  182. package/examples/prealpha-control-plane/src/db/seed.js +6 -0
  183. package/examples/prealpha-control-plane/src/main.js +21 -0
  184. package/examples/prealpha-control-plane/src/routes/apps.js +34 -0
  185. package/examples/prealpha-control-plane/src/routes/builds.js +25 -0
  186. package/examples/prealpha-control-plane/src/routes/deployments.js +19 -0
  187. package/examples/prealpha-control-plane/src/routes/diagnostics.js +11 -0
  188. package/examples/prealpha-control-plane/src/routes/health.js +27 -0
  189. package/examples/prealpha-control-plane/src/routes/projects.js +38 -0
  190. package/examples/prealpha-control-plane/src/services/diagnosticsSink.js +11 -0
  191. package/examples/prealpha-control-plane/src/services/repositories.js +9 -0
  192. package/examples/prealpha-control-plane/src/validation/schemas.js +6 -0
  193. package/examples/program-fs-process/README.md +31 -0
  194. package/examples/program-fs-process/sloppy.json +9 -0
  195. package/examples/program-fs-process/src/main.ts +27 -0
  196. package/examples/program-hello/README.md +32 -0
  197. package/examples/program-hello/main.ts +8 -0
  198. package/examples/program-hello/message.ts +1 -0
  199. package/examples/program-hello/sloppy.json +5 -0
  200. package/examples/rate-limit-auth/README.md +3 -0
  201. package/examples/rate-limit-auth/app.js +14 -0
  202. package/examples/rate-limit-basic/README.md +3 -0
  203. package/examples/rate-limit-basic/app.js +13 -0
  204. package/examples/rate-limit-redis/README.md +5 -0
  205. package/examples/rate-limit-redis/app.js +20 -0
  206. package/examples/rate-limit-testhost/README.md +4 -0
  207. package/examples/rate-limit-testhost/app.js +13 -0
  208. package/examples/rate-limit-websocket/README.md +3 -0
  209. package/examples/rate-limit-websocket/app.js +16 -0
  210. package/examples/realtime-auth/README.md +8 -0
  211. package/examples/realtime-auth/app.js +25 -0
  212. package/examples/realtime-auth/test.mjs +43 -0
  213. package/examples/realtime-chat/README.md +8 -0
  214. package/examples/realtime-chat/app.js +32 -0
  215. package/examples/realtime-chat/test.mjs +52 -0
  216. package/examples/realtime-dashboard/README.md +20 -0
  217. package/examples/realtime-dashboard/app.js +37 -0
  218. package/examples/realtime-presence/README.md +8 -0
  219. package/examples/realtime-presence/app.js +32 -0
  220. package/examples/realtime-presence/test.mjs +50 -0
  221. package/examples/realtime-testhost/README.md +8 -0
  222. package/examples/realtime-testhost/test.mjs +31 -0
  223. package/examples/redis-basic/README.md +17 -0
  224. package/examples/redis-basic/app.js +39 -0
  225. package/examples/redis-cache/README.md +14 -0
  226. package/examples/redis-cache/app.js +36 -0
  227. package/examples/redis-locks/README.md +13 -0
  228. package/examples/redis-locks/app.js +49 -0
  229. package/examples/request-context/README.md +32 -0
  230. package/examples/request-context/app.js +15 -0
  231. package/examples/sqlite-basic/README.md +52 -0
  232. package/examples/sqlite-basic/app.js +56 -0
  233. package/examples/sqlserver-basic/README.md +36 -0
  234. package/examples/sqlserver-basic/app.js +59 -0
  235. package/examples/static-files-basic/README.md +11 -0
  236. package/examples/static-files-basic/app.js +12 -0
  237. package/examples/static-files-basic/public/app.js +1 -0
  238. package/examples/static-files-basic/public/site.css +3 -0
  239. package/examples/static-files-package/README.md +12 -0
  240. package/examples/static-files-package/app.js +10 -0
  241. package/examples/static-files-package/public/index.html +2 -0
  242. package/examples/static-files-precompressed/README.md +12 -0
  243. package/examples/static-files-precompressed/app.js +11 -0
  244. package/examples/static-files-precompressed/public/app.js +1 -0
  245. package/examples/static-files-precompressed/public/app.js.br +0 -0
  246. package/examples/static-files-precompressed/public/app.js.gz +0 -0
  247. package/examples/static-files-spa/README.md +12 -0
  248. package/examples/static-files-spa/app.js +16 -0
  249. package/examples/static-files-spa/dist/assets/app.js +1 -0
  250. package/examples/static-files-spa/dist/index.html +4 -0
  251. package/examples/static-files-testhost/README.md +8 -0
  252. package/examples/static-files-testhost/app.js +13 -0
  253. package/examples/static-files-testhost/public/app.js +1 -0
  254. package/examples/static-files-testhost/public/app.js.gz +0 -0
  255. package/examples/static-files-testhost/test.mjs +38 -0
  256. package/examples/testhost-basic/README.md +26 -0
  257. package/examples/testhost-db/README.md +31 -0
  258. package/examples/testservices-postgres/README.md +68 -0
  259. package/examples/testservices-redis/README.md +71 -0
  260. package/examples/testservices-sqlserver/README.md +75 -0
  261. package/examples/time-basic/README.md +18 -0
  262. package/examples/time-basic/app.js +12 -0
  263. package/examples/time-deadline-cancellation/README.md +11 -0
  264. package/examples/time-deadline-cancellation/app.js +27 -0
  265. package/examples/time-fake-clock/README.md +14 -0
  266. package/examples/time-fake-clock/app.js +25 -0
  267. package/examples/time-interval-schedule/README.md +13 -0
  268. package/examples/time-interval-schedule/app.js +60 -0
  269. package/examples/users-api-sqlite/README.md +74 -0
  270. package/examples/users-api-sqlite/app.js +11 -0
  271. package/examples/users-api-sqlite/appsettings.Development.json +11 -0
  272. package/examples/users-api-sqlite/appsettings.json +11 -0
  273. package/examples/users-api-sqlite/modules/users.js +40 -0
  274. package/examples/users-api-sqlite/sloppy.json +5 -0
  275. package/examples/validation-errors/README.md +36 -0
  276. package/examples/validation-errors/app.js +14 -0
  277. package/examples/validation-errors/invalid-user.http +6 -0
  278. package/examples/validation-errors/sloppy.json +5 -0
  279. package/examples/web-dynamic-routes/README.md +17 -0
  280. package/examples/web-dynamic-routes/app.ts +27 -0
  281. package/examples/webhooks-basic/README.md +11 -0
  282. package/examples/webhooks-basic/app.js +48 -0
  283. package/examples/websocket-auth/README.md +8 -0
  284. package/examples/websocket-auth/app.js +16 -0
  285. package/examples/websocket-echo/README.md +9 -0
  286. package/examples/websocket-echo/app.js +36 -0
  287. package/examples/websocket-json-schema/README.md +5 -0
  288. package/examples/websocket-json-schema/app.js +25 -0
  289. package/examples/websocket-testhost/README.md +11 -0
  290. package/examples/websocket-testhost/test.mjs +49 -0
  291. package/examples/workers-background-service/README.md +7 -0
  292. package/examples/workers-background-service/app.js +16 -0
  293. package/examples/workers-js-isolate/README.md +8 -0
  294. package/examples/workers-js-isolate/app.js +19 -0
  295. package/examples/workers-js-isolate/workers/parser.ts +11 -0
  296. package/examples/workers-shutdown/README.md +6 -0
  297. package/examples/workers-shutdown/app.js +26 -0
  298. package/examples/workers-workerpool/README.md +6 -0
  299. package/examples/workers-workerpool/app.js +23 -0
  300. package/examples/workers-workqueue/README.md +8 -0
  301. package/examples/workers-workqueue/app.js +24 -0
  302. package/manifest.json +59 -0
  303. package/package.json +31 -0
  304. package/stdlib/sloppy/README.md +177 -0
  305. package/stdlib/sloppy/app.js +2142 -0
  306. package/stdlib/sloppy/auth.js +1813 -0
  307. package/stdlib/sloppy/bootstrap.manifest.json +83 -0
  308. package/stdlib/sloppy/cache.js +1542 -0
  309. package/stdlib/sloppy/codec.js +1153 -0
  310. package/stdlib/sloppy/config.js +61 -0
  311. package/stdlib/sloppy/crypto.js +312 -0
  312. package/stdlib/sloppy/data.js +2945 -0
  313. package/stdlib/sloppy/ffi.js +185 -0
  314. package/stdlib/sloppy/fs.js +795 -0
  315. package/stdlib/sloppy/health.js +603 -0
  316. package/stdlib/sloppy/http.js +1595 -0
  317. package/stdlib/sloppy/index.js +59 -0
  318. package/stdlib/sloppy/internal/bytes.js +31 -0
  319. package/stdlib/sloppy/internal/capabilities.js +155 -0
  320. package/stdlib/sloppy/internal/config.js +640 -0
  321. package/stdlib/sloppy/internal/disposable.js +31 -0
  322. package/stdlib/sloppy/internal/headers.js +63 -0
  323. package/stdlib/sloppy/internal/intrinsics.js +2 -0
  324. package/stdlib/sloppy/internal/json.js +20 -0
  325. package/stdlib/sloppy/internal/logging.js +278 -0
  326. package/stdlib/sloppy/internal/modules.js +405 -0
  327. package/stdlib/sloppy/internal/redaction.js +87 -0
  328. package/stdlib/sloppy/internal/routes.js +2279 -0
  329. package/stdlib/sloppy/internal/runtime-classic.js +19837 -0
  330. package/stdlib/sloppy/internal/services.js +690 -0
  331. package/stdlib/sloppy/internal/shared.js +32 -0
  332. package/stdlib/sloppy/internal/testhost-diagnostics.js +88 -0
  333. package/stdlib/sloppy/internal/testhost-http-server.js +238 -0
  334. package/stdlib/sloppy/internal/testhost-http.js +118 -0
  335. package/stdlib/sloppy/internal/testhost-loopback.js +50 -0
  336. package/stdlib/sloppy/internal/testservices-docker.js +154 -0
  337. package/stdlib/sloppy/internal/validation.js +117 -0
  338. package/stdlib/sloppy/metrics.js +427 -0
  339. package/stdlib/sloppy/net.js +5208 -0
  340. package/stdlib/sloppy/node/assert/strict.js +39 -0
  341. package/stdlib/sloppy/node/assert.js +228 -0
  342. package/stdlib/sloppy/node/buffer.js +247 -0
  343. package/stdlib/sloppy/node/console.js +33 -0
  344. package/stdlib/sloppy/node/constants.js +9 -0
  345. package/stdlib/sloppy/node/crypto.js +89 -0
  346. package/stdlib/sloppy/node/diagnostics_channel.js +41 -0
  347. package/stdlib/sloppy/node/events.js +113 -0
  348. package/stdlib/sloppy/node/fs/promises.js +27 -0
  349. package/stdlib/sloppy/node/fs.js +280 -0
  350. package/stdlib/sloppy/node/http.js +11 -0
  351. package/stdlib/sloppy/node/https.js +11 -0
  352. package/stdlib/sloppy/node/module.js +40 -0
  353. package/stdlib/sloppy/node/os.js +22 -0
  354. package/stdlib/sloppy/node/path.js +78 -0
  355. package/stdlib/sloppy/node/perf_hooks.js +12 -0
  356. package/stdlib/sloppy/node/process.js +129 -0
  357. package/stdlib/sloppy/node/querystring.js +21 -0
  358. package/stdlib/sloppy/node/stream/promises.js +3 -0
  359. package/stdlib/sloppy/node/stream.js +132 -0
  360. package/stdlib/sloppy/node/string_decoder.js +23 -0
  361. package/stdlib/sloppy/node/timers.js +26 -0
  362. package/stdlib/sloppy/node/tty.js +18 -0
  363. package/stdlib/sloppy/node/url.js +17 -0
  364. package/stdlib/sloppy/node/util.js +95 -0
  365. package/stdlib/sloppy/node/zlib.js +72 -0
  366. package/stdlib/sloppy/orm.js +2188 -0
  367. package/stdlib/sloppy/os.js +580 -0
  368. package/stdlib/sloppy/problem-details.js +29 -0
  369. package/stdlib/sloppy/providers/sqlite.js +26 -0
  370. package/stdlib/sloppy/rate-limit.js +856 -0
  371. package/stdlib/sloppy/realtime.js +1508 -0
  372. package/stdlib/sloppy/redis.js +1272 -0
  373. package/stdlib/sloppy/request-id.js +184 -0
  374. package/stdlib/sloppy/request-logging.js +101 -0
  375. package/stdlib/sloppy/results.js +933 -0
  376. package/stdlib/sloppy/schema.js +546 -0
  377. package/stdlib/sloppy/testing.js +4081 -0
  378. package/stdlib/sloppy/testservices.js +1041 -0
  379. package/stdlib/sloppy/time.js +894 -0
  380. package/stdlib/sloppy/webhooks.js +1330 -0
  381. package/stdlib/sloppy/workers.js +986 -0
  382. package/templates/api/README.md +82 -0
  383. package/templates/api/appsettings.Development.json +14 -0
  384. package/templates/api/appsettings.json +13 -0
  385. package/templates/api/data/.gitkeep +1 -0
  386. package/templates/api/gitignore +4 -0
  387. package/templates/api/migrations/0001_create_users.sql +1 -0
  388. package/templates/api/package.json +16 -0
  389. package/templates/api/public/hello.txt +1 -0
  390. package/templates/api/sloppy.json +14 -0
  391. package/templates/api/src/config.ts +1 -0
  392. package/templates/api/src/db/migrate.ts +14 -0
  393. package/templates/api/src/db/schema.ts +4 -0
  394. package/templates/api/src/db/usersRepository.ts +23 -0
  395. package/templates/api/src/main.ts +18 -0
  396. package/templates/api/src/models/user.ts +7 -0
  397. package/templates/api/src/routes/health.ts +20 -0
  398. package/templates/api/src/routes/users.ts +40 -0
  399. package/templates/api/src/services/usersService.ts +21 -0
  400. package/templates/api/tsconfig.json +15 -0
  401. package/templates/cli/README.md +16 -0
  402. package/templates/cli/gitignore +2 -0
  403. package/templates/cli/package.json +13 -0
  404. package/templates/cli/sloppy.json +6 -0
  405. package/templates/cli/src/commands/echo.ts +9 -0
  406. package/templates/cli/src/commands/inspect.ts +20 -0
  407. package/templates/cli/src/main.ts +50 -0
  408. package/templates/cli/tsconfig.json +15 -0
  409. package/templates/minimal-api/README.md +14 -0
  410. package/templates/minimal-api/gitignore +3 -0
  411. package/templates/minimal-api/package.json +14 -0
  412. package/templates/minimal-api/sloppy.json +5 -0
  413. package/templates/minimal-api/src/main.ts +9 -0
  414. package/templates/minimal-api/tsconfig.json +15 -0
  415. package/templates/node-compat/README.md +40 -0
  416. package/templates/node-compat/gitignore +2 -0
  417. package/templates/node-compat/package.json +11 -0
  418. package/templates/node-compat/sloppy.json +6 -0
  419. package/templates/node-compat/src/main.ts +40 -0
  420. package/templates/package-api/README.md +44 -0
  421. package/templates/package-api/fixtures/validator-lite/index.js +7 -0
  422. package/templates/package-api/fixtures/validator-lite/package.json +6 -0
  423. package/templates/package-api/gitignore +3 -0
  424. package/templates/package-api/package.json +17 -0
  425. package/templates/package-api/sloppy.json +5 -0
  426. package/templates/package-api/src/main.ts +10 -0
  427. package/templates/package-api/src/routes/health.ts +5 -0
  428. package/templates/package-api/src/routes/users.ts +12 -0
  429. package/templates/package-api/tsconfig.json +15 -0
  430. package/templates/program/README.md +12 -0
  431. package/templates/program/gitignore +1 -0
  432. package/templates/program/package.json +10 -0
  433. package/templates/program/sloppy.json +6 -0
  434. package/templates/program/src/main.ts +9 -0
@@ -0,0 +1,1330 @@
1
+ import { isPlainObject } from "./internal/validation.js";
2
+ import { Hex, Text } from "./codec.js";
3
+ import { Hash, Hmac, Random, Secret } from "./crypto.js";
4
+ import { headerValue } from "./internal/headers.js";
5
+
6
+ const WEBHOOKS_TOKEN_PREFIX = "webhooks";
7
+ const DEFAULT_WEBHOOKS_TOKEN = WEBHOOKS_TOKEN_PREFIX;
8
+ const DEFAULT_BATCH_SIZE = 100;
9
+ const DEFAULT_LEASE_MS = 30000;
10
+ const DEFAULT_MAX_PAYLOAD_BYTES = 1024 * 1024;
11
+ const DEFAULT_MAX_RESPONSE_PREVIEW_BYTES = 4096;
12
+ const DEFAULT_TIMESTAMP_TOLERANCE_MS = 300000;
13
+ const EVENT_NAME_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+$/u;
14
+ const TOKEN_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/u;
15
+ const PRIVATE_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
16
+ const SENSITIVE_HEADER_PATTERN = /^(authorization|cookie|set-cookie|x-api-key|api-key|sloppy-webhook-signature)$/iu;
17
+ const TERMINAL_STATUSES = new Set(["delivered", "dead_letter"]);
18
+ const RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
19
+ let fallbackIdCounter = 0;
20
+
21
+ class SloppyWebhookError extends Error {
22
+ constructor(code, message, options = undefined) {
23
+ super(`${code}: ${message}`);
24
+ this.name = "SloppyWebhookError";
25
+ this.code = code;
26
+ if (options?.cause !== undefined) {
27
+ this.cause = options.cause;
28
+ }
29
+ if (options !== undefined) {
30
+ for (const [key, value] of Object.entries(options)) {
31
+ if (key !== "cause") {
32
+ this[key] = value;
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ function webhookError(code, message, options = undefined) {
40
+ return new SloppyWebhookError(code, message, options);
41
+ }
42
+
43
+ function stableToken(name = undefined) {
44
+ if (name === undefined || name === null || name === "") {
45
+ return DEFAULT_WEBHOOKS_TOKEN;
46
+ }
47
+ if (typeof name !== "string" || !TOKEN_NAME_PATTERN.test(name)) {
48
+ throw new TypeError("Sloppy Webhooks token name must start with a letter and contain only letters, digits, '.', '_', or '-'.");
49
+ }
50
+ return `${WEBHOOKS_TOKEN_PREFIX}.${name}`;
51
+ }
52
+
53
+ function requirePositiveInteger(value, subject, fallback = undefined) {
54
+ if (value === undefined) {
55
+ if (fallback !== undefined) {
56
+ return fallback;
57
+ }
58
+ throw new TypeError(`Sloppy Webhooks ${subject} is required.`);
59
+ }
60
+ if (!Number.isInteger(value) || value <= 0) {
61
+ throw new TypeError(`Sloppy Webhooks ${subject} must be a positive integer.`);
62
+ }
63
+ return value;
64
+ }
65
+
66
+ function requireNonNegativeInteger(value, subject, fallback = undefined) {
67
+ if (value === undefined) {
68
+ return fallback;
69
+ }
70
+ if (!Number.isInteger(value) || value < 0) {
71
+ throw new TypeError(`Sloppy Webhooks ${subject} must be a non-negative integer.`);
72
+ }
73
+ return value;
74
+ }
75
+
76
+ function validateEventName(name) {
77
+ if (typeof name !== "string" || !EVENT_NAME_PATTERN.test(name)) {
78
+ throw new TypeError("Sloppy Webhooks event name must be a stable dotted identifier such as 'order.created'.");
79
+ }
80
+ return name;
81
+ }
82
+
83
+ function isSchema(value) {
84
+ return value !== null && typeof value === "object" && typeof value.validate === "function";
85
+ }
86
+
87
+ function validateSchema(schema, subject) {
88
+ if (!isSchema(schema)) {
89
+ throw new TypeError(`Sloppy Webhooks ${subject} schema must be a Sloppy schema.`);
90
+ }
91
+ return schema;
92
+ }
93
+
94
+ function event(name, options) {
95
+ if (!isPlainObject(options)) {
96
+ throw new TypeError("Sloppy Webhooks.event options must be a plain object.");
97
+ }
98
+ const version = requirePositiveInteger(options.version, "event version");
99
+ const schema = validateSchema(options.schema, "event");
100
+ const descriptor = {
101
+ __sloppyWebhookEvent: true,
102
+ name: validateEventName(name),
103
+ version,
104
+ schema,
105
+ validate(payload) {
106
+ const result = schema.validate(payload);
107
+ if (!result.ok) {
108
+ throw webhookError(
109
+ "SLOPPY_E_WEBHOOK_EVENT_VALIDATION_FAILED",
110
+ `Webhook event '${descriptor.name}' payload failed validation.`,
111
+ { issues: result.issues },
112
+ );
113
+ }
114
+ return result.value;
115
+ },
116
+ };
117
+ return Object.freeze(descriptor);
118
+ }
119
+
120
+ function normalizeRetryStatus(values, fallback) {
121
+ const source = values ?? fallback;
122
+ if (!Array.isArray(source)) {
123
+ throw new TypeError("Sloppy Webhooks retry status list must be an array.");
124
+ }
125
+ return Object.freeze(source.map((status) => {
126
+ if (!Number.isInteger(status) || status < 100 || status > 599) {
127
+ throw new TypeError("Sloppy Webhooks retry status values must be valid HTTP statuses.");
128
+ }
129
+ return status;
130
+ }));
131
+ }
132
+
133
+ function retryFixed(options = {}) {
134
+ if (!isPlainObject(options)) {
135
+ throw new TypeError("Sloppy Webhooks.retry.fixed options must be a plain object.");
136
+ }
137
+ return Object.freeze({
138
+ kind: "fixed",
139
+ maxAttempts: requirePositiveInteger(options.maxAttempts, "retry maxAttempts", 3),
140
+ delayMs: requireNonNegativeInteger(options.delayMs, "retry delayMs", 1000),
141
+ retryOnStatus: normalizeRetryStatus(options.retryOnStatus, [408, 425, 429, 500, 502, 503, 504]),
142
+ jitter: options.jitter !== false,
143
+ });
144
+ }
145
+
146
+ function retryExponential(options = {}) {
147
+ if (!isPlainObject(options)) {
148
+ throw new TypeError("Sloppy Webhooks.retry.exponential options must be a plain object.");
149
+ }
150
+ const initialDelayMs = requireNonNegativeInteger(options.initialDelayMs, "retry initialDelayMs", 1000);
151
+ const maxDelayMs = requireNonNegativeInteger(options.maxDelayMs, "retry maxDelayMs", 300000);
152
+ if (maxDelayMs < initialDelayMs) {
153
+ throw new TypeError("Sloppy Webhooks retry maxDelayMs must be at least initialDelayMs.");
154
+ }
155
+ return Object.freeze({
156
+ kind: "exponential",
157
+ maxAttempts: requirePositiveInteger(options.maxAttempts, "retry maxAttempts", 8),
158
+ initialDelayMs,
159
+ maxDelayMs,
160
+ retryOnStatus: normalizeRetryStatus(options.retryOnStatus, [408, 425, 429, 500, 502, 503, 504]),
161
+ jitter: options.jitter !== false,
162
+ });
163
+ }
164
+
165
+ function resolveSecretValue(secret, config = undefined, subject = "secret") {
166
+ if (secret === undefined || secret === null) {
167
+ throw webhookError("SLOPPY_E_WEBHOOK_SECRET_UNAVAILABLE", `Webhook ${subject} is required.`);
168
+ }
169
+ if (typeof secret === "string") {
170
+ if (secret.length === 0) {
171
+ throw webhookError("SLOPPY_E_WEBHOOK_SECRET_UNAVAILABLE", `Webhook ${subject} must not be empty.`);
172
+ }
173
+ return secret;
174
+ }
175
+ if (secret?.__sloppyConfigReference === true && typeof secret.key === "string") {
176
+ if (config === undefined || typeof config.require !== "function") {
177
+ throw webhookError("SLOPPY_E_WEBHOOK_SECRET_UNAVAILABLE", `Webhook ${subject} requires app config.`);
178
+ }
179
+ const resolved = config.require(secret.key);
180
+ if (typeof resolved !== "string" || resolved.length === 0) {
181
+ throw webhookError("SLOPPY_E_WEBHOOK_SECRET_UNAVAILABLE", `Webhook ${subject} config value is empty.`);
182
+ }
183
+ return resolved;
184
+ }
185
+ if (typeof secret.bytes === "function") {
186
+ return secret;
187
+ }
188
+ throw new TypeError("Sloppy Webhooks secret must be a string, Secret, or Config.requiredSecret reference.");
189
+ }
190
+
191
+ function secretForHmac(secret, config, subject) {
192
+ const resolved = resolveSecretValue(secret, config, subject);
193
+ if (typeof resolved === "string") {
194
+ const owned = Secret.fromUtf8(resolved);
195
+ return Object.freeze({
196
+ secret: owned,
197
+ dispose() {
198
+ owned.dispose();
199
+ },
200
+ });
201
+ }
202
+ return Object.freeze({
203
+ secret: resolved,
204
+ dispose() {},
205
+ });
206
+ }
207
+
208
+ function redactedSecretRef(secret) {
209
+ if (typeof secret === "string") {
210
+ return "[redacted]";
211
+ }
212
+ if (secret?.__sloppyConfigReference === true) {
213
+ return `config:${secret.key}`;
214
+ }
215
+ return "[redacted]";
216
+ }
217
+
218
+ function serializeSubscriptionSecret(secret) {
219
+ if (secret === undefined) {
220
+ return null;
221
+ }
222
+ if (typeof secret === "string") {
223
+ if (secret.length === 0) {
224
+ throw webhookError("SLOPPY_E_WEBHOOK_SECRET_UNAVAILABLE", "Webhook subscription secret must not be empty.");
225
+ }
226
+ return JSON.stringify({ kind: "literal", value: secret });
227
+ }
228
+ if (secret?.__sloppyConfigReference === true && typeof secret.key === "string") {
229
+ return JSON.stringify({ kind: "config", key: secret.key });
230
+ }
231
+ throw new TypeError("Sloppy Webhooks subscription secret must be a durable string or Config.requiredSecret reference.");
232
+ }
233
+
234
+ function subscriptionSecretFromStored(value, fallback) {
235
+ if (value === undefined || value === null || value === "") {
236
+ return fallback;
237
+ }
238
+ try {
239
+ const parsed = JSON.parse(value);
240
+ if (parsed?.kind === "literal" && typeof parsed.value === "string" && parsed.value.length !== 0) {
241
+ return parsed.value;
242
+ }
243
+ if (parsed?.kind === "config" && typeof parsed.key === "string" && parsed.key.length !== 0) {
244
+ return Object.freeze({ __sloppyConfigReference: true, key: parsed.key });
245
+ }
246
+ } catch {
247
+ return fallback;
248
+ }
249
+ return fallback;
250
+ }
251
+
252
+ function validateHeaderMap(headers, subject) {
253
+ if (headers === undefined) {
254
+ return Object.freeze({});
255
+ }
256
+ if (!isPlainObject(headers)) {
257
+ throw new TypeError(`Sloppy Webhooks ${subject} headers must be a plain object.`);
258
+ }
259
+ const normalized = {};
260
+ for (const [name, value] of Object.entries(headers)) {
261
+ if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/u.test(name)) {
262
+ throw new TypeError(`Sloppy Webhooks ${subject} header names must be safe HTTP tokens.`);
263
+ }
264
+ if (SENSITIVE_HEADER_PATTERN.test(name)) {
265
+ throw new TypeError(`Sloppy Webhooks ${subject} headers must not override sensitive delivery headers.`);
266
+ }
267
+ if (typeof value !== "string" || /[\x00-\x08\x0A-\x1F\x7F]/u.test(value)) {
268
+ throw new TypeError(`Sloppy Webhooks ${subject} header values must be safe strings.`);
269
+ }
270
+ normalized[name] = value;
271
+ }
272
+ return Object.freeze(normalized);
273
+ }
274
+
275
+ function isPrivateHostname(hostname) {
276
+ const host = hostname.toLowerCase();
277
+ if (PRIVATE_HOSTS.has(host)) {
278
+ return true;
279
+ }
280
+ const bareHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
281
+ if (PRIVATE_HOSTS.has(bareHost)) {
282
+ return true;
283
+ }
284
+ if (/^10\./u.test(host) || /^192\.168\./u.test(host)) {
285
+ return true;
286
+ }
287
+ const matchLoopback = /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u.exec(host);
288
+ if (matchLoopback !== null) {
289
+ return matchLoopback.slice(1).every((part) => Number(part) <= 255);
290
+ }
291
+ const match172 = /^172\.(\d{1,3})\./u.exec(host);
292
+ if (match172 !== null) {
293
+ const part = Number(match172[1]);
294
+ return part >= 16 && part <= 31;
295
+ }
296
+ return /^169\.254\./u.test(host) ||
297
+ /^fc[0-9a-f]{2}:/u.test(bareHost) ||
298
+ /^fd[0-9a-f]{2}:/u.test(bareHost) ||
299
+ /^fe[89ab][0-9a-f]:/u.test(bareHost);
300
+ }
301
+
302
+ function validateEndpointUrl(url, options = {}) {
303
+ if (typeof url !== "string" || url.length === 0) {
304
+ throw new TypeError("Sloppy Webhooks subscription URL must be a non-empty absolute URL.");
305
+ }
306
+ let parsed;
307
+ try {
308
+ parsed = new URL(url);
309
+ } catch {
310
+ throw new TypeError("Sloppy Webhooks subscription URL must be an absolute http:// or https:// URL.");
311
+ }
312
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
313
+ throw new TypeError("Sloppy Webhooks subscription URL must use http:// or https://.");
314
+ }
315
+ if (parsed.username.length !== 0 || parsed.password.length !== 0 || parsed.hash.length !== 0) {
316
+ throw new TypeError("Sloppy Webhooks subscription URL must not include userinfo or a fragment.");
317
+ }
318
+ if (options.allowPrivateNetworks !== true && isPrivateHostname(parsed.hostname)) {
319
+ throw webhookError(
320
+ "SLOPPY_E_WEBHOOK_INVALID_OPTIONS",
321
+ "Webhook subscription URL targets a private or loopback network. Set allowPrivateNetworks explicitly for trusted local receivers.",
322
+ );
323
+ }
324
+ return parsed.toString();
325
+ }
326
+
327
+ function providerKind(db, configured = undefined) {
328
+ const debug = typeof db?.__debug === "function" ? db.__debug() : undefined;
329
+ if (configured === "test" || configured === "fake-test") {
330
+ if (debug?.kind === "fake-data-provider") {
331
+ return "sqlite";
332
+ }
333
+ throw webhookError(
334
+ "SLOPPY_E_WEBHOOK_OUTBOX_UNAVAILABLE",
335
+ "Webhook test providerKind can only be used with an explicit fake test provider.",
336
+ );
337
+ }
338
+ if (configured !== undefined) {
339
+ return configured;
340
+ }
341
+ if (debug?.kind === "fake-data-provider") {
342
+ if (debug.webhooksTestProvider === true) {
343
+ return "sqlite";
344
+ }
345
+ throw webhookError(
346
+ "SLOPPY_E_WEBHOOK_OUTBOX_UNAVAILABLE",
347
+ "Webhook outbox requires a real data provider; fake providers must opt in with providerKind: 'test'.",
348
+ );
349
+ }
350
+ if (debug?.kind === "sqlite-connection" || debug?.provider === "sqlite") {
351
+ return "sqlite";
352
+ }
353
+ if (debug?.kind === "postgres-connection" || debug?.provider === "postgres") {
354
+ return "postgres";
355
+ }
356
+ if (debug?.kind === "sqlserver-connection" || debug?.provider === "sqlserver") {
357
+ return "sqlserver";
358
+ }
359
+ if (debug?.kind === "fake-data-provider" && typeof debug.placeholderStyle === "string") {
360
+ return debug.placeholderStyle === "postgres" ? "postgres" : "sqlite";
361
+ }
362
+ throw webhookError(
363
+ "SLOPPY_E_WEBHOOK_OUTBOX_UNAVAILABLE",
364
+ "Webhook outbox requires a sqlite, postgres, or sqlserver data provider.",
365
+ );
366
+ }
367
+
368
+ const SQL = {
369
+ sqlite: Object.freeze({
370
+ placeholders: "question",
371
+ now: "datetime('now')",
372
+ createSubscriptions:
373
+ "create table if not exists sloppy_webhook_subscriptions (" +
374
+ "id text primary key, tenant_id text null, event_name text not null, endpoint_url text not null, " +
375
+ "secret_ref text null, enabled integer not null, headers_json text not null, " +
376
+ "allow_private_networks integer not null default 0, created_at text not null, updated_at text not null)",
377
+ createOutbox:
378
+ "create table if not exists sloppy_webhook_outbox (" +
379
+ "id text primary key, event_name text not null, event_version integer not null, payload_json text not null, " +
380
+ "payload_hash text not null, occurred_at text not null, available_at text not null, status text not null, " +
381
+ "attempt_count integer not null, max_attempts integer not null, next_attempt_at text null, locked_by text null, " +
382
+ "locked_until text null, idempotency_key text null, metadata_json text not null, tenant_id text null, " +
383
+ "created_at text not null, updated_at text not null)",
384
+ createAttempts:
385
+ "create table if not exists sloppy_webhook_delivery_attempts (" +
386
+ "id text primary key, outbox_id text not null, subscription_id text not null, delivery_id text not null, " +
387
+ "attempt_number integer not null, status text not null, status_code integer null, error_code text null, " +
388
+ "error_message_redacted text null, request_headers_redacted_json text not null, response_body_preview text null, " +
389
+ "duration_ms integer not null, attempted_at text not null)",
390
+ createInboundDedup:
391
+ "create table if not exists sloppy_webhook_inbound_dedup (" +
392
+ "id text primary key, provider text not null, delivery_id text not null, seen_at text not null, expires_at text not null)",
393
+ indexes: Object.freeze([
394
+ "create index if not exists idx_sloppy_webhook_subscriptions_event_enabled on sloppy_webhook_subscriptions (event_name, enabled)",
395
+ "create index if not exists idx_sloppy_webhook_outbox_pending on sloppy_webhook_outbox (status, available_at, next_attempt_at, locked_until)",
396
+ "create index if not exists idx_sloppy_webhook_attempts_outbox on sloppy_webhook_delivery_attempts (outbox_id, subscription_id, attempted_at)",
397
+ "create unique index if not exists ux_sloppy_webhook_inbound_dedup_provider_delivery on sloppy_webhook_inbound_dedup (provider, delivery_id)",
398
+ "create unique index if not exists ux_sloppy_webhook_outbox_idempotency on sloppy_webhook_outbox (idempotency_key) where idempotency_key is not null",
399
+ ]),
400
+ insertSubscription: "insert into sloppy_webhook_subscriptions (id, tenant_id, event_name, endpoint_url, secret_ref, enabled, headers_json, allow_private_networks, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
401
+ updateSubscription: "update sloppy_webhook_subscriptions set endpoint_url = ?, secret_ref = ?, headers_json = ?, allow_private_networks = ?, updated_at = ? where id = ?",
402
+ setSubscriptionEnabled: "update sloppy_webhook_subscriptions set enabled = ?, updated_at = ? where id = ?",
403
+ deleteSubscription: "delete from sloppy_webhook_subscriptions where id = ?",
404
+ getSubscription: "select id, tenant_id as tenantId, event_name as eventName, endpoint_url as endpointUrl, enabled, headers_json as headersJson, allow_private_networks as allowPrivateNetworks, created_at as createdAt, updated_at as updatedAt from sloppy_webhook_subscriptions where id = ?",
405
+ getSubscriptionSecret: "select secret_ref as secretRef from sloppy_webhook_subscriptions where id = ?",
406
+ listSubscriptions: "select id, tenant_id as tenantId, event_name as eventName, endpoint_url as endpointUrl, enabled, headers_json as headersJson, allow_private_networks as allowPrivateNetworks, created_at as createdAt, updated_at as updatedAt from sloppy_webhook_subscriptions where (? is null or event_name = ?) order by created_at, id",
407
+ insertOutbox: "insert into sloppy_webhook_outbox (id, event_name, event_version, payload_json, payload_hash, occurred_at, available_at, status, attempt_count, max_attempts, next_attempt_at, locked_by, locked_until, idempotency_key, metadata_json, tenant_id, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
408
+ selectOutboxByIdempotencyKey: "select id, event_name as eventName, event_version as eventVersion, payload_hash as payloadHash from sloppy_webhook_outbox where idempotency_key = ?",
409
+ selectPendingOutbox: "select id, event_name as eventName, event_version as eventVersion, payload_json as payloadJson, payload_hash as payloadHash, status, attempt_count as attemptCount, max_attempts as maxAttempts, tenant_id as tenantId from sloppy_webhook_outbox where status in ('pending', 'failed') and available_at <= ? and (next_attempt_at is null or next_attempt_at <= ?) and (locked_until is null or locked_until <= ?) order by available_at, id limit ?",
410
+ claimOutbox: "update sloppy_webhook_outbox set status = 'delivering', locked_by = ?, locked_until = ?, updated_at = ? where id = ? and status in ('pending', 'failed') and (locked_until is null or locked_until <= ?)",
411
+ selectClaimedOutbox: "select id, event_name as eventName, event_version as eventVersion, payload_json as payloadJson, payload_hash as payloadHash, status, attempt_count as attemptCount, max_attempts as maxAttempts, tenant_id as tenantId from sloppy_webhook_outbox where id = ? and locked_by = ? and status = 'delivering'",
412
+ subscriptionsForEvent: "select id, tenant_id as tenantId, event_name as eventName, endpoint_url as endpointUrl, secret_ref as secretRef, enabled, headers_json as headersJson from sloppy_webhook_subscriptions where event_name = ? and enabled = 1",
413
+ insertAttempt: "insert into sloppy_webhook_delivery_attempts (id, outbox_id, subscription_id, delivery_id, attempt_number, status, status_code, error_code, error_message_redacted, request_headers_redacted_json, response_body_preview, duration_ms, attempted_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
414
+ deliveredSubscriptionsForOutbox: "select distinct subscription_id as subscriptionId from sloppy_webhook_delivery_attempts where outbox_id = ? and status = 'delivered'",
415
+ updateOutboxDelivered: "update sloppy_webhook_outbox set status = ?, attempt_count = ?, locked_by = null, locked_until = null, next_attempt_at = ?, updated_at = ? where id = ?",
416
+ insertInboundDedup: "insert into sloppy_webhook_inbound_dedup (id, provider, delivery_id, seen_at, expires_at) values (?, ?, ?, ?, ?)",
417
+ }),
418
+ };
419
+
420
+ SQL.postgres = Object.freeze({
421
+ ...SQL.sqlite,
422
+ placeholders: "postgres",
423
+ createSubscriptions:
424
+ "create table if not exists sloppy_webhook_subscriptions (" +
425
+ "id text primary key, tenant_id text null, event_name text not null, endpoint_url text not null, " +
426
+ "secret_ref text null, enabled boolean not null, headers_json text not null, " +
427
+ "allow_private_networks boolean not null default false, created_at text not null, updated_at text not null)",
428
+ insertSubscription: "insert into sloppy_webhook_subscriptions (id, tenant_id, event_name, endpoint_url, secret_ref, enabled, headers_json, allow_private_networks, created_at, updated_at) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
429
+ updateSubscription: "update sloppy_webhook_subscriptions set endpoint_url = $1, secret_ref = $2, headers_json = $3, allow_private_networks = $4, updated_at = $5 where id = $6",
430
+ setSubscriptionEnabled: "update sloppy_webhook_subscriptions set enabled = $1, updated_at = $2 where id = $3",
431
+ deleteSubscription: "delete from sloppy_webhook_subscriptions where id = $1",
432
+ getSubscription: "select id, tenant_id as \"tenantId\", event_name as \"eventName\", endpoint_url as \"endpointUrl\", enabled, headers_json as \"headersJson\", allow_private_networks as \"allowPrivateNetworks\", created_at as \"createdAt\", updated_at as \"updatedAt\" from sloppy_webhook_subscriptions where id = $1",
433
+ getSubscriptionSecret: "select secret_ref as \"secretRef\" from sloppy_webhook_subscriptions where id = $1",
434
+ listSubscriptions: "select id, tenant_id as \"tenantId\", event_name as \"eventName\", endpoint_url as \"endpointUrl\", enabled, headers_json as \"headersJson\", allow_private_networks as \"allowPrivateNetworks\", created_at as \"createdAt\", updated_at as \"updatedAt\" from sloppy_webhook_subscriptions where ($1 is null or event_name = $2) order by created_at, id",
435
+ insertOutbox: "insert into sloppy_webhook_outbox (id, event_name, event_version, payload_json, payload_hash, occurred_at, available_at, status, attempt_count, max_attempts, next_attempt_at, locked_by, locked_until, idempotency_key, metadata_json, tenant_id, created_at, updated_at) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)",
436
+ selectOutboxByIdempotencyKey: "select id, event_name as \"eventName\", event_version as \"eventVersion\", payload_hash as \"payloadHash\" from sloppy_webhook_outbox where idempotency_key = $1",
437
+ selectPendingOutbox: "select id, event_name as \"eventName\", event_version as \"eventVersion\", payload_json as \"payloadJson\", payload_hash as \"payloadHash\", status, attempt_count as \"attemptCount\", max_attempts as \"maxAttempts\", tenant_id as \"tenantId\" from sloppy_webhook_outbox where status in ('pending', 'failed') and available_at <= $1 and (next_attempt_at is null or next_attempt_at <= $2) and (locked_until is null or locked_until <= $3) order by available_at, id limit $4",
438
+ claimOutbox: "update sloppy_webhook_outbox set status = 'delivering', locked_by = $1, locked_until = $2, updated_at = $3 where id = $4 and status in ('pending', 'failed') and (locked_until is null or locked_until <= $5)",
439
+ selectClaimedOutbox: "select id, event_name as \"eventName\", event_version as \"eventVersion\", payload_json as \"payloadJson\", payload_hash as \"payloadHash\", status, attempt_count as \"attemptCount\", max_attempts as \"maxAttempts\", tenant_id as \"tenantId\" from sloppy_webhook_outbox where id = $1 and locked_by = $2 and status = 'delivering'",
440
+ subscriptionsForEvent: "select id, tenant_id as \"tenantId\", event_name as \"eventName\", endpoint_url as \"endpointUrl\", secret_ref as \"secretRef\", enabled, headers_json as \"headersJson\" from sloppy_webhook_subscriptions where event_name = $1 and enabled = true",
441
+ insertAttempt: "insert into sloppy_webhook_delivery_attempts (id, outbox_id, subscription_id, delivery_id, attempt_number, status, status_code, error_code, error_message_redacted, request_headers_redacted_json, response_body_preview, duration_ms, attempted_at) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)",
442
+ deliveredSubscriptionsForOutbox: "select distinct subscription_id as \"subscriptionId\" from sloppy_webhook_delivery_attempts where outbox_id = $1 and status = 'delivered'",
443
+ updateOutboxDelivered: "update sloppy_webhook_outbox set status = $1, attempt_count = $2, locked_by = null, locked_until = null, next_attempt_at = $3, updated_at = $4 where id = $5",
444
+ insertInboundDedup: "insert into sloppy_webhook_inbound_dedup (id, provider, delivery_id, seen_at, expires_at) values ($1, $2, $3, $4, $5)",
445
+ });
446
+
447
+ SQL.sqlserver = Object.freeze({
448
+ ...SQL.sqlite,
449
+ placeholders: "question",
450
+ createSubscriptions:
451
+ "if object_id(N'dbo.sloppy_webhook_subscriptions', N'U') is null create table dbo.sloppy_webhook_subscriptions (" +
452
+ "id nvarchar(96) not null primary key, tenant_id nvarchar(128) null, event_name nvarchar(256) not null, " +
453
+ "endpoint_url nvarchar(2048) not null, secret_ref nvarchar(max) null, enabled bit not null, " +
454
+ "headers_json nvarchar(max) not null, allow_private_networks bit not null default 0, " +
455
+ "created_at nvarchar(64) not null, updated_at nvarchar(64) not null)",
456
+ createOutbox:
457
+ "if object_id(N'dbo.sloppy_webhook_outbox', N'U') is null create table dbo.sloppy_webhook_outbox (" +
458
+ "id nvarchar(96) not null primary key, event_name nvarchar(256) not null, event_version int not null, " +
459
+ "payload_json nvarchar(max) not null, payload_hash nvarchar(128) not null, occurred_at nvarchar(64) not null, " +
460
+ "available_at nvarchar(64) not null, status nvarchar(32) not null, attempt_count int not null, " +
461
+ "max_attempts int not null, next_attempt_at nvarchar(64) null, locked_by nvarchar(128) null, " +
462
+ "locked_until nvarchar(64) null, idempotency_key nvarchar(256) null, metadata_json nvarchar(max) not null, " +
463
+ "tenant_id nvarchar(128) null, created_at nvarchar(64) not null, updated_at nvarchar(64) not null)",
464
+ createAttempts:
465
+ "if object_id(N'dbo.sloppy_webhook_delivery_attempts', N'U') is null create table dbo.sloppy_webhook_delivery_attempts (" +
466
+ "id nvarchar(96) not null primary key, outbox_id nvarchar(96) not null, subscription_id nvarchar(96) not null, " +
467
+ "delivery_id nvarchar(96) not null, attempt_number int not null, status nvarchar(32) not null, " +
468
+ "status_code int null, error_code nvarchar(128) null, error_message_redacted nvarchar(2048) null, " +
469
+ "request_headers_redacted_json nvarchar(max) not null, response_body_preview nvarchar(4096) null, " +
470
+ "duration_ms int not null, attempted_at nvarchar(64) not null)",
471
+ createInboundDedup:
472
+ "if object_id(N'dbo.sloppy_webhook_inbound_dedup', N'U') is null create table dbo.sloppy_webhook_inbound_dedup (" +
473
+ "id nvarchar(96) not null primary key, provider nvarchar(128) not null, delivery_id nvarchar(128) not null, " +
474
+ "seen_at nvarchar(64) not null, expires_at nvarchar(64) not null)",
475
+ indexes: Object.freeze([
476
+ "if not exists (select 1 from sys.indexes where name = N'idx_sloppy_webhook_subscriptions_event_enabled' and object_id = object_id(N'dbo.sloppy_webhook_subscriptions')) create index idx_sloppy_webhook_subscriptions_event_enabled on dbo.sloppy_webhook_subscriptions (event_name, enabled)",
477
+ "if not exists (select 1 from sys.indexes where name = N'idx_sloppy_webhook_outbox_pending' and object_id = object_id(N'dbo.sloppy_webhook_outbox')) create index idx_sloppy_webhook_outbox_pending on dbo.sloppy_webhook_outbox (status, available_at, next_attempt_at, locked_until)",
478
+ "if not exists (select 1 from sys.indexes where name = N'idx_sloppy_webhook_attempts_outbox' and object_id = object_id(N'dbo.sloppy_webhook_delivery_attempts')) create index idx_sloppy_webhook_attempts_outbox on dbo.sloppy_webhook_delivery_attempts (outbox_id, subscription_id, attempted_at)",
479
+ "if not exists (select 1 from sys.indexes where name = N'ux_sloppy_webhook_inbound_dedup_provider_delivery' and object_id = object_id(N'dbo.sloppy_webhook_inbound_dedup')) create unique index ux_sloppy_webhook_inbound_dedup_provider_delivery on dbo.sloppy_webhook_inbound_dedup (provider, delivery_id)",
480
+ "if not exists (select 1 from sys.indexes where name = N'ux_sloppy_webhook_outbox_idempotency' and object_id = object_id(N'dbo.sloppy_webhook_outbox')) create unique index ux_sloppy_webhook_outbox_idempotency on dbo.sloppy_webhook_outbox (idempotency_key) where idempotency_key is not null",
481
+ ]),
482
+ });
483
+
484
+ Object.freeze(SQL);
485
+
486
+ async function ensureSchema(db, kind) {
487
+ const sql = SQL[kind];
488
+ await db.exec(sql.createSubscriptions, []);
489
+ await db.exec(sql.createOutbox, []);
490
+ await db.exec(sql.createAttempts, []);
491
+ await db.exec(sql.createInboundDedup, []);
492
+ for (const indexSql of sql.indexes) {
493
+ await db.exec(indexSql, []);
494
+ }
495
+ }
496
+
497
+ function nowIso(clock = undefined) {
498
+ if (typeof clock?.now === "function") {
499
+ const value = clock.now();
500
+ return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
501
+ }
502
+ return new Date().toISOString();
503
+ }
504
+
505
+ function addMs(iso, ms) {
506
+ return new Date(new Date(iso).getTime() + ms).toISOString();
507
+ }
508
+
509
+ function randomId(prefix) {
510
+ try {
511
+ return `${prefix}_${Random.uuid()}`;
512
+ } catch {
513
+ fallbackIdCounter += 1;
514
+ return `${prefix}_${Date.now().toString(36)}_${fallbackIdCounter.toString(36)}`;
515
+ }
516
+ }
517
+
518
+ async function sha256Hex(value) {
519
+ return Hash.sha256Hex(value);
520
+ }
521
+
522
+ function assertSerializable(payload) {
523
+ let text;
524
+ try {
525
+ text = JSON.stringify(payload);
526
+ } catch (error) {
527
+ throw webhookError("SLOPPY_E_WEBHOOK_EVENT_VALIDATION_FAILED", "Webhook payload must be JSON-serializable.", { cause: error });
528
+ }
529
+ if (text === undefined) {
530
+ throw webhookError("SLOPPY_E_WEBHOOK_EVENT_VALIDATION_FAILED", "Webhook payload must be JSON-serializable.");
531
+ }
532
+ if (Text.utf8.encode(text).byteLength > DEFAULT_MAX_PAYLOAD_BYTES) {
533
+ throw webhookError("SLOPPY_E_WEBHOOK_EVENT_VALIDATION_FAILED", "Webhook payload exceeds the maximum supported size.");
534
+ }
535
+ return text;
536
+ }
537
+
538
+ function parseHeadersJson(value) {
539
+ if (typeof value !== "string" || value.length === 0) {
540
+ return Object.freeze({});
541
+ }
542
+ try {
543
+ const parsed = JSON.parse(value);
544
+ return isPlainObject(parsed) ? Object.freeze(parsed) : Object.freeze({});
545
+ } catch {
546
+ return Object.freeze({});
547
+ }
548
+ }
549
+
550
+ function subscriptionFromRow(row) {
551
+ if (row === null || row === undefined) {
552
+ return null;
553
+ }
554
+ return Object.freeze({
555
+ id: row.id,
556
+ tenantId: row.tenantId ?? null,
557
+ event: row.eventName,
558
+ url: row.endpointUrl,
559
+ enabled: row.enabled === true || row.enabled === 1,
560
+ headers: parseHeadersJson(row.headersJson),
561
+ allowPrivateNetworks: row.allowPrivateNetworks === true || row.allowPrivateNetworks === 1,
562
+ createdAt: row.createdAt,
563
+ updatedAt: row.updatedAt,
564
+ });
565
+ }
566
+
567
+ function retryDelayMs(policy, attemptNumber, response = undefined) {
568
+ const retryAfter = response?.headers?.get?.("retry-after");
569
+ if (retryAfter !== undefined && retryAfter !== null) {
570
+ const seconds = Number(retryAfter);
571
+ if (Number.isFinite(seconds) && seconds >= 0) {
572
+ return seconds * 1000;
573
+ }
574
+ const dateMs = Date.parse(retryAfter);
575
+ if (Number.isFinite(dateMs)) {
576
+ return Math.max(0, dateMs - Date.now());
577
+ }
578
+ }
579
+ if (policy.kind === "fixed") {
580
+ return policy.jitter ? randomDelayMs(policy.delayMs) : policy.delayMs;
581
+ }
582
+ const base = Math.min(policy.maxDelayMs, policy.initialDelayMs * (2 ** Math.max(0, attemptNumber - 1)));
583
+ return policy.jitter ? randomDelayMs(base) : base;
584
+ }
585
+
586
+ function randomDelayMs(maxInclusive) {
587
+ if (maxInclusive <= 0) {
588
+ return 0;
589
+ }
590
+ const bytes = Random.bytes(4);
591
+ const value = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
592
+ return (value >>> 0) % (maxInclusive + 1);
593
+ }
594
+
595
+ function shouldRetryStatus(policy, status) {
596
+ return policy.retryOnStatus.includes(status);
597
+ }
598
+
599
+ function redactHeaders(headers) {
600
+ const redacted = {};
601
+ for (const [name, value] of Object.entries(headers)) {
602
+ redacted[name] = SENSITIVE_HEADER_PATTERN.test(name) ? "[REDACTED]" : value;
603
+ }
604
+ return Object.freeze(redacted);
605
+ }
606
+
607
+ function safeEndpointHost(url) {
608
+ try {
609
+ return new URL(url).host;
610
+ } catch {
611
+ return "invalid";
612
+ }
613
+ }
614
+
615
+ async function sign(payload, options) {
616
+ if (!isPlainObject(options)) {
617
+ throw new TypeError("Sloppy Webhooks.sign options must be a plain object.");
618
+ }
619
+ const timestamp = String(options.timestamp ?? Math.floor(Date.now() / 1000));
620
+ const id = typeof options.id === "string" && options.id.length > 0 ? options.id : randomId("whdel");
621
+ const eventName = validateEventName(options.event ?? options.eventName);
622
+ const attempt = requirePositiveInteger(options.attempt ?? 1, "signature attempt");
623
+ const body = typeof payload === "string" ? payload : assertSerializable(payload);
624
+ const signingInput = webhookSigningInput(timestamp, Text.utf8.encode(body));
625
+ const prepared = secretForHmac(options.secret, options.config, "signing secret");
626
+ try {
627
+ const digest = await Hmac.sha256(prepared.secret, signingInput);
628
+ const signature = `v1=${Hex.encode(digest)}`;
629
+ return Object.freeze({
630
+ id,
631
+ event: eventName,
632
+ timestamp,
633
+ attempt,
634
+ signature,
635
+ headers: Object.freeze({
636
+ "Sloppy-Webhook-Id": id,
637
+ "Sloppy-Webhook-Event": eventName,
638
+ "Sloppy-Webhook-Timestamp": timestamp,
639
+ "Sloppy-Webhook-Signature": signature,
640
+ "Sloppy-Webhook-Attempt": String(attempt),
641
+ }),
642
+ });
643
+ } finally {
644
+ prepared.dispose();
645
+ }
646
+ }
647
+
648
+ async function requestBodyBytes(ctxOrRequest) {
649
+ const request = ctxOrRequest?.request ?? ctxOrRequest;
650
+ if (typeof request?.bytes === "function") {
651
+ return request.bytes();
652
+ }
653
+ if (typeof request?.text === "function") {
654
+ return Text.utf8.encode(await request.text());
655
+ }
656
+ if (typeof ctxOrRequest?.bytes === "function") {
657
+ return ctxOrRequest.bytes();
658
+ }
659
+ throw new TypeError("Sloppy Webhooks.verify requires a request or context with request bytes.");
660
+ }
661
+
662
+ function parseSignatureHeader(value) {
663
+ if (typeof value !== "string" || value.length === 0) {
664
+ return [];
665
+ }
666
+ return value.split(",")
667
+ .map((part) => part.trim())
668
+ .filter((part) => part.startsWith("v1="))
669
+ .map((part) => part.slice(3).toLowerCase());
670
+ }
671
+
672
+ function constantTimeHexEquals(left, right) {
673
+ if (left.length !== right.length) {
674
+ return false;
675
+ }
676
+ let diff = 0;
677
+ for (let index = 0; index < left.length; index += 1) {
678
+ diff |= left.charCodeAt(index) ^ right.charCodeAt(index);
679
+ }
680
+ return diff === 0;
681
+ }
682
+
683
+ function webhookSigningInput(timestamp, bodyBytes) {
684
+ const prefix = Text.utf8.encode(`${timestamp}.`);
685
+ const bytes = new Uint8Array(prefix.byteLength + bodyBytes.byteLength);
686
+ bytes.set(prefix, 0);
687
+ bytes.set(bodyBytes, prefix.byteLength);
688
+ return bytes;
689
+ }
690
+
691
+ async function verify(ctxOrRequest, options) {
692
+ if (!isPlainObject(options)) {
693
+ throw new TypeError("Sloppy Webhooks.verify options must be a plain object.");
694
+ }
695
+ const request = ctxOrRequest?.request ?? ctxOrRequest;
696
+ const headers = request?.headers;
697
+ const timestamp = String(headerValue(headers, options.timestampHeader ?? "Sloppy-Webhook-Timestamp") ?? "");
698
+ const signatureHeader = String(headerValue(headers, options.signatureHeader ?? "Sloppy-Webhook-Signature") ?? "");
699
+ const deliveryId = String(headerValue(headers, options.idHeader ?? "Sloppy-Webhook-Id") ?? "");
700
+ const eventName = String(headerValue(headers, options.eventHeader ?? "Sloppy-Webhook-Event") ?? "");
701
+ if (timestamp.length === 0 || signatureHeader.length === 0) {
702
+ throw webhookError("SLOPPY_E_WEBHOOK_SIGNATURE_INVALID", "Webhook signature headers are missing.");
703
+ }
704
+ const timestampMs = Number(timestamp) * 1000;
705
+ if (!Number.isFinite(timestampMs)) {
706
+ throw webhookError("SLOPPY_E_WEBHOOK_TIMESTAMP_OUT_OF_RANGE", "Webhook timestamp is invalid.");
707
+ }
708
+ const toleranceMs = requireNonNegativeInteger(options.toleranceMs, "timestamp toleranceMs", DEFAULT_TIMESTAMP_TOLERANCE_MS);
709
+ const now = options.nowMs ?? Date.now();
710
+ if (Math.abs(now - timestampMs) > toleranceMs) {
711
+ throw webhookError("SLOPPY_E_WEBHOOK_TIMESTAMP_OUT_OF_RANGE", "Webhook timestamp is outside the accepted tolerance.");
712
+ }
713
+ const body = await requestBodyBytes(ctxOrRequest);
714
+ if (body.byteLength > (options.maxBodyBytes ?? DEFAULT_MAX_PAYLOAD_BYTES)) {
715
+ throw webhookError("SLOPPY_E_WEBHOOK_EVENT_VALIDATION_FAILED", "Webhook body exceeds the configured limit.");
716
+ }
717
+ const bodyText = Text.utf8.decode(body);
718
+ const signingInput = webhookSigningInput(timestamp, body);
719
+ const secrets = Array.isArray(options.secrets) ? options.secrets : [options.secret];
720
+ const expected = [];
721
+ for (const secret of secrets) {
722
+ const prepared = secretForHmac(secret, options.config ?? ctxOrRequest?.config, "verification secret");
723
+ try {
724
+ const digest = await Hmac.sha256(prepared.secret, signingInput);
725
+ expected.push(Hex.encode(digest).toLowerCase());
726
+ } finally {
727
+ prepared.dispose();
728
+ }
729
+ }
730
+ const provided = parseSignatureHeader(signatureHeader);
731
+ const ok = provided.some((candidate) => expected.some((digest) => constantTimeHexEquals(candidate, digest)));
732
+ if (!ok) {
733
+ throw webhookError("SLOPPY_E_WEBHOOK_SIGNATURE_INVALID", "Webhook signature is invalid.");
734
+ }
735
+ if (options.dedupe !== undefined && deliveryId.length > 0) {
736
+ if (typeof options.dedupe.seen !== "function" || typeof options.dedupe.mark !== "function") {
737
+ throw new TypeError("Sloppy Webhooks.verify dedupe must expose seen() and mark().");
738
+ }
739
+ if (await options.dedupe.seen(deliveryId, options.provider ?? "sloppy")) {
740
+ throw webhookError("SLOPPY_E_WEBHOOK_REPLAY_DETECTED", "Webhook delivery was already processed.");
741
+ }
742
+ }
743
+ let payload;
744
+ try {
745
+ payload = bodyText.length === 0 ? null : JSON.parse(bodyText);
746
+ } catch {
747
+ payload = bodyText;
748
+ }
749
+ if (options.event !== undefined) {
750
+ options.event.validate(payload);
751
+ }
752
+ if (options.dedupe !== undefined && deliveryId.length > 0) {
753
+ await options.dedupe.mark(deliveryId, options.provider ?? "sloppy", {
754
+ timestamp,
755
+ expiresAt: new Date(timestampMs + toleranceMs).toISOString(),
756
+ });
757
+ }
758
+ return Object.freeze({
759
+ id: deliveryId,
760
+ event: eventName,
761
+ timestamp,
762
+ payload,
763
+ body: bodyText,
764
+ headers,
765
+ });
766
+ }
767
+
768
+ function normalizeOutboxOptions(options) {
769
+ if (!isPlainObject(options)) {
770
+ throw new TypeError("Sloppy Webhooks.outbox options must be a plain object.");
771
+ }
772
+ if (options.signingSecret === undefined || (typeof options.signingSecret === "string" && options.signingSecret.length === 0)) {
773
+ throw new TypeError("Sloppy Webhooks.outbox signingSecret is required and must not be empty.");
774
+ }
775
+ const provider = typeof options.provider === "string" && options.provider.length > 0
776
+ ? options.provider
777
+ : "main";
778
+ const client = typeof options.delivery?.client === "string" && options.delivery.client.length > 0
779
+ ? options.delivery.client
780
+ : "webhooks";
781
+ const retry = options.delivery?.retry === undefined
782
+ ? retryExponential()
783
+ : normalizeRetryPolicy(options.delivery.retry);
784
+ return Object.freeze({
785
+ token: stableToken(options.name),
786
+ provider,
787
+ providerKind: options.providerKind,
788
+ signingSecret: options.signingSecret,
789
+ delivery: Object.freeze({
790
+ client,
791
+ retry,
792
+ leaseMs: requirePositiveInteger(options.delivery?.leaseMs, "delivery leaseMs", DEFAULT_LEASE_MS),
793
+ batchSize: requirePositiveInteger(options.delivery?.batchSize, "delivery batchSize", DEFAULT_BATCH_SIZE),
794
+ maxResponsePreviewBytes: requirePositiveInteger(
795
+ options.delivery?.maxResponsePreviewBytes,
796
+ "delivery maxResponsePreviewBytes",
797
+ DEFAULT_MAX_RESPONSE_PREVIEW_BYTES,
798
+ ),
799
+ }),
800
+ retention: Object.freeze({
801
+ deadLetterAfterDays: requirePositiveInteger(options.retention?.deadLetterAfterDays, "retention deadLetterAfterDays", 30),
802
+ }),
803
+ });
804
+ }
805
+
806
+ function normalizeRetryPolicy(policy) {
807
+ if (!isPlainObject(policy) || (policy.kind !== "fixed" && policy.kind !== "exponential")) {
808
+ throw new TypeError("Sloppy Webhooks retry policy must come from Webhooks.retry.");
809
+ }
810
+ return policy;
811
+ }
812
+
813
+ function serviceProviderToken(provider) {
814
+ return provider.includes(".") ? provider : `data.${provider}`;
815
+ }
816
+
817
+ function httpClientToken(client) {
818
+ return client.includes(".") ? client : `http.${client}`;
819
+ }
820
+
821
+ function createWebhooksService(config, scope) {
822
+ let initialized = false;
823
+ let closed = false;
824
+ let cachedDb;
825
+ let cachedKind;
826
+
827
+ function assertOpen() {
828
+ if (closed) {
829
+ throw webhookError("SLOPPY_E_WEBHOOK_CLOSED", "Webhook service is closed.");
830
+ }
831
+ }
832
+
833
+ function db() {
834
+ assertOpen();
835
+ if (cachedDb === undefined) {
836
+ cachedDb = scope.get(serviceProviderToken(config.provider));
837
+ cachedKind = providerKind(cachedDb, config.providerKind);
838
+ }
839
+ return cachedDb;
840
+ }
841
+
842
+ async function init() {
843
+ assertOpen();
844
+ const connection = db();
845
+ if (!initialized) {
846
+ await ensureSchema(connection, cachedKind);
847
+ initialized = true;
848
+ }
849
+ return service;
850
+ }
851
+
852
+ async function createSubscription(options) {
853
+ assertOpen();
854
+ if (!isPlainObject(options)) {
855
+ throw new TypeError("Sloppy Webhooks subscription options must be a plain object.");
856
+ }
857
+ await init();
858
+ const eventName = typeof options.event === "string" ? validateEventName(options.event) : validateEventName(options.event?.name);
859
+ const url = validateEndpointUrl(options.url, options);
860
+ const headers = validateHeaderMap(options.headers, "subscription");
861
+ const id = options.id ?? randomId("whsub");
862
+ const time = nowIso(options.clock);
863
+ const secretRef = serializeSubscriptionSecret(options.secret);
864
+ await db().exec(SQL[cachedKind].insertSubscription, [
865
+ id,
866
+ options.tenantId ?? null,
867
+ eventName,
868
+ url,
869
+ secretRef,
870
+ cachedKind === "postgres" ? options.enabled !== false : options.enabled === false ? 0 : 1,
871
+ JSON.stringify(headers),
872
+ cachedKind === "postgres" ? options.allowPrivateNetworks === true : options.allowPrivateNetworks === true ? 1 : 0,
873
+ time,
874
+ time,
875
+ ]);
876
+ return subscriptionFromRow({
877
+ id,
878
+ tenantId: options.tenantId ?? null,
879
+ eventName,
880
+ endpointUrl: url,
881
+ enabled: options.enabled === false ? 0 : 1,
882
+ headersJson: JSON.stringify(headers),
883
+ allowPrivateNetworks: options.allowPrivateNetworks === true ? 1 : 0,
884
+ createdAt: time,
885
+ updatedAt: time,
886
+ });
887
+ }
888
+
889
+ async function listSubscriptions(options = {}) {
890
+ assertOpen();
891
+ await init();
892
+ const eventFilter = options.event === undefined ? null : validateEventName(typeof options.event === "string" ? options.event : options.event.name);
893
+ const rows = await db().query(SQL[cachedKind].listSubscriptions, [eventFilter, eventFilter]);
894
+ return Object.freeze(rows.map(subscriptionFromRow));
895
+ }
896
+
897
+ async function getSubscription(id) {
898
+ assertOpen();
899
+ await init();
900
+ const row = await db().queryOne(SQL[cachedKind].getSubscription, [id]);
901
+ return subscriptionFromRow(row);
902
+ }
903
+
904
+ async function updateSubscription(id, options) {
905
+ assertOpen();
906
+ if (!isPlainObject(options)) {
907
+ throw new TypeError("Sloppy Webhooks subscription update options must be a plain object.");
908
+ }
909
+ await init();
910
+ const current = await getSubscription(id);
911
+ if (current === null) {
912
+ return null;
913
+ }
914
+ const url = options.url === undefined ? current.url : validateEndpointUrl(options.url, options);
915
+ const headers = options.headers === undefined ? current.headers : validateHeaderMap(options.headers, "subscription");
916
+ const currentSecret = await db().queryOne(SQL[cachedKind].getSubscriptionSecret, [id]);
917
+ const secretRef = options.secret === undefined ? currentSecret?.secretRef ?? null : serializeSubscriptionSecret(options.secret);
918
+ const allowPrivateNetworks = options.allowPrivateNetworks === undefined
919
+ ? current.allowPrivateNetworks
920
+ : options.allowPrivateNetworks === true;
921
+ const time = nowIso(options.clock);
922
+ await db().exec(SQL[cachedKind].updateSubscription, [
923
+ url,
924
+ secretRef,
925
+ JSON.stringify(headers),
926
+ cachedKind === "postgres" ? allowPrivateNetworks : allowPrivateNetworks ? 1 : 0,
927
+ time,
928
+ id,
929
+ ]);
930
+ return getSubscription(id);
931
+ }
932
+
933
+ async function setEnabled(id, enabled) {
934
+ assertOpen();
935
+ await init();
936
+ await db().exec(SQL[cachedKind].setSubscriptionEnabled, [cachedKind === "postgres" ? enabled : enabled ? 1 : 0, nowIso(), id]);
937
+ return getSubscription(id);
938
+ }
939
+
940
+ async function deleteSubscription(id) {
941
+ assertOpen();
942
+ await init();
943
+ await db().exec(SQL[cachedKind].deleteSubscription, [id]);
944
+ return true;
945
+ }
946
+
947
+ async function publish(dbOrTx, eventDescriptor, payload, options = {}) {
948
+ assertOpen();
949
+ if (dbOrTx === undefined || dbOrTx === null || typeof dbOrTx.exec !== "function") {
950
+ throw webhookError("SLOPPY_E_WEBHOOK_OUTBOX_UNAVAILABLE", "Webhook publish requires a data provider or transaction.");
951
+ }
952
+ await init();
953
+ if (eventDescriptor?.__sloppyWebhookEvent !== true) {
954
+ throw new TypeError("Sloppy Webhooks.publish event must come from Webhooks.event.");
955
+ }
956
+ const value = eventDescriptor.validate(payload);
957
+ const payloadJson = assertSerializable(value);
958
+ const time = nowIso(options.clock);
959
+ const availableAt = options.delayMs === undefined ? time : addMs(time, requireNonNegativeInteger(options.delayMs, "publish delayMs"));
960
+ const idempotencyKey = options.idempotencyKey ?? null;
961
+ if (idempotencyKey !== null) {
962
+ if (typeof idempotencyKey !== "string" || idempotencyKey.length === 0) {
963
+ throw new TypeError("Sloppy Webhooks publish idempotencyKey must be a non-empty string.");
964
+ }
965
+ const existing = await dbOrTx.queryOne?.(SQL[providerKind(db(), config.providerKind)].selectOutboxByIdempotencyKey, [idempotencyKey]);
966
+ if (existing !== undefined && existing !== null) {
967
+ return Object.freeze({
968
+ id: existing.id,
969
+ event: existing.eventName,
970
+ version: existing.eventVersion,
971
+ payloadHash: existing.payloadHash,
972
+ idempotent: true,
973
+ });
974
+ }
975
+ }
976
+ const id = options.id ?? randomId("whevt");
977
+ const retry = options.retry === undefined ? config.delivery.retry : normalizeRetryPolicy(options.retry);
978
+ const payloadHash = await sha256Hex(payloadJson);
979
+ await dbOrTx.exec(SQL[providerKind(db(), config.providerKind)].insertOutbox, [
980
+ id,
981
+ eventDescriptor.name,
982
+ eventDescriptor.version,
983
+ payloadJson,
984
+ payloadHash,
985
+ time,
986
+ availableAt,
987
+ "pending",
988
+ 0,
989
+ retry.maxAttempts,
990
+ null,
991
+ null,
992
+ null,
993
+ idempotencyKey,
994
+ JSON.stringify(options.metadata ?? {}),
995
+ options.tenantId ?? null,
996
+ time,
997
+ time,
998
+ ]);
999
+ return Object.freeze({ id, event: eventDescriptor.name, version: eventDescriptor.version, payloadHash, idempotent: false });
1000
+ }
1001
+
1002
+ async function recordAttempt(row, subscription, attemptNumber, result) {
1003
+ const time = nowIso();
1004
+ await db().exec(SQL[cachedKind].insertAttempt, [
1005
+ randomId("whatt"),
1006
+ row.id,
1007
+ subscription.id,
1008
+ result.deliveryId,
1009
+ attemptNumber,
1010
+ result.status,
1011
+ result.statusCode ?? null,
1012
+ result.errorCode ?? null,
1013
+ result.errorMessage ?? null,
1014
+ JSON.stringify(redactHeaders(result.requestHeaders ?? {})),
1015
+ result.responsePreview ?? null,
1016
+ result.durationMs ?? 0,
1017
+ time,
1018
+ ]);
1019
+ }
1020
+
1021
+ async function sendDelivery(row, subscription, attemptNumber, options) {
1022
+ const endpoint = new URL(subscription.endpointUrl);
1023
+ const client = options.clientForOrigin?.(endpoint.origin)
1024
+ ?? options.clientFactory?.get?.(endpoint.origin)
1025
+ ?? options.client
1026
+ ?? scope.get(httpClientToken(config.delivery.client));
1027
+ const body = row.payloadJson;
1028
+ const deliveryId = randomId("whdel");
1029
+ const signature = await sign(body, {
1030
+ secret: subscriptionSecretFromStored(subscription.secretRef, options.signingSecret ?? config.signingSecret),
1031
+ config: scope.config,
1032
+ id: deliveryId,
1033
+ event: row.eventName,
1034
+ attempt: attemptNumber,
1035
+ });
1036
+ const headers = {
1037
+ ...subscription.headers,
1038
+ ...signature.headers,
1039
+ "Content-Type": "application/json; charset=utf-8",
1040
+ "User-Agent": "Sloppy-Webhooks/1",
1041
+ };
1042
+ const started = Date.now();
1043
+ try {
1044
+ const response = await client.post(endpoint.href, {
1045
+ headers,
1046
+ retry: { kind: "none", maxAttempts: 1 },
1047
+ }).textBody(body).send();
1048
+ const text = await response.text();
1049
+ const preview = text.slice(0, options.maxResponsePreviewBytes ?? config.delivery.maxResponsePreviewBytes);
1050
+ const delivered = response.status >= 200 && response.status < 300;
1051
+ const retryable = !delivered && shouldRetryStatus(config.delivery.retry, response.status);
1052
+ return Object.freeze({
1053
+ deliveryId,
1054
+ status: delivered ? "delivered" : "failed",
1055
+ statusCode: response.status,
1056
+ requestHeaders: headers,
1057
+ responsePreview: preview,
1058
+ retryAfterMs: retryable ? retryDelayMs(config.delivery.retry, attemptNumber, response) : undefined,
1059
+ retryable,
1060
+ durationMs: Math.max(0, Date.now() - started),
1061
+ });
1062
+ } catch (error) {
1063
+ return Object.freeze({
1064
+ deliveryId,
1065
+ status: "failed",
1066
+ errorCode: error?.code ?? "SLOPPY_E_WEBHOOK_DELIVERY_FAILED",
1067
+ errorMessage: String(error?.message ?? "delivery failed").replace(String(config.signingSecret ?? ""), "[redacted]"),
1068
+ requestHeaders: headers,
1069
+ retryable: true,
1070
+ durationMs: Math.max(0, Date.now() - started),
1071
+ });
1072
+ }
1073
+ }
1074
+
1075
+ async function deliverPending(options = {}) {
1076
+ assertOpen();
1077
+ await init();
1078
+ const batchSize = requirePositiveInteger(options.batchSize, "deliverPending batchSize", config.delivery.batchSize);
1079
+ const workerId = options.workerId ?? randomId("whworker");
1080
+ const time = nowIso(options.clock);
1081
+ const lockedUntil = addMs(time, options.leaseMs ?? config.delivery.leaseMs);
1082
+ const rows = await db().query(SQL[cachedKind].selectPendingOutbox, [time, time, time, batchSize]);
1083
+ const summary = {
1084
+ claimed: 0,
1085
+ delivered: 0,
1086
+ failed: 0,
1087
+ deadLetter: 0,
1088
+ skipped: 0,
1089
+ };
1090
+ for (const row of rows) {
1091
+ await db().exec(SQL[cachedKind].claimOutbox, [workerId, lockedUntil, time, row.id, time]);
1092
+ const claimedRow = await db().queryOne(SQL[cachedKind].selectClaimedOutbox, [row.id, workerId]);
1093
+ if (claimedRow === null || claimedRow === undefined) {
1094
+ summary.skipped += 1;
1095
+ continue;
1096
+ }
1097
+ summary.claimed += 1;
1098
+ const subscriptions = await db().query(SQL[cachedKind].subscriptionsForEvent, [claimedRow.eventName]);
1099
+ const enabled = subscriptions.filter((subscription) => subscription.enabled === true || subscription.enabled === 1);
1100
+ if (enabled.length === 0) {
1101
+ await db().exec(SQL[cachedKind].updateOutboxDelivered, ["delivered", claimedRow.attemptCount, null, nowIso(), claimedRow.id]);
1102
+ summary.skipped += 1;
1103
+ continue;
1104
+ }
1105
+ let rowDelivered = true;
1106
+ let rowDeadLetter = false;
1107
+ let retryAfterMs = 0;
1108
+ const nextAttempt = claimedRow.attemptCount + 1;
1109
+ const deliveredSubscriptionRows = await db().query(SQL[cachedKind].deliveredSubscriptionsForOutbox, [claimedRow.id]);
1110
+ const deliveredSubscriptionIds = new Set(deliveredSubscriptionRows.map((entry) => entry.subscriptionId));
1111
+ for (const subscriptionRow of enabled) {
1112
+ if (deliveredSubscriptionIds.has(subscriptionRow.id)) {
1113
+ summary.skipped += 1;
1114
+ continue;
1115
+ }
1116
+ const subscription = Object.freeze({
1117
+ id: subscriptionRow.id,
1118
+ endpointUrl: subscriptionRow.endpointUrl,
1119
+ secretRef: subscriptionRow.secretRef,
1120
+ headers: parseHeadersJson(subscriptionRow.headersJson),
1121
+ });
1122
+ const result = await sendDelivery(claimedRow, subscription, nextAttempt, options);
1123
+ await recordAttempt(claimedRow, subscription, nextAttempt, result);
1124
+ if (result.status === "delivered") {
1125
+ summary.delivered += 1;
1126
+ continue;
1127
+ }
1128
+ rowDelivered = false;
1129
+ if (result.retryable === false || nextAttempt >= claimedRow.maxAttempts) {
1130
+ rowDeadLetter = true;
1131
+ summary.deadLetter += 1;
1132
+ } else {
1133
+ retryAfterMs = Math.max(retryAfterMs, result.retryAfterMs ?? retryDelayMs(config.delivery.retry, nextAttempt));
1134
+ summary.failed += 1;
1135
+ }
1136
+ }
1137
+ const status = rowDelivered ? "delivered" : rowDeadLetter ? "dead_letter" : "failed";
1138
+ const nextAttemptAt = status === "failed" ? addMs(nowIso(), retryAfterMs) : null;
1139
+ await db().exec(SQL[cachedKind].updateOutboxDelivered, [status, nextAttempt, nextAttemptAt, nowIso(), claimedRow.id]);
1140
+ }
1141
+ return Object.freeze(summary);
1142
+ }
1143
+
1144
+ function status() {
1145
+ assertOpen();
1146
+ return Object.freeze({
1147
+ provider: config.provider,
1148
+ providerKind: cachedKind ?? config.providerKind ?? "unknown",
1149
+ client: config.delivery.client,
1150
+ initialized,
1151
+ closed,
1152
+ });
1153
+ }
1154
+
1155
+ async function health() {
1156
+ try {
1157
+ await init();
1158
+ return Object.freeze({ status: "healthy", provider: config.provider });
1159
+ } catch (error) {
1160
+ return Object.freeze({
1161
+ status: initialized ? "degraded" : "unhealthy",
1162
+ provider: config.provider,
1163
+ errorCode: error?.code ?? "SLOPPY_E_WEBHOOK_OUTBOX_UNAVAILABLE",
1164
+ });
1165
+ }
1166
+ }
1167
+
1168
+ const service = Object.freeze({
1169
+ init,
1170
+ subscriptions: Object.freeze({
1171
+ create: createSubscription,
1172
+ list: listSubscriptions,
1173
+ get: getSubscription,
1174
+ update: updateSubscription,
1175
+ enable: (id) => setEnabled(id, true),
1176
+ disable: (id) => setEnabled(id, false),
1177
+ delete: deleteSubscription,
1178
+ }),
1179
+ publish,
1180
+ deliverPending,
1181
+ jobs: Object.freeze({
1182
+ deliverPending(jobOptions = {}) {
1183
+ return Object.freeze({
1184
+ run(ctx = {}) {
1185
+ return deliverPending({ ...jobOptions, ...ctx });
1186
+ },
1187
+ handler(ctx = {}) {
1188
+ return deliverPending({ ...jobOptions, ...ctx });
1189
+ },
1190
+ });
1191
+ },
1192
+ }),
1193
+ status,
1194
+ health,
1195
+ metrics() {
1196
+ return Object.freeze({
1197
+ "webhooks.outbox.initialized": initialized ? 1 : 0,
1198
+ });
1199
+ },
1200
+ async dispose() {
1201
+ closed = true;
1202
+ },
1203
+ });
1204
+ return service;
1205
+ }
1206
+
1207
+ function outbox(options) {
1208
+ const config = normalizeOutboxOptions(options);
1209
+ const descriptor = {
1210
+ __sloppyWebhooksOutboxRegistration: true,
1211
+ token: config.token,
1212
+ provider: config.provider,
1213
+ delivery: config.delivery,
1214
+ createService(scope) {
1215
+ return createWebhooksService(config, scope);
1216
+ },
1217
+ __sloppyPlanMetadata() {
1218
+ return Object.freeze({
1219
+ enabled: true,
1220
+ provider: config.provider,
1221
+ client: config.delivery.client,
1222
+ retry: config.delivery.retry.kind,
1223
+ signing: config.signingSecret === undefined ? "missing" : "configured",
1224
+ });
1225
+ },
1226
+ };
1227
+ return Object.freeze(descriptor);
1228
+ }
1229
+
1230
+ function jobsDeliverPending(options = {}) {
1231
+ return Object.freeze({
1232
+ run(ctx = {}) {
1233
+ const service = ctx.webhooks ?? ctx.services?.get?.(DEFAULT_WEBHOOKS_TOKEN);
1234
+ if (service === undefined) {
1235
+ throw webhookError("SLOPPY_E_WEBHOOK_OUTBOX_UNAVAILABLE", "Webhook job requires a registered webhook service.");
1236
+ }
1237
+ return service.deliverPending(options);
1238
+ },
1239
+ handler(ctx = {}) {
1240
+ return this.run(ctx);
1241
+ },
1242
+ });
1243
+ }
1244
+
1245
+ class TestWebhookReceiver {
1246
+ constructor() {
1247
+ this.expected = [];
1248
+ this.received = [];
1249
+ this.status = 200;
1250
+ this.body = { ok: true };
1251
+ }
1252
+
1253
+ expect(eventName) {
1254
+ this.expected.push(validateEventName(eventName));
1255
+ return this;
1256
+ }
1257
+
1258
+ reply(status, body = undefined) {
1259
+ this.status = status;
1260
+ this.body = body;
1261
+ return this;
1262
+ }
1263
+
1264
+ async handler(ctx) {
1265
+ const verified = await verify(ctx, {
1266
+ secret: this.secret ?? "test-secret",
1267
+ toleranceMs: DEFAULT_TIMESTAMP_TOLERANCE_MS,
1268
+ });
1269
+ this.received.push(verified);
1270
+ return {
1271
+ __sloppyResult: true,
1272
+ kind: "json",
1273
+ status: this.status,
1274
+ body: this.body,
1275
+ contentType: "application/json; charset=utf-8",
1276
+ };
1277
+ }
1278
+
1279
+ assertDelivered(eventName) {
1280
+ validateEventName(eventName);
1281
+ if (!this.received.some((entry) => entry.event === eventName)) {
1282
+ throw webhookError("SLOPPY_E_WEBHOOK_DELIVERY_FAILED", `Expected webhook event '${eventName}' to be delivered.`);
1283
+ }
1284
+ return this;
1285
+ }
1286
+
1287
+ assertNoUnexpectedDeliveries() {
1288
+ const allowed = new Set(this.expected);
1289
+ const unexpected = this.received.filter((entry) => !allowed.has(entry.event));
1290
+ if (unexpected.length !== 0) {
1291
+ throw webhookError("SLOPPY_E_WEBHOOK_DELIVERY_FAILED", "Unexpected webhook deliveries were observed.");
1292
+ }
1293
+ return this;
1294
+ }
1295
+ }
1296
+
1297
+ const TestWebhooks = Object.freeze({
1298
+ receiver(options = {}) {
1299
+ const receiver = new TestWebhookReceiver();
1300
+ receiver.secret = options.secret ?? "test-secret";
1301
+ return receiver;
1302
+ },
1303
+ });
1304
+
1305
+ const Webhooks = Object.freeze({
1306
+ event,
1307
+ outbox,
1308
+ token: stableToken,
1309
+ sign,
1310
+ verify,
1311
+ receiver: (options) => TestWebhooks.receiver(options),
1312
+ jobs: Object.freeze({
1313
+ deliverPending: jobsDeliverPending,
1314
+ }),
1315
+ retry: Object.freeze({
1316
+ fixed: retryFixed,
1317
+ exponential: retryExponential,
1318
+ }),
1319
+ sql(provider = "sqlite") {
1320
+ if (!Object.prototype.hasOwnProperty.call(SQL, provider)) {
1321
+ throw new TypeError("Sloppy Webhooks.sql provider must be sqlite, postgres, or sqlserver.");
1322
+ }
1323
+ return SQL[provider];
1324
+ },
1325
+ isTerminalStatus(status) {
1326
+ return TERMINAL_STATUSES.has(status);
1327
+ },
1328
+ });
1329
+
1330
+ export { SloppyWebhookError, TestWebhooks, Webhooks };