@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,414 @@
1
+ import { composeEntries } from '../lib/compose-values.mjs';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { lsFiles } from '../api/git/ls-files.mjs';
5
+ import {parseYaml} from '../../engine/yaml.mjs';
6
+ import {slash} from '../lib/path-key.mjs';
7
+ import { isInside, walkFiles } from '../lib/walk.mjs';
8
+ import { composeIncludePaths, swarmInventoryIssues, swarmModelIssues } from '../lib/swarm-stack.mjs';
9
+ import { swarmHardeningIssues } from './rules/stacks-swarm.mjs';
10
+ import { ingressIssues } from './rules/stacks-ingress.mjs';
11
+ import { liteBackingIssues } from './rules/stacks-lite-backing.mjs';
12
+ import { releaseBackupIssues, scheduleIssues } from './rules/stacks-release.mjs';
13
+ import { stacksDeploymentContexts } from './rules/stacks-runtime.mjs';
14
+ import { appEdition, applicationStackFindings, liteCustodyDirectory } from './starcistacks-runtime.mjs';
15
+
16
+ const RESULT='starci/application-stacks-check@1';
17
+ const STACKS_DIR='.starcistacks';
18
+ const MAX_INPUT_BYTES=4*1024*1024;
19
+ const SCHEMA_ID='starci/application-stacks@1';
20
+ const RUNBOOK_COMMANDS=['prepare','doctor','up','status','logs','down','verification'];
21
+ const PLAINTEXT_SECRET_PATTERN=/\.(key|pem|p12|pfx)$/i;
22
+ const inside=(root,target)=>isInside(path.resolve(root),path.resolve(target));
23
+ const object=value=>value&&typeof value==='object'&&!Array.isArray(value);
24
+ const sensitive=/pass(word)?|secret|token|credential|private[_-]?key|api[_-]?key/i;
25
+ const nonempty=value=>typeof value==='string'&&Boolean(value.trim());
26
+ const manifestSchema=parseYaml(fs.readFileSync(new URL('../../modules/schemas/application-stacks.schema.yaml',import.meta.url),'utf8'));
27
+ const HARDENING_CODES=new Set([
28
+ 'STACKS_SECRET_NOT_EXTERNAL','STACKS_SECRET_UNVERSIONED','STACKS_SECRET_ENV_LITERAL','STACKS_PORT_PUBLISHED',
29
+ 'STACKS_SECOND_INGRESS','STACKS_DNS_UNDECLARED','STACKS_HOST_UNROUTED','STACKS_TLS_UNDECLARED',
30
+ 'STACKS_PLACEMENT_MISSING','STACKS_HEALTHCHECK_MISSING','STACKS_UPDATE_POLICY_MISSING','STACKS_REALTIME_SCALED',
31
+ 'STACKS_RELEASE_UNDECLARED','BACKUP_UNDECLARED','BACKUP_PROOF_MISSING','STACKS_MODEL_UNAVAILABLE',
32
+ 'HFS_SCHEDULE_UNDECLARED','STACKS_INGRESS_UNDECLARED','STACKS_INGRESS_UNKNOWN',
33
+ 'STACKS_LITE_SELFHOSTED_DB','STACKS_SIDECAR_VOLUME_UNDECLARED','STACKS_SIDECAR_DB_UNSUPPORTED',
34
+ ]);
35
+
36
+ function regular(file,{root=null}={}){
37
+ try{
38
+ const stat=fs.lstatSync(file);if(!stat.isFile()||stat.isSymbolicLink())return false;
39
+ return !root||inside(root,fs.realpathSync(file));
40
+ }catch{return false;}
41
+ }
42
+ function directory(file){try{return fs.lstatSync(file).isDirectory()&&!fs.lstatSync(file).isSymbolicLink();}catch{return false;}}
43
+ function safeAncestors(root,target){
44
+ if(!inside(root,target))return false;
45
+ let cursor=path.resolve(target),base=path.resolve(root);
46
+ while(cursor!==base){
47
+ if(fs.existsSync(cursor)&&fs.lstatSync(cursor).isSymbolicLink())return false;
48
+ cursor=path.dirname(cursor);
49
+ }
50
+ return true;
51
+ }
52
+ function resolvedPath(base,relative){
53
+ if(!nonempty(relative)||path.isAbsolute(relative)||String(relative).split(/[\\/]/).includes('..'))return null;
54
+ const target=path.resolve(base,String(relative));return inside(base,target)&&safeAncestors(base,target)?target:null;
55
+ }
56
+
57
+ function validateSchema(value){
58
+ const errors=[],isObject=value=>value!==null&&typeof value==='object'&&!Array.isArray(value),resolve=ref=>ref.slice(2).split('/').reduce((node,key)=>node?.[key],manifestSchema);
59
+ const typeOk=(node,type)=>{
60
+ if(type==='object')return isObject(node);
61
+ if(type==='array')return Array.isArray(node);
62
+ if(type==='integer')return Number.isInteger(node);
63
+ if(type)return typeof node===type;
64
+ return true;
65
+ };
66
+ const checkString=(node,shape,at)=>{
67
+ if(shape.minLength&&node.length<shape.minLength)errors.push({path:at,message:'string too short'});
68
+ if(shape.pattern&&!new RegExp(shape.pattern,'u').test(node))errors.push({path:at,message:'pattern mismatch'});
69
+ };
70
+ const checkNumber=(node,shape,at)=>{
71
+ if(shape.minimum!==undefined&&node<shape.minimum)errors.push({path:at,message:'number below minimum'});
72
+ if(shape.maximum!==undefined&&node>shape.maximum)errors.push({path:at,message:'number above maximum'});
73
+ };
74
+ const checkObject=(node,shape,at)=>{
75
+ if(shape.minProperties&&Object.keys(node).length<shape.minProperties)errors.push({path:at,message:'object has too few properties'});
76
+ for(const key of shape.required??[])if(!Object.hasOwn(node,key))errors.push({path:at,message:`missing ${key}`});
77
+ for(const [key,child] of Object.entries(node)){
78
+ if(Object.hasOwn(shape.properties??{},key))check(child,shape.properties[key],`${at}/${key}`);
79
+ else if(shape.additionalProperties===false)errors.push({path:`${at}/${key}`,message:'unknown field'});
80
+ else if(isObject(shape.additionalProperties))check(child,shape.additionalProperties,`${at}/${key}`);
81
+ }
82
+ };
83
+ const checkAlternatives=(node,shape,at,check)=>{
84
+ const matched=shape.anyOf.some((option)=>{
85
+ const start=errors.length;check(node,option,at);
86
+ if(errors.length===start)return true;
87
+ errors.splice(start);return false;
88
+ });
89
+ if(!matched)errors.push({path:at,message:'matches none of the allowed shapes'});
90
+ };
91
+ const checkArray=(node,shape,at,check)=>{
92
+ if(shape.minItems&&node.length<shape.minItems)errors.push({path:at,message:'array too short'});
93
+ node.forEach((item,index)=>shape.items&&check(item,shape.items,`${at}/${index}`));
94
+ };
95
+ const check=(node,shape,at)=>{
96
+ if(shape?.$ref)return check(node,resolve(shape.$ref),at);
97
+ if(Array.isArray(shape?.anyOf)){
98
+ checkAlternatives(node,shape,at,check);
99
+ return;
100
+ }
101
+ const typed=typeOk(node,shape?.type);
102
+ if(!typed){errors.push({path:at,message:`expected ${shape.type}`});return;}
103
+ if(Object.hasOwn(shape??{},'const')&&node!==shape.const)errors.push({path:at,message:'const mismatch'});
104
+ if(Array.isArray(shape?.enum)&&!shape.enum.includes(node))errors.push({path:at,message:'value outside enum'});
105
+ if(typeof node==='string')checkString(node,shape,at);
106
+ if(typeof node==='number')checkNumber(node,shape,at);
107
+ if(Array.isArray(node)){
108
+ checkArray(node,shape,at,check);
109
+ return;
110
+ }
111
+ if(!isObject(node))return;
112
+ checkObject(node,shape,at);
113
+ };check(value,manifestSchema,'$');return errors;
114
+ }
115
+ const unresolved=value=>typeof value==='string'&&/(^|[^$])\$\{/.test(value);
116
+ const serviceUnresolved=(service)=>{
117
+ const build=object(service?.build)?service.build:{context:service?.build};
118
+ const values=[service?.image,build.context,build.dockerfile,...(Array.isArray(service?.ports)?service.ports:[])];
119
+ for(const volume of Array.isArray(service?.volumes)?service.volumes:[])values.push(object(volume)?volume.source:String(volume).split(':')[0]);
120
+ for(const [key,value] of composeEntries(service?.environment, { isMapping: object }))if(!sensitive.test(key))values.push(value);
121
+ return values.some(unresolved);
122
+ };
123
+ function unresolvedCompose(model){
124
+ for(const service of Object.values(object(model?.services)?model.services:{})){
125
+ if(serviceUnresolved(service))return true;
126
+ }
127
+ return Object.values(object(model?.secrets)?model.secrets:{}).some(item=>unresolved(item?.file));
128
+ }
129
+ function sopsEnvelope(file){
130
+ // Recognizes both the YAML/JSON envelope (a "sops" object) and the flattened dotenv envelope SOPS
131
+ // emits for a .env member (sops_lastmodified=, sops_mac=, sops_age__list_0__map_enc=, ...).
132
+ try{
133
+ if(fs.statSync(file).size>MAX_INPUT_BYTES)return false;
134
+ const text=fs.readFileSync(file,'utf8');
135
+ return (/(?:(?:^|\n)sops:|["']sops["']\s*:|(?:^|\n)sops_[a-z0-9_]+=)/.test(text))&&/ENC\[AES256_GCM,data:/.test(text);
136
+ }catch{return false;}
137
+ }
138
+
139
+ function gitTrackedSet(root){
140
+ try{
141
+ const result=lsFiles(['--',STACKS_DIR],{dir:root,timeout:5000});
142
+ if(result.error||result.status!==0||typeof result.stdout!=='string')return null;
143
+ return new Set(result.stdout.split(/\r?\n/).filter(Boolean).map(slash));
144
+ }catch{return null;}
145
+ }
146
+ function composeLike(file){
147
+ try{
148
+ if(fs.statSync(file).size>MAX_INPUT_BYTES)return false;
149
+ const doc=parseYaml(fs.readFileSync(file,'utf8'));
150
+ return object(doc)&&(object(doc.services)||Array.isArray(doc.include));
151
+ }catch{return false;}
152
+ }
153
+ function reachableComposeFiles(envDir,entries){
154
+ const reachable=new Set(),queue=[...entries];
155
+ while(queue.length){
156
+ const relative=queue.shift(),target=resolvedPath(envDir,relative);
157
+ if(!target||reachable.has(target))continue;
158
+ reachable.add(target);
159
+ if(!regular(target,{root:envDir}))continue;
160
+ let doc=null;try{doc=parseYaml(fs.readFileSync(target,'utf8'));}catch{continue;}
161
+ for(const include of composeIncludePaths(doc??{}).paths)
162
+ queue.push(path.isAbsolute(include)?include:slash(path.join(path.relative(envDir,path.dirname(target)),include)));
163
+ }
164
+ return reachable;
165
+ }
166
+ function scanForUndeclaredCompose(root,envDir,reachable,add,at){
167
+ const infra=path.join(envDir,'infra');
168
+ if(!directory(infra))return;
169
+ const stack=[infra];
170
+ while(stack.length){
171
+ const dir=stack.pop();
172
+ let entries=[];try{entries=fs.readdirSync(dir,{withFileTypes:true});}catch{continue;}
173
+ for(const entry of entries){
174
+ const full=path.join(dir,entry.name);
175
+ if(entry.isSymbolicLink())continue;
176
+ if(entry.isDirectory()){stack.push(full);continue;}
177
+ if(!/\.ya?ml$/i.test(entry.name)||reachable.has(full)||!composeLike(full))continue;
178
+ add('STACKS_UNDECLARED_COMPOSE',at,`${slash(path.relative(root,full))} is a Compose-shaped file under infra/ that is not declared or reachable from composeFiles; the composeFiles list must be the complete list for this environment`);
179
+ }
180
+ }
181
+ }
182
+
183
+ const loadInputs=({manifestFile,deploymentModelFile,errors,add})=>{
184
+ const input={manifest:{},model:{}};
185
+ try{if(!errors.some(item=>['manifest-unavailable','input-too-large'].includes(item.code)&&item.path===`${STACKS_DIR}/application-stacks.yaml`))input.manifest=parseYaml(fs.readFileSync(manifestFile,'utf8'));}
186
+ catch{add('manifest-invalid',`${STACKS_DIR}/application-stacks.yaml`,'manifest YAML could not be parsed');}
187
+ try{if(!errors.some(item=>['deployment-model-unavailable','input-too-large'].includes(item.code)&&item.path==='deploymentModelFile')){
188
+ const text=fs.readFileSync(deploymentModelFile,'utf8');try{input.model=JSON.parse(text);}catch{input.model=parseYaml(text);}
189
+ }}catch{add('deployment-model-invalid','deploymentModelFile','rendered deployment JSON or YAML could not be parsed');}
190
+ if(!object(input.manifest))input.manifest={};
191
+ if(!object(input.model))input.model={};
192
+ return input;
193
+ };
194
+ const checkStackEntry=(entry,environmentNames,add,{stacksRoot,lite})=>{
195
+ if(!entry.isDirectory()||entry.isSymbolicLink())return;
196
+ if(entry.name==='tmp'){add('STACKS_SCRATCH_IN_CANONICAL',`${STACKS_DIR}/tmp`,'scratch belongs in the OS temp directory, not a tracked .starcistacks/tmp');return;}
197
+ if(entry.name==='k8s')return;
198
+ if(lite&&liteCustodyDirectory(path.join(stacksRoot,entry.name)))return;
199
+ if(!environmentNames.has(entry.name))add('STACKS_UNDECLARED_ENVIRONMENT',`${STACKS_DIR}/${entry.name}`,`${entry.name} is not an environment the declaration admits; either the declaration admits it or it is not an environment`);
200
+ };
201
+ const checkDeclaredEnvironmentDirs=(stacksRoot,environmentNames,add)=>{
202
+ for(const name of environmentNames)if(!directory(path.join(stacksRoot,name)))add('environment-directory-missing',`${STACKS_DIR}/${name}`,`declared environment ${name} has no directory`);
203
+ };
204
+ const checkStacksTree=({stacksRoot,manifest,environmentNames,add,lite})=>{
205
+ if(!directory(stacksRoot))return;
206
+ let entries=[];try{entries=fs.readdirSync(stacksRoot,{withFileTypes:true});}catch{}
207
+ for(const entry of entries)checkStackEntry(entry,environmentNames,add,{stacksRoot,lite});
208
+ checkDeclaredEnvironmentDirs(stacksRoot,environmentNames,add);
209
+ const k8sDir=path.join(stacksRoot,'k8s'),k8sExists=directory(k8sDir);
210
+ if(manifest.k8s?.status==='supported'&&!k8sExists)add('k8s-directory-missing',`${STACKS_DIR}/k8s`,'k8s.status is supported but .starcistacks/k8s is missing');
211
+ if(manifest.k8s?.status==='deferred'&&k8sExists)add('k8s-directory-unexpected',`${STACKS_DIR}/k8s`,'k8s.status is deferred, so a .starcistacks/k8s directory contradicts the declaration');
212
+ };
213
+ const checkComposeFiles=({spec,environment,envDir,root,add})=>{
214
+ const composeFiles=Array.isArray(spec.composeFiles)?spec.composeFiles:[];
215
+ if(!composeFiles.length)add('compose-files-empty',`environments.${environment}.composeFiles`,'declare at least one environment-relative Compose source file');
216
+ const safeComposeFiles=[];
217
+ for(const [index,relative] of composeFiles.entries()){
218
+ const target=resolvedPath(envDir,relative);
219
+ if(!target||!regular(target,{root:envDir}))add('compose-path-unsafe',`environments.${environment}.composeFiles[${index}]`,'Compose source must be a regular non-symlink file below the environment directory');
220
+ else safeComposeFiles.push(relative);
221
+ }
222
+ if(directory(envDir)){
223
+ if(nonempty(spec.renderedModel))safeComposeFiles.push(spec.renderedModel);
224
+ const reachable=reachableComposeFiles(envDir,safeComposeFiles);
225
+ scanForUndeclaredCompose(root,envDir,reachable,add,`environments.${environment}.composeFiles`);
226
+ }
227
+ };
228
+ const checkRunbook=({spec,environment,envDir,add})=>{
229
+ if(!nonempty(spec.runbook))add('runbook-path-invalid',`environments.${environment}.runbook`,'runbook must name a file inside the environment directory');
230
+ else{
231
+ const runbookFile=resolvedPath(envDir,spec.runbook);
232
+ const oversized=(()=>{try{return fs.statSync(runbookFile).size>MAX_INPUT_BYTES;}catch{return true;}})();
233
+ if(!runbookFile||!regular(runbookFile,{root:envDir})||oversized)
234
+ add('runbook-path-invalid',`environments.${environment}.runbook`,'runbook must resolve to an existing regular non-symlink file below the environment directory');
235
+ else{
236
+ let text='';try{text=fs.readFileSync(runbookFile,'utf8');}catch{}
237
+ for(const command of RUNBOOK_COMMANDS){
238
+ const rowPattern=new RegExp(String.raw`^\s*\|\s*${command}\s*\|`,'im');
239
+ if(!rowPattern.test(text))add('runbook-command-missing',`environments.${environment}.runbook#${command}`,`runbook must document the ${command} command`);
240
+ }
241
+ }
242
+ }
243
+ };
244
+ const checkSecret=(secret,at,{envDir,environment,root,tracked,secretNames,add})=>{
245
+ if(!object(secret))return;
246
+ if(secretNames.has(secret.name))add('secret-duplicate',`${at}.name`,'secret names must be unique');else secretNames.add(secret.name);
247
+ const plain=nonempty(secret.where)?resolvedPath(envDir,secret.where):null;
248
+ const enc=nonempty(secret.where)?resolvedPath(envDir,`${secret.where}.enc`):null;
249
+ if(!enc||!regular(enc,{root:envDir})||!sopsEnvelope(enc))
250
+ add('encrypted-ref-invalid',`${at}.where`,'secret must resolve to <where>.enc as a recognizable SOPS envelope, an existing regular file below the environment directory');
251
+ if(nonempty(secret.where)&&!plain)
252
+ add('materialized-path-invalid',`${at}.where`,'secret plaintext path and its existing ancestors must remain below the environment directory without symlinks');
253
+ if(!String(secret.recipientPolicy??'').trim()||!String(secret.keyCustody??'').trim())
254
+ add('secret-policy-missing',at,'secret recipient and key custody policies are required');
255
+ if(tracked&&plain&&tracked.has(slash(path.relative(root,plain))))
256
+ add('STACKS_PLAINTEXT_SECRET',`${at}.where`,'a decrypted secret member must never be a tracked file; only its .enc member is committed');
257
+ };
258
+ const checkSecrets=({spec,environment,envDir,root,add})=>{
259
+ const declaredSecrets=Array.isArray(spec.secrets)?spec.secrets:[],secretNames=new Set();
260
+ const tracked=directory(envDir)?gitTrackedSet(root):null;
261
+ for(const [index,secret] of declaredSecrets.entries())
262
+ checkSecret(secret,`environments.${environment}.secrets[${index}]`,{envDir,environment,root,tracked,secretNames,add});
263
+ if(tracked){
264
+ const envPrefix=`${slash(path.relative(root,envDir))}/`;
265
+ for(const relTracked of tracked){
266
+ if(!relTracked.startsWith(envPrefix))continue;
267
+ const base=path.basename(relTracked);
268
+ if(PLAINTEXT_SECRET_PATTERN.test(base)||base==='id_rsa')
269
+ add('STACKS_PLAINTEXT_SECRET',relTracked,'a tracked file below the environment directory matches a plaintext secret pattern; only <name>.enc may be committed');
270
+ }
271
+ }
272
+ };
273
+ const checkEnvironmentSpec=({spec,environment,envDir,ids,root,add})=>{
274
+ if(spec.status!=='supported')add('environment-unsupported',`environments.${environment}`,'environment must be explicitly supported');
275
+ if(!['docker-compose','docker-swarm'].includes(spec.runtime))add('runtime-invalid',`environments.${environment}.runtime`,'runtime must be docker-compose or docker-swarm');
276
+ checkComposeFiles({spec,environment,envDir,root,add});
277
+ const bindings=object(spec.components)?spec.components:{};
278
+ for(const id of Object.keys(bindings))if(!ids.has(id))
279
+ add('binding-component-unknown',`environments.${environment}.components.${id}`,'environment binds a component id that is absent from the top-level inventory');
280
+ checkRunbook({spec,environment,envDir,add});
281
+ checkSecrets({spec,environment,envDir,root,add});
282
+ };
283
+ const checkServiceIds=(services,ids,add)=>{
284
+ for(const service of Object.keys(services))if(!ids.has(service))
285
+ add('compose-service-unclassified',`services.${service}`,'rendered service name is not a declared component id');
286
+ };
287
+ const checkServiceEnvironment=(services,add)=>{
288
+ for(const [service,definition] of Object.entries(services))for(const [key] of composeEntries(definition?.environment, { isMapping: object }))
289
+ if(sensitive.test(key)&&!/_FILE$/i.test(key))add('plaintext-sensitive-environment',`services.${service}.environment.${key}`,'sensitive configuration is present as a direct environment value; value redacted');
290
+ };
291
+ const checkModel=({model,ids,spec,manifest,lite,add})=>{
292
+ const services=object(model.services)?model.services:{};
293
+ checkServiceIds(services,ids,add);
294
+ if(unresolvedCompose(model))add('compose-placeholder-unresolved','composeModel','rendered Compose still contains an unresolved interpolation placeholder in a host-resolved field');
295
+ checkServiceEnvironment(services,add);
296
+ if(object(spec)&&spec.runtime==='docker-swarm')for(const issue of swarmModelIssues({model,components:manifest.components,bindings:spec.components,lite}))
297
+ add('HFS_STACKS_SHAPE',issue.at,issue.message);
298
+ };
299
+
300
+ const checkInputPaths=({environment,stacksRoot,manifestFile,deploymentModelFile,add})=>{
301
+ if(!nonempty(environment))add('environment-invalid','environment','environment must name a declared environment');
302
+ if(!regular(manifestFile,{root:stacksRoot}))add('manifest-unavailable',`${STACKS_DIR}/application-stacks.yaml`,`manifest must be a regular file inside ${STACKS_DIR}`);
303
+ if(!regular(deploymentModelFile))add('deployment-model-unavailable','deploymentModelFile','rendered deployment evidence must be a regular, non-symlink file');
304
+ for(const [file,at] of [[manifestFile,`${STACKS_DIR}/application-stacks.yaml`],[deploymentModelFile,'deploymentModelFile']])
305
+ try{if(fs.statSync(file).size>MAX_INPUT_BYTES)add('input-too-large',at,'input exceeds the 4 MiB static-check limit');}catch{}
306
+ };
307
+ const componentIdsOf=(manifest,add)=>{
308
+ const catalog=object(manifest.components)?manifest.components:{},ids=new Set(Object.keys(catalog));
309
+ if(!ids.size)add('components-empty','components','declare at least one applicable application component');
310
+ for(const id of ids)if(!/^[a-z][a-z0-9-]*$/.test(id))add('component-invalid',`components.${id}`,'component ids must be lowercase kebab-case');
311
+ return ids;
312
+ };
313
+ const environmentMapOf=(manifest,add)=>{
314
+ const environments=object(manifest.environments)?manifest.environments:{},environmentNames=new Set(Object.keys(environments));
315
+ if(!environmentNames.size)add('environments-empty','environments','declare at least one supported environment');
316
+ return {environments,environmentNames};
317
+ };
318
+ const checkKubernetesDeclaration=(manifest,add)=>{
319
+ if(!['deferred','supported'].includes(manifest.k8s?.status)||!String(manifest.k8s?.reason??'').trim())
320
+ add('k8s-status-invalid','k8s','Kubernetes must be explicitly deferred or supported with a reason');
321
+ };
322
+ const checkSelectedEnvironment=({environment,environmentNames,environments,stacksRoot,ids,root,model,manifest,lite,add})=>{
323
+ if(nonempty(environment)&&!environmentNames.has(String(environment)))
324
+ add('environment-unknown','environment',`environment must be one the declaration admits: ${[...environmentNames].join(', ')}`);
325
+ const spec=environments[String(environment)],envDir=path.join(stacksRoot,String(environment??''));
326
+ if(object(spec))checkEnvironmentSpec({spec,environment,envDir,ids,root,add});
327
+ checkModel({model,ids,spec,manifest,lite,add});
328
+ };
329
+ const checkStacksManifest=(manifest,model,{stacksRoot,environment,root,repo,add})=>{
330
+ for(const issue of validateSchema(manifest))add('schema-shape-invalid',issue.path,issue.message);
331
+ if(manifest.schema!==SCHEMA_ID)add('schema-invalid','schema',`expected ${SCHEMA_ID}`);
332
+ const ids=componentIdsOf(manifest,add),{environments,environmentNames}=environmentMapOf(manifest,add);
333
+ const lite=repo.edition==='lite';
334
+ checkStacksTree({stacksRoot,manifest,environmentNames,add,lite});
335
+ checkKubernetesDeclaration(manifest,add);
336
+ for(const issue of swarmInventoryIssues({components:manifest.components,lite}))add('HFS_STACKS_SHAPE',issue.at,issue.message);
337
+ checkSelectedEnvironment({environment,environmentNames,environments,stacksRoot,ids,root,model,manifest,lite,add});
338
+ };
339
+
340
+ function deploymentAvailable(model,errors){
341
+ const invalid=errors.some(item=>item.path==='deploymentModelFile');
342
+ return !invalid&&object(model.services)&&Object.keys(model.services).length>0&&model.include===undefined;
343
+ }
344
+
345
+ function diskEvidenceFiles(root){
346
+ const excluded=new Set(['node_modules','.git','.claude','.aws','.codex']);
347
+ return walkFiles(root,{ignoreReadErrors:true,exclude:(name,_file,entry)=>entry.isSymbolicLink()||excluded.has(name)})
348
+ .map(file=>slash(path.relative(root,file)));
349
+ }
350
+
351
+ function checkSwarmHardening({manifest,model,environment,root,files,repo,errors,add}){
352
+ const spec=manifest.environments?.[environment];
353
+ if(spec?.runtime!=='docker-swarm')return;
354
+ if(!deploymentAvailable(model,errors)){
355
+ add('STACKS_MODEL_UNAVAILABLE',`environments.${environment}`,'a rendered deployment model with nonempty services is required to judge Swarm safety');
356
+ return;
357
+ }
358
+ const context={doc:manifest,environment,spec,model,repoRoot:root,files,repo};
359
+ for(const finder of [swarmHardeningIssues,ingressIssues,liteBackingIssues,releaseBackupIssues,scheduleIssues])
360
+ for(const issue of finder(context))add(issue.code,issue.at,issue.message);
361
+ }
362
+
363
+ export function checkApplicationStacks({repoRoot,environment,deploymentModelFile,files,repo}={}){
364
+ const root=path.resolve(String(repoRoot??'')),stacksRoot=path.join(root,STACKS_DIR),manifestFile=path.join(stacksRoot,'application-stacks.yaml'),errors=[];
365
+ const declaration=repo??{edition:appEdition(root)};
366
+ const add=(code,at,message)=>errors.push({code,path:at,message});
367
+ checkInputPaths({environment,stacksRoot,manifestFile,deploymentModelFile,add});
368
+ const {manifest,model}=loadInputs({manifestFile,deploymentModelFile,errors,add});
369
+ checkStacksManifest(manifest,model,{stacksRoot,environment,root,repo:declaration,add});
370
+ checkSwarmHardening({manifest,model,environment,root,files:files??diskEvidenceFiles(root),repo:declaration,errors,add});
371
+ for(const item of applicationStackFindings(root))add(item.code,item.path,item.message);
372
+
373
+ return {schema:RESULT,ok:errors.length===0,environment,manifest:manifestFile,deploymentModel:path.resolve(String(deploymentModelFile??'')),errors,
374
+ limitations:['static conformance only; Docker was not invoked, host application processes were not started, and remote APIs were not called','the deployment model must be rendered by Docker Compose or Docker Stack tooling; provenance is supplied by the caller and is not independently authenticated','component classification, runbook completeness and secret custody are author assertions checked for consistency and structure, not independently discovered facts','SOPS envelope recognition is structural, not cryptographic verification or decryption','tracked-plaintext detection uses git ls-files when the repository is reachable and is skipped, not assumed clean, when it is not','runbook prepare, doctor and verification commands are declared and documented but not executed']};
375
+ }
376
+
377
+ const declarationFindingCodes=new Set(['STACKS_RELEASE_UNDECLARED','BACKUP_UNDECLARED','BACKUP_PROOF_MISSING','HFS_SCHEDULE_UNDECLARED']);
378
+
379
+ function trackedGateFinding(entry,error){
380
+ let file=entry.file;
381
+ const code=HARDENING_CODES.has(error.code)?error.code:'HFS_STACKS_SHAPE';
382
+ const modelPath=/^(?:services(?:\.|$)|secrets(?:\.|$)|composeModel$|deploymentModelFile$)/.test(error.path);
383
+ if(declarationFindingCodes.has(error.code)||!modelPath&&code==='HFS_STACKS_SHAPE')file=`${STACKS_DIR}/application-stacks.yaml`;
384
+ if(error.code==='HFS_SCHEDULE_UNDECLARED'){
385
+ const source=error.path.replace(/:\d+$/,'');
386
+ if(/\.(?:sql|[cm]?[jt]s)$/.test(source))file=source;
387
+ }
388
+ return {code,level:'error',path:file,message:`${file}: ${error.path}: ${error.message}`};
389
+ }
390
+
391
+ const findingKey=finding=>`${finding.code}\n${slash(finding.path)}\n${finding.message}`;
392
+
393
+ function trackedGateErrors(entry,context,files){
394
+ if(entry.unavailable)return [{code:'STACKS_MODEL_UNAVAILABLE',level:'error',path:entry.file,
395
+ message:`${entry.file}: environments.${entry.environment}: a tracked rendered deployment model is required to judge Swarm safety`}];
396
+ const result=checkApplicationStacks({repoRoot:context.repoRoot,environment:entry.environment,
397
+ deploymentModelFile:path.resolve(context.repoRoot,entry.file),files,repo:context.repo});
398
+ return result.errors.map(error=>trackedGateFinding(entry,error));
399
+ }
400
+
401
+ /** Run the public gate on the app's tracked rendered Swarm evidence, without duplicating finder results. */
402
+ export function trackedStacksGateFindings(context={}){
403
+ const {deployments,files}=stacksDeploymentContexts(context),findings=[];
404
+ const existing=context.existingFindings??[],seen=new Set(existing.map(findingKey));
405
+ for(const entry of deployments){
406
+ for(const finding of trackedGateErrors(entry,context,files)){
407
+ if(context.inScope&&!context.inScope(finding.path))continue;
408
+ const key=findingKey(finding);
409
+ if(seen.has(key))continue;
410
+ seen.add(key);findings.push(finding);
411
+ }
412
+ }
413
+ return findings;
414
+ }
@@ -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
+ }
@@ -9,14 +9,6 @@ import { byCodeUnit } from '../lib/list.mjs';
9
9
  // builds afresh.
10
10
  const runs = new AsyncLocalStorage();
11
11
 
12
- /** A program run: run(fn) shares what is built inside fn; release() drops it. */
13
- export function typeScriptProgramRun() {
14
- const values = new Map();
15
- return {
16
- run: fn => runs.run(values, fn),
17
- release: () => values.clear(),
18
- };
19
- }
20
12
 
21
13
  function keyOf(value) {
22
14
  if (value === undefined) return 'u';
@@ -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
+ }
@@ -11,8 +11,8 @@ export const isSpecRun = (env = process.env) => Boolean(env.NODE_TEST_CONTEXT);
11
11
 
12
12
  /**
13
13
  * The environment for a child that is itself a test runner: `env` without the mark of an enclosing spec run. A `node --test` that inherits NODE_TEST_CONTEXT believes it is a
14
- * subtest of that run: it reports to its parent and exits 0 whatever its tests did, so a verb that runs specs as a child (starci test affected, the land gate, the release cut, the
15
- * suite) would pass red specs from inside a test process. The runner marks the spec files it starts itself, so isSpecRun stays true inside them.
14
+ * subtest of that run: it reports to its parent and exits 0 whatever its tests did, so a verb that runs specs as a child (the package clean-test, the scaffold e2e, the release cut)
15
+ * would pass red specs from inside a test process. The runner marks the spec files it starts itself, so isSpecRun stays true inside them.
16
16
  */
17
17
  export function withoutTestRunner(env = process.env) {
18
18
  const { NODE_TEST_CONTEXT: _enclosing, ...rest } = env;