@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,76 @@
1
+ import { fail } from '../core/errors.mjs';
2
+
3
+ const terminal = new Set(['delivered', 'bounced', 'failed', 'rejected', 'complained']);
4
+ // Only this constructor's stores can be passed as the intake's explicit store.
5
+ const stores = new WeakSet();
6
+ export const isD1EventStore = store => stores.has(store);
7
+ const receipt = `INSERT INTO rer_mail_event_receipts
8
+ (provider, tenant_id, event_namespace, event_id, fingerprint, event_json, commit_token, ordinal)
9
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
10
+ ON CONFLICT(provider, tenant_id, event_namespace, event_id) DO UPDATE SET conflict = 1
11
+ WHERE rer_mail_event_receipts.fingerprint <> excluded.fingerprint
12
+ RETURNING event_id`;
13
+ const state = `INSERT INTO rer_mail_event_states
14
+ (provider, tenant_id, event_namespace, provider_message_id, recipient, status, occurred_at, event_id, terminal)
15
+ SELECT ?, ?, ?, ?, ?, ?, ?, ?, ? WHERE EXISTS (
16
+ SELECT 1 FROM rer_mail_event_receipts WHERE provider = ? AND tenant_id = ? AND event_namespace = ?
17
+ AND event_id = ? AND commit_token = ? AND ordinal = ?)
18
+ ON CONFLICT(provider, tenant_id, event_namespace, provider_message_id, recipient) DO UPDATE SET
19
+ status = excluded.status, occurred_at = excluded.occurred_at, event_id = excluded.event_id, terminal = excluded.terminal
20
+ WHERE (excluded.terminal = 1 AND rer_mail_event_states.terminal = 0)
21
+ OR (excluded.occurred_at > rer_mail_event_states.occurred_at
22
+ AND (rer_mail_event_states.terminal = 0 OR excluded.terminal = 1))
23
+ RETURNING status, occurred_at, event_id`;
24
+ const read = `SELECT status, occurred_at, event_id FROM rer_mail_event_states
25
+ WHERE provider = ? AND tenant_id = ? AND event_namespace = ? AND provider_message_id = ? AND recipient = ?`;
26
+ function conflict(error) {
27
+ for (let current = error, count = 0; current && count < 5; current = current.cause, count++) {
28
+ if (typeof current.message === 'string' && current.message.includes('rer_mail_event_identity_conflict')) return true;
29
+ }
30
+ return false;
31
+ }
32
+ /** D1 batch is one real SQL transaction. No read-before-write and no mutex. */
33
+ export function createD1EventStore({ db } = {}) {
34
+ if (!db || typeof db.prepare !== 'function' || typeof db.batch !== 'function') fail('event_storage_required');
35
+ const store = Object.freeze({
36
+ async commit(events) {
37
+ try {
38
+ const token = crypto.randomUUID();
39
+ const statements = [], layout = [];
40
+ for (const [ordinal, event] of events.entries()) {
41
+ const scope = [event.provider, event.tenantId, event.eventNamespace];
42
+ const entry = { eventId: event.eventId, receipt: statements.length, recipients: [] };
43
+ statements.push(db.prepare(receipt).bind(...scope, event.eventId, event.fingerprint, JSON.stringify(event), token, ordinal));
44
+ for (const recipient of event.recipients) {
45
+ const indices = { recipient, update: null, read: null };
46
+ if (event.status !== null) {
47
+ indices.update = statements.length;
48
+ statements.push(db.prepare(state).bind(...scope, event.providerMessageId, recipient, event.status,
49
+ event.occurredAt, event.eventId, terminal.has(event.status) ? 1 : 0,
50
+ ...scope, event.eventId, token, ordinal));
51
+ }
52
+ indices.read = statements.length;
53
+ statements.push(db.prepare(read).bind(...scope, event.providerMessageId, recipient));
54
+ entry.recipients.push(indices);
55
+ }
56
+ layout.push(entry);
57
+ }
58
+ if (!statements.length) return [];
59
+ const results = await db.batch(statements);
60
+ if (results.some(result => result.success === false)) throw new Error();
61
+ return layout.map(entry => {
62
+ if (!results[entry.receipt].results.length) return { eventId: entry.eventId, outcome: 'duplicate', recipients: [] };
63
+ return { eventId: entry.eventId, outcome: 'stored', recipients: entry.recipients.map(item => {
64
+ const row = results[item.read].results[0];
65
+ return { recipient: item.recipient, applied: item.update !== null && results[item.update].results.length > 0,
66
+ state: row ? { status: row.status, occurredAt: row.occurred_at, eventId: row.event_id } : null };
67
+ }) };
68
+ });
69
+ } catch (error) {
70
+ fail(conflict(error) ? 'event_identity_conflict' : 'event_storage_failed');
71
+ }
72
+ },
73
+ });
74
+ stores.add(store);
75
+ return store;
76
+ }
@@ -0,0 +1,3 @@
1
+ export { verifyWebhook, MAX_WEBHOOK_BYTES } from './verify.mjs';
2
+ export { createDeliveryEventIntake } from './intake.mjs';
3
+ export { createD1EventStore } from './d1-storage.mjs';
@@ -0,0 +1,201 @@
1
+ import { fail } from '../core/errors.mjs';
2
+ import { parseAddress } from '../core/validation.mjs';
3
+ import { verifyWebhook } from './verify.mjs';
4
+ import { createDurableEventStore } from './storage.mjs';
5
+ import { isD1EventStore } from './d1-storage.mjs';
6
+ import { pinnedKeys, verifyEventJws } from '../adapters/messaging/jws.mjs';
7
+ import { serviceOrigin, identity, closed } from '../adapters/messaging/contract.mjs';
8
+ import { messagingEvent } from '../adapters/messaging/event-schema.mjs';
9
+
10
+ const MAX_EVENT_BYTES = 1024 * 1024;
11
+ const MAX_EVENTS = 1000;
12
+ const encoder = new TextEncoder();
13
+ const sgStatuses = { processed: 'accepted', deferred: 'deferred', delivered: 'delivered', bounce: 'bounced', dropped: 'rejected', spamreport: 'complained', open: null, click: null, unsubscribe: null, group_unsubscribe: null, group_resubscribe: null };
14
+ const resendStatuses = { 'email.sent': 'accepted', 'email.delivery_delayed': 'deferred', 'email.delivered': 'delivered', 'email.bounced': 'bounced', 'email.failed': 'failed', 'email.suppressed': 'rejected', 'email.complained': 'complained', 'email.opened': null, 'email.clicked': null };
15
+ const cfStatuses = new Set(['delivered', 'deferred', 'bounced', 'failed', 'rejected', 'complained']);
16
+
17
+ function bounded(value, max = 512) {
18
+ if (typeof value !== 'string' || !value.length || value.length > max || value.trim() !== value || /[\x00-\x1f\x7f]/.test(value)) fail('invalid_event_identity');
19
+ return value;
20
+ }
21
+ function object(value) {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('invalid_event_schema');
23
+ return value;
24
+ }
25
+ function recipient(value) {
26
+ const parsed = parseAddress(value);
27
+ if (parsed.name || parsed.email !== value) fail('invalid_event_recipient');
28
+ // Preserve local-part case; lowering it can merge distinct mailbox identities.
29
+ return value;
30
+ }
31
+ function epoch(value, now) {
32
+ if (!Number.isSafeInteger(value) || value < 0 || value > now + 300_000) fail('invalid_event_timestamp');
33
+ return value;
34
+ }
35
+ function date(value, now) {
36
+ if (typeof value !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?(?:Z|[+-]\d\d:\d\d)$/.test(value)) fail('invalid_event_timestamp');
37
+ const day = new Date(`${value.slice(0, 10)}T00:00:00.000Z`);
38
+ if (!Number.isFinite(day.getTime()) || day.toISOString().slice(0, 10) !== value.slice(0, 10) ||
39
+ Number(value.slice(11, 13)) > 23 || Number(value.slice(14, 16)) > 59 || Number(value.slice(17, 19)) > 59) fail('invalid_event_timestamp');
40
+ return epoch(Date.parse(value), now);
41
+ }
42
+ async function fingerprint(bytes) {
43
+ const digest = await crypto.subtle.digest('SHA-256', bytes);
44
+ return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
45
+ }
46
+
47
+ // Called only on already authenticated, successfully parsed JSON arrays. Slice
48
+ // each original event, so batching/reordering does not alter an event fingerprint.
49
+ function eventSlices(text) {
50
+ const slices = [];
51
+ let depth = 0, quoted = false, escaped = false, start = 0;
52
+ for (let i = 0; i < text.length; i++) {
53
+ const char = text[i];
54
+ if (quoted) {
55
+ if (escaped) escaped = false;
56
+ else if (char === '\\') escaped = true;
57
+ else if (char === '"') quoted = false;
58
+ continue;
59
+ }
60
+ if (char === '"') quoted = true;
61
+ else if (char === '[' || char === '{') {
62
+ if (depth === 1) start = i;
63
+ depth++;
64
+ } else if (char === ']' || char === '}') {
65
+ depth--;
66
+ if (depth === 1) slices.push(text.slice(start, i + 1));
67
+ }
68
+ }
69
+ return slices;
70
+ }
71
+
72
+ /** Only returned intake methods can reach these private normalizers. */
73
+ function sendgrid(event, now) {
74
+ object(event);
75
+ if (!Object.hasOwn(sgStatuses, event.event)) fail('unsupported_delivery_event');
76
+ if (!Number.isSafeInteger(event.timestamp) || event.timestamp < 0) fail('invalid_event_timestamp');
77
+ return {
78
+ eventId: bounded(event.sg_event_id), providerMessageId: bounded(event.sg_message_id, 1024),
79
+ type: event.event, status: sgStatuses[event.event], occurredAt: epoch(event.timestamp * 1000, now),
80
+ recipients: [recipient(event.email)],
81
+ deliveryId: event.rer_delivery_id === undefined ? null : bounded(event.rer_delivery_id),
82
+ product: event.rer_product === undefined ? null : bounded(event.rer_product),
83
+ };
84
+ }
85
+ function resend(event, headers, now) {
86
+ object(event); object(event.data);
87
+ if (!Object.hasOwn(resendStatuses, event.type)) fail('unsupported_delivery_event');
88
+ if (!Array.isArray(event.data.to) || !event.data.to.length || event.data.to.length > MAX_EVENTS) fail('invalid_event_recipients');
89
+ const recipients = event.data.to.map(recipient);
90
+ if (new Set(recipients).size !== recipients.length) fail('invalid_event_recipients');
91
+ return {
92
+ eventId: bounded(headers.get('svix-id')), providerMessageId: bounded(event.data.email_id, 1024),
93
+ type: event.type, status: resendStatuses[event.type], occurredAt: date(event.created_at, now),
94
+ recipients, deliveryId: null, product: null,
95
+ };
96
+ }
97
+ function cloudflare(event, source, now) {
98
+ object(event); object(event.source); object(event.metadata); object(event.payload); object(event.payload.delivery);
99
+ const status = event.payload.delivery.status;
100
+ if (!cfStatuses.has(status) || event.type !== `cf.email.sending.message.${status}` ||
101
+ event.payload.terminal !== (status !== 'deferred') || event.metadata.eventSchemaVersion !== 1 ||
102
+ event.source.type !== 'email.sending') fail('invalid_event_schema');
103
+ if (event.source.zoneId !== source.zoneId || event.source.domain !== source.domain ||
104
+ event.metadata.accountId !== source.accountId || event.metadata.eventSubscriptionId !== source.eventSubscriptionId) fail('event_source_unauthorized');
105
+ return {
106
+ eventId: bounded(event.payload.eventId), providerMessageId: bounded(event.payload.messageId, 1024),
107
+ type: event.type, status, occurredAt: date(event.metadata.eventTimestamp, now),
108
+ recipients: [recipient(event.payload.recipient)], deliveryId: null, product: null,
109
+ };
110
+ }
111
+
112
+ export function createDeliveryEventIntake(options) {
113
+ object(options);
114
+ const { provider, key, storage, authorize, clock = Date.now, toleranceSeconds = 300 } = options;
115
+ const tenantId = bounded(options.tenantId, 256);
116
+ const eventNamespace = bounded(options.eventNamespace, 256);
117
+ if (!['sendgrid', 'resend', 'cloudflare', 'messaging'].includes(provider) || typeof authorize !== 'function' || typeof clock !== 'function') fail('invalid_event_config');
118
+ if (!Number.isSafeInteger(toleranceSeconds) || toleranceSeconds < 0 || toleranceSeconds > 3600) fail('invalid_event_config');
119
+ if (['sendgrid', 'resend'].includes(provider) && (typeof key !== 'string' || !key.length)) fail('event_key_required');
120
+ if (['cloudflare', 'messaging'].includes(provider) && key !== undefined) fail('unsupported_event_key');
121
+ if (options.store !== undefined && (storage !== undefined || !isD1EventStore(options.store))) fail('invalid_event_store');
122
+ const store = options.store ?? createDurableEventStore(storage);
123
+ let messagingSource;
124
+ if (provider === 'messaging') {
125
+ const source = closed(options.messagingSource, ['serviceOrigin', 'installId', 'jwks'], 'invalid_event_config');
126
+ if (options.mode !== undefined && !['production', 'development', 'test'].includes(options.mode)) fail('invalid_event_config');
127
+ messagingSource = { serviceOrigin: serviceOrigin(source.serviceOrigin, options.mode ?? 'production'),
128
+ installId: identity(source.installId, 256), jwks: pinnedKeys(source.jwks) };
129
+ }
130
+ const source = provider === 'cloudflare' ? { ...object(options.cloudflareSource) } : null;
131
+ if (source) {
132
+ for (const field of ['queue', 'zoneId', 'domain', 'accountId', 'eventSubscriptionId']) bounded(source[field]);
133
+ }
134
+ function now() {
135
+ let value;
136
+ try { value = clock(); } catch { fail('invalid_event_clock'); }
137
+ if (!Number.isSafeInteger(value) || value < 0) fail('invalid_event_clock');
138
+ return value;
139
+ }
140
+ async function persist(records) {
141
+ // Complete schema/authorization of the whole batch before the first write.
142
+ for (const event of records) {
143
+ Object.freeze(event.recipients); if (event.detail) Object.freeze(event.detail); Object.freeze(event);
144
+ let permitted;
145
+ try { permitted = await authorize(event); } catch { fail('event_unauthorized'); }
146
+ if (permitted !== true) fail('event_unauthorized');
147
+ }
148
+ return store.commit(records);
149
+ }
150
+ async function record(event, bytes) {
151
+ return { ...event, provider, tenantId, eventNamespace, fingerprint: await fingerprint(bytes) };
152
+ }
153
+ if (provider === 'cloudflare') return Object.freeze({
154
+ async consumeQueue(batch) {
155
+ // This method is solely for the trusted Worker queue handler. Runtime
156
+ // objects are not an HTTP authentication protocol (see README).
157
+ if (!batch || batch.queue !== source.queue || !Array.isArray(batch.messages) ||
158
+ !batch.messages.length || batch.messages.length > MAX_EVENTS) fail('invalid_event_queue');
159
+ const timestamp = now();
160
+ const pending = [];
161
+ let total = 0;
162
+ for (const message of batch.messages) {
163
+ object(message);
164
+ let text, event;
165
+ try { text = JSON.stringify(message.body); event = JSON.parse(text); } catch { fail('invalid_event_schema'); }
166
+ const bytes = encoder.encode(text); total += bytes.length;
167
+ if (total > MAX_EVENT_BYTES) fail('event_body_too_large');
168
+ pending.push([cloudflare(event, source, timestamp), bytes]);
169
+ }
170
+ return persist(await Promise.all(pending.map(([event, bytes]) => record(event, bytes))));
171
+ // Caller ACKs only after resolution; throws leave the queue retryable.
172
+ },
173
+ });
174
+ return Object.freeze({
175
+ async ingestWebhook(input) {
176
+ if (!(input?.rawBody instanceof Uint8Array) || input.rawBody.length > MAX_EVENT_BYTES || !(input.headers instanceof Headers)) fail('invalid_event_input');
177
+ const rawBody = new Uint8Array(input.rawBody);
178
+ const headers = new Headers(input.headers);
179
+ const timestamp = now();
180
+ if (provider === 'messaging') {
181
+ const events = await verifyEventJws(rawBody, messagingSource, timestamp);
182
+ if (!Array.isArray(events) || !events.length || events.length > MAX_EVENTS) fail('invalid_event_batch');
183
+ const normalized = events.map(event => messagingEvent(event, timestamp));
184
+ return persist(await Promise.all(normalized.map(event => record({
185
+ eventId: event.id, providerMessageId: event.messageId, deliveryId: event.deliveryId,
186
+ type: event.type, status: event.type === 'suppressed' ? 'rejected' : event.type,
187
+ occurredAt: Date.parse(event.occurredAt), recipients: [event.recipient], product: null, detail: event.detail,
188
+ }, encoder.encode(JSON.stringify(event))))));
189
+ }
190
+ if (!await verifyWebhook({ provider, rawBody, headers, key, now: timestamp, toleranceSeconds })) fail('event_signature_invalid');
191
+ let text, payload;
192
+ try { text = new TextDecoder('utf-8', { fatal: true }).decode(rawBody); payload = JSON.parse(text); } catch { fail('invalid_event_json'); }
193
+ if (provider === 'resend') return persist([await record(resend(payload, headers, timestamp), rawBody)]);
194
+ if (!Array.isArray(payload) || !payload.length || payload.length > MAX_EVENTS) fail('invalid_event_batch');
195
+ const normalized = payload.map(event => sendgrid(event, timestamp));
196
+ const slices = eventSlices(text);
197
+ if (slices.length !== normalized.length) fail('invalid_event_batch');
198
+ return persist(await Promise.all(normalized.map((event, index) => record(event, encoder.encode(slices[index])))));
199
+ },
200
+ });
201
+ }
@@ -0,0 +1,52 @@
1
+ import { fail, MailValidationError } from '../core/errors.mjs';
2
+
3
+ const terminal = new Set(['delivered', 'bounced', 'failed', 'rejected', 'complained']);
4
+
5
+ /** A real DurableObjectStorage transaction; no process-local deduplication. */
6
+ export function createDurableEventStore(storage) {
7
+ if (!storage || typeof storage.transaction !== 'function') fail('event_storage_required');
8
+ return Object.freeze({
9
+ async commit(events) {
10
+ try {
11
+ return await storage.transaction(async tx => {
12
+ const results = [];
13
+ for (const event of events) {
14
+ const namespace = [event.provider, event.tenantId, event.eventNamespace];
15
+ const receiptKey = await key('receipt', [...namespace, event.eventId]);
16
+ const prior = await tx.get(receiptKey);
17
+ if (prior) {
18
+ if (prior.fingerprint !== event.fingerprint) fail('event_identity_conflict');
19
+ results.push({ eventId: event.eventId, outcome: 'duplicate', recipients: [] });
20
+ continue;
21
+ }
22
+ const recipients = [];
23
+ for (const recipient of event.recipients) {
24
+ const stateKey = await key('state', [...namespace, event.providerMessageId, recipient]);
25
+ const previous = await tx.get(stateKey);
26
+ // Final delivery facts never become accepted/deferred. Newer terminal
27
+ // facts (e.g. a later complaint) may supersede earlier terminal facts.
28
+ const apply = event.status !== null && (!previous ||
29
+ (terminal.has(event.status) && !terminal.has(previous.status)) ||
30
+ (event.occurredAt > previous.occurredAt &&
31
+ (!terminal.has(previous.status) || terminal.has(event.status))));
32
+ const state = apply ? { status: event.status, occurredAt: event.occurredAt, eventId: event.eventId } : previous ?? null;
33
+ if (apply) await tx.put(stateKey, state);
34
+ recipients.push({ recipient, applied: apply, state });
35
+ }
36
+ await tx.put(receiptKey, event);
37
+ results.push({ eventId: event.eventId, outcome: 'stored', recipients });
38
+ }
39
+ return results;
40
+ });
41
+ } catch (error) {
42
+ if (error instanceof MailValidationError && error.code === 'event_identity_conflict') throw error;
43
+ fail('event_storage_failed');
44
+ }
45
+ },
46
+ });
47
+ }
48
+
49
+ async function key(kind, tuple) {
50
+ const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(tuple)));
51
+ return `mail-events:v1:${kind}:${Array.from(new Uint8Array(hash), byte => byte.toString(16).padStart(2, '0')).join('')}`;
52
+ }
@@ -0,0 +1,92 @@
1
+ import { encodeBase64 } from '../core/validation.mjs';
2
+
3
+ // Inbound Parse also uses this verifier; delivery intake applies a smaller cap.
4
+ export const MAX_WEBHOOK_BYTES = 32 * 1024 * 1024;
5
+ const encoder = new TextEncoder();
6
+
7
+ function base64(value, max) {
8
+ if (typeof value !== 'string' || !value.length || value.length > max ||
9
+ !/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4 === 1) throw new Error();
10
+ const bytes = Uint8Array.from(atob(value), c => c.charCodeAt(0));
11
+ const canonical = encodeBase64(bytes);
12
+ if (value !== canonical && value !== canonical.replace(/=+$/, '')) throw new Error();
13
+ return bytes;
14
+ }
15
+
16
+ /** Strict DER INTEGERs; WebCrypto expects fixed-width unsigned r || s. */
17
+ function derToP1363(bytes) {
18
+ if (bytes.length < 8 || bytes.length > 72 || bytes[0] !== 0x30 || bytes[1] !== bytes.length - 2) throw new Error();
19
+ const result = new Uint8Array(64);
20
+ let offset = 2;
21
+ for (let part = 0; part < 2; part++) {
22
+ if (bytes[offset++] !== 2) throw new Error();
23
+ const length = bytes[offset++];
24
+ if (!length || length > 33 || offset + length > bytes.length) throw new Error();
25
+ let integer = bytes.subarray(offset, offset + length);
26
+ offset += length;
27
+ if (integer[0] & 0x80) throw new Error();
28
+ if (integer[0] === 0 && integer.length > 1) {
29
+ if (!(integer[1] & 0x80)) throw new Error();
30
+ integer = integer.subarray(1);
31
+ }
32
+ if (integer.length > 32 || integer.every(byte => byte === 0)) throw new Error();
33
+ result.set(integer, part * 32 + 32 - integer.length);
34
+ }
35
+ if (offset !== bytes.length) throw new Error();
36
+ return result;
37
+ }
38
+
39
+ function signedBytes(prefix, raw) {
40
+ const first = encoder.encode(prefix);
41
+ const bytes = new Uint8Array(first.length + raw.length);
42
+ bytes.set(first); bytes.set(raw, first.length);
43
+ return bytes;
44
+ }
45
+
46
+ export async function verifyWebhook(input) {
47
+ try {
48
+ const { provider, rawBody, headers, key, now, toleranceSeconds = 300 } = input;
49
+ if (!['sendgrid', 'resend'].includes(provider) || !(rawBody instanceof Uint8Array) ||
50
+ rawBody.length > MAX_WEBHOOK_BYTES || !(headers instanceof Headers) ||
51
+ !Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(toleranceSeconds) ||
52
+ toleranceSeconds < 0 || toleranceSeconds > 3600) return false;
53
+ // Copy before the first await; callers cannot mutate the verified bytes mid-flight.
54
+ const raw = new Uint8Array(rawBody);
55
+ const timestamp = headers.get(provider === 'sendgrid' ? 'x-twilio-email-event-webhook-timestamp' : 'svix-timestamp');
56
+ if (!/^(0|[1-9][0-9]{0,12})$/.test(timestamp ?? '')) return false;
57
+ const milliseconds = Number(timestamp) * 1000;
58
+ if (!Number.isSafeInteger(milliseconds) || Math.abs(now - milliseconds) > toleranceSeconds * 1000) return false;
59
+ if (provider === 'sendgrid') {
60
+ const spki = base64(key, 256);
61
+ const signature = derToP1363(base64(headers.get('x-twilio-email-event-webhook-signature'), 128));
62
+ const publicKey = await crypto.subtle.importKey('spki', spki, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']);
63
+ // Reject trailing bytes, alternate/noncanonical ASN.1, and wrong algorithms.
64
+ const exported = new Uint8Array(await crypto.subtle.exportKey('spki', publicKey));
65
+ if (encodeBase64(spki) !== encodeBase64(exported)) return false;
66
+ return await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, publicKey, signature, signedBytes(timestamp, raw));
67
+ }
68
+ const id = headers.get('svix-id');
69
+ const signatures = headers.get('svix-signature');
70
+ if (!/^[A-Za-z0-9_-]{1,512}$/.test(id ?? '') || typeof key !== 'string' || !key.startsWith('whsec_') ||
71
+ !signatures || signatures.length > 4096) return false;
72
+ const secret = base64(key.slice(6), 128);
73
+ if (secret.length < 16 || secret.length > 64) return false;
74
+ const tokens = signatures.split(' ');
75
+ if (tokens.length > 16) return false;
76
+ // Validate the entire header, including entries after a valid rotation key.
77
+ const candidates = tokens.map(token => {
78
+ const match = /^(v[1-9][0-9]*),([A-Za-z0-9+/]+={0,2})$/.exec(token);
79
+ if (!match) throw new Error();
80
+ const bytes = base64(match[2], 256);
81
+ if (match[1] === 'v1' && bytes.length !== 32) throw new Error();
82
+ return { version: match[1], bytes };
83
+ });
84
+ const hmac = await crypto.subtle.importKey('raw', secret, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
85
+ const signed = signedBytes(`${id}.${timestamp}.`, raw);
86
+ let verified = false;
87
+ for (const candidate of candidates) {
88
+ if (candidate.version === 'v1') verified = (await crypto.subtle.verify('HMAC', hmac, candidate.bytes, signed)) || verified;
89
+ }
90
+ return verified;
91
+ } catch { return false; }
92
+ }
@@ -0,0 +1,137 @@
1
+ # Inbound normalization
2
+
3
+ Import `normalizeInbound` from `modules/mail/src/inbound/index.mjs` (also exposed
4
+ by the assembled mail facade). It returns the frozen `InboundMessage` contract.
5
+ These are transport parsers, not authentication or HTTP handlers. No route or
6
+ persistent storage is installed. HTML, text, filenames and sender claims remain
7
+ untrusted content; the consuming application owns rendering sanitization,
8
+ authorization, deduplication, durable quarantine and sender-authentication policy.
9
+
10
+ ## Trust boundary and provider inputs
11
+
12
+ - Cloudflare: call only from the Worker `email(message, env, ctx)` event, passing
13
+ `{provider:'cloudflare', message}`. The real `raw` byte stream is parsed; its
14
+ actual length must equal `rawSize`. `from` and `to` are the SMTP envelope values.
15
+ Headers and threading come from the original MIME stream, avoiding synthetic
16
+ JSON conversion and duplicate-header loss through `Headers`.
17
+ - SendGrid: bound and retain the original HTTP multipart bytes, authenticate
18
+ those exact bytes with the configured Inbound Parse signature key, then pass
19
+ `{provider:'sendgrid', rawBody:Uint8Array, contentType}`. Never call parsing or
20
+ store/hydrate a message before successful verification. The normalizer accepts
21
+ both parsed fields/binary attachment parts and the provider's raw `email`
22
+ field. Duplicate fields, mismatched attachment counts/metadata, malformed
23
+ boundaries and invalid transfer encoding reject the whole message.
24
+ - Resend: verify the exact Svix webhook bytes, validate `type:'email.received'`,
25
+ and extract the authenticated event's `data.email_id`. Only then retrieve
26
+ `GET https://api.resend.com/emails/receiving/{email_id}?html_format=cid` using
27
+ the fixed API origin, an authorized server credential, a bounded response
28
+ reader, a deadline and `redirect:'manual'`. Pass the response object as `email`
29
+ and the original authenticated ID as **`signedEmailId`**. The normalizer
30
+ requires `email.id === signedEmailId` before any attachment callback. Do not
31
+ fill `signedEmailId` from the hydrated response. Requesting `html_format=cid`
32
+ preserves original inline image references across providers; the normalizer
33
+ preserves the returned HTML and does not rewrite provider data-URI HTML.
34
+
35
+ A successful webhook signature authenticates the provider's transport, not the
36
+ person named in the mail's From header. Supplying a `signedEmailId` value does
37
+ not perform cryptographic verification; the verified caller is the trust boundary.
38
+ The events unit owns `verifyWebhook`. This unit neither fabricates verification
39
+ nor retries a failed transport.
40
+
41
+ ## Resend attachment hydration
42
+
43
+ Resend's received-email response contains attachment metadata, not file bytes.
44
+ Every entry requires `id`, `filename`, `content_type`, a nonnegative integer
45
+ `size`, and optional `content_disposition` / `content_id`. Supply an asynchronous
46
+ `fetchAttachment(attachment) -> Uint8Array` on the input when attachments exist.
47
+ The callback receives a frozen object containing only
48
+ `{emailId,id,filename,content_type,content_disposition,content_id,size}`. Provider
49
+ IDs are bounded safe path segments; URLs and arbitrary response fields are not
50
+ passed through. All message metadata and declared aggregate limits are checked
51
+ before the first callback. Missing hydration, duplicate IDs, oversized bytes,
52
+ or actual length differing from `size` fail the entire result. Callbacks must
53
+ be read-only: an eventual failure cannot undo a caller's storage side effects.
54
+
55
+ A real safe implementation is included:
56
+
57
+ ```js
58
+ import { normalizeInbound, createResendAttachmentFetcher } from './index.mjs';
59
+ // After verified event validation and identity-bound email retrieval:
60
+ const fetchAttachment = createResendAttachmentFetcher({
61
+ apiKey, maxAttachmentBytes: limits.maxAttachmentBytes,
62
+ });
63
+ const message = await normalizeInbound({
64
+ provider: 'resend', signedEmailId, email, fetchAttachment,
65
+ }, limits);
66
+ ```
67
+
68
+ `createResendAttachmentFetcher({apiKey,maxAttachmentBytes}, {fetch?,clock?})`
69
+ uses the fixed Resend attachment API and verifies returned ID, filename, MIME
70
+ metadata and size against the received-email entry. It permits only the documented
71
+ `https://inbound-cdn.resend.com/{emailId}/attachments/{id}` download location,
72
+ requires an unexpired URL, refuses redirects on both requests, and sends the
73
+ API credential only to `api.resend.com`. The download request has no authorization
74
+ headers. API metadata is bounded to 64 KiB; streamed content is bounded by the
75
+ expected size and configured attachment limit. One 15-second abort deadline
76
+ covers both requests and bodies. Errors are fixed and redacted. There is no
77
+ configurable arbitrary download origin. A provider CDN contract change fails
78
+ closed and requires an explicit source update.
79
+
80
+ A custom callback must enforce the same identity/metadata, origin, redirect,
81
+ timeout and streaming checks. A callback is trusted application code, not
82
+ untrusted webhook input. Neither helper retrieves email metadata before the
83
+ caller has verified the webhook.
84
+
85
+ ## Limits and normalization details
86
+
87
+ All five limit values must be positive safe integers. Exact limits succeed.
88
+ `maxRawBytes` measures actual Cloudflare stream bytes or SendGrid multipart bytes,
89
+ and checks Cloudflare's claimed size too. For Resend's object input it measures
90
+ the UTF-8 JSON serialization of the hydrated response; this is not a claim to
91
+ have inspected the original SMTP message or the original HTTP whitespace.
92
+ The caller must also bound the actual HTTP response stream during hydration.
93
+ Downloaded files have separate individual and aggregate actual-byte limits.
94
+
95
+ `maxBodyBytes` is aggregate text plus HTML, checked against both decoded source
96
+ bytes and the resulting UTF-8 bytes (the larger size for each body part).
97
+ `maxAttachments`, `maxAttachmentBytes`, and `maxTotalAttachmentBytes` apply to
98
+ all decoded files, including inline and unnamed MIME parts. Both claimed Resend
99
+ sizes and actual hydrated bytes are checked. Results are returned only after
100
+ all parsing and hydration succeeds; errors never contain partial messages.
101
+ Oversized Cloudflare/download streams are cancelled.
102
+
103
+ MIME supports nested multipart alternatives/mixed/related, folded headers,
104
+ RFC 2047 encoded words, RFC 2231 extended/continued filenames, base64,
105
+ quoted-printable, 7bit/8bit/binary and Web `TextDecoder` character sets. Invalid
106
+ encodings, duplicate singleton headers, incomplete multiparts and nesting over
107
+ 30 levels fail closed. Multiple body parts of the same media type are joined
108
+ with a newline, which also counts toward the output body limit. Unsupported
109
+ media types are retained as attachments instead of silently discarded.
110
+
111
+ Output addresses are mailbox addresses (display names are parsed but not
112
+ returned). Cloudflare uses its event envelope; SendGrid and Resend use their
113
+ provided sender/recipient fields. Empty subjects are preserved; absent optional
114
+ thread/body fields use null or empty arrays. Message-ID and ordered References
115
+ retain their angle-bracket values. Content-IDs are normalized without angle
116
+ brackets; an absent content ID is null. Unnamed MIME files retain filename `''`.
117
+ No caller-owned input objects or attachment buffers are mutated.
118
+
119
+ ## Checks and references
120
+
121
+ From the Base worktree:
122
+
123
+ ```sh
124
+ node --test modules/mail/tests/inbound/normalization.test.mjs modules/mail/tests/inbound/boundaries.test.mjs modules/mail/tests/inbound/hydration.test.mjs
125
+ node --test modules/mail/tests/inbound/workerd.test.mjs
126
+ ```
127
+
128
+ The latter uses actual workerd through the module's Miniflare harness without
129
+ Node compatibility, with three provider fixtures and the safe hydration protocol.
130
+ Fixtures and provider responses live only under `./tests/inbound`. These checks do
131
+ not prove hosted mail receipt; owner-authorized domain, provider configuration,
132
+ signed routing and send/receive correlation evidence remain separate acceptance.
133
+
134
+ Provider schemas consulted: [Cloudflare email event](https://developers.cloudflare.com/email-service/api/route-emails/email-handler/),
135
+ [SendGrid Inbound Parse fields](https://www.twilio.com/docs/sendgrid/for-developers/parsing-email/setting-up-the-inbound-parse-webhook),
136
+ [Resend received email](https://resend.com/docs/api-reference/emails/retrieve-received-email),
137
+ and [Resend attachment metadata/download](https://resend.com/docs/api-reference/emails/retrieve-received-email-attachment).
@@ -0,0 +1,91 @@
1
+ import { fail } from '../core/errors.mjs';
2
+ import { encodeBase64 } from '../core/validation.mjs';
3
+ export const utf8 = new TextEncoder();
4
+ export function record(value) {
5
+ if (!value || typeof value !== 'object' || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail('invalid_inbound');
6
+ return value;
7
+ }
8
+ export function limitsFor(value) {
9
+ record(value);
10
+ for (const key of ['maxRawBytes', 'maxBodyBytes', 'maxAttachments', 'maxAttachmentBytes', 'maxTotalAttachmentBytes']) {
11
+ if (!Number.isSafeInteger(value[key]) || value[key] <= 0) fail('invalid_inbound_limits');
12
+ }
13
+ return { ...value };
14
+ }
15
+ export function bounded(size, limit, code) { if (size > limit) fail(code); }
16
+ export function binary(bytes) {
17
+ let value = '';
18
+ for (let i = 0; i < bytes.length; i += 8192) value += String.fromCharCode(...bytes.subarray(i, i + 8192));
19
+ return value;
20
+ }
21
+ export const unbinary = value => Uint8Array.from(value, c => c.charCodeAt(0));
22
+ export function decode(bytes, charset = 'utf-8') {
23
+ try { return new TextDecoder(charset, { fatal: true }).decode(bytes); } catch { fail('invalid_inbound_charset'); }
24
+ }
25
+ export function base64(value) {
26
+ const compact = value.replace(/[\r\n\t ]/g, '');
27
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(compact)) fail('invalid_inbound_base64');
28
+ let bytes;
29
+ try { bytes = unbinary(atob(compact)); } catch { fail('invalid_inbound_base64'); }
30
+ if (encodeBase64(bytes) !== compact) fail('invalid_inbound_base64');
31
+ return bytes;
32
+ }
33
+ export function quotedPrintable(value, word = false) {
34
+ const source = word ? value.replace(/_/g, ' ') : value;
35
+ let result = '';
36
+ for (let i = 0; i < source.length; i++) {
37
+ if (source[i] !== '=') {
38
+ if (/[^\x20-\x7e\t\r\n]/.test(source[i])) fail('invalid_inbound_encoding');
39
+ result += source[i]; continue;
40
+ }
41
+ const next = source.slice(i + 1, i + 3);
42
+ if (!word && next === '\r\n') { i += 2; continue; }
43
+ if (!word && source[i + 1] === '\n') { i++; continue; }
44
+ if (!/^[a-f0-9]{2}$/i.test(next)) fail('invalid_inbound_encoding');
45
+ result += String.fromCharCode(parseInt(next, 16)); i += 2;
46
+ }
47
+ return unbinary(result);
48
+ }
49
+ export function transfer(bytes, encoding = '7bit') {
50
+ switch (encoding.toLowerCase()) {
51
+ case 'base64': return base64(binary(bytes));
52
+ case 'quoted-printable': return quotedPrintable(binary(bytes));
53
+ case '7bit': if (bytes.some(byte => byte > 127)) fail('invalid_inbound_encoding'); return bytes;
54
+ case '8bit': case 'binary': return bytes;
55
+ default: fail('invalid_inbound_encoding');
56
+ }
57
+ }
58
+ export async function readStream(stream, max) {
59
+ if (!stream || typeof stream.getReader !== 'function') fail('invalid_inbound_stream');
60
+ const reader = stream.getReader(); const chunks = []; let size = 0;
61
+ try {
62
+ while (true) {
63
+ const { done, value } = await reader.read();
64
+ if (done) break;
65
+ if (!(value instanceof Uint8Array)) fail('invalid_inbound_stream');
66
+ size += value.byteLength; bounded(size, max, 'inbound_raw_limit'); chunks.push(value.slice());
67
+ }
68
+ } catch (error) {
69
+ try { await reader.cancel(); } catch { /* Preserve the original redacted failure. */ }
70
+ throw error;
71
+ } finally { reader.releaseLock(); }
72
+ const bytes = new Uint8Array(size); let offset = 0;
73
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; }
74
+ return bytes;
75
+ }
76
+ export function bodyBudget(limits) {
77
+ let bodySize = 0, attachmentSize = 0, attachmentCount = 0;
78
+ return {
79
+ body(text, originalSize = 0) {
80
+ bodySize += Math.max(originalSize, utf8.encode(text).length);
81
+ bounded(bodySize, limits.maxBodyBytes, 'inbound_body_limit');
82
+ },
83
+ attachment(bytes) {
84
+ if (!(bytes instanceof Uint8Array)) fail('invalid_inbound_attachment');
85
+ bounded(++attachmentCount, limits.maxAttachments, 'inbound_attachment_count');
86
+ bounded(bytes.length, limits.maxAttachmentBytes, 'inbound_attachment_limit');
87
+ attachmentSize += bytes.length;
88
+ bounded(attachmentSize, limits.maxTotalAttachmentBytes, 'inbound_attachments_limit');
89
+ },
90
+ };
91
+ }