@starci/hfs 4.3.0 → 4.4.1

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 (538) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/db/migrate.mjs +361 -0
  4. package/db/post-cutover.mjs +136 -0
  5. package/emit/contracts.mjs +2 -2
  6. package/emit/db-types.mjs +12 -3
  7. package/lint/run.mjs +3 -3
  8. package/package.json +4 -1
  9. package/release/gate.mjs +298 -0
  10. package/release/index.mjs +103 -0
  11. package/release/push.mjs +110 -0
  12. package/report/sonar.mjs +1 -1
  13. package/runtime/config.example.yaml +7 -6
  14. package/runtime/engine/config.mjs +10 -18
  15. package/runtime/engine/removed-vocabulary.mjs +0 -2
  16. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  17. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  18. package/runtime/knowledge/hfs/rules.yaml +2855 -163
  19. package/runtime/knowledge/hfs/slots.yaml +333 -62
  20. package/runtime/knowledge/patterns/be/api.yaml +32 -5
  21. package/runtime/knowledge/patterns/be/cli.yaml +14 -4
  22. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  23. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  24. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  25. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  26. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  27. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  28. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  29. package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
  30. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  31. package/runtime/knowledge/sonar-gate.yaml +1 -1
  32. package/runtime/modules/kernel/failure-codes.yaml +3091 -43
  33. package/runtime/modules/kernel/removed-vocabulary.yaml +30 -0
  34. package/runtime/modules/models/runtimes.yaml +5 -7
  35. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  36. package/runtime/packages/cli/src/catalog.generated.mjs +13769 -0
  37. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  38. package/runtime/scripts/api/git/add.mjs +5 -0
  39. package/runtime/scripts/api/git/init.mjs +7 -0
  40. package/runtime/scripts/api/git/lib.mjs +4 -1
  41. package/runtime/scripts/api/git/tag.mjs +5 -0
  42. package/runtime/scripts/api/node/lib.mjs +20 -0
  43. package/runtime/scripts/api/node/run-node.mjs +6 -0
  44. package/runtime/scripts/api/sops/lib.mjs +42 -1
  45. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  46. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  47. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  48. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  49. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  50. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  51. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  52. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  53. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  54. package/runtime/scripts/hfs/architecture/config.mjs +1 -1
  55. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  56. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  57. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  58. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  59. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  60. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  61. package/runtime/scripts/hfs/architecture/index.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  63. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  64. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  65. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  66. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  67. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  68. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  69. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  70. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  71. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  72. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  73. package/runtime/scripts/hfs/architecture/tiers.mjs +4 -4
  74. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  75. package/runtime/scripts/hfs/architecture.mjs +1 -1
  76. package/runtime/scripts/hfs/check-results.mjs +28 -0
  77. package/runtime/scripts/hfs/check.mjs +46 -47
  78. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  79. package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
  80. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  81. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  82. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  83. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  84. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  85. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  86. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  87. package/runtime/scripts/hfs/manifest-shape.mjs +69 -9
  88. package/runtime/scripts/hfs/param-names.mjs +1 -1
  89. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  90. package/runtime/scripts/hfs/rule-catalog.mjs +158 -11
  91. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  92. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  93. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  94. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  95. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  96. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  97. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  98. package/runtime/scripts/hfs/rules/cli.mjs +1 -1
  99. package/runtime/scripts/hfs/rules/contract-compat.mjs +1 -1
  100. package/runtime/scripts/hfs/rules/contract.mjs +67 -6
  101. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  102. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  103. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  104. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  105. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  106. package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
  107. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  108. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  109. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  110. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  111. package/runtime/scripts/hfs/rules/database.mjs +9 -0
  112. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  113. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  114. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  115. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  116. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  117. package/runtime/scripts/hfs/rules/edition.mjs +9 -8
  118. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  119. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  120. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  121. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  122. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  123. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  124. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  125. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  126. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  127. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  128. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  129. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  130. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  131. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  132. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  133. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  134. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  135. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  136. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  137. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  138. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  139. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  140. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  141. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  142. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  143. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
  144. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  145. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +2 -2
  146. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  147. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  148. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  149. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  150. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  151. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  152. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  153. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  154. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  155. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  156. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  157. package/runtime/scripts/hfs/secret-init.mjs +127 -19
  158. package/runtime/scripts/hfs/secret-sops.mjs +38 -6
  159. package/runtime/scripts/hfs/secret-store.mjs +56 -54
  160. package/runtime/scripts/hfs/secret.mjs +34 -5
  161. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  162. package/runtime/scripts/hfs/slots.mjs +3 -2
  163. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  164. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  165. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  166. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  167. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  168. package/runtime/scripts/hfs/typescript-programs.mjs +0 -8
  169. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  170. package/runtime/scripts/lib/compose-values.mjs +11 -0
  171. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  172. package/runtime/scripts/lib/env.mjs +2 -2
  173. package/runtime/scripts/lib/git.mjs +1 -1
  174. package/runtime/scripts/lib/i18n.mjs +0 -2
  175. package/runtime/scripts/lib/json-clone.mjs +61 -0
  176. package/runtime/scripts/lib/language.mjs +1 -1
  177. package/runtime/scripts/lib/list.mjs +4 -0
  178. package/runtime/scripts/lib/roots.mjs +2 -4
  179. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  180. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  181. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  182. package/runtime/scripts/lib/stack-services.mjs +1 -1
  183. package/runtime/scripts/lib/swarm-stack.mjs +5 -18
  184. package/runtime/scripts/lib/trim-end.mjs +8 -0
  185. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  186. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  187. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  188. package/scaffold/add-app.mjs +77 -59
  189. package/scaffold/add-cli-lite.mjs +14 -6
  190. package/scaffold/add-compose.mjs +79 -0
  191. package/scaffold/add-database-lite.mjs +14 -0
  192. package/scaffold/add-fe-overlay.mjs +1 -0
  193. package/scaffold/add-fe-page.mjs +1 -0
  194. package/scaffold/add-inbox-lite.mjs +74 -42
  195. package/scaffold/add-table.mjs +110 -87
  196. package/scaffold/add.mjs +159 -44
  197. package/scaffold/app.mjs +124 -39
  198. package/scaffold/edition-gate.mjs +3 -3
  199. package/scaffold/lite-exports.mjs +2 -1
  200. package/scaffold/service.mjs +2 -2
  201. package/scaffold/supabase-ports.mjs +1 -1
  202. package/src/main.mjs +229 -116
  203. package/stack/age-install.mjs +37 -0
  204. package/stack/backup.mjs +100 -0
  205. package/stack/build-args.mjs +42 -0
  206. package/stack/build.mjs +173 -0
  207. package/stack/cli.mjs +130 -0
  208. package/stack/config.mjs +57 -0
  209. package/stack/context.mjs +57 -0
  210. package/stack/converge.mjs +107 -0
  211. package/stack/deploy.mjs +301 -0
  212. package/stack/digest.mjs +16 -0
  213. package/stack/docker-transport.mjs +53 -0
  214. package/stack/edge-alias.mjs +118 -0
  215. package/stack/errors.mjs +21 -0
  216. package/stack/image-scan.mjs +152 -0
  217. package/stack/ingress-create.mjs +114 -0
  218. package/stack/ingress-publish.mjs +273 -0
  219. package/stack/ingress.mjs +706 -0
  220. package/stack/jobs.mjs +51 -0
  221. package/stack/live-restore.mjs +177 -0
  222. package/stack/prepare.mjs +266 -0
  223. package/stack/private-file.mjs +22 -0
  224. package/stack/provision.mjs +90 -0
  225. package/stack/prune.mjs +46 -0
  226. package/stack/release-store.mjs +87 -0
  227. package/stack/remote.mjs +205 -0
  228. package/stack/render.mjs +149 -0
  229. package/stack/retire.mjs +47 -0
  230. package/stack/sealed.mjs +149 -0
  231. package/stack/task.mjs +42 -0
  232. package/stack/verify.mjs +99 -0
  233. package/sync/ci-commands.mjs +13 -0
  234. package/sync/format.mjs +1 -1
  235. package/sync/hygiene.mjs +1 -1
  236. package/sync/index.mjs +110 -54
  237. package/sync/stack-images.mjs +127 -0
  238. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  239. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  240. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  241. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  242. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  243. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  244. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  245. package/templates/app/codegen-graphql.mjs.partial +13 -2
  246. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  247. package/templates/app/codegen-prelude.mjs.partial +8 -0
  248. package/templates/app/gitignore +3 -0
  249. package/templates/app/gitignore-lite +3 -0
  250. package/templates/app/package-scripts/package.json +1 -0
  251. package/templates/app/package-scripts/supabase.json +7 -0
  252. package/templates/app/package-scripts-lite/package.json +1 -7
  253. package/templates/app/skeleton/.editorconfig +1 -0
  254. package/templates/app/skeleton/.gitattributes +7 -1
  255. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  256. package/templates/app/skeleton/.nvmrc +1 -1
  257. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  258. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  259. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  260. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  261. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  262. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  263. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  264. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  265. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  266. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  267. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  268. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  269. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  270. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  271. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  272. package/templates/app/skeleton/README.md +6 -6
  273. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  274. package/templates/app/skeleton-lite/.editorconfig +16 -0
  275. package/templates/app/skeleton-lite/.gitattributes +8 -0
  276. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  277. package/templates/app/skeleton-lite/.nvmrc +1 -0
  278. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  279. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
  280. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  281. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  282. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  283. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  284. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  285. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  286. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  287. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  288. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  289. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  290. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  291. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  292. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  293. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  294. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  295. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  296. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  297. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  298. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  299. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  300. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  301. package/templates/app/skeleton-lite/README.md +5 -3
  302. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  303. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  304. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  305. package/templates/be/image/worker/Dockerfile +1 -1
  306. package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
  307. package/templates/be/inbox/database-client.ts.tpl +12 -11
  308. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  309. package/templates/be/inbox/event-index.ts.tpl +1 -0
  310. package/templates/be/inbox/events.json.tpl +10 -0
  311. package/templates/be/inbox/migration.sql.tpl +1 -0
  312. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  313. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  314. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  315. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  316. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  317. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  318. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  319. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  320. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  321. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  322. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  323. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  324. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  325. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  326. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  327. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  328. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  329. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  330. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  331. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  332. package/templates/be/inbox/service.spec.ts.tpl +42 -9
  333. package/templates/be/inbox/service.ts.tpl +23 -8
  334. package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
  335. package/templates/be/inbox/webhook-door.ts.tpl +2 -2
  336. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  337. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  338. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  339. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  340. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  341. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  342. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  343. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  344. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  345. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  346. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  347. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  348. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  349. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  350. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  351. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  352. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  353. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  354. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  355. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  356. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  357. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  358. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  359. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  360. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  362. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  363. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  364. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  365. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  366. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  367. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  368. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  369. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  370. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  371. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  372. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  373. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  374. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  375. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  376. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  377. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  378. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  379. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  380. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  381. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  382. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  383. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  384. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  385. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  386. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  387. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  388. package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
  389. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  390. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  391. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
  392. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  393. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  394. package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
  395. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  396. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
  397. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  398. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  399. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  400. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  401. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  402. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  403. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  404. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  405. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  406. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  407. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  408. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  409. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  410. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  411. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  412. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  413. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  414. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  415. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  416. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  417. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  418. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  419. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  420. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  421. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  422. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  423. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  424. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  425. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  426. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  427. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  428. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  429. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  430. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  431. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  432. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  433. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  434. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  435. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  436. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  437. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  438. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  439. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  440. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  441. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  442. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  443. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  444. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  445. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  446. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  447. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  448. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  449. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  450. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  451. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  452. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  453. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  454. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  455. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  456. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  457. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  458. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  459. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  460. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  461. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  462. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  463. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  464. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  465. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  466. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  467. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  468. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  469. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
  470. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  471. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  472. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  473. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
  474. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  475. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  476. package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
  477. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  478. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  479. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  480. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  481. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  482. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  483. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  484. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  485. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  486. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  487. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  488. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  489. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  490. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  491. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  492. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  493. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  494. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  495. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  496. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  497. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  498. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  499. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  500. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  501. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  502. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  503. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  504. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  505. package/templates/be/table/migration.sql.tpl +1 -0
  506. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  507. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  508. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  509. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  510. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  511. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  512. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  513. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  514. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  515. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  516. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  517. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  518. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  519. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  520. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  521. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  522. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  523. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  524. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  525. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  526. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  527. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  528. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  529. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  530. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  531. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  532. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  533. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  534. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  535. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  536. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  537. package/upgrade/check.mjs +3 -1
  538. package/upgrade/index.mjs +54 -41
