@starci/hfs 4.0.10 → 4.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 (271) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +3 -2
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +187 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +221 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/removed-vocabulary.mjs +28 -0
  14. package/runtime/engine/resources-config.mjs +52 -0
  15. package/runtime/engine/runtime-root.mjs +17 -0
  16. package/runtime/engine/secrets.mjs +35 -31
  17. package/runtime/engine/sonar-config.mjs +21 -0
  18. package/runtime/engine/temp-root.mjs +32 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  20. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  21. package/runtime/knowledge/hfs/rules.yaml +98 -65
  22. package/runtime/knowledge/hfs/slots.yaml +11 -14
  23. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  24. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  25. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  26. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  27. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +92 -256
  31. package/runtime/modules/models/tiers.yaml +65 -0
  32. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  33. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  34. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  35. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  36. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  37. package/runtime/scripts/api/git/lib.mjs +34 -1
  38. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  39. package/runtime/scripts/api/process/run-program.mjs +2 -1
  40. package/runtime/scripts/api/sops/lib.mjs +198 -120
  41. package/runtime/scripts/hfs/allows.mjs +3 -3
  42. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  43. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  44. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  45. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  47. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  48. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  49. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  50. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  51. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  52. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  53. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  54. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  55. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  56. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  57. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  58. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  59. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  60. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  61. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  62. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  63. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  65. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  66. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  67. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  69. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  70. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  71. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  72. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  74. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  75. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  76. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  77. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  78. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  79. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  80. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  81. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  82. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  83. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  84. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  85. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  86. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  87. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  88. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  89. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  90. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  91. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  92. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  93. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  94. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  95. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  96. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  97. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  98. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  99. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  100. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  101. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  102. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  103. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  104. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  105. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  113. package/runtime/scripts/hfs/architecture.mjs +3 -4
  114. package/runtime/scripts/hfs/check.mjs +61 -70
  115. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  116. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  117. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  118. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  119. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  120. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  121. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  122. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  123. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  124. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  125. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  126. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  127. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  128. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  129. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  130. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  131. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  132. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  133. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  134. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  135. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  136. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  137. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  138. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  139. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  140. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  141. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  142. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  143. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  144. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  145. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  146. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  147. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  148. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  149. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  150. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  151. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  152. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  153. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  154. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  155. package/runtime/scripts/hfs/secret.mjs +59 -48
  156. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  157. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  158. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  159. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  160. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  161. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  162. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  163. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  165. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  166. package/runtime/scripts/hfs/slots.mjs +72 -693
  167. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  168. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  169. package/runtime/scripts/hfs/tree.mjs +19 -14
  170. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  171. package/runtime/scripts/hfs/view.mjs +1 -1
  172. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  173. package/runtime/scripts/lib/event-contract.mjs +17 -13
  174. package/runtime/scripts/lib/git.mjs +2 -2
  175. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  176. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  177. package/runtime/scripts/lib/i18n.mjs +3 -2
  178. package/runtime/scripts/lib/in-order.mjs +71 -0
  179. package/runtime/scripts/lib/language.mjs +3 -3
  180. package/runtime/scripts/lib/list.mjs +8 -1
  181. package/runtime/scripts/lib/path-key.mjs +38 -5
  182. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  183. package/runtime/scripts/lib/regex.mjs +1 -1
  184. package/runtime/scripts/lib/same-text.mjs +1 -1
  185. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  186. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  187. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  188. package/runtime/scripts/lib/stack-services.mjs +1 -2
  189. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  190. package/runtime/scripts/lib/walk.mjs +6 -1
  191. package/scaffold/add-cli-lite.mjs +2 -1
  192. package/scaffold/add-table.mjs +2 -2
  193. package/scaffold/add.mjs +3 -2
  194. package/scaffold/app.mjs +4 -2
  195. package/scaffold/edition-gate.mjs +25 -25
  196. package/scaffold/lite-exports.mjs +2 -1
  197. package/scaffold/service.mjs +5 -4
  198. package/sync/hygiene.mjs +15 -9
  199. package/sync/index.mjs +7 -3
  200. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  201. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  202. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  203. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  204. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  205. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  206. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  207. package/templates/app/starciwork.gitignore +1 -1
  208. package/templates/be/image/api/Dockerfile +1 -1
  209. package/templates/be/image/cli/Dockerfile +1 -1
  210. package/templates/be/image/worker/Dockerfile +1 -1
  211. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  212. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  213. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  214. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  215. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  216. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  217. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  218. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  219. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  220. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  221. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  222. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  223. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  224. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  225. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  226. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  227. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  228. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  229. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  230. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  231. package/templates/fe/image/next/Dockerfile +2 -1
  232. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  233. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  234. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  235. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  236. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  238. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  239. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  241. package/upgrade/index.mjs +9 -2
  242. package/runtime/engine/admission.mjs +0 -308
  243. package/runtime/engine/canonical-json.mjs +0 -10
  244. package/runtime/engine/db/blob.mjs +0 -315
  245. package/runtime/engine/db/ledger-paths.mjs +0 -83
  246. package/runtime/engine/db/ledger.mjs +0 -1118
  247. package/runtime/engine/db/machine-connection.mjs +0 -135
  248. package/runtime/engine/db/machine-schema.mjs +0 -84
  249. package/runtime/engine/db/machine.mjs +0 -1367
  250. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  251. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  252. package/runtime/engine/db/provider-reservations.mjs +0 -101
  253. package/runtime/engine/digest.mjs +0 -16
  254. package/runtime/engine/refuse.mjs +0 -11
  255. package/runtime/modules/ops/_labels.yaml +0 -53
  256. package/runtime/scripts/api/node/lib.mjs +0 -14
  257. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  258. package/runtime/scripts/api/process/lib.mjs +0 -111
  259. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  260. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  261. package/runtime/scripts/connectors/lib.mjs +0 -488
  262. package/runtime/scripts/lib/clip.mjs +0 -19
  263. package/runtime/scripts/lib/display-names.mjs +0 -259
  264. package/runtime/scripts/lib/example-refs.mjs +0 -158
  265. package/runtime/scripts/lib/json-schema.mjs +0 -52
  266. package/runtime/scripts/lib/process-identity.mjs +0 -6
  267. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  268. package/runtime/scripts/lib/redact.mjs +0 -161
  269. package/runtime/scripts/lib/sleep.mjs +0 -7
  270. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  271. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -20,12 +20,12 @@ jobs:
