@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,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
+ }
@@ -0,0 +1,211 @@
1
+ # Application dependency links
2
+
3
+ This module implements the application side of the pull-link protocol. Auth's
4
+ provider implementation (auth-7) is separate. `createDependencies` mounts the
5
+ well-known routes and owner controls; `AuthConfig.dependencies: true` resolves
6
+ OIDC client settings from the persisted link and the server-generated key.
7
+ No client ID or private-key environment answer is needed. The default seed and
8
+ composed foundation declare an optional `oidc-provider@1` dependency using the
9
+ `IDENTITY_PROVIDER` URL answer. A blank answer keeps email-link access working.
10
+ Other interfaces use the same handshake, storage and callback; only the OIDC
11
+ adapter adds OIDC discovery, sessions, domain policy and owner claims.
12
+
13
+ ## Discovery and registration
14
+
15
+ `GET /.well-known/rer-dependencies` returns `name`, `version: 1`, the exact
16
+ `origin`, stable `sector_id`, `provides`, `dependencies`, `claim`, `callback`, and
17
+ `jwks_uri`. Each dependency has `interface` (for example `oidc-provider@1`),
18
+ `optional`, `cardinality: 1`, selected `provider` origin or null, registered
19
+ same-origin `paths` as absolute URLs, and a per-interface `jwks_uri`.
20
+ `GET /.well-known/rer-link/jwks/INTERFACE` publishes an ES256 public JWK only.
21
+ The unqualified JWKS URL selects the identity interface. The application retains
22
+ its private key in the server database. Sector/install identity survives re-link,
23
+ provider moves and export/import. Keys are isolated per interface.
24
+
25
+ Provider origins and metadata endpoints require HTTPS. The application's local
26
+ runtime explicitly permits loopback HTTP; deployed registration requires HTTPS.
27
+ Every provider serves `/.well-known/rer-dependencies` with the same graph fields,
28
+ including its `provides` list and selected dependency provider origins. The CLI
29
+ traverses these documents before setup effects, including selected optional
30
+ edges, and refuses cycles, invalid references and oversized graphs.
31
+
32
+ A provider serves `GET /.well-known/rer-provider` with `issuer`, `version: 1`,
33
+ `interface` or `provides`, and absolute `initiate`, `redeem`, `deregister` URLs.
34
+ All three endpoints must be HTTPS on exactly that issuer, with no query,
35
+ fragment or URL credentials. OIDC additionally serves standards discovery with
36
+ `rer_interfaces: ["oidc-provider@1.0.0"]` and compatible authorization, token,
37
+ JWKS, ES256 and private_key_jwt support. The application pins those metadata and
38
+ interface versions; callback input cannot select an endpoint or change the pin.
39
+
40
+ ## Owner approval and pull handshake
41
+
42
+ 1. The provider owner approves this exact app origin, interface and callback set.
43
+ For unattended `rer setup` or `rer deploy`, the provider issues an opaque,
44
+ one-use approval lasting at most one hour. Save it in an untracked 0600 file
45
+ named by `RER_LINK_TOKEN_FILE`. For multiple interfaces, use
46
+ `RER_LINK_TOKEN_FILES` instead: a JSON map such as
47
+ `{"oidc-provider@1":"auth-approval.json","error-sink@1":"errors-approval.json"}`.
48
+ Each interface needs a distinct private file; all pending approvals are
49
+ validated before setup effects and each consumed file is deleted. Its JSON envelope is
50
+ `{token, issuer, interface, origin, callbacks, issuedAt, expiresAt}`; times are
51
+ integer epoch milliseconds. `callbacks` is exactly the fixed link callback
52
+ plus the dependency's registered paths. Git-tracked files are refused.
53
+ 2. Before any provider callback, the app must hold its own ten-minute approval.
54
+ On an empty app, `POST /auth/dependencies/setup` accepts `{issuer,interface,token}`
55
+ using the app's private setup token, or the local CLI proof on loopback only.
56
+ Local onboarding opens these approvals before native bootstrap. Hosted deploy
57
+ uses the deployed app's setup token, never a local HMAC. Base account bootstrap binds
58
+ pending setup approvals to that owner's version and exact bootstrap session;
59
+ callback commit requires that session to remain active, unrevoked and unexpired.
60
+ A signed IdP owner claim may convert that same session to OIDC while sibling
61
+ setup approvals remain pending. Those approvals also require enabled provider
62
+ sign-in, the current policy generation and the session's matching linked
63
+ provider pin, including when claim completes during callback redemption.
64
+ After claim, use the current owner's fresh email proof at
65
+ `/auth/dependencies/relink`.
66
+ The CLI posts `{origin, interface, token}` to the provider's pinned `initiate`
67
+ endpoint. Interactive provider-owned approval can start the same pull without
68
+ a CLI token. The provider fetches the app's document and public JWKS, verifies
69
+ its approval against that origin and exact callback set, and stores the key,
70
+ sector, interface and registration paths with a short one-use code.
71
+ 3. The provider posts only `{issuer, interface, code}` to the fixed app URL
72
+ `/.well-known/rer-link/callback`. The app accepts only its configured/trusted
73
+ issuer named by that fresh app-local approval. Every callback, including an
74
+ unchanged link or a link after unlink, requires a new approval. The exact
75
+ attempt, owner/session authority and link snapshot are rechecked atomically
76
+ after redemption; replacing an approval invalidates in-flight callbacks. It never
77
+ follows a callback URL supplied in the request.
78
+ 4. The app durably consumes the code digest and posts to the pinned `redeem`
79
+ endpoint: `{code, interface, origin, callback, client_assertion_type,
80
+ client_assertion}`. Assertion type is
81
+ `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. Its signed ES256 JWT
82
+ has `iss` and `sub` equal to the app origin, `aud` equal to the redeem URL,
83
+ `iat`, `exp` at most 60 seconds later, unique `jti`, `code`, `interface`,
84
+ `sector_id` and the fixed `callback`.
85
+ 5. The provider verifies the signature against the key fetched at approval,
86
+ audience, times, unique assertion/code use and every bound registration
87
+ property. Code redemption is atomic: concurrent requests have one winner.
88
+ The response contains `{issuer, interface, origin, callback, sector_id,
89
+ client_id}`. For an existing same-provider link it must retain the client ID
90
+ and provide `continuity: "previous-key"` after proving the previous key, or
91
+ `continuity: "provider-owner"` after fresh provider-owner approval. A new key
92
+ alone is never continuity proof. The app checks these values and commits the
93
+ link and OIDC pins in one transaction. The provider returns `{linked:true}`
94
+ from initiation only after the app accepted the callback.
95
+ 6. The CLI checks the app's accepted link then deletes its approval file. The
96
+ token never enters public answers, runtime variables, logs or exports. An
97
+ unchanged completed link needs no token on later setup. An explicitly supplied
98
+ token is always validated. Changed provider answers retain the old link and
99
+ require a new app-local approval and provider approval.
100
+
101
+ Provider implementations must enforce code and token expiry, one-use redemption,
102
+ origin/callback binding and key continuity themselves. The application-side
103
+ network tests model those provider obligations without contacting a live service.
104
+
105
+ ## App owner controls and claims
106
+
107
+ Normal Base account setup claims the native owner first, then links. Only that first setup's
108
+ successful identity link enables `idpSignIn` automatically. The setup attempt
109
+ must have been opened before that native claim; a native claim without one
110
+ closes eligibility in its transaction. A link completed before a native claim
111
+ cannot authorize a later relink to enable sign-in. The CLI closes that
112
+ first-setup eligibility after successful setup, including a blank optional
113
+ provider. Existing and imported installs remain off until their current owner
114
+ enables the app setting. `GET/POST /auth/settings/identity` has JSON and plain HTML
115
+ forms, requires the current owner, records effective changes in `oa_auth_audit`,
116
+ and never accepts platform authority. Any valid settings POST closes setup
117
+ eligibility, including an unchanged value, without auditing a nonexistent change
118
+ or advancing the session generation. Turning off increments a session generation
119
+ and revokes provider sessions. In-flight callbacks cannot cross that generation;
120
+ turning on never restores old sessions. Provider identity or roles alone grant no
121
+ app membership: accounts or invitations remain authoritative.
122
+
123
+ An app linked before claim advertises `claim: true` and requests the OIDC `claim`
124
+ scope. SETUP owner-claim mode `idp` names the declared identity dependency and
125
+ `/auth/owner-claim`. The provider can return `install_owner` from link redemption
126
+ or an OIDC ID token containing the grant. The former value is itself a signed JWT;
127
+ the latter is the signed ID token. Both use the pinned issuer, client audience,
128
+ subject, fresh verified designated email, unique jti, iat and exp (at most five
129
+ minutes), and `install_owner: {client_id, sector_id, subject, email}`. The app
130
+ requires all four properties to match its own installation and designated owner.
131
+ The existing bootstrap transaction commits owner, grant consumption, binding,
132
+ setting audit and provider session together. Durable issuer/client uniqueness
133
+ allows one owner grant even after a restart; the grant never becomes an ordinary
134
+ role or a general sign-in credential. CLI readiness also verifies the local owner
135
+ exists; linking alone is insufficient.
136
+
137
+ `POST /auth/dependencies/relink` with `{interface,issuer}` requires the current
138
+ owner's fresh app-local email proof. It creates a ten-minute pending approval
139
+ bound to interface, issuer, exact callback set, owner, owner version and session. A provider move
140
+ keeps existing principal bindings immutable; new-issuer access needs a newly
141
+ proved email matching an existing account/invitation. `POST
142
+ /auth/dependencies/unlink` with `{interface}` uses the same owner proof, deletes
143
+ that interface's link, key and pending approval while retaining the sector, revokes only its provider sessions, and lifts only its
144
+ provider domain rules before any network call. Signed deregistration is best
145
+ effort; offline providers cannot prevent local removal.
146
+
147
+ ## Sessions, logout and portability
148
+
149
+ Provider sessions record issuer, subject, sid and policy generation and last at
150
+ most 24 hours. OIDC sessions also record their provider pin. Each completed
151
+ re-link advances that provider's monotonic link timestamp, including an unchanged
152
+ metadata re-link in the same millisecond. Binding, invitation acceptance and
153
+ session issuance check the captured pin inside their transaction; session reads
154
+ and privileged writes also require that pin to remain linked. Unrelated
155
+ providers and native sessions retain their own authority. Existing consumers
156
+ apply `accounts/schema/provider-pins.sql` as a new numbered migration (the
157
+ default seed uses `0014_provider_pins.sql`). Existing consumers also apply
158
+ `operator-auth/accounts/schema/dependency-setup.sql` and
159
+ `dependencies/schema/approval-attempt.sql` as new migrations (default-seed
160
+ `0015` and `0016`). Native-account consumers apply only the latter alongside
161
+ their own native migrations; they must not install the OA setup schema. The
162
+ latter invalidates older pending approvals. Auth email proof must be dated, valid and no older than 30 days,
163
+ even for a bound account. Explicit mappings between an upstream subject and email remain
164
+ supported only when they do not contradict an asserted stale proof.
165
+
166
+ `POST /auth/oidc/auth/logout` accepts `logout_token` and ignores other fields.
167
+ Use a signed JWT with `typ: "logout+jwt"`, pinned issuer/client audience, current
168
+ iat/exp, nonempty durable one-use jti, no nonce property, and sub or sid. When
169
+ both selectors exist they must agree. `events` must contain an empty object at
170
+ `http://schemas.openid.net/event/backchannel-logout` or
171
+ `https://runeditrun.com/events/identity-fence`; unknown events are refused.
172
+ The fence event increments only the selected person's durable identity fence.
173
+ All their fenced sessions and minted credentials then fail admission, while
174
+ other people remain active. Ordinary ID tokens with any `events` property are
175
+ refused. Credential consumers must record/check the fence; the provided
176
+ `mintCredential`/`authenticateCredential` functions also refuse legacy credentials
177
+ with no recorded fence. Editor grants require the actual current install owner;
178
+ OIDC sessions must also match that owner's current issuer/subject account binding.
179
+
180
+ Exports omit server link keys, approvals, code state, provider sessions and OIDC
181
+ transients, preserve durable people/bindings/grant use/sector, set sign-in off and
182
+ mark links as needing re-link. Import checks these conditions and requires an
183
+ explicit fresh provider approval. Local `rer import` returns `needsRelink: true`
184
+ and `nextCommand: "rer setup"`; that setup continuation consumes the fresh approval
185
+ and retains the imported sector and bindings. `rer doctor` reports dangling providers or a
186
+ needed re-link without reading or emitting private keys or approvals.
187
+
188
+ ## Native accounts
189
+
190
+ Native `createOperatorAuth` composition reuses this protocol with
191
+ `accounts.dependencies: NativeDependencyAuthority<R>`. Missing capabilities
192
+ refuse enablement. Native authority checks and revocation statements join the
193
+ same D1 link/unlink transaction; verified logout consumption uses the native
194
+ binding/session/fence store atomically. No OA account tables are queried.
195
+ Every native callback requires a new local owner approval, including initial
196
+ and unchanged links. Shared code fences the exact attempt and approving
197
+ person/session/version snapshot at preflight and commit; native contributions
198
+ repeat their own current authority. Ordinary owner linking preserves disabled
199
+ identity regardless of stale setup flags. Unlink deletes the link, key and
200
+ pending approval while preserving the sector.
201
+
202
+ Native dependency setup before a first owner is currently unsupported:
203
+ `/auth/dependencies/setup` and `auth.authorizeDependencySetup` explicitly refuse
204
+ with `native_capability_unavailable` (501) before pending/key mutation or OA
205
+ setup SQL. Claim the native owner through the seed's supported bootstrap, then
206
+ confirm a local email proof and approve at `/auth/dependencies/relink`. A native
207
+ `claimOwner` capability alone does not make an empty installation's initial IdP
208
+ link/claim journey available. Static incumbent providers remain available beside
209
+ linked Auth.
210
+ See [the exact native contract](../operator-auth/docs/native-accounts.md) for
211
+ method arguments, migration selection, proof fences and adoption steps.
@@ -0,0 +1,51 @@
1
+ import { providerPinFence } from '../operator-auth/accounts/provider-pin.mjs';
2
+ import { ownerFence } from './authority.mjs';
3
+ import { CALLBACK, insist } from './protocol.mjs';
4
+
5
+ export const callbacksFor = (origin, declared) => JSON.stringify([origin+CALLBACK,...declared.paths.map(path=>origin+path)].sort());
6
+ export async function approveSetup({db,auth,request,input,contract,callbacks,clock}) {
7
+ // Native setup needs durable native bootstrap provenance. Until that optional
8
+ // capability exists, refuse before touching pending approvals or OA tables.
9
+ insist(!auth.native,'native_capability_unavailable',501);
10
+ await auth.authorizeDependencySetup(request,input);
11
+ const attempt=crypto.randomUUID(),expires=clock()+600000,guard=crypto.randomUUID();
12
+ await db.batch([
13
+ db.prepare('INSERT INTO rer_dependency_codes(digest,created_at) SELECT ?,CASE WHEN NOT EXISTS(SELECT 1 FROM oa_operators) AND NOT EXISTS(SELECT 1 FROM oa_bootstrap) THEN ? ELSE -1 END').bind(guard,clock()),
14
+ db.prepare("INSERT INTO rer_dependency_pending(interface,issuer,approved_by,expires_at,approved_session,approved_version,callbacks,attempt_id) VALUES(?,?,'setup',?,?,0,?,?) ON CONFLICT(interface) DO UPDATE SET issuer=excluded.issuer,approved_by=excluded.approved_by,expires_at=excluded.expires_at,approved_session=excluded.approved_session,approved_version=excluded.approved_version,callbacks=excluded.callbacks,attempt_id=excluded.attempt_id").bind(contract,input.issuer,expires,attempt,callbacks,attempt),
15
+ db.prepare('INSERT INTO oa_setup_links(interface,attempt_id,expires_at) VALUES(?,?,?) ON CONFLICT(interface) DO UPDATE SET attempt_id=excluded.attempt_id,expires_at=excluded.expires_at,owner_id=NULL,owner_version=NULL,session_id=NULL,completed=0').bind(contract,attempt,expires),
16
+ ]);
17
+ }
18
+ export async function approveOwner({store,actor,input,contract,callbacks,clock,native}) {
19
+ const fence=native?await native.ownerFence(actor,clock()):ownerFence(actor,clock());
20
+ const approval=await store.run(`INSERT INTO rer_dependency_pending(interface,issuer,approved_by,expires_at,approved_session,approved_version,callbacks,attempt_id) SELECT ?,?,?,?,?,?,?,? WHERE ${fence.sql} ON CONFLICT(interface) DO UPDATE SET issuer=excluded.issuer,approved_by=excluded.approved_by,expires_at=excluded.expires_at,approved_session=excluded.approved_session,approved_version=excluded.approved_version,callbacks=excluded.callbacks,attempt_id=excluded.attempt_id`,contract,input.issuer,actor.operator.id,clock()+600000,actor.id,actor.operator.version,callbacks,crypto.randomUUID(),...fence.args);
21
+ insist(approval.meta.changes===1,'authority_changed',409);
22
+ }
23
+ // Snapshot the attempt before network I/O. Every authority and exact attempt
24
+ // predicate is executed again in the transaction that installs the link.
25
+ export async function approvalFence(pending,contract,issuer,callbacks,now,pendingPin,native) {
26
+ const base={sql:'EXISTS(SELECT 1 FROM rer_dependency_pending p WHERE p.interface=? AND p.issuer=? AND p.callbacks=? AND p.expires_at>? AND p.attempt_id=?',args:[contract,issuer,callbacks,now,pending.attempt_id]};
27
+ if(native) {
28
+ const authority=await native.approvalFence({pending,contract,issuer,callbacks,now});
29
+ // The shared protocol owns attempt/snapshot equality. An adopter contributes
30
+ // native authority only; replacing any approval invalidates this callback.
31
+ return {sql:`${base.sql} AND p.approved_by=? AND p.approved_session=? AND p.approved_version=?) AND (${authority.sql})`,args:[...base.args,pending.approved_by,pending.approved_session,pending.approved_version,...authority.args]};
32
+ }
33
+ // A signed IdP claim converts the bootstrap session in the same transaction.
34
+ // Resolve its live provenance here: a callback may have started before claim.
35
+ if(pending.approved_by==='setup')return {
36
+ sql:base.sql+` AND EXISTS(SELECT 1 FROM oa_setup_links l WHERE l.interface=p.interface AND l.attempt_id=p.attempt_id AND l.completed=0 AND l.expires_at>?
37
+ AND ((l.owner_id IS NULL AND l.owner_version IS NULL AND l.session_id IS NULL AND NOT EXISTS(SELECT 1 FROM oa_operators) AND NOT EXISTS(SELECT 1 FROM oa_bootstrap))
38
+ OR EXISTS(SELECT 1 FROM oa_bootstrap b JOIN oa_operators o ON o.id=b.claim_id JOIN oa_sessions s ON s.operator_id=o.id
39
+ WHERE o.id=l.owner_id AND o.role='owner' AND o.status='active' AND o.version=l.owner_version
40
+ AND s.id=l.session_id AND s.operator_version=l.owner_version AND s.revoked_at IS NULL AND s.expires_at>?
41
+ AND (s.kind='native' OR (s.kind='oidc' AND EXISTS(
42
+ SELECT 1 FROM oa_provider_sessions ps
43
+ JOIN oa_auth_settings a ON a.singleton=1 AND a.idp_sign_in=1 AND a.generation=ps.generation
44
+ JOIN oa_provider_session_pins pin ON pin.session_id=ps.session_id AND pin.provider_id='auth' AND pin.issuer=ps.issuer
45
+ JOIN oa_identity_providers provider ON provider.provider_id=pin.provider_id AND provider.status='linked'
46
+ AND provider.issuer=pin.issuer AND provider.client_id=pin.client_id AND provider.linked_at=pin.linked_at
47
+ WHERE ps.session_id=s.id)))))))`,args:[...base.args,now,now],
48
+ };
49
+ const pin=providerPinFence(pendingPin);
50
+ return {sql:`${pin.sql} AND ${base.sql} AND p.approved_by=? AND p.approved_session=? AND p.approved_version=? AND EXISTS(SELECT 1 FROM oa_operators o JOIN oa_sessions s ON s.operator_id=o.id WHERE o.id=p.approved_by AND o.version=p.approved_version AND s.id=p.approved_session AND s.operator_version=o.version AND s.revoked_at IS NULL AND s.expires_at>? AND o.status='active' AND o.id=(SELECT a.id FROM oa_operators a LEFT JOIN oa_bootstrap b ON b.claim_id=a.id WHERE a.role='owner' AND a.status='active' ORDER BY b.claim_id IS NULL,a.created_at,a.id LIMIT 1)))`,args:[...pin.args,...base.args,pending.approved_by,pending.approved_session,pending.approved_version,now]};
51
+ }