@starci/hfs 4.3.0 → 4.4.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 (515) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/db/migrate.mjs +361 -0
  3. package/db/post-cutover.mjs +136 -0
  4. package/emit/db-types.mjs +10 -1
  5. package/lint/run.mjs +1 -1
  6. package/package.json +4 -1
  7. package/release/gate.mjs +298 -0
  8. package/release/index.mjs +103 -0
  9. package/release/push.mjs +110 -0
  10. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  11. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  12. package/runtime/knowledge/hfs/rules.yaml +3401 -152
  13. package/runtime/knowledge/hfs/slots.yaml +344 -62
  14. package/runtime/knowledge/patterns/be/api.yaml +32 -5
  15. package/runtime/knowledge/patterns/be/cli.yaml +14 -4
  16. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  17. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  18. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  19. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  20. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  21. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  22. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  23. package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
  24. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  25. package/runtime/knowledge/sonar-gate.yaml +1 -1
  26. package/runtime/modules/kernel/failure-codes.yaml +3113 -43
  27. package/runtime/modules/models/runtimes.yaml +1 -1
  28. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  29. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  30. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  31. package/runtime/scripts/api/git/add.mjs +5 -0
  32. package/runtime/scripts/api/git/init.mjs +7 -0
  33. package/runtime/scripts/api/git/lib.mjs +3 -0
  34. package/runtime/scripts/api/git/tag.mjs +5 -0
  35. package/runtime/scripts/api/node/lib.mjs +20 -0
  36. package/runtime/scripts/api/node/run-node.mjs +6 -0
  37. package/runtime/scripts/api/sops/lib.mjs +41 -0
  38. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  39. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  40. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  41. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  42. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  43. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  44. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  45. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  47. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  48. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  49. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  50. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  51. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  52. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  53. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  54. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  55. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  56. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  57. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  58. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  59. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  60. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  61. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  63. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/tiers.mjs +3 -3
  65. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  66. package/runtime/scripts/hfs/check-results.mjs +28 -0
  67. package/runtime/scripts/hfs/check.mjs +46 -47
  68. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  69. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  70. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  71. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  72. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  73. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  74. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  75. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  76. package/runtime/scripts/hfs/manifest-shape.mjs +67 -7
  77. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  78. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  79. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  80. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  81. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  82. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  83. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  84. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  85. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  86. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  87. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  88. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  89. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  90. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  91. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  92. package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
  93. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  94. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  95. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  96. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  97. package/runtime/scripts/hfs/rules/database.mjs +9 -0
  98. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  99. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  100. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  101. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  102. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  103. package/runtime/scripts/hfs/rules/edition.mjs +9 -8
  104. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  105. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  106. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  107. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  108. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  109. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  110. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  111. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  112. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  113. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  114. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  115. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  116. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  117. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  118. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  119. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  120. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  121. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  122. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  123. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  124. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  125. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  126. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  127. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  128. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  129. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
  130. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  131. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  132. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  133. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  134. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  135. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  136. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  137. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  138. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  139. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  140. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  141. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  142. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  143. package/runtime/scripts/hfs/secret-init.mjs +127 -19
  144. package/runtime/scripts/hfs/secret-sops.mjs +38 -6
  145. package/runtime/scripts/hfs/secret-store.mjs +56 -54
  146. package/runtime/scripts/hfs/secret.mjs +34 -5
  147. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  148. package/runtime/scripts/hfs/slots.mjs +1 -0
  149. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  150. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  151. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  152. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  153. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  154. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  155. package/runtime/scripts/lib/compose-values.mjs +11 -0
  156. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  157. package/runtime/scripts/lib/json-clone.mjs +61 -0
  158. package/runtime/scripts/lib/list.mjs +4 -0
  159. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  160. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  161. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  162. package/runtime/scripts/lib/stack-services.mjs +1 -1
  163. package/runtime/scripts/lib/swarm-stack.mjs +5 -18
  164. package/runtime/scripts/lib/trim-end.mjs +8 -0
  165. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  166. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  167. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  168. package/scaffold/add-app.mjs +77 -59
  169. package/scaffold/add-cli-lite.mjs +14 -6
  170. package/scaffold/add-compose.mjs +79 -0
  171. package/scaffold/add-database-lite.mjs +14 -0
  172. package/scaffold/add-fe-overlay.mjs +122 -0
  173. package/scaffold/add-fe-page.mjs +269 -0
  174. package/scaffold/add-inbox-lite.mjs +74 -42
  175. package/scaffold/add-table.mjs +110 -87
  176. package/scaffold/add.mjs +159 -44
  177. package/scaffold/app.mjs +124 -39
  178. package/scaffold/edition-gate.mjs +3 -3
  179. package/scaffold/lite-exports.mjs +2 -1
  180. package/scaffold/service.mjs +2 -2
  181. package/src/main.mjs +229 -116
  182. package/stack/age-install.mjs +37 -0
  183. package/stack/backup.mjs +100 -0
  184. package/stack/build-args.mjs +42 -0
  185. package/stack/build.mjs +173 -0
  186. package/stack/cli.mjs +130 -0
  187. package/stack/config.mjs +57 -0
  188. package/stack/context.mjs +57 -0
  189. package/stack/converge.mjs +107 -0
  190. package/stack/deploy.mjs +301 -0
  191. package/stack/digest.mjs +16 -0
  192. package/stack/docker-transport.mjs +53 -0
  193. package/stack/edge-alias.mjs +118 -0
  194. package/stack/errors.mjs +21 -0
  195. package/stack/image-scan.mjs +152 -0
  196. package/stack/ingress-create.mjs +114 -0
  197. package/stack/ingress-publish.mjs +273 -0
  198. package/stack/ingress.mjs +706 -0
  199. package/stack/jobs.mjs +51 -0
  200. package/stack/live-restore.mjs +177 -0
  201. package/stack/prepare.mjs +266 -0
  202. package/stack/private-file.mjs +22 -0
  203. package/stack/provision.mjs +90 -0
  204. package/stack/prune.mjs +46 -0
  205. package/stack/release-store.mjs +87 -0
  206. package/stack/remote.mjs +205 -0
  207. package/stack/render.mjs +149 -0
  208. package/stack/retire.mjs +47 -0
  209. package/stack/sealed.mjs +149 -0
  210. package/stack/task.mjs +42 -0
  211. package/stack/verify.mjs +99 -0
  212. package/sync/ci-commands.mjs +13 -0
  213. package/sync/index.mjs +108 -52
  214. package/sync/stack-images.mjs +127 -0
  215. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  216. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  217. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  218. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  219. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  220. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  221. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  222. package/templates/app/codegen-graphql.mjs.partial +13 -2
  223. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  224. package/templates/app/codegen-prelude.mjs.partial +8 -0
  225. package/templates/app/gitignore +3 -0
  226. package/templates/app/gitignore-lite +3 -0
  227. package/templates/app/package-scripts/package.json +1 -0
  228. package/templates/app/package-scripts/supabase.json +7 -0
  229. package/templates/app/package-scripts-lite/package.json +1 -7
  230. package/templates/app/skeleton/.editorconfig +1 -0
  231. package/templates/app/skeleton/.gitattributes +7 -1
  232. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  233. package/templates/app/skeleton/.nvmrc +1 -1
  234. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  235. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  236. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  237. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  238. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  239. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  240. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  241. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  242. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  243. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  244. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  245. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  246. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  247. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  248. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  249. package/templates/app/skeleton/README.md +6 -6
  250. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  251. package/templates/app/skeleton-lite/.editorconfig +16 -0
  252. package/templates/app/skeleton-lite/.gitattributes +8 -0
  253. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  254. package/templates/app/skeleton-lite/.nvmrc +1 -0
  255. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  256. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
  257. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  258. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  259. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  260. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  261. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  262. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  263. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  264. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  265. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  266. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  267. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  268. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  269. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  270. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  271. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  272. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  273. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  274. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  275. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  276. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  277. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  278. package/templates/app/skeleton-lite/README.md +5 -3
  279. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  280. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  281. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  282. package/templates/be/image/worker/Dockerfile +1 -1
  283. package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
  284. package/templates/be/inbox/database-client.ts.tpl +12 -11
  285. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  286. package/templates/be/inbox/event-index.ts.tpl +1 -0
  287. package/templates/be/inbox/events.json.tpl +10 -0
  288. package/templates/be/inbox/migration.sql.tpl +1 -0
  289. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  290. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  291. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  292. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  293. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  294. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  295. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  296. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  297. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  298. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  299. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  300. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  301. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  302. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  303. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  304. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  305. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  306. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  307. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  308. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  309. package/templates/be/inbox/service.spec.ts.tpl +42 -9
  310. package/templates/be/inbox/service.ts.tpl +23 -8
  311. package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
  312. package/templates/be/inbox/webhook-door.ts.tpl +2 -2
  313. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  314. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  315. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  316. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  317. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  318. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  319. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  320. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  321. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  323. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  324. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  325. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  326. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  327. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  328. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  329. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  330. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  331. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  332. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  333. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  334. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  335. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  336. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  337. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  338. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  339. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  340. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  341. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  342. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  343. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  344. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  345. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  346. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  347. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  348. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  349. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  350. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  351. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  352. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  353. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  354. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  355. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  356. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  357. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  358. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  359. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  360. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  362. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  363. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  364. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  365. package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
  366. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  367. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  368. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
  369. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  370. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  371. package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
  372. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  373. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
  374. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  375. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  376. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  377. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  378. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  379. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  380. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  381. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  382. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  383. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  384. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  385. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  386. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  387. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  388. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  389. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  390. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  391. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  392. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  393. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  394. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  395. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  396. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  397. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  398. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  399. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  400. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  401. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  402. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  403. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  404. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  405. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  406. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  407. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  408. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  409. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  410. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  411. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  412. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  413. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  414. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  415. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  416. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  417. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  418. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  419. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  420. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  421. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  422. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  423. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  424. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  425. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  426. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  427. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  428. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  429. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  430. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  431. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  432. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  433. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  434. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  435. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  436. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  437. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  438. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  439. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  440. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  441. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  442. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  443. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  444. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  445. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  446. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
  447. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  448. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  449. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  450. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
  451. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  452. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  453. package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
  454. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  455. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  456. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  457. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  458. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  459. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  460. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  461. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  462. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  463. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  464. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  465. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  466. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  467. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  468. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  469. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  470. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  471. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  472. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  473. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  474. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  475. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  476. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  477. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  478. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  479. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  480. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  481. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  482. package/templates/be/table/migration.sql.tpl +1 -0
  483. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  484. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  485. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  486. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  487. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  488. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  489. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  490. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  491. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  492. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  493. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  494. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  495. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  496. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  497. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  498. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  499. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  500. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  501. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  502. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  503. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  504. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  505. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  506. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  507. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  508. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  509. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  510. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  511. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  512. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  513. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  514. package/upgrade/check.mjs +3 -1
  515. package/upgrade/index.mjs +53 -40
