@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
@@ -0,0 +1,417 @@
1
+ // R266: declaration-bound Swarm custody, routing and service safety.
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { isPlainObject as plain } from '../../../engine/plain-object.mjs';
5
+ import { composeEntries } from '../../lib/compose-values.mjs';
6
+ import { list } from '../../lib/list.mjs';
7
+ import { repositoryOf } from '../../lib/stack-services.mjs';
8
+ import { isInside } from '../../lib/walk.mjs';
9
+ import { readTypeScriptProject, resolveTypeScriptModule } from '../typescript-programs.mjs';
10
+ import { parseSource, ts } from '../runtime-rules/source-ast.mjs';
11
+ import { readText } from './read.mjs';
12
+ import { externalIngressStack } from './stacks-ingress.mjs';
13
+ import { stacksDeploymentFindings } from './stacks-runtime.mjs';
14
+
15
+ const mapping = value => plain(value) ? value : {};
16
+ const present = value => typeof value === 'string' && Boolean(value.trim());
17
+ const VERSION = /^[1-9][0-9]*$/;
18
+ const VERSIONED_SECRET = /-v[1-9][0-9]*$/;
19
+ const SENSITIVE_KEY = /password|passwd|secret|token|credential|private[_-]?key|api[_-]?key|service[_-]?role|(?:^|_)key$/i;
20
+ const SOURCE = /^be\/(?:apps|src)\/.*\.[cm]?[jt]sx?$/;
21
+ const TEST_SOURCE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$|\/tests\//;
22
+ const REALTIME_SOURCE = /^be\/src\/features\/realtime\/[^/]+\//;
23
+ const HOST = /^(?:[a-z0-9-]+\.)+[a-z0-9-]+$/i;
24
+
25
+ function secretVersionMatches(declaration, secret) {
26
+ const version = String(declaration.version ?? '');
27
+ return VERSION.test(version) && present(declaration.runtimeName)
28
+ && declaration.runtimeName.endsWith(`-v${version}`) && secret.name === declaration.runtimeName;
29
+ }
30
+
31
+ function secretCustodyIssues(spec, model, add) {
32
+ const secrets = mapping(model.secrets);
33
+ const declarations = new Map(list(spec.secrets).map(entry => [entry?.name, entry]));
34
+ const names = new Set([...Object.keys(secrets), ...declarations.keys()]);
35
+ for (const name of names) {
36
+ const secret = mapping(secrets[name]), declaration = mapping(declarations.get(name));
37
+ const at = `secrets.${name}`;
38
+ if (secret.external !== true || ['file', 'environment', 'content', 'value'].some(key => secret[key] !== undefined))
39
+ add('STACKS_SECRET_NOT_EXTERNAL', at, 'Swarm secrets must be external engine secrets without an embedded source');
40
+ if (!present(secret.name) || !VERSIONED_SECRET.test(secret.name) || !secretVersionMatches(declaration, secret))
41
+ add('STACKS_SECRET_UNVERSIONED', at, 'Declare runtimeName and a positive version matching the immutable rendered secret name <logical>-v<N>');
42
+ }
43
+ }
44
+
45
+ function grantedSecretTargets(service, secrets) {
46
+ const targets = new Set();
47
+ for (const entry of list(service.secrets)) {
48
+ const source = plain(entry) ? entry.source : entry;
49
+ if (!Object.hasOwn(secrets, source)) continue;
50
+ const target = plain(entry) ? entry.target ?? source : source;
51
+ if (present(target) && !target.includes('/') && !target.includes('..')) targets.add(`/run/secrets/${target}`);
52
+ }
53
+ return targets;
54
+ }
55
+
56
+ function sensitiveEnvironmentKey(key, declarations) {
57
+ const normalized = key.toUpperCase().replaceAll('-', '_');
58
+ return SENSITIVE_KEY.test(key) || declarations.some(entry => entry?.name?.toUpperCase().replaceAll('-', '_') === normalized);
59
+ }
60
+
61
+ function credentialUri(value) {
62
+ if (!present(value)) return false;
63
+ try {
64
+ const uri = new URL(value);
65
+ return Boolean(uri.password);
66
+ } catch { return false; }
67
+ }
68
+
69
+ function environmentSecretIssues(id, service, spec, model, add) {
70
+ const targets = grantedSecretTargets(service, mapping(model.secrets));
71
+ for (const [key, value] of composeEntries(service.environment, { missingValue: null })) {
72
+ if (key.endsWith('_FILE')) {
73
+ const secretFile = sensitiveEnvironmentKey(key.slice(0, -5), list(spec.secrets)) || String(value).startsWith('/run/secrets/') || credentialUri(value);
74
+ if (secretFile && !targets.has(value)) add('STACKS_SECRET_ENV_LITERAL', `services.${id}.environment.${key}`, 'A secret file binding must name the exact /run/secrets target granted to this service');
75
+ continue;
76
+ }
77
+ if (sensitiveEnvironmentKey(key, list(spec.secrets)) || credentialUri(value))
78
+ add('STACKS_SECRET_ENV_LITERAL', `services.${id}.environment.${key}`, 'Sensitive environment bindings must use a granted Swarm secret file; secret values must never be embedded');
79
+ }
80
+ for (const entry of list(service.secrets)) {
81
+ const source = plain(entry) ? entry.source : entry;
82
+ if (!Object.hasOwn(mapping(model.secrets), source))
83
+ add('STACKS_SECRET_NOT_EXTERNAL', `services.${id}.secrets`, 'A service grant must reference a declared external engine secret');
84
+ }
85
+ }
86
+
87
+ function allowedPort(entry) {
88
+ if (plain(entry)) return [undefined, 'ingress'].includes(entry.mode) && [80, 443].includes(Number(entry.published));
89
+ if (typeof entry !== 'string') return false;
90
+ const parts = String(entry).split('/')[0].split(':');
91
+ return parts.length > 1 && [80, 443].includes(Number(parts.at(-2)));
92
+ }
93
+
94
+ function publishedPortIssues(id, service, spec, add) {
95
+ if (service.ports === undefined || (Array.isArray(service.ports) && !service.ports.length)) return;
96
+ if (id !== spec.ingress?.component || !Array.isArray(service.ports) || !service.ports.every(allowedPort))
97
+ add('STACKS_PORT_PUBLISHED', `services.${id}.ports`, 'Only the declared ingress may publish ports, using ingress mode and published ports 80 or 443');
98
+ }
99
+
100
+ function hostOf(value) {
101
+ if (!present(value)) return null;
102
+ let candidate = value.trim().replace(/^https?:\/\//i, '').split('/')[0].split(':')[0];
103
+ candidate = candidate.toLowerCase().replace(/\.$/, '');
104
+ return HOST.test(candidate) ? candidate : null;
105
+ }
106
+
107
+ function traefikHosts(rule) {
108
+ const hosts = [], text = String(rule);
109
+ for (const match of text.matchAll(/\bHost\s*\(/g)) {
110
+ if (!positiveHostCall(text.slice(0, match.index))) continue;
111
+ const part = text.slice(match.index + match[0].length);
112
+ const argumentsText = part.split(')')[0];
113
+ for (const argument of argumentsText.split(',')) {
114
+ const quoted = argument.trim();
115
+ if (!['`', '"', "'"].includes(quoted[0]) || quoted.at(-1) !== quoted[0]) continue;
116
+ const domain = quoted.slice(1, -1).toLowerCase();
117
+ const host = HOST.test(domain) ? domain : null;
118
+ if (host) hosts.push(host);
119
+ }
120
+ }
121
+ return hosts;
122
+ }
123
+
124
+ function positiveHostCall(prefix) {
125
+ const scopes = [false];
126
+ let pending = false;
127
+ for (const token of prefix.match(/"[^"]*"|'[^']*'|`[^`]*`|[()!]|[A-Za-z_][A-Za-z_0-9]*|\S/g) ?? []) {
128
+ if (token === '!') { pending = !pending; continue; }
129
+ if (token === '(') scopes.push(scopes.at(-1) !== pending);
130
+ if (token === ')' && scopes.length > 1) scopes.pop();
131
+ pending = false;
132
+ }
133
+ return !scopes.at(-1) && !pending;
134
+ }
135
+
136
+ function labelHosts(service, family) {
137
+ const hosts = [];
138
+ const labels = [...composeEntries(service.labels, { missingValue: null }), ...composeEntries(service.deploy?.labels, { missingValue: null })];
139
+ if (family === 'traefik' && labels.some(([key, value]) => key === 'traefik.enable' && String(value) === 'false')) return hosts;
140
+ for (const [key, value] of labels) {
141
+ if (family === 'traefik' && key.startsWith('traefik.http.routers.') && key.endsWith('.rule')) hosts.push(...traefikHosts(value));
142
+ if (family === 'caddy-docker-proxy' && /^caddy(?:_[0-9]+)?$/.test(key)) hosts.push(...String(value).split(/[\s,]+/).map(hostOf).filter(Boolean));
143
+ }
144
+ return hosts;
145
+ }
146
+
147
+ function safeRegularFile(repoRoot, file) {
148
+ const base = path.resolve(repoRoot), target = path.resolve(base, file);
149
+ if (!isInside(base, target)) return false;
150
+ try {
151
+ if (!fs.lstatSync(target).isFile()) return false;
152
+ let cursor = target;
153
+ while (cursor !== base) {
154
+ if (fs.lstatSync(cursor).isSymbolicLink()) return false;
155
+ cursor = path.dirname(cursor);
156
+ }
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+
163
+ function trackedConfigText(input, relative) {
164
+ if (!present(relative) || relative.endsWith('.enc') || path.isAbsolute(relative)) return null;
165
+ const directory = `.starcistacks/${input.environment}/`;
166
+ const stack = input.spec.renderedModel ?? input.spec.composeFiles?.[0] ?? 'infra/swarm/stack.yaml';
167
+ const file = path.posix.normalize(path.posix.join(directory, path.posix.dirname(stack), relative));
168
+ const allowed = file.startsWith(`${directory}infra/`) || file.startsWith(`${directory}runtime/config/`);
169
+ if (!allowed || file.includes('/secrets/') || !list(input.files).includes(file) || !input.repoRoot) return null;
170
+ if (!safeRegularFile(input.repoRoot, file)) return null;
171
+ return readText(input.repoRoot, file);
172
+ }
173
+
174
+ function caddyConfigSources(service, model) {
175
+ const sources = [];
176
+ for (const entry of list(service.configs)) {
177
+ const source = plain(entry) ? entry.source : entry;
178
+ const config = mapping(model.configs)[source];
179
+ if (present(config?.file)) sources.push(config.file);
180
+ }
181
+ for (const entry of list(service.volumes)) {
182
+ if (plain(entry) && entry.type === 'bind' && String(entry.target).endsWith('/Caddyfile')) sources.push(entry.source);
183
+ if (typeof entry === 'string') {
184
+ const [source, target] = entry.split(':');
185
+ if (target?.endsWith('/Caddyfile')) sources.push(source);
186
+ }
187
+ }
188
+ return sources;
189
+ }
190
+
191
+ function caddyfileHosts(text) {
192
+ const hosts = [];
193
+ let depth = 0;
194
+ for (const line of String(text ?? '').split(/\r?\n/)) {
195
+ const clean = line.split('#')[0].trim();
196
+ if (depth === 0 && clean.includes('{')) {
197
+ const header = clean.split('{')[0];
198
+ hosts.push(...header.split(/[\s,]+/).map(hostOf).filter(Boolean));
199
+ }
200
+ depth += clean.split('{').length - clean.split('}').length;
201
+ depth = Math.max(0, depth);
202
+ }
203
+ return hosts;
204
+ }
205
+
206
+ function routedHosts(input) {
207
+ const hosts = new Set();
208
+ const ingress = mapping(input.model.services)[input.spec.ingress?.component];
209
+ const family = repositoryOf(String(ingress?.image ?? '')).toLowerCase();
210
+ for (const service of Object.values(mapping(input.model.services))) {
211
+ for (const host of labelHosts(service, family)) hosts.add(host);
212
+ }
213
+ if (!family.startsWith('caddy')) return hosts;
214
+ for (const source of caddyConfigSources(mapping(ingress), input.model)) {
215
+ for (const host of caddyfileHosts(trackedConfigText(input, source))) hosts.add(host);
216
+ }
217
+ return hosts;
218
+ }
219
+
220
+ function publicRoutingIssues(input, add) {
221
+ const { spec } = input, routed = routedHosts(input);
222
+ const declared = new Set(list(spec.ingress?.hosts).map(hostOf).filter(Boolean));
223
+ const dns = list(spec.dns);
224
+ for (const host of new Set([...declared, ...routed])) {
225
+ if (!dns.some(entry => hostOf(entry?.host) === host && present(entry.owner) && present(entry.receipt)))
226
+ add('STACKS_DNS_UNDECLARED', `environments.${input.environment}.dns`, 'Each public host must declare its DNS owner and receipt');
227
+ // An external shared ingress routes the hosts from its own dynamic files (published by `starci app stack ingress publish`), not from labels here.
228
+ if (!declared.has(host) || (externalIngressStack(spec) === null && !routed.has(host)))
229
+ add('STACKS_HOST_UNROUTED', `environments.${input.environment}.ingress.hosts`, 'Each public host must be declared and bound by an actual ingress route');
230
+ }
231
+ const ingress = mapping(input.model.services)[spec.ingress?.component];
232
+ const publicIngress = declared.size || routed.size || list(ingress?.ports).length;
233
+ if (publicIngress && (!present(spec.tls?.owner) || !present(spec.tls?.receipt)))
234
+ add('STACKS_TLS_UNDECLARED', `environments.${input.environment}.tls`, 'Public ingress must declare its TLS owner and issuance/persistence receipt');
235
+ }
236
+
237
+ function usesPersistentVolume(service) {
238
+ return list(service.volumes).some(entry => {
239
+ if (plain(entry)) return entry.type === 'volume' || (present(entry.source) && !entry.source.startsWith('/'));
240
+ if (typeof entry !== 'string' || !entry.includes(':')) return false;
241
+ const source = entry.split(':')[0];
242
+ return present(source) && !source.startsWith('.') && !source.startsWith('/') && !source.includes(String.fromCodePoint(92));
243
+ });
244
+ }
245
+
246
+ function placementIssues(id, component, service, add) {
247
+ const stateful = component.stateful === true || component.role === 'stateful' || present(component.database?.provider)
248
+ || list(component.volumes).length > 0 || usesPersistentVolume(service);
249
+ if (!stateful) return;
250
+ const constraints = list(service.deploy?.placement?.constraints);
251
+ if (!constraints.length || !constraints.every(present))
252
+ add('STACKS_PLACEMENT_MISSING', `services.${id}.deploy.placement`, 'Stateful services and persistent volumes must declare node placement constraints');
253
+ }
254
+
255
+ function healthCommand(test) {
256
+ if (present(test)) return test.trim();
257
+ if (!Array.isArray(test) || !['CMD', 'CMD-SHELL'].includes(test[0]) || !test.slice(1).every(present)) return null;
258
+ return test.slice(1).join(' ').trim();
259
+ }
260
+
261
+ function nontrivialHealthcheck(healthcheck) {
262
+ const command = healthCommand(healthcheck.test);
263
+ if (healthcheck.disable === true || !command || command === 'NONE') return false;
264
+ const unwrapped = command.replace(/^(?:\/bin\/)?(?:ba)?sh\s+-c\s+/, '').replace(/^["']|["']$/g, '').trim();
265
+ const executable = unwrapped.split(/\s/)[0].split('/').at(-1);
266
+ return !['true', ':', 'echo'].includes(executable) && !/^exit\s+0(?:\s|$)/.test(unwrapped)
267
+ && !/\|\|\s*(?:true|exit\s+0)(?:\s|$)/.test(unwrapped)
268
+ && !endsWithSuccessfulCommand(unwrapped)
269
+ && !/process\.exit\(\s*0\s*\)/.test(unwrapped);
270
+ }
271
+
272
+ function endsWithSuccessfulCommand(command) {
273
+ const commands = command.split(/[;\n]/);
274
+ if (commands.length < 2) return false;
275
+ const last = commands.at(-1).trim();
276
+ return last === 'true' || /^exit\s+0$/.test(last);
277
+ }
278
+
279
+ function positiveMonitor(value) {
280
+ if (typeof value === 'number') return Number.isFinite(value) && value > 0;
281
+ if (!present(value)) return false;
282
+ const units = value.match(/[a-z]+/g) ?? [], quantities = value.split(/[a-z]+/);
283
+ if (quantities.pop() !== '' || !units.length) return false;
284
+ return units.every(unit => ['ns', 'us', 'ms', 's', 'm', 'h'].includes(unit))
285
+ && quantities.every(quantity => /^[0-9]+(?:\.[0-9]+)?$/.test(quantity)) && /[1-9]/.test(value);
286
+ }
287
+
288
+ function updatePolicyIssues(id, service, add) {
289
+ const deploy = mapping(service.deploy), update = mapping(deploy.update_config);
290
+ const updateValid = ['start-first', 'stop-first'].includes(update.order) && update.failure_action === 'rollback' && positiveMonitor(update.monitor);
291
+ const rollbackValid = Object.keys(mapping(deploy.rollback_config)).length > 0;
292
+ const restartValid = ['none', 'any', 'on-failure'].includes(deploy.restart_policy?.condition);
293
+ if (!updateValid || !rollbackValid || !restartValid)
294
+ add('STACKS_UPDATE_POLICY_MISSING', `services.${id}.deploy`, 'Declare update order, failure_action: rollback, a positive monitor, rollback_config and restart_policy');
295
+ }
296
+
297
+ function runtimeStaticReference(t, node) {
298
+ if (!node.moduleSpecifier || !t.isStringLiteralLike(node.moduleSpecifier) || node.isTypeOnly) return null;
299
+ if (t.isImportDeclaration(node)) {
300
+ const clause = node.importClause;
301
+ if (clause?.isTypeOnly) return null;
302
+ const named = clause?.namedBindings;
303
+ if (named && t.isNamedImports(named) && !clause.name && named.elements.every(entry => entry.isTypeOnly)) return null;
304
+ }
305
+ if (t.isExportDeclaration(node) && node.exportClause && t.isNamedExports(node.exportClause)
306
+ && node.exportClause.elements.every(entry => entry.isTypeOnly)) return null;
307
+ return node.moduleSpecifier.text;
308
+ }
309
+
310
+ function runtimeReference(t, node) {
311
+ if (t.isImportDeclaration(node) || t.isExportDeclaration(node)) return runtimeStaticReference(t, node);
312
+ if (t.isImportEqualsDeclaration(node) && !node.isTypeOnly && t.isExternalModuleReference(node.moduleReference)) {
313
+ const reference = node.moduleReference.expression;
314
+ return reference && t.isStringLiteralLike(reference) ? reference.text : null;
315
+ }
316
+ if (!t.isCallExpression(node) || !node.arguments.length || !t.isStringLiteralLike(node.arguments[0])) return null;
317
+ const expression = node.expression;
318
+ if (expression.kind === t.SyntaxKind.ImportKeyword || (t.isIdentifier(expression) && expression.text === 'require')) return node.arguments[0].text;
319
+ return null;
320
+ }
321
+
322
+ function runtimeImports(source) {
323
+ const compiler = ts(), imports = [];
324
+ const visit = node => {
325
+ const reference = runtimeReference(compiler, node);
326
+ if (reference) imports.push(reference);
327
+ compiler.forEachChild(node, visit);
328
+ };
329
+ visit(source);
330
+ return imports;
331
+ }
332
+
333
+ function trackedCompilerHost(repoRoot, files) {
334
+ const relativeOf = absolute => path.relative(repoRoot, absolute).split(path.sep).join('/');
335
+ const readable = absolute => {
336
+ const relative = relativeOf(absolute);
337
+ return files.has(relative) && relative.startsWith('be/') && !relative.includes('/secrets/')
338
+ && (SOURCE.test(relative) || relative.endsWith('.json')) && safeRegularFile(repoRoot, relative);
339
+ };
340
+ return { ...ts().sys,
341
+ readFile: absolute => readable(absolute) ? readText(repoRoot, relativeOf(absolute)) ?? undefined : undefined,
342
+ fileExists: readable,
343
+ };
344
+ }
345
+
346
+ function compilerOptions(repoRoot, app, files, host) {
347
+ for (const file of [`be/apps/${app}/tsconfig.json`, 'be/tsconfig.json']) {
348
+ if (!files.has(file)) continue;
349
+ const project = readTypeScriptProject(ts(), path.join(repoRoot, file), host.readFile);
350
+ if (project.parsed) return project.parsed.options;
351
+ }
352
+ return { baseUrl: path.join(repoRoot, 'be'), paths: { '@features/*': ['src/features/*'], '@modules/*': ['src/modules/*'] } };
353
+ }
354
+
355
+ function sourceTarget(repoRoot, file, specifier, options, files, host) {
356
+ const absolute = resolveTypeScriptModule(ts(), specifier, path.join(repoRoot, file), options, host);
357
+ if (!absolute) return null;
358
+ const relative = path.relative(repoRoot, absolute).split(path.sep).join('/');
359
+ return files.has(relative) && SOURCE.test(relative) && !TEST_SOURCE.test(relative) && !relative.includes('/secrets/') ? relative : null;
360
+ }
361
+
362
+ function appComposesRealtime(repoRoot, app, files) {
363
+ if (!repoRoot) return false;
364
+ const tracked = new Set(files), host = trackedCompilerHost(repoRoot, tracked);
365
+ const options = compilerOptions(repoRoot, app, tracked, host);
366
+ const entry = `be/apps/${app}/src/main.ts`;
367
+ const queue = tracked.has(entry) ? [entry] : [];
368
+ const seen = new Set();
369
+ while (queue.length) {
370
+ const file = queue.shift();
371
+ if (seen.has(file)) continue;
372
+ seen.add(file);
373
+ if (REALTIME_SOURCE.test(file)) return true;
374
+ if (!safeRegularFile(repoRoot, file)) continue;
375
+ const source = parseSource(readText(repoRoot, file) ?? '', file);
376
+ for (const specifier of runtimeImports(source)) {
377
+ const target = sourceTarget(repoRoot, file, specifier, options, tracked, host);
378
+ if (target) queue.push(target);
379
+ }
380
+ }
381
+ return false;
382
+ }
383
+
384
+ function realtimeIssues(input, add) {
385
+ if (input.repo?.edition !== 'lite') return;
386
+ const apis = new Set(list(input.repo?.sides?.be?.apps).filter(app => app.kind === 'api').map(app => app.name));
387
+ for (const [id, service] of Object.entries(mapping(input.model.services))) {
388
+ const component = mapping(input.doc.components)[id], app = component?.app ?? id;
389
+ if (!apis.has(app) || service.deploy?.replicas === 1) continue;
390
+ if (appComposesRealtime(input.repoRoot, app, list(input.files)))
391
+ add('STACKS_REALTIME_SCALED', `services.${id}.deploy.replicas`, 'A lite API composing realtime must pin deploy.replicas: 1 because its gateway runs in one API process');
392
+ }
393
+ }
394
+
395
+ /** Machine issues shared by the HFS finder and the rendered-model gate; no secret contents are read or returned. */
396
+ export function swarmHardeningIssues(input) {
397
+ const normalized = { ...input, doc: mapping(input.doc), spec: mapping(input.spec), model: mapping(input.model) };
398
+ const issues = [], add = (code, at, message) => issues.push({ code, at, message });
399
+ secretCustodyIssues(normalized.spec, normalized.model, add);
400
+ for (const [id, value] of Object.entries(mapping(normalized.model.services))) {
401
+ const service = mapping(value), component = mapping(normalized.doc.components)[id] ?? {};
402
+ environmentSecretIssues(id, service, normalized.spec, normalized.model, add);
403
+ publishedPortIssues(id, service, normalized.spec, add);
404
+ placementIssues(id, component, service, add);
405
+ if (!nontrivialHealthcheck(mapping(service.healthcheck)))
406
+ add('STACKS_HEALTHCHECK_MISSING', `services.${id}.healthcheck`, 'Every Swarm service needs an enabled healthcheck that can fail when its process is unhealthy');
407
+ updatePolicyIssues(id, service, add);
408
+ }
409
+ publicRoutingIssues(normalized, add);
410
+ realtimeIssues(normalized, add);
411
+ return issues;
412
+ }
413
+
414
+ /** R266 findings at the app root, over each available tracked rendered Swarm model. */
415
+ export function stacksSwarmFindings(context) {
416
+ return stacksDeploymentFindings(context, swarmHardeningIssues);
417
+ }
@@ -77,7 +77,7 @@ function providerSecretFindings(repoRoot, file, repo, findings) {
77
77
  }
78
78
 
79
79
  /** Raw L17 findings. The catalog-driven findings filter decides which editions judge them. */
80
- export function liteSecretCustodyFindings({ repoRoot, files, repo }) {
80
+ function liteSecretCustodyFindings({ repoRoot, files, repo }) {
81
81
  const findings = [];
82
82
  for (const file of files) {
83
83
  if (custodyPathFinding(file, repo, findings)) continue;
@@ -94,5 +94,5 @@ export function supabaseSecretFindings(input) {
94
94
  const ordinary = secretFindings(input);
95
95
  const lite = liteSecretCustodyFindings(input);
96
96
  const liteJwtAuthority = (input.repo.edition ?? 'full') === 'lite';
97
- return [...ordinary.filter(item => !(liteJwtAuthority && item.pattern === 'jwt' && inSecretFreeTree(input.repo, item.path))), ...lite];
97
+ return [...ordinary.filter(item => !(liteJwtAuthority && (item.pattern === 'jwt' || item.pattern === 'password-connection-string') && inSecretFreeTree(input.repo, item.path))), ...lite];
98
98
  }
@@ -0,0 +1,167 @@
1
+ // R269: app repositories in both editions declare dependency updates, a production audit gate, and image SBOM/provenance.
2
+ import { parseWorkflow } from '../runtime-rules/workflow-source.mjs';
3
+ import { found, readText } from './read.mjs';
4
+
5
+ const DEPENDABOT = '.github/dependabot.yml';
6
+ const CI = '.github/workflows/ci.yml';
7
+ const IMAGES = '.github/workflows/images.yml';
8
+ const ECOSYSTEMS = ['npm', 'github-actions', 'docker'];
9
+ const enabled = (entry) => entry && (entry.if === undefined || entry.if === true || entry.if === 'true') && !entry['continue-on-error'];
10
+
11
+ function shellSeparator(line, index, quote) {
12
+ if (quote) return null;
13
+ if (line[index] === ';') return ';';
14
+ const pair = line.slice(index, index + 2);
15
+ return pair === '&&' || pair === '||' ? pair : null;
16
+ }
17
+
18
+ function isShellComment(line, index, quote) {
19
+ return !quote && line[index] === '#' && (index === 0 || /\s/.test(line[index - 1]));
20
+ }
21
+
22
+ function shellQuote(char, quote) {
23
+ if (char === quote) return null;
24
+ return !quote && (char === '"' || char === "'") ? char : quote;
25
+ }
26
+
27
+ function appendShellCommand(commands, state, next) {
28
+ const tokens = state.command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((token) => token.replace(/^["']|["']$/g, '')) ?? [];
29
+ if (tokens.length) commands.push({ tokens, separator: state.separator, next });
30
+ state.separator = next;
31
+ state.command = '';
32
+ }
33
+
34
+ function shellLineCommands(line, commands) {
35
+ const state = { quote: null, command: '', separator: null };
36
+ for (let i = 0; i < line.length; i++) {
37
+ const char = line[i];
38
+ if (char === '\\' && state.quote !== "'") {
39
+ i++;
40
+ state.command += char + (line[i] ?? '');
41
+ continue;
42
+ }
43
+ if (isShellComment(line, i, state.quote)) break;
44
+ const next = shellSeparator(line, i, state.quote);
45
+ if (next) {
46
+ appendShellCommand(commands, state, next);
47
+ if (next.length === 2) i++;
48
+ continue;
49
+ }
50
+ state.quote = shellQuote(char, state.quote);
51
+ state.command += char;
52
+ }
53
+ appendShellCommand(commands, state, null);
54
+ }
55
+
56
+ /** Shell evidence is an executable command, never quoted text, a comment or a heredoc body. */
57
+ function shellCommands(run) {
58
+ if (typeof run !== 'string') return [];
59
+ const commands = [];
60
+ let heredoc = null;
61
+ for (const line of run.replace(/\\\r?\n/g, ' ').split(/\r?\n/)) {
62
+ if (heredoc) {
63
+ if (line.trim() === heredoc) heredoc = null;
64
+ continue;
65
+ }
66
+ heredoc = /<<-?\s*['"]?(\w+)['"]?/.exec(line)?.[1] ?? null;
67
+ shellLineCommands(line, commands);
68
+ }
69
+ return commands;
70
+ }
71
+
72
+ function executableCommands(run) {
73
+ const commands = shellCommands(run);
74
+ const uncheckedErrors = commands.some(({ tokens }) => tokens[0] === 'set' && tokens.includes('+e'));
75
+ if (uncheckedErrors) return [];
76
+ return commands.filter((command, index) => {
77
+ if (command.separator === '||' || command.next === '||') return false;
78
+ if (command.separator === '&&' && commands.slice(0, index).some(({ tokens }) => ['false', 'exit', 'return'].includes(tokens[0]))) return false;
79
+ return !commands.slice(index + 1).some(({ tokens }) => tokens[0] === 'exit' && tokens[1] === '0');
80
+ });
81
+ }
82
+
83
+ /** Only enabled jobs and steps can provide a supply-chain gate. Comments, labels and disabled steps are no evidence. */
84
+ function stepsOf(workflow) {
85
+ return Object.values(workflow?.jobs ?? {}).filter(enabled).flatMap((job) => Array.isArray(job.steps) ? job.steps.filter(enabled) : []);
86
+ }
87
+
88
+ function hasDependabot(document) {
89
+ const directory = (value) => typeof value === 'string' && value.startsWith('/');
90
+ return document?.version === 2 && Array.isArray(document.updates) && ECOSYSTEMS.every((ecosystem) => document.updates.some((update) =>
91
+ update?.['package-ecosystem'] === ecosystem &&
92
+ (directory(update.directory) || Array.isArray(update.directories) && update.directories.length > 0 && update.directories.every(directory)) &&
93
+ ['daily', 'weekly', 'monthly'].includes(update.schedule?.interval)));
94
+ }
95
+
96
+ function hasAudit(steps) {
97
+ return steps.some((step) => executableCommands(step.run).some(({ tokens }) =>
98
+ tokens[0] === 'npm' && tokens[1] === 'audit' &&
99
+ (tokens.includes('--omit=dev') || tokens.some((token, index) => token === '--omit' && tokens[index + 1] === 'dev'))));
100
+ }
101
+
102
+ const asserted = (value) => value === true || value === 'true';
103
+ const provenanceEnabled = (value) => asserted(value) || typeof value === 'string' && /^mode=(?:min|max)$/.test(value);
104
+
105
+ function imageActionEvidence(step) {
106
+ const action = step.uses ?? '';
107
+ if (/^docker\/build-push-action@/.test(action)) {
108
+ return { sbom: asserted(step.with?.sbom), provenance: provenanceEnabled(step.with?.provenance) };
109
+ }
110
+ return {
111
+ sbom: /^actions\/attest-sbom@/.test(action) && Boolean(step.with?.['sbom-path']),
112
+ provenance: /^actions\/attest-build-provenance@/.test(action) && Boolean(step.with?.['subject-path'] || step.with?.['subject-digest']),
113
+ };
114
+ }
115
+
116
+ function attestationType(value, type) {
117
+ return value === `type=${type}` || value?.startsWith(`type=${type},`) === true;
118
+ }
119
+
120
+ function hasAttestation(tokens, type) {
121
+ return tokens.some((token, index) =>
122
+ token.startsWith('--attest=') && attestationType(token.slice('--attest='.length), type) ||
123
+ token === '--attest' && attestationType(tokens[index + 1], type));
124
+ }
125
+
126
+ function isSbomCommand(tokens) {
127
+ if (tokens[0] === 'syft') {
128
+ return tokens.length > 1 && !tokens.some((token) => ['--help', '--version', 'help', 'version'].includes(token));
129
+ }
130
+ return tokens[0] === 'docker' && tokens[1] === 'sbom' && tokens.length > 2 && !tokens.includes('--help');
131
+ }
132
+
133
+ function imageCommandEvidence(tokens) {
134
+ if (tokens.slice(0, 3).join(' ') !== 'docker buildx build') return { sbom: isSbomCommand(tokens), provenance: false };
135
+ return {
136
+ sbom: tokens.includes('--sbom') || tokens.includes('--sbom=true') || hasAttestation(tokens, 'sbom'),
137
+ provenance: tokens.some((token) => /^--provenance(?:=(?:true|mode=(?:min|max)))?$/.test(token)) || hasAttestation(tokens, 'provenance'),
138
+ };
139
+ }
140
+
141
+ function imageEvidence(steps) {
142
+ let sbom = false;
143
+ let provenance = false;
144
+ for (const step of steps) {
145
+ const action = imageActionEvidence(step);
146
+ sbom ||= action.sbom;
147
+ provenance ||= action.provenance;
148
+ for (const { tokens } of executableCommands(step.run)) {
149
+ const command = imageCommandEvidence(tokens);
150
+ sbom ||= command.sbom;
151
+ provenance ||= command.provenance;
152
+ }
153
+ }
154
+ return sbom && provenance;
155
+ }
156
+
157
+ /** Synchronous judge of R269 at the app root; root-wide gates run only when their paths are in scope. */
158
+ export function supplyChainFindings({ repoRoot, files, all = files, repo, isRoot, inScope = () => true }) {
159
+ if (repo?.kind !== 'app' || !['lite', 'full'].includes(repo.edition) || isRoot === false) return [];
160
+ const tracked = new Set(all);
161
+ const parsed = (file) => tracked.has(file) ? parseWorkflow(readText(repoRoot, file) ?? '') : null;
162
+ const findings = [];
163
+ if (inScope(DEPENDABOT) && !hasDependabot(parsed(DEPENDABOT))) findings.push(found('HFS_DEPENDABOT_MISSING', DEPENDABOT, 'Declare Dependabot version 2 updates for npm, github-actions and docker, each with a directory and schedule.'));
164
+ if (inScope(CI) && !hasAudit(stepsOf(parsed(CI)))) findings.push(found('HFS_AUDIT_STEP_MISSING', CI, 'CI must run npm audit --omit=dev in an enabled step whose failure stops the job.'));
165
+ if (inScope(IMAGES) && !imageEvidence(stepsOf(parsed(IMAGES)))) findings.push(found('HFS_SBOM_MISSING', IMAGES, 'The images workflow must generate an SBOM and provenance for its release images in enabled steps.'));
166
+ return findings;
167
+ }