@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,275 @@
1
+ """Private dirfd custody, including verification of the live pathname chain."""
2
+ import os, stat, json, pwd, uuid, fcntl, re
3
+
4
+
5
+ def refuse(message):
6
+ raise RuntimeError(message)
7
+
8
+
9
+ def metadata(info, directory=False, ancestor=False):
10
+ kind = stat.S_ISDIR if directory else stat.S_ISREG
11
+ owners = {0, os.getuid()} if ancestor else {os.getuid()}
12
+ mask = 0o022 if ancestor else 0o077
13
+ if not kind(info.st_mode) or info.st_uid not in owners or info.st_mode & mask or (not directory and info.st_nlink != 1):
14
+ refuse('Unsafe host path ownership, permissions or type')
15
+ return info
16
+
17
+
18
+ def checked(fd, directory=False, ancestor=False):
19
+ return metadata(os.fstat(fd), directory, ancestor)
20
+
21
+
22
+ def same_inode(left, right):
23
+ if right is None or (left.st_dev, left.st_ino) != (right.st_dev, right.st_ino):
24
+ refuse('Host deployment path changed')
25
+
26
+
27
+ def directory(parent, name, create=False, ancestor=False):
28
+ if create:
29
+ try:
30
+ os.mkdir(name, 0o700, dir_fd=parent)
31
+ except FileExistsError:
32
+ pass
33
+ fd = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=parent)
34
+ try:
35
+ checked(fd, True, ancestor)
36
+ return fd
37
+ except BaseException:
38
+ os.close(fd)
39
+ raise
40
+
41
+
42
+ def file_info(parent, name):
43
+ try:
44
+ metadata(os.stat(name, dir_fd=parent, follow_symlinks=False))
45
+ fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent)
46
+ except FileNotFoundError:
47
+ return None
48
+ try:
49
+ return checked(fd)
50
+ finally:
51
+ os.close(fd)
52
+
53
+
54
+ def read_json(parent, name):
55
+ metadata(os.stat(name, dir_fd=parent, follow_symlinks=False))
56
+ fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent)
57
+ try:
58
+ checked(fd)
59
+ with os.fdopen(os.dup(fd)) as stream:
60
+ return json.load(stream)
61
+ finally:
62
+ os.close(fd)
63
+
64
+
65
+ def exclusive(parent, name, content):
66
+ fd = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=parent)
67
+ try:
68
+ checked(fd)
69
+ with os.fdopen(os.dup(fd), 'wb') as stream:
70
+ stream.write(content.encode())
71
+ stream.flush()
72
+ os.fsync(stream.fileno())
73
+ return checked(fd)
74
+ finally:
75
+ os.close(fd)
76
+
77
+
78
+ def retain_file(parent, name):
79
+ # Identity plus exact bytes: a same-UID file transplanted from another
80
+ # installation passes every ownership and permission check.
81
+ try:
82
+ metadata(os.stat(name, dir_fd=parent, follow_symlinks=False))
83
+ fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent)
84
+ except FileNotFoundError:
85
+ return None
86
+ try:
87
+ info = checked(fd)
88
+ with os.fdopen(os.dup(fd), 'rb') as stream:
89
+ return info, stream.read(65537)
90
+ finally:
91
+ os.close(fd)
92
+
93
+
94
+ def same_file(parent, name, retained):
95
+ current = retain_file(parent, name)
96
+ if retained is None or current is None:
97
+ if retained is not current:
98
+ refuse('Host deployment path changed')
99
+ return
100
+ same_inode(retained[0], current[0])
101
+ if current[1] != retained[1]:
102
+ refuse('Host deployment file changed')
103
+
104
+
105
+ def open_install(plan):
106
+ # Do not resolve symlinks before checking them. System ancestors may belong
107
+ # to root; the account home and private deployment tree must belong to uid.
108
+ home = pwd.getpwuid(os.getuid()).pw_dir
109
+ if not home.startswith('/') or any(part in ('.', '..') for part in home.split('/')):
110
+ refuse('Unsafe account home')
111
+ fd = os.open('/', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
112
+ checked(fd, True, True)
113
+ chain = [('/', fd, True)]
114
+ for part in filter(None, home.split('/')):
115
+ nxt = directory(fd, part, ancestor=True)
116
+ chain.append((part, nxt, True))
117
+ fd = nxt
118
+ info = os.fstat(fd)
119
+ if info.st_uid != os.getuid():
120
+ refuse('Account home is not owned by login user')
121
+ base = directory(fd, '.rer-host', create=True)
122
+ chain.append(('.rer-host', base, False))
123
+ # One host-account lock also serializes creation and binding admission for
124
+ # cooperating installs. The pathname chain is checked independently of it.
125
+ file_info(base, 'deploy.lock')
126
+ lock = os.open('deploy.lock', os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600, dir_fd=base)
127
+ checked(lock)
128
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
129
+ created = False
130
+ try:
131
+ os.mkdir(plan['name'], 0o700, dir_fd=base)
132
+ created = True
133
+ except FileExistsError:
134
+ pass
135
+ install = directory(base, plan['name'])
136
+ chain.append((plan['name'], install, False))
137
+ if created:
138
+ exclusive(install, 'owner.json', json.dumps(plan['labels']))
139
+ if read_json(install, 'owner.json') != plan['labels']:
140
+ refuse('Installation ownership mismatch')
141
+ validate_install(chain, lock, plan)
142
+ return install, lock, home + '/.rer-host/' + plan['name'], chain
143
+
144
+
145
+ def validate_install(chain, lock, plan):
146
+ # A retained dirfd remains usable after rename. Check each original edge
147
+ # from / as well, so it cannot silently become a detached installation.
148
+ parent = None
149
+ for name, fd, ancestor in chain:
150
+ info = checked(fd, True, ancestor)
151
+ if parent is not None:
152
+ linked = metadata(os.stat(name, dir_fd=parent, follow_symlinks=False), True, ancestor)
153
+ same_inode(info, linked)
154
+ parent = fd
155
+ if os.fstat(chain[-3][1]).st_uid != os.getuid():
156
+ refuse('Account home is not owned by login user')
157
+ same_inode(checked(lock), file_info(chain[-2][1], 'deploy.lock'))
158
+ install = chain[-1][1]
159
+ if read_json(install, 'owner.json') != plan['labels']:
160
+ refuse('Installation ownership mismatch')
161
+ validate_tree(install)
162
+
163
+
164
+ def validate_tree(install):
165
+ checked(install, True)
166
+ for entry in os.listdir(install):
167
+ if entry in ('owner.json', 'current.json'):
168
+ file_info(install, entry)
169
+ elif re.fullmatch(r'current-[0-9a-f]{32}', entry):
170
+ file_info(install, entry)
171
+ elif re.fullmatch(r'(generation|stage)-[0-9a-f]{32}', entry):
172
+ child = directory(install, entry)
173
+ try:
174
+ for name in os.listdir(child):
175
+ if name not in ('settings.env', 'compose.yaml', 'Caddyfile'):
176
+ refuse('Unexpected deployment file')
177
+ file_info(child, name)
178
+ finally:
179
+ os.close(child)
180
+ else:
181
+ refuse('Unexpected installation path')
182
+
183
+
184
+ def stage_files(install, files):
185
+ token = uuid.uuid4().hex
186
+ stage = 'stage-' + token
187
+ os.mkdir(stage, 0o700, dir_fd=install)
188
+ fd = directory(install, stage)
189
+ try:
190
+ records = {}
191
+ for name, contents in files.items():
192
+ records[name] = (contents, exclusive(fd, name, contents))
193
+ os.fsync(fd)
194
+ return stage, 'generation-' + token, fd, records
195
+ except BaseException:
196
+ os.close(fd)
197
+ raise
198
+
199
+
200
+ def validate_generation(install, name, fd, records):
201
+ validate_generation_entries(install, name, fd, {filename: created for filename, (_, created) in records.items()})
202
+ for filename, (contents, created) in records.items():
203
+ file = os.open(filename, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=fd)
204
+ try:
205
+ same_inode(created, checked(file))
206
+ with os.fdopen(os.dup(file), 'rb') as stream:
207
+ if stream.read(len(contents.encode()) + 1) != contents.encode():
208
+ refuse('Host deployment file changed')
209
+ finally:
210
+ os.close(file)
211
+
212
+
213
+ def validate_generation_entries(install, name, fd, entries):
214
+ same_inode(checked(fd, True), metadata(os.stat(name, dir_fd=install, follow_symlinks=False), True))
215
+ if set(os.listdir(fd)) != set(entries):
216
+ refuse('Unexpected deployment file')
217
+ for filename, created in entries.items():
218
+ same_inode(created, file_info(fd, filename))
219
+
220
+
221
+ def retain_generation(install, name):
222
+ fd = directory(install, name)
223
+ try:
224
+ entries = {}
225
+ for filename in os.listdir(fd):
226
+ if filename not in ('settings.env', 'compose.yaml', 'Caddyfile'):
227
+ refuse('Unexpected deployment file')
228
+ info = file_info(fd, filename)
229
+ if info is None:
230
+ refuse('Host deployment path changed')
231
+ entries[filename] = info
232
+ validate_generation_entries(install, name, fd, entries)
233
+ return fd, entries
234
+ except BaseException:
235
+ os.close(fd)
236
+ raise
237
+
238
+
239
+ def publish_generation(install, stage, generation):
240
+ validate_tree(install)
241
+ os.rename(stage, generation, src_dir_fd=install, dst_dir_fd=install)
242
+
243
+
244
+ def commit_manifest(install, generation, current):
245
+ # A single atomic commit points to a complete generation; interrupted
246
+ # transfers cannot make an existing installation run partial configuration.
247
+ # Only the manifest retained before asynchronous work may be replaced.
248
+ content = json.dumps({'generation': generation})
249
+ pending = 'current-' + uuid.uuid4().hex
250
+ committed = (exclusive(install, pending, content), content.encode())
251
+ try:
252
+ same_file(install, pending, committed)
253
+ same_file(install, 'current.json', current)
254
+ os.replace(pending, 'current.json', src_dir_fd=install, dst_dir_fd=install)
255
+ os.fsync(install)
256
+ same_file(install, 'current.json', committed)
257
+ return committed
258
+ finally:
259
+ remaining = file_info(install, pending)
260
+ if remaining is not None:
261
+ same_inode(committed[0], remaining)
262
+ os.unlink(pending, dir_fd=install)
263
+
264
+
265
+ def remove_generation(install, name, fd, entries):
266
+ # A private path can still have been replaced by the same login UID.
267
+ # Validate the whole retained generation before removing any member, then
268
+ # require the captured identity again immediately before each deletion.
269
+ validate_generation_entries(install, name, fd, entries)
270
+ for entry, created in entries.items():
271
+ same_inode(checked(fd, True), metadata(os.stat(name, dir_fd=install, follow_symlinks=False), True))
272
+ same_inode(created, file_info(fd, entry))
273
+ os.unlink(entry, dir_fd=fd)
274
+ same_inode(checked(fd, True), metadata(os.stat(name, dir_fd=install, follow_symlinks=False), True))
275
+ os.rmdir(name, dir_fd=install)
@@ -0,0 +1,157 @@
1
+ """Operate only on fresh Docker inventory, never on stored executable config."""
2
+ import subprocess, socket, io, tarfile
3
+
4
+
5
+ def docker(*args, input=None):
6
+ result = subprocess.run(['docker', *args], input=input, capture_output=True, text=not isinstance(input, bytes), timeout=120)
7
+ if result.returncode:
8
+ # Docker may echo environment/configuration. Never propagate its output.
9
+ refuse('Docker host operation failed: ' + args[0])
10
+ return (result.stdout.decode() if isinstance(result.stdout, bytes) else result.stdout).strip()
11
+
12
+
13
+ def inspect(kind, names):
14
+ return json.loads(docker(kind, 'inspect', *names)) if names else []
15
+
16
+
17
+ def labels_for(plan, role):
18
+ return {**plan['labels'], 'io.runeditrun.role': role}
19
+
20
+
21
+ def check_labels(plan, role, actual):
22
+ if any((actual or {}).get(key) != value for key, value in labels_for(plan, role).items()):
23
+ refuse('Docker resource ownership mismatch: ' + role)
24
+
25
+
26
+ def inventory(plan):
27
+ resources = {'container': {}, 'volume': {}, 'network': {}}
28
+ # Inventory every container so foreign mounts, network peers and occupied
29
+ # bindings are accounted for, even when they carry no RER label at all.
30
+ containers = inspect('container', docker('container', 'ls', '-aq').split())
31
+ for item in containers:
32
+ resources['container'][item['Name'].lstrip('/')] = item
33
+ for kind in ('volume', 'network'):
34
+ names = docker(kind, 'ls', '--format', '{{.Name}}').splitlines()
35
+ for item in inspect(kind, names):
36
+ resources[kind][item['Name']] = item
37
+ expected = {'container': ['app', 'caddy'], 'volume': ['data', 'caddy', 'caddy-config'], 'network': ['ingress']}
38
+ owned = {'container': {}, 'volume': {}, 'network': {}}
39
+ for kind, objects in resources.items():
40
+ for name, item in objects.items():
41
+ labels = item.get('Config', {}).get('Labels') if kind == 'container' else item.get('Labels')
42
+ matching_name = name in [plan['name'] + '-' + role for role in expected[kind]]
43
+ matching_owner = (labels or {}).get('io.runeditrun.installation') == plan['labels']['io.runeditrun.installation']
44
+ # Old compose-labelled extra services can otherwise be selected by
45
+ # compose down. Refuse these too rather than silently adopting them.
46
+ matching_compose = (labels or {}).get('com.docker.compose.project') == plan['name']
47
+ if matching_name or matching_owner or matching_compose:
48
+ role = name.removeprefix(plan['name'] + '-')
49
+ if role not in expected[kind] or name != plan['name'] + '-' + role:
50
+ refuse('Unexpected Docker installation resource')
51
+ check_labels(plan, role, labels)
52
+ owned[kind][role] = item
53
+ if kind == 'volume' and (item.get('Driver') != 'local' or item.get('Options')):
54
+ refuse('Unsafe Docker volume driver/options')
55
+ if kind == 'network' and (item.get('Driver') != 'bridge' or item.get('Options') or item.get('Internal') or item.get('ConfigOnly')):
56
+ refuse('Unsafe Docker ingress network')
57
+ owned_ids = {item['Id'] for item in owned['container'].values()}
58
+ for item in containers:
59
+ if item['Id'] not in owned_ids:
60
+ if any(mount.get('Name') in {v['Name'] for v in owned['volume'].values()} for mount in item.get('Mounts', [])):
61
+ refuse('Foreign container uses installation volume')
62
+ for network in owned['network'].values():
63
+ if any(key not in owned_ids for key in (network.get('Containers') or {})):
64
+ refuse('Foreign container uses installation network')
65
+ return owned, containers
66
+
67
+
68
+ def admit_ports(plan, owned, containers):
69
+ own_ids = {item['Id'] for item in owned['container'].values()}
70
+ for binding in plan['ports']:
71
+ address, port, _ = binding.split(':')
72
+ occupied_by_own = False
73
+ for item in containers:
74
+ if not item.get('State', {}).get('Running'):
75
+ continue
76
+ for entries in (item.get('NetworkSettings', {}).get('Ports') or {}).values():
77
+ for entry in entries or []:
78
+ overlap = address == '0.0.0.0' or entry['HostIp'] in ('0.0.0.0', '::', address)
79
+ if entry['HostPort'] == port and overlap:
80
+ if item['Id'] not in own_ids:
81
+ refuse('Host ingress binding is already occupied')
82
+ occupied_by_own = True
83
+ if not occupied_by_own:
84
+ with socket.socket() as listener:
85
+ try:
86
+ listener.bind((address, int(port)))
87
+ except OSError:
88
+ refuse('Host ingress binding is unavailable')
89
+
90
+
91
+ def label_args(plan, role):
92
+ return [arg for key, value in labels_for(plan, role).items() for arg in ('--label', key + '=' + value)]
93
+
94
+
95
+ def prepare_resources(plan, owned):
96
+ # Images and persistent resources must be available before stopping service.
97
+ app = json.loads(docker('image', 'inspect', plan['digest']))[0]
98
+ if app['Id'] != plan['digest']:
99
+ refuse('Application image identity mismatch')
100
+ caddy = None
101
+ if not plan['local']:
102
+ if not docker('image', 'ls', '--quiet', 'caddy:2.10.2'):
103
+ docker('image', 'pull', 'caddy:2.10.2')
104
+ caddy = json.loads(docker('image', 'inspect', 'caddy:2.10.2'))[0]['Id']
105
+ for role in (['data'] if plan['local'] else ['data', 'caddy', 'caddy-config']):
106
+ if role not in owned['volume']:
107
+ docker('volume', 'create', *label_args(plan, role), plan['name'] + '-' + role)
108
+ if 'ingress' not in owned['network']:
109
+ docker('network', 'create', '--driver', 'bridge', *label_args(plan, 'ingress'), plan['name'] + '-ingress')
110
+ return caddy
111
+
112
+
113
+ def replace_containers(plan, validate, files, caddy):
114
+ # Configuration is the exact content checked in the owned generation.
115
+ # Never ask either Docker client or daemon to reopen a host pathname after
116
+ # shutdown; their filesystem/process namespaces may also be different.
117
+ archive = io.BytesIO()
118
+ if caddy:
119
+ content = files['Caddyfile'].encode()
120
+ with tarfile.open(fileobj=archive, mode='w') as bundle:
121
+ entry = tarfile.TarInfo('Caddyfile')
122
+ entry.size, entry.mode = len(content), 0o600
123
+ bundle.addfile(entry, io.BytesIO(content))
124
+ owned, containers = inventory(plan)
125
+ admit_ports(plan, owned, containers)
126
+ validate()
127
+ # Stop by immutable IDs, with fresh labels on each individual operation.
128
+ # rm never uses -v: persistent data survives replacement and failures.
129
+ for role, item in owned['container'].items():
130
+ current = inspect('container', [item['Id']])[0]
131
+ check_labels(plan, role, current['Config'].get('Labels'))
132
+ validate()
133
+ docker('container', 'stop', '--time', '10', item['Id'])
134
+ check_labels(plan, role, inspect('container', [item['Id']])[0]['Config'].get('Labels'))
135
+ docker('container', 'rm', item['Id'])
136
+ common = ['--restart', 'unless-stopped', '--network', plan['name'] + '-ingress']
137
+ app = ['container', 'create', '--name', plan['name'] + '-app', *label_args(plan, 'app'), *common, '--network-alias', 'app', '--env-file', '/dev/stdin', '--env', 'PORT=8080', '--env', 'RER_IMAGE_DIGEST=' + plan['digest'], '--mount', 'type=volume,source=' + plan['name'] + '-data,target=/data']
138
+ if plan['local']:
139
+ for binding in plan['ports']:
140
+ app.extend(['--publish', binding])
141
+ else:
142
+ app.extend(['--env', 'TRUSTED_PROXY=caddy'])
143
+ app.append(plan['digest'])
144
+ inventory(plan)
145
+ app_id = docker(*app, input=files['settings.env'])
146
+ if caddy:
147
+ args = ['container', 'create', '--name', plan['name'] + '-caddy', *label_args(plan, 'caddy'), *common, '--network-alias', 'caddy', '--mount', 'type=volume,source=' + plan['name'] + '-caddy,target=/data', '--mount', 'type=volume,source=' + plan['name'] + '-caddy-config,target=/config']
148
+ for binding in plan['ports']:
149
+ args.extend(['--publish', binding])
150
+ args.append(caddy)
151
+ inventory(plan)
152
+ caddy_id = docker(*args)
153
+ check_labels(plan, 'caddy', inspect('container', [caddy_id])[0]['Config'].get('Labels'))
154
+ docker('container', 'cp', '-', caddy_id + ':/etc/caddy/', input=archive.getvalue())
155
+ docker('container', 'start', caddy_id)
156
+ check_labels(plan, 'app', inspect('container', [app_id])[0]['Config'].get('Labels'))
157
+ docker('container', 'start', app_id)
@@ -0,0 +1,10 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ export function superviseRunner({ root, manifest, settings, failed }) {
4
+ if (!manifest.runner) return { running:()=>true, async close(){} };
5
+ const child=spawn(process.execPath,[join(root,'runner',manifest.runner.entry)],{cwd:join(root,'runner'),env:{...process.env,...settings},stdio:['ignore','ignore','ignore']});
6
+ let stopping=false;
7
+ child.once('error',()=>{if(!stopping)failed();});
8
+ child.once('exit',()=>{if(!stopping)failed();});
9
+ return { running:()=>child.exitCode===null&&child.signalCode===null, async close(){stopping=true;if(child.exitCode!==null||child.signalCode!==null)return;child.kill('SIGTERM');await new Promise(resolve=>{const timer=setTimeout(()=>child.kill('SIGKILL'),35000);child.once('exit',()=>{clearTimeout(timer);resolve();});});} };
10
+ }
@@ -0,0 +1,44 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { ingressBindings } from '../deploy-kit/container-config.mjs';
3
+ export { ingressBindings } from '../deploy-kit/container-config.mjs';
4
+
5
+ export function installationIdentity(project, environment = 'production') {
6
+ if (typeof project !== 'string' || !/^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*$/.test(project) || project.length > 512 || !/^[a-z][a-z0-9-]{0,39}$/.test(environment)) throw Error('Invalid installation identity');
7
+ const identity = createHash('sha256').update(JSON.stringify([project, environment])).digest('hex');
8
+ return { name: `rer-${identity}`, labels: { 'io.runeditrun.managed': 'ssh-v1', 'io.runeditrun.installation': identity, 'io.runeditrun.project': project, 'io.runeditrun.environment': environment } };
9
+ }
10
+
11
+
12
+ export function deploymentPlan({ declaration, settings, environment, digest, ingress }) {
13
+ const { name, labels } = installationIdentity(declaration.project, environment);
14
+ if (!/^sha256:[a-f0-9]{64}$/.test(digest)) throw Error('Invalid Docker image digest');
15
+ const origin = settings[declaration.originVar] ?? settings.PUBLIC_ORIGIN;
16
+ if (!origin) throw Error('Declare the public origin in the private env file');
17
+ const url = new URL(origin);
18
+ if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) throw Error('Invalid public origin');
19
+ const local = /^(localhost|127\.0\.0\.1|\[::1\])$/.test(url.hostname);
20
+ if (!local && url.protocol !== 'https:') throw Error('Production ingress requires HTTPS');
21
+ if (local && url.protocol !== 'http:') throw Error('Loopback qualification requires an HTTP origin');
22
+ const hostnames = [url.hostname];
23
+ for (const variable of declaration.container?.hostVariables ?? []) {
24
+ const hostname = settings[variable];
25
+ if (!hostname || !/^(?=.{1,253}$)[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i.test(hostname)) throw Error('Invalid declared ingress hostname');
26
+ if (!hostnames.includes(hostname)) hostnames.push(hostname);
27
+ }
28
+ const bindings = ingressBindings({ ...declaration.container?.ingress, ...ingress });
29
+ const roleLabels = role => ({ ...labels, 'io.runeditrun.role': role });
30
+ const volume = role => ({ name: `${name}-${role}`, labels: roleLabels(role) });
31
+ const ports = local ? [`127.0.0.1:${url.port || 80}:8080`] : [`${bindings.address}:${bindings.httpPort}:80`, `${bindings.address}:${bindings.httpsPort}:443`];
32
+ const compose = { name, services: { app: { image: digest, container_name: `${name}-app`, labels: roleLabels('app'), restart: 'unless-stopped', environment: { RER_IMAGE_DIGEST: digest, PORT: '8080', ...(local ? {} : { TRUSTED_PROXY: 'caddy' }) }, env_file: [{ path: 'settings.env', format: 'raw' }], volumes: ['data:/data'], networks: ['ingress'], ...(local ? { ports } : {}) } }, volumes: { data: volume('data') }, networks: { ingress: { name: `${name}-ingress`, labels: roleLabels('ingress') } } };
33
+ if (!local) {
34
+ compose.services.caddy = { image: 'caddy:2.10.2', container_name: `${name}-caddy`, labels: roleLabels('caddy'), restart: 'unless-stopped', ports, configs: [{ source: 'caddy', target: '/etc/caddy/Caddyfile' }], volumes: ['caddy:/data', 'caddy-config:/config'], networks: ['ingress'] };
35
+ compose.volumes.caddy = volume('caddy'); compose.volumes['caddy-config'] = volume('caddy-config');
36
+ }
37
+ // Listener ports are internal. A TCP/SNI proxy must preserve the public
38
+ // authority and route ACME HTTP/TLS challenges to these private bindings.
39
+ const sites = hostnames.map(hostname => `https://${hostname}`).join(', ');
40
+ const redirects = hostnames.map(hostname => `http://${hostname} {\n redir https://${hostname}${url.port ? ':' + url.port : ''}{uri} permanent\n}`).join('\n');
41
+ const caddyfile = `{\n auto_https disable_redirects\n}\n${sites} {\n reverse_proxy app:8080\n}\n${redirects}\n`;
42
+ if (!local) compose.configs = { caddy: { content: caddyfile } };
43
+ return { name, labels, origin, digest, local, ports, compose, caddyfile };
44
+ }
@@ -0,0 +1,63 @@
1
+ import { spawn, execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
+ import { join, resolve } from 'node:path';
5
+ import { deploymentPlan } from './ssh-plan.mjs';
6
+ import { deployInputs } from './deploy-input.mjs';
7
+ const execute=promisify(execFile);
8
+ export const quote=value=>"'"+String(value).replaceAll("'","'\\''")+"'";
9
+ export function sshTarget(value){
10
+ let url;try{url=new URL(value);}catch{throw new Error('Host must be ssh://user@host[:port]');}
11
+ if(url.protocol!=='ssh:'||url.password||url.search||url.hash||!['','/'].includes(url.pathname)||!url.username||!/^[a-zA-Z0-9_.-]+$/.test(url.username)||!/^([a-zA-Z0-9.-]+|\[[0-9a-fA-F:]+\])$/.test(url.hostname))throw new Error('Invalid ssh host');
12
+ const args=['-o','BatchMode=yes','-o','StrictHostKeyChecking=yes'];
13
+ if(process.env.RER_SSH_IDENTITY)args.push('-i',resolve(process.env.RER_SSH_IDENTITY));
14
+ if(process.env.RER_SSH_KNOWN_HOSTS)args.push('-o','UserKnownHostsFile='+resolve(process.env.RER_SSH_KNOWN_HOSTS));
15
+ if(url.port)args.push('-p',url.port);args.push(url.username+'@'+url.hostname.replace(/^\[|\]$/g,''));return args;
16
+ }
17
+ export async function deploySSH({root,host,image,envFile,environment='production',localOnlyKeys=[],dryRun=false,ingress}) {
18
+ const ssh=sshTarget(host),{declaration,settings,encoded}=await deployInputs({root,envFile,localOnlyKeys});
19
+ const {stdout:digestText}=await execute('docker',['image','inspect','--format','{{.Id}}',image]);
20
+ const plan=deploymentPlan({declaration,settings,environment,digest:digestText.trim(),ingress});
21
+ const directory=join(root,'.rer/container/deploy');await mkdir(directory,{recursive:true,mode:0o700});
22
+ await writeFile(join(directory,'compose.yaml'),JSON.stringify(plan.compose,null,2)+'\n',{mode:0o600});
23
+ await writeFile(join(directory,'Caddyfile'),plan.caddyfile,{mode:0o600});
24
+ if(dryRun)return{dryRun:true,deployed:false,origin:plan.origin,installation:'.rer-host/'+plan.name,compose:join(directory,'compose.yaml')};
25
+ // The code is transferred from this installation's maintained CLI. No remote
26
+ // Python, shell or compose file is ever imported or executed from disk.
27
+ const source=(await Promise.all(['files','resources','deploy'].map(name=>readFile(new URL('./remote/'+name+'.py',import.meta.url),'utf8')))).join('\n');
28
+ const command='python3 -I -c '+quote(source);
29
+ const remote=async payload=>{
30
+ const child=spawn('ssh',[...ssh,command],{stdio:['pipe','pipe','pipe']});
31
+ const result=completed(child,true);child.stdin.on('error',()=>{});child.stdin.end(JSON.stringify(payload));return result;
32
+ };
33
+ // This also proves Python and Docker availability, filesystem custody and
34
+ // existing resource ownership before loading an image or sending secrets.
35
+ await remote({plan,phase:'check'});
36
+ const load=spawn('ssh',[...ssh,'docker load'],{stdio:['pipe','pipe','pipe']}),save=spawn('docker',['save',plan.digest],{stdio:['ignore','pipe','pipe']});
37
+ load.stdin.on('error',()=>{});save.stdout.pipe(load.stdin);
38
+ try{await Promise.all([completed(save),completed(load)]);}finally{if(save.exitCode===null)save.kill('SIGTERM');if(load.exitCode===null)load.kill('SIGTERM');}
39
+ const result=JSON.parse(await remote({plan,files:{'settings.env':encoded,'compose.yaml':JSON.stringify(plan.compose,null,2)+'\n','Caddyfile':plan.caddyfile}}));
40
+ const end=Date.now()+60000;
41
+ while(Date.now()<end){
42
+ try{const response=await fetch(plan.origin+'/health',{signal:AbortSignal.timeout(3000)});if(response.ok){const health=await response.json();if(health.platform==='container'&&health.deployment?.versionId===plan.digest)return{deployed:true,origin:plan.origin,imageDigest:plan.digest,...result};}}catch{}
43
+ await new Promise(resolve=>setTimeout(resolve,250));
44
+ }
45
+ throw new Error('Container public health did not become ready; inspect host logs');
46
+ }
47
+
48
+ const hostRefusals=new Set(['Unsafe host path ownership, permissions or type','Unsafe account home','Account home is not owned by login user','Host deployment path changed','Host deployment file changed','Installation ownership mismatch','Unexpected deployment file','Unexpected installation path','Unexpected Docker installation resource','Unsafe Docker volume driver/options','Unsafe Docker ingress network','Foreign container uses installation volume','Foreign container uses installation network','Host ingress binding is already occupied','Host ingress binding is unavailable','Application image identity mismatch','Host deployment refused: unsafe path or failed operation']);
49
+ function hostFailure(stderr){
50
+ const reason=stderr.trim();
51
+ const message=hostRefusals.has(reason)||/^Docker resource ownership mismatch: (app|caddy|caddy-config|data|ingress)$/.test(reason)||/^Docker host operation failed: (image|volume|network|container)$/.test(reason)?reason:'SSH host operation failed';
52
+ return Object.assign(Error(message),{code:'SSH_HOST_REFUSAL'});
53
+ }
54
+ function completed(child, capture=false) {
55
+ return new Promise((resolve,reject)=>{
56
+ let stdout='',stderr='';
57
+ const timer=setTimeout(()=>{child.kill('SIGTERM');reject(Error('Host operation timed out'));},120000);
58
+ child.stdout?.on('data',chunk=>{if(capture&&stdout.length<1024*1024)stdout+=chunk;});
59
+ child.stderr?.on('data',chunk=>{if(capture&&stderr.length<2048)stderr+=chunk;});
60
+ child.once('error',()=>{clearTimeout(timer);reject(Error('Host operation failed'));});
61
+ child.once('close',code=>{clearTimeout(timer);code===0?resolve(stdout):reject(capture?hostFailure(stderr):Error('Host operation failed'));});
62
+ });
63
+ }
@@ -0,0 +1,27 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { D1 } from './adapters/d1.mjs';
5
+ import { Objects } from './adapters/objects.mjs';
6
+ import { ensurePortabilitySchema } from './portability-schema.mjs';
7
+ import { migrate } from './migrations.mjs';
8
+ import { createPortability } from '../portability/index.mjs';
9
+ import { migrationReplay } from '../portability/migration-history.mjs';
10
+ import { exportArchive, importArchive } from '../portability/files.mjs';
11
+ let db,platform,worker;
12
+ try {
13
+ const spec=JSON.parse(await readFile('/private/transfer.json','utf8')),manifest=JSON.parse(await readFile('/app/container.json','utf8'));
14
+ const buckets={};
15
+ if(manifest.engine==='node'){
16
+ db=new D1('/data/'+spec.binding+'.sqlite');await migrate(db,manifest.migrations[spec.binding]);await ensurePortabilitySchema(db,manifest.portabilityMigrations);
17
+ platform=new DatabaseSync('/data/platform.sqlite');
18
+ for(const [name,binding]of Object.entries(spec.buckets))buckets[name]=new Objects(platform,'/data/objects',binding);
19
+ }else{
20
+ const {workerEngine}=await import('./worker-engine.mjs');worker=await workerEngine({root:'/app',data:'/data',manifest,settings:{}});db=worker.databases[spec.binding];
21
+ for(const [name,binding]of Object.entries(spec.buckets))buckets[name]=worker.objects[binding];
22
+ }
23
+ const service=await createPortability({db,buckets,policy:spec.policy,migrations:async()=>migrationReplay(manifest.migrations[spec.binding],spec.policy)});
24
+ const receipt=await(spec.direction==='export'?exportArchive:importArchive)(service,'/archive/'+spec.file,spec.transient,spec.journal);
25
+ console.log(JSON.stringify(receipt));
26
+ }catch(cause){console.error('Container transfer refused or failed:',cause.name);process.exitCode=1;}
27
+ finally{await worker?.close();await db?.close?.();platform?.close();}
@@ -0,0 +1,32 @@
1
+ import { Miniflare, convertV4MiniflareOptions } from 'miniflare';
2
+ import { readdir } from 'node:fs/promises';
3
+ import { join, relative } from 'node:path';
4
+ import { rootCertificates } from 'node:tls';
5
+ import { ensurePortabilitySchema } from './portability-schema.mjs';
6
+ import { migrate } from './migrations.mjs';
7
+ async function files(root) { const result = []; for (const item of await readdir(root, { withFileTypes: true })) { const path = join(root, item.name); if (item.isDirectory()) result.push(...await files(path)); else if (/\.(m?js)$/.test(item.name)) result.push(path); } return result; }
8
+ export async function workerEngine({ root, data, manifest, settings, edge }) {
9
+ const config = manifest.config, directory = join(root, 'dist/server'), entry = join(root, manifest.entry);
10
+ const modules = [entry, ...(await files(directory)).filter(path => path !== entry)].map(path => ({ type: 'ESModule', path }));
11
+ const app = { name:'app', modules, modulesRoot:directory, compatibilityDate:config.compatibility_date, compatibilityFlags:config.compatibility_flags,
12
+ bindings:{ ...config.vars, ...settings }, d1Databases:Object.fromEntries((config.d1_databases ?? []).map(item => [item.binding, item.database_name])),
13
+ r2Buckets:Object.fromEntries((config.r2_buckets ?? []).map(item => [item.binding, item.bucket_name])),
14
+ durableObjects:Object.fromEntries((config.durable_objects?.bindings ?? []).map(item => [item.name, { className:item.class_name, useSQLite:true }])),
15
+ queueProducers:Object.fromEntries((config.queues?.producers ?? []).map(item => [item.binding, item.queue])),
16
+ queueConsumers:Object.fromEntries((config.queues?.consumers ?? []).map(item => [item.queue, { maxBatchSize:item.max_batch_size, maxRetries:item.max_retries }])),
17
+ assets:{ directory:join(root, 'dist/client'), binding:config.assets?.binding ?? 'ASSETS', routerConfig:{ has_user_worker:true }, run_worker_first:config.assets?.run_worker_first ?? false },
18
+ outboundService: edge ? request => edge(request, request => mf.getWorker('egress').then(worker => worker.fetch(request))) : 'egress',
19
+ };
20
+ const mf = new Miniflare(convertV4MiniflareOptions({ host:'127.0.0.1',port:0,cf:false,resourcePersistencePath:data,unsafeLocalExplorer:false,
21
+ workers:[{ name:'ingress',modules:true,script:`export default {fetch(request,env){const observed=request.headers.get('x-rer-original-url');if(!observed)return new Response(null,{status:400});const url=new URL(observed),headers=new Headers(request.headers);headers.delete('x-rer-original-url');headers.set('host',url.host);return env.APP.fetch(new Request(url,{method:request.method,headers,redirect:'manual',...(!['GET','HEAD'].includes(request.method)?{body:request.body}:{})}))}}`,serviceBindings:{ APP:'app' } }, app,
22
+ { name:'egress',modules:true,script:'export default {fetch(request,env){return env.NETWORK.fetch(request)}}',serviceBindings:{ NETWORK:{ network:{ allow:['public'],tlsOptions:{ trustBrowserCas:true,trustedCertificates:[...rootCertificates] } } } } }],
23
+ }));
24
+ await mf.ready; const databases = {}, objects = {};
25
+ for (const [binding, plan] of Object.entries(manifest.migrations)) { const db = await mf.getD1Database(binding, 'app'); databases[binding] = db; await migrate(db, plan); await ensurePortabilitySchema(db, manifest.portabilityMigrations); }
26
+ for (const bucket of config.r2_buckets ?? []) objects[bucket.binding] = await mf.getR2Bucket(bucket.binding, 'app');
27
+ const worker = await mf.getWorker('app');
28
+ return { mf, databases, objects, app:worker,
29
+ fetch(request) { const headers = new Headers(request.headers); headers.set('x-rer-original-url', request.url); return mf.dispatchFetch(request.url, { method:request.method,headers,body:request.body,duplex:'half',redirect:'manual' }); },
30
+ scheduled:event => worker.scheduled(event), async drain() {}, dead:() => 0, close:() => mf.dispose(),
31
+ };
32
+ }