20
20
  env:
21
21
  SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
22
22
  steps:
23
- - uses: actions/checkout@v4
24
- - uses: actions/setup-node@v4
23
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
24
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
25
25
  with:
26
26
  node-version: {{nodeMajor}}
27
27
  cache: npm
28
- - run: npm ci
28
+ - run: npm ci --ignore-scripts
29
29
  - name: lint
30
30
  if: env.SONAR_TOKEN == ''
31
31
  run: npm run lint
@@ -47,12 +47,12 @@ jobs:
47
47
  db-types:
48
48
  runs-on: ubuntu-latest
49
49
  steps:
50
- - uses: actions/checkout@v4
51
- - uses: actions/setup-node@v4
50
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
51
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
52
52
  with:
53
53
  node-version: {{nodeMajor}}
54
54
  cache: npm
55
- - run: npm ci
55
+ - run: npm ci --ignore-scripts
56
56
  - name: start Supabase
57
57
  run: npm run db:start
58
58
  - name: apply migrations
@@ -21,11 +21,11 @@ jobs:
21
21
  SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
22
22
  SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
23
23
  steps:
24
- - uses: actions/checkout@v4
25
- - uses: actions/setup-node@v4
24
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
25
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
26
26
  with:
27
27
  node-version: {{nodeMajor}}
28
28
  cache: npm
29
- - run: npm ci
29
+ - run: npm ci --ignore-scripts
30
30
  - name: push migrations
31
31
  run: npm run db:push
@@ -25,9 +25,9 @@ jobs:
25
25
  include:
26
26
  {{imageMatrix}}
27
27
  steps:
28
- - uses: actions/checkout@v4
29
- - uses: docker/setup-buildx-action@v3
30
- - uses: docker/build-push-action@v6
28
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
29
+ - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
30
+ - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
31
31
  with:
32
32
  context: .
33
33
  file: ${{ matrix.file }}
@@ -1,4 +1,4 @@
1
- # StarCi .starciwork: product content only (ARCHITECTURE-DB §5.1, work-layout.yaml shape.productPaths).
1
+ # StarCi .starciwork: product content only (work-layout.yaml shape.productPaths).
2
2
  # Agent output (reports, checks, captures, draw rounds, UAT runs, logs, ledgers, worktrees) lives in
3
3
  # runtime.sqlite rows and blobs outside the repository.
