@starci/hfs 4.2.17 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (564) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +7 -0
  3. package/db/migrate.mjs +361 -0
  4. package/db/post-cutover.mjs +136 -0
  5. package/emit/db-types.mjs +10 -1
  6. package/lint/run.mjs +1 -1
  7. package/package.json +4 -1
  8. package/release/gate.mjs +298 -0
  9. package/release/index.mjs +103 -0
  10. package/release/push.mjs +110 -0
  11. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  12. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  13. package/runtime/knowledge/hfs/rules.yaml +3437 -160
  14. package/runtime/knowledge/hfs/slots.yaml +430 -92
  15. package/runtime/knowledge/patterns/be/api.yaml +92 -2
  16. package/runtime/knowledge/patterns/be/cli.yaml +37 -9
  17. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  18. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  19. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  20. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  21. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  22. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  23. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  24. package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
  25. package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
  26. package/runtime/knowledge/sonar-gate.yaml +1 -1
  27. package/runtime/modules/kernel/failure-codes.yaml +3128 -47
  28. package/runtime/modules/models/runtimes.yaml +1 -1
  29. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  30. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  31. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  32. package/runtime/scripts/api/git/add.mjs +5 -0
  33. package/runtime/scripts/api/git/cat-file.mjs +5 -0
  34. package/runtime/scripts/api/git/init.mjs +7 -0
  35. package/runtime/scripts/api/git/lib.mjs +3 -0
  36. package/runtime/scripts/api/git/tag.mjs +5 -0
  37. package/runtime/scripts/api/node/lib.mjs +20 -0
  38. package/runtime/scripts/api/node/run-node.mjs +6 -0
  39. package/runtime/scripts/api/sops/lib.mjs +41 -0
  40. package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
  41. package/runtime/scripts/api/sops/seal.mjs +1 -1
  42. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  43. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  44. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  45. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  46. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  47. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  48. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
  49. package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
  50. package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
  51. package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
  52. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
  53. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  54. package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
  55. package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
  56. package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
  57. package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
  58. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  59. package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
  60. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  61. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
  62. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
  63. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  64. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  65. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  66. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
  67. package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
  68. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
  69. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  70. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  71. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  72. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  73. package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
  74. package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
  75. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  76. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
  77. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
  78. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
  79. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  80. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  81. package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
  82. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
  83. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
  84. package/runtime/scripts/hfs/check-results.mjs +28 -0
  85. package/runtime/scripts/hfs/check.mjs +63 -72
  86. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  87. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  88. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  89. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  90. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  91. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  92. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  93. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  94. package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
  95. package/runtime/scripts/hfs/path-findings.mjs +32 -2
  96. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  97. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  98. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  99. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  100. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  101. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  102. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  103. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  104. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  105. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  106. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  107. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  108. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  109. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  110. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  111. package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
  112. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  113. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  114. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  115. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  116. package/runtime/scripts/hfs/rules/database.mjs +11 -2
  117. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  118. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  119. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  120. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  121. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  122. package/runtime/scripts/hfs/rules/edition.mjs +11 -10
  123. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  124. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  125. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  126. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  127. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  128. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  129. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  130. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  131. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  132. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  133. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  134. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  135. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  136. package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
  137. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  138. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  139. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  140. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  141. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  142. package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
  143. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  144. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  145. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  146. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  147. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  148. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  149. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
  150. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  151. package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
  152. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  153. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  154. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  155. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  156. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  157. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  158. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  159. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  160. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  161. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  162. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  163. package/runtime/scripts/hfs/secret-document.mjs +97 -0
  164. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  165. package/runtime/scripts/hfs/secret-errors.mjs +20 -0
  166. package/runtime/scripts/hfs/secret-files.mjs +124 -0
  167. package/runtime/scripts/hfs/secret-init.mjs +300 -0
  168. package/runtime/scripts/hfs/secret-output.mjs +100 -0
  169. package/runtime/scripts/hfs/secret-sops.mjs +130 -0
  170. package/runtime/scripts/hfs/secret-store.mjs +163 -0
  171. package/runtime/scripts/hfs/secret.mjs +162 -193
  172. package/runtime/scripts/hfs/slot-imports.mjs +18 -3
  173. package/runtime/scripts/hfs/slot-required.mjs +13 -0
  174. package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
  175. package/runtime/scripts/hfs/slots.mjs +1 -0
  176. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  177. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  178. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  179. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  180. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  181. package/runtime/scripts/hfs/tree-findings.mjs +22 -0
  182. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  183. package/runtime/scripts/lib/compose-values.mjs +11 -0
  184. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  185. package/runtime/scripts/lib/json-clone.mjs +61 -0
  186. package/runtime/scripts/lib/list.mjs +4 -0
  187. package/runtime/scripts/lib/roots.mjs +59 -0
  188. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  189. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  190. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  191. package/runtime/scripts/lib/stack-services.mjs +1 -1
  192. package/runtime/scripts/lib/swarm-stack.mjs +126 -0
  193. package/runtime/scripts/lib/trim-end.mjs +8 -0
  194. package/runtime/scripts/lib/ts-analysis.mjs +23 -0
  195. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  196. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  197. package/scaffold/add-app.mjs +77 -59
  198. package/scaffold/add-cli-lite.mjs +24 -17
  199. package/scaffold/add-compose.mjs +79 -0
  200. package/scaffold/add-database-lite.mjs +14 -0
  201. package/scaffold/add-fe-overlay.mjs +122 -0
  202. package/scaffold/add-fe-page.mjs +269 -0
  203. package/scaffold/add-inbox-lite.mjs +87 -124
  204. package/scaffold/add-plan.mjs +183 -0
  205. package/scaffold/add-table.mjs +132 -70
  206. package/scaffold/add.mjs +186 -204
  207. package/scaffold/app.mjs +128 -43
  208. package/scaffold/edition-gate.mjs +3 -3
  209. package/scaffold/image.mjs +2 -2
  210. package/scaffold/lite-exports.mjs +2 -1
  211. package/scaffold/service.mjs +2 -2
  212. package/src/main.mjs +238 -119
  213. package/stack/age-install.mjs +37 -0
  214. package/stack/backup.mjs +100 -0
  215. package/stack/build-args.mjs +42 -0
  216. package/stack/build.mjs +173 -0
  217. package/stack/cli.mjs +130 -0
  218. package/stack/config.mjs +57 -0
  219. package/stack/context.mjs +57 -0
  220. package/stack/converge.mjs +107 -0
  221. package/stack/deploy.mjs +301 -0
  222. package/stack/digest.mjs +16 -0
  223. package/stack/docker-transport.mjs +53 -0
  224. package/stack/edge-alias.mjs +118 -0
  225. package/stack/errors.mjs +21 -0
  226. package/stack/image-scan.mjs +152 -0
  227. package/stack/ingress-create.mjs +114 -0
  228. package/stack/ingress-publish.mjs +273 -0
  229. package/stack/ingress.mjs +706 -0
  230. package/stack/jobs.mjs +51 -0
  231. package/stack/live-restore.mjs +177 -0
  232. package/stack/prepare.mjs +266 -0
  233. package/stack/private-file.mjs +22 -0
  234. package/stack/provision.mjs +90 -0
  235. package/stack/prune.mjs +46 -0
  236. package/stack/release-store.mjs +87 -0
  237. package/stack/remote.mjs +205 -0
  238. package/stack/render.mjs +149 -0
  239. package/stack/retire.mjs +47 -0
  240. package/stack/sealed.mjs +149 -0
  241. package/stack/task.mjs +42 -0
  242. package/stack/verify.mjs +99 -0
  243. package/sync/ci-commands.mjs +13 -0
  244. package/sync/index.mjs +127 -64
  245. package/sync/stack-images.mjs +127 -0
  246. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  247. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  248. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  249. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  250. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  251. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  252. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  253. package/templates/app/codegen-graphql.mjs.partial +13 -2
  254. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  255. package/templates/app/codegen-prelude.mjs.partial +8 -0
  256. package/templates/app/gitignore +4 -0
  257. package/templates/app/gitignore-lite +4 -0
  258. package/templates/app/hooks/husky/pre-commit +7 -12
  259. package/templates/app/package-scripts/package.json +1 -0
  260. package/templates/app/package-scripts/supabase.json +7 -0
  261. package/templates/app/package-scripts-lite/package.json +1 -7
  262. package/templates/app/skeleton/.editorconfig +1 -0
  263. package/templates/app/skeleton/.gitattributes +7 -1
  264. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  265. package/templates/app/skeleton/.nvmrc +1 -1
  266. package/templates/app/skeleton/.sops.yaml +1 -1
  267. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  268. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  269. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  270. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  271. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  272. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  273. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  275. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  276. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  277. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  278. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  279. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  280. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  281. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  282. package/templates/app/skeleton/README.md +6 -6
  283. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  284. package/templates/app/skeleton-lite/.editorconfig +16 -0
  285. package/templates/app/skeleton-lite/.gitattributes +8 -0
  286. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  287. package/templates/app/skeleton-lite/.nvmrc +1 -0
  288. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  289. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
  290. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  291. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  292. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  293. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  294. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  295. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  296. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  297. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  298. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  299. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  300. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  301. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  302. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  303. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  304. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  305. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  306. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  307. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  308. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  309. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  310. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  311. package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
  312. package/templates/app/skeleton-lite/README.md +8 -4
  313. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  314. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  315. package/templates/app/starciwork.gitignore-lite +6 -0
  316. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  317. package/templates/be/image/worker/Dockerfile +1 -1
  318. package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
  319. package/templates/be/inbox/database-client.ts.tpl +14 -28
  320. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  321. package/templates/be/inbox/event-index.ts.tpl +1 -0
  322. package/templates/be/inbox/events.json.tpl +10 -0
  323. package/templates/be/inbox/migration.sql.tpl +1 -0
  324. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  325. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  326. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  327. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  328. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  329. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  330. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  331. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  332. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  333. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  334. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  335. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  336. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  337. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  338. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  339. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  340. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  341. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  342. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  343. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  344. package/templates/be/inbox/service.spec.ts.tpl +49 -20
  345. package/templates/be/inbox/service.ts.tpl +27 -16
  346. package/templates/be/inbox/sql.ts.tpl +2 -2
  347. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
  348. package/templates/be/inbox/webhook-door.ts.tpl +4 -3
  349. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  350. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  351. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  352. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  353. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  354. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  355. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  356. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  357. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  358. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  359. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  360. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  362. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  363. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  364. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  365. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  366. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  367. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  368. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  369. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  370. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  371. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  372. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  373. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  374. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  375. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  376. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  377. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  378. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  379. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  380. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  381. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  382. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  383. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  384. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  385. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  386. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  387. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  388. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  389. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  390. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  391. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  392. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  393. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  394. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  395. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  396. package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
  397. package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
  398. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  399. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  400. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  401. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  402. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  403. package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
  404. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
  405. package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
  406. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  407. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  408. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
  409. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  410. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  411. package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
  412. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  413. package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
  414. package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
  415. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  416. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  417. package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
  418. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  419. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  420. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  421. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  422. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  423. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  424. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  425. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  426. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  427. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  428. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  429. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  430. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  431. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  432. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  433. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  434. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  435. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  436. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  437. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  438. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  439. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  440. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  441. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  442. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  443. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  444. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  445. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  446. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  447. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  448. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  449. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  450. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
  451. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  452. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  453. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  454. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  455. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  456. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  457. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  458. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  459. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  460. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  461. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  462. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  463. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  464. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  465. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  466. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  467. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  468. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  469. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  470. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  471. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  472. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  473. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  474. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  475. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  476. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  477. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  478. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  479. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  480. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  481. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  482. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  483. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  484. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  485. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  486. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  487. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  488. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  489. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
  490. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
  491. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
  492. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  493. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  494. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  495. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
  496. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  497. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  498. package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
  499. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
  500. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  501. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  502. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  503. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  504. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  505. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  506. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  507. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  508. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  509. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  510. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  511. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  512. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  513. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  514. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  515. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  516. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  517. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  518. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  519. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  520. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  521. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  522. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  523. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  524. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  525. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  526. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  527. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  528. package/templates/be/table/migration.sql.tpl +1 -0
  529. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  530. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  531. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  532. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  533. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  534. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  535. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  536. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  537. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  538. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  539. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  540. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  541. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  542. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  543. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  544. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  545. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  546. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  547. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  548. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  549. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  550. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  551. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  552. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  553. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  554. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  555. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  556. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  557. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  558. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  559. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  560. package/upgrade/check.mjs +3 -1
  561. package/upgrade/file-state.mjs +68 -0
  562. package/upgrade/index.mjs +58 -94
  563. package/runtime/scripts/api/git/log.mjs +0 -5
  564. package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