@@ -0,0 +1,61 @@
1
+ // Add app deployment conformance to the delivery/quality service gate without changing legacy runtime custody rules.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { walkFiles } from '../lib/walk.mjs';
5
+ import { readJson } from './rules/read.mjs';
6
+ import { stacksFindings } from './rules/stacks.mjs';
7
+ import { stacksRuntimeFindings } from './rules/stacks-runtime.mjs';
8
+ import { findStackDeclaration } from '../lib/stack-declaration.mjs';
9
+ import { loadSlotManifest, openHfs } from './slots.mjs';
10
+
11
+ /** Source paths admitted to stack and scheduling checks; never descend through symlinks. */
12
+ function safeSourceDirectory(repoRoot, relative) {
13
+ const root = path.resolve(repoRoot);
14
+ let cursor = path.resolve(root, relative);
15
+ while (cursor !== root) {
16
+ try { if (fs.lstatSync(cursor).isSymbolicLink()) return false; } catch { return false; }
17
+ cursor = path.dirname(cursor);
18
+ }
19
+ return true;
20
+ }
21
+
22
+ function applicationStackFiles(repoRoot) {
23
+ return ['.starcistacks', '.github/workflows', 'deploy', 'scripts'].flatMap(relative => {
24
+ if (!safeSourceDirectory(repoRoot, relative)) return [];
25
+ return walkFiles(path.join(repoRoot, relative), { ignoreReadErrors: true, exclude: (_name, _file, entry) => entry.isSymbolicLink() })
26
+ .map(file => path.relative(repoRoot, file).replaceAll('\\', '/'));
27
+ });
28
+ }
29
+
30
+ export const appEdition = repoRoot => readJson(repoRoot, 'hfs.json')?.edition ?? 'full';
31
+
32
+ /** An HFS app uses its edition's slot shape; other repository declarations still receive Swarm runtime checks. */
33
+ export function applicationStackFindings(repoRoot) {
34
+ const files = applicationStackFiles(repoRoot), declaration = readJson(repoRoot, 'hfs.json');
35
+ if (declaration?.kind === 'app') {
36
+ try {
37
+ const resolver = openHfs({ declaration, manifest: loadSlotManifest() });
38
+ const findings = stacksFindings({ repoRoot, files, resolver }), manifestFile = '.starcistacks/application-stacks.yaml';
39
+ if (resolver.slot('app.starcistacks')?.requires?.includes('application-stacks.yaml') && !files.includes(manifestFile))
40
+ return [{ code: 'HFS_STACKS_SHAPE', level: 'error', path: manifestFile, message: `${manifestFile}: the app stacks slot requires this declaration` }, ...findings];
41
+ return findings;
42
+ } catch (error) {
43
+ return [{ code: 'HFS_STACKS_SHAPE', level: 'error', path: 'hfs.json', message: `hfs.json: cannot resolve the app stacks shape: ${error.message}` }];
44
+ }
45
+ }
46
+ const doc = findStackDeclaration(repoRoot).doc;
47
+ return doc ? stacksRuntimeFindings({ repoRoot, files, doc }) : [];
48
+ }
49
+
50
+ /** Lite may retain value-free development custody metadata without declaring a self-hosted development stack. */
51
+ export function liteCustodyDirectory(directory) {
52
+ let entries;
53
+ try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch { return false; }
54
+ return entries.length > 0 && entries.every(entry => {
55
+ if (entry.isSymbolicLink()) return false;
56
+ if (entry.isFile()) return entry.name === 'environment.json';
57
+ if (!entry.isDirectory() || entry.name !== 'secrets') return false;
58
+ return fs.readdirSync(path.join(directory, entry.name), { withFileTypes: true })
59
+ .every(secret => secret.isFile() && !secret.isSymbolicLink() && (secret.name === '.gitkeep' || /^[a-z0-9-]+\.enc$/.test(secret.name)));
60
+ });
61
+ }
@@ -0,0 +1,126 @@
1
+ // command-tokens.mjs — the tokens of the tail of a documented `starci` command (the lexer under doc-commands.mjs).
2
+ //
3
+ // A tail is split into shell-like atoms: a run of non-space characters in which <...>, [...], {...}, (...) and quoted parts may hold
4
+ // spaces. [..] and (..) groups are optional text (all their alternatives are named); a (..) group that opens with a plain word is
5
+ // commentary and ends the command; a token that ends a sentence ends the command.
6
+
7
+ import { trimEndWhile } from './trim-end.mjs';
8
+
9
+ const CLOSERS = { '<': '>', '[': ']', '{': '}', '(': ')' };
10
+ const TRAILING_CHARS = '.,;:!?)\'"`]}';
11
+ const GROUP_TRAILING_CHARS = '.,;:)';
12
+ const CUT = { text: '#', optional: false };
13
+ const OPENS_VALUE = /^\s*(--|<|\[|\$)/;
14
+ const QUOTED = /^(['"]).*\1$/;
15
+ const NAME = /^[\w.]+$/;
16
+
17
+ /** The index of the character that closes the bracket group opened at `from`, or -1 when it never closes. */
18
+ function groupEnd(s, from) {
19
+ const open = s[from];
20
+ let depth = 0;
21
+ for (let j = from; j < s.length; j += 1) {
22
+ if (s[j] === open) depth += 1;
23
+ else if (s[j] === CLOSERS[open]) {
24
+ depth -= 1;
25
+ if (depth === 0) return j;
26
+ }
27
+ }
28
+ return -1;
29
+ }
30
+
31
+ /** The end of the atom that starts at `start`. */
32
+ function readAtom(s, start) {
33
+ let i = start;
34
+ while (i < s.length && !/\s/.test(s[i])) {
35
+ const c = s[i];
36
+ if (c in CLOSERS) {
37
+ const end = groupEnd(s, i);
38
+ if (end < 0) return s.length;
39
+ i = end + 1;
40
+ } else if ((c === '"' || c === "'") && (i === start || s[i - 1] === '=')) {
41
+ const j = s.indexOf(c, i + 1);
42
+ i = j < 0 ? i + 1 : j + 1;
43
+ } else i += 1;
44
+ }
45
+ return i;
46
+ }
47
+
48
+ const stripTrailing = (atom, chars) => trimEndWhile(atom, (c) => chars.includes(c));
49
+
50
+ /** The maximal run of sentence punctuation at the end of an atom: {index, text}, or null. */
51
+ const trailingRun = (atom) => {
52
+ let start = atom.length;
53
+ while (start > 0 && TRAILING_CHARS.includes(atom[start - 1])) start -= 1;
54
+ return start < atom.length ? { index: start, text: atom.slice(start) } : null;
55
+ };
56
+
57
+ // ${NAME}, $NAME and ${NAME with an optional closing brace
58
+ const isDollarName = (t) => {
59
+ if (!t.startsWith('$')) return false;
60
+ let rest = t.slice(1);
61
+ if (rest.startsWith('{')) rest = rest.slice(1);
62
+ if (rest.endsWith('}')) rest = rest.slice(0, -1);
63
+ return NAME.test(rest);
64
+ };
65
+
66
+ // an <...> part with at least one character inside
67
+ const hasAnglePart = (t) => {
68
+ for (let i = t.indexOf('<'); i >= 0; i = t.indexOf('<', i + 1)) {
69
+ if (t.indexOf('>', i + 1) > i + 1) return true;
70
+ }
71
+ return false;
72
+ };
73
+
74
+ const wrapped = (t, open, close) => t.startsWith(open) && t.endsWith(close) && t.length >= open.length + close.length;
75
+
76
+ /** A value written as a placeholder: <id>, $VAR, ${VAR}, {x}. */
77
+ export const isPlaceholder = (t) => wrapped(t, '<', '>') || isDollarName(t) || wrapped(t, '${', '}') || wrapped(t, '{', '}')
78
+ || hasAnglePart(t) || t.includes('${');
79
+
80
+ /** <a|b>: alternatives written inside angle brackets. */
81
+ export const isAlternation = (t) => {
82
+ if (!wrapped(t, '<', '>')) return false;
83
+ const bar = t.indexOf('|', 2);
84
+ return bar > 0 && bar <= t.length - 3;
85
+ };
86
+
87
+ const isGroup = (closed) => closed.length > 2 && ((closed.startsWith('[') && closed.endsWith(']')) || (closed.startsWith('(') && closed.endsWith(')')));
88
+ const unquote = (atom) => (atom.length > 1 && QUOTED.test(atom) ? atom.slice(1, -1) : atom);
89
+
90
+ // {tokens, stop} of a bracket group atom
91
+ function groupTokens(atom, closed) {
92
+ const body = closed.slice(1, -1);
93
+ if (atom.startsWith('(') && !OPENS_VALUE.test(body)) return { tokens: [CUT], stop: true };
94
+ return {
95
+ tokens: tokenise(body, true).filter((t) => t.text !== '|'),
96
+ stop: closed !== atom && /[.,;:]/.test(atom.slice(closed.length)),
97
+ };
98
+ }
99
+
100
+ // {tokens, stop} of a plain atom
101
+ function plainTokens(atom, optional) {
102
+ const tail = /^\.{1,3}$/.test(atom) ? null : trailingRun(atom);
103
+ const trimmed = tail !== null && !isPlaceholder(atom);
104
+ const bare = trimmed ? atom.slice(0, tail.index) : atom;
105
+ return { tokens: bare ? [{ text: bare, optional }] : [], stop: trimmed && /[^\]]/.test(tail.text) };
106
+ }
107
+
108
+ /** The flat tokens {text, optional} of a command tail. */
109
+ export function tokenise(text, optional = false) {
110
+ const tokens = [];
111
+ let i = 0;
112
+ while (i < text.length) {
113
+ if (/\s/.test(text[i])) {
114
+ i += 1;
115
+ continue;
116
+ }
117
+ const end = readAtom(text, i);
118
+ const atom = text.slice(i, end);
119
+ i = end;
120
+ const closed = atom.startsWith('[') ? stripTrailing(atom, GROUP_TRAILING_CHARS) : atom;
121
+ const step = isGroup(closed) ? groupTokens(atom, closed) : plainTokens(unquote(atom), optional);
122
+ tokens.push(...step.tokens);
123
+ if (step.stop) break;
124
+ }
125
+ return tokens;
126
+ }
@@ -0,0 +1,11 @@
1
+ import { isPlainObject } from '../../engine/plain-object.mjs';
2
+
3
+ /** Compose mapping/list entries; callers choose how bare keys and non-string list items are interpreted. */
4
+ export function composeEntries(value, { missingValue = '', stringsOnly = false, isMapping = isPlainObject } = {}) {
5
+ if (!Array.isArray(value)) return Object.entries(isMapping(value) ? value : {});
6
+ const entries = stringsOnly ? value.filter(entry => typeof entry === 'string') : value;
7
+ return entries.map(entry => {
8
+ const text = String(entry), separator = text.indexOf('=');
9
+ return separator < 0 ? [text, missingValue] : [text.slice(0, separator), text.slice(separator + 1)];
10
+ });
11
+ }
@@ -0,0 +1,7 @@
1
+ /** Project issues from available rendered deployments with the caller's finding factory. */
2
+ export function deploymentFindings(deployments, issuesFor, findingFor, defaultFile) {
3
+ return deployments.filter(entry => !entry.unavailable).flatMap(entry => {
4
+ const file = entry.file ?? defaultFile;
5
+ return issuesFor(entry).map(issue => findingFor(issue.code, file, `${file}: ${issue.at}: ${issue.message}`));
6
+ });
7
+ }
@@ -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
+ }
@@ -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);
@@ -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
+ }