@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,86 @@
1
+ import { assert } from './validation.mjs';
2
+
3
+ async function bodyPrefix(response) {
4
+ if (!response.body) return '';
5
+ const reader = response.body.getReader(); let bytes = Buffer.alloc(0);
6
+ try {
7
+ while (bytes.length < 256) {
8
+ const {done,value} = await reader.read();
9
+ if (done) break;
10
+ bytes = Buffer.concat([bytes,value.subarray(0,256-bytes.length)]);
11
+ }
12
+ } catch { /* HTTP status remains authoritative if its diagnostic body breaks. */ }
13
+ finally { await reader.cancel().catch(() => {}); }
14
+ return bytes.toString('utf8');
15
+ }
16
+
17
+ /** One credential-free, bounded request. Retain only bounded response evidence. */
18
+ export async function healthRequest({origin, receipt, versionId, request, timeoutMs, offsetMs}) {
19
+ const attempt = {offsetMs,status:null,cfRay:null,contentType:null,body:'',errorClass:null};
20
+ let response;
21
+ try {
22
+ response = await request(`${origin}/health`, {redirect:'manual',signal:AbortSignal.timeout(timeoutMs),headers:{'Cache-Control':'no-cache','Accept':'application/json'}});
23
+ } catch {
24
+ attempt.errorClass = 'health-transport-failed';
25
+ return {attempt,transient:true};
26
+ }
27
+ attempt.status = response.status;
28
+ attempt.cfRay = response.headers.get('cf-ray')?.slice(0,256) ?? null;
29
+ attempt.contentType = response.headers.get('content-type')?.slice(0,256) ?? null;
30
+ if (response.status !== 200 || response.redirected) {
31
+ attempt.body = await bodyPrefix(response);
32
+ attempt.errorClass = 'health-http-failed';
33
+ return {attempt,transient:!response.redirected && (response.status === 404 || response.status >= 500 && response.status <= 599)};
34
+ }
35
+ let bytes = Buffer.alloc(0);
36
+ try {
37
+ assert(response.body,'empty-response');
38
+ const reader = response.body.getReader();
39
+ try {
40
+ while (true) {
41
+ const {done,value} = await reader.read();
42
+ if (done) break;
43
+ bytes = Buffer.concat([bytes, value]);
44
+ attempt.body = bytes.subarray(0,256).toString('utf8');
45
+ assert(bytes.length <= 65536,'response-too-large');
46
+ }
47
+ } finally { await reader.cancel().catch(() => {}); }
48
+ } catch (error) {
49
+ attempt.errorClass = ['response-too-large','empty-response'].includes(error.message) ? error.message : 'health-transport-failed';
50
+ // A redirect or permanent status can never become retryable via body IO.
51
+ return {attempt,transient:attempt.errorClass === 'health-transport-failed' && (response.status === 200 || response.status === 404 || response.status >= 500 && response.status <= 599)};
52
+ }
53
+ let body;
54
+ try { body = JSON.parse(bytes.toString('utf8')); }
55
+ catch { attempt.errorClass = 'malformed-response'; return {attempt,transient:false}; }
56
+ if (body?.ok !== true || body.deployment?.sourceSha !== receipt.source.sha || body.deployment?.fingerprint !== receipt.fingerprint || body.deployment?.versionId !== versionId) {
57
+ attempt.errorClass = 'health-identity-mismatch';
58
+ }
59
+ return {attempt,transient:false};
60
+ }
61
+
62
+ /** Retries are spaced 1 s, doubling to a 10 s cap, never closer than the 2 s poll
63
+ * interval of the standalone readiness window: 2, 2, 4, 8, 10, 10 ... seconds. */
64
+ const MIN_DELAY_MS = 2000, MAX_DELAY_MS = 10000;
65
+
66
+ /** The total bound includes requests, body reads and backoff; no request starts at the deadline.
67
+ * Only a transient outcome is retried. Every attempt is recorded, in order, whatever the end. */
68
+ export async function waitHealth({origin,receipt,versionId,request,timeoutMs=30000,totalMs=90000,clock=()=>performance.now(),sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms)),onAttempt=()=>{},single=false}) {
69
+ assert(Number.isFinite(totalMs) && totalMs > 0 && totalMs <= 90000 && Number.isFinite(timeoutMs) && timeoutMs > 0,'invalid-health-bound');
70
+ const start = clock(); const attempts = []; let delay = 1000;
71
+ while (true) {
72
+ const remaining = totalMs - (clock()-start);
73
+ if (remaining <= 0) break;
74
+ const result = await healthRequest({origin,receipt,versionId,request,timeoutMs:Math.max(1,Math.ceil(Math.min(timeoutMs,remaining))),offsetMs:Math.round(clock()-start)});
75
+ attempts.push(result.attempt); await onAttempt(result.attempt,attempts);
76
+ if (!result.attempt.errorClass) return {ok:true,versionId,attempts};
77
+ if (single || !result.transient) break;
78
+ const rest = totalMs - (clock()-start);
79
+ if (rest <= 0) break;
80
+ await sleep(Math.min(Math.max(delay,MIN_DELAY_MS),rest)); delay = Math.min(delay*2,MAX_DELAY_MS);
81
+ }
82
+ const last = attempts.at(-1);
83
+ const error = new Error(`${last?.errorClass ?? 'health-timeout'}-last-status-${last?.status ?? 'transport'}`);
84
+ error.attempts = attempts; error.status = last?.status ?? null;
85
+ throw error;
86
+ }
@@ -0,0 +1,99 @@
1
+ import { verifyServiceGrants } from './service-grants.mjs';
2
+ import { assert, stable } from './validation.mjs';
3
+ import { boundedJSON } from './provider.mjs';
4
+ import { activeWorker, workerOrigin, durableNamespaces, verifyScheduledHandler } from './worker.mjs';
5
+ import { reconcileResources, reconcileTriggers } from './resources.mjs';
6
+ import { migrate } from './migrations.mjs';
7
+ import { bindingsFor, verifyBindings } from './render.mjs';
8
+ import { waitHealth } from './health-request.mjs';
9
+
10
+ function healthError(message, code, lastResult) {
11
+ const error = new Error(message); error.code = code; error.lastResult = lastResult; return error;
12
+ }
13
+
14
+ /** Cloudflare may publish the route/bindings shortly after an upload succeeds.
15
+ * Only 404, 503 and network failures are retried, inside one bounded window;
16
+ * every other status, including redirects, is terminal and never healthy. */
17
+ export async function waitForDeploymentHealth(url, {
18
+ timeoutMs = 30_000,
19
+ intervalMs = 2_000,
20
+ attemptTimeoutMs = 5_000,
21
+ headers,
22
+ fetchImpl = globalThis.fetch,
23
+ now = Date.now,
24
+ sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
25
+ } = {}) {
26
+ const deadline = now() + timeoutMs;
27
+ let lastResult = 'no response';
28
+ while (now() < deadline) {
29
+ let response;
30
+ try {
31
+ response = await fetchImpl(url, { redirect: 'manual', signal: AbortSignal.timeout(Math.max(1, Math.min(attemptTimeoutMs, deadline - now()))), ...(headers ? { headers } : {}) });
32
+ } catch {
33
+ lastResult = 'network error';
34
+ }
35
+ if (response) {
36
+ if (response.ok) return response;
37
+ lastResult = `HTTP ${response.status}`;
38
+ await response.body?.cancel().catch(() => {});
39
+ if (response.status !== 404 && response.status !== 503) {
40
+ throw healthError(`Deployment health check failed at ${url}: ${lastResult}.`, 'health-http-failed', lastResult);
41
+ }
42
+ }
43
+ const remaining = deadline - now();
44
+ if (remaining > 0) await sleep(Math.min(intervalMs, remaining));
45
+ }
46
+ throw healthError(`Deployment did not become healthy at ${url} within ${timeoutMs}ms; last result: ${lastResult}.`, lastResult === 'network error' ? 'health-transport-failed' : 'health-http-failed', lastResult);
47
+ }
48
+
49
+ /** One unauthenticated GET of a built file; anything but 200 means the browser gets no assets. */
50
+ async function servedAsset(origin, path, request, timeoutMs) {
51
+ let response;
52
+ try { response = await request(`${origin}/${path.split('/').map(encodeURIComponent).join('/')}`,{redirect:'manual',signal:AbortSignal.timeout(timeoutMs),headers:{'Cache-Control':'no-cache'}}); }
53
+ catch { throw new Error('asset-transport-failed'); }
54
+ await response.body?.cancel().catch(() => {});
55
+ assert(response.status === 200 && !response.redirected,'asset-http-failed');
56
+ return {path,status:response.status};
57
+ }
58
+
59
+ /** Authenticated provider readback, then the unauthenticated account-derived /health.
60
+ * Without `readiness`, /health gets the single bounded window of waitForDeploymentHealth.
61
+ * With `readiness` (deploy after an upload) it gets the recorded 90-second wait of health-request.mjs.
62
+ * `asset`, a built file's path, is then requested once and must answer 200. */
63
+ export async function verifyHealth({api,plan,receipt,fetch:request=globalThis.fetch,timeoutMs=30000,intervalMs=2000,now,sleep,readiness,asset}) {
64
+ assert(receipt?.version === 1 && receipt.accountId === api.accountId && receipt.worker === plan.worker,'receipt-target-mismatch');
65
+ await verifyServiceGrants(api, plan);
66
+ const current = await activeWorker(api,plan.worker);
67
+ assert(current && current.versionId === receipt.provider.versionId && current.deploymentId === receipt.provider.deploymentId && current.etag === receipt.provider.etag,'active-deployment-mismatch');
68
+ verifyScheduledHandler(current,plan);
69
+ const origin = await workerOrigin(api,plan.worker,{workersDev:plan.workersDev !== false});
70
+ assert(origin === receipt.origin,'health-origin-mismatch');
71
+ const resources = await reconcileResources(api,plan,{create:false});
72
+ assert(stable(resources) === stable(receipt.resources),'resource-identity-drift');
73
+ verifyBindings(current.settings.bindings,bindingsFor(receipt.config,resources,plan));
74
+ await reconcileTriggers(api,plan,resources,{mutate:false});
75
+ const namespaces = await durableNamespaces(api,plan,{requireAll:true});
76
+ assert(stable(namespaces) === stable(receipt.durableObjects),'durable-object-identity-drift');
77
+ for (const item of plan.d1) await migrate(api,resources.d1.find(x => x.binding === item.binding).id,item.migrations,{apply:false});
78
+ const secrets = await api.request(`/workers/scripts/${plan.worker}/secrets`);
79
+ assert(Array.isArray(secrets) && plan.secrets.every(x => secrets.some(y => y.name === x.binding && y.type === 'secret_text')),'missing-deployed-secret');
80
+ let health;
81
+ // A Worker without a public route (reached only through service bindings) has no /health to request.
82
+ if (plan.workersDev === false) health = {ok:true,versionId:current.versionId,route:'none'};
83
+ else if (readiness) health = await waitHealth({origin,receipt,versionId:current.versionId,request,timeoutMs,...readiness});
84
+ else {
85
+ let response;
86
+ try {
87
+ response = await waitForDeploymentHealth(`${origin}/health`,{timeoutMs,intervalMs,attemptTimeoutMs:timeoutMs,headers:{'Cache-Control':'no-cache','Accept':'application/json'},fetchImpl:request,...(now ? {now} : {}),...(sleep ? {sleep} : {})});
88
+ } catch (error) { throw new Error(error.code ?? 'health-transport-failed'); }
89
+ assert(response.ok && response.status === 200 && !response.redirected,'health-http-failed');
90
+ const body = await boundedJSON(response,65536);
91
+ assert(body?.ok === true && body.deployment?.sourceSha === receipt.source.sha && body.deployment?.fingerprint === receipt.fingerprint && body.deployment?.versionId === current.versionId,'health-identity-mismatch');
92
+ health = {ok:true,versionId:current.versionId};
93
+ }
94
+ if (asset && origin) health.asset = await servedAsset(origin,asset,request,timeoutMs);
95
+ const after = await activeWorker(api,plan.worker);
96
+ assert(after.versionId === current.versionId && after.deploymentId === current.deploymentId && after.etag === current.etag,'deployment-changed-during-health');
97
+ await verifyServiceGrants(api, plan);
98
+ return health;
99
+ }
@@ -0,0 +1,12 @@
1
+ export { cloudflare } from './provider.mjs';
2
+ export { deploy } from './deploy.mjs';
3
+ export { verifyHealth, waitForDeploymentHealth } from './health.mjs';
4
+ export { ownerWorkerName, ownerResourceName, ownerTarget, ownerPlan, localOnlyBinding, LOCAL_ONLY_KEYS, ownerDeploy, ownerHealth, readOwnerState, withScopeHints } from './owner.mjs';
5
+ export { verifyTeardown, deleteWorker } from './teardown.mjs';
6
+ export { writeHealthReceipt } from './receipt.mjs';
7
+ export { workerConsumerScript } from './queue-consumer.mjs';
8
+ export { readArtifact } from './artifact.mjs';
9
+ export { renderConfig } from './render.mjs';
10
+ export { defaultStagePlan, stage } from './default-stage.mjs';
11
+ export { check, mailPlan } from './check.mjs';
12
+ export { createTemplates, lockTemplates } from './templates.mjs';
@@ -0,0 +1,10 @@
1
+ import { lstat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ export const LOCAL_MODELS_FILE = '.rer/local/models.json';
5
+ /** Refuse presence, including symlinks: never read this key to build a plan. */
6
+ export async function refuseLocalModels(root) {
7
+ try { await lstat(join(root, LOCAL_MODELS_FILE)); }
8
+ catch (error) { if (error.code === 'ENOENT') return; throw error; }
9
+ throw new Error('local-only-models-file');
10
+ }
@@ -0,0 +1,48 @@
1
+ import { assert, hash } from './validation.mjs';
2
+ import { assertMigrationSqlSafe } from './sql-safety.mjs';
3
+ import { sqlTokens } from './sql-tokens.mjs';
4
+ const ledger = '_rer_deploy_migrations';
5
+ const literal = value => `'${value.replaceAll("'","''")}'`;
6
+ export function validateMigrations(migrations) {
7
+ let previous = '';
8
+ for (const migration of migrations) {
9
+ assert(/^[0-9]+_[a-zA-Z0-9_-]+\.sql$/.test(migration.name) && migration.name > previous, 'migration-order-or-name'); previous = migration.name;
10
+ assert(typeof migration.sql === 'string' && migration.sql.trim() && hash(migration.sql) === migration.sha256 && migration.sql.length <= 512000, 'invalid-migration-bytes');
11
+ assertMigrationSqlSafe(migration.sql);
12
+ assert(sqlTokens(migration.sql).length > 0, 'invalid-migration-bytes');
13
+ }
14
+ }
15
+ export async function query(api, id, sql) {
16
+ const rows = await api.request(`/d1/database/${id}/query`,{method:'POST',body:{sql}});
17
+ assert(Array.isArray(rows) && rows.length > 0 && rows.every(x => x.success === true && Array.isArray(x.results)), 'd1-query-failed');
18
+ return rows;
19
+ }
20
+ export async function migrate(api, id, migrations, { apply = true } = {}) {
21
+ validateMigrations(migrations);
22
+ const tables = (await query(api,id,"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('_rer_deploy_migrations','d1_migrations')"))[0].results.map(x => x.name);
23
+ let applied = [];
24
+ if (tables.includes(ledger)) applied = (await query(api,id,`SELECT name, sha256, state FROM ${ledger} ORDER BY name`))[0].results;
25
+ // Wrangler history has no hashes. Never assume old SQL bytes from its names alone.
26
+ if (tables.includes('d1_migrations')) {
27
+ const prior = (await query(api,id,'SELECT name FROM d1_migrations ORDER BY id'))[0].results;
28
+ assert(prior.every(x => applied.some(y => x.name === y.name && y.state === 'applied')), 'legacy-migration-history-needs-reviewed-hash-adoption');
29
+ }
30
+ assert(applied.every((row,index) => row.name === migrations[index]?.name && row.sha256 === migrations[index]?.sha256 && row.state === 'applied'), 'changed-or-uncertain-migration-history');
31
+ const pending = migrations.slice(applied.length);
32
+ assert(apply || pending.length === 0,'pending-migrations');
33
+ if (pending.length && !tables.includes(ledger)) await query(api,id,`CREATE TABLE ${ledger} (name TEXT PRIMARY KEY, sha256 TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('applying','applied')))`);
34
+ for (const migration of pending) {
35
+ // Claim before execution. A lost response/partial provider failure is never automatically replayed.
36
+ await query(api,id,`INSERT INTO ${ledger}(name,sha256,state) VALUES (${literal(migration.name)},${literal(migration.sha256)},'applying')`);
37
+ const last = sqlTokens(migration.sql).at(-1);
38
+ // A second terminator after trailing comments creates an empty D1 statement.
39
+ // Keep the newline even when terminated: a final line comment must end.
40
+ const separator = last?.kind === 'symbol' && last.value === ';' ? '' : '; ';
41
+ await query(api,id,`${migration.sql}\n${separator}UPDATE ${ledger} SET state='applied' WHERE name=${literal(migration.name)} AND sha256=${literal(migration.sha256)};`);
42
+ }
43
+ if (pending.length) {
44
+ const final = (await query(api,id,`SELECT name, sha256, state FROM ${ledger} ORDER BY name`))[0].results;
45
+ assert(final.length === migrations.length && final.every((x,i) => x.state === 'applied' && x.name === migrations[i].name && x.sha256 === migrations[i].sha256), 'migration-readback-mismatch');
46
+ }
47
+ return { applied:pending.map(x => x.name), history:migrations.map(({name,sha256}) => ({name,sha256})) };
48
+ }
@@ -0,0 +1,63 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { resolve, join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { parseArgs } from 'node:util';
5
+ import { assert, hexId } from './validation.mjs';
6
+ import { regularBytes } from './artifact.mjs';
7
+ import { packageBuild } from './prepare.mjs';
8
+ import { cloudflare } from './provider.mjs';
9
+ import { ownerTarget, ownerPlan, ownerDeploy, ownerHealth } from './owner.mjs';
10
+ import { refuseLocalModels } from './local-only.mjs';
11
+
12
+ export const cronNotice = 'This Worker has a Cron Trigger. Cloudflare may take up to about two hours to start its first scheduled run on some accounts.';
13
+ const usage = 'usage: owner-cli.mjs deploy|health --declaration <file> --environment <name> [--product <dir>] [--state <file>]';
14
+
15
+ /** The owner's committed source and existing local build. Run the seed's build first. */
16
+ async function ownerArtifact(root, product) {
17
+ const git = (...args) => { try { return execFileSync('git',args,{cwd:root,encoding:'utf8',stdio:['ignore','pipe','pipe']}).trim(); } catch { throw new Error('owner-source-not-a-git-checkout'); } };
18
+ const source = {sha:git('rev-parse','HEAD'),tree:git('rev-parse','HEAD^{tree}')};
19
+ assert(!git('status','--porcelain','--untracked-files=no'),'owner-source-not-committed');
20
+ return packageBuild({root,source,runIdentity:{id:'0',attempt:'1'},productDirectory:product});
21
+ }
22
+
23
+ /** Inputs: CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN, plus the secrets the declaration names on first deploy. */
24
+ export async function main(argv = process.argv.slice(2), env = process.env, {fetch = globalThis.fetch, cwd = process.cwd(), healthOptions, localOnlyKeys = [], notice = line => console.error(line)} = {}) {
25
+ let parsed;
26
+ try { parsed = parseArgs({args:argv,allowPositionals:true,strict:true,options:{declaration:{type:'string'},environment:{type:'string'},product:{type:'string',default:''},state:{type:'string'}}}); }
27
+ catch { throw new Error('usage'); }
28
+ const {positionals:[command,...rest],values} = parsed;
29
+ if (!['deploy','health'].includes(command) || rest.length || !values.declaration || !values.environment) throw new Error('usage');
30
+ await refuseLocalModels(resolve(cwd));
31
+ const missing = ['CLOUDFLARE_ACCOUNT_ID','CLOUDFLARE_API_TOKEN'].filter(key => typeof env[key] !== 'string' || !env[key].trim());
32
+ if (missing.length) { const error = new Error('missing-input'); error.inputs = missing; throw error; }
33
+ assert(hexId(env.CLOUDFLARE_ACCOUNT_ID) && !/[\r\n]/.test(env.CLOUDFLARE_API_TOKEN),'invalid-cloudflare-input');
34
+ const root = resolve(cwd);
35
+ let declaration;
36
+ try { declaration = JSON.parse(await regularBytes(resolve(root,values.declaration),256 * 1024)); }
37
+ catch (error) { throw new Error(error.code === 'ENOENT' ? 'missing-owner-declaration' : 'invalid-owner-declaration'); }
38
+ assert(typeof declaration?.project === 'string','missing-owner-project');
39
+ const api = cloudflare({accountId:env.CLOUDFLARE_ACCOUNT_ID,token:env.CLOUDFLARE_API_TOKEN,fetch});
40
+ const target = await ownerTarget(api,{project:declaration.project,environment:values.environment});
41
+ const statePath = values.state ? resolve(root,values.state) : join(root,'.rer','deploy',`${target.environment}.json`);
42
+ const artifact = await ownerArtifact(root,values.product);
43
+ const plan = ownerPlan(target,declaration,artifact,{localOnlyKeys});
44
+ if (command === 'health') {
45
+ await ownerHealth({api,target,plan,statePath,fetch,healthOptions:{...healthOptions,readiness:{single:true,totalMs:30000}}});
46
+ return {ok:true,worker:target.worker,origin:target.origin};
47
+ }
48
+ // Dispatch is the provider's: on some accounts a new Cron starts only at the account scheduler's next refresh.
49
+ if (plan.crons.length) notice(cronNotice);
50
+ const {receipt,created} = await ownerDeploy({api,target,plan,artifact,statePath,env,fetch,healthOptions});
51
+ return {ok:true,changed:receipt.changed,worker:target.worker,origin:receipt.origin,versionId:receipt.provider.versionId,created,state:statePath};
52
+ }
53
+
54
+ /** Prints refusal codes and resource names only; never inputs, tokens or provider bodies. */
55
+ export function report(error) {
56
+ if (error.message === 'missing-input') return {ok:false,code:'missing-input',inputs:error.inputs};
57
+ if (error.message === 'usage') return {ok:false,code:'usage',usage};
58
+ return {ok:false,code:/^[a-zA-Z0-9_-]{1,120}$/.test(error.message) ? error.message : 'owner-deploy-failed',...(error.resource ? {resource:error.resource} : {})};
59
+ }
60
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
61
+ try { console.log(JSON.stringify(await main())); }
62
+ catch (error) { console.error(JSON.stringify(report(error))); process.exitCode = error.message === 'local-only-binding' ? 4 : 1; }
63
+ }
@@ -0,0 +1,297 @@
1
+ import { ingressBindings } from './container-config.mjs';
2
+ import { mkdir, open, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { assert, hash, identifier, resourceName, validatePlan } from './validation.mjs';
5
+ import { regularBytes } from './artifact.mjs';
6
+ import { resourceKinds, findResource, checkCronOwnership } from './resources.mjs';
7
+ import { secretBindings, activeWorker, deployedFingerprint } from './worker.mjs';
8
+ import { validateMigrations } from './migrations.mjs';
9
+ import { deploy } from './deploy.mjs';
10
+ import { verifyHealth } from './health.mjs';
11
+
12
+ /* Owner-account mode: the owner's own Cloudflare account, token and account id.
13
+ * Names are derived from the project id and the owner's environment. A local
14
+ * state file records what this kit created; a same-named resource it did not
15
+ * create is refused, never adopted. */
16
+
17
+ const segment = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
18
+ const STATE_KIND = 'rer-deploy-owner-state';
19
+ const KINDS = ['d1','r2','kv','queues'];
20
+ const DECLARATION_KEYS = ['version','project','compatibilityDate','compatibilityFlags','d1','r2','kv','queues','durableObjects','secrets','crons','vars','originVar','services','workersDev','platforms','container'];
21
+ const ENTRY_KEYS = {d1:['binding'],r2:['binding','locationHint'],kv:['binding'],queues:['binding','consumer','deadLetterQueue'],durableObjects:['binding','className','storage'],secrets:['binding','environment'],services:['binding','project','entrypoint','allowedHosts','grant']};
22
+
23
+ function refuse(code, kind, name) { const error = new Error(code); error.resource = `${kind}:${name}`; return error; }
24
+
25
+ export function ownerWorkerName(project, environment) {
26
+ assert(typeof project === 'string' && /^(?:[a-z0-9][a-z0-9-]*\/)?[a-z0-9-]+$/.test(project) && segment.test(project.split('/').at(-1)), 'invalid-owner-project');
27
+ assert(typeof environment === 'string' && environment.length <= 32 && segment.test(environment), 'invalid-owner-environment');
28
+ const worker = `${project.split('/').at(-1)}-${environment}`;
29
+ assert(resourceName(worker), 'owner-name-too-long');
30
+ return worker;
31
+ }
32
+ /** `JOBS_DEAD` on Worker `penny-production` is named `penny-production-jobs-dead`. */
33
+ export function ownerResourceName(worker, binding) {
34
+ assert(identifier(binding), 'duplicate-or-invalid-binding');
35
+ const slug = binding.toLowerCase().replaceAll('_','-');
36
+ assert(segment.test(slug), 'invalid-owner-binding');
37
+ assert(resourceName(`${worker}-${slug}`), 'owner-name-too-long');
38
+ return `${worker}-${slug}`;
39
+ }
40
+ /* A 401 or 403 on a known path names the token permission that failed, never the request. */
41
+ const SCOPES = [[/^\/d1\//,'d1'],[/^\/r2\//,'r2'],[/^\/storage\/kv\//,'kv'],[/^\/queues(?:[/?]|$)/,'queues'],[/^\/workers\//,'workers']];
42
+ export function withScopeHints(api) {
43
+ if (api.scopeHints) return api;
44
+ const hint = (path, error) => {
45
+ const scope = (error.status === 401 || error.status === 403) && SCOPES.find(([pattern]) => pattern.test(path));
46
+ if (scope) error.message = `token-scope-missing-${scope[1]}`;
47
+ throw error;
48
+ };
49
+ const guard = method => async (path, ...rest) => { try { return await api[method](path, ...rest); } catch (error) { return hint(path, error); } };
50
+ return {...api, scopeHints:true, request:guard('request'), envelope:guard('envelope'), list:guard('list')};
51
+ }
52
+ /** Reads the account's workers.dev subdomain; no other provider request. */
53
+ export async function ownerTarget(provider, {project, environment}) {
54
+ const api = withScopeHints(provider);
55
+ const worker = ownerWorkerName(project, environment);
56
+ let result;
57
+ try { result = await api.request('/workers/subdomain'); }
58
+ catch (error) { if (error.status === 404) throw new Error('workers-subdomain-missing'); throw error; }
59
+ assert(typeof result?.subdomain === 'string' && result.subdomain, 'workers-subdomain-missing');
60
+ assert(/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(result.subdomain),'invalid-account-subdomain');
61
+ return {accountId:api.accountId,project,environment,worker,origin:`https://${worker}.${result.subdomain}.workers.dev`};
62
+ }
63
+
64
+ function entries(declaration, key) {
65
+ const list = declaration[key] ?? [];
66
+ assert(Array.isArray(list) && list.every(item => item && typeof item === 'object' && !Array.isArray(item) && Object.keys(item).every(k => ENTRY_KEYS[key].includes(k))), 'invalid-owner-declaration');
67
+ return list;
68
+ }
69
+ /* RER_DEV_KEY opens local dev identity; it and every key a seed adapter declares
70
+ * local-only never become a deployed binding, secret or secret source. */
71
+ export const LOCAL_ONLY_KEYS = Object.freeze(['RER_DEV_KEY', 'RER_MODELS_KEY', 'RER_MODELS_SERVICE', '.rer/local/models.json']);
72
+ export function localOnlyBinding(declaration, localOnlyKeys = []) {
73
+ const refused = new Set([...LOCAL_ONLY_KEYS, ...localOnlyKeys]);
74
+ const names = [...Object.keys(declaration?.vars ?? {}), declaration?.originVar, ...(Array.isArray(declaration?.secrets) ? declaration.secrets.flatMap(item => [item?.binding, item?.environment]) : [])];
75
+ return names.find(name => refused.has(name)) ?? null;
76
+ }
77
+ /** A seed declares bindings, never names. Migrations come from the packaged artifact. */
78
+ export function ownerPlan(target, declaration, artifact, { localOnlyKeys = [] } = {}) {
79
+ assert(declaration?.version === 1 && typeof declaration === 'object' && Object.keys(declaration).every(key => DECLARATION_KEYS.includes(key)), 'invalid-owner-declaration');
80
+ if (declaration.platforms !== undefined) assert(Array.isArray(declaration.platforms) && declaration.platforms.length > 0 && new Set(declaration.platforms).size === declaration.platforms.length && declaration.platforms.every(item => ['workers','container'].includes(item)), 'invalid-platforms');
81
+ if (declaration.container !== undefined) {
82
+ const config=declaration.container;
83
+ assert(config&&typeof config==='object'&&!Array.isArray(config)&&Object.keys(config).every(key=>['engine','cacheContract','runner','hostVariables','ingress'].includes(key))&&['node','workerd'].includes(config.engine),'invalid-container');
84
+ if(config.ingress!==undefined)ingressBindings(config.ingress);
85
+ assert(config.hostVariables===undefined||Array.isArray(config.hostVariables)&&config.hostVariables.every(name=>/^[A-Z][A-Z0-9_]*$/.test(name)),'invalid-container-host-variables');
86
+ assert(config.cacheContract===undefined||config.cacheContract==='lamp-retained-v1','invalid-container-cache');
87
+ if(config.runner!==undefined){const runner=config.runner,relative=path=>typeof path==='string'&&!path.startsWith('/')&&path.split('/').every(part=>part&&!['.','..'].includes(part));assert(runner&&Object.keys(runner).every(key=>['entry','files'].includes(key))&&relative(runner.entry)&&Array.isArray(runner.files)&&runner.files.length>0&&runner.files.every(relative),'invalid-container-runner');}
88
+ }
89
+ const local = localOnlyBinding(declaration, localOnlyKeys);
90
+ if (local) throw refuse('local-only-binding', 'binding', local);
91
+ assert(declaration.project === undefined || declaration.project === target.project, 'owner-project-mismatch');
92
+ const name = binding => ownerResourceName(target.worker, binding);
93
+ const d1 = entries(declaration,'d1');
94
+ assert(artifact.migrations.every(migration => d1.some(item => item.binding === migration.binding)), 'undeclared-migration-binding');
95
+ const queues = entries(declaration,'queues');
96
+ for (const item of queues) assert(item.deadLetterQueue === undefined || queues.some(other => other.binding === item.deadLetterQueue), 'invalid-dead-letter-queue');
97
+ const vars = {...(declaration.vars ?? {})};
98
+ if (declaration.originVar !== undefined) {
99
+ assert(identifier(declaration.originVar) && !Object.hasOwn(vars,declaration.originVar), 'invalid-owner-origin-variable');
100
+ vars[declaration.originVar] = target.origin;
101
+ }
102
+ const kv = entries(declaration,'kv').map(({binding}) => ({binding,name:name(binding)}));
103
+ // A service binding names another project; its Worker name derives from the same environment.
104
+ const services = entries(declaration,'services').map(({binding,project,entrypoint,allowedHosts,grant}) => ({binding,service:ownerWorkerName(project,target.environment),
105
+ ...(grant === undefined ? {} : {grant}),...(entrypoint === undefined ? {} : {entrypoint}),...(allowedHosts === undefined ? {} : {props:{allowedHosts}})}));
106
+ assert(declaration.workersDev === undefined || declaration.workersDev === false, 'invalid-owner-declaration');
107
+ return validatePlan({version:1,worker:target.worker,compatibilityDate:declaration.compatibilityDate,compatibilityFlags:declaration.compatibilityFlags ?? [],
108
+ d1:d1.map(({binding}) => ({binding,name:name(binding),migrations:artifact.migrations.filter(x => x.binding === binding).map(({binding:_,...rest}) => rest)})),
109
+ r2:entries(declaration,'r2').map(item => ({...item,name:name(item.binding)})),
110
+ ...(kv.length ? {kv} : {}),
111
+ queues:queues.map(item => ({...item,name:name(item.binding),...(item.deadLetterQueue === undefined ? {} : {deadLetterQueue:name(item.deadLetterQueue)})})),
112
+ durableObjects:entries(declaration,'durableObjects').map(item => ({...item})),
113
+ secrets:entries(declaration,'secrets').map(item => ({...item})),
114
+ crons:[...(declaration.crons ?? [])],vars,
115
+ // Only declarations using these add the keys, so earlier plans and fingerprints are unchanged.
116
+ ...(services.length ? {services} : {}),...(declaration.workersDev === false ? {workersDev:false} : {})});
117
+ }
118
+ function assertOwnerNames(target, plan) {
119
+ assert(plan.worker === target.worker, 'owner-plan-name-mismatch');
120
+ for (const kind of KINDS) for (const item of resourceKinds[kind].items(plan)) assert(item.name === ownerResourceName(target.worker,item.binding), 'owner-plan-name-mismatch');
121
+ }
122
+
123
+ function emptyState(target) {
124
+ return {version:1,kind:STATE_KIND,accountId:target.accountId,project:target.project,environment:target.environment,worker:target.worker,
125
+ owned:{worker:null,d1:{},r2:{},kv:{},queues:{}},receipt:null};
126
+ }
127
+ export async function readOwnerState(path, target) {
128
+ let bytes;
129
+ try { bytes = await regularBytes(path, 4 * 1024 * 1024); }
130
+ catch (error) { if (error.code === 'ENOENT') return emptyState(target); throw error; }
131
+ let state; try { state = JSON.parse(bytes); } catch { throw new Error('invalid-owner-state'); }
132
+ assert(state?.version === 1 && state.kind === STATE_KIND && state.owned && typeof state.owned === 'object'
133
+ && (state.owned.worker === null || ['pending','created'].includes(state.owned.worker?.status))
134
+ && KINDS.every(kind => state.owned[kind] && typeof state.owned[kind] === 'object' && Object.values(state.owned[kind]).every(entry => ['pending','created'].includes(entry?.status))), 'invalid-owner-state');
135
+ assert(state.pendingUpload === undefined || typeof state.pendingUpload?.fingerprint === 'string' && typeof state.pendingUpload?.build === 'string', 'invalid-owner-state');
136
+ assert(state.accountId === target.accountId && state.project === target.project && state.environment === target.environment && state.worker === target.worker, 'owner-state-target-mismatch');
137
+ return state;
138
+ }
139
+ async function writeOwnerState(path, state) {
140
+ await mkdir(dirname(path),{recursive:true,mode:0o700});
141
+ const temporary = `${path}.${process.pid}.tmp`;
142
+ await writeFile(temporary,JSON.stringify(state,null,2)+'\n',{flag:'wx',mode:0o600});
143
+ await rename(temporary,path);
144
+ }
145
+ const resourceId = (kind, row) => ({d1:row.uuid,r2:row.creation_date,kv:row.id,queues:row.queue_id})[kind];
146
+
147
+ function checkResourceIdentity(kind, name, row, entry) {
148
+ if (row) {
149
+ if (!entry) throw refuse('resource-name-collision',kind,name);
150
+ if (entry.status !== 'created') throw refuse('uncertain-resource-create',kind,name);
151
+ // A state file from before bucket identities were recorded has no id to compare.
152
+ if ((kind !== 'r2' || entry.id !== undefined) && entry.id !== resourceId(kind,row)) throw refuse('owned-resource-replaced',kind,name);
153
+ } else if (entry?.status === 'created') throw refuse('owned-resource-missing',kind,name);
154
+ }
155
+
156
+ /** Read-only: every declared name is either created by this kit or absent. */
157
+ async function ownedResourcesToCreate(api, plan, state) {
158
+ const create = [];
159
+ for (const kind of KINDS) {
160
+ const items = resourceKinds[kind].items(plan);
161
+ if (!items.length) continue;
162
+ const rows = await resourceKinds[kind].list(api);
163
+ for (const item of items) {
164
+ const row = findResource(rows,kind,item.name);
165
+ const entry = state.owned[kind][item.name];
166
+ checkResourceIdentity(kind,item.name,row,entry);
167
+ if (!row) create.push({kind,item});
168
+ }
169
+ }
170
+ return create;
171
+ }
172
+ /** Re-read every saved resource, including resources omitted from a later plan. */
173
+ async function checkSavedResourceIdentities(api, state) {
174
+ for (const kind of KINDS) {
175
+ const entries = Object.entries(state.owned[kind]);
176
+ if (!entries.length) continue;
177
+ const rows = await resourceKinds[kind].list(api);
178
+ for (const [name,entry] of entries) checkResourceIdentity(kind,name,findResource(rows,kind,name),entry);
179
+ }
180
+ }
181
+ /** Read-only: a bound service Worker must already exist in the account; deploy it first. */
182
+ async function checkServiceWorkers(api, plan) {
183
+ for (const {service} of plan.services ?? []) if (await workerSettings(api, service) === null) throw refuse('service-worker-missing','service',service);
184
+ }
185
+ async function workerSettings(api, worker) {
186
+ try { return await api.request(`/workers/scripts/${worker}/settings`); }
187
+ catch (error) { if (error.status === 404) return null; throw error; }
188
+ }
189
+ /** The fingerprint an upload is about to write, read from its own metadata part. */
190
+ async function uploadFingerprint(body) {
191
+ const metadata = JSON.parse(await body.get('metadata').text());
192
+ const fingerprint = deployedFingerprint(metadata);
193
+ assert(typeof fingerprint === 'string' && fingerprint, 'invalid-upload-metadata');
194
+ return fingerprint;
195
+ }
196
+ const sameDeployment = (a, b) => a?.versionId === b?.versionId && a?.deploymentId === b?.deploymentId && a?.etag === b?.etag;
197
+
198
+ /** A completed receipt or the exact identity of an interrupted successful upload proves ownership.
199
+ * Only the first upload, with no receipt, may recover a lost response by its pending build. */
200
+ async function checkWorkerIdentity(api, plan, artifact, state) {
201
+ const live = await activeWorker(api, plan.worker);
202
+ const pending = state.pendingUpload;
203
+ const expected = pending?.provider ?? state.receipt?.provider;
204
+ if (expected) {
205
+ if (!sameDeployment(live, expected)) throw refuse('owned-resource-replaced','worker',plan.worker);
206
+ } else {
207
+ const mine = state.owned.worker?.status === 'pending' && pending?.build === hash({plan,artifact}) && pending.fingerprint === deployedFingerprint(live?.settings);
208
+ if (!mine) throw refuse('uncertain-resource-create','worker',plan.worker);
209
+ }
210
+ return live;
211
+ }
212
+ async function checkOwnedWorker(api, plan, artifact, state) {
213
+ const settings = await workerSettings(api, plan.worker);
214
+ if (settings === null) {
215
+ if (state.owned.worker?.status === 'created') throw refuse('owned-resource-missing','worker',plan.worker);
216
+ return null;
217
+ }
218
+ if (!state.owned.worker) throw refuse('resource-name-collision','worker',plan.worker);
219
+ return checkWorkerIdentity(api, plan, artifact, state);
220
+ }
221
+
222
+ /** One deploy at a time per state file: an exclusive lock file for the whole run. */
223
+ async function withStateLock(path, run) {
224
+ await mkdir(dirname(path),{recursive:true,mode:0o700});
225
+ const lock = `${path}.lock`;
226
+ let handle;
227
+ try { handle = await open(lock,'wx',0o600); }
228
+ catch (error) { throw error.code === 'EEXIST' ? new Error('owner-state-locked') : error; }
229
+ try {
230
+ await handle.writeFile(`${process.pid}\n`);
231
+ await handle.close();
232
+ return await run();
233
+ } finally { try { await handle.close(); } finally { await rm(lock,{force:true}); } }
234
+ }
235
+
236
+ /** Find or create every declared resource under the owner's account, migrate, upload and check health.
237
+ * Ownership preflight precedes provider writes. A re-run of the same build writes nothing. */
238
+ export async function ownerDeploy({api: provider, target, plan, artifact, statePath, env = {}, fetch = globalThis.fetch, healthOptions = {}, readiness = {}}) {
239
+ assert(typeof statePath === 'string' && statePath.length, 'missing-owner-state-path');
240
+ const api = withScopeHints(provider);
241
+ assert(target?.accountId === api.accountId, 'owner-account-mismatch');
242
+ validatePlan(plan); assertOwnerNames(target, plan);
243
+ for (const item of plan.d1) validateMigrations(item.migrations);
244
+ return withStateLock(statePath, () => deployLocked({api, target, plan, artifact, statePath, env, fetch, healthOptions, readiness}));
245
+ }
246
+ async function deployLocked({api, target, plan, artifact, statePath, env, fetch, healthOptions, readiness}) {
247
+ const state = await readOwnerState(statePath, target);
248
+ const save = () => writeOwnerState(statePath, state);
249
+ await checkServiceWorkers(api, plan);
250
+ const priorWorker = await checkOwnedWorker(api, plan, artifact, state);
251
+ const exists = priorWorker !== null;
252
+ const create = await ownedResourcesToCreate(api, plan, state);
253
+ if (exists) await checkCronOwnership(api, plan);
254
+ await secretBindings(api, plan.worker, plan, env, exists);
255
+ // Record intent before each create. A failed or lost response leaves it pending:
256
+ // absent on resume means create again; present means an uncertain outcome to inspect.
257
+ for (const {kind,item} of create) {
258
+ state.owned[kind][item.name] = {status:'pending'}; await save();
259
+ const row = await resourceKinds[kind].create(api,item);
260
+ assert(findResource([row],kind,item.name) === row, 'invalid-created-resource');
261
+ state.owned[kind][item.name] = {status:'created',...(resourceId(kind,row) === undefined ? {} : {id:resourceId(kind,row)})}; await save();
262
+ }
263
+ if (!exists && !state.owned.worker) { state.owned.worker = {status:'pending'}; await save(); }
264
+ const upload = `/workers/scripts/${plan.worker}?bindings_inherit=strict`;
265
+ const observed = {...api, async request(path, options) {
266
+ const sending = path === upload && options?.method === 'PUT';
267
+ if (sending) {
268
+ await checkSavedResourceIdentities(api, state);
269
+ const current = await checkOwnedWorker(api, plan, artifact, state);
270
+ if (!sameDeployment(priorWorker,current)) throw refuse('owned-resource-replaced','worker',plan.worker);
271
+ state.pendingUpload = {fingerprint:await uploadFingerprint(options.body),build:hash({plan,artifact})}; await save();
272
+ }
273
+ const result = await api.request(path, options);
274
+ if (sending) {
275
+ const active = await activeWorker(api, plan.worker);
276
+ assert(active && deployedFingerprint(active.settings) === state.pendingUpload.fingerprint, 'uploaded-worker-drift');
277
+ state.pendingUpload.provider = {versionId:active.versionId,deploymentId:active.deploymentId,etag:active.etag};
278
+ state.owned.worker = {status:'created'}; await save();
279
+ }
280
+ return result;
281
+ }};
282
+ const receipt = await deploy({api:observed, plan, artifact, artifactSha256:hash(artifact), previousReceipt:state.receipt ?? undefined,
283
+ createResources:false, uploadWithoutReceipt:true, env, fetch, healthOptions, readiness,
284
+ onReceipt: async candidate => { if (candidate.changed || !state.receipt) { state.receipt = candidate; delete state.pendingUpload; await save(); } },
285
+ // Keep each first-health attempt beside the identity it belongs to.
286
+ onHealth: async candidate => { if (candidate.changed) { state.receipt = candidate; await save(); } }});
287
+ return {receipt, created:create.map(({kind,item}) => `${kind}:${item.name}`)};
288
+ }
289
+
290
+ /** Independent read-only health of the last owner deployment recorded in the state file. */
291
+ export async function ownerHealth({api: provider, target, plan, statePath, fetch = globalThis.fetch, healthOptions = {}}) {
292
+ const api = withScopeHints(provider);
293
+ validatePlan(plan); assertOwnerNames(target, plan);
294
+ const state = await readOwnerState(statePath, target);
295
+ assert(state.receipt, 'missing-owner-receipt');
296
+ return verifyHealth({...healthOptions, api, plan, receipt:state.receipt, fetch});
297
+ }