4
4
  /*
@@ -19,7 +19,7 @@ RUN npm ci --ignore-scripts
19
19
 
20
20
  COPY be/tsconfig.json be/tsconfig.build.json be/
21
21
  COPY be/src be/src
22
- COPY be/apps/{{app}} be/apps/{{app}}
22
+ {{copyBeInputs}}COPY be/apps/{{app}} be/apps/{{app}}
23
23
  RUN npm run build:be
24
24
 
25
25
  FROM {{nodeImage}} AS runtime
@@ -20,7 +20,7 @@ RUN npm ci --ignore-scripts
20
20
 
21
21
  COPY be/tsconfig.json be/tsconfig.build.json be/
22
22
  COPY be/src be/src
23
- COPY be/apps/{{app}} be/apps/{{app}}
23
+ {{copyBeInputs}}COPY be/apps/{{app}} be/apps/{{app}}
24
24
  RUN npm run build:be
25
25
 
26
26
  FROM {{nodeImage}} AS runtime
@@ -19,7 +19,7 @@ RUN npm ci --ignore-scripts
19
19
 
20
20
  COPY be/tsconfig.json be/tsconfig.build.json be/
21
21
  COPY be/src be/src
22
- COPY be/apps/{{app}} be/apps/{{app}}
22
+ {{copyBeInputs}}COPY be/apps/{{app}} be/apps/{{app}}
23
23
  RUN npm run build:be
24
24
 
25
25
  FROM {{nodeImage}} AS runtime
@@ -8,7 +8,7 @@ describe("@@Group@@Cli", () => {
8
8
  const help = jest.fn()
9
9
  Object.defineProperty(group, "command", { value: { help } })
10
10
 
11
- await expect(group.run()).resolves.toBeUndefined()
11
+ await expect(Promise.resolve(group.run())).resolves.toBeUndefined()
12
12
 
13
13
  expect(help).toHaveBeenCalledTimes(1)
14
14
  })
@@ -5,7 +5,7 @@ import { RunCli } from "./subs/run.cli"
5
5
  /** `cli @@group@@`: the @@group@@ group; its commands are the sub-commands (`cli @@group@@ run`). */
6
6
  export class @@Group@@Cli extends CommandRunner {
7
7
  /** Without a sub-command the group shows its help. */
8
- async run(): Promise<void> {
9
- this.command.help()
8
+ run(): Promise<void> {
9
+ return this.command.help()
10
10
  }
11
11
  }
@@ -197,6 +197,19 @@ describe("EventRunnerService", () => {
197
197
  ])
198
198
  })
199
199
 