@@ -1,220 +1,189 @@
1
- // secret.mjs - `starci app secret list|show|set|gen`: the canon command over an app's sealed secrets.
2
- // (R06: a secret exists only as a sops envelope there). It is the one way an operator or an agent reads or writes one; a plain
3
- // `sops` command in a KEYS.md or a runbook is not the way. Every sops use goes through scripts/api/sops (decrypt.mjs, seal.mjs):
4
- // starci app secret list [--env <name>] every secret of the env and the keys it holds (names only, nothing decrypted)
5
- // starci app secret show <slug> [--key <NAME>] [--env <name>] decrypt one value to stdout
6
- // starci app secret set <slug> [--key <NAME>] [--age <recipient>]... [--env <name>]
7
- // the value is read from stdin (never from an argument); a new secret takes its
8
- // recipients from --age, else from the repository's .sops.yaml
9
- // starci app secret gen <slug> [--key <NAME>] [--bytes <n>] [--age <recipient>]... [--env <name>]
10
- // a random value (base64url of n bytes, default 32) sealed without being printed
11
- // A single-value secret holds its value under the key `data`. Every verb runs at the app root (the folder of hfs.json), or at --cwd.
12
- // Exit 0 done, 2 a refusal or bad usage (a secret value is never part of a message).
13
- import fs from 'node:fs';
14
- import path from 'node:path';
15
- import { runProgram } from '../api/process/run-program.mjs';
16
- import { resolveRealTool } from '../api/process/resolve-real-tool.mjs';
17
- const sopsInvocation = Object.freeze({runProgram,resolveRealTool});
18
- import { randomBytes, randomUUID } from 'node:crypto';
19
- import { claimFile } from '../api/fs/claim-file.mjs';
20
- import { isSopsEnvelope } from '../lib/sops-envelope.mjs';
21
- import { decrypt as sopsDecrypt } from '../api/sops/decrypt.mjs';
22
- import { seal as sopsSeal } from '../api/sops/seal.mjs';
23
- import { byCodeUnit } from '../lib/list.mjs';
24
-
25
- class SecretError extends Error {}
26
-
27
- const SLUG = /^[a-z0-9][a-z0-9-]*$/u;
28
- const KEY = /^[A-Za-z_]\w*$/u;
29
- const WHITESPACE = String.raw`\s`;
30
- const REQUIRED_INDENT = `${WHITESPACE}+`;
31
- const OPTIONAL_DASH = '-?';
32
- const OPTIONAL_SPACING = `${WHITESPACE}*`;
33
- const RECIPIENT_NAME = 'recipient:';
34
- const RECIPIENT_VALUE = String.raw`(\S+)`;
35
- const RECIPIENT_LINE = new RegExp(`^${REQUIRED_INDENT}${OPTIONAL_DASH}${OPTIONAL_SPACING}${RECIPIENT_NAME}${OPTIONAL_SPACING}${RECIPIENT_VALUE}`, 'gmu');
36
- const DEFAULT_KEY = 'data';
37
- const VERBS = new Set(['list', 'show', 'set', 'gen']);
38
-
39
- /** The envelope format of a sealed secret, read from its text: json, yaml or dotenv. */
40
- export function formatOf(text) {
41
- const body = String(text).trimStart();
42
- if (body.startsWith('{')) return 'json';
43
- if (/^sops:\s*$/mu.test(text)) return 'yaml';
44
- return 'dotenv';
45
- }
46
-
47
- /** The key names a sealed document holds and the age recipients it is sealed to, read without decrypting anything. */
48
- export function envelopeOf(text) {
49
- const format = formatOf(text);
50
- if (format === 'json') {
51
- const doc = JSON.parse(text);
52
- return { format, keys: Object.keys(doc).filter((name) => name !== 'sops'), recipients: (doc.sops?.age ?? []).map((entry) => entry.recipient).filter(Boolean) };
53
- }
54
- if (format === 'yaml') {
55
- const keys = [...text.matchAll(/^([A-Za-z_]\w*):/gmu)].map((match) => match[1]).filter((name) => name !== 'sops');
56
- return { format, keys, recipients: [...text.matchAll(RECIPIENT_LINE)].map((match) => match[1]) };
57
- }
58
- const keys = [...text.matchAll(/^([A-Za-z_]\w*)=/gmu)].map((match) => match[1]).filter((name) => !name.startsWith('sops_'));
59
- return { format, keys, recipients: [...text.matchAll(/^sops_age__list_\d+__map_recipient=(\S+)/gmu)].map((match) => match[1]) };
60
- }
61
-
62
- /** The plaintext document `map` in the given format. A dotenv value cannot hold a line break. */
63
- export function plaintextOf(format, map) {
64
- if (format === 'json') return `${JSON.stringify(map)}\n`;
65
- if (format === 'yaml') return Object.entries(map).map(([name, value]) => name + ': ' + JSON.stringify(value)).join('\n') + '\n';
66
- for (const [name, value] of Object.entries(map)) if (/[\r\n]/u.test(value)) throw new SecretError(`${name} holds a line break; a dotenv secret holds one-line values only`);
67
- return Object.entries(map).map(([name, value]) => name + '=' + value).join('\n') + '\n';
68
- }
69
-
70
- /** The directory of the sealed secrets of the app at `repoRoot` for `env`; the one env that has secrets when none is named. */
71
- function secretsDirectory(repoRoot, env) {
72
- const stacks = path.join(repoRoot, '.starcistacks');
73
- if (!fs.existsSync(stacks)) throw new SecretError('this app has no .starcistacks; run starci app secret at the app root, or pass --cwd');
74
- if (env) {
75
- if (!/^[a-z0-9][a-z0-9-]*$/u.test(env)) throw new SecretError(`--env ${env} is not an environment name`);
76
- return path.join(stacks, env, 'secrets');
77
- }
78
- const withSecrets = fs.readdirSync(stacks, { withFileTypes: true }).filter((entry) => entry.isDirectory() && fs.existsSync(path.join(stacks, entry.name, 'secrets'))).map((entry) => entry.name);
79
- if (withSecrets.length === 1) return path.join(stacks, withSecrets[0], 'secrets');
80
- throw new SecretError(withSecrets.length === 0 ? 'no environment of .starcistacks has a secrets folder; pass --env to create the first secret' : `more than one environment has secrets (${withSecrets.join(', ')}); pass --env`);
81
- }
82
-
83
- const fileOf = (directory, slug) => {
84
- if (!SLUG.test(slug ?? '')) throw new SecretError(`${slug === undefined ? 'a secret name' : slug} is not a secret slug: lowercase letters, digits and dashes`);
85
- return path.join(directory, `${slug}.enc`);
86
- };
87
-
88
- /** The default sops seam: the runtime's sops api, which finds the binary on this machine. */
89
- function defaultSops(env = process.env) {
90
- const refuse = (what, result) => {
91
- if (result.error?.identityRefusal) return Object.assign(new SecretError(result.error.message), { identityRefusal: result.error.identityRefusal });
92
- if (result.error?.code === 'SOPS_MISSING') return new SecretError(result.error.message);
93
- return new SecretError(`sops could not ${what} (exit ${String(result.status)})`);
94
- };
95
- return {
96
- decrypt(file, format) {
97
- const result = sopsDecrypt(null, ['decrypt', '--input-type', format, '--output-type', 'json', file], { env, invocation: sopsInvocation });
98
- if (result.status !== 0) throw refuse(`decrypt ${path.basename(file)}; is the age identity installed?`, result);
99
- try { return JSON.parse(result.stdout); } catch { throw new SecretError('sops returned an invalid plaintext document'); }
100
- },
101
- seal(request) {
102
- const result = sopsSeal(null, request, { env, invocation: sopsInvocation });
103
- if (result.status !== 0) throw refuse('seal the secret', result);
104
- return result.stdout;
105
- },
106
- };
1
+ // secret.mjs - app secret init|set|get|unset|rename|remove|materialize|list|verify: sealed custody and private KEY files.
2
+ // SOPS plaintext travels on stdin and stays in memory; normal CLI output contains names and local file paths only.
3
+ // Every verb runs at the app root or --cwd. Exit 0 done, 2 refusal; --reveal is the only plaintext stdout path.
4
+ // unset, rename and remove edit one sealed member and are dry runs until --execute (scripts/hfs/secret-edit.mjs).
5
+ // verify decrypts each member in memory and names only keys and violated shapes, never a value (R249).
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { randomBytes } from "node:crypto";
9
+ import { invocationDir } from "../lib/roots.mjs";
10
+ import { initializeSecrets } from "./secret-init.mjs";
11
+ import { validateKey } from "./secret-document.mjs";
12
+ import { defaultSops } from "./secret-sops.mjs";
13
+ import { fileOf, secretsDirectory, writeSecret } from "./secret-store.mjs";
14
+ import { getSecret, listSecrets, materializeSecrets } from "./secret-output.mjs";
15
+ import { SecretError, publicStorage, secretErrorMessage } from "./secret-errors.mjs";
16
+ import { editSecret, validateEditNames } from "./secret-edit.mjs";
17
+ import { verifySecrets } from "./rules/env-value-shape.mjs";
18
+
19
+ export { envelopeOf, formatOf, plaintextOf } from "./secret-document.mjs";
20
+ export { setSecretValues } from "./secret-store.mjs";
21
+
22
+ const VERBS = new Set(["init", "list", "get", "set", "unset", "rename", "remove", "materialize", "verify"]);
23
+ const EDIT_VERBS = new Set(["unset", "rename", "remove"]);
24
+
25
+ function valueOption(opts, flag, value) {
26
+ if (value === undefined || value.startsWith("--"))
27
+ throw new SecretError(`${flag} needs a value`);
28
+ if (Object.hasOwn(opts, flag.slice(2)))
29
+ throw new SecretError(`${flag} may be given only once`);
30
+ opts[flag.slice(2)] = value;
107
31
  }
108
32
 
109
33
  function parse(argv) {
110
- const opts = { positional: [], age: [] };
34
+ const opts = { positional: [] };
111
35
  for (let i = 0; i < argv.length; i += 1) {
112
36
  const arg = argv[i];
113
- if (['--cwd', '--env', '--key', '--bytes', '--age'].includes(arg)) {
114
- if (argv[i + 1] === undefined) throw new SecretError(`${arg} needs a value`);
115
- if (arg === '--age') opts.age.push(argv[i + 1]); else opts[arg.slice(2)] = argv[i + 1];
116
- i += 1;
117
- } else if (arg.startsWith('--')) throw new SecretError(`unknown flag ${arg}`);
37
+ const [flag, inline] = arg.split(/=(.*)/su);
38
+ if (["--cwd", "--generate"].includes(flag)) {
39
+ valueOption(opts, flag, inline ?? argv[++i]);
40
+ } else if (["--to-file", "--reveal", "--execute", "--force-nonempty"].includes(flag)) {
41
+ if (inline !== undefined && !["true", "false"].includes(inline))
42
+ throw new SecretError(`${flag} expects true or false`);
43
+ opts[flag.slice(2)] = inline !== "false";
44
+ } else if (arg.startsWith("--"))
45
+ throw new SecretError(`unknown flag ${arg}`);
118
46
  else opts.positional.push(arg);
119
47
  }
120
48
  return opts;
121
49
  }
122
50
 
123
- const readStdin = () => fs.readFileSync(0, 'utf8');
124
-
125
- /** Seal `key = value` into the secret `slug`: an existing document keeps its other keys and its recipients. */
126
- function writeSecret({ file, values, age, sops }) {
127
- for (const key of Object.keys(values)) if (!KEY.test(key)) throw new SecretError(`${key} is not a key name`);
128
- const held = claimFile(path.resolve(file));
129
- if (!held.ok) throw new SecretError('the sealed secret is being updated; retry after that write settles');
130
- try {
131
- let format = 'json';
132
- let map = {};
133
- let recipients = age;
134
- if (fs.existsSync(file)) {
135
- const text = fs.readFileSync(file, 'utf8');
136
- const envelope = envelopeOf(text);
137
- format = envelope.format;
138
- map = sops.decrypt(file, format);
139
- if (!age.length) recipients = envelope.recipients;
140
- }
141
- Object.assign(map, values);
142
- const sealed = sops.seal({ inputType: format, plaintext: plaintextOf(format, map), recipients, filenameOverride: file });
143
- let parsed;
144
- try { parsed = envelopeOf(sealed); } catch { throw new SecretError('sops returned an invalid sealed document'); }
145
- if (!isSopsEnvelope(sealed) || parsed.keys.length !== Object.keys(map).length || !parsed.keys.every(key => Object.hasOwn(map, key))) throw new SecretError('sops returned an invalid sealed document');
146
- fs.mkdirSync(path.dirname(file), { recursive: true });
147
- const temporary = path.join(path.dirname(file), `.${path.basename(file)}-${randomUUID()}.tmp`);
148
- try {
149
- fs.writeFileSync(temporary, sealed, { flag: 'wx', mode: 0o600, flush: true });
150
- fs.renameSync(temporary, file);
151
- } finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
152
- } finally { held.release(); }
153
- }
51
+ const readStdin = () => {
52
+ if (process.stdin.isTTY)
53
+ throw new SecretError(
54
+ "set reads a value from redirected stdin; use --generate <bytes> to generate one",
55
+ );
56
+ return fs.readFileSync(0, "utf8");
57
+ };
154
58
 
155
- /** Update named keys through the same canonical sealed-secret owner used by the CLI. */
156
- export function setSecretValues(repoRoot, { slug, values, env = null }, { sops = null, age = [], processEnv = process.env } = {}) {
157
- const directory = secretsDirectory(repoRoot, env);
158
- const file = fileOf(directory, slug);
159
- if (!values || typeof values !== 'object' || Array.isArray(values) || !Object.keys(values).length) throw new SecretError('secret update requires named values');
160
- for (const value of Object.values(values)) if (typeof value !== 'string' || value === '') throw new SecretError('secret values must be nonempty strings');
161
- writeSecret({ file, values, age, sops: sops ?? defaultSops(processEnv) });
162
- return { file, via: 'sealed-secret' };
59
+ function usageArguments(verb) {
60
+ if (verb === "init") return "";
61
+ if (verb === "set") return "<env> <slug> <KEY> [--generate <bytes>]";
62
+ if (verb === "get") return "<env> <slug> [KEY] --to-file [--reveal]";
63
+ if (verb === "unset") return "<env> <slug> <KEY> [--execute]";
64
+ if (verb === "rename") return "<env> <slug> <OLD> <NEW> [--execute]";
65
+ if (verb === "remove") return "<env> <slug> [--execute] [--force-nonempty]";
66
+ return "<env>";
163
67
  }
164
68
 
165
- function listSecrets(opts, directory, repoRoot, stdout) {
166
- if (opts.positional.length) throw new SecretError('starci app secret list takes no name');
167
- const files = fs.existsSync(directory) ? fs.readdirSync(directory).filter((name) => name.endsWith('.enc')).sort(byCodeUnit) : [];
168
- for (const name of files) stdout(`${name.slice(0, -'.enc'.length)} ${envelopeOf(fs.readFileSync(path.join(directory, name), 'utf8')).keys.join(', ')}\n`);
169
- stdout(`${files.length} secret${files.length === 1 ? '' : 's'} in ${path.relative(repoRoot, directory).split(path.sep).join('/')}\n`);
170
- return 0;
69
+ function editOptionsFor(verb, opts) {
70
+ if (opts.execute !== undefined && !EDIT_VERBS.has(verb))
71
+ throw new SecretError("--execute is only valid for unset, rename and remove");
72
+ if (opts["force-nonempty"] !== undefined && verb !== "remove")
73
+ throw new SecretError("--force-nonempty is only valid for remove");
171
74
  }
172
75
 
173
- function showSecret(opts, file, seam, stdout) {
174
- if (!fs.existsSync(file)) throw new SecretError(`${opts.positional[0]} is not sealed in this env`);
175
- const { format, keys } = envelopeOf(fs.readFileSync(file, 'utf8'));
176
- if (opts.key === undefined && !keys.includes(DEFAULT_KEY) && keys.length !== 1) throw new SecretError(`${opts.positional[0]} holds ${keys.join(', ')}; name one with --key`);
177
- const map = seam.decrypt(file, format);
178
- const name = opts.key ?? (keys.includes(DEFAULT_KEY) ? DEFAULT_KEY : keys[0]);
179
- if (!Object.hasOwn(map, name)) throw new SecretError(`${opts.positional[0]} has no key ${name}`);
180
- stdout(`${String(map[name])}\n`);
181
- return 0;
76
+ function outputOptionsFor(verb, opts) {
77
+ if (
78
+ (opts["to-file"] !== undefined || opts.reveal !== undefined) &&
79
+ verb !== "get"
80
+ )
81
+ throw new SecretError("--to-file and --reveal are only valid for get");
82
+ if (verb === "get" && !opts["to-file"] && !opts.reveal)
83
+ throw new SecretError("get requires --to-file or explicit --reveal");
182
84
  }
183
85
 
184
- function setSecretFromStdin(opts, file, key, seam, stdin, stdout) {
185
- const value = String(stdin()).replace(/\r?\n$/u, '');
186
- if (value === '') throw new SecretError('the value on stdin is empty');
187
- writeSecret({ file, values: { [key]: value }, age: opts.age, sops: seam });
188
- stdout(`sealed ${opts.positional[0]} (${key})\n`);
189
- return 0;
86
+ function optionsFor(verb, opts) {
87
+ const count = opts.positional.length;
88
+ const bounds = {
89
+ init: [0, 0],
90
+ list: [1, 1],
91
+ materialize: [1, 1],
92
+ verify: [1, 1],
93
+ set: [3, 3],
94
+ get: [2, 3],
95
+ unset: [3, 3],
96
+ rename: [4, 4],
97
+ remove: [2, 2],
98
+ }[verb];
99
+ if (count < bounds[0] || count > bounds[1])
100
+ throw new SecretError(
101
+ `usage: starci app secret ${verb} ${usageArguments(verb)}`,
102
+ );
103
+ if (opts.generate !== undefined && verb !== "set")
104
+ throw new SecretError("--generate is only valid for set");
105
+ outputOptionsFor(verb, opts);
106
+ editOptionsFor(verb, opts);
107
+ if (EDIT_VERBS.has(verb)) validateEditNames(verb, opts.positional.slice(2));
108
+ if (verb === "set") validateKey(opts.positional[2]);
109
+ if (verb === "get" && opts.positional[2] !== undefined)
110
+ validateKey(opts.positional[2]);
190
111
  }
191
112
 
192
- function generateSecret(opts, file, key, seam, random, stdout) {
193
- const bytes = opts.bytes === undefined ? 32 : Number(opts.bytes);
194
- if (!Number.isInteger(bytes) || bytes < 16 || bytes > 1024) throw new SecretError('--bytes is a whole number from 16 to 1024');
195
- writeSecret({ file, values: { [key]: random(bytes).toString('base64url') }, age: opts.age, sops: seam });
196
- stdout(`generated and sealed ${opts.positional[0]} (${key}, ${bytes} bytes); read it with starci app secret show\n`);
197
- return 0;
113
+ function secretValue(opts, stdin, random) {
114
+ let value;
115
+ if (opts.generate !== undefined) {
116
+ const bytes = Number(opts.generate);
117
+ if (!Number.isInteger(bytes) || bytes < 1 || bytes > 1024)
118
+ throw new SecretError(
119
+ "--generate must be a whole byte count from 1 to 1024",
120
+ );
121
+ value = random(bytes).toString("base64url");
122
+ } else {
123
+ if (typeof stdin !== "function" && stdin?.isTTY)
124
+ throw new SecretError("set requires redirected stdin");
125
+ value = String(
126
+ typeof stdin === "function"
127
+ ? stdin()
128
+ : fs.readFileSync(stdin?.fd ?? 0, "utf8"),
129
+ ).replace(/\r?\n$/u, "");
130
+ }
131
+ if (!value) throw new SecretError("the value on stdin is empty");
132
+ return value;
198
133
  }
199
134
 
200
- /** `starci app secret <verb> ...`; `stdin` returns the value `set` seals, `sops` and `random` are test seams. */
201
- export function secretMain(argv, { stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), stdin = readStdin, sops = null, random = randomBytes, env = process.env, cwd = process.cwd() } = {}) {
135
+ /** All app secrets verbs; stdin, sops, random and tool ports permit isolated custody specs. */
136
+ export function secretMain(
137
+ argv,
138
+ {
139
+ stdout = (s) => process.stdout.write(s),
140
+ stderr = (s) => process.stderr.write(s),
141
+ stdin = readStdin,
142
+ sops = null,
143
+ random = randomBytes,
144
+ env = process.env,
145
+ cwd = invocationDir(),
146
+ platform = process.platform,
147
+ tools,
148
+ } = {},
149
+ ) {
202
150
  const [verb, ...rest] = argv;
203
151
  try {
204
- if (!VERBS.has(verb)) throw new SecretError('usage: starci app secret list | show <slug> | set <slug> | gen <slug> [--key NAME] [--env NAME] [--age RECIPIENT] [--bytes N] [--cwd DIR]');
152
+ if (!VERBS.has(verb))
153
+ throw new SecretError(
154
+ "usage: starci app secret init | set <env> <slug> <KEY> | get <env> <slug> [KEY] --to-file | unset <env> <slug> <KEY> | rename <env> <slug> <OLD> <NEW> | remove <env> <slug> | materialize <env> | list <env> | verify <env>",
155
+ );
205
156
  const opts = parse(rest);
206
- const repoRoot = path.resolve(cwd, opts.cwd ?? '.');
207
- const directory = secretsDirectory(repoRoot, opts.env);
208
- const key = opts.key ?? DEFAULT_KEY;
209
- if (verb === 'list') return listSecrets(opts, directory, repoRoot, stdout);
210
- if (opts.positional.length !== 1) throw new SecretError(`starci app secret ${verb} takes one secret name`);
211
- const file = fileOf(directory, opts.positional[0]);
212
- const seam = sops ?? defaultSops(env);
213
- if (verb === 'show') return showSecret(opts, file, seam, stdout);
214
- if (verb === 'set') return setSecretFromStdin(opts, file, key, seam, stdin, stdout);
215
- return generateSecret(opts, file, key, seam, random, stdout);
157
+ optionsFor(verb, opts);
158
+ const repoRoot = path.resolve(cwd, opts.cwd ?? ".");
159
+ if (verb === "init")
160
+ return publicStorage(() =>
161
+ initializeSecrets(repoRoot, { env, platform, tools, stdout }),
162
+ );
163
+ const [environment, slug, key] = opts.positional;
164
+ const directory = secretsDirectory(repoRoot, environment);
165
+ if (verb === "list") return listSecrets(directory, stdout);
166
+ const open = () => sops ?? defaultSops(env, repoRoot, platform, tools);
167
+ if (EDIT_VERBS.has(verb))
168
+ return editSecret(verb, {
169
+ directory, environment, slug, names: opts.positional.slice(2),
170
+ execute: opts.execute === true, force: opts["force-nonempty"] === true, sops: open, stdout,
171
+ });
172
+ const seam = open();
173
+ if (verb === "verify")
174
+ return verifySecrets({ repoRoot, directory, environment, sops: seam, stdout });
175
+ const output = { repoRoot, environment, env, platform, stdout };
176
+ if (verb === "materialize")
177
+ return materializeSecrets(directory, seam, output);
178
+ const file = fileOf(directory, slug);
179
+ if (verb === "get") return getSecret(file, key, seam, opts, output, stderr);
180
+ const value = secretValue(opts, stdin, random);
181
+ writeSecret({ file, values: { [key]: value }, age: [], sops: seam });
182
+ stdout(`sealed ${environment}/${slug} (${key})\n`);
183
+ return 0;
216
184
  } catch (error) {
217
- if (error instanceof SecretError || error instanceof SyntaxError) { stderr(`starci app secret: ${error.message}\n`); return 2; }
218
- throw error;
185
+ const message = secretErrorMessage(error);
186
+ stderr(`starci app secret: ${message}\n`);
187
+ return 2;
219
188
  }
220
189
  }
@@ -2,9 +2,14 @@
2
2
  // owner units, the layer order and the public entry of an owner. Cycles are a graph property of the architecture check, not of one edge.
3
3
  import { RUNTIME_KIND } from './manifest-shape.mjs';
4
4
  import { classifyIn, ownerIn, tierIn } from './slot-classify.mjs';
5
+ import { braceVariants, globExpression } from '../lib/glob.mjs';
5
6
 
6
7
  const isEntryFile = (name) => name === 'index.ts' || name === 'index.tsx';
7
8
 
9
+ function declaredEntry(slot, relative) {
10
+ return (slot.entries ?? []).some(entry => braceVariants(entry).some(pattern => globExpression(pattern).test(relative)));
11
+ }
12
+
8
13
  function unownedPathProblem(from, to) {
9
14
  for (const side of [from, to]) {
10
15
  if (side.status === 'no-slot') return { allowed: false, reason: 'unowned', code: side.code, path: side.path, nearest: side.nearest };
@@ -39,12 +44,22 @@ function privateEntryProblem({ repo, byId }, to, toOwner) {
39
44
  if (toOwner && repo.kind !== RUNTIME_KIND) {
40
45
  const relative = to.path === toOwner.root ? '' : to.path.slice(toOwner.root.length + 1);
41
46
  const ownerSlot = byId.get(toOwner.slot);
42
- const entry = isEntryFile(relative) || (byId.get(to.slot).entries ?? []).includes(to.path.slice(to.root.length + 1)) || (ownerSlot.tier === 'package' && relative === 'src/index.ts') || (ownerSlot.tier === 'app' && relative === 'app.module.ts');
47
+ const entry = isEntryFile(relative) || declaredEntry(byId.get(to.slot), to.path.slice(to.root.length + 1)) || (ownerSlot.tier === 'package' && relative === 'src/index.ts') || (ownerSlot.tier === 'app' && relative === 'app.module.ts');
43
48
  if (!entry) return { allowed: false, reason: 'notPublicEntry', owner: toOwner.root, path: to.path };
44
49
  }
45
50
  return null;
46
51
  }
47
52
 
53
+ function tierImportProblem(scope, from, to, fromTier, toTier) {
54
+ const { mayImport, mayImportKinds, mayImportSlots } = scope.manifest.tiers[scope.profile][fromTier] ?? {};
55
+ if (!mayImport?.includes(toTier)) return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport: mayImport ?? [] };
56
+ const kindRestriction = mayImportKinds?.[toTier];
57
+ if (kindRestriction && (!kindRestriction.from.includes(from.kind) || !kindRestriction.to.includes(to.kind))) return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport, kindRestriction, fromKind: from.kind ?? null, toKind: to.kind ?? null };
58
+ const slotRestriction = mayImportSlots?.[toTier];
59
+ if (slotRestriction && !slotRestriction.from.includes(from.slot)) return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport, slotRestriction, fromSlot: from.slot };
60
+ return null;
61
+ }
62
+
48
63
  /**
49
64
  * Whether `fromPath` may import `toPath`: {allowed, reason, ...}. Reasons: sameOwner, untiered, crossApp,
50
65
  * tierDirection, layerOrder, notPublicEntry, allowed, slotForbidden, slotNotEnabled, slotAmbiguous, and unowned (HFS_SLOT_UNDECLARED for the path no slot owns).
@@ -65,8 +80,8 @@ export function importAllowedIn(scope, fromPath, toPath) {
65
80
  const fromOwner = ownerIn(scope, from.path);
66
81
  const toOwner = ownerIn(scope, to.path);
67
82
  if (sameOwnerUnit(from, fromOwner) === sameOwnerUnit(to, toOwner)) return { allowed: true, reason: 'sameOwner' };
68
- const { mayImport } = scope.manifest.tiers[scope.profile][fromTier] ?? {};
69
- if (!mayImport?.includes(toTier)) return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport: mayImport ?? [] };
83
+ const tierProblem = tierImportProblem(scope, from, to, fromTier, toTier);
84
+ if (tierProblem) return tierProblem;
70
85
  const layerProblem = lowerLayerProblem(scope, from, to, fromTier, toTier, fromOwner, toOwner);
71
86
  if (layerProblem) return layerProblem;
72
87
  // A runtime owner is imported file by file (crossOwner of knowledge/hfs/runtime-slots.yaml): no public entry.
@@ -5,6 +5,19 @@ import { braceVariants } from '../lib/glob.mjs';
5
5
  import { classifyIn, slotEnabled } from './slot-classify.mjs';
6
6
  import { fillVars, varsOf } from './slot-match.mjs';
7
7
 
8
+ /** A populated fe.modules instance needs its index.ts only when it contains TypeScript, including nested files. */
9
+ export function requiredFileApplies(slotId, target, files) {
10
+ if (!/^fe\.modules(?:\.|$)/u.test(slotId) || path.posix.basename(target) !== 'index.ts') return true;
11
+ const prefix = `${path.posix.dirname(target)}/`;
12
+ let populated = false;
13
+ for (const file of files) {
14
+ if (!file.startsWith(prefix)) continue;
15
+ populated = true;
16
+ if (/\.tsx?$/u.test(file)) return true;
17
+ }
18
+ return !populated;
19
+ }
20
+
8
21
  /** One `requires` entry of a slot placed under `root`: a leading / makes it repository-relative, the variables are filled from `bindings`. */
9
22
  function requiredEntry(entry, bindings, root) {
10
23
  const rooted = entry.startsWith('/');
@@ -2,7 +2,7 @@
2
2
  import { isPlainObject } from '../../engine/plain-object.mjs';
3
3
  import { triggerProblems } from './declaration-slots.mjs';
4
4
  import { litePresenceOf, slotInEdition } from './edition-slots.mjs';
5
- import { manifestKind, RUNTIME_KIND, runtimeSemanticProblems } from './manifest-shape.mjs';
5
+ import { manifestKind, RUNTIME_KIND, runtimeSemanticProblems, tierKindProblems } from './manifest-shape.mjs';
6
6
  import { APP_SCOPE, PROFILES } from './slot-manifest-shape.mjs';
7
7
 
8
8
  function checkAppRootSlot(slot, bad) {
@@ -113,6 +113,7 @@ function checkSideReads(manifest, bad, braceVariants) {
113
113
  }
114
114
 
115
115
  function checkProfileTierTargets(manifest, profile, bad) {
116
+ bad.push(...tierKindProblems(manifest, profile));
116
117
  for (const [tier, def] of Object.entries(manifest.tiers[profile])) {
117
118
  for (const target of def.mayImport) {
118
119
  if (!(target in manifest.tiers[profile])) bad.push(`tiers.${profile}.${tier}.mayImport names ${target}, which is not a ${profile} tier`);
@@ -119,6 +119,7 @@ function sideDeclaration(manifest, declaration, name, edition, providers) {
119
119
  connections: Object.freeze((s.connections ?? []).map(connectionView)),
120
120
  providers,
121
121
  reads: Object.freeze([...(s.reads ?? [])]),
122
+ ...(s.extraPackages === undefined ? {} : { extraPackages: Object.freeze(s.extraPackages.map(entry => Object.freeze({ ...entry }))) }),
122
123
  manifestVersion: manifest.version,
123
124
  });
124
125
  }
@@ -0,0 +1,34 @@
1
+ // stack-config-kit.mjs - the small validators the deploy block (stack-deploy-config.mjs) and its ingress block (stack-ingress-config.mjs)
2
+ // share: a problem collector, closed field lists, pattern checks and the environment-relative file check.
3
+ import path from 'node:path';
4
+ import { isPlainObject as plain } from '../../engine/plain-object.mjs';
5
+
6
+ const BACKSLASH = String.fromCodePoint(92);
7
+
8
+ /** Collects problems with the path they concern; `problem(at, message)` is the only way to add one. */
9
+ export function collector() {
10
+ const problems = [];
11
+ return { problems, problem: (at, message) => { problems.push({ at, message }); } };
12
+ }
13
+
14
+ /** True for a mapping; reports every key outside `allowed` as "not a field of the <label> block". */
15
+ export function closedFields(c, at, value, allowed, label = 'deploy') {
16
+ if (!plain(value)) { c.problem(at, 'must be a mapping'); return false; }
17
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) c.problem(`${at}.${key}`, `is not a field of the ${label} block`);
18
+ return true;
19
+ }
20
+
21
+ /** True when `value` is a string matching `pattern`; otherwise reports it. */
22
+ export function patternField(c, at, value, pattern) {
23
+ if (typeof value !== 'string' || !pattern.test(value)) { c.problem(at, `must match ${pattern}`); return false; }
24
+ return true;
25
+ }
26
+
27
+ /** A path inside .starcistacks/<env>/ : relative, forward slashes, no `..`, and present in the tree. */
28
+ export function treeFile(c, at, value, probe) {
29
+ if (typeof value !== 'string' || value === '' || path.posix.isAbsolute(value) || value.includes(BACKSLASH) || path.posix.normalize(value).split('/').includes('..')) {
30
+ c.problem(at, 'must be a relative path inside the environment directory');
31
+ return;
32
+ }
33
+ if (!probe.exists(value)) c.problem(at, `${value} does not exist under the environment directory`);
34
+ }