@@ -12,7 +12,7 @@ export const unquoteDiffPath = (value) =>
12
12
 
13
13
  // git's repository-local variables (git rev-parse --local-env-vars). A hook or alias run in a linked worktree exports
14
14
  // GIT_DIR=<main>/.git/worktrees/<wt>; a spec inheriting it pointed every fixture git at the LIVE .claude repo and re-inited
15
- // it core.bare=true (2026-09-29, a6f60352c). tests/setup/isolated-registry.mjs drops the same list.
15
+ // it core.bare=true (2026-09-29, a6f60352c). e2e/setup/isolated-registry.mjs drops the same list.
16
16
  export const GIT_LOCAL_ENV_VARS = ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_IMPLICIT_WORK_TREE', 'GIT_PREFIX', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_COUNT', 'GIT_GRAFT_FILE', 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_SHALLOW_FILE', 'GIT_INTERNAL_SUPER_PREFIX'];
17
17
  /** `parent` (the caller's env) without git's repository-local variables (GIT_CONFIG_KEY_n/VALUE_n go with GIT_CONFIG_COUNT). Pure. */
18
18
  export function withoutGitLocalEnv(parent) {
@@ -49,8 +49,6 @@ export function loadCatalog(root = skillRoot, { scope = 'runtime' } = {}) {
49
49
  return cache.get(key);
50
50
  }
51
51
 
52
- /** Forget the cached catalogs (a spec that rewrites a catalog file). */
53
- export const resetCatalogCache = () => cache.clear();
54
52
 
55
53
  /** The names each `pattern` match captures (group 1) in `text`, in match order (`sort` sorts them). */
56
54
  export const captureNames = (text, pattern, { sort = false } = {}) => {
@@ -0,0 +1,61 @@
1
+ // json-clone.mjs — a deep copy that keeps exactly what survives a JSON text and nothing else: the plain-data
2
+ // copy of a value, detached from every reference, frozen state and class of the original. `structuredClone` keeps
3
+ // what JSON drops (undefined members, Dates, Maps, sparse holes) and refuses what JSON accepts (functions,
4
+ // `toJSON` objects), so it is not a substitute; this walks the value once under the JSON.stringify rules.
5
+
6
+ /** The boxed primitives JSON.stringify unwraps: recognised by the internal slot a valueOf call reads, as JSON does. */
7
+ const BOXED = [
8
+ { slot: Number.prototype.valueOf, read: Number },
9
+ { slot: String.prototype.valueOf, read: String },
10
+ { slot: Boolean.prototype.valueOf, read: (box) => Boolean.prototype.valueOf.call(box) },
11
+ { slot: BigInt.prototype.valueOf, read: (box) => BigInt.prototype.valueOf.call(box) },
12
+ ];
13
+
14
+ /** Whether `value` carries the internal slot `slot` reads (a valueOf of that boxed type does not throw on it). */
15
+ const hasSlot = (slot, value) => {
16
+ try { slot.call(value); return true; } catch { return false; }
17
+ };
18
+
19
+ /** What JSON.stringify takes of `holder[key]`: the toJSON result when there is one, a boxed primitive unwrapped. */
20
+ const serialized = (holder, key) => {
21
+ let value = holder[key];
22
+ if (value !== null && (typeof value === 'object' || typeof value === 'bigint') && typeof value.toJSON === 'function') value = value.toJSON(key);
23
+ if (value === null || typeof value !== 'object') return value;
24
+ const box = BOXED.find((candidate) => hasSlot(candidate.slot, value));
25
+ return box ? box.read(value) : value;
26
+ };
27
+
28
+ /** A member JSON leaves out of an object (and writes as null in an array). */
29
+ const isAbsent = (value) => value === undefined || typeof value === 'function' || typeof value === 'symbol';
30
+
31
+ const copyValue = (value, ancestors) => {
32
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
33
+ if (typeof value === 'number') return Number.isFinite(value) ? value + 0 : null;
34
+ if (typeof value === 'bigint') throw new TypeError('Do not know how to serialize a BigInt');
35
+ if (ancestors.includes(value)) throw new TypeError('Converting circular structure to JSON');
36
+ const inside = [...ancestors, value];
37
+ if (Array.isArray(value)) {
38
+ return Array.from({ length: value.length }, (_, index) => {
39
+ const item = serialized(value, String(index));
40
+ return isAbsent(item) ? null : copyValue(item, inside);
41
+ });
42
+ }
43
+ const entries = [];
44
+ for (const name of Object.keys(value)) {
45
+ const member = serialized(value, name);
46
+ if (!isAbsent(member)) entries.push([name, copyValue(member, inside)]);
47
+ }
48
+ return Object.fromEntries(entries);
49
+ };
50
+
51
+ /**
52
+ * `value` copied as JSON data: members that are undefined, functions or symbols are dropped (an array item becomes
53
+ * null), non-finite numbers become null and -0 becomes 0, `toJSON` results replace their objects (a Date becomes its ISO string), boxed
54
+ * primitives unwrap, and a cycle or a BigInt throws a TypeError. A value JSON has no text for (undefined, a function,
55
+ * a symbol) throws a SyntaxError, as parsing the missing text does.
56
+ */
57
+ export function jsonClone(value) {
58
+ const top = serialized({ '': value }, '');
59
+ if (isAbsent(top)) throw new SyntaxError('"undefined" is not valid JSON');
60
+ return copyValue(top, []);
61
+ }
@@ -76,7 +76,7 @@ const I18N_CATALOG_DIR = 'modules/i18n/messages/';
76
76
  const OP_MANIFEST_VIETNAMESE_FIELDS = Object.freeze({ fields: Object.freeze(['vi']) });
77
77
  const OP_MANIFEST_DIR = 'modules/ops/ops/';
78
78
  /** The declared Vietnamese fields of `rel`: its own entry, or the op-manifest text field for a manifest of modules/ops/ops. */
79
- export const declaredVietnameseFieldsOf = (rel) => DECLARED_VIETNAMESE_FIELDS[rel]
79
+ const declaredVietnameseFieldsOf = (rel) => DECLARED_VIETNAMESE_FIELDS[rel]
80
80
  ?? (rel.startsWith(I18N_CATALOG_DIR) && !rel.slice(I18N_CATALOG_DIR.length).includes('/') && /\.ya?ml$/.test(rel) ? I18N_CATALOG_VIETNAMESE_FIELDS : null)
81
81
  ?? (rel.startsWith(OP_MANIFEST_DIR) && !rel.slice(OP_MANIFEST_DIR.length).includes('/') && /\.ya?ml$/.test(rel) ? OP_MANIFEST_VIETNAMESE_FIELDS : null);
82
82
 
@@ -17,6 +17,10 @@ export const objectList = (v) => (Array.isArray(v) ? v : []).filter((item) => it
17
17
  export const stringList = (v, { blank = true } = {}) =>
18
18
  Array.isArray(v) && v.every((s) => typeof s === 'string' && s.length > 0 && (blank || s.trim().length > 0));
19
19
 
20
+ /** A non-empty list of distinct values, all accepted by the supplied predicate. */
21
+ export const nonemptyUniqueList = (values, accepts) => Array.isArray(values) && values.length > 0
22
+ && values.every((entry) => accepts(entry)) && new Set(values).size === values.length;
23
+
20
24
  /** The items of a `sep`-separated list: trimmed, empties dropped; `dedupe` keeps first occurrences only. */
21
25
  export const splitList = (v, { sep = ',', dedupe = false } = {}) => {
22
26
  const items = String(v ?? '').split(sep).map((s) => s.trim()).filter(Boolean);
@@ -17,16 +17,14 @@
17
17
  // directory or from the Work directory name outside this module.
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
- import { skillRoot, runtimeStateDir, starciLocalRoot, starciSourceRoot } from '../../engine/runtime-root.mjs';
20
+ import { skillRoot, starciLocalRoot, starciSourceRoot } from '../../engine/runtime-root.mjs';
21
21
  import { tempRoot } from '../../engine/temp-root.mjs';
22
22
 
23
23
  export { tempRoot, starciLocalRoot, starciSourceRoot };
24
24
 
25
25
  /** The runtime tree this code runs from. */
26
- export const runtimeTree = skillRoot;
26
+ const runtimeTree = skillRoot;
27
27
 
28
- /** The state directory of a runtime tree (default: this one). */
29
- export const runtimeState = (tree = runtimeTree) => runtimeStateDir(tree);
30
28
 
31
29
  /** The name of the Work records directory inside a product tree. */
32
30
  export const WORK_DIR_NAME = '.starciwork';
@@ -0,0 +1,13 @@
1
+ // Identify runtime-owned example apps; an unrelated directory named examples grants no fixture behavior.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ export function isRuntimeExampleApp(repoRoot) {
6
+ if (path.basename(path.dirname(repoRoot)) !== 'examples') return false;
7
+ try {
8
+ const runtime = JSON.parse(fs.readFileSync(path.resolve(repoRoot, '..', '..', 'hfs.json'), 'utf8'));
9
+ return runtime.kind === 'runtime';
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
@@ -12,6 +12,57 @@ export const FORBIDDEN_FILES = [
12
12
  { name: 'starcistacks-secret-plaintext', test: (f) => /(^|\/)\.starcistacks\/[^/]+\/secrets\//i.test(f) && !/(\.enc|\/\.gitkeep|\/KEYS\.md)$/i.test(f) },
13
13
  { name: 'credentials-json', test: (f) => /(^|\/)(credentials|service-account|client_secret)[^/]*\.json$/i.test(f) },
14
14
  ];
15
+ // `word` in any case (name checks are case-aware by construction, not by flag).
16
+ const fold = (word) => word.replace(/[a-z]/gi, (c) => `[${c.toLowerCase()}${c.toUpperCase()}]`);
17
+
18
+ // A name that holds secret material: the keyword is an identifier segment, so DB_PASSWORD, dbPassword,
19
+ // OPENAI_API_KEY, api_secret and accessToken are candidates while `secondary`, `tokenize`, `monkey`, `secrets.x`
20
+ // and `$secret` are not. A segment starts at the name edge, after `_`/`-`/`.`, or at a camel hump.
21
+ const SECRET_WORD = [
22
+ 'password', 'passwd', 'passphrase', 'secret', 'credentials?', 'token',
23
+ 'api[_-]?key', 'access[_-]?token', 'auth[_-]?token', 'refresh[_-]?token', 'id[_-]?token', 'session[_-]?token',
24
+ 'client[_-]?secret', 'api[_-]?secret', 'session[_-]?secret', 'jwt[_-]?secret',
25
+ 'private[_-]?key', 'secret[_-]?key', 'signing[_-]?key', 'encryption[_-]?key', 'master[_-]?key',
26
+ 'service[_-]?role[_-]?key', 'service[_-]?key', 'session[_-]?key',
27
+ ].map((word) => fold(word)).join('|');
28
+
29
+ // A name whose last segment is configuration, not material: SECRET_FILE and *_PATH point at the materialized
30
+ // secret file (the materializer's own form), PASSWORD_MIN_LENGTH is a number, *_REF/_ID a reference.
31
+ const BENIGN_WORD = [
32
+ 'file', 'path', 'dir', 'url', 'uri', 'endpoint', 'host', 'refs?', 'names?', 'ids?', 'arn', 'urn',
33
+ 'length', 'min', 'max', 'size', 'count', 'budget', 'limit', 'regex', 'pattern', 'policy',
34
+ 'algorithm', 'scheme', 'expiry', 'expires', 'ttl', 'required', 'enabled', 'disabled', 'type', 'version',
35
+ ].map((word) => fold(word)).join('|');
36
+
37
+ const SEGMENT_BEFORE = String.raw`(?:(?<![A-Za-z0-9_$])|(?<=[_\-.])|(?<=[a-z])(?=[A-Z]))`;
38
+ const NAME_REST = '[A-Za-z0-9_$.-]*?';
39
+ const SECRET_NAME = [
40
+ '(?<![A-Za-z0-9_$])',
41
+ String.raw`(?!${NAME_REST}${SEGMENT_BEFORE}(?:${BENIGN_WORD})(?=\s*[:=]))`,
42
+ String.raw`(?=${NAME_REST}${SEGMENT_BEFORE}(?:${SECRET_WORD})(?=[_\-.]|[A-Z]|[^A-Za-z0-9_$]|$))`,
43
+ String.raw`[A-Za-z_$][A-Za-z0-9_$.-]*\s*[:=]\s*`,
44
+ ].join('');
45
+
46
+ // `password = "hunter2hunter2"` — the quoted literal. `env(NAME)` is a reference, never a secret.
47
+ const ASSIGNED_QUOTED = new RegExp(
48
+ SECRET_NAME + String.raw`['"\`](?!env\([A-Z_][A-Z0-9_]*\)['"\`])([^'"\`\n\s$<{]{12,})['"\`]`,
49
+ );
50
+ // `DB_PASSWORD=hunter2hunter2` — the compose list-item and dotenv form; the dotenv value has no `:` so
51
+ // `password: secret:db/password` and `token: ref:vault/api` stay references, and the end boundary keeps
52
+ // calls and member/index references such as `secret = credentialsSource.current` out of the literals.
53
+ const ASSIGNED_UNQUOTED = new RegExp(
54
+ SECRET_NAME + String.raw`(?!['"\`]|env\()([A-Za-z0-9_+=-]{12,})(?![A-Za-z0-9_+=.(\[-])`,
55
+ );
56
+
57
+ const NAME_WORD = (word, tail) => new RegExp(`^${NAME_REST}${SEGMENT_BEFORE}(?:${word})${tail}`);
58
+ const SECRET_NAME_TEST = NAME_WORD(SECRET_WORD, String.raw`(?:[_\-.]|[A-Z]|[^A-Za-z0-9_$]|$)`);
59
+ const BENIGN_TAIL_TEST = NAME_WORD(BENIGN_WORD, '$');
60
+
61
+ /** True when a bare variable `name` holds secret material as an identifier segment (DB_PASSWORD, dbPassword,
62
+ * SUPABASE_SERVICE_ROLE_KEY) and does not end in a config suffix (PASSWORD_MIN_LENGTH, SECRET_FILE,
63
+ * tokenBudget). The same name judgement the `assigned-secret` patterns make, for callers that read keys. */
64
+ export const secretNameLike = (name) => SECRET_NAME_TEST.test(name) && !BENIGN_TAIL_TEST.test(name);
65
+
15
66
  export const SECRET_PATTERNS = [
16
67
  { name: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
17
68
  { name: 'aws-access-key', re: /\bAKIA[0-9A-Z]{16}\b/ },
@@ -23,17 +74,20 @@ export const SECRET_PATTERNS = [
23
74
  { name: 'telegram-bot-token', re: /\b\d{8,10}:AA[0-9A-Za-z_-]{33}\b/ },
24
75
  { name: 'stripe-secret', re: /\b(?:sk|rk)_live_[0-9A-Za-z]{20,}\b/ },
25
76
  { name: 'jwt', re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
77
+ { name: 'age-secret-key', re: /\bAGE-SECRET-KEY-1[0-9A-Z]{20,}\b/ },
78
+ { name: 'password-connection-string', re: /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s/${}]{8,}@/iu },
26
79
  // A value written `env(NAME)` (the Supabase config.toml reference form) names a variable, not a secret.
27
80
  // A keyword-assigned value that names itself a stand-in (a test stub's `accessToken: "fixture-..."`) is no
28
81
  // candidate; only this heuristic takes the exemption, never a provider-shaped token above.
29
82
  // A spec file's keyword-assigned values are test inputs (a spec's sign-in
30
83
  // form password and a mocked accessToken once refused a push): the heuristic skips spec files too.
31
- { name: 'assigned-secret', re: new RegExp([
32
- String.raw`\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|client[_-]?secret)`,
33
- String.raw`\b\s*[:=]\s*['"](?!env\([A-Z_][A-Z0-9_]*\)['"])([^'"\s$<{]{12,})`,
34
- `['"]`,
35
- ].join(''), 'i'), placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
84
+ // A SCREAMING_SNAKE quoted value in source is a public code (an enum member whose value is an upper-case code), not a credential.
85
+ { name: 'assigned-secret', re: ASSIGNED_QUOTED, sourceCode: /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/, placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
36
86
  skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$/ },
87
+ // An unquoted value in TypeScript or JavaScript source is an identifier or a type (`token: InjectionToken`,
88
+ // `MODULE_OPTIONS_TOKEN: DATABASE_OPTIONS_TOKEN`), never a literal; the unquoted form is the dotenv, compose and toml shape.
89
+ { name: 'assigned-secret', re: ASSIGNED_UNQUOTED, placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
90
+ skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$|\.[cm]?[jt]sx?$/ },
37
91
  ];
38
92
 
39
93
 
@@ -43,6 +97,7 @@ export function secretHits(file, text) {
43
97
  for (const rule of SECRET_PATTERNS) {
44
98
  if (rule.skipFile?.test(file ?? '')) continue;
45
99
  const hit = rule.re.exec(text);
100
+ if (hit && rule.sourceCode?.test(hit[1] ?? '') && /\.[cm]?[jt]sx?$/.test(file ?? '')) continue;
46
101
  if (hit && !rule.placeholder?.test(hit[1] ?? '')) names.push(rule.name);
47
102
  }
48
103
  return names;
@@ -0,0 +1,32 @@
1
+ // PostgreSQL AST primitives shared by database verbs and synchronous content rules.
2
+
3
+ /** String identifier components from libpg-query's wrapped String nodes. */
4
+ export const sqlStringParts = (nodes) => (nodes ?? []).map((node) => node?.String?.sval).filter((value) => value !== undefined);
5
+
6
+ /** Traverse SQL/PLpgSQL objects; executable scans leave function declarations inert. */
7
+ export function walkSqlAst(value, visit, executable = false) {
8
+ if (!value || typeof value !== 'object' || (executable && value.CreateFunctionStmt)) return;
9
+ visit(value);
10
+ for (const child of Object.values(value)) walkSqlAst(child, visit, executable);
11
+ }
12
+
13
+ /** Build a synchronous parser with the caller's failure policy and initialized parser. */
14
+ export function sqlParser(parser, reject = () => null) {
15
+ return (text) => {
16
+ try { return parser?.parseSync(text) ?? null; }
17
+ catch (error) { return reject(error); }
18
+ };
19
+ }
20
+
21
+ /** Resolve named arguments before positional arguments, as PostgreSQL does. */
22
+ export function sqlCallArgument(call, position, names = []) {
23
+ const args = call.args ?? [];
24
+ return args.find((item) => names.includes(item?.NamedArgExpr?.name))?.NamedArgExpr?.arg
25
+ ?? args.filter((item) => !item.NamedArgExpr)[position];
26
+ }
27
+
28
+ /** Read a literal string through static casts without evaluating an expression. */
29
+ export function sqlStringLiteral(node) {
30
+ if (node?.TypeCast) return sqlStringLiteral(node.TypeCast.arg);
31
+ return node?.A_Const?.sval?.sval ?? null;
32
+ }
@@ -58,7 +58,7 @@ const KNOWN_IMAGES = Object.freeze({
58
58
  export const STATEFUL_KINDS = Object.freeze(new Set(['database', 'cache', 'identity', 'storage', 'mail', 'queue', 'search']));
59
59
 
60
60
  /** The repository name of an image reference: `quay.io/keycloak/keycloak:26.0` -> `keycloak`. */
61
- const repositoryOf = image => {
61
+ export const repositoryOf = image => {
62
62
  const withoutDigest = image.split('@')[0];
63
63
  const lastSegment = withoutDigest.split('/').at(-1) ?? withoutDigest;
64
64
  return lastSegment.split(':')[0];
@@ -1,5 +1,7 @@
1
1
  // Shared static Swarm contract for app checks and rendered deployment evidence.
2
2
  import { isPlainObject as plain } from '../../engine/plain-object.mjs';
3
+ import { composeEntries } from './compose-values.mjs';
4
+ import { repositoryOf } from './stack-services.mjs';
3
5
 
4
6
  const PINNED_IMAGE = /@sha256:[a-f0-9]{64}$/i;
5
7
  const VERSIONED_SECRET = /-v[1-9][0-9]*$/;
@@ -29,13 +31,6 @@ function runnableHealthcheck(value) {
29
31
  return value.slice(1).some(present);
30
32
  }
31
33
 
32
- /** Match image repository names, including registry and vendor namespaces, without matching application tags. */
33
- function duplicatesManagedBacking(image) {
34
- if (!present(image)) return false;
35
- const repository = image.split('@')[0].split('/').at(-1).split(':')[0];
36
- return BACKING_IMAGE.test(repository);
37
- }
38
-
39
34
  function jobDeclarationIssues(id, component, components, add) {
40
35
  if (component.mode !== 'replicated-job') return;
41
36
  const scheduler = components[component.scheduler];
@@ -51,7 +46,7 @@ export function swarmInventoryIssues({ components, lite = false }) {
51
46
  const add = (at, message) => errors.push({ at, message });
52
47
  for (const [id, entry] of Object.entries(catalog)) {
53
48
  const component = mapping(entry);
54
- if (lite && duplicatesManagedBacking(component.image))
49
+ if (lite && present(component.image) && BACKING_IMAGE.test(repositoryOf(component.image)))
55
50
  add(`components.${id}.image`, 'lite must use managed Supabase backing services; this image duplicates a Supabase-provided service');
56
51
  jobDeclarationIssues(id, component, catalog, add);
57
52
  }
@@ -73,14 +68,6 @@ function serviceSafetyIssues(id, service, add) {
73
68
  add(`${at}.deploy.restart_policy`, 'every Swarm service needs deploy.restart_policy with a condition');
74
69
  }
75
70
 
76
- function labelsOf(value) {
77
- if (!Array.isArray(value)) return mapping(value);
78
- return Object.fromEntries(value.map(label => {
79
- const text = String(label), separator = text.indexOf('=');
80
- return separator < 0 ? [text, ''] : [text.slice(0, separator), text.slice(separator + 1)];
81
- }));
82
- }
83
-
84
71
  function jobServiceIssues(id, service, component, services, bindings, add) {
85
72
  const deploy = mapping(service.deploy), at = `services.${id}.deploy`;
86
73
  if (component.mode !== 'replicated-job') {
@@ -90,7 +77,7 @@ function jobServiceIssues(id, service, component, services, bindings, add) {
90
77
  if (deploy.mode !== 'replicated-job') add(`${at}.mode`, 'replicated-job component must use deploy.mode: replicated-job');
91
78
  if (deploy.replicas !== 0) add(`${at}.replicas`, 'scheduled replicated-job must keep deploy.replicas: 0 until its scheduler triggers it');
92
79
  if (deploy.restart_policy?.condition !== 'none') add(`${at}.restart_policy.condition`, 'scheduled replicated-job must use restart_policy.condition: none');
93
- const labels = labelsOf(deploy.labels);
80
+ const labels = Object.fromEntries(composeEntries(deploy.labels));
94
81
  if (String(labels['swarm.cronjob.enable']) !== 'true') add(`${at}.labels`, 'scheduled replicated-job needs swarm.cronjob.enable: "true"');
95
82
  if (!present(component.schedule) || labels['swarm.cronjob.schedule'] !== component.schedule)
96
83
  add(`${at}.labels`, 'swarm.cronjob.schedule must match the component cron schedule');
@@ -127,7 +114,7 @@ export function swarmModelIssues({ model, components, bindings, lite = false })
127
114
  add(`services.${id}.ports`, 'Swarm ports must be an array of published port definitions');
128
115
  else if (service.ports?.length && component.role !== 'ingress')
129
116
  add(`services.${id}.ports`, 'only a component with role ingress may publish Swarm ports');
130
- if (lite && duplicatesManagedBacking(service.image))
117
+ if (lite && present(service.image) && BACKING_IMAGE.test(repositoryOf(service.image)))
131
118
  add(`services.${id}.image`, 'lite stack must not run an image that duplicates a Supabase-provided backing service');
132
119
  jobServiceIssues(id, service, component, services, bound, add);
133
120
  }
@@ -0,0 +1,8 @@
1
+ // trim-end.mjs — a string without the run of characters at its end that `matches` accepts.
2
+
3
+ /** `value` with its trailing characters removed while `matches(char)` holds. */
4
+ export const trimEndWhile = (value, matches) => {
5
+ let end = value.length;
6
+ while (end > 0 && matches(value[end - 1])) end -= 1;
7
+ return value.slice(0, end);
8
+ };
@@ -1,7 +1,7 @@
1
1
  /** The expression under the wrappers selected by an AST check's contract. */
2
- export function unwrapExpression(ts, expression, { awaitExpressions = false, typeAssertions = true, satisfiesExpressions = true } = {}) {
2
+ export function unwrapExpression(ts, expression, { awaitExpressions = false, typeAssertions = true, nonNullExpressions = true, satisfiesExpressions = true } = {}) {
3
3
  while (expression && (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression)
4
- || (typeAssertions && ts.isTypeAssertionExpression(expression)) || ts.isNonNullExpression(expression)
4
+ || (typeAssertions && ts.isTypeAssertionExpression(expression)) || (nonNullExpressions && ts.isNonNullExpression(expression))
5
5
  || (satisfiesExpressions && (ts.isSatisfiesExpression?.(expression) ?? false))
6
6
  || (awaitExpressions && ts.isAwaitExpression(expression)))) expression = expression.expression;
7
7
  return expression;
@@ -18,3 +18,6 @@ export function importsOf(ts, sourceFile, { moduleKey = 'module', stringLiterals
18
18
  }
19
19
  return imports;
20
20
  }
21
+
22
+ /** Catalog values unwrap parentheses, `as`, and `satisfies`, preserving assertions and non-null nodes. */
23
+ export const unwrapCatalogExpression = (ts, node) => unwrapExpression(ts, node, { typeAssertions: false, nonNullExpressions: false });
@@ -11,8 +11,18 @@ export const hasAnyFlag = (flags, mask) => ((flags ?? 0) & mask) !== 0;
11
11
  /** Whether the variable `declaration` belongs to a `const` list: the TypeScript node-flag test, an explicit comparison of the masked flags. */
12
12
  export const isConstVariable = (ts, declaration) => (ts.getCombinedNodeFlags(declaration.parent) & ts.NodeFlags.Const) !== 0;
13
13
 
14
- /** The text of an identifier or a string-literal-like `node`, or null (property names, literal specifiers). */
15
- export const nameText = (ts, node) => (node && (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) ? node.text : null);
14
+ /** Text of a string-literal-like node or an enabled identifier/numeric name; null for other expressions. */
15
+ export function nameText(ts, node, { identifiers = true, numbers = false } = {}) {
16
+ if (!node) return null;
17
+ if (ts.isStringLiteralLike(node) || (identifiers && ts.isIdentifier(node)) || (numbers && ts.isNumericLiteral(node))) return node.text;
18
+ return null;
19
+ }
20
+
21
+ /** A statically selected property name, including string-literal element access; null for computed selections. */
22
+ export function accessName(ts, node) {
23
+ if (ts.isPropertyAccessExpression(node)) return node.name.text;
24
+ return ts.isElementAccessExpression(node) ? literalText(ts, node.argumentExpression) : null;
25
+ }
16
26
 
17
27
  /** The text of a string-literal-like `node` (a string literal or a no-substitution template), or null. */
18
28
  export const literalText = (ts, node) => (node && ts.isStringLiteralLike(node) ? node.text : null);
@@ -42,3 +52,18 @@ export function propertyText(ts, text, { file = 'source.ts', key, declaration =
42
52
  visit(source);
43
53
  return value;
44
54
  }
55
+
56
+ /** Initializers of every identifier-named variable, grouped in traversal order across the source's scopes. */
57
+ export function variableInitializerGroups(ts, source) {
58
+ const groups = new Map();
59
+ const collect = (node) => {
60
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
61
+ const initializers = groups.get(node.name.text) ?? [];
62
+ initializers.push(node.initializer);
63
+ groups.set(node.name.text, initializers);
64
+ }
65
+ ts.forEachChild(node, collect);
66
+ };
67
+ collect(source);
68
+ return groups;
69
+ }
@@ -0,0 +1,8 @@
1
+ // GitHub workflow `on` supports a scalar event, an event list or an options map.
2
+
3
+ /** Convert shorthand workflow events into an options map; mappings retain their values. */
4
+ export function workflowEventOptions(on, eventName = (event) => event) {
5
+ if (typeof on === 'string') return { [on]: null };
6
+ if (Array.isArray(on)) return Object.fromEntries(on.map((event) => [eventName(event), null]));
7
+ return on;
8
+ }
@@ -7,7 +7,7 @@ import {
7
7
  } from "../runtime/scripts/hfs/slots.mjs";
8
8
  import { imageFiles, render, TEMPLATES_DIR } from "../sync/index.mjs";
9
9
  import { ScaffoldError, pascalOf } from "./service.mjs";
10
- import { PARTIAL_VARIABLES, coreFeApps, jsonText, packageJsonText } from "./app.mjs";
10
+ import { PARTIAL_VARIABLES, bundledPins, callbackSourceFiles, coreFeApps, hasAuthCallback, jsonText, packageJsonText } from "./app.mjs";
11
11
 
12
12
  /** The shared fe app core (modules/api, auth, toast, redux, the providers, the hooks): the base tree, then the edition overlay. */
13
13
  const CORE_DIRS = Object.freeze({
@@ -35,37 +35,8 @@ const fill = (text, values, file) =>
35
35
  return values[key];
36
36
  });
37
37
 
38
- /**
39
- * Adds one Next workspace from the edition's canonical app skeleton and the shared app core (both editions, owner ruling
40
- * 2026-10-13: lite and full have one fe structure), then registers it in hfs.json. The root workspace glob is added only when an
41
- * older app manifest does not already carry it.
42
- */
43
- export function addApp({ root, name }) {
44
- if (!NAME.test(String(name)))
45
- throw new ScaffoldError(
46
- "HFS_ADD_NAME_INVALID",
47
- `app name ${name} must be kebab-case (letters, digits and single dashes)`,
48
- );
49
- const declarationFile = path.join(root, "hfs.json");
50
- const packageFile = path.join(root, "package.json");
51
- if (!fs.existsSync(declarationFile) || !fs.existsSync(packageFile))
52
- throw new ScaffoldError(
53
- "HFS_ADD_NOT_AN_APP",
54
- "add app runs at the app root containing hfs.json and package.json",
55
- );
56
- const manifest = loadSlotManifest();
57
- const repo = readRepoDeclaration(manifest, root);
58
- if (!repo.sides?.fe)
59
- throw new ScaffoldError(
60
- "HFS_ADD_NO_FRONT_END",
61
- "this app has no front-end side",
62
- );
63
- if (repo.sides.fe.apps.some((app) => app.name === name))
64
- throw new ScaffoldError(
65
- "HFS_ADD_EXISTS",
66
- `front-end app ${name} is already declared`,
67
- );
68
-
38
+ /** Render the edition skeleton followed by its shared core and edition overlay. */
39
+ function planAppTemplates(repo, name) {
69
40
  const source = path.join(
70
41
  TEMPLATES_DIR,
71
42
  "fe",
@@ -94,8 +65,13 @@ export function addApp({ root, name }) {
94
65
  if (!fs.existsSync(dir)) continue;
95
66
  for (const relative of listFiles(dir)) byPath.set(relative, { relative: `${targetBase}/${relative}`, body: readTemplate(dir, relative) });
96
67
  }
97
- const planned = [...byPath.values()];
68
+ return callbackSourceFiles([...byPath.values()].map(file => ({ path: file.relative, content: file.body })), repo)
69
+ .map(file => ({ relative: file.path, body: file.content }));
70
+ }
98
71
 
72
+ /** The package and TypeScript manifests of a new Next workspace. */
73
+ function planWorkspaceManifests(root, repo, name) {
74
+ const targetBase = `fe/apps/${name}`;
99
75
  // The dependency source is a product app (it carries the core's fe-stack packages), else the first Next app.
100
76
  const firstApp = coreFeApps(repo.sides.fe.apps)[0] ?? repo.sides.fe.apps.find((app) => app.kind === "next");
101
77
  const firstPackage = firstApp
@@ -108,8 +84,14 @@ export function addApp({ root, name }) {
108
84
  );
109
85
  const workspacePackage = JSON.parse(fs.readFileSync(firstPackage, "utf8"));
110
86
  workspacePackage.name = `@${repo.project}/${name}`;
87
+ if (hasAuthCallback(repo)) {
88
+ const pins = bundledPins();
89
+ workspacePackage.dependencies ??= {};
90
+ for (const dependency of ["@supabase/ssr", "@supabase/supabase-js", "server-only"])
91
+ workspacePackage.dependencies[dependency] = pins[dependency].version;
92
+ }
111
93
  const appTemplates = path.join(TEMPLATES_DIR, "fe", "app");
112
- planned.push(
94
+ return [
113
95
  {
114
96
  relative: `${targetBase}/package.json`,
115
97
  body:
@@ -121,32 +103,10 @@ export function addApp({ root, name }) {
121
103
  relative: `${targetBase}/tsconfig.json`,
122
104
  body: fs.readFileSync(path.join(appTemplates, "tsconfig.json"), "utf8"),
123
105
  },
124
- );
125
- const declaration = JSON.parse(fs.readFileSync(declarationFile, "utf8"));
126
- declaration.sides.fe.apps.push({ name, kind: "next" });
127
- if (repo.edition === "full") {
128
- const image = imageFiles(
129
- resolveRepoDeclaration(manifest, declaration),
130
- ).find((file) => file.path === `${targetBase}/Dockerfile`);
131
- if (!image)
132
- throw new ScaffoldError(
133
- "HFS_ADD_TEMPLATE_MISSING",
134
- `the Dockerfile of front-end app ${name} did not render`,
135
- );
136
- planned.push({ relative: image.path, body: image.content });
137
- }
138
-
139
- const clashes = planned
140
- .filter((file) =>
141
- fs.existsSync(path.join(root, ...file.relative.split("/"))),
142
- )
143
- .map((file) => file.relative);
144
- if (clashes.length)
145
- throw new ScaffoldError(
146
- "HFS_ADD_EXISTS",
147
- `front-end app ${name} already exists: ${clashes.join(", ")}`,
148
- );
106
+ ];
107
+ }
149
108
 
109
+ function persistAppFiles({ root, name, planned, declaration, declarationFile, packageFile }) {
150
110
  const rootPackage = JSON.parse(fs.readFileSync(packageFile, "utf8"));
151
111
  const oldDeclaration = fs.readFileSync(declarationFile, "utf8");
152
112
  const oldPackage = fs.readFileSync(packageFile, "utf8");
@@ -173,6 +133,64 @@ export function addApp({ root, name }) {
173
133
  fs.writeFileSync(packageFile, oldPackage);
174
134
  throw error;
175
135
  }
136
+ }
137
+
138
+ /**
139
+ * Adds one Next workspace from the edition's canonical app skeleton and the shared app core (both editions, owner ruling
140
+ * 2026-10-13: lite and full have one fe structure), then registers it in hfs.json. The root workspace glob is added only when an
141
+ * older app manifest does not already carry it.
142
+ */
143
+ export function addApp({ root, name }) {
144
+ if (!NAME.test(String(name)))
145
+ throw new ScaffoldError(
146
+ "HFS_ADD_NAME_INVALID",
147
+ `app name ${name} must be kebab-case (letters, digits and single dashes)`,
148
+ );
149
+ const declarationFile = path.join(root, "hfs.json");
150
+ const packageFile = path.join(root, "package.json");
151
+ if (!fs.existsSync(declarationFile) || !fs.existsSync(packageFile))
152
+ throw new ScaffoldError(
153
+ "HFS_ADD_NOT_AN_APP",
154
+ "add app runs at the app root containing hfs.json and package.json",
155
+ );
156
+ const manifest = loadSlotManifest();
157
+ const repo = readRepoDeclaration(manifest, root);
158
+ if (!repo.sides?.fe)
159
+ throw new ScaffoldError(
160
+ "HFS_ADD_NO_FRONT_END",
161
+ "this app has no front-end side",
162
+ );
163
+ if (repo.sides.fe.apps.some((app) => app.name === name))
164
+ throw new ScaffoldError(
165
+ "HFS_ADD_EXISTS",
166
+ `front-end app ${name} is already declared`,
167
+ );
168
+ const targetBase = `fe/apps/${name}`;
169
+ const planned = [...planAppTemplates(repo, name), ...planWorkspaceManifests(root, repo, name)];
170
+ const declaration = JSON.parse(fs.readFileSync(declarationFile, "utf8"));
171
+ declaration.sides.fe.apps.push({ name, kind: "next" });
172
+ const image = imageFiles(
173
+ resolveRepoDeclaration(manifest, declaration),
174
+ ).find((file) => file.path === `${targetBase}/Dockerfile`);
175
+ if (!image && repo.edition !== "lite")
176
+ throw new ScaffoldError(
177
+ "HFS_ADD_TEMPLATE_MISSING",
178
+ `the Dockerfile of front-end app ${name} did not render`,
179
+ );
180
+ if (image) planned.push({ relative: image.path, body: image.content });
181
+
182
+ const clashes = planned
183
+ .filter((file) =>
184
+ fs.existsSync(path.join(root, ...file.relative.split("/"))),
185
+ )
186
+ .map((file) => file.relative);
187
+ if (clashes.length)
188
+ throw new ScaffoldError(
189
+ "HFS_ADD_EXISTS",
190
+ `front-end app ${name} already exists: ${clashes.join(", ")}`,
191
+ );
192
+
193
+ persistAppFiles({ root, name, planned, declaration, declarationFile, packageFile });
176
194
  return {
177
195
  created: planned.map((file) => file.relative),
178
196
  registered: { app: name, workspace: "fe/apps/*" },