@runeditrun/rer 0.0.0-stage → 0.1.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 (526) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -2
  3. package/bin/rer.mjs +3 -0
  4. package/cli/README.md +243 -0
  5. package/cli/adapter.mjs +10 -0
  6. package/cli/ai-local.mjs +65 -0
  7. package/cli/archive-path.mjs +52 -0
  8. package/cli/archive-vite.mjs +55 -0
  9. package/cli/archive.mjs +8 -0
  10. package/cli/auth-copy.mjs +80 -0
  11. package/cli/bounded.mjs +70 -0
  12. package/cli/cache-doctor.mjs +63 -0
  13. package/cli/container-transfer.mjs +35 -0
  14. package/cli/container.mjs +62 -0
  15. package/cli/contract.mjs +25 -0
  16. package/cli/dependencies.mjs +45 -0
  17. package/cli/dependency-approval.mjs +33 -0
  18. package/cli/dependency-doctor.mjs +34 -0
  19. package/cli/dependency-input.mjs +43 -0
  20. package/cli/deploy-config.mjs +118 -0
  21. package/cli/deploy.mjs +37 -0
  22. package/cli/dev-identity.mjs +80 -0
  23. package/cli/errors.mjs +14 -0
  24. package/cli/export.mjs +42 -0
  25. package/cli/font-cache.mjs +29 -0
  26. package/cli/hosted-output.mjs +110 -0
  27. package/cli/independent.mjs +29 -0
  28. package/cli/inspect.mjs +174 -0
  29. package/cli/jsonc.mjs +111 -0
  30. package/cli/lifecycle.mjs +148 -0
  31. package/cli/local.mjs +100 -0
  32. package/cli/main.mjs +57 -0
  33. package/cli/managed-server.mjs +143 -0
  34. package/cli/node-runtime-cache.mjs +68 -0
  35. package/cli/node-runtime-discovery.mjs +56 -0
  36. package/cli/node-runtime-download.mjs +59 -0
  37. package/cli/node-runtime.mjs +50 -0
  38. package/cli/output.mjs +22 -0
  39. package/cli/parse.mjs +77 -0
  40. package/cli/port.mjs +52 -0
  41. package/cli/portability-worker.mjs +56 -0
  42. package/cli/portability.mjs +66 -0
  43. package/cli/preview-cache.mjs +99 -0
  44. package/cli/project.mjs +48 -0
  45. package/cli/release.mjs +24 -0
  46. package/cli/runtime-front-door.mjs +46 -0
  47. package/cli/scheduled.mjs +81 -0
  48. package/cli/server-activity.mjs +12 -0
  49. package/cli/server-proxy.mjs +93 -0
  50. package/cli/server-registry.mjs +177 -0
  51. package/cli/server-resources.mjs +22 -0
  52. package/cli/settings.mjs +15 -0
  53. package/cli/setup-answers.mjs +29 -0
  54. package/cli/setup-input.mjs +113 -0
  55. package/cli/shared.mjs +7 -0
  56. package/cli/transfers.mjs +33 -0
  57. package/cli/version.mjs +36 -0
  58. package/composition/git-source.mjs +111 -0
  59. package/edition-authoring/files.mjs +81 -0
  60. package/edition-authoring/manifest.mjs +65 -0
  61. package/global/README.md +155 -0
  62. package/global/checkout.mjs +72 -0
  63. package/global/cli/command.mjs +13 -0
  64. package/global/cli/install.mjs +75 -0
  65. package/global/cli/layout.mjs +45 -0
  66. package/global/cli/status.mjs +54 -0
  67. package/global/cli/uninstall.mjs +36 -0
  68. package/global/delegate.mjs +13 -0
  69. package/global/launch/capacity.mjs +72 -0
  70. package/global/launch/dev.mjs +129 -0
  71. package/global/launch/facts.mjs +93 -0
  72. package/global/launch/failure.mjs +5 -0
  73. package/global/launch/local-defaults.mjs +4 -0
  74. package/global/launch/prepare.mjs +61 -0
  75. package/global/launch/render.mjs +44 -0
  76. package/global/launch/state.mjs +97 -0
  77. package/global/launch/steps.mjs +81 -0
  78. package/global/launch/worker.mjs +43 -0
  79. package/global/list.mjs +59 -0
  80. package/global/main.mjs +53 -0
  81. package/global/package-info.mjs +38 -0
  82. package/global/registry.mjs +24 -0
  83. package/global/rer.mjs +29 -0
  84. package/global/update/archive.mjs +89 -0
  85. package/global/update/command.mjs +90 -0
  86. package/global/update/latest.mjs +6 -0
  87. package/global/update/notice.mjs +58 -0
  88. package/global/update/registry.mjs +47 -0
  89. package/global/version.mjs +29 -0
  90. package/install.sh +144 -0
  91. package/modules/ai/LICENSE +21 -0
  92. package/modules/ai/README.md +90 -0
  93. package/modules/ai/client.mjs +71 -0
  94. package/modules/ai/config.mjs +39 -0
  95. package/modules/ai/dev/index.d.mts +6 -0
  96. package/modules/ai/dev/index.mjs +64 -0
  97. package/modules/ai/dev/outbox.mjs +33 -0
  98. package/modules/ai/errors.mjs +28 -0
  99. package/modules/ai/fake/index.d.mts +3 -0
  100. package/modules/ai/fake/index.mjs +82 -0
  101. package/modules/ai/index.d.mts +10 -0
  102. package/modules/ai/index.mjs +3 -0
  103. package/modules/ai/package.json +6 -0
  104. package/modules/container/README.md +156 -0
  105. package/modules/container/adapters/d1.mjs +34 -0
  106. package/modules/container/adapters/egress.mjs +40 -0
  107. package/modules/container/adapters/jobs.mjs +47 -0
  108. package/modules/container/adapters/objects.mjs +84 -0
  109. package/modules/container/adapters/retained.mjs +14 -0
  110. package/modules/container/adapters/sqlite-error.mjs +7 -0
  111. package/modules/container/adapters/sqlite-worker.mjs +26 -0
  112. package/modules/container/assets.mjs +14 -0
  113. package/modules/container/bindings.mjs +1 -0
  114. package/modules/container/boundary.mjs +38 -0
  115. package/modules/container/build.mjs +65 -0
  116. package/modules/container/deploy-input.mjs +17 -0
  117. package/modules/container/entrypoint.sh +5 -0
  118. package/modules/container/host.mjs +43 -0
  119. package/modules/container/ingress.mjs +23 -0
  120. package/modules/container/lock.mjs +11 -0
  121. package/modules/container/migrations.mjs +16 -0
  122. package/modules/container/node-engine.mjs +39 -0
  123. package/modules/container/portability-schema.mjs +14 -0
  124. package/modules/container/remote/deploy.py +76 -0
  125. package/modules/container/remote/files.py +275 -0
  126. package/modules/container/remote/resources.py +157 -0
  127. package/modules/container/runner.mjs +10 -0
  128. package/modules/container/ssh-plan.mjs +44 -0
  129. package/modules/container/ssh.mjs +63 -0
  130. package/modules/container/transfer.mjs +27 -0
  131. package/modules/container/worker-engine.mjs +32 -0
  132. package/modules/dependencies/README.md +211 -0
  133. package/modules/dependencies/approval.mjs +51 -0
  134. package/modules/dependencies/auth.mjs +40 -0
  135. package/modules/dependencies/authority.mjs +14 -0
  136. package/modules/dependencies/crypto.mjs +13 -0
  137. package/modules/dependencies/index.d.mts +23 -0
  138. package/modules/dependencies/index.mjs +113 -0
  139. package/modules/dependencies/logout.mjs +42 -0
  140. package/modules/dependencies/native.mjs +9 -0
  141. package/modules/dependencies/protocol.mjs +23 -0
  142. package/modules/dependencies/schema/approval-attempt.sql +3 -0
  143. package/modules/dependencies/schema/dependencies.sql +15 -0
  144. package/modules/dependencies/store.mjs +19 -0
  145. package/modules/deploy-kit/README.md +308 -0
  146. package/modules/deploy-kit/artifact.mjs +47 -0
  147. package/modules/deploy-kit/build-modules.mjs +21 -0
  148. package/modules/deploy-kit/check.mjs +13 -0
  149. package/modules/deploy-kit/cli.mjs +50 -0
  150. package/modules/deploy-kit/container-config.mjs +8 -0
  151. package/modules/deploy-kit/default-stage.mjs +14 -0
  152. package/modules/deploy-kit/deploy.mjs +80 -0
  153. package/modules/deploy-kit/health-request.mjs +86 -0
  154. package/modules/deploy-kit/health.mjs +99 -0
  155. package/modules/deploy-kit/index.mjs +12 -0
  156. package/modules/deploy-kit/local-only.mjs +10 -0
  157. package/modules/deploy-kit/migrations.mjs +48 -0
  158. package/modules/deploy-kit/owner-cli.mjs +63 -0
  159. package/modules/deploy-kit/owner.mjs +297 -0
  160. package/modules/deploy-kit/prepare.mjs +62 -0
  161. package/modules/deploy-kit/provider.mjs +99 -0
  162. package/modules/deploy-kit/queue-consumer.mjs +10 -0
  163. package/modules/deploy-kit/receipt.mjs +16 -0
  164. package/modules/deploy-kit/render.mjs +47 -0
  165. package/modules/deploy-kit/resources.mjs +106 -0
  166. package/modules/deploy-kit/service-grants.mjs +25 -0
  167. package/modules/deploy-kit/sql-safety.mjs +94 -0
  168. package/modules/deploy-kit/sql-tokens.mjs +49 -0
  169. package/modules/deploy-kit/teardown.mjs +100 -0
  170. package/modules/deploy-kit/templates.mjs +31 -0
  171. package/modules/deploy-kit/validation.mjs +58 -0
  172. package/modules/deploy-kit/worker.mjs +105 -0
  173. package/modules/erasure/CONTRACT.md +90 -0
  174. package/modules/erasure/README.md +221 -0
  175. package/modules/erasure/audit.mjs +23 -0
  176. package/modules/erasure/blobs.mjs +45 -0
  177. package/modules/erasure/cascade.mjs +91 -0
  178. package/modules/erasure/completion.mjs +62 -0
  179. package/modules/erasure/declaration.mjs +51 -0
  180. package/modules/erasure/fence.mjs +19 -0
  181. package/modules/erasure/index.d.mts +62 -0
  182. package/modules/erasure/index.mjs +85 -0
  183. package/modules/erasure/order.mjs +56 -0
  184. package/modules/erasure/plan.mjs +121 -0
  185. package/modules/erasure/rate.mjs +57 -0
  186. package/modules/erasure/receipt.mjs +32 -0
  187. package/modules/erasure/root.mjs +21 -0
  188. package/modules/erasure/schema/erasure-rates.sql +5 -0
  189. package/modules/erasure/schema/erasure.sql +27 -0
  190. package/modules/erasure/scope.mjs +29 -0
  191. package/modules/erasure/sql.mjs +23 -0
  192. package/modules/erasure/verification.mjs +69 -0
  193. package/modules/integrations/core/CONTRACT.md +50 -0
  194. package/modules/integrations/core/README.md +27 -0
  195. package/modules/integrations/core/crypto.ts +19 -0
  196. package/modules/integrations/core/database.ts +21 -0
  197. package/modules/integrations/core/delivery.ts +97 -0
  198. package/modules/integrations/core/destination.ts +13 -0
  199. package/modules/integrations/core/index.ts +7 -0
  200. package/modules/integrations/core/node-transport.ts +72 -0
  201. package/modules/integrations/core/public-address.ts +57 -0
  202. package/modules/integrations/core/repository.ts +74 -0
  203. package/modules/integrations/core/schema.sql +36 -0
  204. package/modules/integrations/core/sqlite.ts +20 -0
  205. package/modules/integrations/core/timestamped-body-signature.ts +38 -0
  206. package/modules/integrations/core/tokens.ts +71 -0
  207. package/modules/integrations/core/types.ts +52 -0
  208. package/modules/integrations/core/upgrade-token-usage-context.sql +5 -0
  209. package/modules/integrations/core/webhook.ts +73 -0
  210. package/modules/job-queue/README.md +173 -0
  211. package/modules/job-queue/cli.mjs +64 -0
  212. package/modules/job-queue/command.mjs +64 -0
  213. package/modules/job-queue/config.mjs +48 -0
  214. package/modules/job-queue/identity.mjs +48 -0
  215. package/modules/job-queue/index.mjs +135 -0
  216. package/modules/job-queue/membership.mjs +30 -0
  217. package/modules/job-queue/module.json +10 -0
  218. package/modules/job-queue/policy.mjs +29 -0
  219. package/modules/job-queue/processes.mjs +80 -0
  220. package/modules/job-queue/resources.mjs +37 -0
  221. package/modules/job-queue/storage.mjs +64 -0
  222. package/modules/job-queue/supervisor.mjs +30 -0
  223. package/modules/job-queue/waiting.mjs +16 -0
  224. package/modules/mail/LICENSE +21 -0
  225. package/modules/mail/README.md +336 -0
  226. package/modules/mail/doctor.mjs +81 -0
  227. package/modules/mail/index.mjs +8 -0
  228. package/modules/mail/live-proof/README.md +113 -0
  229. package/modules/mail/live-proof/run.mjs +55 -0
  230. package/modules/mail/package.json +19 -0
  231. package/modules/mail/pnpm-lock.yaml +537 -0
  232. package/modules/mail/src/adapters/cloudflare/README.md +84 -0
  233. package/modules/mail/src/adapters/cloudflare/headers.mjs +63 -0
  234. package/modules/mail/src/adapters/cloudflare/index.mjs +47 -0
  235. package/modules/mail/src/adapters/cloudflare/response.mjs +99 -0
  236. package/modules/mail/src/adapters/messaging/README.md +92 -0
  237. package/modules/mail/src/adapters/messaging/contract.mjs +66 -0
  238. package/modules/mail/src/adapters/messaging/event-schema.mjs +28 -0
  239. package/modules/mail/src/adapters/messaging/index.mjs +61 -0
  240. package/modules/mail/src/adapters/messaging/jws.mjs +68 -0
  241. package/modules/mail/src/adapters/messaging/request.mjs +52 -0
  242. package/modules/mail/src/adapters/messaging/serialize.mjs +21 -0
  243. package/modules/mail/src/adapters/resend/README.md +120 -0
  244. package/modules/mail/src/adapters/resend/index.mjs +22 -0
  245. package/modules/mail/src/adapters/resend/response.mjs +54 -0
  246. package/modules/mail/src/adapters/resend/serialize.mjs +46 -0
  247. package/modules/mail/src/adapters/resend/size.mjs +99 -0
  248. package/modules/mail/src/adapters/sendgrid/README.md +75 -0
  249. package/modules/mail/src/adapters/sendgrid/index.mjs +35 -0
  250. package/modules/mail/src/adapters/sendgrid/serialize.mjs +104 -0
  251. package/modules/mail/src/core/errors.mjs +9 -0
  252. package/modules/mail/src/core/transport.mjs +33 -0
  253. package/modules/mail/src/core/validation.mjs +172 -0
  254. package/modules/mail/src/development/index.d.mts +37 -0
  255. package/modules/mail/src/development/index.mjs +35 -0
  256. package/modules/mail/src/development/outbox.mjs +78 -0
  257. package/modules/mail/src/development/schema.sql +9 -0
  258. package/modules/mail/src/development/viewer-html.mjs +42 -0
  259. package/modules/mail/src/development/viewer.mjs +65 -0
  260. package/modules/mail/src/doctor/README.md +107 -0
  261. package/modules/mail/src/doctor/common.mjs +64 -0
  262. package/modules/mail/src/doctor/dns.mjs +102 -0
  263. package/modules/mail/src/doctor/index.mjs +28 -0
  264. package/modules/mail/src/doctor/probe.mjs +66 -0
  265. package/modules/mail/src/doctor/receive.mjs +81 -0
  266. package/modules/mail/src/doctor/records.mjs +45 -0
  267. package/modules/mail/src/events/README.md +268 -0
  268. package/modules/mail/src/events/d1-schema.sql +25 -0
  269. package/modules/mail/src/events/d1-storage.mjs +76 -0
  270. package/modules/mail/src/events/index.mjs +3 -0
  271. package/modules/mail/src/events/intake.mjs +201 -0
  272. package/modules/mail/src/events/storage.mjs +52 -0
  273. package/modules/mail/src/events/verify.mjs +92 -0
  274. package/modules/mail/src/inbound/README.md +137 -0
  275. package/modules/mail/src/inbound/bytes.mjs +91 -0
  276. package/modules/mail/src/inbound/headers.mjs +143 -0
  277. package/modules/mail/src/inbound/index.mjs +29 -0
  278. package/modules/mail/src/inbound/mime.mjs +36 -0
  279. package/modules/mail/src/inbound/multipart.mjs +27 -0
  280. package/modules/mail/src/inbound/resend-attachment.mjs +43 -0
  281. package/modules/mail/src/inbound/resend.mjs +54 -0
  282. package/modules/mail/src/inbound/sendgrid.mjs +60 -0
  283. package/modules/mail/src/sender.mjs +33 -0
  284. package/modules/mail/src/sendgrid-email/index.d.mts +33 -0
  285. package/modules/mail/src/sendgrid-email/index.mjs +86 -0
  286. package/modules/mail/src/unsubscribe/index.mjs +142 -0
  287. package/modules/operator-auth/README.md +599 -0
  288. package/modules/operator-auth/accounts/adapter.mjs +41 -0
  289. package/modules/operator-auth/accounts/admin.mjs +159 -0
  290. package/modules/operator-auth/accounts/credentials.mjs +27 -0
  291. package/modules/operator-auth/accounts/editor-owner.mjs +13 -0
  292. package/modules/operator-auth/accounts/index.d.mts +9 -0
  293. package/modules/operator-auth/accounts/index.mjs +9 -0
  294. package/modules/operator-auth/accounts/native.d.mts +38 -0
  295. package/modules/operator-auth/accounts/native.mjs +171 -0
  296. package/modules/operator-auth/accounts/oa.mjs +90 -0
  297. package/modules/operator-auth/accounts/provider-control.mjs +50 -0
  298. package/modules/operator-auth/accounts/provider-pin.mjs +14 -0
  299. package/modules/operator-auth/accounts/roles.mjs +26 -0
  300. package/modules/operator-auth/accounts/schema/dependency-setup.sql +17 -0
  301. package/modules/operator-auth/accounts/schema/dev-sessions.sql +22 -0
  302. package/modules/operator-auth/accounts/schema/domain-rules-step-up.sql +20 -0
  303. package/modules/operator-auth/accounts/schema/invitation-cancellation.sql +17 -0
  304. package/modules/operator-auth/accounts/schema/operator-auth.sql +142 -0
  305. package/modules/operator-auth/accounts/schema/provider-control.sql +30 -0
  306. package/modules/operator-auth/accounts/schema/provider-pins.sql +6 -0
  307. package/modules/operator-auth/accounts/settings-page.mjs +4 -0
  308. package/modules/operator-auth/accounts/store.mjs +246 -0
  309. package/modules/operator-auth/branding.mjs +66 -0
  310. package/modules/operator-auth/config.mjs +35 -0
  311. package/modules/operator-auth/conformance/README.md +81 -0
  312. package/modules/operator-auth/conformance/index.d.mts +43 -0
  313. package/modules/operator-auth/conformance/index.mjs +300 -0
  314. package/modules/operator-auth/conformance/presentation.mjs +57 -0
  315. package/modules/operator-auth/crypto.mjs +17 -0
  316. package/modules/operator-auth/dev/badge.mjs +62 -0
  317. package/modules/operator-auth/dev/index.d.mts +14 -0
  318. package/modules/operator-auth/dev/index.mjs +137 -0
  319. package/modules/operator-auth/dev/personas.mjs +17 -0
  320. package/modules/operator-auth/dev/sign-in-as.d.mts +3 -0
  321. package/modules/operator-auth/dev/sign-in-as.mjs +25 -0
  322. package/modules/operator-auth/dev-admission.d.mts +4 -0
  323. package/modules/operator-auth/dev-admission.mjs +10 -0
  324. package/modules/operator-auth/dev-gate.mjs +86 -0
  325. package/modules/operator-auth/docs/README.md +78 -0
  326. package/modules/operator-auth/docs/native-accounts.md +313 -0
  327. package/modules/operator-auth/docs/tanstack-start.md +218 -0
  328. package/modules/operator-auth/docs/vite-spa.md +144 -0
  329. package/modules/operator-auth/errors.mjs +9 -0
  330. package/modules/operator-auth/http.mjs +42 -0
  331. package/modules/operator-auth/identity/admission.mjs +105 -0
  332. package/modules/operator-auth/identity/after-confirm.mjs +18 -0
  333. package/modules/operator-auth/identity/email-link/confirmation.mjs +14 -0
  334. package/modules/operator-auth/identity/email-link/index.d.mts +48 -0
  335. package/modules/operator-auth/identity/email-link/index.mjs +128 -0
  336. package/modules/operator-auth/identity/email-link/message.mjs +16 -0
  337. package/modules/operator-auth/identity/index.d.mts +7 -0
  338. package/modules/operator-auth/identity/index.mjs +5 -0
  339. package/modules/operator-auth/identity/oidc/README.md +171 -0
  340. package/modules/operator-auth/identity/oidc/client-auth.mjs +16 -0
  341. package/modules/operator-auth/identity/oidc/config.mjs +102 -0
  342. package/modules/operator-auth/identity/oidc/crypto.mjs +29 -0
  343. package/modules/operator-auth/identity/oidc/errors.mjs +5 -0
  344. package/modules/operator-auth/identity/oidc/index.d.mts +33 -0
  345. package/modules/operator-auth/identity/oidc/index.mjs +287 -0
  346. package/modules/operator-auth/identity/oidc/jwt.mjs +102 -0
  347. package/modules/operator-auth/identity/oidc/network.mjs +93 -0
  348. package/modules/operator-auth/identity/oidc/presets.mjs +26 -0
  349. package/modules/operator-auth/identity/oidc/schema/oidc.sql +50 -0
  350. package/modules/operator-auth/identity/oidc/store.mjs +68 -0
  351. package/modules/operator-auth/identity/principal.mjs +28 -0
  352. package/modules/operator-auth/index.d.mts +223 -0
  353. package/modules/operator-auth/index.mjs +11 -0
  354. package/modules/operator-auth/input.mjs +55 -0
  355. package/modules/operator-auth/local-cli.mjs +16 -0
  356. package/modules/operator-auth/page.mjs +34 -0
  357. package/modules/operator-auth/policy.mjs +20 -0
  358. package/modules/operator-auth/routes.mjs +91 -0
  359. package/modules/operator-auth/service.mjs +152 -0
  360. package/modules/operator-auth/signin/index.mjs +67 -0
  361. package/modules/owner-claim/README.md +137 -0
  362. package/modules/owner-claim/idp.mjs +40 -0
  363. package/modules/owner-claim/index.d.mts +27 -0
  364. package/modules/owner-claim/index.mjs +39 -0
  365. package/modules/owner-claim/page.mjs +11 -0
  366. package/modules/owner-claim/schema/idp.sql +4 -0
  367. package/modules/owner-claim/schema/owner-claim.sql +18 -0
  368. package/modules/owner-claim/transaction.mjs +87 -0
  369. package/modules/owner-claim/verify.mjs +80 -0
  370. package/modules/portability/README.md +444 -0
  371. package/modules/portability/bridge.mjs +102 -0
  372. package/modules/portability/catalogue.mjs +118 -0
  373. package/modules/portability/cli-options.mjs +38 -0
  374. package/modules/portability/cli.mjs +56 -0
  375. package/modules/portability/copies.mjs +68 -0
  376. package/modules/portability/dev-data.mjs +32 -0
  377. package/modules/portability/diff.mjs +33 -0
  378. package/modules/portability/export.mjs +129 -0
  379. package/modules/portability/files.mjs +65 -0
  380. package/modules/portability/format.mjs +122 -0
  381. package/modules/portability/generated.mjs +60 -0
  382. package/modules/portability/hash.mjs +79 -0
  383. package/modules/portability/http.mjs +42 -0
  384. package/modules/portability/import.mjs +185 -0
  385. package/modules/portability/index.d.mts +58 -0
  386. package/modules/portability/index.mjs +22 -0
  387. package/modules/portability/journal.mjs +49 -0
  388. package/modules/portability/legacy-files.mjs +43 -0
  389. package/modules/portability/legacy-safety.mjs +27 -0
  390. package/modules/portability/lines.mjs +36 -0
  391. package/modules/portability/migration-history.mjs +18 -0
  392. package/modules/portability/migration-statements.mjs +23 -0
  393. package/modules/portability/payloads.mjs +52 -0
  394. package/modules/portability/provider-data.mjs +25 -0
  395. package/modules/portability/restore-safety.mjs +30 -0
  396. package/modules/portability/schema/portability.sql +25 -0
  397. package/modules/portability/storage.mjs +49 -0
  398. package/modules/release/README.md +212 -0
  399. package/modules/release/catalogue.mjs +79 -0
  400. package/modules/release/export.mjs +51 -0
  401. package/modules/release/produce.mjs +136 -0
  402. package/modules/release/public-child-transport-state.cjs +29 -0
  403. package/modules/release/public-child.mjs +71 -0
  404. package/modules/release/public-git.mjs +21 -0
  405. package/modules/release/publication-scan.mjs +22 -0
  406. package/modules/release/publish-handoff.mjs +37 -0
  407. package/modules/release/source.mjs +71 -0
  408. package/modules/release/test-summary.mjs +50 -0
  409. package/onboarding/README.md +163 -0
  410. package/onboarding/activity.mjs +31 -0
  411. package/onboarding/adapter-auth.mjs +88 -0
  412. package/onboarding/adapter-hosting.mjs +35 -0
  413. package/onboarding/adapter-labels.mjs +13 -0
  414. package/onboarding/adapter-settings.mjs +129 -0
  415. package/onboarding/adapter.mjs +238 -0
  416. package/onboarding/agent-handoff.mjs +9 -0
  417. package/onboarding/app-processes.mjs +21 -0
  418. package/onboarding/archive-output.d.mts +3 -0
  419. package/onboarding/archive-output.mjs +54 -0
  420. package/onboarding/command-custody.mjs +24 -0
  421. package/onboarding/command-recovery.mjs +8 -0
  422. package/onboarding/commands.mjs +166 -0
  423. package/onboarding/dependency-approval.mjs +36 -0
  424. package/onboarding/dev-identity.mjs +23 -0
  425. package/onboarding/email.mjs +24 -0
  426. package/onboarding/failures.mjs +42 -0
  427. package/onboarding/input-limits.mjs +7 -0
  428. package/onboarding/installer-inputs.mjs +24 -0
  429. package/onboarding/launcher.mjs +34 -0
  430. package/onboarding/local/build-files.mjs +64 -0
  431. package/onboarding/local/build.mjs +49 -0
  432. package/onboarding/local/environment.mjs +30 -0
  433. package/onboarding/local/files.mjs +35 -0
  434. package/onboarding/local/operation.mjs +166 -0
  435. package/onboarding/local/owner.mjs +34 -0
  436. package/onboarding/local/process-table.mjs +114 -0
  437. package/onboarding/local/recovery.mjs +79 -0
  438. package/onboarding/local/state.mjs +31 -0
  439. package/onboarding/metadata.mjs +7 -0
  440. package/onboarding/operator-install.mjs +45 -0
  441. package/onboarding/operator-installer.mjs +39 -0
  442. package/onboarding/operator-prerequisites.mjs +27 -0
  443. package/onboarding/package.json +6 -0
  444. package/onboarding/prefill.mjs +16 -0
  445. package/onboarding/private-files.mjs +97 -0
  446. package/onboarding/private.mjs +35 -0
  447. package/onboarding/process-cleanup.mjs +134 -0
  448. package/onboarding/process-diagnostics.mjs +91 -0
  449. package/onboarding/process-scope.mjs +118 -0
  450. package/onboarding/process-table.mjs +118 -0
  451. package/onboarding/process-tree.mjs +36 -0
  452. package/onboarding/production-modules.mjs +22 -0
  453. package/onboarding/production-options.mjs +40 -0
  454. package/onboarding/production-plan.mjs +8 -0
  455. package/onboarding/production-runtime.mjs +30 -0
  456. package/onboarding/project-vite.mjs +14 -0
  457. package/onboarding/providers.mjs +289 -0
  458. package/onboarding/queue.mjs +53 -0
  459. package/onboarding/queued-entry.mjs +34 -0
  460. package/onboarding/readiness.mjs +59 -0
  461. package/onboarding/redaction-jwt.mjs +32 -0
  462. package/onboarding/redaction.mjs +298 -0
  463. package/onboarding/schema/SETUP.schema.json +514 -0
  464. package/onboarding/schema/dependencies.mjs +35 -0
  465. package/onboarding/schema/json-schema.mjs +202 -0
  466. package/onboarding/schema/setup-validation.mjs +325 -0
  467. package/onboarding/secrets.mjs +21 -0
  468. package/onboarding/server.mjs +188 -0
  469. package/onboarding/setup-manifest.mjs +116 -0
  470. package/onboarding/state.mjs +46 -0
  471. package/onboarding/theme-files.mjs +37 -0
  472. package/onboarding/theme.mjs +118 -0
  473. package/onboarding/ui/activity.js +98 -0
  474. package/onboarding/ui/address-client.mjs +33 -0
  475. package/onboarding/ui/app.js +377 -0
  476. package/onboarding/ui/index.html +1 -0
  477. package/onboarding/ui/model-core.mjs +127 -0
  478. package/onboarding/ui/model.mjs +5 -0
  479. package/onboarding/ui/style.css +63 -0
  480. package/onboarding/ui/theme.css +37 -0
  481. package/onboarding/ui/workspace-art.js +30 -0
  482. package/onboarding/wrangler-auth.mjs +43 -0
  483. package/operations/README.md +246 -0
  484. package/operations/operations.mjs +168 -0
  485. package/operations/project-operations.mjs +157 -0
  486. package/package.json +11 -4
  487. package/scripts/publish-check/README.md +72 -0
  488. package/scripts/publish-check/checksum_maps.py +52 -0
  489. package/scripts/publish-check/cli.py +34 -0
  490. package/scripts/publish-check/command_arguments.py +51 -0
  491. package/scripts/publish-check/command_shell_quotes.py +41 -0
  492. package/scripts/publish-check/command_source_quotes.py +120 -0
  493. package/scripts/publish-check/content_identities.py +250 -0
  494. package/scripts/publish-check/copy_identities.py +41 -0
  495. package/scripts/publish-check/der.py +61 -0
  496. package/scripts/publish-check/destination.py +117 -0
  497. package/scripts/publish-check/env_values.py +74 -0
  498. package/scripts/publish-check/font_licence.py +23 -0
  499. package/scripts/publish-check/git_history.py +115 -0
  500. package/scripts/publish-check/github_refs.py +217 -0
  501. package/scripts/publish-check/gitleaks.py +72 -0
  502. package/scripts/publish-check/gitleaks.toml +40 -0
  503. package/scripts/publish-check/history_scan.py +176 -0
  504. package/scripts/publish-check/hit_location.py +18 -0
  505. package/scripts/publish-check/hygiene_config.py +42 -0
  506. package/scripts/publish-check/hygiene_text.py +11 -0
  507. package/scripts/publish-check/identity-recipe.mjs +54 -0
  508. package/scripts/publish-check/json_identity.py +79 -0
  509. package/scripts/publish-check/license-exceptions.json +11 -0
  510. package/scripts/publish-check/licenses.py +49 -0
  511. package/scripts/publish-check/policy.py +18 -0
  512. package/scripts/publish-check/project_identity.py +89 -0
  513. package/scripts/publish-check/provenance.py +91 -0
  514. package/scripts/publish-check/provenance_pins.py +33 -0
  515. package/scripts/publish-check/public_base.py +63 -0
  516. package/scripts/publish-check/public_hygiene.py +80 -0
  517. package/scripts/publish-check/publication-config.json +12 -0
  518. package/scripts/publish-check/publication_config.py +34 -0
  519. package/scripts/publish-check/report.py +52 -0
  520. package/scripts/publish-check/repository_commands.py +75 -0
  521. package/scripts/publish-check/repository_intent.py +52 -0
  522. package/scripts/publish-check/reviewed.py +41 -0
  523. package/scripts/publish-check/source_hashes.mjs +6 -0
  524. package/scripts/publish-check/tree_scan.py +88 -0
  525. package/scripts/publish-check/trust_bundle.py +87 -0
  526. package/scripts/publish-check.mjs +58 -0
