@ultimat3/cli 29.4.0 → 30.0.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 (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +11 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +9 -0
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +41 -81
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +7 -1
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1313 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
@@ -0,0 +1,189 @@
1
+ // `x g sign-in --oidc <provider>`: an OpenID Connect issuer, discovered when the first sign-in
2
+ // arrives (`discoverOAuthProvider`), its two legs (`oauthLogin`) mounted on `runtime.routes`, and
3
+ // the page that links to the first one. PKCE, the sealed handshake, the state check and the account
4
+ // link are the framework's; which IdP group means which role stays the app's (`resolveGrants`).
5
+
6
+ import { catalogJson } from './catalog-json';
7
+ import { catalogPath } from './locales';
8
+ import type { GeneratedFile } from './naming';
9
+ import {
10
+ SIGN_IN_CATALOG,
11
+ type SignInForm,
12
+ type SignInTemplateOptions,
13
+ signInPageSource,
14
+ signInPageTest,
15
+ } from './sign-in-pages';
16
+ import { AUTH_DIR, sharedSignInFiles } from './sign-in-shared';
17
+
18
+ /** `bigco-sso` → `BIGCO_SSO`: the prefix `discoverOAuthProvider` derives its two env keys from. */
19
+ export const oidcEnvPrefix = (provider: string): string =>
20
+ provider.toUpperCase().replace(/[^A-Z0-9]+/g, '_');
21
+
22
+ /** The settings an OIDC sign-in cannot run without, in the order `x env example` should list. */
23
+ export const oidcSignInEnv = (provider: string): readonly string[] => {
24
+ const prefix = oidcEnvPrefix(provider);
25
+ return [
26
+ `${prefix}_ISSUER`,
27
+ `${prefix}_CLIENT_ID`,
28
+ `${prefix}_CLIENT_SECRET`,
29
+ 'SESSION_SECRET',
30
+ 'APP_URL',
31
+ ];
32
+ };
33
+
34
+ const oidcSource = (
35
+ provider: string,
36
+ ): string => `// Sign-in through ${provider}, an OpenID Connect issuer. Discovered on the FIRST sign-in, never at
37
+ // import: the boot scan imports this module before anything may open a socket, and an IdP that is
38
+ // down must cost a sign-in, not a deploy. A failed discovery is retried by the next request.
39
+ //
40
+ // Who a first-time user IS here — roles, org — is \`resolveGrants\` (\`oauthLogin\`'s option): omitted,
41
+ // they arrive with no roles and no org, and every \`can()\` denies them. That mapping is this app's.
42
+ import {
43
+ type Auth,
44
+ defineAuth,
45
+ discoverOAuthProvider,
46
+ hasOAuthProvider,
47
+ type OAuthLoginRoutes,
48
+ oauthLogin,
49
+ oauthStartPath,
50
+ registerOAuthProvider,
51
+ } from '@ultimat3/auth';
52
+ import { appAuth } from './auth';
53
+ import { AFTER_SIGN_IN } from './sign-in-flow';
54
+
55
+ export const OIDC_PROVIDER = '${provider}';
56
+ export const OIDC_ISSUER_ENV = '${oidcEnvPrefix(provider)}_ISSUER';
57
+ /** Where the sign-in page sends a browser: the start leg, \`/auth/oauth/${provider}\`. */
58
+ export const OIDC_START_PATH = oauthStartPath(OIDC_PROVIDER);
59
+
60
+ type Env = Readonly<Record<string, string | undefined>>;
61
+
62
+ /**
63
+ * The app's \`Auth\` over the same adapter, naming the one provider it serves. A second instance
64
+ * and not an edit to \`auth.ts\`: its sessions are the same rows, so \`authenticator.ts\` reads them.
65
+ */
66
+ const signInAuth = (): Auth =>
67
+ defineAuth({ adapter: appAuth().adapter, providers: [OIDC_PROVIDER] });
68
+
69
+ let legs: Promise<OAuthLoginRoutes> | undefined;
70
+
71
+ /** The two legs, built once the issuer answered. \`sign-in-guard.ts\` refuses a deploy without it. */
72
+ export const oidcLegs = (env: Env = process.env): Promise<OAuthLoginRoutes> => {
73
+ legs ??= (async () => {
74
+ const issuer = (env[OIDC_ISSUER_ENV] ?? '').trim();
75
+ if (!hasOAuthProvider(OIDC_PROVIDER)) {
76
+ registerOAuthProvider(await discoverOAuthProvider({ id: OIDC_PROVIDER, issuer }));
77
+ }
78
+ return oauthLogin(signInAuth(), { successPath: AFTER_SIGN_IN });
79
+ })().catch((error: unknown) => {
80
+ legs = undefined;
81
+ throw error;
82
+ });
83
+ return legs;
84
+ };
85
+ `;
86
+
87
+ const oidcTest = (
88
+ provider: string,
89
+ ): string => `// The provider's names, and the one rule discovery keeps: nothing is fetched at import.
90
+ import { hasOAuthProvider } from '@ultimat3/auth';
91
+ import { expect, unitTest } from '@ultimat3/testing';
92
+ import { OIDC_ISSUER_ENV, OIDC_PROVIDER, OIDC_START_PATH, oidcLegs } from './oidc';
93
+
94
+ unitTest('the provider, its issuer key and its start path are spelled once', () => {
95
+ expect(OIDC_PROVIDER).toBe('${provider}');
96
+ expect(OIDC_ISSUER_ENV).toBe('${oidcEnvPrefix(provider)}_ISSUER');
97
+ expect(OIDC_START_PATH).toBe('/auth/oauth/${provider}');
98
+ // Importing this module registered nothing: the first sign-in does.
99
+ expect(hasOAuthProvider(OIDC_PROVIDER)).toBe(false);
100
+ });
101
+
102
+ unitTest('a discovery that fails is refused, and the next sign-in tries again', async () => {
103
+ // No issuer configured: the sealed network refuses the fetch, and nothing is memoised.
104
+ await expect(oidcLegs({})).rejects.toBeDefined();
105
+ await expect(oidcLegs({})).rejects.toBeDefined();
106
+ expect(hasOAuthProvider(OIDC_PROVIDER)).toBe(false);
107
+ });
108
+ `;
109
+
110
+ const routesSource =
111
+ (): string => `// The two OIDC legs as plain HTTP routes — \`runtime.ts\` hands them to both boots. Plain routes,
112
+ // not actions: the callback is a redirect protocol whose shape the provider fixes.
113
+ import { OAUTH_CALLBACK_ROUTE_PATH, OAUTH_START_ROUTE_PATH } from '@ultimat3/auth';
114
+ import type { Route } from '@ultimat3/http';
115
+ import { oidcLegs } from './oidc';
116
+
117
+ const leg = (name: 'start' | 'callback', path: string): Route => ({
118
+ method: 'GET',
119
+ path,
120
+ // Public by definition: these two requests are how an anonymous visitor stops being one.
121
+ meta: { name: \`auth.oidc.\${name}\`, auth: 'public' },
122
+ handler: async (request) => (await oidcLegs())[name].handle(request.raw),
123
+ });
124
+
125
+ export const signInRoutes: readonly Route[] = [
126
+ leg('start', OAUTH_START_ROUTE_PATH),
127
+ leg('callback', OAUTH_CALLBACK_ROUTE_PATH),
128
+ ];
129
+ `;
130
+
131
+ const routesTest =
132
+ (): string => `// Both legs are mounted, public, at the framework's one declaration of where they live.
133
+ import { OAUTH_CALLBACK_ROUTE_PATH, OAUTH_START_ROUTE_PATH } from '@ultimat3/auth';
134
+ import { expect, unitTest } from '@ultimat3/testing';
135
+ import { signInRoutes } from './sign-in-routes';
136
+
137
+ unitTest('the start and callback legs are public GET routes at the framework paths', () => {
138
+ expect(signInRoutes.map((route) => [route.method, route.path, route.meta.auth])).toEqual([
139
+ ['GET', OAUTH_START_ROUTE_PATH, 'public'],
140
+ ['GET', OAUTH_CALLBACK_ROUTE_PATH, 'public'],
141
+ ]);
142
+ });
143
+ `;
144
+
145
+ /** A link, not a form: the start leg is a GET that redirects to the provider. */
146
+ const OIDC_FORM: SignInForm = {
147
+ comment: `// Sign in through the identity provider: one link to the start leg, which redirects there. Nothing
148
+ // here hydrates, and no password ever reaches this app.`,
149
+ imports: ["import { Button, Link } from '@ultimat3/ui';"],
150
+ localImports: ["import { OIDC_START_PATH } from '../oidc';"],
151
+ props: false,
152
+ body: ` <Link href={OIDC_START_PATH} appearance="button">
153
+ {t('app.auth.signIn.continue')}
154
+ </Link>`,
155
+ };
156
+
157
+ export function oidcSignInFiles(
158
+ provider: string,
159
+ options: SignInTemplateOptions,
160
+ ): readonly GeneratedFile[] {
161
+ const module = options.catalogModule;
162
+ return [
163
+ ...sharedSignInFiles(
164
+ oidcSignInEnv(provider),
165
+ `The issuer and the client are ${provider}'s; SESSION_SECRET seals the handshake and APP_URL is where the provider sends the browser back.`,
166
+ {
167
+ importLine: "import { signInRoutes } from './app/auth/sign-in-routes';",
168
+ expression: 'signInRoutes',
169
+ },
170
+ ),
171
+ { path: `${AUTH_DIR}/oidc.ts`, contents: oidcSource(provider) },
172
+ { path: `${AUTH_DIR}/oidc.test.ts`, contents: oidcTest(provider) },
173
+ { path: `${AUTH_DIR}/sign-in-routes.ts`, contents: routesSource() },
174
+ { path: `${AUTH_DIR}/sign-in-routes.test.ts`, contents: routesTest() },
175
+ { path: `${AUTH_DIR}/sign-in/page.tsx`, contents: signInPageSource(module, OIDC_FORM) },
176
+ {
177
+ path: `${AUTH_DIR}/sign-in/page.test.ts`,
178
+ contents: signInPageTest(module, 'app.auth.signIn', `'href="/auth/oauth/${provider}"'`),
179
+ },
180
+ ...options.locales.map((locale) => ({
181
+ path: catalogPath(locale),
182
+ contents: catalogJson({
183
+ ...SIGN_IN_CATALOG,
184
+ 'app.auth.signIn.continue': `Continue with ${provider}`,
185
+ }),
186
+ merge: 'json' as const,
187
+ })),
188
+ ];
189
+ }
@@ -0,0 +1,148 @@
1
+ // The pages `x g sign-in` writes, both shapes: the sign-in page (what proves who you are is the
2
+ // shape's — a password form or a provider link), the second-factor page, and the sign-out form.
3
+ // Native forms and links only: nothing hydrates, so each page ships 0 bytes of script.
4
+
5
+ import { sortedImports } from './imports';
6
+ import { catalogImport, SIGN_IN_PATH, translatorBinding } from './sign-in-shared';
7
+
8
+ export interface SignInTemplateOptions {
9
+ readonly locales: readonly string[];
10
+ /** The app's `@<app>/i18n`; absent, the page imports `t` from `@ultimat3/i18n`. */
11
+ readonly catalogModule?: string | undefined;
12
+ }
13
+
14
+ /** What one shape puts on the sign-in page: its imports, its proof of identity, why. */
15
+ export interface SignInForm {
16
+ readonly comment: string;
17
+ /** Bare specifiers only, sorted with the page's own. */
18
+ readonly imports: readonly string[];
19
+ /** Relative imports, after the bare block. */
20
+ readonly localImports: readonly string[];
21
+ readonly props: boolean;
22
+ readonly body: string;
23
+ }
24
+
25
+ const routeConfig = (key: string): string => `export const config = defineRoute({
26
+ render: 'stream',
27
+ hydrate: 'never',
28
+ // Never from a cache: a stale sign-in form posts at a build that has moved on.
29
+ offline: 'network-only',
30
+ budget: { js: '0kb' },
31
+ meta: ({ t }) => ({
32
+ title: t('${key}.title'),
33
+ description: t('${key}.description'),
34
+ robots: { index: false, follow: false },
35
+ }),
36
+ });`;
37
+
38
+ /** Every sign-in page carries the way out too: the one form that posts at `destroySession`. */
39
+ const SIGN_OUT_FORM = ` <form method="post" action={derivePath('destroySession').path}>
40
+ <input type="hidden" name="confirm" value="sign-out" />
41
+ <Button type="submit" variant="secondary">
42
+ {t('app.auth.signOut.submit')}
43
+ </Button>
44
+ </form>`;
45
+
46
+ export const signInPageSource = (
47
+ module: string | undefined,
48
+ form: SignInForm,
49
+ ): string => `${form.comment}
50
+ ${sortedImports([
51
+ catalogImport(module),
52
+ "import { derivePath } from '@ultimat3/action';",
53
+ "import { defineRoute } from '@ultimat3/render';",
54
+ ...form.imports,
55
+ ])}
56
+ ${[...form.localImports, "import styles from './page.module.scss';"].join('\n')}
57
+
58
+ ${routeConfig('app.auth.signIn')}
59
+ ${
60
+ form.props
61
+ ? `
62
+ export interface SignInProps {
63
+ readonly query?: Readonly<Record<string, string>> | undefined;
64
+ }
65
+ `
66
+ : ''
67
+ }
68
+ export function Page(${form.props ? 'props: SignInProps' : ''}) {${translatorBinding(module)}
69
+ return (
70
+ <main class={styles.page}>
71
+ <h1>{t('app.auth.signIn.title')}</h1>
72
+ ${form.body}
73
+ ${SIGN_OUT_FORM}
74
+ </main>
75
+ );
76
+ }
77
+ `;
78
+
79
+ /** The second leg posts the sign-in's \`next\` back, so \`confirmSession\` lands where it was going. */
80
+ export const secondFactorSource = (
81
+ module: string | undefined,
82
+ ): string => `// The second leg of a sign-in: the code from an authenticator app, posted at \`confirmSession\`.
83
+ ${sortedImports([
84
+ catalogImport(module),
85
+ "import { derivePath } from '@ultimat3/action';",
86
+ "import { defineRoute } from '@ultimat3/render';",
87
+ "import { Button, Field, Input } from '@ultimat3/ui';",
88
+ ])}
89
+ import { NEXT_PARAM, nextPath } from '../../sign-in-flow';
90
+ import styles from '../page.module.scss';
91
+
92
+ ${routeConfig('app.auth.secondFactor')}
93
+
94
+ export interface SecondFactorProps {
95
+ readonly query?: Readonly<Record<string, string>> | undefined;
96
+ }
97
+
98
+ export function Page(props: SecondFactorProps) {${translatorBinding(module)}
99
+ return (
100
+ <main class={styles.page}>
101
+ <h1>{t('app.auth.secondFactor.title')}</h1>
102
+ <form method="post" action={derivePath('confirmSession').path}>
103
+ <input type="hidden" name={NEXT_PARAM} value={nextPath(props.query?.[NEXT_PARAM])} />
104
+ <Field label={t('app.auth.secondFactor.code')} required>
105
+ {(control) => (
106
+ <Input {...control} name="code" inputmode="numeric" autocomplete="one-time-code" />
107
+ )}
108
+ </Field>
109
+ <Button type="submit">{t('app.auth.secondFactor.submit')}</Button>
110
+ </form>
111
+ </main>
112
+ );
113
+ }
114
+ `;
115
+
116
+ /** The line that holds a page to its sign-out form. */
117
+ const SIGN_OUT_EXPECTED = ` expect(view.html).toContain(\`action="\${derivePath('destroySession').path}"\`);
118
+ `;
119
+
120
+ /** The page renders, names itself, ships no island, and its form posts where `contains` says. */
121
+ export const signInPageTest = (
122
+ module: string | undefined,
123
+ key: string,
124
+ contains: string,
125
+ signOut = true,
126
+ ): string => `// The page renders its form at the route it posts to, with no script and no search listing.
127
+ ${sortedImports([
128
+ catalogImport(module),
129
+ "import { derivePath } from '@ultimat3/action';",
130
+ "import { expect, renderRoute, unitTest } from '@ultimat3/testing';",
131
+ ])}
132
+ import * as page from './page';
133
+
134
+ unitTest('the page renders its form, names itself, and ships no island', async () => {${translatorBinding(module)}
135
+ const view = await renderRoute(page, { url: 'https://example.test${SIGN_IN_PATH}', params: {} });
136
+ expect(view.html).toContain(${contains});
137
+ ${signOut ? SIGN_OUT_EXPECTED : ''} expect(view.meta.title).toBe(t('${key}.title'));
138
+ expect(view.islands).toEqual([]);
139
+ expect(page.config.budget.js).toBe('0kb');
140
+ });
141
+ `;
142
+
143
+ /** The keys every sign-in page reads; a shape adds its own. */
144
+ export const SIGN_IN_CATALOG: Readonly<Record<string, string>> = {
145
+ 'app.auth.signIn.title': 'Sign in',
146
+ 'app.auth.signIn.description': 'Sign in to your account.',
147
+ 'app.auth.signOut.submit': 'Sign out',
148
+ };
@@ -0,0 +1,216 @@
1
+ // `x g sign-in --password`: an address and a password over `login()`, the second leg over
2
+ // `completeMfa()`, and the two native-form pages that post at them. No island: the browser's own
3
+ // form handling is the client, so the pages ship 0 bytes of script.
4
+
5
+ import { catalogJson } from './catalog-json';
6
+ import { catalogPath } from './locales';
7
+ import type { GeneratedFile } from './naming';
8
+ import { confirmSessionTest, createSessionTest } from './sign-in-action-tests';
9
+ import {
10
+ SIGN_IN_CATALOG,
11
+ type SignInForm,
12
+ type SignInTemplateOptions,
13
+ secondFactorSource,
14
+ signInPageSource,
15
+ signInPageTest,
16
+ } from './sign-in-pages';
17
+ import { AUTH_DIR, SIGN_IN_PATH, sharedSignInFiles } from './sign-in-shared';
18
+
19
+ /** The master key seals the second-factor challenge between the two legs. */
20
+ export const PASSWORD_SIGN_IN_ENV: readonly string[] = ['ULTIMATE_SECRETS_KEY'];
21
+
22
+ const challengeSource =
23
+ (): string => `// The second-factor challenge between the two legs: a sealed, five-minute credential \`login()\`
24
+ // hands back inside \`X_MFA_REQUIRED\`, carried in an HttpOnly cookie rather than a URL a log keeps.
25
+ import { MFA_CHALLENGE_TTL_MS } from '@ultimat3/auth';
26
+ import { isUltimateError } from '@ultimat3/core';
27
+ import { deleteCookie, setCookie, useRequestCookie } from '@ultimat3/http';
28
+
29
+ export const MFA_CHALLENGE_COOKIE = 'x_mfa_challenge';
30
+ export const SECOND_FACTOR_PATH = '${SIGN_IN_PATH}/second-factor';
31
+
32
+ /** The sealed challenge an \`X_MFA_REQUIRED\` carries, or \`null\` for any other throw. */
33
+ export const challengeOf = (error: unknown): string | null => {
34
+ if (!isUltimateError(error) || error.code !== 'X_MFA_REQUIRED') return null;
35
+ const challenge = error.meta?.['challenge'];
36
+ return typeof challenge === 'string' && challenge !== '' ? challenge : null;
37
+ };
38
+
39
+ export const holdChallenge = (challenge: string): void =>
40
+ setCookie(MFA_CHALLENGE_COOKIE, challenge, { maxAge: Math.floor(MFA_CHALLENGE_TTL_MS / 1000) });
41
+
42
+ export const heldChallenge = (): string | null => {
43
+ const value = useRequestCookie(MFA_CHALLENGE_COOKIE);
44
+ return value === null || value === '' ? null : value;
45
+ };
46
+
47
+ export const dropChallenge = (): void => deleteCookie(MFA_CHALLENGE_COOKIE);
48
+ `;
49
+
50
+ const challengeTest =
51
+ (): string => `// Only an X_MFA_REQUIRED carrying a challenge is a second leg; every other throw is not.
52
+ import { UltimateError } from '@ultimat3/core';
53
+ import { expect, unitTest } from '@ultimat3/testing';
54
+ import { challengeOf } from './sign-in-challenge';
55
+
56
+ const coded = (code: string, meta?: Record<string, unknown>) =>
57
+ new UltimateError({ code, cause: 'a test throw', fix: 'none', ...(meta ? { meta } : {}) });
58
+
59
+ unitTest('a sealed challenge is read off X_MFA_REQUIRED, and nothing else is one', () => {
60
+ expect(challengeOf(coded('X_MFA_REQUIRED', { challenge: 'sealed' }))).toBe('sealed');
61
+ expect(challengeOf(coded('X_MFA_REQUIRED'))).toBeNull();
62
+ expect(challengeOf(coded('X_UNAUTHENTICATED', { challenge: 'sealed' }))).toBeNull();
63
+ expect(challengeOf('X_MFA_REQUIRED')).toBeNull();
64
+ });
65
+ `;
66
+
67
+ const createSessionSource =
68
+ (): string => `// Sign in with an address and a password. \`login()\` is the whole check — the hash, the lockout and
69
+ // one indistinguishable \`X_UNAUTHENTICATED\` for every way a credential is wrong. A second factor
70
+ // still owed stops here with the sealed challenge held for the next page.
71
+ import { action, t } from '@ultimat3/action';
72
+ import { login } from '@ultimat3/auth';
73
+ import { useRequestContext } from '@ultimat3/http';
74
+ import { allow } from '@ultimat3/policy';
75
+ import { appAuth } from '../auth';
76
+ import { challengeOf, holdChallenge, SECOND_FACTOR_PATH } from '../sign-in-challenge';
77
+ import { landAt, NEXT_PARAM, nextPath } from '../sign-in-flow';
78
+
79
+ export const createSession = action({
80
+ input: t.object({
81
+ email: t.email,
82
+ password: t.string.min(1).max(1024),
83
+ [NEXT_PARAM]: t.optional(t.string.max(2048)),
84
+ }),
85
+ output: t.object({ ok: t.boolean, next: t.string, secondFactor: t.boolean }),
86
+ // Public by declaration: signing in is the one operation nobody holds a permission for yet.
87
+ policy: allow('public'),
88
+ mcp: { expose: false },
89
+ async handle({ input }) {
90
+ const request = useRequestContext('createSession');
91
+ try {
92
+ const signedIn = await login(appAuth(), {
93
+ email: input.email,
94
+ password: input.password,
95
+ ip: request.ip,
96
+ });
97
+ request.headers.append('set-cookie', signedIn.cookie);
98
+ return { ok: true, next: landAt(nextPath(input[NEXT_PARAM])), secondFactor: false };
99
+ } catch (error) {
100
+ const challenge = challengeOf(error);
101
+ if (challenge === null) throw error;
102
+ holdChallenge(challenge);
103
+ // The way on survives the second leg: its form posts \`next\` back to \`confirmSession\`.
104
+ const next = input[NEXT_PARAM];
105
+ const then =
106
+ next === undefined
107
+ ? SECOND_FACTOR_PATH
108
+ : \`\${SECOND_FACTOR_PATH}?\${NEXT_PARAM}=\${encodeURIComponent(nextPath(next))}\`;
109
+ return { ok: false, next: landAt(then), secondFactor: true };
110
+ }
111
+ },
112
+ });
113
+ `;
114
+
115
+ const confirmSessionSource =
116
+ (): string => `// The second leg: the six digits (or one recovery code) against the challenge the first leg held.
117
+ // \`completeMfa()\` meters the guess against the same lockout a password guess spends.
118
+ import { action, t } from '@ultimat3/action';
119
+ import { authUnauthenticated, completeMfa } from '@ultimat3/auth';
120
+ import { useRequestContext } from '@ultimat3/http';
121
+ import { allow } from '@ultimat3/policy';
122
+ import { appAuth } from '../auth';
123
+ import { dropChallenge, heldChallenge } from '../sign-in-challenge';
124
+ import { landAt, NEXT_PARAM, nextPath } from '../sign-in-flow';
125
+
126
+ export const confirmSession = action({
127
+ input: t.object({
128
+ code: t.string.min(1).max(64),
129
+ [NEXT_PARAM]: t.optional(t.string.max(2048)),
130
+ }),
131
+ output: t.object({ ok: t.boolean, next: t.string }),
132
+ policy: allow('public'),
133
+ mcp: { expose: false },
134
+ async handle({ input }) {
135
+ const request = useRequestContext('confirmSession');
136
+ const challenge = heldChallenge();
137
+ if (challenge === null) throw authUnauthenticated('POST confirmSession');
138
+ const signedIn = await completeMfa(appAuth(), challenge, input.code, { ip: request.ip });
139
+ dropChallenge();
140
+ request.headers.append('set-cookie', signedIn.cookie);
141
+ return { ok: true, next: landAt(nextPath(input[NEXT_PARAM])) };
142
+ },
143
+ });
144
+ `;
145
+
146
+ /** An address and a password, posted at `createSession`, with the way back to where they were. */
147
+ const PASSWORD_FORM: SignInForm = {
148
+ comment: `// Sign in: a native \`<form method="post">\` at the route \`createSession\` derives. Nothing here
149
+ // hydrates, so the browser's own form handling is the client, and the page ships no script.`,
150
+ imports: ["import { Button, Field, Input } from '@ultimat3/ui';"],
151
+ localImports: ["import { NEXT_PARAM, nextPath } from '../sign-in-flow';"],
152
+ props: true,
153
+ body: ` <form method="post" action={derivePath('createSession').path}>
154
+ <input type="hidden" name={NEXT_PARAM} value={nextPath(props.query?.[NEXT_PARAM])} />
155
+ <Field label={t('app.auth.signIn.email')} required>
156
+ {(control) => <Input {...control} name="email" type="email" autocomplete="username" />}
157
+ </Field>
158
+ <Field label={t('app.auth.signIn.password')} required>
159
+ {(control) => (
160
+ <Input {...control} name="password" type="password" autocomplete="current-password" />
161
+ )}
162
+ </Field>
163
+ <Button type="submit">{t('app.auth.signIn.submit')}</Button>
164
+ </form>`,
165
+ };
166
+
167
+ const CATALOG: Readonly<Record<string, string>> = {
168
+ ...SIGN_IN_CATALOG,
169
+ 'app.auth.signIn.email': 'Email',
170
+ 'app.auth.signIn.password': 'Password',
171
+ 'app.auth.signIn.submit': 'Sign in',
172
+ 'app.auth.secondFactor.title': 'Second factor',
173
+ 'app.auth.secondFactor.description': 'Enter the code from your authenticator app.',
174
+ 'app.auth.secondFactor.code': 'Code',
175
+ 'app.auth.secondFactor.submit': 'Continue',
176
+ };
177
+
178
+ /** The test's expectation that the page's form posts at `action`'s derived route. */
179
+ const POSTS_AT = (action: string): string => `\`action="\${derivePath('${action}').path}"\``;
180
+
181
+ export function passwordSignInFiles(options: SignInTemplateOptions): readonly GeneratedFile[] {
182
+ const module = options.catalogModule;
183
+ return [
184
+ ...sharedSignInFiles(
185
+ PASSWORD_SIGN_IN_ENV,
186
+ 'The master key seals the second-factor challenge between the two legs.',
187
+ ),
188
+ { path: `${AUTH_DIR}/sign-in-challenge.ts`, contents: challengeSource() },
189
+ { path: `${AUTH_DIR}/sign-in-challenge.test.ts`, contents: challengeTest() },
190
+ { path: `${AUTH_DIR}/actions/create-session.ts`, contents: createSessionSource() },
191
+ {
192
+ path: `${AUTH_DIR}/actions/create-session.test.ts`,
193
+ contents: createSessionTest(),
194
+ },
195
+ { path: `${AUTH_DIR}/actions/confirm-session.ts`, contents: confirmSessionSource() },
196
+ {
197
+ path: `${AUTH_DIR}/actions/confirm-session.test.ts`,
198
+ contents: confirmSessionTest(),
199
+ },
200
+ { path: `${AUTH_DIR}/sign-in/page.tsx`, contents: signInPageSource(module, PASSWORD_FORM) },
201
+ {
202
+ path: `${AUTH_DIR}/sign-in/page.test.ts`,
203
+ contents: signInPageTest(module, 'app.auth.signIn', POSTS_AT('createSession')),
204
+ },
205
+ { path: `${AUTH_DIR}/sign-in/second-factor/page.tsx`, contents: secondFactorSource(module) },
206
+ {
207
+ path: `${AUTH_DIR}/sign-in/second-factor/page.test.ts`,
208
+ contents: signInPageTest(module, 'app.auth.secondFactor', POSTS_AT('confirmSession'), false),
209
+ },
210
+ ...options.locales.map((locale) => ({
211
+ path: catalogPath(locale),
212
+ contents: catalogJson(CATALOG),
213
+ merge: 'json' as const,
214
+ })),
215
+ ];
216
+ }