@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,74 @@
1
+ import type { SqlDatabase, SqlStatement } from './database.ts';
2
+ import type { Attempt, Delivery, Envelope, Outcome } from './types.ts';
3
+ import { assertLabel, randomId } from './crypto.ts';
4
+
5
+ export class IntegrationsRepository {
6
+ readonly namespace: string;
7
+ readonly sql: SqlDatabase;
8
+ constructor(sql: SqlDatabase, namespace: string) { assertLabel(namespace, 'namespace'); this.sql = sql; this.namespace = namespace; }
9
+ async query(sql: string, params: (string | number | null)[] = []) {
10
+ return (await this.sql.batch([{ sql, params: [this.namespace, ...params] }]))[0]!;
11
+ }
12
+ async insert(delivery: Delivery) {
13
+ await this.query(`INSERT INTO integration_deliveries(namespace,id,fingerprint,destination_hash,envelope,state,created_at,next_at,max_attempts,retry_delays)
14
+ VALUES(?,?,?,?,?,'queued',?,?,?,?) ON CONFLICT(namespace,id) DO NOTHING`,
15
+ [delivery.id, delivery.fingerprint, delivery.destinationHash, JSON.stringify(pickEnvelope(delivery)), delivery.createdAt, delivery.nextAt, delivery.maxAttempts, JSON.stringify(delivery.retryDelaysMs)]);
16
+ const current = (await this.get(delivery.id))!;
17
+ if (current.fingerprint !== delivery.fingerprint) throw new Error('Delivery identity conflict');
18
+ return current;
19
+ }
20
+ async get(id: string): Promise<Delivery | null> {
21
+ const row = (await this.query('SELECT * FROM integration_deliveries WHERE namespace=? AND id=?', [id]))[0];
22
+ return row ? { ...JSON.parse(String(row.envelope)), fingerprint: row.fingerprint, destinationHash: row.destination_hash, state: row.state,
23
+ attempts: row.attempts, createdAt: row.created_at, nextAt: row.next_at, claim: row.claim, claimUntil: row.claim_until,
24
+ maxAttempts: row.max_attempts, retryDelaysMs: JSON.parse(String(row.retry_delays)) } as Delivery : null;
25
+ }
26
+ async due(now: number, limit = 100) {
27
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) throw new Error('Invalid delivery batch limit');
28
+ return (await this.query("SELECT id FROM integration_deliveries WHERE namespace=? AND state='queued' AND next_at<=? AND attempts<max_attempts ORDER BY next_at,id LIMIT ?", [now, limit])).map(row => String(row.id));
29
+ }
30
+ async claim(id: string, now: number, leaseMs: number) {
31
+ const claim = randomId();
32
+ const statements: SqlStatement[] = [
33
+ { sql: `UPDATE integration_deliveries SET state='sending', attempts=attempts+1,claim=?,claim_until=? WHERE namespace=? AND id=? AND state='queued' AND next_at<=? AND attempts<max_attempts RETURNING id`, params: [claim, now + leaseMs, this.namespace, id, now] },
34
+ { sql: `INSERT INTO integration_attempts(namespace,id,delivery_id,number,started_at) SELECT namespace,claim,id,attempts,? FROM integration_deliveries WHERE namespace=? AND id=? AND claim=?`, params: [now, this.namespace, id, claim] },
35
+ ];
36
+ const result = await this.sql.batch(statements);
37
+ return result[0]!.length ? claim : null;
38
+ }
39
+ async finish(id: string, claim: string, outcome: Outcome, now: number, nextAt: number | null) {
40
+ const state = outcome.kind === 'retryable' ? (nextAt === null ? 'rejected' : 'queued') : outcome.kind;
41
+ const result = await this.sql.batch([
42
+ { sql: `UPDATE integration_attempts SET finished_at=?,outcome=? WHERE namespace=? AND id=? AND finished_at IS NULL AND EXISTS(SELECT 1 FROM integration_deliveries d WHERE d.namespace=? AND d.id=? AND d.state='sending' AND d.claim=? AND d.claim_until>?)`, params: [now, JSON.stringify(outcome), this.namespace, claim, this.namespace, id, claim, now] },
43
+ { sql: `UPDATE integration_deliveries SET state=?,claim=NULL,claim_until=NULL,next_at=? WHERE namespace=? AND id=? AND state='sending' AND claim=? AND claim_until>? RETURNING id`, params: [state, nextAt ?? now, this.namespace, id, claim, now] },
44
+ ]);
45
+ return result[1]!.length > 0;
46
+ }
47
+ async recoverExpired(now: number) {
48
+ const outcome = JSON.stringify({ kind: 'uncertain', code: 'claim_expired' });
49
+ const result = await this.sql.batch([
50
+ { sql: `UPDATE integration_attempts SET finished_at=?,outcome=? WHERE namespace=? AND finished_at IS NULL AND id IN (SELECT claim FROM integration_deliveries WHERE namespace=? AND state='sending' AND claim_until<=?)`, params: [now, outcome, this.namespace, this.namespace, now] },
51
+ { sql: `UPDATE integration_deliveries SET state='uncertain',claim=NULL,claim_until=NULL WHERE namespace=? AND state='sending' AND claim_until<=? RETURNING id`, params: [this.namespace, now] },
52
+ ]);
53
+ return result[1]!.map(row => String(row.id));
54
+ }
55
+ async log(id: string): Promise<Attempt[]> {
56
+ return (await this.query('SELECT * FROM integration_attempts WHERE namespace=? AND delivery_id=? ORDER BY number', [id])).map(r => ({ id: String(r.id), deliveryId: String(r.delivery_id), number: Number(r.number), startedAt: Number(r.started_at), finishedAt: r.finished_at === null ? null : Number(r.finished_at), outcome: r.outcome ? JSON.parse(String(r.outcome)) : null }));
57
+ }
58
+ async resolve(id: string, outcome: Outcome, now: number) {
59
+ const rows = await this.sql.batch([
60
+ { sql: `INSERT INTO integration_resolutions(namespace,id,delivery_id,at,outcome) SELECT namespace,?,id,?,? FROM integration_deliveries WHERE namespace=? AND id=? AND state='uncertain'`, params: [randomId(), now, JSON.stringify(outcome), this.namespace, id] },
61
+ { sql: `UPDATE integration_deliveries SET state=? WHERE namespace=? AND id=? AND state='uncertain' RETURNING id`, params: [outcome.kind, this.namespace, id] },
62
+ ]);
63
+ if (!rows[1]!.length) throw new Error('Delivery is not uncertain');
64
+ }
65
+ async resolutions(id: string) { return this.query('SELECT at,outcome FROM integration_resolutions WHERE namespace=? AND delivery_id=? ORDER BY at', [id]); }
66
+ async acceptReplay(input: { envelope: Envelope; fingerprint: string; receivedAt: number }) {
67
+ if (input.envelope.tenant !== this.namespace) throw new Error('Receiver namespace mismatch');
68
+ const rows = await this.query('INSERT INTO integration_replays(namespace,id,fingerprint,envelope,received_at) VALUES(?,?,?,?,?) ON CONFLICT(namespace,id) DO NOTHING RETURNING id', [input.envelope.id, input.fingerprint, JSON.stringify(input.envelope), input.receivedAt]);
69
+ const existing = (await this.query('SELECT fingerprint FROM integration_replays WHERE namespace=? AND id=?', [input.envelope.id]))[0]!;
70
+ if (existing.fingerprint !== input.fingerprint) throw new Error('Replay identity conflict');
71
+ return rows.length > 0;
72
+ }
73
+ }
74
+ export function pickEnvelope(e: Envelope): Envelope { return { id: e.id, tenant: e.tenant, destinationId: e.destinationId, event: e.event, protocol: e.protocol, body: e.body }; }
@@ -0,0 +1,36 @@
1
+ CREATE TABLE IF NOT EXISTS integration_deliveries (
2
+ namespace TEXT NOT NULL, id TEXT NOT NULL, fingerprint TEXT NOT NULL, destination_hash TEXT NOT NULL,
3
+ envelope TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('queued','sending','accepted','rejected','uncertain')),
4
+ attempts INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, next_at INTEGER NOT NULL,
5
+ claim TEXT, claim_until INTEGER, max_attempts INTEGER NOT NULL, retry_delays TEXT NOT NULL,
6
+ PRIMARY KEY(namespace,id)
7
+ );
8
+ CREATE INDEX IF NOT EXISTS integration_deliveries_due ON integration_deliveries(namespace,state,next_at);
9
+ CREATE TABLE IF NOT EXISTS integration_attempts (
10
+ namespace TEXT NOT NULL, id TEXT NOT NULL, delivery_id TEXT NOT NULL, number INTEGER NOT NULL,
11
+ started_at INTEGER NOT NULL, finished_at INTEGER, outcome TEXT,
12
+ PRIMARY KEY(namespace,id), UNIQUE(namespace,delivery_id,number)
13
+ );
14
+ CREATE TABLE IF NOT EXISTS integration_resolutions (
15
+ namespace TEXT NOT NULL, id TEXT NOT NULL, delivery_id TEXT NOT NULL, at INTEGER NOT NULL, outcome TEXT NOT NULL,
16
+ PRIMARY KEY(namespace,id)
17
+ );
18
+ CREATE TABLE IF NOT EXISTS integration_replays (
19
+ namespace TEXT NOT NULL, id TEXT NOT NULL, fingerprint TEXT NOT NULL, envelope TEXT NOT NULL, received_at INTEGER NOT NULL,
20
+ PRIMARY KEY(namespace,id)
21
+ );
22
+ CREATE TABLE IF NOT EXISTS integration_tokens (
23
+ namespace TEXT NOT NULL, id TEXT NOT NULL, token_hash TEXT NOT NULL, name TEXT NOT NULL, scopes TEXT NOT NULL,
24
+ created_at INTEGER NOT NULL, expires_at INTEGER, revoked_at INTEGER,
25
+ PRIMARY KEY(namespace,id), UNIQUE(namespace,token_hash)
26
+ );
27
+ CREATE TABLE IF NOT EXISTS integration_token_usage (
28
+ namespace TEXT NOT NULL, id TEXT NOT NULL, token_id TEXT, at INTEGER NOT NULL, required_scopes TEXT NOT NULL, allowed INTEGER NOT NULL,
29
+ request_id TEXT, resource TEXT,
30
+ PRIMARY KEY(namespace,id)
31
+ );
32
+ CREATE TABLE IF NOT EXISTS integration_token_events (
33
+ namespace TEXT NOT NULL, id TEXT NOT NULL, token_id TEXT NOT NULL, at INTEGER NOT NULL,
34
+ action TEXT NOT NULL, feature TEXT NOT NULL,
35
+ PRIMARY KEY(namespace,id)
36
+ );
@@ -0,0 +1,20 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import type { SqlDatabase, SqlStatement } from './database.ts';
3
+ /** Node 22.13+ SQLite. Durable files should be on a local filesystem with reliable locking. */
4
+ export class SqliteDatabase implements SqlDatabase {
5
+ private db: DatabaseSync;
6
+ constructor(path: string) {
7
+ this.db = new DatabaseSync(path);
8
+ this.db.exec('PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;');
9
+ }
10
+ migrate(sql: string) { this.db.exec(sql); }
11
+ close() { this.db.close(); }
12
+ async batch(statements: SqlStatement[]): Promise<Record<string, unknown>[][]> {
13
+ this.db.exec('BEGIN IMMEDIATE');
14
+ try {
15
+ const result = statements.map(s => this.db.prepare(s.sql).all(...(s.params ?? [])) as Record<string, unknown>[]);
16
+ this.db.exec('COMMIT');
17
+ return result;
18
+ } catch (error) { this.db.exec('ROLLBACK'); throw error; }
19
+ }
20
+ }
@@ -0,0 +1,38 @@
1
+ import { hmacBytes } from './crypto.ts';
2
+ import type { Envelope } from './types.ts';
3
+
4
+ export interface TimestampedBodySignature {
5
+ timestampHeader: string;
6
+ signatureHeader: string;
7
+ /** Omit to reuse the Base webhook signing key resolved for this preparation. */
8
+ signingKey?: (envelope: Envelope) => Promise<Uint8Array>;
9
+ }
10
+
11
+ const reservedHeaders = new Set([
12
+ 'content-type', 'webhook-id', 'webhook-tenant', 'webhook-destination',
13
+ 'webhook-event', 'webhook-timestamp', 'webhook-signature',
14
+ 'host', 'connection', 'content-length', 'transfer-encoding', 'proxy-authorization',
15
+ 'authorization', 'keep-alive', 'proxy-connection', 'te', 'trailer', 'upgrade',
16
+ ]);
17
+ const headerToken = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
18
+
19
+ export function timestampedBodySigner(options: TimestampedBodySignature) {
20
+ // Snapshot trusted configuration so later mutation cannot bypass validation.
21
+ const { timestampHeader, signatureHeader, signingKey } = options;
22
+ const names = [timestampHeader, signatureHeader];
23
+ if (names.some(name => typeof name !== 'string' || !headerToken.test(name)))
24
+ throw new Error('Invalid timestamped-body signature header name');
25
+ const normalized = names.map(name => name.toLowerCase());
26
+ if (new Set(normalized).size !== 2 || normalized.some(name => reservedHeaders.has(name)))
27
+ throw new Error('Timestamped-body signature headers must be distinct and cannot replace Base headers or transport headers');
28
+ if (signingKey !== undefined && typeof signingKey !== 'function')
29
+ throw new Error('Invalid timestamped-body signing key resolver');
30
+
31
+ return async (envelope: Envelope, timestampSeconds: string, baseKey: Uint8Array): Promise<Record<string, string>> => {
32
+ const timestamp = new Date(Number(timestampSeconds) * 1000).toISOString();
33
+ const key = signingKey === undefined ? baseKey : await signingKey(envelope);
34
+ const digest = await hmacBytes(key, `${timestamp}.${envelope.body}`);
35
+ const signature = btoa(String.fromCharCode(...digest)).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
36
+ return { [timestampHeader]: timestamp, [signatureHeader]: signature };
37
+ };
38
+ }
@@ -0,0 +1,71 @@
1
+ import { assertLabel, hex, randomId, sha256 } from './crypto.ts';
2
+ import { IntegrationsRepository } from './repository.ts';
3
+ import type { TokenAuthority, TokenMetadata, TokenRequestContext, TokenUsage } from './types.ts';
4
+ function scopes(input: readonly string[]) {
5
+ if (!Array.isArray(input) || input.length > 100) throw new Error('Invalid scopes');
6
+ for (const value of input) assertLabel(value, 'scope');
7
+ return [...new Set(input)].sort();
8
+ }
9
+ function metadata(row: Record<string, unknown>): TokenMetadata {
10
+ return { id: String(row.id), name: String(row.name), scopes: JSON.parse(String(row.scopes)), createdAt: Number(row.created_at), expiresAt: row.expires_at === null ? null : Number(row.expires_at), revokedAt: row.revoked_at === null ? null : Number(row.revoked_at) };
11
+ }
12
+ export class TokenService<Context> {
13
+ private repository: IntegrationsRepository;
14
+ private authority: TokenAuthority<Context>;
15
+ private now: () => number;
16
+ constructor(repository: IntegrationsRepository, authority: TokenAuthority<Context>, now: () => number = Date.now) { this.repository = repository; this.authority = authority; this.now = now; }
17
+ async issue(context: Context, input: { name: string; scopes: readonly string[]; expiresAt?: number }) {
18
+ const requested = scopes(input.scopes), { name, expiresAt } = input;
19
+ const grant = await this.authority.grant(context), now = this.now();
20
+ // A scope ceiling read before an await is insufficient. Require the native authority
21
+ // snapshot's durable condition, evaluated by the INSERT itself in the fixed SQL batch.
22
+ if (!grant || !grant.commitGuard || typeof grant.commitGuard.sql !== 'string' || !grant.commitGuard.sql.trim()
23
+ || !Array.isArray(grant.commitGuard.params) || grant.commitGuard.params.some(p => p !== null && typeof p !== 'string' && (typeof p !== 'number' || !Number.isFinite(p)))) throw new Error('Token grant rejected: commit guard required');
24
+ const granted = scopes(grant.scopes), guard = { sql: grant.commitGuard.sql, params: [...grant.commitGuard.params] };
25
+ if (typeof name !== 'string' || !name || name.length > 200 || /[\x00-\x1f]/.test(name) || requested.some(scope => !granted.includes(scope))) throw new Error('Token grant rejected');
26
+ if (expiresAt !== undefined && (!Number.isSafeInteger(expiresAt) || expiresAt <= now)) throw new Error('Invalid token expiry');
27
+ const id = randomId(), token = `int_${hex(crypto.getRandomValues(new Uint8Array(32)))}`;
28
+ const rows = await this.repository.sql.batch([
29
+ { sql: `INSERT INTO integration_tokens(namespace,id,token_hash,name,scopes,created_at,expires_at) SELECT ?,?,?,?,?,?,? WHERE EXISTS(${guard.sql}) RETURNING *`, params: [this.repository.namespace, id, await sha256(token), name, JSON.stringify(requested), now, expiresAt ?? null, ...guard.params] },
30
+ { sql: "INSERT INTO integration_token_events(namespace,id,token_id,at,action,feature) SELECT namespace,?,id,?,'issued','integrations.tokens' FROM integration_tokens WHERE namespace=? AND id=?", params: [randomId(), now, this.repository.namespace, id] },
31
+ ]);
32
+ if (!rows[0]!.length) throw new Error('Token grant rejected: authority changed');
33
+ return { token, credential: metadata(rows[0]![0]!) };
34
+ }
35
+ async authorize(token: string, requiredScopes: readonly string[], context: TokenRequestContext): Promise<TokenMetadata | null> {
36
+ const requestId = context?.requestId, resource = context?.resource;
37
+ assertLabel(requestId, 'request ID'); assertLabel(resource, 'resource');
38
+ const required = scopes(requiredScopes), now = this.now(), id = randomId();
39
+ const hash = await sha256(typeof token === 'string' && token.length <= 1024 ? token : '');
40
+ const ns = this.repository.namespace;
41
+ // Authorization and its usage receipt have a single DB linearization point. Never authorize from a cached token row.
42
+ const rows = await this.repository.sql.batch([
43
+ { sql: `INSERT INTO integration_token_usage(namespace,id,token_id,at,required_scopes,request_id,resource,allowed)
44
+ VALUES(?,?,(SELECT id FROM integration_tokens WHERE namespace=? AND token_hash=?),?,?,?,?,
45
+ EXISTS(SELECT 1 FROM integration_tokens t WHERE t.namespace=? AND t.token_hash=? AND t.revoked_at IS NULL AND (t.expires_at IS NULL OR t.expires_at>?)
46
+ AND NOT EXISTS(SELECT value FROM json_each(?) WHERE value NOT IN (SELECT value FROM json_each(t.scopes))))) RETURNING allowed`,
47
+ params: [ns, id, ns, hash, now, JSON.stringify(required), requestId, resource, ns, hash, now, JSON.stringify(required)] },
48
+ { sql: `SELECT t.* FROM integration_tokens t JOIN integration_token_usage u ON u.namespace=t.namespace AND u.token_id=t.id WHERE u.namespace=? AND u.id=? AND u.allowed=1`, params: [ns, id] },
49
+ ]);
50
+ return rows[1]![0] ? metadata(rows[1]![0]) : null;
51
+ }
52
+ async revoke(context: Context, id: string) {
53
+ await this.authority.administer(context, 'revoke', id);
54
+ const now = this.now(), ns = this.repository.namespace;
55
+ const rows = await this.repository.sql.batch([
56
+ { sql: "INSERT INTO integration_token_events(namespace,id,token_id,at,action,feature) SELECT namespace,?,id,?,'revoked','integrations.tokens' FROM integration_tokens WHERE namespace=? AND id=? AND revoked_at IS NULL", params: [randomId(), now, ns, id] },
57
+ { sql: 'UPDATE integration_tokens SET revoked_at=COALESCE(revoked_at,?) WHERE namespace=? AND id=? RETURNING id', params: [now, ns, id] },
58
+ ]);
59
+ if (!rows[1]!.length) throw new Error('Token not found');
60
+ return { revoked: true };
61
+ }
62
+ async list(context: Context): Promise<TokenMetadata[]> {
63
+ await this.authority.administer(context, 'list');
64
+ return (await this.repository.query('SELECT * FROM integration_tokens WHERE namespace=? ORDER BY created_at,id')).map(metadata);
65
+ }
66
+ async usage(context: Context): Promise<TokenUsage[]> {
67
+ await this.authority.administer(context, 'usage');
68
+ return (await this.repository.query('SELECT * FROM integration_token_usage WHERE namespace=? ORDER BY at,id')).map(r => ({ id: String(r.id), tokenId: r.token_id === null ? null : String(r.token_id), at: Number(r.at), requiredScopes: JSON.parse(String(r.required_scopes)), allowed: r.allowed === 1,
69
+ requestId: r.request_id === null ? null : String(r.request_id), resource: r.resource === null ? null : String(r.resource) }));
70
+ }
71
+ }
@@ -0,0 +1,52 @@
1
+ export type DeliveryState = 'queued' | 'sending' | 'accepted' | 'rejected' | 'uncertain';
2
+ export interface Envelope {
3
+ id: string; tenant: string; destinationId: string; event: string; protocol: string;
4
+ /** Exact UTF-8 bytes to send; never reserialize during a retry. */
5
+ body: string;
6
+ }
7
+ export interface Delivery extends Envelope {
8
+ fingerprint: string; destinationHash: string; state: DeliveryState; attempts: number;
9
+ createdAt: number; nextAt: number; claim: string | null; claimUntil: number | null;
10
+ maxAttempts: number; retryDelaysMs: number[];
11
+ }
12
+ export interface WireRequest { url: string; headers: Record<string, string>; body: string }
13
+ export interface WireResponse { status: number; headers: Record<string, string>; body: string }
14
+ export interface Outcome {
15
+ kind: 'accepted' | 'rejected' | 'retryable' | 'uncertain';
16
+ /** Fixed safe machine code, not raw response/error/URL/token. */
17
+ code: string; status?: number; retryAfterMs?: number;
18
+ }
19
+ export interface DeliveryProtocol {
20
+ id: string;
21
+ /** Trusted configuration lookup. Destination IDs must identify immutable versions. */
22
+ prepare(envelope: Envelope, now: number): Promise<WireRequest>;
23
+ classify(response: WireResponse, now: number): Outcome;
24
+ }
25
+ /** Security boundary: implementation MUST pin validated public DNS resolution to the connection,
26
+ * reject private/reserved IPs, require verified TLS and never follow redirects. Use NodePinnedTransport
27
+ * or a deployment-owned egress proxy that enforces those guarantees. Ordinary fetch is NOT sufficient. */
28
+ export interface TrustedTransport { send(request: WireRequest): Promise<WireResponse> }
29
+ export interface Attempt { id: string; deliveryId: string; number: number; startedAt: number; finishedAt: number | null; outcome: Outcome | null }
30
+ export interface TokenMetadata { id: string; name: string; scopes: string[]; createdAt: number; expiresAt: number | null; revokedAt: number | null }
31
+ /** Safe application identifiers supplied by the server handling the concrete request.
32
+ * Do not pass bearer tokens, secret URLs, query strings or request bodies. */
33
+ export interface TokenRequestContext { requestId: string; resource: string }
34
+ export interface TokenUsage {
35
+ id: string; tokenId: string | null; at: number; requiredScopes: string[]; allowed: boolean;
36
+ /** Null only for historical receipts created before request context was required. */
37
+ requestId: string | null; resource: string | null;
38
+ }
39
+ export interface TokenGrant {
40
+ scopes: readonly string[];
41
+ /** Trusted SELECT returning a row only while the authenticated issuer and the entire
42
+ * grant snapshot remain authorized. Bind namespace, issuer identity and the durable
43
+ * revision(s) read to derive scopes. Runs inside the token INSERT transaction on the
44
+ * repository database; all authority changes must update those rows/revisions there.
45
+ * Never use a constant SELECT or caller-provided SQL. No preflight-only grants. */
46
+ commitGuard: { sql: string; params: readonly (string | number | null)[] };
47
+ }
48
+ /** The consumer authenticates context on the server; never pass caller-supplied roles as authority. */
49
+ export interface TokenAuthority<Context> {
50
+ grant(context: Context): Promise<TokenGrant>;
51
+ administer(context: Context, action: 'revoke' | 'list' | 'usage', tokenId?: string): Promise<void>;
52
+ }
@@ -0,0 +1,5 @@
1
+ -- Unnumbered expansion for consumers that applied the pre-v2 schema proposal.
2
+ -- Apply exactly once via the consumer's migration ledger, not on fresh v2 installs.
3
+ -- Preserve historical provenance as NULL; the v2 API requires context for every new use.
4
+ ALTER TABLE integration_token_usage ADD COLUMN request_id TEXT;
5
+ ALTER TABLE integration_token_usage ADD COLUMN resource TEXT;
@@ -0,0 +1,73 @@
1
+ import { assertLabel, equalSignature, hmac, sha256 } from './crypto.ts';
2
+ import type { DeliveryProtocol, Envelope, WireResponse } from './types.ts';
3
+ import { timestampedBodySigner } from './timestamped-body-signature.ts';
4
+ import type { TimestampedBodySignature } from './timestamped-body-signature.ts';
5
+ export type { TimestampedBodySignature } from './timestamped-body-signature.ts';
6
+ const wireData = (e: Envelope, timestamp: string) => JSON.stringify(['integrations.webhook.v1', e.tenant, e.destinationId, e.event, e.id, timestamp, e.body]);
7
+ function exactBody(input: string | Uint8Array): string {
8
+ if (typeof input !== 'string' && !(input instanceof Uint8Array)) throw new Error('Webhook body must be a string or raw Uint8Array');
9
+ // Fatal decoding rejects replacement; ignoreBOM preserves a leading U+FEFF as
10
+ // signed content instead of silently removing its original UTF-8 bytes.
11
+ const body = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(
12
+ typeof input === 'string' ? new TextEncoder().encode(input) : input,
13
+ );
14
+ if (typeof input === 'string' && body !== input) throw new Error('Webhook body must be valid UTF-8');
15
+ return body;
16
+ }
17
+ export function retryAfter(response: WireResponse, now: number): number | undefined {
18
+ const value = response.headers['retry-after'];
19
+ if (!value) return undefined;
20
+ if (/^\d+$/.test(value)) return Number(value) * 1000;
21
+ const parsed = Date.parse(value); return Number.isFinite(parsed) ? Math.max(0, parsed - now) : undefined;
22
+ }
23
+ export function createWebhookProtocol(options: {
24
+ resolveDestination: (destinationId: string) => Promise<string>;
25
+ signingKey: (envelope: Envelope) => Promise<Uint8Array>;
26
+ timestampedBodySignature?: TimestampedBodySignature;
27
+ }): DeliveryProtocol {
28
+ const signBody = options.timestampedBodySignature === undefined ? undefined : timestampedBodySigner(options.timestampedBodySignature);
29
+ return {
30
+ id: 'webhook',
31
+ async prepare(envelope, now) {
32
+ const timestamp = String(Math.floor(now / 1000));
33
+ const url = await options.resolveDestination(envelope.destinationId);
34
+ const resolvedKey = await options.signingKey(envelope);
35
+ if (!(resolvedKey instanceof Uint8Array) || resolvedKey.length < 32) throw new Error('HMAC key must contain at least 32 bytes');
36
+ // Own one snapshot before asynchronous signing so both formats use the
37
+ // same bytes even if the resolver's shared key rotates during preparation.
38
+ const key = new Uint8Array(resolvedKey);
39
+ const headers = {
40
+ 'content-type': 'application/json', 'webhook-id': envelope.id,
41
+ 'webhook-tenant': envelope.tenant, 'webhook-destination': envelope.destinationId, 'webhook-event': envelope.event,
42
+ 'webhook-timestamp': timestamp, 'webhook-signature': `v1=${await hmac(key, wireData(envelope, timestamp))}`,
43
+ };
44
+ if (!signBody) return { url, body: envelope.body, headers };
45
+ exactBody(envelope.body);
46
+ return { url, body: envelope.body, headers: { ...headers, ...await signBody(envelope, timestamp, key) } };
47
+ },
48
+ classify(response, now) {
49
+ const status = response.status;
50
+ if (status >= 200 && status < 300) return { kind: 'accepted', code: 'webhook_accepted', status };
51
+ if (status === 429) return { kind: 'retryable', code: 'webhook_rate_limited', status, retryAfterMs: retryAfter(response, now) };
52
+ if (status >= 400 && status < 500 && status !== 408) return { kind: 'rejected', code: 'webhook_rejected', status };
53
+ return { kind: 'uncertain', code: 'webhook_response_uncertain', status };
54
+ },
55
+ };
56
+ }
57
+ export async function verifyWebhook(options: {
58
+ /** Receivers must pass original bytes (e.g. new Uint8Array(await request.arrayBuffer())).
59
+ * Strings are for already-owned canonical text, never request.text() or another lossy decode. */
60
+ body: string | Uint8Array; headers: Headers | Record<string, string>; key: Uint8Array;
61
+ tenant: string; destinationId: string; now?: number; toleranceMs?: number;
62
+ }) {
63
+ const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers);
64
+ const read = (name: string) => { const value = headers.get(name); if (!value) throw new Error('Missing webhook header'); return value; };
65
+ const envelope: Envelope = { id: read('webhook-id'), tenant: read('webhook-tenant'), destinationId: read('webhook-destination'), event: read('webhook-event'), protocol: 'webhook', body: exactBody(options.body) };
66
+ for (const value of [envelope.id, envelope.tenant, envelope.destinationId, envelope.event]) assertLabel(value);
67
+ if (envelope.tenant !== options.tenant || envelope.destinationId !== options.destinationId) throw new Error('Webhook audience mismatch');
68
+ const timestamp = read('webhook-timestamp'), now = options.now ?? Date.now(), tolerance = options.toleranceMs ?? 300000;
69
+ if (!Number.isFinite(now) || !Number.isSafeInteger(tolerance) || tolerance < 1 || tolerance > 300000 || !/^\d{1,12}$/.test(timestamp) || Math.abs(now - Number(timestamp) * 1000) > tolerance) throw new Error('Webhook timestamp rejected');
70
+ const signature = read('webhook-signature');
71
+ if (!signature.startsWith('v1=') || !equalSignature(signature.slice(3), await hmac(options.key, wireData(envelope, timestamp)))) throw new Error('Webhook signature rejected');
72
+ return { envelope, fingerprint: await sha256(JSON.stringify(envelope)), receivedAt: now };
73
+ }
@@ -0,0 +1,173 @@
1
+ # Machine-wide development job queue
2
+
3
+ Dependency-free Node 22+ library and CLI for macOS and Linux. All repositories,
4
+ worktrees, terminals and Codex homes belonging to the same OS user coordinate in
5
+ `~/.rer/jobs`. This uses the OS home, independent of `CODEX_HOME` and the working
6
+ directory (even if a launcher overrides `HOME`). `RER_JOBS_DIR` selects another
7
+ shared local directory. Cross-account scheduling requires a common execution
8
+ identity with access to both state and private lifetime sockets; the default is
9
+ private to its owner. Use a local filesystem with atomic hard links and rename,
10
+ not a network filesystem. No daemon, install, service or provider is needed.
11
+
12
+ ```sh
13
+ node /path/to/base/modules/job-queue/cli.mjs run --kind heavy --label 'app build' -- pnpm build
14
+ node /path/to/base/modules/job-queue/cli.mjs run --kind heavy --priority owner --label 'Owner browser review' -- pnpm exec playwright test --workers=2
15
+ node /path/to/base/modules/job-queue/cli.mjs status
16
+ node /path/to/base/modules/job-queue/cli.mjs status --json
17
+ node /path/to/base/modules/job-queue/cli.mjs drain --max-wait 30 --json
18
+ ```
19
+
20
+ Run waits indefinitely unless `--max-wait SECONDS` is supplied. Expiry removes the
21
+ request, reports the admission reason, and exits **75** (`EX_TEMPFAIL`). Command
22
+ exit codes pass through, including 75; `--json` identifies queue expiry with
23
+ `timedOut: true`. SIGINT returns 130 and SIGTERM returns 143. Invalid invocation
24
+ or queue failure returns 64. Command arguments pass directly without shell
25
+ interpolation; use `sh -c` explicitly when a shell is required. Command stdout
26
+ and stderr are inherited. For `run --json`, the final control result is written
27
+ to stderr; `status` and `drain` JSON use stdout. `drain` waits for an empty queue,
28
+ reclaims dead leases and never cancels live work. It is useful in tests; ongoing
29
+ producers can keep it waiting indefinitely.
30
+
31
+ ## Admission and the owner's machine rule
32
+
33
+ Heavy work starts only when its request heads the ordered queue, occupied heavy
34
+ slots are below the configured count, the one-minute OS load is at or below the
35
+ threshold when one is set, and available memory is strictly above the floor. Defaults:
36
+
37
+ | Setting in config.json | Default | Environment override |
38
+ | --- | --- | --- |
39
+ | slots | max(1, min(3, floor(CPUs / 4), floor(RAM / 8 GiB))) | RER_JOBS_SLOTS |
40
+ | loadThreshold | none: a fleet sets one in its own environment | RER_JOBS_LOAD_THRESHOLD |
41
+ | minFreeMemoryBytes | 2 GiB | RER_JOBS_MIN_FREE_MEMORY_BYTES |
42
+ | pollMs | 3000 | RER_JOBS_POLL_MS |
43
+ | nice | 15 | RER_JOBS_NICE |
44
+ | killGraceMs | 1000 | RER_JOBS_KILL_GRACE_MS |
45
+
46
+ Put numeric settings in `$RER_JOBS_DIR/config.json` (or `~/.rer/jobs/config.json`).
47
+ Environment overrides take precedence. All clients should use the same policy;
48
+ central config avoids differing limits between sessions. Resource settings are
49
+ read when a client is created. `status` prints its effective policy, current load,
50
+ available and raw free memory, running labels/repositories/elapsed times, ordered
51
+ waiting jobs, and the head's admission reason. Available memory is what the OS
52
+ can reclaim: on macOS the free, inactive, speculative and purgeable pages from
53
+ `vm_stat` (macOS keeps little memory "free" on purpose, so `os.freemem()` reads
54
+ 0.1 to 4 GiB on an idle Mac with tens of GiB reclaimable); on Linux
55
+ `MemAvailable`. The 2 GiB floor is below what a normal idle Mac with 16 GiB or
56
+ more has available, and above the point where the compressor and swap start
57
+ working hard. A waiting job reports its queue position, the jobs ahead of it
58
+ with their repository paths, and available memory against the floor; set
59
+ `RER_JOBS_MIN_FREE_MEMORY_BYTES` (bytes, 0 for no floor) to change it. Fleets
60
+ that need a stricter floor set it in their own environment.
61
+
62
+ FIFO is the sequence assigned at atomic enqueue. Priority classes are `owner`,
63
+ `normal` (default), then `background`; FIFO holds within each class. Owner work
64
+ can overtake queued normal/background jobs, but cannot preempt running commands.
65
+ Continuous higher-priority arrivals can delay lower-priority work.
66
+
67
+ The queue replaces independent load polling with coordinated admission. It
68
+ cannot control commands launched outside it or prevent unrelated applications
69
+ raising load after admission. Keep the owner's 15 GiB disk guard and small test
70
+ worker counts. Agents should still run at most one heavy job per session.
71
+
72
+ ## State, crash recovery and cancellation
73
+
74
+ `state.json` contains monotonically assigned sequence numbers, queued requests
75
+ and running leases with PID, process start time, command, repository and label.
76
+ Each mutation replaces a complete snapshot by atomic rename. A short Lamport
77
+ bakery lock publishes fully written immutable contender identities by exclusive
78
+ hard link in `locks/`; choosing/ticket changes use atomic rename. This avoids
79
+ read-then-write admission and stale-reclaimer races against a replacement lock.
80
+ Lock entries have unique names and are removed after the transaction. Abandoned
81
+ temporary files from a hard crash are ignored; they carry no authority and may
82
+ be removed when no clients are using the directory.
83
+
84
+ Linux identities include boot ID and kernel start ticks. macOS uses UTC `ps` start
85
+ time plus a random lifetime nonce served by an owned Unix socket. The nonce
86
+ distinguishes recycled PIDs even within macOS's one-second start-time precision.
87
+ The detached supervisor has a unique process title, also checked before sending
88
+ group signals on both platforms. The lifetime sockets are private files in the OS temp
89
+ directory (short `/tmp` paths if needed). Dead lease sockets are removed on
90
+ recovery. An unavailable identity probe that cannot establish death fails closed.
91
+ If a lifetime socket resets during shutdown, the queue rechecks PID/start: proven
92
+ death is reclaimed, while an uncertain live owner stays held until the next
93
+ sample. Ordinary client shutdown therefore cannot abort another queued command.
94
+
95
+ A reservation initially belongs to its calling process. A separate detached
96
+ supervisor establishes its process group and transfers its identity into the
97
+ lease **before** receiving permission to spawn the command under `nice -n 15`.
98
+ The command and ordinary descendants inherit that group. Normal command exit,
99
+ SIGINT or SIGTERM triggers TERM to the entire group, followed by KILL after the
100
+ grace interval, then releases the slot. IPC loss tells the supervisor to stop
101
+ its group if the caller is killed. If the supervisor itself is killed, the next
102
+ queue operation kills remaining group members before reclaiming the lease.
103
+ PID reuse never authorizes killing a replacement group. POSIX keeps group IDs
104
+ reserved while old group members exist ([process ID reuse](https://pubs.opengroup.org/onlinepubs/009696699/basedefs/xbd_chap04.html)).
105
+ Node's [detached child option](https://nodejs.org/api/child_process.html#optionsdetached)
106
+ establishes the supervisor's new group and session on these platforms.
107
+
108
+ This controls descendants that remain in the command's process group. Commands
109
+ that deliberately call `setsid`/detach into another group need their own cleanup;
110
+ portable process groups do not provide sandbox containment. SIGKILL cannot run
111
+ JavaScript cleanup, so recovery occurs when another client runs or through the
112
+ surviving supervisor. Zombies are stopped processes and their OS parent/init
113
+ must reap them. No macOS or Linux guarantee requires an installed daemon.
114
+ Darwin can report `EPERM` for a zombie-only group; cleanup confirms that no live
115
+ members remain before treating this as completion. A real zombie-only process
116
+ group test covers this behavior.
117
+ Permission failures with live members, or an unavailable inspection, retain the
118
+ lease and report failure.
119
+
120
+ Never pass credentials in command arguments: the private state records argv for
121
+ lease inspection. Supply credentials through environment variables. Do not
122
+ print them in labels, commands or diagnostics.
123
+
124
+ ## Library and CLI integration
125
+
126
+ ```js
127
+ import { createQueue } from './modules/job-queue/index.mjs';
128
+ const queue = await createQueue();
129
+ try {
130
+ const result = await queue.run(['pnpm', 'verify'], {
131
+ kind: 'heavy', label: 'Project verification', cwd: projectDirectory,
132
+ // priority: 'owner', maxWait: 120, signal: abortController.signal
133
+ });
134
+ process.exitCode = result.code;
135
+ } finally {
136
+ await queue.close();
137
+ }
138
+ ```
139
+
140
+ The CLI should route the actual command array for `rer verify`, `build`, `dev`
141
+ and `preview` through `queue.run`, preserving cwd, inherited environment/stdin,
142
+ and the returned exit code. Browser suites, dependency installs and any separate
143
+ workerd/build steps use the same wrapper. Use one queue boundary around a whole
144
+ workflow; wrapping a subcommand again while its parent holds the only slot would
145
+ deadlock. Long-running dev/preview serving uses the separate managed-server cap. Only
146
+ finite preparation holds a heavy-job slot; no persistent server may outlive a
147
+ released job as its descendant. Avoid spawning independent unsupervised
148
+ servers around this wrapper. No `rer` verbs are changed by this module.
149
+
150
+ Tests inject `sampler: () => ({ load, availableMemoryBytes })` through the library;
151
+ the CLI always uses actual OS samples. Library config overrides are also useful
152
+ for isolated test directories. Call `close()` only after all operations finish.
153
+ `pnpm verify:job-queue` runs the focused suite with file concurrency one. Its
154
+ cross-process probes launch lightweight real children, not actual builds.
155
+
156
+ ## Proposed text for Codex homes' AGENTS.md
157
+
158
+ This is a proposal only; no Codex home instructions were edited:
159
+
160
+ > Queue bounded dependency installs, builds, verify/test/browser suites and
161
+ > temporary wrangler/workerd resource journeys. Run those commands through
162
+ > `node /path/to/base/modules/job-queue/cli.mjs run --kind heavy --label TEXT -- COMMAND ARGS`.
163
+ > All sessions use the same `~/.rer/jobs` (or owner-set `RER_JOBS_DIR`); never use a
164
+ > per-worktree queue. The queue admits only within slots, load <=36 and the memory
165
+ > floor, and runs commands at low priority. Let it wait; use `--max-wait 1200`
166
+ > only when a task explicitly requires a twenty-minute bounded wait. Use
167
+ > `--priority owner` only for the owner's interactive work. Keep at least 15 GiB
168
+ > disk free, one heavy job at a time per session, and few test workers. Wrap one
169
+ > boundary per workflow; nested first-party commands validate and share that
170
+ > live membership. Dev/preview queue finite preparation and use the separate
171
+ > server registry for persistent serving. Do not hold a heavy lease for an idle
172
+ > server or leave a server descended from a finished job. Stop servers when
173
+ > finished. Inspect waiting reasons with `status`; do not bypass admission.
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { createQueue } from './index.mjs';
3
+ import { waitingMessage } from './waiting.mjs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { realpathSync } from 'node:fs';
6
+
7
+ const usage = 'job-queue run --kind heavy [--label TEXT] [--priority owner|normal|background] [--max-wait SECONDS] [--json] -- <command> [args]\njob-queue status|drain [--json] [--max-wait SECONDS]';
8
+ export function parse(args) {
9
+ const verb = args.shift(), options = {};
10
+ while (args.length && args[0] !== '--') {
11
+ const flag = args.shift();
12
+ if (flag === '--json') options.json = true;
13
+ else if (['--kind', '--label', '--priority', '--max-wait'].includes(flag)) {
14
+ if (!args.length || args[0].startsWith('--')) throw Error(`Missing value for ${flag}`);
15
+ options[{ '--kind': 'kind', '--label': 'label', '--priority': 'priority', '--max-wait': 'maxWait' }[flag]] = args.shift();
16
+ } else throw Error(`Unknown option ${flag}`);
17
+ }
18
+ if (options.maxWait !== undefined) {
19
+ options.maxWait = Number(options.maxWait);
20
+ if (!Number.isFinite(options.maxWait) || options.maxWait < 0) throw Error('max-wait must be nonnegative seconds');
21
+ }
22
+ if (!['run', 'status', 'drain'].includes(verb)) throw Error(usage);
23
+ if (verb === 'run' && (args.shift() !== '--' || !args.length || options.kind !== 'heavy')) throw Error(usage);
24
+ if (verb !== 'run' && args.length) throw Error(usage);
25
+ return { verb, options, command: args };
26
+ }
27
+
28
+ function render(view) {
29
+ const elapsed = job => Math.floor((Date.now() - (job.startedAt ?? job.enqueuedAt)) / 1000);
30
+ return [`Load ${view.resources.load.toFixed(2)}; available memory ${(view.resources.availableMemoryBytes / 1024 ** 3).toFixed(2)} GiB (raw free ${(view.resources.freeMemoryBytes / 1024 ** 3).toFixed(2)} GiB) against a floor of ${(view.config.minFreeMemoryBytes / 1024 ** 3).toFixed(2)} GiB; slots ${view.running.length}/${view.config.slots}`,
31
+ ...view.running.map(job => `Running: ${job.label} | ${job.repository} | ${elapsed(job)}s`),
32
+ ...view.queued.map((job, index) => `Queued ${index + 1}: ${job.label} (${job.priority}) | ${job.repository} | ${elapsed(job)}s`),
33
+ `Admission: ${view.admissionReason}`].join('\n');
34
+ }
35
+
36
+ function shellQuote(value) {
37
+ if (process.platform === 'win32') return `"${String(value).replaceAll('"', '""')}"`;
38
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
39
+ }
40
+
41
+ async function main() {
42
+ const { verb, options, command } = parse(process.argv.slice(2));
43
+ const queue = await createQueue();
44
+ try {
45
+ let result;
46
+ if (verb === 'run') {
47
+ const statusCommand = [process.execPath, fileURLToPath(import.meta.url), 'status', '--json'].map(shellQuote).join(' ');
48
+ result = await queue.run(command, { ...options, onWaiting: (reason, details) => {
49
+ if (!options.json) console.error(`${waitingMessage(details)}\nInspect the queue with: ${statusCommand}`);
50
+ } });
51
+ if (result.timedOut) console.error(`Job queue max-wait expired: ${result.reason} (exit ${result.code})`);
52
+ if (result.error) console.error(result.error);
53
+ // Command stdout is preserved; JSON control output goes to stderr for run.
54
+ if (options.json) console.error(JSON.stringify(result));
55
+ } else {
56
+ result = verb === 'status' ? await queue.status() : await queue.drain(options);
57
+ console.log(options.json ? JSON.stringify(result) : render(result));
58
+ if (result.timedOut) console.error('Job queue drain max-wait expired (exit 75)');
59
+ }
60
+ process.exitCode = result.code ?? 0;
61
+ } finally { await queue.close(); }
62
+ }
63
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url))
64
+ main().catch(error => { console.error(error.message); process.exitCode = 64; });