@ultimat3/cli 29.3.2 → 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 +13 -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 +28 -8
  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 +42 -138
  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 +17 -27
  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,17 @@
1
+ // The one table from a `--shape` name to its overlay. Typed over `NEW_SHAPES`, so a name with no
2
+ // overlay (or an overlay with no name) is a compile error rather than a shape `x new` half-writes.
3
+
4
+ import type { NewShape } from '../../new-shapes';
5
+ import { apiShape } from './api';
6
+ import { blogShape } from './blog';
7
+ import { dashboardShape } from './dashboard';
8
+ import type { ShapeOverlay } from './overlay';
9
+
10
+ export const SHAPE_OVERLAYS: { readonly [S in NewShape]: ShapeOverlay } = {
11
+ api: apiShape,
12
+ blog: blogShape,
13
+ dashboard: dashboardShape,
14
+ };
15
+
16
+ export type { ShapeEdit, ShapeOverlay } from './overlay';
17
+ export { applyEdit, applyOverlay, dropCatalogKeys } from './overlay';
@@ -0,0 +1,99 @@
1
+ // What a shape IS: an overlay over the one scaffold `x new` already writes — files it leaves out,
2
+ // catalog keys it drops, in-place edits, and files it adds. Never a second scaffold, so every fix to
3
+ // the default reaches every shape on the next release.
4
+
5
+ import { assert } from '@ultimat3/core';
6
+ import type { GeneratedFile, NameSet } from '../naming';
7
+
8
+ /** One in-place edit of a scaffold file: `find` must occur exactly once, or the overlay is stale. */
9
+ export interface ShapeEdit {
10
+ readonly path: string;
11
+ readonly find: string;
12
+ readonly replace: string;
13
+ }
14
+
15
+ export interface ShapeOverlay {
16
+ /** Which scaffold it starts from: `true` is the one with the example `post` slice. */
17
+ readonly example: boolean;
18
+ /** Path prefixes of the base scaffold this shape leaves out. */
19
+ readonly drop: readonly string[];
20
+ /** Dotted catalog keys (a whole subtree each) the dropped files were the only readers of. */
21
+ readonly dropKeys: readonly string[];
22
+ readonly edits: (app: NameSet) => readonly ShapeEdit[];
23
+ /** Added; a path the base already holds is REPLACED, never merged (catalogs merge as JSON). */
24
+ readonly files: (app: NameSet) => readonly GeneratedFile[];
25
+ /**
26
+ * `x g` argv run in the written app, in order, after these files land (`new-shapes-generate.ts`):
27
+ * a capability a generator owns arrives through it, never re-spelled here. Absent: none.
28
+ */
29
+ readonly generate?: readonly (readonly string[])[];
30
+ }
31
+
32
+ const CATALOG = 'packages/i18n/catalogs/en.json';
33
+
34
+ /** A stale overlay is the framework's bug, never the app's: the shape tests name which one. */
35
+ const FIX = 'bun test packages/cli/src/templates/shapes';
36
+
37
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
38
+ typeof value === 'object' && value !== null && !Array.isArray(value);
39
+
40
+ /** The catalog with each dotted key's subtree removed, re-serialised the way `catalogJson` does. */
41
+ export function dropCatalogKeys(contents: string, keys: readonly string[]): string {
42
+ const catalog: unknown = JSON.parse(contents);
43
+ assert(isRecord(catalog), `${CATALOG} is not a JSON object`, FIX);
44
+ for (const key of keys) {
45
+ const segments = key.split('.');
46
+ const leaf = segments.pop() ?? '';
47
+ let node: unknown = catalog;
48
+ for (const segment of segments) node = isRecord(node) ? node[segment] : undefined;
49
+ assert(
50
+ isRecord(node) && Object.hasOwn(node, leaf),
51
+ `a shape drops catalog key ${key}, which the base scaffold no longer writes`,
52
+ FIX,
53
+ );
54
+ delete node[leaf];
55
+ }
56
+ return `${JSON.stringify(catalog, null, 2)}\n`;
57
+ }
58
+
59
+ /** `edit` applied to `contents`; an edit whose anchor moved is refused rather than skipped. */
60
+ export function applyEdit(contents: string, edit: ShapeEdit): string {
61
+ const at = contents.indexOf(edit.find);
62
+ assert(
63
+ at !== -1 && contents.indexOf(edit.find, at + 1) === -1,
64
+ `a shape edits ${edit.path} at an anchor that is missing or not unique`,
65
+ FIX,
66
+ );
67
+ return contents.slice(0, at) + edit.replace + contents.slice(at + edit.find.length);
68
+ }
69
+
70
+ const dropped = (overlay: ShapeOverlay, path: string): boolean =>
71
+ overlay.drop.some((prefix) => path === prefix || path.startsWith(prefix));
72
+
73
+ /** The base scaffold with `overlay` applied, in the base's order, added files last. */
74
+ export function applyOverlay(
75
+ base: readonly GeneratedFile[],
76
+ overlay: ShapeOverlay,
77
+ app: NameSet,
78
+ ): readonly GeneratedFile[] {
79
+ const added = overlay.files(app);
80
+ const replaced = new Set(added.filter((file) => file.merge !== 'json').map((file) => file.path));
81
+ const edits = overlay.edits(app);
82
+ const kept = base
83
+ .filter((file) => !dropped(overlay, file.path) && !replaced.has(file.path))
84
+ .map((file): GeneratedFile => {
85
+ const own = edits.filter((edit) => edit.path === file.path);
86
+ if (file.path === CATALOG && file.merge === 'json') {
87
+ return { ...file, contents: dropCatalogKeys(file.contents, overlay.dropKeys) };
88
+ }
89
+ if (own.length === 0 || typeof file.contents !== 'string') return file;
90
+ return { ...file, contents: own.reduce(applyEdit, file.contents) };
91
+ });
92
+ const unapplied = edits.filter((edit) => !kept.some((file) => file.path === edit.path));
93
+ assert(
94
+ unapplied.length === 0,
95
+ `a shape edits ${unapplied.map((edit) => edit.path).join(', ')}, which the base scaffold does not write`,
96
+ FIX,
97
+ );
98
+ return [...kept, ...added];
99
+ }
@@ -0,0 +1,46 @@
1
+ // The edits more than one shape makes to the scaffold's own prose: the CLAUDE.md rows that point at
2
+ // files a shape leaves out, and the one line each shape adds to CLAUDE.md (AGENTS.md) and README.md
3
+ // naming itself.
4
+
5
+ import type { NewShape } from '../../new-shapes';
6
+ import type { ShapeEdit } from './overlay';
7
+
8
+ /** The "Copy from" cells of CLAUDE.md's test table that name a file this shape does not write. */
9
+ export const claudeRows = (instead: {
10
+ readonly page: string;
11
+ readonly island: string;
12
+ }): readonly ShapeEdit[] => [
13
+ {
14
+ path: 'CLAUDE.md',
15
+ find: '| `apps/web/app/dashboard/page.test.ts` |',
16
+ replace: `| ${instead.page} |`,
17
+ },
18
+ {
19
+ path: 'CLAUDE.md',
20
+ find: '| `apps/web/shared/shell.test.ts` |',
21
+ replace: '| `packages/ui/src/card.test.ts` |',
22
+ },
23
+ {
24
+ path: 'CLAUDE.md',
25
+ find: '| `apps/web/shared/theme-toggle.island.test.ts` |',
26
+ replace: `| ${instead.island} |`,
27
+ },
28
+ ];
29
+
30
+ const CLAUDE_ANCHOR =
31
+ 'There is no global `x`: every command here is `bunx x …`, the CLI this app installs.\n';
32
+ const README_ANCHOR = '## 🚀 Start\n\n';
33
+
34
+ /** One line in CLAUDE.md and one in README.md: which shape this app started as, and what it is. */
35
+ export const shapeLines = (shape: NewShape, what: string): readonly ShapeEdit[] => [
36
+ {
37
+ path: 'CLAUDE.md',
38
+ find: CLAUDE_ANCHOR,
39
+ replace: `${CLAUDE_ANCHOR}\nScaffolded as \`x new --shape ${shape}\`: ${what}.\n`,
40
+ },
41
+ {
42
+ path: 'README.md',
43
+ find: README_ANCHOR,
44
+ replace: `${README_ANCHOR}Scaffolded as \`--shape ${shape}\`: ${what}. Next: the three commands below, then \`bunx x g --help\`.\n\n`,
45
+ },
46
+ ];
@@ -0,0 +1,237 @@
1
+ // The tests `x g sign-in` writes for its actions: each handler RUN inside a real request context,
2
+ // over the app's migrated database (`migratedDatabase`, the framework tables included), so the
3
+ // paths a person takes — refused, signed in, a second factor owed, signed out — are what is held.
4
+ // `destroySessionTest` is both shapes'; the other two are the password shape's.
5
+
6
+ /** The app's migrated database installed under `db()` for the file, as `page-db.test.ts` does. */
7
+ const DATABASE = `// A template restore is ~0.5 s; its first build (initdb plus every migration) is what this bounds.
8
+ const BOOT_MS = 120_000;
9
+
10
+ const workerDatabase = reusableDatabase(() => migratedDatabase({ root: import.meta.dir }));
11
+ beforeAll(async () => {
12
+ setDbClient(await workerDatabase());
13
+ }, BOOT_MS);
14
+ // Back to memory, so this file's database never reaches the next file's unit tests.
15
+ afterAll(() => setDbClient(undefined));`;
16
+
17
+ /** The imports every one of the three files shares, after its own `@ultimat3/auth` line. */
18
+ const SHARED_IMPORTS = `import { migratedDatabase } from '@ultimat3/cli';
19
+ import { runWithContext } from '@ultimat3/core';
20
+ import { setDbClient } from '@ultimat3/db';
21
+ import { asCtx, defineHttpConfig, requestContext } from '@ultimat3/http';
22
+ import { afterAll, beforeAll, expect, reusableDatabase, unitTest } from '@ultimat3/testing';
23
+ import { appAuth } from '../auth';`;
24
+
25
+ /** The request the pipeline would build, the action run inside it, and both handed back. */
26
+ const requestOf = (
27
+ path: string,
28
+ headers: string,
29
+ call: string,
30
+ ): string => ` const request = requestContext({
31
+ url: new URL('https://app.test${path}'),
32
+ method: 'POST',
33
+ role: 'web',
34
+ config: defineHttpConfig({ dev: true, rateLimit: { scope: 'process' } }),
35
+ requestHeaders: ${headers},
36
+ });
37
+ const ctx = asCtx(request);
38
+ const output = await runWithContext(ctx, () => ${call});
39
+ return { output, request };`;
40
+
41
+ export const createSessionTest =
42
+ (): string => `// createSession, run inside a real request over the app's migrated database: a wrong password is
43
+ // refused, a right one sets the session cookie, and a second factor still owed holds the challenge.
44
+ import { generateTotpSecret, register, saveTotpSecret } from '@ultimat3/auth';
45
+ ${SHARED_IMPORTS}
46
+ import { MFA_CHALLENGE_COOKIE, SECOND_FACTOR_PATH } from '../sign-in-challenge';
47
+ import { AFTER_SIGN_IN } from '../sign-in-flow';
48
+ import { createSession } from './create-session';
49
+
50
+ ${DATABASE}
51
+
52
+ const target = createSession.named('createSession');
53
+ const PASSWORD = 'a long scaffold test passphrase';
54
+
55
+ /** One POST, as the pipeline hands it to the action: the response headers come back with it. */
56
+ const post = async (input: Parameters<typeof target>[0], accept = 'application/json') => {
57
+ ${requestOf('/auth/sign-in', '{ accept }', 'target(input, { ctx })')}
58
+ };
59
+
60
+ /** A person who can sign in, under an address no other test in this file uses. */
61
+ const person = (email: string) => register(appAuth(), { email, password: PASSWORD });
62
+
63
+ /** Whether the response sets cookie \`name\`. */
64
+ const sets = (request: { headers: Headers }, name: string): boolean =>
65
+ request.headers.getSetCookie().some((line) => line.startsWith(\`\${name}=\`));
66
+
67
+ unitTest('createSession is a declared action', () => {
68
+ expect(target.kind).toBe('action');
69
+ expect(target.describe().name).toBe('createSession');
70
+ });
71
+
72
+ unitTest('createSession takes what its form posts and refuses the rest', async () => {
73
+ await expect(target.input).toAcceptInput({ email: 'ada@example.com', password: 'a passphrase' });
74
+ await expect(target.input).toRejectInput({ email: 'not an address', password: 'a passphrase' });
75
+ });
76
+
77
+ unitTest('a wrong password is refused with the one credential error', async () => {
78
+ await person('wrong@example.com');
79
+ const input = { email: 'wrong@example.com', password: 'not the passphrase' };
80
+ const refused = await post(input).catch((error: unknown) => error);
81
+ expect(refused).toBeUltimateError('X_UNAUTHENTICATED');
82
+ });
83
+
84
+ unitTest('the right password sets the session cookie and lands where it was going', async () => {
85
+ await person('right@example.com');
86
+ const input = { email: 'right@example.com', password: PASSWORD, next: '/posts' };
87
+ const { output, request } = await post(input);
88
+ expect(output).toEqual({ ok: true, next: '/posts', secondFactor: false });
89
+ expect(sets(request, appAuth().sessions.policy.cookieName)).toBe(true);
90
+ // An agent posting JSON gets the path as data, and no redirect.
91
+ expect(request.redirect).toBeUndefined();
92
+ });
93
+
94
+ unitTest('a browser posting the form is sent on with a 303', async () => {
95
+ await person('form@example.com');
96
+ const input = { email: 'form@example.com', password: PASSWORD };
97
+ const { output, request } = await post(input, 'text/html');
98
+ expect(output.next).toBe(AFTER_SIGN_IN);
99
+ expect(request.redirect).toEqual({ location: AFTER_SIGN_IN, status: 303 });
100
+ });
101
+
102
+ unitTest('a second factor still owed holds the challenge and sets no session', async () => {
103
+ const user = await person('mfa@example.com');
104
+ await saveTotpSecret(appAuth(), user.id, generateTotpSecret());
105
+ const { output, request } = await post({ email: 'mfa@example.com', password: PASSWORD });
106
+ expect(output).toEqual({ ok: false, next: SECOND_FACTOR_PATH, secondFactor: true });
107
+ expect(sets(request, MFA_CHALLENGE_COOKIE)).toBe(true);
108
+ expect(sets(request, appAuth().sessions.policy.cookieName)).toBe(false);
109
+ });
110
+ `;
111
+
112
+ export const confirmSessionTest =
113
+ (): string => `// confirmSession, run inside a real request over the app's migrated database: with no challenge
114
+ // held it refuses, a wrong code is refused, and the held challenge plus the right code signs in.
115
+ import {
116
+ generateTotpSecret,
117
+ login,
118
+ register,
119
+ saveTotpSecret,
120
+ totpCode,
121
+ totpStep,
122
+ } from '@ultimat3/auth';
123
+ ${SHARED_IMPORTS}
124
+ import { challengeOf, MFA_CHALLENGE_COOKIE } from '../sign-in-challenge';
125
+ import { confirmSession } from './confirm-session';
126
+
127
+ ${DATABASE}
128
+
129
+ const target = confirmSession.named('confirmSession');
130
+ const PASSWORD = 'a long scaffold test passphrase';
131
+
132
+ /** One POST carrying \`cookie\`, as the pipeline hands it to the action. */
133
+ const post = async (input: Parameters<typeof target>[0], cookie?: string) => {
134
+ ${requestOf(
135
+ '/auth/sign-in/second-factor',
136
+ "{ accept: 'application/json', ...(cookie === undefined ? {} : { cookie }) }",
137
+ 'target(input, { ctx })',
138
+ )}
139
+ };
140
+
141
+ /** A person with a second factor, and the cookie holding the challenge the first leg sealed. */
142
+ const challenged = async (email: string) => {
143
+ const user = await register(appAuth(), { email, password: PASSWORD });
144
+ const secret = generateTotpSecret();
145
+ await saveTotpSecret(appAuth(), user.id, secret);
146
+ const first = await login(appAuth(), { email, password: PASSWORD }).catch((e: unknown) => e);
147
+ const challenge = challengeOf(first);
148
+ expect(challenge).not.toBeNull();
149
+ return { secret, cookie: \`\${MFA_CHALLENGE_COOKIE}=\${challenge}\` };
150
+ };
151
+
152
+ unitTest('confirmSession is a declared action', () => {
153
+ expect(target.kind).toBe('action');
154
+ expect(target.describe().name).toBe('confirmSession');
155
+ });
156
+
157
+ unitTest('confirmSession takes what its form posts and refuses the rest', async () => {
158
+ await expect(target.input).toAcceptInput({ code: '123456' });
159
+ await expect(target.input).toRejectInput({});
160
+ });
161
+
162
+ unitTest('a code with no challenge held is refused before anything is checked', async () => {
163
+ const refused = await post({ code: '123456' }).catch((error: unknown) => error);
164
+ expect(refused).toBeUltimateError('X_UNAUTHENTICATED');
165
+ });
166
+
167
+ unitTest('a wrong code against the held challenge is refused, and signs nobody in', async () => {
168
+ const { cookie } = await challenged('guess@example.com');
169
+ // Not six digits, so it is read as a recovery code — and this person was issued none.
170
+ const refused = await post({ code: 'not-a-recovery-code' }, cookie).catch((e: unknown) => e);
171
+ expect(refused).toBeUltimateError('X_UNAUTHENTICATED');
172
+ });
173
+
174
+ unitTest('the held challenge and the right code sign in, and drop the challenge', async () => {
175
+ const { secret, cookie } = await challenged('second@example.com');
176
+ const code = totpCode(secret, totpStep(new Date()));
177
+ const { output, request } = await post({ code, next: '/posts' }, cookie);
178
+ expect(output).toEqual({ ok: true, next: '/posts' });
179
+ const session = appAuth().sessions.policy.cookieName;
180
+ const lines = request.headers.getSetCookie();
181
+ expect(lines.some((line) => line.startsWith(\`\${session}=\`))).toBe(true);
182
+ // Cleared, an empty value: the challenge is spent, and a reload starts the sign-in over.
183
+ expect(lines.some((line) => line.startsWith(\`\${MFA_CHALLENGE_COOKIE}=;\`))).toBe(true);
184
+ });
185
+ `;
186
+
187
+ export const destroySessionTest =
188
+ (): string => `// destroySession, run inside a real request over the app's migrated database: a live session is
189
+ // revoked, and a caller with none still signs out — cleared, and landed at the home page.
190
+ import { login, register } from '@ultimat3/auth';
191
+ ${SHARED_IMPORTS}
192
+ import { destroySession } from './destroy-session';
193
+
194
+ ${DATABASE}
195
+
196
+ const target = destroySession.named('destroySession');
197
+
198
+ /** One sign-out POST carrying \`cookie\`, as the pipeline hands it to the action. */
199
+ const signOut = async (accept: string, cookie?: string) => {
200
+ ${requestOf(
201
+ '/auth/sign-out',
202
+ '{ accept, ...(cookie === undefined ? {} : { cookie }) }',
203
+ "target({ confirm: 'sign-out' }, { ctx })",
204
+ )}
205
+ };
206
+
207
+ unitTest('destroySession is a declared action off the MCP surface', () => {
208
+ expect(target.kind).toBe('action');
209
+ expect(target.describe().name).toBe('destroySession');
210
+ });
211
+
212
+ unitTest('it takes the one statement a sign-out form sends', async () => {
213
+ await expect(target.input).toAcceptInput({ confirm: 'sign-out' });
214
+ await expect(target.input).toRejectInput({});
215
+ await expect(target.input).toRejectInput({ confirm: 'yes' });
216
+ });
217
+
218
+ unitTest('a live session is revoked, and a second sign-out finds nothing to revoke', async () => {
219
+ const password = 'a long scaffold test passphrase';
220
+ await register(appAuth(), { email: 'leaving@example.com', password });
221
+ const { token } = await login(appAuth(), { email: 'leaving@example.com', password });
222
+ const cookie = \`\${appAuth().sessions.policy.cookieName}=\${token}\`;
223
+ const { output } = await signOut('application/json', cookie);
224
+ expect(output).toEqual({ ok: true, next: '/', revoked: true });
225
+ expect((await signOut('application/json', cookie)).output.revoked).toBe(false);
226
+ });
227
+
228
+ unitTest('with no session it still signs out, and a browser lands at the home page', async () => {
229
+ const { output, request } = await signOut('text/html');
230
+ expect(output).toEqual({ ok: true, next: '/', revoked: false });
231
+ expect(request.redirect).toEqual({ location: '/', status: 303 });
232
+ // The expired session cookie and the cleared browser state, whether or not a session existed.
233
+ const expired = \`\${appAuth().sessions.policy.cookieName}=;\`;
234
+ expect(request.headers.getSetCookie().some((line) => line.startsWith(expired))).toBe(true);
235
+ expect(request.headers.get('clear-site-data')).not.toBeNull();
236
+ });
237
+ `;
@@ -0,0 +1,112 @@
1
+ // The authenticator once sign-in exists: a session cookie is its user and no session is nobody, in
2
+ // every environment — `x new`'s development viewer (`dev-actor.ts`) and its fallback line are gone,
3
+ // because a developer now signs in the way everyone does. `x g sign-in --oidc` swaps it in, and only
4
+ // over the files `x new` wrote byte for byte: an author's edit is never overwritten.
5
+
6
+ import type { GeneratedFile } from './naming';
7
+ import { AUTH_DIR } from './sign-in-shared';
8
+
9
+ const authenticatorSource =
10
+ (): string => `// Who a request is: the session its cookie names, resolved by \`@ultimat3/auth\` — the one
11
+ // authenticator this app installs, in every environment. Everything downstream (pages, policies,
12
+ // live subscribers, MCP tools) reads the actor this returns. With no session the request is
13
+ // anonymous, everywhere: a developer signs in through \`/auth/sign-in\` like everyone else.
14
+ import { type Auth, authenticate } from '@ultimat3/auth';
15
+ import type { Actor } from '@ultimat3/core';
16
+ import { configureAuthenticator } from '@ultimat3/http';
17
+ import { appAuth } from './auth';
18
+
19
+ /** The one method this reads off a request. */
20
+ export interface SessionRequest {
21
+ cookie(name: string): string | null;
22
+ }
23
+
24
+ /**
25
+ * A session cookie is that session's user, or a refusal — never a fallback to anyone else:
26
+ * \`X_SESSION_EXPIRED\` past its expiry, \`X_UNAUTHENTICATED\` for one unknown, revoked or forged.
27
+ */
28
+ export const sessionAuthenticator =
29
+ (auth: () => Auth) =>
30
+ async (request: SessionRequest): Promise<Actor | null> => {
31
+ const resolved = auth();
32
+ const token = request.cookie(resolved.sessions.policy.cookieName);
33
+ return token !== null && token.length > 0 ? await authenticate(resolved, token) : null;
34
+ };
35
+
36
+ /** Installs the session authenticator. */
37
+ export function installAuthenticator(auth: () => Auth = appAuth): void {
38
+ configureAuthenticator(sessionAuthenticator(auth));
39
+ }
40
+
41
+ // Module scope, which IS the wiring: the boot scan imports every module under \`apps/*\` before a
42
+ // listener binds, and \`x dev\` and the container both read the configured value back at start.
43
+ installAuthenticator();
44
+ `;
45
+
46
+ const authenticatorTest =
47
+ (): string => `// A session cookie is the user it was issued to; a bad one is refused; none is nobody.
48
+ import { defineAuth, login, memoryAuthAdapter, register } from '@ultimat3/auth';
49
+ import { configuredAuthenticator, resetAuthenticator } from '@ultimat3/http';
50
+ import { expect, unitTest } from '@ultimat3/testing';
51
+ import { DEMO_ORG_ID } from '../../shared/demo-org';
52
+ import { installAuthenticator, sessionAuthenticator } from './authenticator';
53
+
54
+ const memoryAuth = () => {
55
+ const auth = defineAuth({ adapter: memoryAuthAdapter() });
56
+ return () => auth;
57
+ };
58
+
59
+ const requestWith = (cookies: Readonly<Record<string, string>>) => ({
60
+ cookie: (name: string) => cookies[name] ?? null,
61
+ });
62
+
63
+ const PASSWORD = 'a long scaffold test passphrase';
64
+
65
+ unitTest('a session cookie is the user it was issued to', async () => {
66
+ const auth = memoryAuth();
67
+ const user = await register(auth(), {
68
+ email: 'ada@example.com',
69
+ password: PASSWORD,
70
+ orgId: DEMO_ORG_ID,
71
+ roles: ['member'],
72
+ });
73
+ const { token } = await login(auth(), { email: 'ada@example.com', password: PASSWORD });
74
+ const actor = await sessionAuthenticator(auth)(
75
+ requestWith({ [auth().sessions.policy.cookieName]: token }),
76
+ );
77
+ expect(actor?.id).toBe(user.id);
78
+ expect(actor?.roles).toEqual(['member']);
79
+ });
80
+
81
+ unitTest('a forged cookie is refused, and no cookie is anonymous', async () => {
82
+ const auth = memoryAuth();
83
+ const forged = requestWith({ [auth().sessions.policy.cookieName]: 'forged.token' });
84
+ await expect(sessionAuthenticator(auth)(forged)).rejects.toMatchObject({
85
+ code: 'X_UNAUTHENTICATED',
86
+ });
87
+ expect(await sessionAuthenticator(auth)(requestWith({}))).toBeNull();
88
+ });
89
+
90
+ unitTest('it installs', () => {
91
+ try {
92
+ installAuthenticator(memoryAuth());
93
+ expect(configuredAuthenticator()).toBeDefined();
94
+ } finally {
95
+ resetAuthenticator();
96
+ }
97
+ });
98
+ `;
99
+
100
+ /** The two files the swap writes over `x new`'s, when those are still `x new`'s. */
101
+ export function signedInAuthenticatorFiles(): readonly GeneratedFile[] {
102
+ return [
103
+ { path: `${AUTH_DIR}/authenticator.ts`, contents: authenticatorSource() },
104
+ { path: `${AUTH_DIR}/authenticator.test.ts`, contents: authenticatorTest() },
105
+ ];
106
+ }
107
+
108
+ /** The development viewer the swap deletes, with the authenticator's fallback that asked it. */
109
+ export const DEV_ACTOR_FILES: readonly string[] = [
110
+ `${AUTH_DIR}/dev-actor.ts`,
111
+ `${AUTH_DIR}/dev-actor.test.ts`,
112
+ ];