@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,64 @@
1
+ import { devGate, devNotFound } from '../../operator-auth/dev-gate.mjs';
2
+ import { createAiClient, AiError } from '../index.mjs';
3
+ import { createFakeTransport, DEV_AI_MARKER } from '../fake/index.mjs';
4
+ import { createAiOutbox } from './outbox.mjs';
5
+
6
+ export { createAiOutbox } from './outbox.mjs';
7
+ export { DEV_AI_MARKER } from '../fake/index.mjs';
8
+ const headers = { 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', 'x-robots-tag': 'noindex, nofollow', 'content-security-policy': "default-src 'none'; frame-ancestors 'none'", 'x-rer-ai-dev': DEV_AI_MARKER };
9
+
10
+ /** The originating product request must pass operator-auth's gate, too. */
11
+ export function createDevAiClient(config, { request, key, origin, local, outbox, live, fetch: send = globalThis.fetch, script }) {
12
+ const gate = devGate({ key, origin, local });
13
+ const capture = async record => {
14
+ try { await outbox.put(record); }
15
+ catch { throw new AiError('dev_outbox_unavailable', 503); }
16
+ };
17
+ const fake = createFakeTransport({ script, capture });
18
+ const selected = live ? { ...config, direct: undefined, models: live, features: {} } : { ...config, direct: { baseUrl: origin, apiKey: 'dev-double' }, models: undefined, features: {} };
19
+ const client = createAiClient(selected, live ? { fetch: send } : { transport: fake });
20
+ return Object.freeze(Object.fromEntries(['chat', 'responses', 'embeddings', 'receipt'].map(operation => [operation, async (body, options) => {
21
+ if (!gate || !await gate.admits(request)) throw new AiError('dev_access_denied', 404);
22
+ if (!live) return client[operation](body, options);
23
+ let response;
24
+ try {
25
+ response = await client[operation](body, options);
26
+ // Live dev retains receipt headers only. Never read/tee the response body.
27
+ const receipt = Object.fromEntries(['x-models-request', 'x-models-target', 'x-models-attempts', 'x-models-cost-microusd'].map(name => [name, response.headers.get(name)]).filter(([, value]) => value !== null));
28
+ await capture({ feature: options?.feature ?? 'default', operation, receipt });
29
+ return response;
30
+ } catch (error) {
31
+ if (response) {
32
+ try { await response.body?.cancel(); } catch {}
33
+ } else await capture({ feature: options?.feature ?? 'default', operation, error: { code: error instanceof AiError ? error.code : 'ai_unavailable' } });
34
+ throw error;
35
+ }
36
+ }])));
37
+ }
38
+
39
+ /** Mount only in server.dev; production has neither this route nor the double. */
40
+ export function withDevAi(worker, { bindings = env => env } = {}) {
41
+ return {
42
+ ...worker,
43
+ async fetch(request, original, execution) {
44
+ const path = new URL(request.url).pathname;
45
+ if (path !== '/__dev/ai' && !path.startsWith('/__dev/ai/')) return worker.fetch(request, original, execution);
46
+ const env = bindings(original), origin = env.RER_DEV_ORIGIN ?? env.AUTH_ORIGIN;
47
+ const gate = devGate({ key: env.RER_DEV_KEY, origin, local: env.AUTH_LOCAL === 'true' });
48
+ if (!gate || !await gate.admits(request)) return devNotFound();
49
+ try {
50
+ const outbox = createAiOutbox({ db: env.DB });
51
+ if (path === '/__dev/ai' && ['GET', 'HEAD'].includes(request.method)) return new Response(request.method === 'HEAD' ? null : JSON.stringify(await outbox.list()), { headers: { ...headers, 'content-type': 'application/json' } });
52
+ const operation = { '/__dev/ai/v1/chat/completions': 'chat', '/__dev/ai/v1/responses': 'responses', '/__dev/ai/v1/embeddings': 'embeddings' }[path];
53
+ if (!operation || request.method !== 'POST') return devNotFound();
54
+ let body; try { body = await request.json(); } catch { throw new AiError('ai_invalid_request', 400); }
55
+ const live = env.RER_MODELS_SERVICE || env.RER_MODELS_KEY ? { baseUrl: env.RER_MODELS_SERVICE, apiKey: env.RER_MODELS_KEY } : undefined;
56
+ const client = createDevAiClient({ enabled: true }, { request, key: env.RER_DEV_KEY, origin, local: true, outbox, live });
57
+ return await client[operation](body, { feature: request.headers.get('x-models-feature') ?? undefined, signal: request.signal });
58
+ } catch (error) {
59
+ const safe = error instanceof AiError ? error : new AiError('ai_unavailable');
60
+ return Response.json({ error: { code: safe.code, message: safe.message } }, { status: safe.status, headers });
61
+ }
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,33 @@
1
+ const schema = `CREATE TABLE IF NOT EXISTS rer_ai_development_outbox (
2
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE,
3
+ created_at INTEGER NOT NULL, record TEXT NOT NULL
4
+ )`;
5
+
6
+ /** Uses the seed's real local D1; no memory-storage alternative. */
7
+ export function createAiOutbox({ db, clock = Date.now, maxRequests = 100, maxAgeMs = 7 * 86400000 }) {
8
+ if (!db?.prepare || !db?.batch || !Number.isSafeInteger(maxRequests) || maxRequests < 1 || !Number.isSafeInteger(maxAgeMs) || maxAgeMs < 1) throw new TypeError('Invalid AI outbox');
9
+ let initialized;
10
+ const statement = (sql, ...values) => db.prepare(sql).bind(...values);
11
+ async function ready() {
12
+ initialized ??= db.prepare(schema).run().then(result => { if (!result.success) throw new Error('AI outbox unavailable'); }).catch(error => { initialized = undefined; throw error; });
13
+ await initialized;
14
+ }
15
+ return {
16
+ async put(record) {
17
+ await ready();
18
+ const time = clock();
19
+ const results = await db.batch([
20
+ statement('INSERT INTO rer_ai_development_outbox(id,created_at,record) VALUES(?,?,?)', crypto.randomUUID(), time, JSON.stringify(record)),
21
+ statement('DELETE FROM rer_ai_development_outbox WHERE created_at <= ?', time - maxAgeMs),
22
+ statement('DELETE FROM rer_ai_development_outbox WHERE sequence NOT IN (SELECT sequence FROM rer_ai_development_outbox ORDER BY sequence DESC LIMIT ?)', maxRequests),
23
+ ]);
24
+ if (results.some(result => result.success !== true)) throw new Error('AI outbox unavailable');
25
+ },
26
+ async list() {
27
+ await ready();
28
+ const result = await statement('SELECT id,created_at,record FROM rer_ai_development_outbox WHERE created_at > ? ORDER BY sequence DESC LIMIT ?', clock() - maxAgeMs, maxRequests).all();
29
+ if (!result.success) throw new Error('AI outbox unavailable');
30
+ return result.results.map(row => ({ id: row.id, createdAt: row.created_at, ...JSON.parse(row.record) }));
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,28 @@
1
+ const messages = {
2
+ ai_disabled: 'AI is off for this feature.',
3
+ ai_misconfigured: 'Configure exactly one AI transport for this feature.',
4
+ ai_unavailable: 'The AI provider is unavailable.',
5
+ ai_target_mismatch: 'The served target differs from the pinned model requested.',
6
+ ai_invalid_response: 'The AI provider returned an invalid response.',
7
+ ai_invalid_request: 'The AI request is invalid.',
8
+ dev_fake_cannot_answer: 'The dev double cannot answer schema-typed outputs or forced tool calls. Use live dev with a capped Models dev app.',
9
+ dev_access_denied: 'Not found',
10
+ dev_outbox_unavailable: 'The local AI outbox is unavailable.',
11
+ };
12
+
13
+ /** Fixed messages only: credentials and upstream exception bodies never escape. */
14
+ export class AiError extends Error {
15
+ constructor(code, status = 502) {
16
+ super(messages[code] ?? 'The AI request was refused.');
17
+ this.name = 'AiError';
18
+ this.code = code;
19
+ this.status = status;
20
+ }
21
+ }
22
+
23
+ export const serviceErrorCodes = new Set([
24
+ 'ai_unavailable', 'ai_misconfigured', 'no_eligible_target', 'field_not_allowed',
25
+ 'budget_exhausted', 'upstream_limited', 'upstream_unavailable', 'upstream_auth',
26
+ 'upstream_policy_unmet', 'upstream_uncertain', 'upstream_error',
27
+ 'response_too_large', 'dev_fake_cannot_answer',
28
+ ]);
@@ -0,0 +1,3 @@
1
+ import type { AiTransport, AiRequest } from '../index.mjs';
2
+ export const DEV_AI_MARKER: string;
3
+ export function createFakeTransport(options?: { script?: AiTransport['request']; capture?: (record: { feature: string; operation: string; request?: AiRequest; response?: unknown; usage?: unknown; error?: { code: string; status: number } }) => Promise<void> }): AiTransport;
@@ -0,0 +1,82 @@
1
+ import { AiError } from '../errors.mjs';
2
+
3
+ export const DEV_AI_MARKER = 'rer-ai-dev:local-only-never-deploy';
4
+ const encoder = new TextEncoder();
5
+ async function hash(input) {
6
+ const bytes = await crypto.subtle.digest('SHA-256', encoder.encode(JSON.stringify(input)));
7
+ return [...new Uint8Array(bytes)];
8
+ }
9
+ const hex = bytes => bytes.map(b => b.toString(16).padStart(2, '0')).join('');
10
+ const headers = { 'cache-control': 'no-store', 'x-rer-ai-dev': DEV_AI_MARKER };
11
+ function sse(events, signal) {
12
+ return new Response(new ReadableStream({
13
+ pull(controller) {
14
+ if (signal?.aborted) { controller.error(new AiError('upstream_uncertain', 504)); return; }
15
+ const event = events.shift();
16
+ if (event === undefined) { controller.close(); return; }
17
+ controller.enqueue(encoder.encode(event));
18
+ },
19
+ }), { headers: { ...headers, 'content-type': 'text/event-stream' } });
20
+ }
21
+ const data = value => `data: ${JSON.stringify(value)}\n\n`;
22
+ const event = (type, value) => `event: ${type}\n${data({ type, ...value })}`;
23
+
24
+ /** Explicit provider-edge double. Scripted mode never invents an answer. */
25
+ export function createFakeTransport({ script, capture = async () => {} } = {}) {
26
+ return {
27
+ async request({ operation, body, feature, signal }) {
28
+ if (signal?.aborted) throw new AiError('upstream_uncertain', 504);
29
+ try {
30
+ if (script) {
31
+ const response = await script({ operation, body, feature, signal });
32
+ if (!response.headers.get('content-type')?.includes('text/event-stream')) await capture({ feature, operation, request: body, response: await response.clone().json() });
33
+ return response;
34
+ }
35
+ const format = body.response_format?.type ?? body.text?.format?.type;
36
+ const forcedTool = body.tool_choice !== undefined && !['auto', 'none'].includes(body.tool_choice);
37
+ if (format === 'json_schema' || forcedTool) throw new AiError('dev_fake_cannot_answer', 422);
38
+ if (format !== undefined && !['text', 'json_object'].includes(format)) throw new AiError('ai_invalid_request', 400);
39
+ const { stream: _stream, stream_options: _streamOptions, ...content } = body;
40
+ const digest = await hash(content), id = 'dev_' + hex(digest).slice(0, 16);
41
+ const text = format === 'json_object' ? '{}' : `[dev] ${feature} ${hex(digest).slice(0, 8)}`;
42
+ const tokens = Math.ceil(encoder.encode(JSON.stringify(body)).length / 4);
43
+ let answer, usage, events;
44
+ if (operation === 'embeddings') {
45
+ const inputs = typeof body.input === 'string' || (Array.isArray(body.input) && body.input.every(x => typeof x === 'number')) ? [body.input] : body.input;
46
+ const dimensions = body.dimensions ?? 8;
47
+ if (!Array.isArray(inputs) || !inputs.length || !Number.isInteger(dimensions) || dimensions < 1 || dimensions > 4096) throw new AiError('ai_invalid_request', 400);
48
+ const vectors = await Promise.all(inputs.map(async (input, index) => {
49
+ const bytes = await hash(input);
50
+ const embedding = Array.from({ length: dimensions }, (_, i) => (bytes[i % bytes.length] - 127.5) / 127.5);
51
+ if (body.encoding_format !== undefined && !['float', 'base64'].includes(body.encoding_format)) throw new AiError('ai_invalid_request', 400);
52
+ if (body.encoding_format === 'base64') {
53
+ const binary = new Uint8Array(dimensions * 4), view = new DataView(binary.buffer);
54
+ embedding.forEach((value, i) => view.setFloat32(i * 4, value, true));
55
+ return { object: 'embedding', index, embedding: btoa(String.fromCharCode(...binary)) };
56
+ }
57
+ return { object: 'embedding', index, embedding };
58
+ }));
59
+ usage = { prompt_tokens: tokens, total_tokens: tokens };
60
+ answer = { object: 'list', model: body.model, data: vectors, usage };
61
+ } else if (operation === 'chat') {
62
+ usage = { prompt_tokens: tokens, completion_tokens: Math.ceil(text.length / 4), total_tokens: tokens + Math.ceil(text.length / 4) };
63
+ answer = { id, object: 'chat.completion', model: body.model, choices: [{ index: 0, message: { role: 'assistant', content: text }, finish_reason: 'stop' }], usage };
64
+ const split = Math.ceil(text.length / 2);
65
+ events = [text.slice(0, split), text.slice(split)].map(content => data({ id, object: 'chat.completion.chunk', model: body.model, choices: [{ index: 0, delta: { content }, finish_reason: null }] }));
66
+ events.push(data({ id, object: 'chat.completion.chunk', model: body.model, choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], usage }), 'data: [DONE]\n\n');
67
+ } else if (operation === 'responses') {
68
+ usage = { input_tokens: tokens, output_tokens: Math.ceil(text.length / 4), total_tokens: tokens + Math.ceil(text.length / 4) };
69
+ answer = { id, object: 'response', status: 'completed', model: body.model, output: [{ id: id + '_message', type: 'message', role: 'assistant', status: 'completed', content: [{ type: 'output_text', text, annotations: [] }] }], usage };
70
+ const split = Math.ceil(text.length / 2);
71
+ events = [event('response.created', { response: { ...answer, status: 'in_progress', output: [], usage: null } }), ...[text.slice(0, split), text.slice(split)].map(delta => event('response.output_text.delta', { item_id: id + '_message', output_index: 0, content_index: 0, delta })), event('response.completed', { response: answer })];
72
+ } else throw new AiError('ai_invalid_request', 400);
73
+ await capture({ feature, operation, request: body, response: answer, usage });
74
+ return body.stream && events ? sse(events, signal) : Response.json(answer, { headers });
75
+ } catch (error) {
76
+ const safe = error instanceof AiError ? error : new AiError('ai_unavailable');
77
+ await capture({ feature, operation, request: body, error: { code: safe.code, status: safe.status } });
78
+ throw safe;
79
+ }
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,10 @@
1
+ export type AiErrorCode = 'ai_disabled' | 'ai_misconfigured' | 'ai_unavailable' | 'ai_target_mismatch' | 'ai_invalid_request' | 'ai_invalid_response' | 'dev_fake_cannot_answer' | 'dev_access_denied' | 'dev_outbox_unavailable' | 'no_eligible_target' | 'field_not_allowed' | 'budget_exhausted' | 'upstream_limited' | 'upstream_unavailable' | 'upstream_auth' | 'upstream_policy_unmet' | 'upstream_uncertain' | 'upstream_error' | 'response_too_large';
2
+ export class AiError extends Error { readonly code: AiErrorCode; readonly status: number; constructor(code: AiErrorCode, status?: number); }
3
+ export interface AiConfig { enabled?: boolean; model?: string; direct?: { baseUrl?: string; apiKey?: string }; models?: { baseUrl?: string; apiKey?: string }; features?: Record<string, 'direct' | 'models'>; }
4
+ export interface AiOptions { feature?: string; signal?: AbortSignal; subject?: string; }
5
+ export interface AiRequest { model?: string; stream?: boolean; [field: string]: unknown; }
6
+ export interface AiClient { chat(body: AiRequest, options?: AiOptions): Promise<Response>; responses(body: AiRequest, options?: AiOptions): Promise<Response>; embeddings(body: AiRequest, options?: AiOptions): Promise<Response>; receipt(id: string, options?: AiOptions): Promise<Response>; }
7
+ export interface AiTransport { request(input: { operation: 'chat' | 'responses' | 'embeddings' | 'receipt'; body?: AiRequest; feature: string; signal?: AbortSignal; selected: { name: string; baseUrl: string; apiKey: string } }): Promise<Response>; }
8
+ export function createAiClient(config?: AiConfig, dependencies?: { fetch?: typeof fetch; transport?: AiTransport }): AiClient;
9
+ export function aiConfig(env?: Record<string, unknown>, features?: AiConfig['features']): AiConfig;
10
+ export function aiDiagnostics(config?: AiConfig): Array<{ feature: string; code: AiErrorCode; message: string }>;
@@ -0,0 +1,3 @@
1
+ export { createAiClient } from './client.mjs';
2
+ export { aiConfig, aiDiagnostics } from './config.mjs';
3
+ export { AiError } from './errors.mjs';
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@runeditrun/base-ai",
3
+ "private": true,
4
+ "type": "module",
5
+ "exports": { ".": "./index.mjs", "./dev": "./dev/index.mjs", "./fake": "./fake/index.mjs" }
6
+ }
@@ -0,0 +1,156 @@
1
+ # Portable container hosting
2
+
3
+ An image contains the seed's real Vite server/client build, migrations and the
4
+ shared host. Workers remains the default. Opt in with `platforms` in `deploy.json`:
5
+
6
+ ```json
7
+ {"platforms":["workers","container"],"container":{"engine":"node"}}
8
+ ```
9
+
10
+ Choose the engine for the adopting seed's runtime requirements and qualify it
11
+ at the intended memory limit. A retained-public-snapshot cache contract is
12
+ selected through `cacheContract` when the seed implements that contract. This
13
+ is not a general Durable Object adapter. A seed requiring KV or other
14
+ Cloudflare-only APIs cannot use Node.
15
+ The import boundary rejects those APIs and new Cloudflare imports outside the
16
+ platform binding/cache boundary.
17
+
18
+ The entrypoint holds a Linux kernel `flock` for the lifetime of the host. Each
19
+ installation has one local data volume and one writer. The lock inode retains
20
+ PID/heartbeat diagnostics and is never unlinked. The host applies hashed
21
+ migrations using a separately committed claim, followed by atomic SQL plus
22
+ completion. Changed or uncertain history stops startup. Cron uses Base's five
23
+ numeric-field grammar and dispatches matching handlers at minute boundaries.
24
+ Node queues retain pending/claimed/dead messages across restarts; expired claims
25
+ are delivered again. Consumers must already be idempotent. Dead jobs make health
26
+ unhealthy. `/health` reports the container platform and the image ID supplied by
27
+ the launcher. No image ID is invented when an operator starts an image manually.
28
+
29
+ `rer build --platform container` builds the image. `rer dev` and `rer preview`
30
+ with that flag use it with local settings and a persistent Docker volume named for the checkout.
31
+ SQLite stays on the Docker host's local filesystem; host-shared checkout mounts
32
+ are not used for database storage. Back up with export before deleting a volume. Build
33
+ uses the ordinary private-file custody and command admission boundary. Secrets
34
+ are provided at runtime, never baked into the image or transferred in archives.
35
+
36
+ `rer deploy --platform container --host ssh://user@host --env-file FILE` uses
37
+ strict SSH host-key checking and a Linux host with Python 3 and Docker. FILE must
38
+ be owned and private (0600); local-only keys are rejected. Settings travel as a
39
+ raw Docker env file so dollar signs and other secret bytes are not interpolated. Caddy terminates production TLS; only its
40
+ peer can supply forwarded origin/address facts. Inbound Cloudflare and forwarded
41
+ headers from other peers are discarded. `RER_SSH_IDENTITY` and
42
+ `RER_SSH_KNOWN_HOSTS` select explicit SSH files; neither key contents nor settings
43
+ are logged. Local loopback origins support a disposable SSH Docker host for
44
+ qualification. Owner hosting/account setup remains an owner step.
45
+
46
+ A seed can package its own scoped Node runner with a declaration such as:
47
+
48
+ ```json
49
+ {"engine":"workerd","runner":{"entry":"runners/src/private-runner.mjs","files":["runners/src","src/domain","src/base","scripts/runner-config.mjs"]}}
50
+ ```
51
+
52
+ The entrypoint, supporting files and `scripts/runner-config.mjs` belong to the
53
+ adopting seed; replace these example paths with its actual runner dependencies.
54
+ Provide the configuration names that runner reads (for example,
55
+ `APP_CONTROL_ORIGIN` and `APP_RUNNER_KEY` if the adopter defines them). The host
56
+ supervises the process; an unexpected runner exit stops the host so Compose can
57
+ restart the complete unit. The runner owns its transport's public-address
58
+ validation and TLS behavior.
59
+
60
+ Local container export/import uses the same Base archive service and exclusion
61
+ policy as Workers. Stop the serving process first: one-shot transfers acquire
62
+ the same volume lock. `rer archive diff BEFORE AFTER` verifies both entire
63
+ archives, then reports added/removed/changed row keys, objects and payloads.
64
+ It never merges writes and does not print row values.
65
+
66
+ ## Qualification
67
+
68
+ `tests/conformance.mjs SEED SOURCE RESULTS` runs the same native public journey
69
+ against local Workers and the selected container image. It uses real migrations,
70
+ D1, object storage, queues and handlers. Test-only helpers are mounted separately;
71
+ the only fakes are paid mail/network edges. It checks ingress, health, assets,
72
+ SIGKILL recovery, exclusive volume custody, same-volume upgrades and public-only
73
+ egress. Run with the host's Docker socket and an admitted machine budget.
74
+
75
+ `tests/differential.mjs` compares real local Workers D1 with Node SQLite. Run it
76
+ on Node 26.8.1 (the image runtime), with Base's pinned Miniflare dependency.
77
+ `node --test modules/container/tests/command.test.mjs` covers the public archive
78
+ command. Product source paths are explicit inputs; this suite never downloads
79
+ private products, opens provider accounts or performs a provider deployment.
80
+
81
+ ### SSH installation custody and ingress bindings
82
+
83
+ The remote Linux account requires Python 3, Docker, and permission to use its
84
+ Docker daemon. HTTPS deployment acquires `caddy:2.10.2` from its standard registry
85
+ if absent, then pins its resolved image ID before shutting down anything. Preload
86
+ that image on isolated hosts. `--image IMAGE` deploys an
87
+ already built local image; the CLI resolves it once and transfers/runs that exact
88
+ image ID, so moving a shared tag cannot substitute another image.
89
+
90
+ The installation key hashes the full project and environment as a structured
91
+ pair. Every app, Caddy container, data/config volume and ingress network carries
92
+ that identity plus the complete project/environment and resource role labels.
93
+ Missing, mismatched or unexpected resources are refused before replacement;
94
+ legacy last-segment namespaces are never taken over. Same-identity redeployment
95
+ preserves its volumes. There is no remote `deploy --stop` command. Internal
96
+ replacement verifies labels and stops/removes only the inspected container IDs;
97
+ it never executes stored Compose files or deletes volumes.
98
+
99
+ Remote settings live below the account's `.rer-host/INSTALLATION/` in private,
100
+ immutable generations. The account home must be owned by the login UID. System
101
+ ancestors may be owned by that UID or root, and must not be writable by group or
102
+ others; links are refused throughout. Installation directories/files must be
103
+ owned by the UID with no group/other permissions. Files must be regular with one
104
+ hard link. Pre-existing unmarked directories, unsafe paths and unexpected files
105
+ are refused without chmod, truncation or adoption. Exclusive dirfd-relative
106
+ writes create a complete generation; one atomic manifest replacement commits it
107
+ before shutdown. Interrupted staging leaves the previous generation intact.
108
+ A private account lock serializes deployments. Before shutdown the CLI checks
109
+ that every ancestor still names the original owned directory and that the
110
+ generation still contains its exclusively written files and exact content.
111
+ This catches path substitution by the login UID as well as unsafe permissions.
112
+ Failure cleanup and removal of previous generations also require the retained
113
+ directory and file identities; replacement paths and unexpected files are left
114
+ untouched. Previous-generation identities are captured before Docker work and
115
+ checked again before shutdown. The current manifest is retained the same way
116
+ (identity and exact bytes); the commit replaces only that retained manifest, then
117
+ checks that the committed manifest is its own exclusively written file before
118
+ any shutdown. A transplanted manifest is refused and left untouched.
119
+ Verified settings go to the Docker client on stdin; verified Caddy configuration
120
+ goes through the daemon's archive API into the stopped replacement container.
121
+ Neither client nor daemon reopens a host configuration pathname after shutdown.
122
+
123
+ Multiple HTTPS installations can use distinct bindings in `deploy.json`:
124
+
125
+ ```json
126
+ {"container":{"engine":"node","ingress":{"address":"127.0.0.1","httpPort":18080,"httpsPort":18443}}}
127
+ ```
128
+
129
+ The matching CLI overrides are `--bind-address 127.0.0.1 --http-port 18080
130
+ --https-port 18443`. The defaults are `0.0.0.0:80` and `0.0.0.0:443`; duplicate
131
+ ports are rejected. These bindings apply to production Caddy ingress. Loopback
132
+ HTTP qualification binds the app to the origin's loopback port. Occupied bindings
133
+ are refused before stopping the currently installed service.
134
+
135
+ Place a shared TCP/SNI proxy in front of distinct Caddy TLS ports. Route each
136
+ hostname's TLS (including TLS-ALPN certificate challenges) and HTTP challenge
137
+ traffic to its installation. Caddy still terminates TLS and remains the app's
138
+ only trusted forwarding peer on a separate labeled network. Preserve Host/SNI
139
+ when routing. A public non-default port belongs in the origin setting; bindings
140
+ alone do not change the public origin. Certificate issuance needs a reachable
141
+ public challenge path; private tests should supply a local certificate authority
142
+ at that network edge. Changing to a proxy that terminates TLS itself requires a
143
+ separate reviewed trust/certificate configuration.
144
+
145
+ Run the real SSH safety suite with an isolated local Docker/SSH host and a built
146
+ reference image:
147
+
148
+ ```sh
149
+ RER_SSH_TEST_HOST=ssh://root@127.0.0.1:60420 \
150
+ RER_SSH_TEST_IMAGE=host1-fix-probe-app:latest \
151
+ node --test --test-concurrency=1 modules/container/tests/ssh.test.mjs modules/container/tests/ssh-tls.test.mjs
152
+ ```
153
+
154
+ Set `DOCKER_HOST`, `RER_SSH_IDENTITY` and `RER_SSH_KNOWN_HOSTS` for that fixture.
155
+ The suite uses public source and packaged CLI commands and removes only its own
156
+ labeled test resources. It never supplies real provider credentials.
@@ -0,0 +1,34 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ const value=input=>{
3
+ if(typeof input==='boolean')return Number(input);
4
+ if(input===null||typeof input==='string'||typeof input==='number'||input instanceof ArrayBuffer||ArrayBuffer.isView(input))return input instanceof ArrayBuffer?new Uint8Array(input):input;
5
+ if(Array.isArray(input)&&input.every(x=>Number.isInteger(x)&&x>=0&&x<=255))return new Uint8Array(input);
6
+ throw new Error(`D1_TYPE_ERROR: Type '${typeof input}' not supported for value '${input}'`);
7
+ };
8
+ export class D1 {
9
+ constructor(path){this.path=path;this.pending=new Map();this.next=0;this.start();}
10
+ start(){
11
+ this.worker=new Worker(new URL('./sqlite-worker.mjs',import.meta.url),{workerData:{path:this.path}});
12
+ this.worker.on('message',message=>{
13
+ const pending=this.pending.get(message.id);if(!pending)return;
14
+ if(message.started){pending.timer=setTimeout(()=>{void this.worker.terminate();this.fail(new Error('D1_ERROR: query timed out: SQLITE_INTERRUPT'));},30000);return;}
15
+ clearTimeout(pending.timer);this.pending.delete(message.id);message.error?pending.reject(new Error(message.error)):pending.resolve(message.result);
16
+ });
17
+ this.worker.on('error',cause=>this.fail(cause));this.worker.on('exit',code=>{this.worker=null;if(this.pending.size)this.fail(new Error('D1_ERROR: database worker stopped'));});
18
+ }
19
+ fail(cause){for(const pending of this.pending.values()){clearTimeout(pending.timer);pending.reject(cause);}this.pending.clear();}
20
+ call(operation,statements,sql){if(!this.worker)this.start();const id=++this.next;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({id,operation,statements,sql});});}
21
+ prepare(sql){return new Statement(this,sql);}
22
+ async batch(statements){if(statements.some(item=>item.db!==this))throw new Error('Foreign statement');const results=await this.call('batch',statements.map(({sql,args})=>({sql,args})));return results.map(({columns,rawResults,...result})=>result);}
23
+ exec(sql){return this.call('exec',undefined,sql);}
24
+ async close(){this.fail(new Error('D1 database closed'));await this.worker?.terminate();this.worker=null;}
25
+ }
26
+ class Statement {
27
+ constructor(db,sql,args=[]){this.db=db;this.sql=sql;this.args=args;}
28
+ bind(...args){return new Statement(this.db,this.sql,args.map(value));}
29
+ execute(){return this.db.call('statement',[{sql:this.sql,args:this.args}]);}
30
+ async all(){const {columns,rawResults,...result}=await this.execute();return result;}
31
+ run(){return this.all();}
32
+ async first(column){const first=(await this.execute()).results[0];if(!first)return null;if(column!==undefined){if(!Object.hasOwn(first,column))throw new Error(`D1_COLUMN_NOTFOUND: Column not found (${column})`);return first[column];}return first;}
33
+ async raw({columnNames=false}={}){const {rawResults,columns}=await this.execute();return columnNames?[columns,...rawResults]:rawResults;}
34
+ }
@@ -0,0 +1,40 @@
1
+ import { lookup } from 'node:dns/promises';
2
+ import { isIP } from 'node:net';
3
+ import { request as https } from 'node:https';
4
+ import { request as http } from 'node:http';
5
+ import { Readable } from 'node:stream';
6
+ import { isPublicAddress } from '../../integrations/core/public-address.ts';
7
+ export async function publicFetch(input, init, redirects = 0) {
8
+ const request = new Request(input, init), url = new URL(request.url), host = url.hostname.replace(/^\[|\]$/g, '');
9
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || host === 'localhost' || /\.(?:localhost|local|internal)$/.test(host)) throw new Error('Egress destination refused');
10
+ const addresses = isIP(host) ? [{ address: host, family: isIP(host) }] : await lookup(host, { all: true, verbatim: true });
11
+ if (!addresses.length || addresses.some(item => !isPublicAddress(item.address))) throw new Error('Egress destination is not public');
12
+ const pinned = addresses[0], headers = Object.fromEntries(request.headers);
13
+ for (const name of ['host', 'connection', 'proxy-authorization', 'transfer-encoding', 'content-length']) delete headers[name];
14
+ const body = ['GET', 'HEAD'].includes(request.method) ? undefined : Buffer.from(await request.arrayBuffer());
15
+ if (body) headers['content-length'] = String(body.length);
16
+ const response = await new Promise((resolve, reject) => {
17
+ const outgoing = (url.protocol === 'https:' ? https : http)(url, { method: request.method, headers, agent: false, rejectUnauthorized: true,
18
+ lookup: (_host, options, done) => options.all ? done(null, [pinned]) : done(null, pinned.address, pinned.family),
19
+ }, incoming => {
20
+ const responseHeaders = new Headers();
21
+ for (let i = 0; i < incoming.rawHeaders.length; i += 2) responseHeaders.append(incoming.rawHeaders[i], incoming.rawHeaders[i + 1]);
22
+ incoming.once('end', () => request.signal.removeEventListener('abort', abort));
23
+ incoming.once('close', () => request.signal.removeEventListener('abort', abort));
24
+ resolve(new Response(request.method === 'HEAD' || [204, 304].includes(incoming.statusCode) ? null : Readable.toWeb(incoming), { status: incoming.statusCode, headers: responseHeaders }));
25
+ });
26
+ const abort = () => outgoing.destroy(new Error('Egress aborted')); request.signal.addEventListener('abort', abort, { once: true });
27
+ if (request.signal.aborted) abort();
28
+ outgoing.setTimeout(30000, () => outgoing.destroy(new Error('Egress timeout'))); outgoing.once('error', reject); outgoing.end(body);
29
+ });
30
+ if ([301, 302, 303, 307, 308].includes(response.status) && response.headers.has('location')) {
31
+ if (request.redirect === 'manual') return response;
32
+ await response.body?.cancel();
33
+ if (request.redirect === 'error' || redirects >= 10) throw new Error('Egress redirect refused');
34
+ const destination = new URL(response.headers.get('location'), url), nextHeaders = new Headers(request.headers);
35
+ if (destination.origin !== url.origin) for (const name of ['authorization', 'cookie']) nextHeaders.delete(name);
36
+ const get = response.status === 303 || ([301, 302].includes(response.status) && request.method === 'POST');
37
+ return publicFetch(destination, { method: get ? 'GET' : request.method, headers: nextHeaders, signal: request.signal, redirect: request.redirect, ...(!get && body ? { body } : {}) }, redirects + 1);
38
+ }
39
+ return response;
40
+ }
@@ -0,0 +1,47 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ function jsonBody(body,seen=new Set()){
3
+ if(body===null||typeof body==='string'||typeof body==='boolean'||(typeof body==='number'&&Number.isFinite(body)))return;
4
+ if(typeof body!=='object'||seen.has(body)||(!Array.isArray(body)&&![Object.prototype,null].includes(Object.getPrototypeOf(body))))throw new Error('Queue body must be JSON');
5
+ seen.add(body);for(const value of Object.values(body))jsonBody(value,seen);seen.delete(body);
6
+ }
7
+ export class Jobs {
8
+ constructor(sqlite, queue, { maxRetries = 3, batchSize = 10, visibilitySeconds = 60 } = {}) {
9
+ this.sqlite = sqlite; this.queue = queue; this.maxRetries = maxRetries; this.batchSize = batchSize; this.visibilitySeconds = visibilitySeconds;
10
+ sqlite.exec("CREATE TABLE IF NOT EXISTS platform_jobs (id TEXT PRIMARY KEY, queue TEXT NOT NULL, body TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('pending','claimed','dead')), attempts INTEGER NOT NULL DEFAULT 0, available_at INTEGER NOT NULL, claimed_until INTEGER, token TEXT)");
11
+ }
12
+ record(body,{delaySeconds=0,contentType='json'}={}){
13
+ jsonBody(body);if(contentType!=='json'||!Number.isInteger(delaySeconds)||delaySeconds<0)throw new Error('Unsupported queue options');return[randomUUID(),this.queue,JSON.stringify(body),Date.now()+delaySeconds*1000];
14
+ }
15
+ insert(record){this.sqlite.prepare("INSERT INTO platform_jobs(id,queue,body,state,available_at) VALUES(?,?,?,'pending',?)").run(...record);}
16
+ async send(body,options){this.insert(this.record(body,options));}
17
+ async sendBatch(messages) {const records=messages.map(message=>this.record(message.body,message));this.sqlite.exec('BEGIN');try{records.forEach(record=>this.insert(record));this.sqlite.exec('COMMIT');}catch(cause){this.sqlite.exec('ROLLBACK');throw cause;}}
18
+ async drain(consumer, bindings, execution) {
19
+ if (this.running) return; this.running = true;
20
+ try {
21
+ const now = Date.now(); this.sqlite.prepare("UPDATE platform_jobs SET state='pending',token=NULL WHERE queue=? AND state='claimed' AND claimed_until<=?").run(this.queue, now);
22
+ const rows = this.sqlite.prepare("SELECT * FROM platform_jobs WHERE queue=? AND state='pending' AND available_at<=? ORDER BY available_at LIMIT ?").all(this.queue, now, this.batchSize);
23
+ const messages = [];
24
+ for (const row of rows) {
25
+ const token = randomUUID(); const claim = this.sqlite.prepare("UPDATE platform_jobs SET state='claimed',token=?,attempts=attempts+1,claimed_until=? WHERE id=? AND state='pending'").run(token, now + this.visibilitySeconds * 1000, row.id);
26
+ if (!claim.changes) continue;
27
+ let disposition;
28
+ const message = { id: row.id, timestamp: new Date(row.available_at), body: JSON.parse(row.body), attempts: row.attempts + 1, ack() { disposition = 'ack'; }, retry(options = {}) { disposition = options; } };
29
+ messages.push({ row, token, message, disposition: () => disposition });
30
+ }
31
+ if (!messages.length) return;
32
+ let failed = false;
33
+ try { await consumer({ queue: this.queue, messages: messages.map(x => x.message), ackAll() { messages.forEach(x => x.message.ack()); }, retryAll(options) { messages.forEach(x => x.message.retry(options)); } }, bindings, execution); }
34
+ catch { failed = true; }
35
+ for (const item of messages) {
36
+ const disposition = item.disposition();
37
+ if (disposition === 'ack' || (!failed && disposition === undefined)) this.sqlite.prepare('DELETE FROM platform_jobs WHERE id=? AND token=?').run(item.row.id, item.token);
38
+ else {
39
+ const dead = item.message.attempts > this.maxRetries;
40
+ const delay = disposition?.delaySeconds ?? Math.min(3600, 2 ** item.message.attempts);
41
+ this.sqlite.prepare('UPDATE platform_jobs SET state=?,available_at=?,claimed_until=NULL,token=NULL WHERE id=? AND token=?').run(dead ? 'dead' : 'pending', Date.now() + delay * 1000, item.row.id, item.token);
42
+ }
43
+ }
44
+ } finally { this.running = false; }
45
+ }
46
+ dead() { return this.sqlite.prepare("SELECT count(*) AS n FROM platform_jobs WHERE queue=? AND state='dead'").get(this.queue).n; }
47
+ }
@@ -0,0 +1,84 @@
1
+ import { mkdir, readFile, open, rename, readdir, unlink } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex');
5
+ const bytes = async body => body === null ? Buffer.alloc(0) : typeof body === 'string' ? Buffer.from(body) : body instanceof Blob ? Buffer.from(await body.arrayBuffer()) : body?.getReader ? Buffer.from(await new Response(body).arrayBuffer()) : Buffer.from(body);
6
+ export class Objects {
7
+ constructor(sqlite, directory, bucket) {
8
+ this.sqlite = sqlite; this.directory = directory; this.bucket = bucket;
9
+ sqlite.exec('CREATE TABLE IF NOT EXISTS platform_objects(bucket TEXT NOT NULL,key TEXT NOT NULL,file TEXT NOT NULL,size INTEGER NOT NULL,etag TEXT NOT NULL,uploaded TEXT NOT NULL,metadata TEXT NOT NULL,PRIMARY KEY(bucket,key)); CREATE TABLE IF NOT EXISTS platform_multipart(id TEXT PRIMARY KEY,bucket TEXT NOT NULL,key TEXT NOT NULL,metadata TEXT NOT NULL); CREATE TABLE IF NOT EXISTS platform_parts(id TEXT NOT NULL,part INTEGER NOT NULL,file TEXT NOT NULL,etag TEXT NOT NULL,PRIMARY KEY(id,part))');
10
+ }
11
+ async recover() {
12
+ await mkdir(this.directory, { recursive: true });
13
+ const live = new Set([...this.sqlite.prepare('SELECT file FROM platform_objects').all(), ...this.sqlite.prepare('SELECT file FROM platform_parts').all()].map(row => row.file));
14
+ for (const file of await readdir(this.directory)) if (!live.has(file)) await unlink(join(this.directory, file));
15
+ }
16
+ async writeImmutable(data) {
17
+ await mkdir(this.directory,{recursive:true});const file=hash(data),temporary=join(this.directory,randomUUID()+'.partial');
18
+ const handle=await open(temporary,'wx',0o600);
19
+ try{await handle.writeFile(data);await handle.sync();}finally{await handle.close();}
20
+ await rename(temporary,join(this.directory,file));const directory=await open(this.directory,'r');try{await directory.sync();}finally{await directory.close();}
21
+ return file;
22
+ }
23
+ record(key) { return this.sqlite.prepare('SELECT * FROM platform_objects WHERE bucket=? AND key=?').get(this.bucket, key); }
24
+ view(row, data) {
25
+ const metadata = JSON.parse(row.metadata);if(metadata.httpMetadata?.cacheExpiry)metadata.httpMetadata.cacheExpiry=new Date(metadata.httpMetadata.cacheExpiry);
26
+ const object = { key: row.key, version: row.file, size: row.size, etag: row.etag, httpEtag: `"${row.etag}"`, uploaded: new Date(row.uploaded), httpMetadata: metadata.httpMetadata ?? {}, customMetadata: metadata.customMetadata ?? {}, checksums: { sha256: Buffer.from(row.file, 'hex').buffer }, storageClass: 'Standard', writeHttpMetadata(headers) { for (const [key, value] of Object.entries(this.httpMetadata)) { const name = { contentType: 'content-type', contentLanguage: 'content-language', contentDisposition: 'content-disposition', contentEncoding: 'content-encoding', cacheControl: 'cache-control', cacheExpiry: 'expires' }[key]; if (name) headers.set(name, value instanceof Date ? value.toUTCString() : value); } } };
27
+ object.checksums.sha256=Uint8Array.from(Buffer.from(row.file,'hex')).buffer;
28
+ if (data !== undefined) {const response=new Response(data);Object.assign(object,{body:response.body,arrayBuffer:()=>response.arrayBuffer(),text:()=>response.text(),json:()=>response.json(),blob:()=>response.blob()});Object.defineProperty(object,'bodyUsed',{enumerable:true,get:()=>response.bodyUsed});}
29
+ return object;
30
+ }
31
+ matches(row, condition) {
32
+ if (!condition) return true;
33
+ if (condition instanceof Headers) condition = { etagMatches: condition.get('if-match') ?? undefined, etagDoesNotMatch: condition.get('if-none-match') ?? undefined };
34
+ const equals = value => value === '*' ? Boolean(row) : Boolean(row && value.replaceAll('"', '') === row.etag);
35
+ return (condition.etagMatches === undefined || equals(condition.etagMatches)) && (condition.etagDoesNotMatch === undefined || !equals(condition.etagDoesNotMatch)) && (!condition.uploadedBefore || !row || new Date(row.uploaded) < condition.uploadedBefore) && (!condition.uploadedAfter || row && new Date(row.uploaded) > condition.uploadedAfter);
36
+ }
37
+ async head(key) { const row = this.record(key); return row ? this.view(row) : null; }
38
+ async get(key, options = {}) {
39
+ const row = this.record(key); if (!row) return null;
40
+ if (!this.matches(row, options.onlyIf)) return this.view(row);
41
+ let data = await readFile(join(this.directory, row.file)); let range;
42
+ if(options.range instanceof Headers){const value=options.range.get('range');const match=/^bytes=(\d*)-(\d*)$/.exec(value??'');if(match)options={...options,range:match[1]?{offset:Number(match[1]),...(match[2]?{length:Number(match[2])-Number(match[1])+1}:{})}:{suffix:Number(match[2])}};else options={...options,range:undefined};}
43
+ if (options.range) {
44
+ const offset = options.range.suffix ? Math.max(0, data.length - options.range.suffix) : options.range.offset ?? 0;
45
+ const length = options.range.length ?? data.length - offset; data = data.subarray(offset, offset + length); range = { offset, length: data.length };
46
+ }
47
+ return Object.assign(this.view(row,data),range?{range}:{});
48
+ }
49
+ async put(key, body, options = {}) {
50
+ const data = await bytes(body), file = await this.writeImmutable(data);
51
+ // Immutable files precede the committed pointer. A crash never leaves a pointer to unfinished bytes.
52
+ this.sqlite.exec('BEGIN IMMEDIATE');
53
+ try {
54
+ const old = this.record(key); if (!this.matches(old, options.onlyIf)) { this.sqlite.exec('ROLLBACK'); return null; }
55
+ const metadata = JSON.stringify({ httpMetadata: options.httpMetadata ?? {}, customMetadata: options.customMetadata ?? {} }), etag = createHash('md5').update(data).digest('hex');
56
+ this.sqlite.prepare('INSERT OR REPLACE INTO platform_objects VALUES(?,?,?,?,?,?,?)').run(this.bucket, key, file, data.length, etag, new Date().toISOString(), metadata);
57
+ this.sqlite.exec('COMMIT'); return this.head(key);
58
+ } catch (cause) { this.sqlite.exec('ROLLBACK'); throw cause; }
59
+ }
60
+ async delete(keys) { for (const key of Array.isArray(keys) ? keys : [keys]) this.sqlite.prepare('DELETE FROM platform_objects WHERE bucket=? AND key=?').run(this.bucket, key); }
61
+ async list({ prefix = '', cursor, limit = 1000, delimiter, startAfter = '' } = {}) {
62
+ const after = cursor ? Buffer.from(cursor, 'base64url').toString() : startAfter;
63
+ const rows = this.sqlite.prepare('SELECT * FROM platform_objects WHERE bucket=? AND key>? ORDER BY key').all(this.bucket, after).filter(row => row.key.startsWith(prefix));
64
+ const objects = [], delimitedPrefixes = new Set(); let last, consumed = 0;
65
+ for (const row of rows) {
66
+ const tail = row.key.slice(prefix.length), separator = delimiter ? tail.indexOf(delimiter) : -1;
67
+ const group=separator>=0?prefix+tail.slice(0,separator+delimiter.length):null;
68
+ if (objects.length + delimitedPrefixes.size >= limit && !delimitedPrefixes.has(group)) break;
69
+ last = row.key; consumed++;
70
+ if (group) delimitedPrefixes.add(group); else objects.push(this.view(row));
71
+ }
72
+ const truncated = consumed < rows.length;
73
+ return { objects, delimitedPrefixes: [...delimitedPrefixes], truncated, ...(truncated ? { cursor: Buffer.from(last).toString('base64url') } : {}) };
74
+ }
75
+ async createMultipartUpload(key, options = {}) { const uploadId = randomUUID(); this.sqlite.prepare('INSERT INTO platform_multipart VALUES(?,?,?,?)').run(uploadId, this.bucket, key, JSON.stringify(options)); return this.resumeMultipartUpload(key, uploadId); }
76
+ resumeMultipartUpload(key, uploadId) {
77
+ const check = () => { const row = this.sqlite.prepare('SELECT * FROM platform_multipart WHERE id=? AND bucket=? AND key=?').get(uploadId, this.bucket, key); if (!row) throw new Error('Multipart upload missing'); return row; };
78
+ return { key, uploadId,
79
+ uploadPart: async (partNumber, body) => { check(); if (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > 10000) throw new Error('Invalid multipart part'); const data = await bytes(body), file = await this.writeImmutable(data), etag = hash(data); this.sqlite.prepare('INSERT OR REPLACE INTO platform_parts VALUES(?,?,?,?)').run(uploadId, partNumber, file, etag); return { partNumber, etag }; },
80
+ abort: async () => { check(); this.sqlite.prepare('DELETE FROM platform_parts WHERE id=?').run(uploadId); this.sqlite.prepare('DELETE FROM platform_multipart WHERE id=?').run(uploadId); },
81
+ complete: async parts => { const upload = check(); const data = []; let previous = 0; for (const part of parts) { if (part.partNumber <= previous) throw new Error('Multipart order'); previous = part.partNumber; const row = this.sqlite.prepare('SELECT * FROM platform_parts WHERE id=? AND part=? AND etag=?').get(uploadId, part.partNumber, part.etag); if (!row) throw new Error('Multipart part missing'); data.push(await readFile(join(this.directory, row.file))); } const result = await this.put(key, Buffer.concat(data), JSON.parse(upload.metadata)); this.sqlite.prepare('DELETE FROM platform_parts WHERE id=?').run(uploadId); this.sqlite.prepare('DELETE FROM platform_multipart WHERE id=?').run(uploadId); return result; },
82
+ };
83
+ }
84
+ }