@@ -0,0 +1,32 @@
1
+ import { statement, canonicalJson, insist } from './sql.mjs';
2
+
3
+ const parse = row => row && ({
4
+ id: row.id, entity: row.entity, phase: row.phase, label: row.label,
5
+ planned: JSON.parse(row.planned), deleted: JSON.parse(row.deleted), retained: JSON.parse(row.retained),
6
+ blobs: { total: row.blobs_total, deleted: row.blobs_deleted },
7
+ requestedAt: row.requested_at, committedAt: row.committed_at, completedAt: row.completed_at,
8
+ seconds: row.completed_at ? (Date.parse(row.completed_at) - Date.parse(row.requested_at)) / 1000 : null,
9
+ });
10
+
11
+ export async function receipt(db, id) {
12
+ const row = await statement(db, 'SELECT * FROM rer_erasure_operations WHERE id=?', id).first();
13
+ insist(row, 'erasure_not_found', 404);
14
+ return parse(row);
15
+ }
16
+ export async function activeFor(db, d, subject) {
17
+ return parse(await statement(db, "SELECT * FROM rer_erasure_operations o WHERE entity=? AND (subject=? OR (?=1 AND phase='blobs' AND EXISTS(SELECT 1 FROM rer_erasure_scope s WHERE s.operation=o.id AND s.kind='graph' AND s.table_name=? AND s.key_json=?)))", d.entity, canonicalJson(subject), Number(Boolean(d.root.redact)), d.root.table, canonicalJson(subject)).first());
18
+ }
19
+ /** The subject key is held only while rows are still being deleted. */
20
+ export function openStatement(db, d, { id, subject, label, planned, blobs, now }) {
21
+ return statement(db, 'INSERT INTO rer_erasure_operations(id,entity,subject,label,phase,planned,retained,blobs_total,requested_at) VALUES(?,?,?,?,?,?,?,?,?)',
22
+ id, d.entity, canonicalJson(subject), label, 'deleting', canonicalJson(planned), canonicalJson([...d.retain.map(({ table, reason }) => ({ table, reason })), ...(d.root.redact ? [{ table: d.root.table, reason: d.root.redact.reason }] : [])]), blobs, now);
23
+ }
24
+ export function progressStatement(db, id, step, deleted) {
25
+ return statement(db, 'UPDATE rer_erasure_operations SET step=step+1,deleted=? WHERE id=? AND step=?', canonicalJson(deleted), id, step);
26
+ }
27
+ export function committedStatement(db, id, now) {
28
+ return statement(db, "UPDATE rer_erasure_operations SET phase='blobs',subject=NULL,committed_at=? WHERE id=? AND phase='deleting'", now, id);
29
+ }
30
+ export function completeStatement(db, id, now) {
31
+ return statement(db, "UPDATE rer_erasure_operations SET phase='complete',completed_at=? WHERE id=? AND phase='blobs' AND NOT EXISTS(SELECT 1 FROM rer_erasure_blobs WHERE operation=?)", now, id, id);
32
+ }
@@ -0,0 +1,21 @@
1
+ import { insist, statement, quote, randomHex, canonicalJson } from './sql.mjs';
2
+
3
+ /** Only the opaque primary key survives; every other root column is explicitly replaced. */
4
+ export async function rootValues(d, keys, subject, anonymousId) {
5
+ if (!d.root.redact) return null;
6
+ const set = typeof d.root.redact.set === 'function' ? await d.root.redact.set({ subject, anonymousId }) : d.root.redact.set;
7
+ const identity = d.root.redact.identityColumns ?? [];
8
+ insist(identity.every(c => keys.columns.includes(c) && !keys.pk.includes(c)) && new Set(identity).size === identity.length, 'root_identity_columns', 409);
9
+ const columns = keys.columns.filter(c => !keys.pk.includes(c) && !identity.includes(c));
10
+ insist(set && columns.length === Object.keys(set).length && columns.every(c => Object.hasOwn(set, c)), 'root_redact_columns', 409);
11
+ insist(Object.values(set).every(v => v === null || typeof v === 'string' || (typeof v === 'number' && Number.isFinite(v))), 'root_redact_values', 409);
12
+ return set;
13
+ }
14
+ export async function rootStatements(ctx, d, plan, id) {
15
+ if (!d.root.redact || !plan.root) return [];
16
+ const keys = plan.keys.get(d.root.table);
17
+ // Independent of the audit label, so a retained root cannot identify that label.
18
+ const anonymousId = plan.anonymousId ??= randomHex(16);
19
+ const set = plan.rootSet ??= await rootValues(d, keys, plan.subject, anonymousId);
20
+ return [statement(ctx.db, "INSERT OR IGNORE INTO rer_erasure_scope VALUES(?,'redaction',?,?,?)", id, d.root.table, canonicalJson(plan.subject), canonicalJson({ anonymousId })), statement(ctx.db, `UPDATE ${quote(d.root.table)} SET ${Object.keys(set).map(c => `${quote(c)}=?`).join(',')} WHERE ${keys.pk.map(c => `${quote(c)}=?`).join(' AND ')}`, ...Object.values(set), ...keys.pk.map(c => plan.subject[c])), ...await ctx.fence.redaction()];
21
+ }
@@ -0,0 +1,5 @@
1
+ CREATE TABLE rer_erasure_rates (
2
+ rate_table TEXT NOT NULL, rate_key TEXT NOT NULL, entity TEXT NOT NULL, subject TEXT NOT NULL,
3
+ PRIMARY KEY(rate_table, rate_key, entity, subject)
4
+ );
5
+ CREATE INDEX rer_erasure_rates_subject ON rer_erasure_rates(entity, subject);
@@ -0,0 +1,27 @@
1
+ CREATE TABLE rer_erasure_operations (
2
+ id TEXT PRIMARY KEY NOT NULL,
3
+ entity TEXT NOT NULL,
4
+ subject TEXT,
5
+ label TEXT NOT NULL,
6
+ phase TEXT NOT NULL CHECK(phase IN ('deleting','blobs','complete')),
7
+ step INTEGER NOT NULL DEFAULT 0,
8
+ planned TEXT NOT NULL,
9
+ deleted TEXT NOT NULL DEFAULT '{}',
10
+ retained TEXT NOT NULL,
11
+ blobs_total INTEGER NOT NULL DEFAULT 0,
12
+ blobs_deleted INTEGER NOT NULL DEFAULT 0,
13
+ requested_at TEXT NOT NULL,
14
+ committed_at TEXT,
15
+ completed_at TEXT
16
+ );
17
+ CREATE UNIQUE INDEX rer_erasure_active_subject ON rer_erasure_operations(entity,subject) WHERE subject IS NOT NULL;
18
+ CREATE TABLE rer_erasure_blobs (
19
+ operation TEXT NOT NULL, bucket TEXT NOT NULL, key TEXT NOT NULL, PRIMARY KEY(operation,bucket,key)
20
+ );
21
+ CREATE TABLE rer_erasure_guard (value INTEGER NOT NULL CHECK(value=1),
22
+ scope INTEGER NOT NULL DEFAULT 1 CONSTRAINT erasure_scope_changed CHECK(scope=1),
23
+ retained INTEGER NOT NULL DEFAULT 1 CONSTRAINT erasure_retained_missing CHECK(retained=1));
24
+ CREATE TABLE rer_erasure_scope (
25
+ operation TEXT NOT NULL, kind TEXT NOT NULL, table_name TEXT NOT NULL, key_json TEXT NOT NULL, row_json TEXT NOT NULL,
26
+ PRIMARY KEY(operation,kind,table_name,key_json,row_json)
27
+ );
@@ -0,0 +1,29 @@
1
+ import { all, statement, canonicalJson, quote } from './sql.mjs';
2
+ import { tableKeys } from './plan.mjs';
3
+
4
+ /** Temporary private scope, cleared atomically at completion. It lets a later
5
+ * batch find new copies through parents that an earlier batch already deleted. */
6
+ export async function readScope(db, id) {
7
+ return (await all(db, "SELECT table_name,key_json,row_json FROM rer_erasure_scope WHERE operation=? AND kind='graph'", id)).map(r => ({ table: r.table_name, key: JSON.parse(r.key_json), row: JSON.parse(r.row_json) }));
8
+ }
9
+ export async function scopeStatements(db, d, plan, id, entries = plan.entries) {
10
+ const out = [];
11
+ for (const { table, row, key } of entries) {
12
+ out.push(statement(db, "INSERT OR IGNORE INTO rer_erasure_scope VALUES(?,'graph',?,?,?)", id, table, canonicalJson(key), canonicalJson(row)));
13
+ }
14
+ for (const { table } of d.retain) {
15
+ const { pk } = await tableKeys(db, table);
16
+ const key = `json_object(${pk.flatMap(c => ["'" + c.replaceAll("'", "''") + "'", quote(c)]).join(',')})`;
17
+ out.push(statement(db, `INSERT OR IGNORE INTO rer_erasure_scope SELECT ?,'retain',?,${key},${key} FROM ${quote(table)}`, id, table));
18
+ }
19
+ if (d.root.redact && plan.root) {
20
+ const identity = Object.fromEntries((d.root.redact.identityColumns ?? []).map(c => [c, plan.root.row[c]]));
21
+ out.push(statement(db, "INSERT INTO rer_erasure_scope SELECT ?,'identity',?,?,? WHERE NOT EXISTS(SELECT 1 FROM rer_erasure_scope WHERE operation=? AND kind='identity')", id, d.root.table, canonicalJson(plan.subject), canonicalJson(identity), id));
22
+ }
23
+ return out;
24
+ }
25
+ export async function readRedaction(db, id) {
26
+ const rows = await all(db, "SELECT kind,row_json FROM rer_erasure_scope WHERE operation=? AND kind IN ('identity','redaction')", id);
27
+ return Object.fromEntries(rows.map(r => [r.kind, JSON.parse(r.row_json)]));
28
+ }
29
+ export const forgetScope = (db, id) => statement(db, 'DELETE FROM rer_erasure_scope WHERE operation=?', id);
@@ -0,0 +1,23 @@
1
+ export class ErasureError extends Error {
2
+ constructor(code, status = 400) { super(code); this.code = code; this.status = status; }
3
+ }
4
+ export function insist(condition, code, status = 400) { if (!condition) throw new ErasureError(code, status); }
5
+ export const quote = name => '"' + name.replaceAll('"', '""') + '"';
6
+ export const statement = (db, sql, ...values) => values.length ? db.prepare(sql).bind(...values) : db.prepare(sql);
7
+ export const all = async (db, sql, ...values) => (await statement(db, sql, ...values).all()).results;
8
+ // A failed guard aborts the whole D1 batch through the CHECK constraint.
9
+ export const guard = (db, condition, ...values) =>
10
+ statement(db, `INSERT INTO rer_erasure_guard(value) SELECT CASE WHEN (${condition}) THEN 1 ELSE 0 END`, ...values);
11
+ export function canonicalJson(value) {
12
+ if (Array.isArray(value)) return '[' + value.map(canonicalJson).join(',') + ']';
13
+ if (value && typeof value === 'object') return '{' + Object.keys(value).sort().map(k => JSON.stringify(k) + ':' + canonicalJson(value[k])).join(',') + '}';
14
+ return JSON.stringify(value);
15
+ }
16
+ export async function sha256(text) {
17
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
18
+ return [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, '0')).join('');
19
+ }
20
+ export function randomHex(bytes) {
21
+ return [...crypto.getRandomValues(new Uint8Array(bytes))].map(b => b.toString(16).padStart(2, '0')).join('');
22
+ }
23
+ export const chunks = (list, size) => Array.from({ length: Math.ceil(list.length / size) }, (_, i) => list.slice(i * size, (i + 1) * size));
@@ -0,0 +1,69 @@
1
+ import { quote, statement, chunks } from './sql.mjs';
2
+ import { matchSql, tableKeys } from './plan.mjs';
3
+
4
+ const verify = (db, kind, condition, ...values) => statement(db, `INSERT INTO rer_erasure_guard(value,${kind}) SELECT 1,CASE WHEN (${condition}) THEN 1 ELSE 0 END`, ...values);
5
+
6
+ /** Re-read every declared path and audit inside the same atomic batch as the
7
+ * final root effect and portability fence. A new dependent rolls the batch back
8
+ * and returns to collection; it cannot be silently cascaded away. */
9
+ export async function verificationStatements(db, d, plan, id, label, { final = false, intake = !final } = {}) {
10
+ const out = [];
11
+ if (intake) {
12
+ for (const [table, entries] of plan.rows) {
13
+ const columns = [...new Set([...entries.values()].flatMap(e => Object.keys(e.row)))];
14
+ for (const part of chunks([...entries.values()], 200)) out.push(verify(db, 'scope', `NOT EXISTS(SELECT 1 FROM json_each(?) j WHERE NOT EXISTS(SELECT 1 FROM ${quote(table)} WHERE ${columns.map((c, i) => `${quote(c)} IS json_extract(j.value,'$[${i}]')`).join(' AND ')}))`, JSON.stringify(part.map(e => columns.map(c => e.row[c])))));
15
+ }
16
+ // The schema can contain another FK path into a selected parent. Protect it
17
+ // in the transaction too, including rows inserted after collection.
18
+ for (const { table, parentTable, group } of plan.foreignKeys) {
19
+ const pk = plan.keys.get(table).pk, parentPk = plan.keys.get(parentTable).pk;
20
+ const parents = [...plan.rows.get(parentTable).values()].map(e => parentPk.map(c => e.key[c]));
21
+ const children = [...plan.rows.get(table).values()].map(e => pk.map(c => e.key[c]));
22
+ const keyMatch = (alias, columns) => columns.map((c, i) => `${alias}.${quote(c)} IS json_extract(j.value,'$[${i}]')`).join(' AND ');
23
+ const joined = group.map(f => `c.${quote(f.from)}=p.${quote(f.to || parentPk[f.seq])}`).join(' AND ');
24
+ out.push(verify(db, 'scope', `NOT EXISTS(SELECT 1 FROM ${quote(table)} c JOIN ${quote(parentTable)} p ON ${joined} WHERE EXISTS(SELECT 1 FROM json_each(?) j WHERE ${keyMatch('p', parentPk)}) AND NOT EXISTS(SELECT 1 FROM json_each(?) j WHERE ${keyMatch('c', pk)}))`, JSON.stringify(parents), JSON.stringify(children)));
25
+ }
26
+ for (const edge of d.edges) for (const m of matchSql(edge, [...plan.known.get(edge.from).values()])) {
27
+ const pk = plan.keys.get(edge.table).pk, keys = [...plan.rows.get(edge.table).values()].map(e => pk.map(c => e.key[c]));
28
+ const selected = `EXISTS(SELECT 1 FROM json_each(?) k WHERE ${pk.map((c, i) => `${quote(edge.table)}.${quote(c)} IS json_extract(k.value,'$[${i}]')`).join(' AND ')})`;
29
+ out.push(verify(db, 'scope', `NOT EXISTS(SELECT 1 FROM ${quote(edge.table)} WHERE ${m.where} AND NOT (${selected}))`, ...m.values, JSON.stringify(keys)));
30
+ }
31
+ }
32
+ if (final) {
33
+ for (const edge of d.edges) for (const m of matchSql(edge, [...plan.known.get(edge.from).values()])) {
34
+ out.push(verify(db, 'scope', `NOT EXISTS(SELECT 1 FROM ${quote(edge.table)} WHERE ${m.where})`, ...m.values));
35
+ }
36
+ for (const item of d.audit) {
37
+ const keys = [...plan.known.get(item.of).values()].map(row => row[plan.keys.get(item.of).pk[0]]);
38
+ for (const part of chunks(keys, 200)) out.push(verify(db, 'scope', `NOT EXISTS(SELECT 1 FROM ${quote(item.table)} WHERE ${quote(item.column)} IN (SELECT value FROM json_each(?))${item.where ? ` AND (${item.where})` : ''})`, JSON.stringify(part)));
39
+ const clear = Object.entries(item.clear ?? {});
40
+ if (clear.length) out.push(verify(db, 'scope', `NOT EXISTS(SELECT 1 FROM ${quote(item.table)} WHERE ${quote(item.column)} IS ? AND (${clear.map(([c]) => `${quote(c)} IS NOT ?`).join(' OR ')})${item.where ? ` AND (${item.where})` : ''})`, label, ...clear.map(([, value]) => value)));
41
+ }
42
+ if (d.root.redact) {
43
+ const pk = plan.keys.get(d.root.table).pk, set = plan.rootSet, identity = d.root.redact.identityColumns ?? [];
44
+ out.push(verify(db, 'retained', `EXISTS(SELECT 1 FROM ${quote(d.root.table)} WHERE ${[...pk.map(c => `${quote(c)} IS ?`), ...identity.map(c => `${quote(c)} IS ?`), ...Object.keys(set).map(c => `${quote(c)} IS ?`)].join(' AND ')})`, ...pk.map(c => plan.subject[c]), ...identity.map(c => (plan.rootIdentity ?? plan.root.row)[c]), ...Object.values(set)));
45
+ }
46
+ }
47
+ for (const { table } of d.retain) {
48
+ const { pk } = await tableKeys(db, table);
49
+ out.push(verify(db, 'retained', `NOT EXISTS(SELECT 1 FROM rer_erasure_scope s WHERE s.operation=? AND s.kind='retain' AND s.table_name=? AND NOT EXISTS(SELECT 1 FROM ${quote(table)} t WHERE ${pk.map(c => `t.${quote(c)} IS json_extract(s.key_json,'$.${c}')`).join(' AND ')}))`, id, table));
50
+ }
51
+ return out;
52
+ }
53
+
54
+ export function rootGoneStatement(db, d, plan) {
55
+ const pk = plan.keys.get(d.root.table).pk;
56
+ return verify(db, 'scope', `NOT EXISTS(SELECT 1 FROM ${quote(d.root.table)} WHERE ${pk.map(c => `${quote(c)} IS ?`).join(' AND ')})`, ...pk.map(c => plan.subject[c]));
57
+ }
58
+
59
+ /** Immediately before a parent DELETE, its physical FK children must be gone.
60
+ * This also protects against children created by a trigger in the same batch. */
61
+ export function childAbsenceStatements(db, plan, entry) {
62
+ const out = [], parentPk = plan.keys.get(entry.table).pk;
63
+ for (const { table, parentTable, group } of plan.foreignKeys.filter(f => f.parentTable === entry.table)) {
64
+ const join = group.map(f => `c.${quote(f.from)}=p.${quote(f.to || parentPk[f.seq])}`).join(' AND ');
65
+ const self = table === entry.table ? ` AND NOT (${parentPk.map(c => `c.${quote(c)} IS p.${quote(c)}`).join(' AND ')})` : '';
66
+ out.push(verify(db, 'scope', `NOT EXISTS(SELECT 1 FROM ${quote(table)} c JOIN ${quote(parentTable)} p ON ${join} WHERE ${parentPk.map(c => `p.${quote(c)} IS ?`).join(' AND ')}${self})`, ...parentPk.map(c => entry.key[c])));
67
+ }
68
+ return out;
69
+ }
@@ -0,0 +1,50 @@
1
+ # Integrations core public contract (v2)
2
+
3
+ Source exports are `core/index.ts`. Types live in `core/types.ts`. No dependencies are required; Node SQLite and Node HTTPS adapters are separate imports (`core/sqlite.ts`, `core/node-transport.ts`) from the portable D1/core code.
4
+
5
+ Provider units implement `DeliveryProtocol` with `id`, async `prepare(envelope, nowMilliseconds)` returning `{url, headers, body}`, and synchronous `classify({status, headers, body}, now)` returning an `Outcome`. `prepare` reads trusted credential/destination configuration using `envelope.destinationId`; never accept arbitrary bot URLs or caller credentials. URL must be stable for a destination version. Core hashes the prepared URL at enqueue and rejects configuration drift at dispatch. `body` must exactly equal the immutable envelope body. Prepare must perform no HTTP requests. Providers expose their own serialization and configuration factory; pass the resulting protocol to core. Classifier returns accepted/rejected/retryable/uncertain, fixed safe `code`, optional status and retryAfterMs. Never include raw provider text, URLs, auth, or personal data in codes. HTTP errors without proven rejection are uncertain. All lost responses and classifier errors become uncertain. Non-idempotent providers never automatically replay uncertain attempts.
6
+
7
+ `new DeliveryService({repository, protocols, transport, now?, hashDestination?})`; `enqueue(envelope, {maxAttempts?, retryDelaysMs?}?)`; `dispatch(id)`; `dispatchDue(limit=100)`; `recoverExpired()`; `get(id)`; `log(id)`; `resolveUncertain(id, 'accepted'|'rejected', evidenceCode)`. Calls are tenant-scoped by the repository. Logical ID, tenant, destination ID, protocol, event and exact body are immutable. Reused IDs with changed content reject. Default maximum 4 attempts, delays 1s/5s/30s; explicit retryable outcomes are bounded. Accepted means protocol acknowledgement, not confirmed human receipt. Expired claims remain uncertain with durable history. Reconciliation is a trusted operator call; no general replay switch exists.
8
+
9
+ `hashDestination?: (url: string) => Promise<string>` is a trusted consumer callback, called with the exact prepared URL after URL/body validation at both enqueue and dispatch. It must return 64 lowercase hexadecimal characters, optionally prefixed by a 1–16 character version identifier (lowercase ASCII letter first, then lowercase letters, digits or hyphens) and `:`; for example `hmac-v1:<64 lowercase hex characters>`. Other output rejects before insertion or transport; dispatch records only `rejected` with code `preflight_failed`. Without this option, the SHA-256 URL digest and all default delivery/request fields are unchanged. The resulting string participates in the existing immutable fingerprint and destination comparison. Hashing does not change the wire request, retry policy, claim check or uncertain-send semantics.
10
+
11
+ Consumers needing protection against offline URL guessing can capture a separately configured 32+ random-byte HMAC key in this callback and return a versioned HMAC-SHA256 digest. One consumer format is `hmac-v1:` plus lowercase HMAC-SHA256 of the UTF-8 lowercase SHA-256 hex digest of the prepared URL; it also permits consumer-owned rekeying of existing URL digests without recovering plaintext. Base performs no URL normalization in the callback path. The consumer owns canonical URL preparation, stable algorithm/key custody across enqueue and dispatch, immutable destination versions, and any coordinated migration of existing hashes and fingerprints. Changing the algorithm/key without that migration rejects existing work. Keep the key outside delivery storage and transport requests. Default SHA-256 is unkeyed, and this option does not encrypt destinations or automatically protect consumers that do not adopt it.
12
+
13
+ `new IntegrationsRepository(sql, namespace)` uses an actual transactional SQLite/D1 database. `D1SqlDatabase(binding)` adapts D1; `SqliteDatabase(path)` adapts Node built-in SQLite. Apply `core/schema.sql` once in the consumer migration chain; numbering is the integrator's. Namespace must be server-selected per installation/tenant; envelope tenant must equal namespace. Never expose repository or service management calls directly to unauthenticated input.
14
+
15
+ `createWebhookProtocol({resolveDestination, signingKey, timestampedBodySignature?})` accepts a trusted `(destinationId)=>Promise<string>` and async `signingKey(envelope)=>Promise<Uint8Array>`. 32+ random-byte HMAC keys, held outside delivery storage. Signatures bind version, tenant, destination ID, event, logical ID, timestamp and exact UTF-8 body. Receiver uses `verifyWebhook({body,headers,key,tenant,destinationId,now?,toleranceMs?})`, then `repository.acceptReplay(verified)` BEFORE applying its effect. Replay claims are permanent; duplicate effect must not be reapplied. For transactional effect processing consume the durable replay inbox, with the edition's effect in the same DB transaction; external effect uncertainty still needs reconciliation. Receiver signature success alone is not replay protection.
16
+
17
+ `timestampedBodySignature: {timestampHeader: string, signatureHeader: string, signingKey?: (envelope: Envelope) => Promise<Uint8Array>}` is an optional generic compatibility format, also exported as the `TimestampedBodySignature` type. It adds an ISO 8601 UTC timestamp (`new Date(timestampSeconds * 1000).toISOString()`, with `.000Z`) and unprefixed, unpadded base64url HMAC-SHA256 over UTF-8 `isoTimestamp + '.' + exactBodyString`. Both timestamps represent the same whole-second instant. Exact Unicode, whitespace, newlines and BOM are preserved. Header names must be HTTP token names, unique ignoring case, and must not collide ignoring case with `content-type`, `webhook-id`, `webhook-tenant`, `webhook-destination`, `webhook-event`, `webhook-timestamp` or `webhook-signature`. Configuration is validated and captured at creation; no metadata override is provided. The optional key callback resolves a separate key; otherwise the already-resolved Base key is reused. Both keys must be `Uint8Array` values of at least 32 bytes. A consumer whose established contract signs with a random secret string may explicitly encode its literal UTF-8 bytes for either key instead of decoding it as hex/base64. Keys and configured values never enter delivery rows or logs.
18
+
19
+ Without this option the request is byte-identical to the default Base format. With it the Base signature and headers remain intact, and `verifyWebhook` continues to verify only the Base format. The additional signature does not bind Base metadata; its receiver owns freshness and replay/effect handling. It changes no classifier, uncertain-send, retry, transport or security semantics.
20
+
21
+ The compatibility header names also cannot replace transport-controlled or authorization headers: `host`, `connection`, `content-length`, `transfer-encoding`, `proxy-authorization`, `authorization`, `keep-alive`, `proxy-connection`, `te`, `trailer` and `upgrade`. These reject at protocol creation even when using a Worker transport.
22
+
23
+ `new TokenService(repository, authority, now?)`: `issue(context,{name,scopes,expiresAt?})`, `authorize(token,requiredScopes,{requestId,resource})` (durably records every allowed/denied attempt and returns safe metadata or null), `revoke(context,id)`, `list(context)`, `usage(context)`. TokenAuthority's trusted async grant callback returns `{scopes, commitGuard: {sql, params}}`, and administer rejects unauthorized administration. `commitGuard.sql` is a trusted SELECT that returns a row only if the authenticated issuer is still active/authorized and every durable revision used to derive `scopes` is unchanged. Core embeds it as an EXISTS predicate inside the token INSERT in the same SQLite/D1 batch as the issuance audit. A stale/missing issuer or grant revision rejects issuance, returns no token and creates no issued audit event. An audit failure rolls back issuance. A plain scope array or missing guard rejects; there is no compatibility fallback. The consumer must store authority in the repository database and update its revision(s) in every transaction changing issuer permissions or grant ceilings, including removal and reactivation. A query checking a constant/preflight value or a different database does not implement this contract. Every use checks current scopes, expiry/revocation in SQL and records usage atomically. Plaintext is returned only by issue, database persists SHA-256 hash only.
24
+
25
+ Transport is required and trusted: `TrustedTransport.send` MUST enforce public IP validation at connect time, verified TLS, no redirects and bounded response/request time. `NodePinnedTransport` is the supplied production enforcing implementation. Worker consumers must supply a trusted pinned-resolution egress service/runtime port; plain fetch is explicitly unsupported. Only tests may provide a transport mapping synthetic HTTPS URLs to a real loopback receiver. There is no production test flag.
26
+
27
+ `isPublicAddress` uses the single maintained range policy in [public-address.ts](public-address.ts), shared by `validateDestination` for literal URLs and `NodePinnedTransport` for **every** DNS answer before connecting. Mixed public/non-public answers reject as a whole. The conservative policy excludes private, loopback, link-local, multicast, reserved, documentation, benchmarking and special-purpose allocations, even when a special-purpose address is globally reachable. This includes Azure `168.63.129.16`, IPv4 <code>192.31.196.0/24</code>, <code>192.52.193.0/24</code>, <code>192.175.48.0/24</code>, IPv6 `2620:4f:8000::/48`, `3ffe::/16`, and both ISATAP interface-identifier forms. IPv6 must be ordinary `2000::/3` unicast outside the exclusions; mapped/translation/transition forms reject. Base retains its broader <code>192.0.0.0/16</code> and `3fff::/16` exclusions. Compressed and expanded IPv6 follow the same policy. The direct IPv4 guard requires four canonical decimal octets (0–255), with no leading zeros except a single `0`. URL parsing normalizes IP aliases before literal validation; malformed addresses reject. The portable guard imports no Node APIs.
28
+
29
+ ## Adoption details
30
+
31
+ `dispatchDue` discovers due jobs from durable storage after restart; schedule it from a consumer queue/cron. Claims last 60 seconds, and the supplied transport bounds DNS to 15 seconds and a connection/request to a further 15 seconds by default. User-supplied prepare callbacks should also finish within the claim horizon. Core checks the durable claim and its expiry again after preparation; an expired or reconciled claim cannot start a late request. An expired in-flight attempt is never overwritten by a late response. Any database exception propagates to the caller; the application must expose that failure. No send occurs without a committed attempt row. A retryable final attempt has state `rejected` and retains its explicit retryable provider reason in the attempt log.
32
+
33
+ Signature canonical input is the UTF-8 encoding of JSON.stringify(["integrations.webhook.v1", tenant, destinationId, event, id, timestampSeconds, exactBodyString]). Outbound bodies must round-trip through UTF-8 without replacement. The header set is `webhook-id`, `webhook-tenant`, `webhook-destination`, `webhook-event`, `webhook-timestamp`, `webhook-signature` (`v1=` plus lower-case SHA-256 hex). A request is fresh within 300 seconds by default (configurable downward only). The key is raw random bytes, not a password or hex string; decode a configured hex/base64 secret to bytes before calling. This version has one active verification key; coordinated key rotation is consumer-owned and no implicit multi-key behavior is claimed.
34
+
35
+ Receiver `body` is the original `Uint8Array`, obtained from `new Uint8Array(await request.arrayBuffer())` in Fetch/Workers or concatenated raw Node request chunks. Verification decodes strict UTF-8 with `{fatal: true, ignoreBOM: true}`: malformed encodings reject, and a leading UTF-8 BOM remains part of the signed text. No Unicode normalization or JSON parsing occurs. The existing string input is only for caller-owned canonical text and rejects unpaired surrogates; it cannot prove bytes already lost by `Request.text()`, `Buffer.toString('utf8')`, a default TextDecoder, or middleware. Those are unsupported receiver boundaries. Pass only the returned `verified.envelope.body` to subsequent parsing and inbox processing. See the [native receiver guide](../receiver.md).
36
+
37
+ Receiver replay rows are a durable inbox, not proof an external side effect completed. `acceptReplay` returns true exactly once for a logical ID and rejects changed signed contents, even after restart. Do not erase replay rows while the logical ID can be reused. For transactional business effects, adopt `integration_replays` as an inbox and maintain the application's processed marker in the same database transaction as that effect; do not blindly execute an effect again after a receiver crash. Response-lost external effects require reconciliation just like outbound delivery. There is deliberately no automatic uncertain-send retry, even for custom webhooks.
38
+
39
+ Secrets never enter delivery rows: URL hashes bind the trusted destination version, while full secret URLs, bearer headers and signing keys are resolved only for the current attempt. Payloads are durable product data and must not contain credentials. Logs retain machine codes, status, timestamps, feature/protocol identity through the envelope, and reconciliation records. They do not retain provider bodies/error messages. Consumer credentials, destination lookup, authorization contexts, scheduling, egress declarations and UI/operator audit actors remain consumer wiring. Do not treat any caller-controlled `Context` or destination registration as trusted without native server authorization.
40
+
41
+ Retry policy (maximum attempts and delay vector) also participates in immutable enqueue identity. Attempts are capped at ten and each configured delay at one day. A retry-after delay above one day terminates as `rejected` with code `retry_delay_exceeded` rather than retrying earlier than the destination requested. There is no total elapsed retry horizon; multiple delays and late dispatch can extend beyond one day. Issuance/revocation and their feature-tagged audit events commit together; token event rows survive token deletion. Integrating editions may supplement these with native human actor audit identity.
42
+
43
+
44
+ ## Token API v2 adoption
45
+
46
+ `DeliveryProtocol`, signing, delivery and transport signatures are unchanged. Token grants require a commit guard and token authorization requires concrete request context. For example, a trusted consumer reading its own `issuers(namespace,id,revision,active,scopes)` record can return the parsed scopes plus `commitGuard: { sql: 'SELECT 1 FROM issuers WHERE namespace=? AND id=? AND revision=? AND active=1', params: [namespace, authenticatedIssuerId, readRevision] }`. Revisions must never be reused across removal/recreation. If scope ceilings depend on multiple rows (roles, application approvals, session state), the guard must bind/check all of them. The SQL is supplied only by trusted server policy, never by request input. It is a SELECT without a trailing semicolon; it may join native consumer tables and uses positional `?` parameters. Parameter arrays and scopes are copied before asynchronous hashing. Core owns no issuer registry or identity policy.
47
+
48
+ Each `authorize` call must supply `{requestId, resource}` from the handling application's request/resource identifiers, including denied or unknown-token attempts. Both fields use the existing safe identifier grammar: 1–200 ASCII characters, first alphanumeric, then alphanumeric or `_.:/-`. They must not contain credentials or raw URL/query/body data. Missing/invalid context rejects before authorization and does not invent a usage receipt. The recorded fields bind the decision to the supplied request and resource; this does not make resource-level access decisions on behalf of the consumer. Required scopes remain consumer-selected, and repeated request IDs produce separate attempt receipts. Request context is copied before asynchronous work. Both allowed and denied valid calls commit their context together with the live token decision. Usage storage failure rejects visibly.
49
+
50
+ `schema.sql` remains an unnumbered fresh-install proposal. Consumers that already applied the earlier proposal must separately apply `upgrade-token-usage-context.sql` exactly once in their migration chain (numbered by their integrator); rerunning CREATE TABLE IF NOT EXISTS is not an upgrade. That expansion preserves historical usage with null request/resource rather than inventing provenance. New service calls always write both validated fields. Audit and usage survive credential deletion.
@@ -0,0 +1,27 @@
1
+ # Durable integration core
2
+
3
+ Import portable delivery, signed webhook, token and D1 APIs from `./index.ts`.
4
+ Import Node adapters explicitly from `./sqlite.ts` and `./node-transport.ts`.
5
+ Node 26 is the tested runtime; no runtime npm dependency is needed.
6
+
7
+ The [consumer guide](../README.md) contains fresh SQLite migration, enqueue and
8
+ Worker scheduling examples. The [public contract](CONTRACT.md) defines exact
9
+ HMAC framing, retry outcomes, durable replay and same-database token authority.
10
+ The [receiver guide](../receiver.md) supplies executable Node and Fetch receiver
11
+ boundaries that verify original bytes before claiming replay identities.
12
+
13
+ Apply `schema.sql` once through the application's numbered migration ledger.
14
+ Existing installations of the earlier proposal instead apply
15
+ `upgrade-token-usage-context.sql` once. Namespaces and authority callbacks come
16
+ from native authenticated server state. Use a durable database, trusted secret
17
+ storage and a transport enforcing public destinations at connect time. Worker
18
+ consumers must supply an enforcing egress binding; ordinary fetch is insufficient.
19
+
20
+ Schedule `dispatchDue()` after restart and at future due times. Expose durable
21
+ attempts and uncertain outcomes in the native operator experience. Receiving a
22
+ signed delivery and claiming its replay ID do not complete a domain effect:
23
+ commit an inbox processed marker with the effect in the application's transaction.
24
+
25
+ Run `node --test modules/integrations/tests/integration.test.ts` from Base's root
26
+ for the retained integration suites, including local HTTP/TLS, SQLite and actual
27
+ D1 tests. These checks establish local behavior, not production delivery.
@@ -0,0 +1,19 @@
1
+ const encoder = new TextEncoder();
2
+ export const hex = (bytes: Uint8Array) => Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
3
+ export async function sha256(text: string) { return hex(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(text)))); }
4
+ export function randomId() { return crypto.randomUUID(); }
5
+ export function assertLabel(value: string, name = 'identifier') {
6
+ if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,199}$/.test(value)) throw new Error(`Invalid ${name}`);
7
+ }
8
+ export function assertCode(code: string) { if (!/^[a-zA-Z0-9_.-]{1,100}$/.test(code)) throw new Error('Invalid safe outcome code'); }
9
+ export async function hmacBytes(key: Uint8Array, data: string) {
10
+ if (!(key instanceof Uint8Array) || key.length < 32) throw new Error('HMAC key must contain at least 32 bytes');
11
+ const imported = await crypto.subtle.importKey('raw', new Uint8Array(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
12
+ return new Uint8Array(await crypto.subtle.sign('HMAC', imported, encoder.encode(data)));
13
+ }
14
+ export async function hmac(key: Uint8Array, data: string) { return hex(await hmacBytes(key, data)); }
15
+ export function equalSignature(a: string, b: string) {
16
+ if (!/^[0-9a-f]{64}$/.test(a) || !/^[0-9a-f]{64}$/.test(b)) return false;
17
+ let result = 0; for (let i = 0; i < a.length; i++) result |= a.charCodeAt(i) ^ b.charCodeAt(i);
18
+ return result === 0;
19
+ }
@@ -0,0 +1,21 @@
1
+ export interface SqlStatement { sql: string; params?: (string | number | null)[] }
2
+ export interface SqlDatabase {
3
+ /** All statements execute in one transaction; any failure rolls everything back. */
4
+ batch(statements: SqlStatement[]): Promise<Record<string, unknown>[][]>;
5
+ }
6
+ interface D1Statement {
7
+ bind(...values: (string | number | null)[]): D1Statement;
8
+ }
9
+ export interface D1Binding {
10
+ prepare(sql: string): D1Statement;
11
+ batch(statements: D1Statement[]): Promise<{ results: Record<string, unknown>[]; success: boolean }[]>;
12
+ }
13
+ export class D1SqlDatabase implements SqlDatabase {
14
+ private binding: D1Binding;
15
+ constructor(binding: D1Binding) { this.binding = binding; }
16
+ async batch(statements: SqlStatement[]) {
17
+ const result = await this.binding.batch(statements.map(s => this.binding.prepare(s.sql).bind(...(s.params ?? []))));
18
+ if (result.some(r => !r.success)) throw new Error('Integration database transaction failed');
19
+ return result.map(r => r.results);
20
+ }
21
+ }
@@ -0,0 +1,97 @@
1
+ import { assertCode, assertLabel, sha256 } from './crypto.ts';
2
+ import { validateDestination } from './destination.ts';
3
+ import { IntegrationsRepository, pickEnvelope } from './repository.ts';
4
+ import type { Delivery, DeliveryProtocol, Envelope, Outcome, TrustedTransport, WireRequest } from './types.ts';
5
+
6
+ export class DeliveryService {
7
+ private repository: IntegrationsRepository;
8
+ private protocols: Map<string, DeliveryProtocol>;
9
+ private transport: TrustedTransport;
10
+ private now: () => number;
11
+ private hashDestination: (url: string) => Promise<string>;
12
+ constructor(options: { repository: IntegrationsRepository; protocols: DeliveryProtocol[]; transport: TrustedTransport; now?: () => number; hashDestination?: (url: string) => Promise<string> }) {
13
+ this.repository = options.repository; this.protocols = new Map(); this.transport = options.transport; this.now = options.now ?? Date.now;
14
+ this.hashDestination = options.hashDestination ?? sha256;
15
+ for (const protocol of options.protocols) { assertLabel(protocol.id); if (this.protocols.has(protocol.id)) throw new Error('Duplicate protocol'); this.protocols.set(protocol.id, protocol); }
16
+ }
17
+ private protocol(id: string) { const protocol = this.protocols.get(id); if (!protocol) throw new Error('Unknown delivery protocol'); return protocol; }
18
+ private async destinationHash(url: string) {
19
+ const hash = await this.hashDestination(url);
20
+ if (typeof hash !== 'string' || !/^(?:[a-z][a-z0-9-]{0,15}:)?[0-9a-f]{64}(?![\s\S])/.test(hash)) throw new Error('Invalid destination hash');
21
+ return hash;
22
+ }
23
+ private checkWire(wire: WireRequest, envelope: Envelope) {
24
+ validateDestination(wire.url);
25
+ if (wire.body !== envelope.body) throw new Error('Protocol changed immutable body');
26
+ const bytes = new TextEncoder().encode(wire.body);
27
+ if (bytes.length > 1048576 || new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes) !== wire.body) throw new Error('Delivery body must be bounded valid UTF-8');
28
+ }
29
+ async enqueue(input: Envelope, options: { maxAttempts?: number; retryDelaysMs?: number[] } = {}) {
30
+ const envelope = pickEnvelope(input);
31
+ for (const label of [envelope.id, envelope.tenant, envelope.destinationId, envelope.event, envelope.protocol]) assertLabel(label);
32
+ if (envelope.tenant !== this.repository.namespace || typeof envelope.body !== 'string') throw new Error('Invalid envelope tenant or body');
33
+ const maxAttempts = options.maxAttempts ?? 4, delays = options.retryDelaysMs ?? [1000, 5000, 30000];
34
+ if (!Array.isArray(delays)) throw new Error('Invalid bounded retry policy');
35
+ const retryDelaysMs = [...delays];
36
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 10 || retryDelaysMs.length < maxAttempts - 1 || retryDelaysMs.some(d => !Number.isSafeInteger(d) || d < 1 || d > 86400000)) throw new Error('Invalid bounded retry policy');
37
+ const now = this.now(); const wire = await this.protocol(envelope.protocol).prepare(envelope, now); this.checkWire(wire, envelope);
38
+ const destinationHash = await this.destinationHash(wire.url);
39
+ const fingerprint = await sha256(JSON.stringify({ envelope, destinationHash, maxAttempts, retryDelaysMs }));
40
+ return this.repository.insert({ ...envelope, destinationHash, fingerprint, maxAttempts, retryDelaysMs: [...retryDelaysMs], state: 'queued', attempts: 0, createdAt: now, nextAt: now, claim: null, claimUntil: null });
41
+ }
42
+ get(id: string) { return this.repository.get(id); }
43
+ log(id: string) { return this.repository.log(id); }
44
+ recoverExpired() { return this.repository.recoverExpired(this.now()); }
45
+ async resolveUncertain(id: string, kind: 'accepted' | 'rejected', evidenceCode: string) {
46
+ assertCode(evidenceCode); if (kind !== 'accepted' && kind !== 'rejected') throw new Error('Invalid resolution');
47
+ await this.repository.resolve(id, { kind, code: evidenceCode }, this.now());
48
+ return this.get(id);
49
+ }
50
+ async dispatchDue(limit = 100): Promise<(Delivery | null)[]> {
51
+ await this.recoverExpired();
52
+ const results: (Delivery | null)[] = [];
53
+ for (const id of await this.repository.due(this.now(), limit)) results.push(await this.dispatch(id));
54
+ return results;
55
+ }
56
+ async dispatch(id: string): Promise<Delivery | null> {
57
+ await this.recoverExpired();
58
+ const claim = await this.repository.claim(id, this.now(), 60000);
59
+ if (!claim) return this.get(id);
60
+ const delivery = (await this.get(id))!;
61
+ let outcome: Outcome;
62
+ // Preflight failures provably happen before transport; no credential/error text enters the log.
63
+ let wire: WireRequest;
64
+ try {
65
+ wire = await this.protocol(delivery.protocol).prepare(pickEnvelope(delivery), this.now());
66
+ this.checkWire(wire, delivery);
67
+ if (await this.destinationHash(wire.url) !== delivery.destinationHash) throw new Error('Destination changed');
68
+ } catch {
69
+ outcome = { kind: 'rejected', code: 'preflight_failed' };
70
+ if (!await this.repository.finish(id, claim, outcome, this.now(), null)) await this.recoverExpired();
71
+ return this.get(id);
72
+ }
73
+ // Preparing a destination can yield. Recheck the durable claim before starting
74
+ // an external effect; expired or reconciled work must never start a late send.
75
+ const current = await this.get(id);
76
+ if (!current || current.state !== 'sending' || current.claim !== claim || current.claimUntil! <= this.now()) {
77
+ await this.recoverExpired();
78
+ return this.get(id);
79
+ }
80
+ try {
81
+ const response = await this.transport.send(wire);
82
+ outcome = this.protocol(delivery.protocol).classify(response, this.now());
83
+ if (!['accepted', 'rejected', 'retryable', 'uncertain'].includes(outcome.kind)) throw new Error('Invalid classification');
84
+ assertCode(outcome.code);
85
+ if (outcome.status !== undefined && (!Number.isInteger(outcome.status) || outcome.status < 100 || outcome.status > 599)) throw new Error('Invalid status');
86
+ if (outcome.retryAfterMs !== undefined && (!Number.isFinite(outcome.retryAfterMs) || outcome.retryAfterMs < 0)) throw new Error('Invalid retry delay');
87
+ if (outcome.kind === 'retryable' && (outcome.retryAfterMs ?? 0) > 86400000) outcome = { kind: 'rejected', code: 'retry_delay_exceeded', status: outcome.status };
88
+ outcome = { kind: outcome.kind, code: outcome.code, ...(outcome.status === undefined ? {} : { status: outcome.status }), ...(outcome.retryAfterMs === undefined ? {} : { retryAfterMs: outcome.retryAfterMs }) };
89
+ } catch { outcome = { kind: 'uncertain', code: 'response_unknown' }; }
90
+ const now = this.now();
91
+ const nextAt = outcome.kind === 'retryable' && delivery.attempts < delivery.maxAttempts
92
+ ? now + Math.max(delivery.retryDelaysMs[delivery.attempts - 1]!, (outcome.retryAfterMs ?? 0)) : null;
93
+ // Storage failures propagate; a still-held claim later recovers as uncertain, never a replay.
94
+ if (!await this.repository.finish(id, claim, outcome, now, nextAt)) await this.recoverExpired();
95
+ return this.get(id);
96
+ }
97
+ }
@@ -0,0 +1,13 @@
1
+ import { isPublicAddress } from './public-address.ts';
2
+ export { isPublicAddress } from './public-address.ts';
3
+
4
+ export function validateDestination(value: string): URL {
5
+ let url: URL;
6
+ try { url = new URL(value); } catch { throw new Error('Invalid destination'); }
7
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash || (url.port && url.port !== '443') || value.length > 8192) throw new Error('Destination must be public HTTPS without credentials, fragment or nonstandard port');
8
+ const host = url.hostname.toLowerCase();
9
+ if (!host || host.endsWith('.') || host === 'localhost' || /\.(localhost|local|internal|invalid|test)$/.test(host)) throw new Error('Destination hostname is not public');
10
+ if (host.includes(':') || /^[\d.]+$/.test(host)) { if (!isPublicAddress(host)) throw new Error('Destination address is not public'); }
11
+ else if (!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(host)) throw new Error('Invalid destination hostname');
12
+ return url;
13
+ }
@@ -0,0 +1,7 @@
1
+ export * from './types.ts';
2
+ export * from './database.ts';
3
+ export * from './repository.ts';
4
+ export * from './delivery.ts';
5
+ export * from './destination.ts';
6
+ export * from './webhook.ts';
7
+ export * from './tokens.ts';
@@ -0,0 +1,72 @@
1
+ import { lookup } from 'node:dns/promises';
2
+ import { request } from 'node:https';
3
+ import { isIP } from 'node:net';
4
+ import type { IncomingMessage } from 'node:http';
5
+ import { isPublicAddress, validateDestination } from './destination.ts';
6
+ import type { TrustedTransport, WireRequest, WireResponse } from './types.ts';
7
+
8
+ /** Resolves once, validates EVERY answer, and pins that address to the TLS connection's lookup.
9
+ * TLS SNI/certificate verification still uses the original hostname; no redirect is followed. */
10
+ export class NodePinnedTransport implements TrustedTransport {
11
+ private timeoutMs: number;
12
+ private maxResponseBytes: number;
13
+ constructor(options: { timeoutMs?: number; maxResponseBytes?: number } = {}) {
14
+ this.timeoutMs = options.timeoutMs ?? 15000; this.maxResponseBytes = options.maxResponseBytes ?? 65536;
15
+ if (!Number.isSafeInteger(this.timeoutMs) || this.timeoutMs < 1 || this.timeoutMs > 30000 || !Number.isSafeInteger(this.maxResponseBytes) || this.maxResponseBytes < 1 || this.maxResponseBytes > 1048576) throw new Error('Invalid transport limits');
16
+ }
17
+ async send(wire: WireRequest): Promise<WireResponse> {
18
+ const url = validateDestination(wire.url);
19
+ const host = url.hostname.replace(/^\[|\]$/g, '');
20
+ let timer: ReturnType<typeof setTimeout> | undefined;
21
+ const addresses = isIP(host) ? [{ address: host, family: isIP(host) }] : await Promise.race([
22
+ lookup(host, { all: true, verbatim: true }),
23
+ new Promise<never>((_resolve, reject) => { timer = setTimeout(() => reject(new Error('DNS timeout')), this.timeoutMs); }),
24
+ ]).finally(() => { if (timer) clearTimeout(timer); });
25
+ if (!addresses.length || addresses.some(a => !isPublicAddress(a.address))) throw new Error('Destination DNS is not public');
26
+ const pinned = addresses[0]!;
27
+ for (const name of Object.keys(wire.headers)) if (/^(host|connection|transfer-encoding|content-length|proxy-authorization)$/i.test(name)) throw new Error('Forbidden transport header');
28
+ return new Promise((resolve, reject) => {
29
+ let settled = false, response: IncomingMessage | undefined;
30
+ let timer: ReturnType<typeof setTimeout> | undefined;
31
+ const fail = (error: Error) => {
32
+ if (settled) return;
33
+ // Reject synchronously before destroy can race a queued response end event.
34
+ settled = true; clearTimeout(timer); reject(error);
35
+ response?.destroy(); req.destroy();
36
+ };
37
+ const req = request(url, {
38
+ method: 'POST', agent: false, rejectUnauthorized: true,
39
+ headers: { ...wire.headers, 'content-length': Buffer.byteLength(wire.body) },
40
+ lookup: (_hostname, options, callback) => {
41
+ if (options.all) callback(null, [pinned]);
42
+ else callback(null, pinned.address, pinned.family);
43
+ },
44
+ }, res => {
45
+ response = res;
46
+ if (settled) { res.destroy(); return; }
47
+ const chunks: Buffer[] = []; let size = 0;
48
+ res.on('data', (chunk: Buffer) => {
49
+ if (settled) return;
50
+ size += chunk.length;
51
+ if (size > this.maxResponseBytes) fail(new Error('Response limit exceeded'));
52
+ else chunks.push(chunk);
53
+ });
54
+ res.on('error', fail);
55
+ res.on('aborted', () => fail(new Error('Response aborted')));
56
+ res.on('close', () => { if (!res.complete) fail(new Error('Response incomplete')); });
57
+ res.on('end', () => {
58
+ if (settled) return;
59
+ if (!res.complete) { fail(new Error('Response incomplete')); return; }
60
+ const headers: Record<string, string> = {};
61
+ for (const [name, value] of Object.entries(res.headers)) if (value !== undefined) headers[name] = Array.isArray(value) ? value.join(', ') : value;
62
+ settled = true; clearTimeout(timer);
63
+ resolve({ status: res.statusCode ?? 0, headers, body: Buffer.concat(chunks).toString('utf8') });
64
+ });
65
+ });
66
+ timer = setTimeout(() => fail(new Error('Transport timeout')), this.timeoutMs);
67
+ req.on('close', () => { if (!settled) fail(new Error('Transport closed')); });
68
+ req.on('error', fail);
69
+ req.end(wire.body);
70
+ });
71
+ }
72
+ }
@@ -0,0 +1,57 @@
1
+ /** Maintained conservative routing policy for both saved literals and every DNS answer.
2
+ * Special-purpose allocations reject even when some are globally reachable. */
3
+ const nonPublicRanges = {
4
+ ipv4: [
5
+ ['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8],
6
+ ['168.63.129.16', 32], // Azure platform virtual address
7
+ ['169.254.0.0', 16], ['172.16.0.0', 12],
8
+ ['192.0.0.0', 16], // Preserve Base's broader exclusion, including 192.0.2.0/24.
9
+ ['192.31.196.0', 24], ['192.52.193.0', 24], ['192.88.99.0', 24],
10
+ ['192.168.0.0', 16], ['192.175.48.0', 24], ['198.18.0.0', 15],
11
+ ['198.51.100.0', 24], ['203.0.113.0', 24], ['224.0.0.0', 3],
12
+ ],
13
+ ipv6: [
14
+ ['::', 3], ['4000::', 2], ['8000::', 1], // Only ordinary 2000::/3 unicast is eligible.
15
+ ['2001::', 23], ['2001:db8::', 32], ['2002::', 16],
16
+ ['2620:4f:8000::', 48], ['3ffe::', 16],
17
+ ['3fff::', 16], // Preserve Base's broader exclusion than the 3fff::/20 allocation.
18
+ ],
19
+ } as const;
20
+
21
+ function ipv4(address: string): number | null {
22
+ const octets = address.split('.');
23
+ if (octets.length !== 4 || octets.some(octet => !/^(0|[1-9]\d{0,2})$/.test(octet))) return null;
24
+ const bytes = octets.map(Number);
25
+ if (bytes.some(byte => byte > 255)) return null;
26
+ return bytes.reduce((value, byte) => value * 256 + byte, 0);
27
+ }
28
+
29
+ function ipv6(address: string): bigint | null {
30
+ if (!/^[0-9a-f:]+$/.test(address) || !address.includes(':')) return null;
31
+ const pieces = address.split('::');
32
+ if (pieces.length > 2) return null;
33
+ const left = pieces[0] ? pieces[0].split(':') : [];
34
+ const right = pieces[1] ? pieces[1].split(':') : [];
35
+ if ([...left, ...right].some(word => !/^[0-9a-f]{1,4}$/.test(word))) return null;
36
+ const missing = 8 - left.length - right.length;
37
+ if ((pieces.length === 1 && missing !== 0) || (pieces.length === 2 && missing < 1)) return null;
38
+ return [...left, ...Array(pieces.length === 2 ? missing : 0).fill('0'), ...right]
39
+ .reduce((value: bigint, word: string) => (value << 16n) | BigInt(parseInt(word, 16)), 0n);
40
+ }
41
+
42
+ const ipv4Ranges = nonPublicRanges.ipv4.map(([base, bits]) => ({ base: ipv4(base)!, divisor: 2 ** (32 - bits) }));
43
+ const ipv6Ranges = nonPublicRanges.ipv6.map(([base, bits]) => ({ base: ipv6(base)!, shift: BigInt(128 - bits) }));
44
+
45
+ /** Portable Worker/Node guard; DNS and URL callers share this exact policy. */
46
+ export function isPublicAddress(raw: string): boolean {
47
+ if (typeof raw !== 'string') return false;
48
+ const address = (raw.startsWith('[') && raw.endsWith(']') ? raw.slice(1, -1) : raw).toLowerCase();
49
+ const v4 = ipv4(address);
50
+ if (v4 !== null) return !ipv4Ranges.some(({ base, divisor }) => Math.floor(v4 / divisor) === Math.floor(base / divisor));
51
+ const v6 = ipv6(address);
52
+ if (v6 === null) return false;
53
+ // ISATAP embeds IPv4 in an interface identifier under otherwise-public prefixes.
54
+ const interfacePrefix = (v6 >> 32n) & 0xffffffffn;
55
+ if (interfacePrefix === 0x00005efen || interfacePrefix === 0x02005efen) return false;
56
+ return !ipv6Ranges.some(({ base, shift }) => v6 >> shift === base >> shift);
57
+ }