200
+ it("buries once and runs no later consumer of the event when an envelope cannot be read", async () => {
201
+ const { runner, handle, transport } = await build()
202
+ const later = jest.fn<Promise<void>, Array<never>>()
203
+ runner.add({ event: PingEvent, handle: later })
204
+ const value = JSON.stringify({ eventId: "o-2", eventName: "probe.ping", payload: {} })
205
+
206
+ await runner.receive(message({ key: "o-2", value }))
207
+
208
+ expect(handle).not.toHaveBeenCalled()
209
+ expect(later).not.toHaveBeenCalled()
210
+ expect(transport.send).toHaveBeenCalledTimes(1)
211
+ })
212
+
200
213
  it("puts a failed delivery on the retry topic with the next attempt and a doubled backoff", async () => {
201
214
  const { runner, handle, transport, logger } = await build()
202
215
  const failure = new Error("the database is down")
@@ -4,6 +4,7 @@ import { InjectClock } from "@modules/platform/clock"
4
4
  import type { Clock } from "@modules/platform/clock"
5
5
  import { InjectLogger } from "@modules/platform/logging"
6
6
  import type { Logger } from "@modules/platform/logging"
7
+ import { eachInOrder } from "@modules/platform/primitives"
7
8
  import type { BaseEvent } from "./event-bus.contracts"
8
9
  import { InjectEventBusOptions, InjectEventTransport } from "./event-bus.decorators"
9
10
  import { EventBusLogEvent } from "./event-bus.log-events"
@@ -74,9 +75,12 @@ export class EventRunnerService implements EventConsumerRegistry, OnApplicationB
74
75
  const pause = notBefore - this.clock.now().getTime()
75
76
  if (pause > 0) await this.transport.wait(pause)
76
77
  const failures: Array<unknown> = []
77
- for (const consumer of consumers) {
78
+ let buried = false
79
+ await eachInOrder(consumers, async (consumer) => {
80
+ if (buried) return
78
81
  const event = consumer.event.parse(envelope)
79
82
  if (event === null) {
83
+ buried = true
80
84
  await this.bury(message, eventName, attempt, "the envelope does not have the shape of the event")
81
85
  return
82
86
  }
@@ -86,7 +90,8 @@ export class EventRunnerService implements EventConsumerRegistry, OnApplicationB
86
90
  this.logger.error(EventBusLogEvent.DeliveryFailed, cause, { event: eventName, attempt })
87
91
  failures.push(cause)
88
92
  }
89
- }
93
+ })
94
+ if (buried) return
90
95
  const [first] = failures
91
96
  if (failures.length > 0) {
92
97
  await this.failed(message, eventName, attempt, first instanceof Error ? first.message : String(first))
@@ -23,7 +23,10 @@ export const REQUEUED_HEADER = "requeued"
23
23
  export const DEAD_LETTER_ID_SEPARATOR = "|"
24
24
 
25
25
  /** The service an event belongs to: the part of its name before the first dot. */
26
- export const serviceOf = (eventName: string): string => eventName.replace(/\..*$/s, "")
26
+ export const serviceOf = (eventName: string): string => {
27
+ const dot = eventName.indexOf(".")
28
+ return dot === -1 ? eventName : eventName.slice(0, dot)
29
+ }
27
30
 
28
31
  /** The topic the events of a service travel on. */
29
32
  export const topicOf = (prefix: string, eventName: string): string => `${prefix}events.${serviceOf(eventName)}`
@@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common"
2
2
  import type { OnApplicationShutdown } from "@nestjs/common"
3
3
  import { InjectIds } from "@modules/platform/ids"
4
4
  import type { Ids } from "@modules/platform/ids"
5
+ import { eachInOrder } from "@modules/platform/primitives"
5
6
  import type { Probe } from "@modules/platform/probes"
6
7
  import { EventBusError, EventBusErrorCode } from "./errors/event-bus.error"
7
8
  import { InjectEventBusOptions, InjectKafkaFactory } from "./event-bus.decorators"
@@ -181,7 +182,7 @@ export class KafkaEventTransportClient implements EventTransport, Probe, OnAppli
181
182
 
182
183
  /** Disconnects the producer, the consumer and the admin client. */
183
184
  async onApplicationShutdown(): Promise<void> {
184
- for (const client of [this.producer, this.consumer, this.admin]) await this.close(client)
185
+ await eachInOrder([this.producer, this.consumer, this.admin], (client) => this.close(client))
185
186
  }
186
187
 
187
188
  /** The end offsets of the topic; a topic that does not exist yet answers no partitions and the cause. */
@@ -1,6 +1,7 @@
1
1
  import type { OnApplicationBootstrap, OnApplicationShutdown } from "@nestjs/common"
2
2
  import type { EntityManager } from "typeorm"
3
3
  import type { Logger } from "@modules/platform/logging"
4
+ import { eachInOrder } from "@modules/platform/primitives"
4
5
  import { OutboxLogEvent } from "./outbox.log-events"
5
6
 
6
7
  /**
@@ -28,7 +29,9 @@ export abstract class OutboxRelayPolicy<Row extends { readonly id: string }>
28
29
  /** One pass over every connection: relays the oldest waiting rows of each and answers how many it relayed. */
29
30
  async relay(): Promise<number> {
30
31
  let relayed = 0
31
- for (const manager of this.managers) relayed += await this.relayOf(manager)
32
+ await eachInOrder(this.managers, async (manager) => {
33
+ relayed += await this.relayOf(manager)
34
+ })
32
35
  return relayed
33
36
  }
34
37
 
@@ -66,13 +69,25 @@ export abstract class OutboxRelayPolicy<Row extends { readonly id: string }>
66
69
  })
67
70
  }
68
71
 
69
- private async run(): Promise<void> {
70
- while (this.running) {
71
- const relayed = await this.relay().catch((cause: unknown) => {
72
- this.logger.error(OutboxLogEvent.RelayFailed, cause, { outbox: this.outbox })
73
- return 0
74
- })
75
- if (relayed === 0 && this.running) await this.idle()
76
- }
72
+ /** One pass, then the next, until the app stops: each pass starts from the settled promise of the last, so the loop holds no growing chain. */
73
+ private run(): Promise<void> {
74
+ return new Promise<void>((resolve, reject) => {
75
+ const next = (): void => {
76
+ if (!this.running) {
77
+ resolve()
78
+ return
79
+ }
80
+ void this.pass().then(next, reject)
81
+ }
82
+ next()
83
+ })
84
+ }
85
+
86
+ private async pass(): Promise<void> {
87
+ const relayed = await this.relay().catch((cause: unknown) => {
88
+ this.logger.error(OutboxLogEvent.RelayFailed, cause, { outbox: this.outbox })
89
+ return 0
90
+ })
91
+ if (relayed === 0 && this.running) await this.idle()
77
92
  }
78
93
  }
