@runeditrun/rer 0.0.0-stage → 0.1.1

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 (527) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -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 +182 -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 +158 -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 +117 -0
  71. package/global/launch/facts.mjs +94 -0
  72. package/global/launch/failure.mjs +5 -0
  73. package/global/launch/local-defaults.mjs +4 -0
  74. package/global/launch/prepare.mjs +63 -0
  75. package/global/launch/render.mjs +44 -0
  76. package/global/launch/state.mjs +106 -0
  77. package/global/launch/steps.mjs +90 -0
  78. package/global/launch/stop.mjs +89 -0
  79. package/global/launch/worker.mjs +62 -0
  80. package/global/list.mjs +59 -0
  81. package/global/main.mjs +53 -0
  82. package/global/package-info.mjs +38 -0
  83. package/global/registry.mjs +24 -0
  84. package/global/rer.mjs +29 -0
  85. package/global/update/archive.mjs +89 -0
  86. package/global/update/command.mjs +90 -0
  87. package/global/update/latest.mjs +6 -0
  88. package/global/update/notice.mjs +58 -0
  89. package/global/update/registry.mjs +47 -0
  90. package/global/version.mjs +29 -0
  91. package/install.sh +144 -0
  92. package/modules/ai/LICENSE +21 -0
  93. package/modules/ai/README.md +90 -0
  94. package/modules/ai/client.mjs +71 -0
  95. package/modules/ai/config.mjs +39 -0
  96. package/modules/ai/dev/index.d.mts +6 -0
  97. package/modules/ai/dev/index.mjs +64 -0
  98. package/modules/ai/dev/outbox.mjs +33 -0
  99. package/modules/ai/errors.mjs +28 -0
  100. package/modules/ai/fake/index.d.mts +3 -0
  101. package/modules/ai/fake/index.mjs +82 -0
  102. package/modules/ai/index.d.mts +10 -0
  103. package/modules/ai/index.mjs +3 -0
  104. package/modules/ai/package.json +6 -0
  105. package/modules/container/README.md +156 -0
  106. package/modules/container/adapters/d1.mjs +34 -0
  107. package/modules/container/adapters/egress.mjs +40 -0
  108. package/modules/container/adapters/jobs.mjs +47 -0
  109. package/modules/container/adapters/objects.mjs +84 -0
  110. package/modules/container/adapters/retained.mjs +14 -0
  111. package/modules/container/adapters/sqlite-error.mjs +7 -0
  112. package/modules/container/adapters/sqlite-worker.mjs +26 -0
  113. package/modules/container/assets.mjs +14 -0
  114. package/modules/container/bindings.mjs +1 -0
  115. package/modules/container/boundary.mjs +38 -0
  116. package/modules/container/build.mjs +65 -0
  117. package/modules/container/deploy-input.mjs +17 -0
  118. package/modules/container/entrypoint.sh +5 -0
  119. package/modules/container/host.mjs +43 -0
  120. package/modules/container/ingress.mjs +23 -0
  121. package/modules/container/lock.mjs +11 -0
  122. package/modules/container/migrations.mjs +16 -0
  123. package/modules/container/node-engine.mjs +39 -0
  124. package/modules/container/portability-schema.mjs +14 -0
  125. package/modules/container/remote/deploy.py +76 -0
  126. package/modules/container/remote/files.py +275 -0
  127. package/modules/container/remote/resources.py +157 -0
  128. package/modules/container/runner.mjs +10 -0
  129. package/modules/container/ssh-plan.mjs +44 -0
  130. package/modules/container/ssh.mjs +63 -0
  131. package/modules/container/transfer.mjs +27 -0
  132. package/modules/container/worker-engine.mjs +32 -0
  133. package/modules/dependencies/README.md +211 -0
  134. package/modules/dependencies/approval.mjs +51 -0
  135. package/modules/dependencies/auth.mjs +40 -0
  136. package/modules/dependencies/authority.mjs +14 -0
  137. package/modules/dependencies/crypto.mjs +13 -0
  138. package/modules/dependencies/index.d.mts +23 -0
  139. package/modules/dependencies/index.mjs +113 -0
  140. package/modules/dependencies/logout.mjs +42 -0
  141. package/modules/dependencies/native.mjs +9 -0
  142. package/modules/dependencies/protocol.mjs +23 -0
  143. package/modules/dependencies/schema/approval-attempt.sql +3 -0
  144. package/modules/dependencies/schema/dependencies.sql +15 -0
  145. package/modules/dependencies/store.mjs +19 -0
  146. package/modules/deploy-kit/README.md +308 -0
  147. package/modules/deploy-kit/artifact.mjs +47 -0
  148. package/modules/deploy-kit/build-modules.mjs +21 -0
  149. package/modules/deploy-kit/check.mjs +13 -0
  150. package/modules/deploy-kit/cli.mjs +50 -0
  151. package/modules/deploy-kit/container-config.mjs +8 -0
  152. package/modules/deploy-kit/default-stage.mjs +14 -0
  153. package/modules/deploy-kit/deploy.mjs +80 -0
  154. package/modules/deploy-kit/health-request.mjs +86 -0
  155. package/modules/deploy-kit/health.mjs +99 -0
  156. package/modules/deploy-kit/index.mjs +12 -0
  157. package/modules/deploy-kit/local-only.mjs +10 -0
  158. package/modules/deploy-kit/migrations.mjs +48 -0
  159. package/modules/deploy-kit/owner-cli.mjs +63 -0
  160. package/modules/deploy-kit/owner.mjs +297 -0
  161. package/modules/deploy-kit/prepare.mjs +62 -0
  162. package/modules/deploy-kit/provider.mjs +99 -0
  163. package/modules/deploy-kit/queue-consumer.mjs +10 -0
  164. package/modules/deploy-kit/receipt.mjs +16 -0
  165. package/modules/deploy-kit/render.mjs +47 -0
  166. package/modules/deploy-kit/resources.mjs +106 -0
  167. package/modules/deploy-kit/service-grants.mjs +25 -0
  168. package/modules/deploy-kit/sql-safety.mjs +94 -0
  169. package/modules/deploy-kit/sql-tokens.mjs +49 -0
  170. package/modules/deploy-kit/teardown.mjs +100 -0
  171. package/modules/deploy-kit/templates.mjs +31 -0
  172. package/modules/deploy-kit/validation.mjs +58 -0
  173. package/modules/deploy-kit/worker.mjs +105 -0
  174. package/modules/erasure/CONTRACT.md +90 -0
  175. package/modules/erasure/README.md +221 -0
  176. package/modules/erasure/audit.mjs +23 -0
  177. package/modules/erasure/blobs.mjs +45 -0
  178. package/modules/erasure/cascade.mjs +91 -0
  179. package/modules/erasure/completion.mjs +62 -0
  180. package/modules/erasure/declaration.mjs +51 -0
  181. package/modules/erasure/fence.mjs +19 -0
  182. package/modules/erasure/index.d.mts +62 -0
  183. package/modules/erasure/index.mjs +85 -0
  184. package/modules/erasure/order.mjs +56 -0
  185. package/modules/erasure/plan.mjs +121 -0
  186. package/modules/erasure/rate.mjs +57 -0
  187. package/modules/erasure/receipt.mjs +32 -0
  188. package/modules/erasure/root.mjs +21 -0
  189. package/modules/erasure/schema/erasure-rates.sql +5 -0
  190. package/modules/erasure/schema/erasure.sql +27 -0
  191. package/modules/erasure/scope.mjs +29 -0
  192. package/modules/erasure/sql.mjs +23 -0
  193. package/modules/erasure/verification.mjs +69 -0
  194. package/modules/integrations/core/CONTRACT.md +50 -0
  195. package/modules/integrations/core/README.md +27 -0
  196. package/modules/integrations/core/crypto.ts +19 -0
  197. package/modules/integrations/core/database.ts +21 -0
  198. package/modules/integrations/core/delivery.ts +97 -0
  199. package/modules/integrations/core/destination.ts +13 -0
  200. package/modules/integrations/core/index.ts +7 -0
  201. package/modules/integrations/core/node-transport.ts +72 -0
  202. package/modules/integrations/core/public-address.ts +57 -0
  203. package/modules/integrations/core/repository.ts +74 -0
  204. package/modules/integrations/core/schema.sql +36 -0
  205. package/modules/integrations/core/sqlite.ts +20 -0
  206. package/modules/integrations/core/timestamped-body-signature.ts +38 -0
  207. package/modules/integrations/core/tokens.ts +71 -0
  208. package/modules/integrations/core/types.ts +52 -0
  209. package/modules/integrations/core/upgrade-token-usage-context.sql +5 -0
  210. package/modules/integrations/core/webhook.ts +73 -0
  211. package/modules/job-queue/README.md +173 -0
  212. package/modules/job-queue/cli.mjs +64 -0
  213. package/modules/job-queue/command.mjs +64 -0
  214. package/modules/job-queue/config.mjs +48 -0
  215. package/modules/job-queue/identity.mjs +48 -0
  216. package/modules/job-queue/index.mjs +135 -0
  217. package/modules/job-queue/membership.mjs +30 -0
  218. package/modules/job-queue/module.json +10 -0
  219. package/modules/job-queue/policy.mjs +29 -0
  220. package/modules/job-queue/processes.mjs +80 -0
  221. package/modules/job-queue/resources.mjs +37 -0
  222. package/modules/job-queue/storage.mjs +64 -0
  223. package/modules/job-queue/supervisor.mjs +30 -0
  224. package/modules/job-queue/waiting.mjs +16 -0
  225. package/modules/mail/LICENSE +21 -0
  226. package/modules/mail/README.md +336 -0
  227. package/modules/mail/doctor.mjs +81 -0
  228. package/modules/mail/index.mjs +8 -0
  229. package/modules/mail/live-proof/README.md +113 -0
  230. package/modules/mail/live-proof/run.mjs +55 -0
  231. package/modules/mail/package.json +19 -0
  232. package/modules/mail/pnpm-lock.yaml +537 -0
  233. package/modules/mail/src/adapters/cloudflare/README.md +84 -0
  234. package/modules/mail/src/adapters/cloudflare/headers.mjs +63 -0
  235. package/modules/mail/src/adapters/cloudflare/index.mjs +47 -0
  236. package/modules/mail/src/adapters/cloudflare/response.mjs +99 -0
  237. package/modules/mail/src/adapters/messaging/README.md +92 -0
  238. package/modules/mail/src/adapters/messaging/contract.mjs +66 -0
  239. package/modules/mail/src/adapters/messaging/event-schema.mjs +28 -0
  240. package/modules/mail/src/adapters/messaging/index.mjs +61 -0
  241. package/modules/mail/src/adapters/messaging/jws.mjs +68 -0
  242. package/modules/mail/src/adapters/messaging/request.mjs +52 -0
  243. package/modules/mail/src/adapters/messaging/serialize.mjs +21 -0
  244. package/modules/mail/src/adapters/resend/README.md +120 -0
  245. package/modules/mail/src/adapters/resend/index.mjs +22 -0
  246. package/modules/mail/src/adapters/resend/response.mjs +54 -0
  247. package/modules/mail/src/adapters/resend/serialize.mjs +46 -0
  248. package/modules/mail/src/adapters/resend/size.mjs +99 -0
  249. package/modules/mail/src/adapters/sendgrid/README.md +75 -0
  250. package/modules/mail/src/adapters/sendgrid/index.mjs +35 -0
  251. package/modules/mail/src/adapters/sendgrid/serialize.mjs +104 -0
  252. package/modules/mail/src/core/errors.mjs +9 -0
  253. package/modules/mail/src/core/transport.mjs +33 -0
  254. package/modules/mail/src/core/validation.mjs +172 -0
  255. package/modules/mail/src/development/index.d.mts +37 -0
  256. package/modules/mail/src/development/index.mjs +35 -0
  257. package/modules/mail/src/development/outbox.mjs +78 -0
  258. package/modules/mail/src/development/schema.sql +9 -0
  259. package/modules/mail/src/development/viewer-html.mjs +42 -0
  260. package/modules/mail/src/development/viewer.mjs +65 -0
  261. package/modules/mail/src/doctor/README.md +107 -0
  262. package/modules/mail/src/doctor/common.mjs +64 -0
  263. package/modules/mail/src/doctor/dns.mjs +102 -0
  264. package/modules/mail/src/doctor/index.mjs +28 -0
  265. package/modules/mail/src/doctor/probe.mjs +66 -0
  266. package/modules/mail/src/doctor/receive.mjs +81 -0
  267. package/modules/mail/src/doctor/records.mjs +45 -0
  268. package/modules/mail/src/events/README.md +268 -0
  269. package/modules/mail/src/events/d1-schema.sql +25 -0
  270. package/modules/mail/src/events/d1-storage.mjs +76 -0
  271. package/modules/mail/src/events/index.mjs +3 -0
  272. package/modules/mail/src/events/intake.mjs +201 -0
  273. package/modules/mail/src/events/storage.mjs +52 -0
  274. package/modules/mail/src/events/verify.mjs +92 -0
  275. package/modules/mail/src/inbound/README.md +137 -0
  276. package/modules/mail/src/inbound/bytes.mjs +91 -0
  277. package/modules/mail/src/inbound/headers.mjs +143 -0
  278. package/modules/mail/src/inbound/index.mjs +29 -0
  279. package/modules/mail/src/inbound/mime.mjs +36 -0
  280. package/modules/mail/src/inbound/multipart.mjs +27 -0
  281. package/modules/mail/src/inbound/resend-attachment.mjs +43 -0
  282. package/modules/mail/src/inbound/resend.mjs +54 -0
  283. package/modules/mail/src/inbound/sendgrid.mjs +60 -0
  284. package/modules/mail/src/sender.mjs +33 -0
  285. package/modules/mail/src/sendgrid-email/index.d.mts +33 -0
  286. package/modules/mail/src/sendgrid-email/index.mjs +86 -0
  287. package/modules/mail/src/unsubscribe/index.mjs +142 -0
  288. package/modules/operator-auth/README.md +599 -0
  289. package/modules/operator-auth/accounts/adapter.mjs +41 -0
  290. package/modules/operator-auth/accounts/admin.mjs +159 -0
  291. package/modules/operator-auth/accounts/credentials.mjs +27 -0
  292. package/modules/operator-auth/accounts/editor-owner.mjs +13 -0
  293. package/modules/operator-auth/accounts/index.d.mts +9 -0
  294. package/modules/operator-auth/accounts/index.mjs +9 -0
  295. package/modules/operator-auth/accounts/native.d.mts +38 -0
  296. package/modules/operator-auth/accounts/native.mjs +171 -0
  297. package/modules/operator-auth/accounts/oa.mjs +90 -0
  298. package/modules/operator-auth/accounts/provider-control.mjs +50 -0
  299. package/modules/operator-auth/accounts/provider-pin.mjs +14 -0
  300. package/modules/operator-auth/accounts/roles.mjs +26 -0
  301. package/modules/operator-auth/accounts/schema/dependency-setup.sql +17 -0
  302. package/modules/operator-auth/accounts/schema/dev-sessions.sql +22 -0
  303. package/modules/operator-auth/accounts/schema/domain-rules-step-up.sql +20 -0
  304. package/modules/operator-auth/accounts/schema/invitation-cancellation.sql +17 -0
  305. package/modules/operator-auth/accounts/schema/operator-auth.sql +142 -0
  306. package/modules/operator-auth/accounts/schema/provider-control.sql +30 -0
  307. package/modules/operator-auth/accounts/schema/provider-pins.sql +6 -0
  308. package/modules/operator-auth/accounts/settings-page.mjs +4 -0
  309. package/modules/operator-auth/accounts/store.mjs +246 -0
  310. package/modules/operator-auth/branding.mjs +66 -0
  311. package/modules/operator-auth/config.mjs +35 -0
  312. package/modules/operator-auth/conformance/README.md +81 -0
  313. package/modules/operator-auth/conformance/index.d.mts +43 -0
  314. package/modules/operator-auth/conformance/index.mjs +300 -0
  315. package/modules/operator-auth/conformance/presentation.mjs +57 -0
  316. package/modules/operator-auth/crypto.mjs +17 -0
  317. package/modules/operator-auth/dev/badge.mjs +62 -0
  318. package/modules/operator-auth/dev/index.d.mts +14 -0
  319. package/modules/operator-auth/dev/index.mjs +137 -0
  320. package/modules/operator-auth/dev/personas.mjs +17 -0
  321. package/modules/operator-auth/dev/sign-in-as.d.mts +3 -0
  322. package/modules/operator-auth/dev/sign-in-as.mjs +25 -0
  323. package/modules/operator-auth/dev-admission.d.mts +4 -0
  324. package/modules/operator-auth/dev-admission.mjs +10 -0
  325. package/modules/operator-auth/dev-gate.mjs +86 -0
  326. package/modules/operator-auth/docs/README.md +78 -0
  327. package/modules/operator-auth/docs/native-accounts.md +313 -0
  328. package/modules/operator-auth/docs/tanstack-start.md +218 -0
  329. package/modules/operator-auth/docs/vite-spa.md +144 -0
  330. package/modules/operator-auth/errors.mjs +9 -0
  331. package/modules/operator-auth/http.mjs +42 -0
  332. package/modules/operator-auth/identity/admission.mjs +105 -0
  333. package/modules/operator-auth/identity/after-confirm.mjs +18 -0
  334. package/modules/operator-auth/identity/email-link/confirmation.mjs +14 -0
  335. package/modules/operator-auth/identity/email-link/index.d.mts +48 -0
  336. package/modules/operator-auth/identity/email-link/index.mjs +128 -0
  337. package/modules/operator-auth/identity/email-link/message.mjs +16 -0
  338. package/modules/operator-auth/identity/index.d.mts +7 -0
  339. package/modules/operator-auth/identity/index.mjs +5 -0
  340. package/modules/operator-auth/identity/oidc/README.md +171 -0
  341. package/modules/operator-auth/identity/oidc/client-auth.mjs +16 -0
  342. package/modules/operator-auth/identity/oidc/config.mjs +102 -0
  343. package/modules/operator-auth/identity/oidc/crypto.mjs +29 -0
  344. package/modules/operator-auth/identity/oidc/errors.mjs +5 -0
  345. package/modules/operator-auth/identity/oidc/index.d.mts +33 -0
  346. package/modules/operator-auth/identity/oidc/index.mjs +287 -0
  347. package/modules/operator-auth/identity/oidc/jwt.mjs +102 -0
  348. package/modules/operator-auth/identity/oidc/network.mjs +93 -0
  349. package/modules/operator-auth/identity/oidc/presets.mjs +26 -0
  350. package/modules/operator-auth/identity/oidc/schema/oidc.sql +50 -0
  351. package/modules/operator-auth/identity/oidc/store.mjs +68 -0
  352. package/modules/operator-auth/identity/principal.mjs +28 -0
  353. package/modules/operator-auth/index.d.mts +223 -0
  354. package/modules/operator-auth/index.mjs +11 -0
  355. package/modules/operator-auth/input.mjs +55 -0
  356. package/modules/operator-auth/local-cli.mjs +16 -0
  357. package/modules/operator-auth/page.mjs +34 -0
  358. package/modules/operator-auth/policy.mjs +20 -0
  359. package/modules/operator-auth/routes.mjs +91 -0
  360. package/modules/operator-auth/service.mjs +152 -0
  361. package/modules/operator-auth/signin/index.mjs +67 -0
  362. package/modules/owner-claim/README.md +137 -0
  363. package/modules/owner-claim/idp.mjs +40 -0
  364. package/modules/owner-claim/index.d.mts +27 -0
  365. package/modules/owner-claim/index.mjs +39 -0
  366. package/modules/owner-claim/page.mjs +11 -0
  367. package/modules/owner-claim/schema/idp.sql +4 -0
  368. package/modules/owner-claim/schema/owner-claim.sql +18 -0
  369. package/modules/owner-claim/transaction.mjs +87 -0
  370. package/modules/owner-claim/verify.mjs +80 -0
  371. package/modules/portability/README.md +444 -0
  372. package/modules/portability/bridge.mjs +102 -0
  373. package/modules/portability/catalogue.mjs +118 -0
  374. package/modules/portability/cli-options.mjs +38 -0
  375. package/modules/portability/cli.mjs +56 -0
  376. package/modules/portability/copies.mjs +68 -0
  377. package/modules/portability/dev-data.mjs +32 -0
  378. package/modules/portability/diff.mjs +33 -0
  379. package/modules/portability/export.mjs +129 -0
  380. package/modules/portability/files.mjs +65 -0
  381. package/modules/portability/format.mjs +122 -0
  382. package/modules/portability/generated.mjs +60 -0
  383. package/modules/portability/hash.mjs +79 -0
  384. package/modules/portability/http.mjs +42 -0
  385. package/modules/portability/import.mjs +185 -0
  386. package/modules/portability/index.d.mts +58 -0
  387. package/modules/portability/index.mjs +22 -0
  388. package/modules/portability/journal.mjs +49 -0
  389. package/modules/portability/legacy-files.mjs +43 -0
  390. package/modules/portability/legacy-safety.mjs +27 -0
  391. package/modules/portability/lines.mjs +36 -0
  392. package/modules/portability/migration-history.mjs +18 -0
  393. package/modules/portability/migration-statements.mjs +23 -0
  394. package/modules/portability/payloads.mjs +52 -0
  395. package/modules/portability/provider-data.mjs +25 -0
  396. package/modules/portability/restore-safety.mjs +30 -0
  397. package/modules/portability/schema/portability.sql +25 -0
  398. package/modules/portability/storage.mjs +49 -0
  399. package/modules/release/README.md +212 -0
  400. package/modules/release/catalogue.mjs +79 -0
  401. package/modules/release/export.mjs +51 -0
  402. package/modules/release/produce.mjs +136 -0
  403. package/modules/release/public-child-transport-state.cjs +29 -0
  404. package/modules/release/public-child.mjs +71 -0
  405. package/modules/release/public-git.mjs +21 -0
  406. package/modules/release/publication-scan.mjs +22 -0
  407. package/modules/release/publish-handoff.mjs +37 -0
  408. package/modules/release/source.mjs +71 -0
  409. package/modules/release/test-summary.mjs +50 -0
  410. package/onboarding/README.md +163 -0
  411. package/onboarding/activity.mjs +31 -0
  412. package/onboarding/adapter-auth.mjs +88 -0
  413. package/onboarding/adapter-hosting.mjs +35 -0
  414. package/onboarding/adapter-labels.mjs +13 -0
  415. package/onboarding/adapter-settings.mjs +129 -0
  416. package/onboarding/adapter.mjs +238 -0
  417. package/onboarding/agent-handoff.mjs +9 -0
  418. package/onboarding/app-processes.mjs +21 -0
  419. package/onboarding/archive-output.d.mts +3 -0
  420. package/onboarding/archive-output.mjs +54 -0
  421. package/onboarding/command-custody.mjs +24 -0
  422. package/onboarding/command-recovery.mjs +8 -0
  423. package/onboarding/commands.mjs +166 -0
  424. package/onboarding/dependency-approval.mjs +36 -0
  425. package/onboarding/dev-identity.mjs +23 -0
  426. package/onboarding/email.mjs +24 -0
  427. package/onboarding/failures.mjs +42 -0
  428. package/onboarding/input-limits.mjs +7 -0
  429. package/onboarding/installer-inputs.mjs +24 -0
  430. package/onboarding/launcher.mjs +34 -0
  431. package/onboarding/local/build-files.mjs +64 -0
  432. package/onboarding/local/build.mjs +49 -0
  433. package/onboarding/local/environment.mjs +30 -0
  434. package/onboarding/local/files.mjs +35 -0
  435. package/onboarding/local/operation.mjs +166 -0
  436. package/onboarding/local/owner.mjs +34 -0
  437. package/onboarding/local/process-table.mjs +114 -0
  438. package/onboarding/local/recovery.mjs +79 -0
  439. package/onboarding/local/state.mjs +31 -0
  440. package/onboarding/metadata.mjs +7 -0
  441. package/onboarding/operator-install.mjs +45 -0
  442. package/onboarding/operator-installer.mjs +39 -0
  443. package/onboarding/operator-prerequisites.mjs +27 -0
  444. package/onboarding/package.json +6 -0
  445. package/onboarding/prefill.mjs +16 -0
  446. package/onboarding/private-files.mjs +97 -0
  447. package/onboarding/private.mjs +35 -0
  448. package/onboarding/process-cleanup.mjs +134 -0
  449. package/onboarding/process-diagnostics.mjs +91 -0
  450. package/onboarding/process-scope.mjs +118 -0
  451. package/onboarding/process-table.mjs +118 -0
  452. package/onboarding/process-tree.mjs +36 -0
  453. package/onboarding/production-modules.mjs +22 -0
  454. package/onboarding/production-options.mjs +40 -0
  455. package/onboarding/production-plan.mjs +8 -0
  456. package/onboarding/production-runtime.mjs +30 -0
  457. package/onboarding/project-vite.mjs +14 -0
  458. package/onboarding/providers.mjs +289 -0
  459. package/onboarding/queue.mjs +53 -0
  460. package/onboarding/queued-entry.mjs +34 -0
  461. package/onboarding/readiness.mjs +59 -0
  462. package/onboarding/redaction-jwt.mjs +32 -0
  463. package/onboarding/redaction.mjs +298 -0
  464. package/onboarding/schema/SETUP.schema.json +514 -0
  465. package/onboarding/schema/dependencies.mjs +35 -0
  466. package/onboarding/schema/json-schema.mjs +202 -0
  467. package/onboarding/schema/setup-validation.mjs +325 -0
  468. package/onboarding/secrets.mjs +21 -0
  469. package/onboarding/server.mjs +188 -0
  470. package/onboarding/setup-manifest.mjs +116 -0
  471. package/onboarding/state.mjs +46 -0
  472. package/onboarding/theme-files.mjs +37 -0
  473. package/onboarding/theme.mjs +118 -0
  474. package/onboarding/ui/activity.js +98 -0
  475. package/onboarding/ui/address-client.mjs +33 -0
  476. package/onboarding/ui/app.js +377 -0
  477. package/onboarding/ui/index.html +1 -0
  478. package/onboarding/ui/model-core.mjs +127 -0
  479. package/onboarding/ui/model.mjs +5 -0
  480. package/onboarding/ui/style.css +63 -0
  481. package/onboarding/ui/theme.css +37 -0
  482. package/onboarding/ui/workspace-art.js +30 -0
  483. package/onboarding/wrangler-auth.mjs +43 -0
  484. package/operations/README.md +246 -0
  485. package/operations/operations.mjs +168 -0
  486. package/operations/project-operations.mjs +157 -0
  487. package/package.json +11 -4
  488. package/scripts/publish-check/README.md +72 -0
  489. package/scripts/publish-check/checksum_maps.py +52 -0
  490. package/scripts/publish-check/cli.py +34 -0
  491. package/scripts/publish-check/command_arguments.py +51 -0
  492. package/scripts/publish-check/command_shell_quotes.py +41 -0
  493. package/scripts/publish-check/command_source_quotes.py +120 -0
  494. package/scripts/publish-check/content_identities.py +250 -0
  495. package/scripts/publish-check/copy_identities.py +41 -0
  496. package/scripts/publish-check/der.py +61 -0
  497. package/scripts/publish-check/destination.py +117 -0
  498. package/scripts/publish-check/env_values.py +74 -0
  499. package/scripts/publish-check/font_licence.py +23 -0
  500. package/scripts/publish-check/git_history.py +115 -0
  501. package/scripts/publish-check/github_refs.py +217 -0
  502. package/scripts/publish-check/gitleaks.py +72 -0
  503. package/scripts/publish-check/gitleaks.toml +40 -0
  504. package/scripts/publish-check/history_scan.py +176 -0
  505. package/scripts/publish-check/hit_location.py +18 -0
  506. package/scripts/publish-check/hygiene_config.py +42 -0
  507. package/scripts/publish-check/hygiene_text.py +11 -0
  508. package/scripts/publish-check/identity-recipe.mjs +54 -0
  509. package/scripts/publish-check/json_identity.py +79 -0
  510. package/scripts/publish-check/license-exceptions.json +11 -0
  511. package/scripts/publish-check/licenses.py +49 -0
  512. package/scripts/publish-check/policy.py +18 -0
  513. package/scripts/publish-check/project_identity.py +89 -0
  514. package/scripts/publish-check/provenance.py +91 -0
  515. package/scripts/publish-check/provenance_pins.py +33 -0
  516. package/scripts/publish-check/public_base.py +63 -0
  517. package/scripts/publish-check/public_hygiene.py +80 -0
  518. package/scripts/publish-check/publication-config.json +12 -0
  519. package/scripts/publish-check/publication_config.py +34 -0
  520. package/scripts/publish-check/report.py +52 -0
  521. package/scripts/publish-check/repository_commands.py +75 -0
  522. package/scripts/publish-check/repository_intent.py +52 -0
  523. package/scripts/publish-check/reviewed.py +41 -0
  524. package/scripts/publish-check/source_hashes.mjs +6 -0
  525. package/scripts/publish-check/tree_scan.py +88 -0
  526. package/scripts/publish-check/trust_bundle.py +87 -0
  527. package/scripts/publish-check.mjs +58 -0
