@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,144 @@
1
+ # Vite SPA adoption
2
+
3
+ Keep the auth module, D1 access, sender construction, and every credential in the
4
+ Worker's server entry. The browser imports no auth module and never receives
5
+ `OWNER_EMAIL`, `SESSION_SECRET`, `SETUP_TOKEN`, `MAIL_*`, D1 bindings, or raw
6
+ confirmation tokens. Serve the SPA and API from the same canonical origin so
7
+ its HTTP-only session cookie is sent naturally.
8
+
9
+ ## Worker API
10
+
11
+ This Hono example uses the actual `createOperatorAuth` and
12
+ `createAuthHandler` entry points. `createMailSender` is the real IF-056 factory
13
+ from the selected `modules/mail/index.mjs`; production configuration must include
14
+ the provider's real server-side credentials.
15
+
16
+ ```ts
17
+ // src/server.ts
18
+ import { Hono, type Context, type Next } from 'hono'
19
+ import { env } from 'cloudflare:workers'
20
+ import { createMailSender } from '../modules/mail/index.mjs'
21
+ import { createOperatorAuth, createAuthHandler, type Session } from '../modules/operator-auth/index.mjs'
22
+
23
+ const origin = 'https://app.example.com'
24
+ // Secrets are Worker secret bindings, never source text.
25
+ const { SESSION_SECRET, SETUP_TOKEN, MAIL_API_KEY: apiKey } = env
26
+ const mailSender = env.MAIL_PROVIDER && apiKey && env.MAIL_FROM
27
+ ? createMailSender({
28
+ provider: env.MAIL_PROVIDER,
29
+ mode: 'production',
30
+ apiKey,
31
+ ...(env.MAIL_ACCOUNT_ID ? { accountId: env.MAIL_ACCOUNT_ID } : {}),
32
+ })
33
+ : undefined
34
+ const auth = createOperatorAuth({
35
+ db: env.DB,
36
+ config: {
37
+ origin,
38
+ OWNER_EMAIL: env.OWNER_EMAIL,
39
+ SESSION_SECRET,
40
+ SETUP_TOKEN,
41
+ ...(env.MAIL_FROM ? { MAIL_FROM: env.MAIL_FROM } : {}),
42
+ },
43
+ mailSender,
44
+ // This is trusted only because Cloudflare is the public Worker boundary.
45
+ clientIp: (request) => request.headers.get('CF-Connecting-IP') ?? '',
46
+ })
47
+
48
+ const handleAuth = createAuthHandler(auth)
49
+ const app = new Hono<{ Variables: { operatorSession: Session } }>()
50
+
51
+ app.all('/auth/*', async (context) => {
52
+ const response = await handleAuth(context.req.raw)
53
+ return response ?? context.notFound()
54
+ })
55
+
56
+ const protectPrivate = async (context: Context<{ Variables: { operatorSession: Session } }>, next: Next) => {
57
+ const session = await auth.getSession(context.req.raw)
58
+ if (!session) return context.json({ error: 'unauthenticated' }, 401)
59
+ context.set('operatorSession', session)
60
+ await next()
61
+ }
62
+ app.use('/api/private', protectPrivate)
63
+ app.use('/api/private/*', protectPrivate)
64
+
65
+ app.get('/api/private/profile', (context) => {
66
+ const session = context.get('operatorSession')
67
+ return context.json({ operator: { id: session.operator.id, role: session.operator.role } })
68
+ })
69
+
70
+ export default { fetch: (request: Request) => app.fetch(request, env) }
71
+ ```
72
+
73
+ Keep auth dispatch ahead of protected route matching so private unknown routes
74
+ are gated before their 404. For browser mutations, check exact Origin and
75
+ Fetch Metadata, validate input, and call `getSession(context.req.raw)` before
76
+ the product's real persistence operation.
77
+ `getSession(request)` must protect each private API request independently; a
78
+ client-side route guard or an earlier profile request does not authorize another
79
+ request.
80
+
81
+ When first-owner setup is pending, omit `mailSender` and `MAIL_FROM` from the
82
+ auth construction; bootstrap needs neither. Keep `SETUP_TOKEN` in the
83
+ server-only setup operation and invoke POST `/auth/bootstrap` there. Never embed
84
+ the token in the SPA bundle, form, or browser request. The endpoint creates no
85
+ product data, only the configured owner and its session. Remove the token binding
86
+ after verifying first-owner access.
87
+
88
+ ## SPA sign-in and private requests
89
+
90
+ The sign-in page requests a link; it does not receive one. The public response is
91
+ uniform for unknown or disallowed addresses. The browser never gets the token
92
+ from the email and cannot consume a link through GET or HEAD: the confirmation
93
+ page posts its hidden value only when the operator presses Confirm.
94
+
95
+ ```ts
96
+ export async function requestOperatorLink(email: string) {
97
+ const response = await fetch('/auth/magic-link', {
98
+ method: 'POST',
99
+ credentials: 'same-origin',
100
+ headers: { 'content-type': 'application/json' },
101
+ body: JSON.stringify({ email, returnTo: '/app' }),
102
+ })
103
+ if (response.status !== 202) throw new Error('Sign-in is unavailable; try again later.')
104
+ return 'If this address is eligible, a sign-in link will arrive shortly.'
105
+ }
106
+
107
+ export async function loadOperatorProfile() {
108
+ const response = await fetch('/api/private/profile', { credentials: 'same-origin' })
109
+ if (response.status === 401) {
110
+ window.location.assign('/signin')
111
+ return null
112
+ }
113
+ if (!response.ok) throw new Error('The operator profile could not be loaded.')
114
+ return response.json() as Promise<{ operator: { id: string; role: 'owner' | 'admin' | 'member' } }>
115
+ }
116
+ ```
117
+
118
+ The same-origin cookie is `HttpOnly`, `SameSite=Lax`, and `Secure` in production;
119
+ JavaScript does not read or write it. The auth handler enforces POST and exact
120
+ Origin for its mutations. The product's own state-changing APIs keep their
121
+ Origin/Fetch-Metadata checks too. Never use a client role or cached profile as
122
+ the server's authorization decision.
123
+
124
+ ## Real sender outcomes and OIDC
125
+
126
+ Pass the actual `createMailSender` result directly to the auth core. An
127
+ `accepted` result records provider submission only. `rejected`, `rate_limited`,
128
+ `uncertain`, and production `development` results revoke the new link and become
129
+ `503 mail_unavailable`; there is no fallback or automatic retry. Resolve an
130
+ uncertain attempt before requesting another link.
131
+
132
+ Optional OIDC remains server-side: configure one exact HTTPS issuer, use the
133
+ same `auth` and D1 for `createOidcClient`, dispatch its handler before
134
+ `createAuthHandler`, and forward every Set-Cookie header. The client constructs
135
+ its trusted admission closure from `auth.createIdentityAdmission({ issuer,
136
+ subjectEmails })`; custom auth adapters must implement the same server-only
137
+ contract. An existing issuer/subject binding may sign in again, while a
138
+ different principal cannot join an already-bound operator by presenting the same
139
+ verified email. Configure any subject-to-existing-email association from trusted
140
+ server configuration, never callback or browser input. Do not expose its
141
+ provider response, tokens or callback query string. The conformance suite's
142
+ optional provider-key-free case exercises only a first verified identity
143
+ admission into the shared native session store; the separate OIDC tests prove
144
+ the protocol.
@@ -0,0 +1,9 @@
1
+ export class AuthError extends Error {
2
+ constructor(code, status = 400) {
3
+ super(code);
4
+ this.name = 'AuthError';
5
+ this.code = code;
6
+ this.status = status;
7
+ }
8
+ }
9
+ export function deny(code = 'forbidden', status = 403) { throw new AuthError(code, status); }
@@ -0,0 +1,42 @@
1
+ import { deny } from './errors.mjs';
2
+ import { validToken } from './crypto.mjs';
3
+ export function isLoopbackHost(hostname) {
4
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
5
+ }
6
+ // Browsers keep cookies per host, not per port, so checkouts side by side on
7
+ // one loopback host would overwrite each other's cookies. A local cookie name
8
+ // therefore carries its origin's port; hosted names never change. Seeds on
9
+ // separate `<name>.localhost` hosts stay apart by host as well.
10
+ export function localCookieName(name, origin) {
11
+ const url = new URL(origin);
12
+ return `${name}_${url.port || (url.protocol === 'https:' ? '443' : '80')}`;
13
+ }
14
+ export function requireLoopback(request) {
15
+ const url = new URL(request.url);
16
+ const host = request.headers.get('host');
17
+ if (!isLoopbackHost(url.hostname) || !host || host.toLowerCase() !== url.host.toLowerCase()) deny('loopback_required');
18
+ }
19
+ export function requireOrigin(request, config) {
20
+ const url = new URL(request.url);
21
+ const host = request.headers.get('host');
22
+ if (url.origin !== config.origin || (host && host.toLowerCase() !== url.host.toLowerCase())) deny('invalid_host');
23
+ if (config.local) requireLoopback(request);
24
+ }
25
+ export function requirePost(request, config) {
26
+ requireOrigin(request, config);
27
+ if (request.method !== 'POST') deny('method_not_allowed', 405);
28
+ if (request.headers.get('origin') !== config.origin || request.headers.get('sec-fetch-site') === 'cross-site') deny('invalid_origin');
29
+ }
30
+ export function readCookie(request, config) {
31
+ const entries = (request.headers.get('cookie') || '').split(';').map((p) => p.trim()).filter((p) => p.startsWith(`${config.cookieName}=`));
32
+ if (entries.length !== 1) return null;
33
+ const value = entries[0].slice(config.cookieName.length + 1);
34
+ return validToken(value) ? value : null;
35
+ }
36
+ export function sessionCookie(token, config, maxAge = config.sessionSeconds) {
37
+ return `${config.cookieName}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}${config.secure ? '; Secure' : ''}`;
38
+ }
39
+ export function securityHeaders() {
40
+ return { 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer', 'X-Content-Type-Options': 'nosniff', 'X-Robots-Tag': 'noindex', 'Content-Security-Policy': "default-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'" };
41
+ }
42
+ export function escapeHtml(value) { return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#39;'); }
@@ -0,0 +1,105 @@
1
+ import { AuthError, deny } from '../errors.mjs';
2
+ import { normalizeEmail } from '../policy.mjs';
3
+ import { requireOrigin } from '../http.mjs';
4
+ import { accountsAdapter, person as checked } from '../accounts/adapter.mjs';
5
+ import { trustedEmail } from './principal.mjs';
6
+ import { completionHook } from './after-confirm.mjs';
7
+
8
+ // Turns a verified principal into a session through an accounts adapter.
9
+ // Protocol verification belongs to the identity method that produced the
10
+ // principal; account association belongs to closed-over server configuration.
11
+ //
12
+ // A principal is { issuer, subject, provider?, email?, emailVerified?,
13
+ // emailVerifiedAt?, amr?, associatedEmail? }. associatedEmail comes only from
14
+ // trusted configuration (subjectEmails), never from a token or a request.
15
+ export function createAdmission({ accounts: adapter, config, clock = Date.now, hooks = {} }) {
16
+ const accounts = accountsAdapter(adapter);
17
+ const afterConfirm = completionHook({ hooks, revokeSession: id => accounts.revokeSession(id) });
18
+ async function admit(request, principal, { returnTo } = {}) {
19
+ requireOrigin(request, config);
20
+ if (!principal || typeof principal.issuer !== 'string' || typeof principal.subject !== 'string' || !principal.subject || principal.subject.length > 255) deny('invalid_identity');
21
+ if (!accounts.identityEnabled || !await accounts.identityEnabled()) deny('provider_sign_in_disabled');
22
+ const associated = principal.associatedEmail === undefined ? undefined : normalizeEmail(principal.associatedEmail);
23
+ const age = principal.emailVerifiedAt;
24
+ // An unused, unverified email claim is not an asserted proof. A configured
25
+ // association still cannot override an asserted stale or malformed proof.
26
+ if (principal.emailVerified === true && age !== undefined && (!Number.isSafeInteger(age) || age > clock()+300000 || clock()-age > 30*86400000) || associated === undefined && principal.datedEmailRequired && (age === undefined || principal.emailVerified !== true)) deny('email_proof_required');
27
+ const proven = trustedEmail(principal, clock());
28
+ // A durable binding identifies the account; it does not renew email proof.
29
+ if (!proven && associated === undefined) deny('invalid_identity');
30
+ let person = checked(await accounts.findBinding(principal));
31
+ if (person) {
32
+ // A binding names one account for good: neither an association nor a
33
+ // changed provider email moves it to another existing account.
34
+ if (associated !== undefined && associated !== person.email) deny();
35
+ if (proven && proven !== person.email && await accounts.findByEmail(proven)) deny();
36
+ } else {
37
+ if (!proven && associated === undefined) deny('email_proof_required');
38
+ const email = associated ?? proven;
39
+ if (!email) deny('invalid_identity');
40
+ person = checked(await accounts.findByEmail(email));
41
+ if (!person) deny();
42
+ }
43
+ await requireRoute(person, principal.provider);
44
+ if (person.status === 'invited' && typeof accounts.acceptInvitation === 'function') {
45
+ const accepted = await accounts.acceptInvitation({ person, principal, returnTo, request });
46
+ if (!accepted) deny();
47
+ return afterConfirm(request, accepted, 'oidc');
48
+ }
49
+ if (person.status !== 'active') deny();
50
+ if (accounts.bindAndIssueSession) {
51
+ const completed=await accounts.bindAndIssueSession({person,principal,method:'oidc',returnTo,request});
52
+ if(!completed)deny();
53
+ return afterConfirm(request,completed,'oidc');
54
+ }
55
+ if (!await accounts.bind({ issuer: principal.issuer, subject: principal.subject, person, associated: associated !== undefined })) deny();
56
+ const completed = await accounts.issueSession({ person, principal, method: 'oidc', returnTo, request });
57
+ if (!completed) deny();
58
+ return afterConfirm(request, completed, 'oidc');
59
+ }
60
+ // A domain's required provider refuses every other route; owners are exempt.
61
+ async function requireRoute(person, provider) {
62
+ if (person.owner || typeof accounts.requiredProvider !== 'function') return;
63
+ const required = await accounts.requiredProvider(person.email);
64
+ if (required && required !== provider) deny('provider_required');
65
+ }
66
+ return { admit, afterConfirm, requireRoute, accounts };
67
+ }
68
+
69
+ // The original server seams, kept for existing callers: verified email may
70
+ // establish a first binding, and createIdentityAdmission snapshots an exact
71
+ // issuer's subject associations.
72
+ export function legacyAdmission({ admission, config, policyVersion }) {
73
+ async function principalOf(identity) {
74
+ // Transaction pins belong to the OIDC verifier. These server-verified seams
75
+ // instead snapshot the native app policy before admission reads authority.
76
+ const { associatedEmail: _, providerPin: _pin, providerGeneration: _generation, ...principal } = identity;
77
+ return { ...principal, ...(policyVersion ? { providerGeneration: await policyVersion() } : {}) };
78
+ }
79
+ return {
80
+ async admitVerifiedIdentity(request, identity, input) {
81
+ if (!identity || !config.externalIssuers.includes(identity.issuer) || identity.emailVerified !== true) deny('invalid_identity');
82
+ return admission.admit(request, await principalOf(identity), input);
83
+ },
84
+ createIdentityAdmission(input) {
85
+ try {
86
+ const issuer = input?.issuer;
87
+ if (!config.externalIssuers.includes(issuer)) throw new Error();
88
+ const entries = input.subjectEmails ?? {};
89
+ if (typeof entries !== 'object' || Array.isArray(entries) || entries === null) throw new Error();
90
+ const subjects = new Map(Object.entries(entries).map(([subject, email]) => {
91
+ if (!subject || subject.length > 255) throw new Error();
92
+ return [subject, normalizeEmail(email)];
93
+ }));
94
+ // Neither request input nor token claims can replace this snapshot or opt
95
+ // into association. The returned function is bound to this exact issuer.
96
+ return async (request, identity, options) => {
97
+ if (!identity || identity.issuer !== issuer) deny('invalid_identity');
98
+ const associatedEmail = subjects.get(identity.subject);
99
+ if (associatedEmail === undefined && identity.emailVerified !== true) deny('invalid_identity');
100
+ return admission.admit(request, { ...await principalOf(identity), ...(associatedEmail === undefined ? {} : { associatedEmail }) }, options);
101
+ };
102
+ } catch { throw new AuthError('invalid_configuration', 503); }
103
+ },
104
+ };
105
+ }
@@ -0,0 +1,18 @@
1
+ import { AuthError } from '../errors.mjs';
2
+ import { safeReturnTo } from '../policy.mjs';
3
+
4
+ // The owner of the session store supplies revocation; a refused sign-in must
5
+ // lose its persisted authority before its error is returned to the caller.
6
+ export function completionHook({ hooks, revokeSession }) {
7
+ return async (request, completed, method) => {
8
+ completed = { ...completed, returnTo: safeReturnTo(completed.returnTo) };
9
+ if (typeof hooks.afterConfirm !== 'function') return completed;
10
+ try {
11
+ const result = await hooks.afterConfirm({ request, operator: completed.operator, session: completed.session, method, returnTo: completed.returnTo });
12
+ return result?.returnTo === undefined ? completed : { ...completed, returnTo: safeReturnTo(result.returnTo) };
13
+ } catch (error) {
14
+ await revokeSession(completed.session.id);
15
+ throw error instanceof AuthError ? error : new AuthError('sign_in_refused', 403);
16
+ }
17
+ };
18
+ }
@@ -0,0 +1,14 @@
1
+ import { escapeHtml } from '../../http.mjs';
2
+ import { pageResponse } from '../../page.mjs';
3
+
4
+ const HEADINGS = { invite: 'Accept your invitation to', magic: 'Confirm sign-in to', step_up: "Confirm it's you in" };
5
+
6
+ export async function confirmationResponse({ branding, preview, token, method }) {
7
+ const product = escapeHtml(branding.productName);
8
+ const heading = preview ? `${HEADINGS[preview.kind]} ${product}` : 'This link is invalid or expired';
9
+ const content = preview
10
+ ? `<p>This link works once and expires in ${preview.expiresInMinutes} ${preview.expiresInMinutes === 1 ? 'minute' : 'minutes'}.</p><form method="post" action="/auth/confirm"><input type="hidden" name="token" value="${escapeHtml(token)}"><button type="submit">Confirm</button></form>`
11
+ : `<p>This link may have expired or already been used. Request a new link to continue.</p><a class="action" href="${escapeHtml(branding.signInPath)}">Request a new link</a>`;
12
+ // Native form POSTs need their Origin; never disclose the token path/query.
13
+ return pageResponse({ branding, heading, content, status: preview ? 200 : 400, method, referrer: preview ? 'strict-origin' : undefined });
14
+ }
@@ -0,0 +1,48 @@
1
+ import type { AuthConfig, AuthHooks, CompletedSession, Session, StepUpAction, StepUpConfirmed, LinkPreview, MailSender, OperatorAuth } from '../../index.mjs';
2
+ import type { NativePerson } from '../../accounts/native.mjs';
3
+ export const STEP_UP_ACTIONS: readonly StepUpAction[];
4
+ export const STEP_UP_SECONDS: number;
5
+ export interface LinkRecord { id: string; hash: string; kind: 'magic'; returnTo: string; expiresAt: number; operatorId: string; expectedVersion: number; now: number }
6
+ export interface NativeLinkPreview { kind: 'magic'; email: string; operatorId: string; version: number; expiresAt: number; returnTo: string; dev?: boolean }
7
+ export interface StepUpChallenge { id: string; hash: string; action: StepUpAction; returnTo: string; now: number; expiresAt: number }
8
+ /** Persistence is the authority boundary. Consumption + current authority check + issuance are one transaction, not separate read/write calls. */
9
+ export interface NativeProofPersistence<R extends string = string> {
10
+ limits(keys: string[], now: number, windowMs: number): Promise<number[]>;
11
+ token(record: LinkRecord): Promise<boolean>;
12
+ preview(digest: string, now: number): Promise<NativeLinkPreview | null>;
13
+ consumeLink(input: {request: Request; digest: string; person: NativePerson<R>; now: number; returnTo: string}): Promise<CompletedSession<R> | null>;
14
+ revokeLink(id: string, now: number): Promise<unknown>;
15
+ stepUpPreview(digest: string, now: number): Promise<{expiresAt: number} | null>;
16
+ stepUpChallenge(record: StepUpChallenge, actor: Session<R>): Promise<boolean>;
17
+ consumeStepUp(digest: string, actor: Session<R>, now: number): Promise<{action: StepUpAction; returnTo: string} | null>;
18
+ revokeStepUp(id: string, now: number): Promise<unknown>;
19
+ /** Recheck asking session, current person version/status and persisted local proof; never infer freshness from session kind. */
20
+ steppedUp(actor: Session<R>, since: number, now: number): Promise<boolean>;
21
+ }
22
+ export interface EmailLinkPersistence<R extends string = string> extends Omit<NativeProofPersistence<R>, 'preview' | 'consumeLink'> {
23
+ /** Required when supplying hooks.afterConfirm directly; durably revoke the issued session before reporting refusal. */
24
+ revokeSession?(id: string): Promise<unknown>;
25
+ authenticate(request: Request): Promise<Session<R> | null>;
26
+ requiredProvider(email: string): Promise<string | null>;
27
+ byEmail(email: string): Promise<NativePerson<R> | null>;
28
+ authorizedLink(request: Request, digest: string): Promise<{link: NativeLinkPreview; person?: NativePerson<R>} | null>;
29
+ consumeLink(input: {request: Request; digest: string; approval: {link: NativeLinkPreview; person?: NativePerson<R>}; now: number}): Promise<CompletedSession<R> | null>;
30
+ }
31
+ /** A direct veto hook requires revocation authority. A composed callback owns its hooks and revocation; never supply the same hook through both paths. */
32
+ export type EmailLinkCompletion<R extends string> = {
33
+ persistence: EmailLinkPersistence<R> & { revokeSession(id: string): Promise<unknown> };
34
+ hooks?: AuthHooks<R>;
35
+ afterConfirm?: never;
36
+ } | {
37
+ persistence: EmailLinkPersistence<R>;
38
+ hooks?: Omit<AuthHooks<R>, 'afterConfirm'> & { afterConfirm?: never };
39
+ afterConfirm?: (request: Request, completed: CompletedSession<R>, method: 'email') => Promise<CompletedSession<R>>;
40
+ };
41
+ /** Standalone composition normalizes the same AuthConfig used by createOperatorAuth. */
42
+ export function createEmailLinkLifecycle<R extends string>(options: {
43
+ config: AuthConfig;
44
+ clock?: () => number; mailSender?: MailSender; clientIp: (request: Request) => string | Promise<string>;
45
+ hash: (purpose: string, token: string) => Promise<string>;
46
+ } & EmailLinkCompletion<R>): Pick<OperatorAuth<R>, 'requestMagicLink' | 'previewLink' | 'confirmLink' | 'requestStepUp' | 'requireStepUp'> & {
47
+ requireFreshProof(actor: Session<R>): Promise<void>;
48
+ };
@@ -0,0 +1,128 @@
1
+ import { configure } from '../../config.mjs';
2
+ import { AuthError, deny } from '../../errors.mjs';
3
+ import { opaqueToken, validToken } from '../../crypto.mjs';
4
+ import { safeReturnTo, normalizeEmail } from '../../policy.mjs';
5
+ import { requirePost, requireOrigin } from '../../http.mjs';
6
+ import { isActiveOperator } from '../../accounts/roles.mjs';
7
+ import { isLocalCli } from '../../local-cli.mjs';
8
+ import { linkMessage } from './message.mjs';
9
+ import { completionHook } from '../after-confirm.mjs';
10
+ export const STEP_UP_ACTIONS = Object.freeze(['providers:unlink', 'providers:relink', 'domains:change', 'owner:transfer', 'export', 'delete', 'credentials:mint']);
11
+ export const STEP_UP_SECONDS = 600;
12
+
13
+ // Persistence owns the atomic authority check, proof consumption and session
14
+ // issuance. It never receives a raw email token; only its keyed digest.
15
+ export function createEmailLinkLifecycle({ config: input, clock = Date.now, mailSender, clientIp, hash, persistence, hooks = {}, afterConfirm }) {
16
+ const config = configure(input);
17
+ const required = ['limits', 'token', 'revokeLink', 'authorizedLink', 'consumeLink', 'byEmail', 'authenticate', 'requiredProvider', 'stepUpPreview', 'stepUpChallenge', 'consumeStepUp', 'revokeStepUp', 'steppedUp'];
18
+ if (!persistence || required.some(k => typeof persistence[k] !== 'function') || typeof hash !== 'function' || typeof clientIp !== 'function') throw new AuthError('invalid_proof_persistence', 503);
19
+ if (afterConfirm !== undefined && typeof afterConfirm !== 'function' || hooks.afterConfirm !== undefined && (typeof hooks.afterConfirm !== 'function' || afterConfirm !== undefined || typeof persistence.revokeSession !== 'function')) throw new AuthError('invalid_confirmation_hooks', 503);
20
+ const complete = afterConfirm ?? completionHook({ hooks, revokeSession: id => persistence.revokeSession(id) });
21
+ const store = persistence;
22
+ async function limit(request, email, scope) {
23
+ // The CLI's own loopback sign-in and owner claim are not a person's attempts.
24
+ if ((scope === 'magic' || scope === 'bootstrap') && await isLocalCli(request, config)) return;
25
+ const ip = await clientIp(request);
26
+ if (typeof ip !== 'string' || !ip || ip.length > 128) deny('client_ip_unavailable', 503);
27
+ const keys = await Promise.all([hash('rate', `${scope}:ip:${ip}`), hash('rate', `${scope}:email:${email}`)]);
28
+ const counts = await store.limits(keys, clock(), config.limitWindowSeconds * 1000);
29
+ if (!Array.isArray(counts) || counts.length !== keys.length || counts.some(value => !Number.isSafeInteger(value) || value < 0)) deny('invalid_proof_persistence', 503);
30
+ if (counts[0] > config.ipLimit || counts[1] > config.emailLimit) deny('rate_limited', 429);
31
+ }
32
+ async function newToken(kind, returnTo) {
33
+ const token = opaqueToken();
34
+ return { id: crypto.randomUUID(), token, hash: await hash('link', token), kind, returnTo, expiresAt: clock() + config.linkSeconds * 1000 };
35
+ }
36
+ // The email link: the app's own proof that a person holds an address.
37
+ async function sendLink(email, token, revoke = () => store.revokeLink(token.id, clock())) {
38
+ if (!mailSender || !config.MAIL_FROM) { await revoke(); deny('mail_unavailable', 503); }
39
+ const url = new URL('/auth/confirm', config.origin);
40
+ url.searchParams.set('token', token.token);
41
+ let result;
42
+ try {
43
+ result = await mailSender.send({ deliveryId: token.id, from: config.MAIL_FROM, to: [email],
44
+ ...linkMessage(config, token, url.href),
45
+ headers: { 'Auto-Submitted': 'auto-generated' } });
46
+ } catch { await revoke(); deny('mail_unavailable', 503); }
47
+ if (result?.status !== 'accepted' && !(result?.status === 'development' && config.local)) {
48
+ // Even ambiguous delivery cannot leave a valid, unreported auth link.
49
+ await revoke(); deny('mail_unavailable', 503);
50
+ }
51
+ }
52
+
53
+ async function requireFreshProof(actor) {
54
+ if (!await store.steppedUp(actor, clock() - STEP_UP_SECONDS * 1000, clock())) deny('step_up_required', 403);
55
+ }
56
+ const authenticate = request => store.authenticate(request);
57
+ return {
58
+ limit, newToken, sendLink, requireFreshProof,
59
+ async requestMagicLink(request, { email: raw, returnTo }) {
60
+ requirePost(request, config);
61
+ const email = normalizeEmail(raw);
62
+ await limit(request, email, 'magic');
63
+ // Configuration failure is uniform for all addresses; unknown addresses
64
+ // otherwise have the same successful public response as admitted ones.
65
+ if (!mailSender || !config.MAIL_FROM) deny('mail_unavailable', 503);
66
+ // A domain rule is said for the whole domain, whoever asks.
67
+ const required = await store.requiredProvider(email);
68
+ const accepted = { status: 'accepted', ...(required ? { requiredProvider: required } : {}) };
69
+ const operator = await store.byEmail(email);
70
+ if (!isActiveOperator(operator, config.allowList)) return accepted;
71
+ if (required && !operator.owner && operator.role !== 'owner') return accepted;
72
+ if (hooks.beforeMagicLink && await hooks.beforeMagicLink({ request, email, operator }) === false) return accepted;
73
+ const token = await newToken('magic', safeReturnTo(returnTo));
74
+ const { token: _secret, ...record } = token;
75
+ if (await store.token({ ...record, operatorId: operator.id, expectedVersion: operator.version, now: clock() })) await sendLink(email, token);
76
+ return accepted;
77
+ },
78
+ async previewLink(request, token) {
79
+ requireOrigin(request, config);
80
+ if (!['GET', 'HEAD'].includes(request.method)) deny('method_not_allowed', 405);
81
+ if (!validToken(token)) return null;
82
+ const digest = await hash('link', token);
83
+ const result = await store.authorizedLink(request, digest);
84
+ if (result) return { kind: result.link.kind, expiresInMinutes: Math.max(1, Math.ceil((result.link.expiresAt - clock()) / 60000)) };
85
+ const challenge = await store.stepUpPreview(digest, clock());
86
+ return challenge ? { kind: 'step_up', expiresInMinutes: Math.max(1, Math.ceil((challenge.expiresAt - clock()) / 60000)) } : null;
87
+ },
88
+ async confirmLink(request, { token }) {
89
+ requirePost(request, config);
90
+ await limit(request, typeof token === 'string' ? token.slice(0, 128) : '', 'confirmation');
91
+ if (!validToken(token)) deny('invalid_or_expired', 400);
92
+ const digest = await hash('link', token);
93
+ const approval = await store.authorizedLink(request, digest);
94
+ if (!approval) {
95
+ // A step-up link completes only in the session that asked for it.
96
+ const actor = await authenticate(request);
97
+ const proof = actor && await store.consumeStepUp(digest, actor, clock());
98
+ if (!proof) deny('invalid_or_expired', 400);
99
+ return { stepUp: proof.action, returnTo: safeReturnTo(proof.returnTo) };
100
+ }
101
+ const completed = await store.consumeLink({ request, digest, approval, now: clock() });
102
+ if (!completed) deny('invalid_or_expired', 400);
103
+ const result = await complete(request, { ...completed, returnTo: safeReturnTo(completed.returnTo) }, 'email');
104
+ return { ...result, returnTo: safeReturnTo(result.returnTo) };
105
+ },
106
+ /** Ask for a step-up proof: an email link to the signed-in operator's own address. */
107
+ async requestStepUp(request, { action, returnTo } = {}) {
108
+ requirePost(request, config);
109
+ if (!STEP_UP_ACTIONS.includes(action)) throw new AuthError('invalid_action');
110
+ const actor = await authenticate(request);
111
+ if (!actor) deny('unauthenticated', 401);
112
+ await limit(request, actor.operator.email, 'step-up');
113
+ const token = await newToken('step_up', safeReturnTo(returnTo));
114
+ if (!await store.stepUpChallenge({ id: token.id, hash: token.hash, action, returnTo: token.returnTo, now: clock(), expiresAt: token.expiresAt }, actor)) deny('authority_changed', 409);
115
+ await sendLink(actor.operator.email, token, () => store.revokeStepUp(token.id, clock()));
116
+ return { status: 'accepted' };
117
+ },
118
+ /** Server-side guard for a listed action: the session holds a fresh email-link proof. */
119
+ async requireStepUp(request, action) {
120
+ if (!STEP_UP_ACTIONS.includes(action)) throw new AuthError('invalid_action');
121
+ const actor = await authenticate(request);
122
+ if (!actor) deny('unauthenticated', 401);
123
+ await requireFreshProof(actor);
124
+ const { hash: _, ...session } = actor;
125
+ return session;
126
+ },
127
+ };
128
+ }
@@ -0,0 +1,16 @@
1
+ import { accentInk } from '../../branding.mjs';
2
+ import { escapeHtml } from '../../http.mjs';
3
+
4
+ export function linkMessage(config, token, url) {
5
+ const { productName, accentColor } = config.branding;
6
+ const invitation = token.kind === 'invite', stepUp = token.kind === 'step_up';
7
+ const action = invitation ? 'accept your invitation to' : stepUp ? 'confirm a sensitive change in' : 'sign in to';
8
+ const minutes = config.linkSeconds / 60;
9
+ const expiry = `This link works once and expires in ${minutes} ${minutes === 1 ? 'minute' : 'minutes'}.`;
10
+ const heading = invitation ? `Accept your invitation to ${productName}` : stepUp ? `Confirm it's you in ${productName}` : `Confirm sign-in to ${productName}`;
11
+ return {
12
+ subject: invitation ? `You're invited to ${productName}` : stepUp ? `Confirm it's you in ${productName}` : `Your sign-in link for ${productName}`,
13
+ text: `Open this link to review and confirm. On the confirmation page, press Confirm to ${action} ${productName}. ${expiry}\n\n${url}`,
14
+ html: `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(heading)}</title></head><body style="margin:0;font-family:Arial,sans-serif;background:#fff;color:#172033"><main style="max-width:480px;margin:0 auto;padding:32px 24px"><h1>${escapeHtml(heading)}</h1><p>Open this link to review and confirm. On the confirmation page, press Confirm to ${action} ${escapeHtml(productName)}.</p><p>${expiry}</p><p><a href="${escapeHtml(url)}" style="display:inline-block;padding:14px 24px;border:1px solid #64748b;border-radius:8px;background:${accentColor};color:${accentInk(accentColor)};font-family:Arial,sans-serif;font-weight:bold;text-decoration:none">Confirm</a></p></main></body></html>`,
15
+ };
16
+ }
@@ -0,0 +1,7 @@
1
+ import type { AccountsAdapter, Admission, AuthHooks, Principal } from '../index.mjs';
2
+ export { createOidcProviders, createOidcHandler, OidcError, PRESETS, MINIMUM_PROVIDER_VERSION } from './oidc/index.mjs';
3
+ export const EMAIL_PROOF_DAYS: number;
4
+ /** Admission over any accounts adapter: a verified principal in, a session out. */
5
+ export function createAdmission<R extends string>(input: { accounts: AccountsAdapter<R>; config: { origin: string; local?: boolean }; clock?: () => number; hooks?: AuthHooks<R> }): Admission<R>;
6
+ export function trustedEmail(principal: Principal, now: number): string | null;
7
+ export function refuseInProduction(principal: Principal, config: { local?: boolean }): void;
@@ -0,0 +1,5 @@
1
+ // Identity: proves who a person is and outputs a verified principal. It never
2
+ // decides who may enter; an accounts adapter does (../accounts/adapter.mjs).
3
+ export { createAdmission } from './admission.mjs';
4
+ export { trustedEmail, refuseInProduction, EMAIL_PROOF_DAYS } from './principal.mjs';
5
+ export { createOidcProviders, createOidcHandler, OidcError, PRESETS, MINIMUM_PROVIDER_VERSION } from './oidc/index.mjs';