@@ -4,6 +4,7 @@ import { InjectClock } from "@modules/platform/clock"
4
4
  import type { Clock } from "@modules/platform/clock"
5
5
  import { InjectLogger } from "@modules/platform/logging"
6
6
  import type { Logger } from "@modules/platform/logging"
7
+ import { eachInOrder } from "@modules/platform/primitives"
7
8
  import { OutboxRelayPolicy } from "@modules/platform/outbox"
8
9
  import { InjectQueueOptions, InjectQueueRelayManagers, InjectQueueTransport } from "./queue.decorators"
9
10
  import type { QueueOptions } from "./queue.options"
@@ -34,8 +35,8 @@ export class QueueRelayService extends OutboxRelayPolicy<QueueRow> {
34
35
  return tx.query(SELECT_WAITING_ROWS, [this.options.relayBatch])
35
36
  }
36
37
 
37
- protected async deliver(rows: ReadonlyArray<QueueRow>): Promise<void> {
38
- for (const row of rows) await this.transport.add(row.queue, row.id, row.payload)
38
+ protected deliver(rows: ReadonlyArray<QueueRow>): Promise<void> {
39
+ return eachInOrder(rows, (row) => this.transport.add(row.queue, row.id, row.payload))
39
40
  }
40
41
 
41
42
  protected async mark(tx: EntityManager, rows: ReadonlyArray<QueueRow>): Promise<void> {
@@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common"
2
2
  import type { OnApplicationBootstrap, OnApplicationShutdown } from "@nestjs/common"
3
3
  import { InjectLogger } from "@modules/platform/logging"
4
4
  import type { Logger } from "@modules/platform/logging"
5
+ import { eachInOrder } from "@modules/platform/primitives"
5
6
  import type { QueueHandler } from "./queue.contracts"
6
7
  import { InjectQueueOptions, InjectQueueTransport } from "./queue.decorators"
7
8
  import { QueueLogEvent } from "./queue.log-events"
@@ -30,12 +31,14 @@ export class QueueWorkerService implements QueueWorkerRegistry, OnApplicationBoo
30
31
 
31
32
  /** Starts one worker per registered queue, then registers the schedulers of the options. */
32
33
  async onApplicationBootstrap(): Promise<void> {
33
- for (const handler of this.handlers.values()) await this.transport.work(handler, this.options.concurrency)
34
- for (const scheduler of this.options.schedulers) {
35
- await this.transport.upsertScheduler(scheduler).catch((cause: unknown) => {
34
+ await eachInOrder([...this.handlers.values()], (handler) =>
35
+ this.transport.work(handler, this.options.concurrency),
36
+ )
37
+ await eachInOrder(this.options.schedulers, (scheduler) =>
38
+ this.transport.upsertScheduler(scheduler).catch((cause: unknown) => {
36
39
  this.logger.error(QueueLogEvent.SchedulerFailed, cause, { scheduler: scheduler.id })
37
- })
38
- }
40
+ }),
41
+ )
39
42
  }
40
43
 
41
44
  /** Closes the workers and the queues. */
@@ -10,7 +10,7 @@ describe("MigrateCli", () => {
10
10
  const help = jest.fn()
11
11
  Object.defineProperty(group, "command", { value: { help } })
12
12
 
13
- await expect(group.run()).resolves.toBeUndefined()
13
+ await expect(Promise.resolve(group.run())).resolves.toBeUndefined()
14
14
 
15
15
  expect(help).toHaveBeenCalledTimes(1)
16
16
  })
@@ -9,7 +9,7 @@ import { RunCli } from "./subs/run.cli"
9
9
  /** `cli migrate`: the migration group; its commands are the sub-commands (`cli migrate run`). */
10
10
  export class MigrateCli extends CommandRunner {
11
11
  /** Without a sub-command the group shows its help. */
12
- async run(): Promise<void> {
13
- this.command.help()
12
+ run(): Promise<void> {
13
+ return this.command.help()
14
14
  }
15
15
  }
@@ -10,7 +10,7 @@ describe("SeedCli", () => {
10
10
  const help = jest.fn()
11
11
  Object.defineProperty(group, "command", { value: { help } })
12
12
 
13
- await expect(group.run()).resolves.toBeUndefined()
13
+ await expect(Promise.resolve(group.run())).resolves.toBeUndefined()
14
14
 
15
15
  expect(help).toHaveBeenCalledTimes(1)
16
16
  })
@@ -9,7 +9,7 @@ import { RunSeedsCli } from "./subs/run.cli"
9
9
  /** `cli seed`: the seed group; its commands are the sub-commands (`cli seed run`). */
10
10
  export class SeedCli extends CommandRunner {
11
11
  /** Without a sub-command the group shows its help. */
12
- async run(): Promise<void> {
13
- this.command.help()
12
+ run(): Promise<void> {
13
+ return this.command.help()
14
14
  }
15
15
  }
@@ -1,3 +1,4 @@
1
+ import { eachInOrder } from "@modules/platform/primitives"
1
2
  import type { DatabaseConnectionOptions } from "./database.options"
2
3
  import type { ConnectionOpener } from "./database.port"
3
4
 
@@ -16,7 +17,7 @@ export async function migrateConnections(
16
17
  opener: ConnectionOpener,
17
18
  ): Promise<AppliedMigrations> {
18
19
  const applied: Record<string, ReadonlyArray<string>> = {}
19
- for (const connection of connections) {
20
+ await eachInOrder(connections, async (connection) => {
20
21
  const source = opener.open(connection)
21
22
  await source.initialize()
22
23
  try {
@@ -25,6 +26,6 @@ export async function migrateConnections(
25
26
  } finally {
26
27
  await source.destroy()
27
28
  }
28
- }
29
+ })
29
30
  return applied
30
31
  }
@@ -1,5 +1,6 @@
1
1
  import { readFile, readdir } from "node:fs/promises"
2
2
  import { join } from "node:path"
3
+ import { eachInOrder } from "@modules/platform/primitives"
3
4
  import { seedText } from "./database.sql"
4
5
  import type { SqlText } from "./database.sql"
5
6
  import type { DatabaseConnectionOptions } from "./database.options"
@@ -56,18 +57,18 @@ export async function seedConnections(
56
57
  opener: ConnectionOpener,
57
58
  ): Promise<SeedReport> {
58
59
  const applied: Record<string, ReadonlyArray<string>> = {}
59
- for (const connection of connections) {
60
+ await eachInOrder(connections, async (connection) => {
60
61
  const own = files.filter((file) => seeds(file, connection))
61
- if (own.length === 0) continue
62
+ if (own.length === 0) return
62
63
  const source = opener.open(connection)
63
64
  await source.initialize()
64
65
  try {
65
- for (const file of own) await source.query(file.text)
66
+ await eachInOrder(own, (file) => source.query(file.text))
66
67
  } finally {
67
68
  await source.destroy()
68
69
  }
69
70
  applied[connection.name] = own.map((file) => file.name)
70
- }
71
+ })
71
72
  const unmatched = files
72
73
  .filter((file) => !connections.some((connection) => seeds(file, connection)))
73
74
  .map((file) => file.name)
@@ -1,2 +1,3 @@
1
1
  export type { Outcome } from "./outcome.contracts"
2
2
  export { ok, refused, unwrapOutcome } from "./outcome.mapper"
3
+ export { eachInOrder } from "./sequence.policy"
@@ -0,0 +1,47 @@
1
+ import { DomainError } from "@modules/platform/errors"
2
+ import { eachInOrder } from "./sequence.policy"
3
+
4
+ type SequenceErrorCode = "STEP_FAILED"
5
+
6
+ class SequenceError extends DomainError<SequenceErrorCode> {}
7
+
8
+ const pause = (): Promise<void> => new Promise((resolve) => setImmediate(resolve))
9
+
10
+ describe("eachInOrder", () => {
11
+ it("starts an item only after the one before it settled, in item order", async () => {
12
+ const events: Array<string> = []
13
+
14
+ await eachInOrder(new Set(["a", "b", "c"]), async (item, index) => {
15
+ events.push(`start ${item} ${index}`)
16
+ await pause()
17
+ events.push(`end ${item}`)
18
+ })
19
+
20
+ expect(events).toEqual(["start a 0", "end a", "start b 1", "end b", "start c 2", "end c"])
21
+ })
22
+
23
+ it("answers nothing for an empty list", async () => {
24
+ await expect(eachInOrder([], () => Promise.resolve())).resolves.toBeUndefined()
25
+ })
26
+
27
+ it("rejects with the first failure and never starts the items after it", async () => {
28
+ const started: Array<number> = []
29
+
30
+ const run = eachInOrder([1, 2, 3], async (item) => {
31
+ started.push(item)
32
+ await pause()
33
+ if (item === 2) throw new SequenceError({ code: "STEP_FAILED" })
34
+ })
35
+
36
+ await expect(run).rejects.toThrow(expect.objectContaining({ code: "STEP_FAILED" }))
37
+ expect(started).toEqual([1, 2])
38
+ })
39
+
40
+ it("rejects when a step throws before it returns a promise", async () => {
41
+ const run = eachInOrder([1], (): Promise<void> => {
42
+ throw new SequenceError({ code: "STEP_FAILED" })
43
+ })
44
+
45
+ await expect(run).rejects.toThrow(expect.objectContaining({ code: "STEP_FAILED" }))
46
+ })
47
+ })
@@ -0,0 +1,16 @@
1
+ /** One step of a sequence: it receives the item and its index and answers a promise that settles when the step is done. */
2
+ type SequenceStep<Item> = (item: Item, index: number) => Promise<unknown>
3
+
4
+ /**
5
+ * Runs `step` over `items` one at a time: a step starts only after the one before it settled, so the order the caller relies on
6
+ * (ordered writes, one connection at a time, stop at the first failure) is the order in the code. The first rejection (or throw)
7
+ * rejects the answer and the items after it never start.
8
+ */
9
+ export const eachInOrder = <Item>(items: Iterable<Item>, step: SequenceStep<Item>): Promise<void> =>
10
+ [...items].reduce<Promise<void>>(
11
+ (done, item, index) =>
12
+ done.then(async () => {
13
+ await step(item, index)
14
+ }),
15
+ Promise.resolve(),
16
+ )
@@ -18,6 +18,7 @@ RUN npm ci --ignore-scripts
18
18
 
19
19
  COPY be/tsconfig.json be/tsconfig.build.json be/
20
20
  COPY be/src be/src
21
+ COPY supabase/types supabase/types
21
22
  COPY be/apps/api be/apps/api
22
23
  RUN npm run build:be
23
24
 
@@ -0,0 +1,2 @@
1
+ export type { Outcome } from "./outcome.contracts"
2
+ export { ok, refused, unwrapOutcome } from "./outcome.mapper"
@@ -23,8 +23,9 @@ COPY turbo.json ./
23
23
  COPY scripts scripts
24
24
  COPY fe/tsconfig.json fe/tsconfig.json
25
25
  {{copyInputs}}COPY fe/apps/{{app}} fe/apps/{{app}}
26
+ RUN mkdir -p fe/apps/{{app}}/public
26
27
  RUN npm run codegen --silent
27
- RUN npx turbo run build --filter=@{{project}}/{{app}}
28
+ RUN node_modules/.bin/turbo run build --filter=@{{project}}/{{app}}
28
29
 
29
30
  FROM {{nodeImage}} AS runtime
30
31
  WORKDIR /app
@@ -1,7 +1,7 @@
1
- import { AppHomePage, appHomeMetadata } from "@/features/pages/AppHomePage"
1
+ import { AppHomePage } from "@/features/pages/AppHomePage"
2
2
 
3
3
  /** The document title of this page, from the catalog of the requested locale. */
4
- export const generateMetadata = appHomeMetadata
4
+ export { appHomeMetadata as generateMetadata } from "@/features/pages/AppHomePage"
5
5
 
6
6
  /** The locale root's page slot: it mounts the product app's home page and nothing else. */
7
7
  const Page = () => <AppHomePage />
@@ -1,7 +1,7 @@
1
- import { LandingPage, landingMetadata } from "@/features/pages/LandingPage"
1
+ import { LandingPage } from "@/features/pages/LandingPage"
2
2
 
3
3
  /** The document title of this page, from the catalog of the requested locale. */
4
- export const generateMetadata = landingMetadata
4
+ export { landingMetadata as generateMetadata } from "@/features/pages/LandingPage"
5
5
 
6
6
  /** The locale root's page slot: it mounts the landing page and nothing else. */
7
7
  const Page = () => <LandingPage />
@@ -1,7 +1,7 @@
1
1
  import "server-only"
2
2
  import { hasLocale } from "next-intl"
3
3
  import { createNavigation } from "next-intl/navigation"
4
- import { getMessages, getRequestConfig, setRequestLocale } from "next-intl/server"
4
+ import { getMessages, getRequestConfig } from "next-intl/server"
5
5
  import { notFound } from "next/navigation"
6
6
  import { locale as routeLocale } from "next/root-params"
7
7
  import { PRODUCT_TIME_ZONE, routing } from "./routing"
@@ -43,7 +43,6 @@ export const createAppI18n = (app: string, loadMessages: LoadMessages) => ({
43
43
  readLocaleSegment: async (params: Promise<{ readonly locale: string }>) => {
44
44
  const { locale } = await params
45
45
  if (!hasLocale(routing.locales, locale)) notFound()
46
- setRequestLocale(locale)
47
46
  return { locale, messages: await getMessages(), timeZone: PRODUCT_TIME_ZONE }
48
47
  },
49
48
  })
@@ -1,5 +1,5 @@
1
1
  import { hasLocale } from "next-intl"
2
- import { getMessages, setRequestLocale } from "next-intl/server"
2
+ import { getMessages } from "next-intl/server"
3
3
  import { notFound } from "next/navigation"
4
4
  import type { ReactNode } from "react"
5
5
  import { AppLayout, appLayoutMetadata } from "@/features/layouts/AppLayout"
@@ -20,7 +20,6 @@ export const generateMetadata = async (props: LayoutProps) => appLayoutMetadata(
20
20
  const Layout = async (props: LayoutProps) => {
21
21
  const { locale } = await props.params
22
22
  if (!hasLocale(routing.locales, locale)) notFound()
23
- setRequestLocale(locale)
24
23
  const messages = await getMessages()
25
24
  return (
26
25
  <AppLayout
@@ -1,7 +1,7 @@
1
- import { AppHomePage, appHomeMetadata } from "@/features/pages/AppHomePage"
1
+ import { AppHomePage } from "@/features/pages/AppHomePage"
2
2
 
3
3
  /** The document title of this page. */
4
- export const generateMetadata = appHomeMetadata
4
+ export { appHomeMetadata as generateMetadata } from "@/features/pages/AppHomePage"
5
5
 
6
6
  /** The locale root's page slot. */
7
7
  const Page = () => <AppHomePage />
@@ -8,6 +8,12 @@ import { SignInFormBase } from "./component"
8
8
 
9
9
  type SignInFailure = "refused" | "not-found" | "invalid" | "unavailable"
10
10
 
11
+ /** The state the form shows: working while a request runs, failed after a refusal, else ready. */
12
+ const stateOf = (pending: boolean, failure: SignInFailure | undefined): "working" | "ready" | "failed" => {
13
+ if (pending) return "working"
14
+ return failure === undefined ? "ready" : "failed"
15
+ }
16
+
11
17
  /** The interactive sign-in form wired to the schema-checked Server Action. */
12
18
  export const SignInForm = () => {
13
19
  const router = navigation.useRouter()
@@ -27,7 +33,7 @@ export const SignInForm = () => {
27
33
  }
28
34
  return (
29
35
  <SignInFormBase
30
- state={pending ? "working" : failure === undefined ? "ready" : "failed"}
36
+ state={stateOf(pending, failure)}
31
37
  props={{
32
38
  title: t("title"),
33
39
  emailLabel: t("email"),
@@ -9,9 +9,9 @@ import type { DbOutcome } from "../outcome"
9
9
  /** Ends the cookie-backed Supabase session, including an already-anonymous one. */
10
10
  export const writeSignOut = async (): Promise<DbOutcome<true>> => {
11
11
  const principal = await getPrincipal()
12
- void principal
13
12
  const client = await createServerDbClient()
14
13
  const result = await client.auth.signOut()
15
14
  if (result.error === null) return dbOk(true)
15
+ if (principal.kind !== "ok" && result.error.status === 401) return dbOk(true)
16
16
  return dbFailure(result.error.status === 401 ? "refused" : "unavailable", result.error.code ?? "sign-out")
17
17
  }
@@ -12,7 +12,15 @@ interface SignInInput {
12
12
  type SignInParse = { readonly success: true; readonly data: SignInInput } | { readonly success: false }
13
13
 
14
14
  const UUID = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/u
15
- const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u
15
+ const ADDRESS = /^[^\s@]+@([^\s@]+)$/u
16
+
17
+ /** Whether the text is one local part, one `@` and a domain with a dot that has a character on each side; linear in the length. */
18
+ const isEmail = (value: string): boolean => {
19
+ const domain = ADDRESS.exec(value)?.[1]
20
+ if (domain === undefined) return false
21
+ const dot = domain.indexOf(".", 1)
22
+ return dot !== -1 && dot < domain.length - 1
23
+ }
16
24
 
17
25
  /** The bounded UUID schema shared by generated table Server Actions. */
18
26
  export const rowSchema = {
@@ -38,7 +46,7 @@ export const signInInputSchema = {
38
46
  if (
39
47
  typeof email !== "string" ||
40
48
  email.length > 254 ||
41
- !EMAIL.test(email) ||
49
+ !isEmail(email) ||
42
50
  typeof password !== "string" ||
43
51
  password.length < 8 ||
44
52
  password.length > 256