@@ -0,0 +1,31 @@
1
+ import { access, mkdir, readdir, copyFile, writeFile } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { resolve, join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { assert } from './validation.mjs';
7
+ const base = resolve(dirname(fileURLToPath(import.meta.url)),'../..');
8
+ /** Generate alongside an edition. The existing edition tree is never an output directory. */
9
+ export async function createTemplates({destination,project,sourcePaths,commands,loops=[]}) {
10
+ assert(/^[a-z0-9-]+\/[a-z0-9-]+$/.test(project),'invalid-project');
11
+ assert(Array.isArray(sourcePaths) && sourcePaths.length && sourcePaths.every(x => typeof x === 'string' && x.length),'missing-actual-source-paths');
12
+ const root = resolve(destination);
13
+ await mkdir(root,{recursive:false});
14
+ // A new directory only: callers add actual edition sources then run the lock command.
15
+ const scripts = join(root,'scripts'); await mkdir(scripts);
16
+ await mkdir(join(scripts,'edition-authoring'));
17
+ for (const name of ['operations.mjs','project-operations.mjs']) await copyFile(join(base,'operations',name),join(scripts,name),constants.COPYFILE_EXCL);
18
+ for (const name of await readdir(join(base,'edition-authoring'))) if (name.endsWith('.mjs') && !name.endsWith('.test.mjs')) await copyFile(join(base,'edition-authoring',name),join(scripts,'edition-authoring',name),constants.COPYFILE_EXCL);
19
+ await writeFile(join(root,'OPERATIONS.json'),JSON.stringify({version:2,project,runtime:{requirements:[{name:'node',version:'>=22.12.0'}]},requiredEnvironment:[],commands,loops},null,2)+'\n',{flag:'wx'});
20
+ await writeFile(join(root,'edition.json'),JSON.stringify({schemaVersion:1,id:project,toolchain:{id:'runeditrun/edition-authoring',version:'1.1.0'},parent:null,components:[{id:project,role:'edition',source:{kind:'local',paths:sourcePaths}}]},null,2)+'\n',{flag:'wx'});
21
+ return {root,lock:['node','scripts/edition-authoring/cli.mjs','lock'],check:['node','scripts/edition-authoring/cli.mjs','check']};
22
+ }
23
+ /** Actual authoring tool creates the lock and retained sources; no hand-forged lock template. */
24
+ export async function lockTemplates(root) {
25
+ try { await access(join(root,'sources.lock.json')); throw new Error('existing-source-lock-refused'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
26
+ const env = {PATH:process.env.PATH,HOME:process.env.HOME};
27
+ for (const command of ['lock','check']) {
28
+ try { execFileSync(process.execPath,[join(base,'edition-authoring/cli.mjs'),command],{cwd:root,env,stdio:'pipe',timeout:60000,maxBuffer:1024*1024}); }
29
+ catch { throw new Error('edition-authoring-failed'); }
30
+ }
31
+ }
@@ -0,0 +1,58 @@
1
+ import { createHash } from 'node:crypto';
2
+ export const hash = value => createHash('sha256').update(typeof value === 'string' || Buffer.isBuffer(value) ? value : stable(value)).digest('hex');
3
+ export function stable(value) {
4
+ if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
5
+ if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`;
6
+ return JSON.stringify(value);
7
+ }
8
+ export function assert(value, code) { if (!value) throw new Error(code); }
9
+ export const identifier = value => typeof value === 'string' && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value);
10
+ export const resourceName = value => typeof value === 'string' && /^[a-z][a-z0-9-]{2,62}$/.test(value);
11
+ export const uuid = value => typeof value === 'string' && /^(?!00000000-0000-0000-0000-000000000000)[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value);
12
+ export const hexId = value => typeof value === 'string' && /^[a-f0-9]{32}$/i.test(value) && !/^0+$/.test(value);
13
+ export function localPath(value) { assert(typeof value === 'string' && value.length && !value.startsWith('/') && !value.includes('\\') && value.split('/').every(x => x && x !== '.' && x !== '..') && !/[\x00-\x1f]/.test(value), 'invalid-local-path'); return value; }
14
+ /** Declared KV namespaces; absent means none. */
15
+ export const kvOf = plan => plan.kv ?? [];
16
+ export function validatePlan(plan) {
17
+ assert(plan?.version === 1 && resourceName(plan.worker) && /^\d{4}-\d{2}-\d{2}$/.test(plan.compatibilityDate), 'invalid-plan');
18
+ for (const key of ['d1','r2','queues','durableObjects','secrets','crons']) assert(Array.isArray(plan[key]), 'invalid-plan-resources');
19
+ // KV is optional so plans written before KV support keep their exact shape.
20
+ assert(plan.kv === undefined || Array.isArray(plan.kv), 'invalid-plan-resources');
21
+ const kv = kvOf(plan);
22
+ const bindings = new Set(['RER_DEPLOY_SOURCE','RER_DEPLOY_FINGERPRINT','RER_VERSION']);
23
+ // Service bindings and the workers.dev opt-out are optional so earlier plans keep their exact shape.
24
+ assert(plan.services === undefined || Array.isArray(plan.services) && plan.services.length, 'invalid-plan-resources');
25
+ assert(plan.workersDev === undefined || plan.workersDev === false, 'invalid-plan');
26
+ for (const entry of [...plan.d1,...plan.r2,...kv,...plan.queues,...plan.durableObjects,...plan.secrets,...(plan.services ?? [])]) {
27
+ assert(identifier(entry.binding) && !bindings.has(entry.binding), 'duplicate-or-invalid-binding'); bindings.add(entry.binding);
28
+ }
29
+ for (const entry of [...plan.d1,...plan.r2,...kv,...plan.queues]) assert(resourceName(entry.name), 'invalid-resource-name');
30
+ for (const list of [plan.d1,plan.r2,kv,plan.queues]) assert(new Set(list.map(x => x.name)).size === list.length, 'duplicate-resource-name');
31
+ for (const entry of plan.d1) assert(Array.isArray(entry.migrations), 'invalid-migrations');
32
+ for (const entry of plan.durableObjects) assert(identifier(entry.className) && ['sqlite','classic'].includes(entry.storage), 'invalid-durable-object');
33
+ for (const secret of plan.secrets) assert(identifier(secret.environment), 'invalid-secret-environment');
34
+ for (const item of plan.services ?? []) assert(Object.keys(item).every(k => ['binding','service','entrypoint','props','grant'].includes(k)) && resourceName(item.service) && item.service !== plan.worker
35
+ && (item.entrypoint === undefined || identifier(item.entrypoint))
36
+ && (item.props === undefined || Object.keys(item.props).join() === 'allowedHosts' && Array.isArray(item.props.allowedHosts)
37
+ && item.props.allowedHosts.every(host => typeof host === 'string' && /^(?:\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(host))), 'invalid-service-binding');
38
+ for (const item of plan.services ?? []) if (item.grant !== undefined) {
39
+ const grant = item.grant;
40
+ assert(item.entrypoint && !item.props && grant && Object.keys(grant).sort().join() === 'allowedHosts,fingerprint,provider,sourceSha'
41
+ && /^[a-f0-9]{40}$/.test(grant.sourceSha) && /^[a-f0-9]{64}$/.test(grant.fingerprint)
42
+ && grant.provider && Object.keys(grant.provider).sort().join() === 'deploymentId,etag,versionId'
43
+ && uuid(grant.provider.versionId) && uuid(grant.provider.deploymentId) && typeof grant.provider.etag === 'string' && grant.provider.etag.length > 0
44
+ && (grant.allowedHosts === null || Array.isArray(grant.allowedHosts) && new Set(grant.allowedHosts).size === grant.allowedHosts.length
45
+ && grant.allowedHosts.every(host => typeof host === 'string' && /^(?:\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(host))), 'invalid-service-grant');
46
+ }
47
+ for (const cron of plan.crons) assert(typeof cron === 'string' && /^[\d*/,-]+(?: [\d*/,-]+){4}$/.test(cron), 'invalid-cron');
48
+ assert(new Set(plan.crons).size === plan.crons.length, 'duplicate-cron');
49
+ for (const [key,value] of Object.entries(plan.vars ?? {})) assert(identifier(key) && !bindings.has(key) && typeof value === 'string', 'invalid-public-variable');
50
+ for (const queue of plan.queues) {
51
+ if (queue.deadLetterQueue !== undefined) assert(queue.consumer && resourceName(queue.deadLetterQueue) && queue.deadLetterQueue !== queue.name && plan.queues.some(x => x.name === queue.deadLetterQueue && !x.consumer), 'invalid-dead-letter-queue');
52
+ if (queue.consumer) {
53
+ assert(Object.keys(queue.consumer).every(k => ['batch_size','max_retries','max_wait_time_ms','max_concurrency','retry_delay'].includes(k)), 'invalid-consumer-setting');
54
+ assert(Object.values(queue.consumer).every(x => Number.isSafeInteger(x) && x >= 0), 'invalid-consumer-setting');
55
+ }
56
+ }
57
+ return plan;
58
+ }
@@ -0,0 +1,105 @@
1
+ import { extname } from 'node:path';
2
+ import { assert, hash, hexId, uuid } from './validation.mjs';
3
+ import { moduleType, assetType } from './artifact.mjs';
4
+ import { RUN_WORKER_FIRST } from './render.mjs';
5
+
6
+ export async function activeWorker(api, worker) {
7
+ let settings;
8
+ try { settings = await api.request(`/workers/scripts/${worker}/settings`); }
9
+ catch (error) { if (error.status === 404) return null; throw error; }
10
+ assert(Array.isArray(settings.bindings),'invalid-worker-settings');
11
+ const result = await api.request(`/workers/scripts/${worker}/deployments`);
12
+ assert(Array.isArray(result.deployments) && result.deployments.length > 0, 'invalid-deployments');
13
+ const deployment = result.deployments[0];
14
+ assert(uuid(deployment.id) && deployment.versions?.length === 1 && deployment.versions[0].percentage === 100 && uuid(deployment.versions[0].version_id), 'mixed-or-invalid-deployment');
15
+ const versionId = deployment.versions[0].version_id;
16
+ const version = await api.request(`/workers/scripts/${worker}/versions/${versionId}`);
17
+ assert(version.id === versionId && typeof version.resources?.script?.etag === 'string' && version.resources.script.etag.length > 0, 'invalid-active-version');
18
+ return {settings,versionId,deploymentId:deployment.id,etag:version.resources.script.etag,version};
19
+ }
20
+ /** Read authenticated runtime detection, never infer handlers from uploaded source text. */
21
+ export function verifyScheduledHandler(active, plan) {
22
+ if (plan.crons.length) assert(Array.isArray(active?.version?.resources?.script?.handlers) && active.version.resources.script.handlers.includes('scheduled'), 'missing-scheduled-handler');
23
+ }
24
+ /** The build fingerprint a Worker's settings (or an upload's metadata) carry as a plain-text binding. */
25
+ export const deployedFingerprint = settings => settings?.bindings?.find(x => x.type === 'plain_text' && x.name === 'RER_DEPLOY_FINGERPRINT')?.text;
26
+ export async function durableNamespaces(api, plan, { requireAll = false } = {}) {
27
+ if (!plan.durableObjects.length) return [];
28
+ const rows = await api.list('/workers/durable_objects/namespaces');
29
+ return plan.durableObjects.map(item => {
30
+ const selected = rows.filter(x => x.script === plan.worker && x.class === item.className);
31
+ assert(selected.length <= 1, 'ambiguous-durable-object');
32
+ if (!selected.length) { assert(!requireAll,'missing-durable-object'); return {...item,id:null}; }
33
+ const row = selected[0]; assert(hexId(row.id) && row.use_sqlite === (item.storage === 'sqlite'), 'incompatible-durable-object');
34
+ return {...item,id:row.id};
35
+ });
36
+ }
37
+ export function durableMigration(namespaces, current) {
38
+ const missing = namespaces.filter(x => !x.id);
39
+ if (!missing.length) return undefined;
40
+ assert(missing.every(x => x.storage === 'sqlite'),'new-durable-objects-require-sqlite');
41
+ const old = current?.settings.migration_tag ?? '';
42
+ return {old_tag:old,new_tag:`rer-${hash({old,classes:missing.map(x => x.className)}).slice(0,24)}`,new_sqlite_classes:missing.map(x => x.className)};
43
+ }
44
+ export async function secretBindings(api, worker, plan, env, exists) {
45
+ const present = exists ? await api.request(`/workers/scripts/${worker}/secrets`) : [];
46
+ assert(Array.isArray(present) && present.every(x => typeof x.name === 'string' && x.type === 'secret_text'),'invalid-secret-list');
47
+ const bindings = []; const names = present.map(x => x.name);
48
+ for (const item of plan.secrets) {
49
+ if (names.includes(item.binding)) bindings.push({type:'inherit',name:item.binding});
50
+ else {
51
+ assert(typeof env[item.environment] === 'string' && env[item.environment].trim(), `missing-secret-${item.environment}`);
52
+ bindings.push({type:'secret_text',name:item.binding,text:env[item.environment]});
53
+ }
54
+ }
55
+ return {bindings,names,missing:plan.secrets.filter(x => !names.includes(x.binding)).map(x => x.binding)};
56
+ }
57
+ export async function uploadAssets(api, worker, files) {
58
+ const manifest = {}; const byHash = new Map();
59
+ for (const file of files) {
60
+ const digest = hash(file.base64 + extname(file.path).slice(1)).slice(0,32);
61
+ manifest[`/${file.path}`] = {hash:digest,size:Buffer.from(file.base64,'base64').length};
62
+ byHash.set(digest,file);
63
+ }
64
+ const session = await api.request(`/workers/scripts/${worker}/assets-upload-session`,{method:'POST',body:{manifest}});
65
+ assert(typeof session.jwt === 'string' && session.jwt && Array.isArray(session.buckets),'invalid-asset-session');
66
+ let completion = session.buckets.length ? null : session.jwt;
67
+ const seen = new Set();
68
+ for (const bucket of session.buckets) {
69
+ assert(Array.isArray(bucket) && bucket.length,'invalid-asset-bucket');
70
+ const data = new FormData();
71
+ for (const key of bucket) {
72
+ assert(byHash.has(key) && !seen.has(key),'invalid-asset-request'); seen.add(key);
73
+ const file = byHash.get(key); data.set(key,new Blob([file.base64],{type:assetType(file.path)}),file.path.split('/').at(-1));
74
+ }
75
+ const uploaded = await api.request('/workers/assets/upload?base64=true',{method:'POST',body:data,assetToken:session.jwt});
76
+ if (uploaded.jwt) completion = uploaded.jwt;
77
+ }
78
+ assert(typeof completion === 'string' && completion,'incomplete-asset-upload');
79
+ return completion;
80
+ }
81
+ export async function uploadWorker(api, {plan,artifact,bindings,secrets,migration,fingerprint}) {
82
+ const data = new FormData();
83
+ const metadata = {main_module:artifact.main,compatibility_date:plan.compatibilityDate,compatibility_flags:plan.compatibilityFlags ?? [],bindings:[...bindings,...secrets.bindings],keep_bindings:['secret_text'],annotations:{'workers/message':`rer-deploy:${fingerprint}`}};
84
+ if (migration) metadata.migrations = migration;
85
+ if (artifact.assets.length) { metadata.assets = {jwt:await uploadAssets(api,plan.worker,artifact.assets),config:{run_worker_first:RUN_WORKER_FIRST}}; }
86
+ data.set('metadata',new Blob([JSON.stringify(metadata)],{type:'application/json'}));
87
+ for (const file of artifact.modules) data.set(file.path,new Blob([Buffer.from(file.base64,'base64')],{type:moduleType(file.path)}),file.path);
88
+ await api.request(`/workers/scripts/${plan.worker}?bindings_inherit=strict`,{method:'PUT',body:data});
89
+ }
90
+ /** The workers.dev origin, enabling it when allowed. With `workersDev: false` the route is kept
91
+ * off (a Worker reached only through service bindings) and there is no origin. */
92
+ export async function workerOrigin(api, worker, { enable = false, workersDev = true } = {}) {
93
+ if (!workersDev) {
94
+ const state = await api.request(`/workers/scripts/${worker}/subdomain`);
95
+ assert(typeof state.enabled === 'boolean','invalid-worker-subdomain');
96
+ if (state.enabled) { assert(enable,'worker-subdomain-enabled'); await api.request(`/workers/scripts/${worker}/subdomain`,{method:'POST',body:{enabled:false,previews_enabled:false}}); }
97
+ return null;
98
+ }
99
+ const result = await api.request('/workers/subdomain');
100
+ assert(typeof result.subdomain === 'string' && /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(result.subdomain),'invalid-account-subdomain');
101
+ const state = await api.request(`/workers/scripts/${worker}/subdomain`);
102
+ assert(typeof state.enabled === 'boolean','invalid-worker-subdomain');
103
+ if (!state.enabled) { assert(enable,'worker-subdomain-disabled'); await api.request(`/workers/scripts/${worker}/subdomain`,{method:'POST',body:{enabled:true,previews_enabled:false}}); }
104
+ return `https://${worker}.${result.subdomain}.workers.dev`;
105
+ }
@@ -0,0 +1,90 @@
1
+ # Erasure contract v1
2
+
3
+ The module erases one subject (an entity that holds personal data) and everything
4
+ declared as depending on it, for BASE-DATA-003. It keeps audit records with an
5
+ anonymised reference, as BASE-OPS-005 requires. Public signatures are in
6
+ `index.d.mts`. The seed owns authorization, confirmation UI, holds and any effect
7
+ outside D1 and R2.
8
+
9
+ - **ERASE-001 — Declared graph.** A seed declares one root table per entity and
10
+ the edges that reach its dependents: column equality (`on`) or a bound SQL
11
+ predicate (`where` + `params` taken from the parent row). A table may be
12
+ reached through several edges, and the closure runs to a fixed point. Kept
13
+ rows that must lose columns are declared as `redact` with a reason. Audit
14
+ references are declared as `audit`. `root.redact` keeps an opaque anonymous
15
+ root with a reason and explicit replacements for every non-identity column.
16
+ The same transaction advances the maintained portability redaction checkpoint;
17
+ old archives are refused with a trusted later journal, and post-redaction
18
+ archives preserve anonymous attribution. Structural identities are verified.
19
+ Root-return edges and cyclic deletion dependencies are refused before mutation.
20
+ - **ERASE-002 — No silent dependents.** Before preview or erasure, every table
21
+ with a foreign key into a tracked table, including a redacted root, must be declared (as an edge, a
22
+ redaction, an audit reference or a retained exception). Otherwise the call
23
+ refuses with `undeclared_dependent:<child>:<parent>` (409) and changes
24
+ nothing. This includes product tables named `rer_*`. Destructive FK actions
25
+ into retained/redacted/audit rows are refused; retained keys are verified
26
+ after effects, so `ON DELETE` actions never remove a promised retained row.
27
+ - **ERASE-003 — Preview without values.** `preview` returns counts per deleted,
28
+ redacted and audited table, the blob count, the retained exceptions, a hold
29
+ reason if any, and a plan hash. It never returns a stored value.
30
+ - **ERASE-004 — Cascade and fence in one commit.** Every deleted row (dependents
31
+ leaves-first, the subject row last) is deleted in a D1 batch that also holds
32
+ portability's `erasureStatements(table, key)` for that row and
33
+ `invalidateTransfersStatements()`. A batch holds at most `rows` dependents, so
34
+ deletion work is bounded. Scope/audit and completeness checks also read the
35
+ declared closure; their cost grows with that closure. A guard on the operation's step
36
+ counter makes a concurrent runner fail with `erasure_busy` (409) instead of
37
+ committing twice.
38
+ - **ERASE-005 — Blobs after the commit.** For a declared `blob` edge, each row's
39
+ object key is recorded in the same batch that deletes the row. Objects are
40
+ deleted only after that commit. Each recorded key is checked with a read, and
41
+ leaves the work list only once the object is gone. Repeating the blob phase is
42
+ safe.
43
+ - **ERASE-006 — Complete means complete.** A receipt reads `deleting` while rows
44
+ remain, `blobs` once every row is committed but some object may remain, and
45
+ `complete` only when the final fenced transaction has re-read every declared
46
+ dependent and audit path, retained/root-redacted rows have been verified, and
47
+ the blob work list is empty. New dependents restart collection; a persistently
48
+ changing scope refuses with `erasure_scope_unstable` and remains resumable. `completedAt` and `seconds` are
49
+ set only at `complete`.
50
+ - **ERASE-007 — Untraceable audit label.** Declared audit references are
51
+ replaced with one label per erasure, `erased:<128 random bits>`. The label is
52
+ not derived from the subject, so nobody who holds the database or an older
53
+ archive can recompute it or test a guess against it. Declared `clear` columns
54
+ are overwritten. Audit rows for selected dependents are relabelled in every batch. Temporary
55
+ scope preserves earlier keys until the final pass, catching audit rows written
56
+ after those dependents disappeared; that scope is then removed atomically.
57
+ - **ERASE-008 — Value-free receipt.** The receipt row holds the entity name,
58
+ label, planned and deleted counts per table, retained exceptions with reasons,
59
+ blob totals and times. It holds the subject key only while rows are still
60
+ being deleted, and clears it when the subject row commits.
61
+ - **ERASE-009 — Declared exceptions.** A table kept on purpose is declared in
62
+ `retain` with a reason. Preview and receipt list it, including a root redaction reason. Nothing is skipped
63
+ without a declaration.
64
+ - **ERASE-010 — One open erasure per subject.** `start` for a subject that has an
65
+ open erasure in `deleting` returns that erasure unchanged. Once the subject row
66
+ is gone, `start` answers `subject_not_found` (404) and changes nothing.
67
+ - **ERASE-011 — Seed policy hooks.** `hold` can refuse with a reason
68
+ (`held:<reason>`, 409). `statements` adds product statements to the batch that
69
+ commits the subject row, after deletion. `beforeStatements` adds durable
70
+ pre-deletion policy effects in the first batch. Hooks run in the seed's code.
71
+ - **ERASE-012 — import-kit composition.** `nativeStatements(entity, subject)`
72
+ returns the whole D1 phase as import-kit `D1Sql` (`{sql, params}`), opening
73
+ the receipt in the same statements. These are passed to
74
+ `eraseD1ImportTarget(kind, id, nativeStatements, context)`, so kit custody,
75
+ the product cascade and the fence commit in one batch. `deleteBlobs` then runs
76
+ as usual. `retireD1ImportCustody` takes the same statements for a whole
77
+ person's import custody and its erasure journal.
78
+ - **ERASE-013 — Stated limits.** An old archive restored into a brand-new store
79
+ that has no journal cannot be recognised. Raw database imports bypass every
80
+ product fence. Copies outside the installation are out of reach: downloaded
81
+ archives, delivered mail, provider records and platform backups.
82
+ - **ERASE-014 — Attributable counters.** A counter or rate row keyed by a person id,
83
+ email or digest is written with a `rer_erasure_rates` correlation in the same D1
84
+ batch (`rateWrite` / `rateCorrelation`), guarded on the subject row existing and,
85
+ for a redacted root, on its `live` predicate: a declaration with a redacted root
86
+ and a rate edge on it is refused without one, and erasure refuses a redaction that
87
+ leaves it true, so no write is admitted after erasure in either mode.
88
+ `rateEdges` declares the correlation and the counter rows it names as edges, so one
89
+ erasure plan deletes both and completion re-reads them. Uncorrelated aggregates are
90
+ untouched. A key that was never correlated cannot be found.
@@ -0,0 +1,221 @@
1
+ # Subject erasure
2
+
3
+ This dependency-free source module erases one subject (a person, contact,
4
+ requester, booking and so on) and everything a seed declares as depending on
5
+ it, for BASE-DATA-003. Audit records survive with an anonymised reference
6
+ (BASE-OPS-005). It was generalised from the erasure code of Penny, Orbit, Dial,
7
+ HappyToHelp, Cradle and Slate. Each seed keeps its own authorization, confirmation UI, holds,
8
+ retained exceptions and effects outside D1 and R2. Clauses are in
9
+ [CONTRACT.md](CONTRACT.md).
10
+
11
+ ## Declaring an entity
12
+
13
+ ```js
14
+ import { defineErasure, createErasure } from './vendor/base/modules/erasure/index.mjs';
15
+ export const person = defineErasure({
16
+ entity: 'person',
17
+ root: { table: 'people' },
18
+ edges: [
19
+ { table: 'person_notes', from: 'people', on: { person_id: 'id' } },
20
+ { table: 'person_files', from: 'people', on: { person_id: 'id' }, blob: { bucket: 'FILES', keyColumn: 'storage_key' } },
21
+ { table: 'signals', from: 'people', where: 'instr(payload_json,?)>0', params: ['email'] },
22
+ ],
23
+ redact: [{ table: 'messages', from: 'people', on: { author_id: 'id' }, set: { body: '' }, reason: 'transcript order is kept' }],
24
+ audit: [{ table: 'audit_events', column: 'entity_id', of: 'people', where: "entity_type='person'", clear: { details: '{}' } }],
25
+ retain: [{ table: 'opt_out_hashes', reason: 'keyed hash prevents re-contact' }],
26
+ hold: async ({ db, subject }) => null, // or a reason string: refuses with 409
27
+ beforeStatements: async ({ db, subject, label }) => [], // durable copy/unlink before the first child batch
28
+ statements: async ({ db, subject, label }) => [], // product SQL committed with the subject row
29
+ });
30
+ ```
31
+
32
+ | Part | Meaning |
33
+ | --- | --- |
34
+ | `root` | The subject's table; optional `redact: {set, reason, identityColumns?}` keeps an anonymous root. `set` overwrites every non-key, non-identity column with declared constants, or a function of `{subject,anonymousId}`. `identityColumns` may contain only opaque structural links (e.g. project/workspace IDs), never personal fields. Keys come from its primary key (`{rowid}` for keyless tables). |
35
+ | `edges` | Rows deleted with the subject. `on` maps child columns to parent columns. `where` is a bound SQL predicate whose `params` name parent columns. A table may appear in several edges. Overlapping paths are deduplicated and ordered by dependency. A cyclic deletion graph is refused before mutation; unlink cycles in seed policy before using this helper. |
36
+ | `blob` | On an edge: the R2 binding and the column holding the object key. Keys should be opaque (no file names). |
37
+ | `redact` | Kept rows whose columns are overwritten, each with a reason. |
38
+ | `audit` | Audit columns referencing rows of a deleted table (single-column key). They receive the erasure's label, and `clear` columns are overwritten. |
39
+ | `retain` | Tables kept on purpose, with reasons. Listed in preview and receipt. |
40
+ | `hold`, `beforeStatements`, `statements` | Refusal, durable pre-deletion effects (first batch), and final effects (after deletes). |
41
+
42
+ Any table with a foreign key into a deleted table must be declared. Otherwise
43
+ preview and erasure refuse with `undeclared_dependent:<child>:<parent>`, because a
44
+ `CASCADE` would remove a row that erasure never fenced. This includes `rer_*` product
45
+ tables. A destructive FK action into a retained/redacted/audit table is refused
46
+ with `unsafe_retained_fk:<child>:<parent>`. This is a schema-level refusal, even
47
+ when current links are null. Migrate such retained links to non-destructive
48
+ `NO ACTION` FKs and detach them before admission, or keep a redacted parent. Retained keys are
49
+ verified after each batch and before completion.
50
+
51
+ ## Phases
52
+
53
+ ```js
54
+ const erasure = createErasure({ db: env.DB, buckets: { FILES: env.FILES }, portability, declarations: [person] });
55
+ const preview = await erasure.preview('person', { id }); // counts, blob count, retained, planHash
56
+ const opened = await erasure.start('person', { id }, { planHash }); // receipt, phase 'deleting'
57
+ const receipt = await erasure.run(opened.id, { until: Date.now() + 20_000 });
58
+ if (receipt.phase !== 'complete') await env.JOBS.send({ erasure: opened.id }); // continue later
59
+ ```
60
+
61
+ `portability` is the seed's `createPortability({db,buckets,policy})` service.
62
+ `commit(id,{rows,until})` runs only the D1 phase: bounded atomic batches of at
63
+ most `rows` dependents (default 500), leaves first, with the subject row last.
64
+ Each deleted row's `erasureStatements` and `invalidateTransfersStatements()` go
65
+ in the same batch. `deleteBlobs(id,{until})` runs only the blob phase, after the
66
+ commit. `run` does both within the time budget. Every phase is safe to repeat,
67
+ and a concurrent runner gets `erasure_busy` (409). The receipt says `complete`
68
+ only after the completion transaction re-reads every declared dependent and audit path
69
+ inside the fence, then every recorded object has been deleted and read back as
70
+ absent. Scope changes roll back that step and resume collection. Temporary scope
71
+ remembers earlier parents through blob cleanup, so rows or audits added after the
72
+ first D1 fence return to deletion before completion. It is scrubbed atomically
73
+ with completion. A root deleted outside the helper is refused rather than reported complete.
74
+
75
+ For retained attribution, use `root: {table: 'operators', redact: {reason: 'stable
76
+ anonymous attribution', set: ({anonymousId}) => ({email: anonymousId + '@removed.invalid',
77
+ name: 'Removed operator', status: 'removed'})}}` and explicitly set any other
78
+ non-identity columns in that schema. Only opaque IDs survive; root redaction also
79
+ advances portability's redaction checkpoint; completion verifies the replacements
80
+ and preserved structural IDs. Older archives are refused when the destination or
81
+ a supplied trusted later journal knows that checkpoint, while a post-redaction
82
+ archive can restore the anonymous row. The portability adapter must forward
83
+ `redactionStatements()` for this mode.
84
+ `anonymousId` is independent of the audit label, so retained root values cannot
85
+ link that label back to the opaque identity. Root setters must be deterministic
86
+ for their input and must not read or copy personal data.
87
+
88
+ A subject too large for one request continues through the seed's queue, and a
89
+ scheduled sweep picks up any erasure whose message was lost. The default seed's
90
+ `src/core/features/people/routes.ts` shows both. The receipt (`receipt(id)`)
91
+ holds entity, label, planned and deleted counts, retained exceptions, blob totals
92
+ and times, and never a stored value. The subject key is held only while rows
93
+ are still being deleted.
94
+
95
+ ## Attributable counters
96
+
97
+ A rate limit or counter whose key is a person id, an email or a digest of one is
98
+ personal data, and a hashed key cannot be traced back to its person afterwards. So the
99
+ write records who the key belongs to, in the same D1 batch as the counter upsert, and
100
+ the declaration erases both together.
101
+
102
+ ```js
103
+ import { rateEdges, rateWrite } from './vendor/base/modules/erasure/index.mjs';
104
+ const root = { table: 'people', column: 'id' };
105
+ // in the declaration
106
+ edges: [...rateEdges({ entity: 'person', root, counters: [{ table: 'rate_limits', keyColumn: 'key' }] })],
107
+ // at every write whose key identifies a known person
108
+ const [counted] = await rateWrite(db, { entity: 'person', root, subject: person.id, rateTable: 'rate_limits', rateKey: key },
109
+ [db.prepare('INSERT INTO rate_limits(key,window,count) VALUES(?,?,1) ON CONFLICT(key) DO UPDATE SET count=count+1 RETURNING count').bind(key, window)]);
110
+ ```
111
+
112
+ `rateWrite` returns the results of your statements (so admission still reads `count`).
113
+ Where the seed owns its batch, put `rateCorrelation(db, spec)` (three statements) first
114
+ in that batch instead. They abort the batch when the subject row is already gone, so a
115
+ write admitted after erasure writes nothing and `rateWrite` refuses `subject_gone`
116
+ (409); a write admitted while an erasure runs is caught by the completion re-read.
117
+
118
+ A redacted root (retained attribution, above) keeps its row under the same id, so "the row
119
+ exists" no longer means "the person is still here". Give that root a `live` predicate
120
+ over its row, false once redacted, and use the same `root` in `rateEdges` and every
121
+ `rateWrite`/`rateCorrelation`:
122
+
123
+ ```js
124
+ const root = { table: 'operators', column: 'id', live: "status<>'removed'" };
125
+ ```
126
+
127
+ The write guard then requires the row and the predicate. `defineErasure` refuses a
128
+ declaration with a redacted root and a rate edge on that root but no `live`
129
+ (`rate_root_live:<edge>`), and admission refuses (`rate_root_live`, 409) when the
130
+ predicate is still true for the redaction's `set` values, or names a column the
131
+ redaction does not set. A deleted root needs no `live`.
132
+ Preview and receipt count `rer_erasure_rates` and the counter table like any edge.
133
+
134
+ Seed adoption note:
135
+
136
+ 1. Apply `schema/erasure-rates.sql` as the next numbered migration, after the erasure
137
+ migrations. Add `rer_erasure_rates` and each counter table to the portability
138
+ policy's `excludeTables` (rate windows are ephemeral).
139
+ 2. Spread `rateEdges(...)` into the declaration of the entity the key identifies, one
140
+ `counters` item per rate table. Every counter table needs a primary key.
141
+ 3. Route each write whose key is derived from a known person through `rateWrite` or
142
+ `rateCorrelation`. Keys that identify nobody (client IP, global or route scope)
143
+ are written directly and survive as aggregates; never correlate those.
144
+ 4. One key must identify one person. Erasing a person deletes every counter row their
145
+ correlation names.
146
+
147
+ Limits: a row written before the person existed, or for an address with no person
148
+ record, has no subject and is not found; use an expiring window for it or key it by
149
+ something that identifies nobody. Rows written before adoption have no correlation:
150
+ the seed retires them in its own migration.
151
+
152
+ ## Seams
153
+
154
+ - **Portability.** This module adds no second fence. It calls the service's
155
+ `erasureStatements` for every deleted row (children and files included, not
156
+ only the subject), so an older archive that contains any of those rows is
157
+ refused on restore. The fence's own limit stays: an old archive offered to a
158
+ brand-new store that has no journal cannot be recognised. Supply the owner's
159
+ later journal there (`rer import --journal LATER_ARCHIVE`).
160
+ - **import-kit.** `nativeStatements(entity, subject)` returns the whole D1 phase
161
+ as `{sql, params}` statements for `eraseD1ImportTarget(kind, id,
162
+ nativeStatements, context)`. Kit custody, the product cascade and the fence
163
+ then commit in one batch. Afterwards call `deleteBlobs(operationId)`. This
164
+ path is unbounded (one batch), so use it for imported targets of ordinary size.
165
+ To erase a person (not one record), pass the same statements to
166
+ `retireD1ImportCustody({ target, values }, nativeStatements, context)`: it also
167
+ retires imports whose headers hold the person's exact identifiers and writes
168
+ the import erasure journal (import-kit `src/d1/README.md`).
169
+
170
+ ## Adopting
171
+
172
+ Copy these files byte for byte to a folder the Worker build serves, such as
173
+ `src/modules/erasure/`. A seed made by `rer` export also receives them under
174
+ `scripts/base-cli/modules/erasure/`, but `vite dev` serves only `src`, `public`
175
+ and `node_modules`, so runtime imports must not reach into `scripts/`. The
176
+ standalone default seed copies this module and portability into `src/modules/`.
177
+ The files are:
178
+
179
+ ```text
180
+ modules/erasure/index.mjs
181
+ modules/erasure/index.d.mts
182
+ modules/erasure/declaration.mjs
183
+ modules/erasure/plan.mjs
184
+ modules/erasure/order.mjs
185
+ modules/erasure/root.mjs
186
+ modules/erasure/scope.mjs
187
+ modules/erasure/completion.mjs
188
+ modules/erasure/verification.mjs
189
+ modules/erasure/cascade.mjs
190
+ modules/erasure/blobs.mjs
191
+ modules/erasure/audit.mjs
192
+ modules/erasure/receipt.mjs
193
+ modules/erasure/fence.mjs
194
+ modules/erasure/sql.mjs
195
+ modules/erasure/schema/erasure.sql
196
+ modules/erasure/rate.mjs
197
+ modules/erasure/schema/erasure-rates.sql # only when the seed keeps attributable counters
198
+ ```
199
+
200
+ Apply `schema/erasure.sql` as the seed's next numbered migration, after
201
+ portability's. For existing installations apply the scope/guard additions as a new migration
202
+ (default-seed `0017_erasure_scope.sql`), leaving applied migrations unchanged.
203
+ Finish or reconcile open operations with the old helper before upgrading. Older
204
+ operations that discarded their scope refuse `erasure_scope_missing`; the helper
205
+ cannot prove completeness without the earlier parent keys.
206
+ Add `rer_erasure_blobs`, `rer_erasure_guard`, and `rer_erasure_scope` to the portability
207
+ policy's `excludeTables`. Receipts (`rer_erasure_operations`) travel in
208
+ archives; the blob work list does not.
209
+
210
+ ## Historical measurements before the review fixes
211
+
212
+ Through the default seed's route in a local `rer dev` Worker, a subject with
213
+ 10,000 dependent rows (9,500 notes and 500 file rows) and 500 R2 blobs was
214
+ erased in 10.7 s in one run and 15.0 s in another. That is one request: 20
215
+ bounded D1 steps, then the blob phase, with no queue continuation needed. The
216
+ full proof table is kept with the consolidation evidence for this change.
217
+ These pre-fix measurements do not establish the performance of the corrected
218
+ helper. They are local Miniflare measurements, not production capacity claims. Real D1
219
+ statement, CPU and request limits decide `rows` per step and the request budget.
220
+ Larger subjects continue on the queue, and the receipt stays `deleting` or
221
+ `blobs` until done.
@@ -0,0 +1,23 @@
1
+ import { statement, quote, chunks, randomHex } from './sql.mjs';
2
+
3
+ /** A fresh random label per erasure: it is not derived from the subject, so
4
+ * nobody holding the database (or an old archive) can recompute or test it. */
5
+ export const newLabel = () => 'erased:' + randomHex(16);
6
+
7
+ export function auditStatements(db, d, plan, label, { only } = {}) {
8
+ const out = [];
9
+ for (const item of d.audit) {
10
+ if (only && item.of !== only) continue;
11
+ const keys = [...plan.known.get(item.of).values()].map(row => row[plan.keys.get(item.of).pk[0]]);
12
+ const clear = Object.entries(item.clear ?? {});
13
+ const set = [`${quote(item.column)}=?`, ...clear.map(([c]) => `${quote(c)}=?`)].join(',');
14
+ // A previously relabelled row is still this operation's audit: a writer
15
+ // may have changed its detail after the first cleanup.
16
+ out.push(statement(db, `UPDATE ${quote(item.table)} SET ${set} WHERE ${quote(item.column)}=?${item.where ? ` AND (${item.where})` : ''}`, label, ...clear.map(([, v]) => v), label));
17
+ for (const part of chunks(keys, 200)) {
18
+ out.push(statement(db, `UPDATE ${quote(item.table)} SET ${set} WHERE ${quote(item.column)} IN (SELECT value FROM json_each(?))${item.where ? ` AND (${item.where})` : ''}`,
19
+ label, ...clear.map(([, v]) => v), JSON.stringify(part)));
20
+ }
21
+ }
22
+ return out;
23
+ }
@@ -0,0 +1,45 @@
1
+ import { all, statement, insist } from './sql.mjs';
2
+ import { settle } from './completion.mjs';
3
+
4
+ /** Runs only after the D1 commit recorded every key. Deleting an absent object
5
+ * succeeds, so an interrupted run repeats safely; a key leaves the work list
6
+ * only after a read shows the object gone. */
7
+ export async function deleteBlobs(ctx, d, id, { until = Infinity, page = 1000 } = {}) {
8
+ const { db, buckets } = ctx;
9
+ const op = await statement(db, 'SELECT phase FROM rer_erasure_operations WHERE id=?', id).first();
10
+ insist(op, 'erasure_not_found', 404);
11
+ if (op.phase !== 'blobs') return op.phase;
12
+ if (!(await all(db, "SELECT 1 FROM rer_erasure_scope WHERE operation=? AND kind='graph' AND table_name=? LIMIT 1", id, d.root.table)).length) {
13
+ const current = await statement(db, 'SELECT phase FROM rer_erasure_operations WHERE id=?', id).first();
14
+ if (current?.phase === 'complete') return 'complete';
15
+ insist(false, 'erasure_scope_missing', 409);
16
+ }
17
+ let changes = 0;
18
+ for (;;) {
19
+ const rows = await all(db, 'SELECT bucket,key FROM rer_erasure_blobs WHERE operation=? ORDER BY bucket,key LIMIT ?', id, page);
20
+ if (!rows.length) {
21
+ const phase = await settle(ctx, d, id);
22
+ if (phase === 'complete' || phase === 'deleting') return phase;
23
+ if (phase === 'changed') insist(++changes < 3, 'erasure_scope_unstable', 409);
24
+ else changes = 0;
25
+ if (Date.now() > until) return 'blobs';
26
+ continue;
27
+ }
28
+ for (const name of new Set(rows.map(r => r.bucket))) {
29
+ const bucket = buckets[name];
30
+ insist(bucket && typeof bucket.delete === 'function', 'missing_bucket:' + name, 500);
31
+ const keys = rows.filter(r => r.bucket === name).map(r => r.key);
32
+ await bucket.delete(keys);
33
+ for (let i = 0; i < keys.length; i += 16) {
34
+ const left = await Promise.all(keys.slice(i, i + 16).map(k => bucket.head(k)));
35
+ insist(left.every(x => x === null), 'blob_remains', 503);
36
+ }
37
+ await db.batch([
38
+ statement(db, 'DELETE FROM rer_erasure_blobs WHERE operation=? AND bucket=? AND key IN (SELECT value FROM json_each(?))', id, name, JSON.stringify(keys)),
39
+ // Derived from what remains, so a concurrent or repeated run cannot double count.
40
+ statement(db, 'UPDATE rer_erasure_operations SET blobs_deleted=blobs_total-(SELECT count(*) FROM rer_erasure_blobs WHERE operation=?) WHERE id=?', id, id),
41
+ ]);
42
+ }
43
+ if (Date.now() > until) return 'blobs';
44
+ }
45
+ }