@starci/hfs 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (515) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/db/migrate.mjs +361 -0
  3. package/db/post-cutover.mjs +136 -0
  4. package/emit/db-types.mjs +10 -1
  5. package/lint/run.mjs +1 -1
  6. package/package.json +4 -1
  7. package/release/gate.mjs +298 -0
  8. package/release/index.mjs +103 -0
  9. package/release/push.mjs +110 -0
  10. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  11. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  12. package/runtime/knowledge/hfs/rules.yaml +3401 -152
  13. package/runtime/knowledge/hfs/slots.yaml +344 -62
  14. package/runtime/knowledge/patterns/be/api.yaml +32 -5
  15. package/runtime/knowledge/patterns/be/cli.yaml +14 -4
  16. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  17. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  18. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  19. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  20. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  21. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  22. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  23. package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
  24. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  25. package/runtime/knowledge/sonar-gate.yaml +1 -1
  26. package/runtime/modules/kernel/failure-codes.yaml +3113 -43
  27. package/runtime/modules/models/runtimes.yaml +1 -1
  28. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  29. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  30. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  31. package/runtime/scripts/api/git/add.mjs +5 -0
  32. package/runtime/scripts/api/git/init.mjs +7 -0
  33. package/runtime/scripts/api/git/lib.mjs +3 -0
  34. package/runtime/scripts/api/git/tag.mjs +5 -0
  35. package/runtime/scripts/api/node/lib.mjs +20 -0
  36. package/runtime/scripts/api/node/run-node.mjs +6 -0
  37. package/runtime/scripts/api/sops/lib.mjs +41 -0
  38. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  39. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  40. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  41. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  42. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  43. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  44. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  45. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  47. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  48. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  49. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  50. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  51. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  52. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  53. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  54. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  55. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  56. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  57. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  58. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  59. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  60. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  61. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  63. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/tiers.mjs +3 -3
  65. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  66. package/runtime/scripts/hfs/check-results.mjs +28 -0
  67. package/runtime/scripts/hfs/check.mjs +46 -47
  68. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  69. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  70. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  71. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  72. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  73. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  74. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  75. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  76. package/runtime/scripts/hfs/manifest-shape.mjs +67 -7
  77. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  78. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  79. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  80. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  81. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  82. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  83. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  84. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  85. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  86. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  87. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  88. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  89. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  90. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  91. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  92. package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
  93. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  94. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  95. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  96. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  97. package/runtime/scripts/hfs/rules/database.mjs +9 -0
  98. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  99. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  100. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  101. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  102. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  103. package/runtime/scripts/hfs/rules/edition.mjs +9 -8
  104. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  105. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  106. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  107. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  108. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  109. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  110. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  111. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  112. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  113. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  114. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  115. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  116. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  117. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  118. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  119. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  120. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  121. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  122. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  123. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  124. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  125. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  126. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  127. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  128. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  129. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
  130. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  131. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  132. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  133. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  134. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  135. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  136. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  137. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  138. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  139. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  140. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  141. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  142. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  143. package/runtime/scripts/hfs/secret-init.mjs +127 -19
  144. package/runtime/scripts/hfs/secret-sops.mjs +38 -6
  145. package/runtime/scripts/hfs/secret-store.mjs +56 -54
  146. package/runtime/scripts/hfs/secret.mjs +34 -5
  147. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  148. package/runtime/scripts/hfs/slots.mjs +1 -0
  149. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  150. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  151. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  152. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  153. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  154. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  155. package/runtime/scripts/lib/compose-values.mjs +11 -0
  156. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  157. package/runtime/scripts/lib/json-clone.mjs +61 -0
  158. package/runtime/scripts/lib/list.mjs +4 -0
  159. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  160. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  161. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  162. package/runtime/scripts/lib/stack-services.mjs +1 -1
  163. package/runtime/scripts/lib/swarm-stack.mjs +5 -18
  164. package/runtime/scripts/lib/trim-end.mjs +8 -0
  165. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  166. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  167. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  168. package/scaffold/add-app.mjs +77 -59
  169. package/scaffold/add-cli-lite.mjs +14 -6
  170. package/scaffold/add-compose.mjs +79 -0
  171. package/scaffold/add-database-lite.mjs +14 -0
  172. package/scaffold/add-fe-overlay.mjs +122 -0
  173. package/scaffold/add-fe-page.mjs +269 -0
  174. package/scaffold/add-inbox-lite.mjs +74 -42
  175. package/scaffold/add-table.mjs +110 -87
  176. package/scaffold/add.mjs +159 -44
  177. package/scaffold/app.mjs +124 -39
  178. package/scaffold/edition-gate.mjs +3 -3
  179. package/scaffold/lite-exports.mjs +2 -1
  180. package/scaffold/service.mjs +2 -2
  181. package/src/main.mjs +229 -116
  182. package/stack/age-install.mjs +37 -0
  183. package/stack/backup.mjs +100 -0
  184. package/stack/build-args.mjs +42 -0
  185. package/stack/build.mjs +173 -0
  186. package/stack/cli.mjs +130 -0
  187. package/stack/config.mjs +57 -0
  188. package/stack/context.mjs +57 -0
  189. package/stack/converge.mjs +107 -0
  190. package/stack/deploy.mjs +301 -0
  191. package/stack/digest.mjs +16 -0
  192. package/stack/docker-transport.mjs +53 -0
  193. package/stack/edge-alias.mjs +118 -0
  194. package/stack/errors.mjs +21 -0
  195. package/stack/image-scan.mjs +152 -0
  196. package/stack/ingress-create.mjs +114 -0
  197. package/stack/ingress-publish.mjs +273 -0
  198. package/stack/ingress.mjs +706 -0
  199. package/stack/jobs.mjs +51 -0
  200. package/stack/live-restore.mjs +177 -0
  201. package/stack/prepare.mjs +266 -0
  202. package/stack/private-file.mjs +22 -0
  203. package/stack/provision.mjs +90 -0
  204. package/stack/prune.mjs +46 -0
  205. package/stack/release-store.mjs +87 -0
  206. package/stack/remote.mjs +205 -0
  207. package/stack/render.mjs +149 -0
  208. package/stack/retire.mjs +47 -0
  209. package/stack/sealed.mjs +149 -0
  210. package/stack/task.mjs +42 -0
  211. package/stack/verify.mjs +99 -0
  212. package/sync/ci-commands.mjs +13 -0
  213. package/sync/index.mjs +108 -52
  214. package/sync/stack-images.mjs +127 -0
  215. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  216. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  217. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  218. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  219. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  220. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  221. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  222. package/templates/app/codegen-graphql.mjs.partial +13 -2
  223. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  224. package/templates/app/codegen-prelude.mjs.partial +8 -0
  225. package/templates/app/gitignore +3 -0
  226. package/templates/app/gitignore-lite +3 -0
  227. package/templates/app/package-scripts/package.json +1 -0
  228. package/templates/app/package-scripts/supabase.json +7 -0
  229. package/templates/app/package-scripts-lite/package.json +1 -7
  230. package/templates/app/skeleton/.editorconfig +1 -0
  231. package/templates/app/skeleton/.gitattributes +7 -1
  232. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  233. package/templates/app/skeleton/.nvmrc +1 -1
  234. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  235. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  236. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  237. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  238. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  239. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  240. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  241. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  242. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  243. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  244. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  245. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  246. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  247. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  248. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  249. package/templates/app/skeleton/README.md +6 -6
  250. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  251. package/templates/app/skeleton-lite/.editorconfig +16 -0
  252. package/templates/app/skeleton-lite/.gitattributes +8 -0
  253. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  254. package/templates/app/skeleton-lite/.nvmrc +1 -0
  255. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  256. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
  257. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  258. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  259. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  260. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  261. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  262. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  263. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  264. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  265. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  266. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  267. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  268. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  269. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  270. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  271. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  272. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  273. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  274. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  275. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  276. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  277. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  278. package/templates/app/skeleton-lite/README.md +5 -3
  279. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  280. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  281. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  282. package/templates/be/image/worker/Dockerfile +1 -1
  283. package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
  284. package/templates/be/inbox/database-client.ts.tpl +12 -11
  285. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  286. package/templates/be/inbox/event-index.ts.tpl +1 -0
  287. package/templates/be/inbox/events.json.tpl +10 -0
  288. package/templates/be/inbox/migration.sql.tpl +1 -0
  289. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  290. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  291. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  292. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  293. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  294. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  295. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  296. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  297. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  298. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  299. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  300. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  301. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  302. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  303. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  304. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  305. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  306. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  307. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  308. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  309. package/templates/be/inbox/service.spec.ts.tpl +42 -9
  310. package/templates/be/inbox/service.ts.tpl +23 -8
  311. package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
  312. package/templates/be/inbox/webhook-door.ts.tpl +2 -2
  313. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  314. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  315. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  316. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  317. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  318. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  319. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  320. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  321. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  323. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  324. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  325. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  326. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  327. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  328. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  329. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  330. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  331. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  332. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  333. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  334. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  335. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  336. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  337. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  338. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  339. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  340. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  341. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  342. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  343. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  344. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  345. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  346. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  347. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  348. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  349. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  350. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  351. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  352. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  353. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  354. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  355. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  356. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  357. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  358. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  359. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  360. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  362. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  363. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  364. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  365. package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
  366. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  367. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  368. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
  369. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  370. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  371. package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
  372. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  373. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
  374. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  375. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  376. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  377. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  378. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  379. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  380. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  381. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  382. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  383. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  384. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  385. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  386. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  387. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  388. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  389. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  390. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  391. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  392. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  393. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  394. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  395. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  396. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  397. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  398. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  399. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  400. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  401. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  402. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  403. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  404. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  405. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  406. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  407. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  408. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  409. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  410. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  411. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  412. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  413. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  414. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  415. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  416. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  417. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  418. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  419. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  420. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  421. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  422. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  423. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  424. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  425. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  426. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  427. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  428. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  429. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  430. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  431. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  432. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  433. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  434. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  435. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  436. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  437. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  438. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  439. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  440. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  441. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  442. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  443. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  444. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  445. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  446. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
  447. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  448. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  449. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  450. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
  451. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  452. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  453. package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
  454. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  455. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  456. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  457. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  458. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  459. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  460. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  461. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  462. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  463. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  464. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  465. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  466. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  467. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  468. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  469. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  470. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  471. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  472. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  473. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  474. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  475. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  476. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  477. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  478. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  479. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  480. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  481. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  482. package/templates/be/table/migration.sql.tpl +1 -0
  483. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  484. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  485. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  486. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  487. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  488. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  489. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  490. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  491. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  492. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  493. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  494. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  495. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  496. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  497. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  498. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  499. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  500. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  501. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  502. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  503. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  504. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  505. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  506. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  507. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  508. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  509. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  510. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  511. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  512. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  513. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  514. package/upgrade/check.mjs +3 -1
  515. package/upgrade/index.mjs +53 -40
@@ -0,0 +1,200 @@
1
+ import {
2
+ createCipheriv, createHash, createHmac, createPrivateKey, createPublicKey,
3
+ diffieHellman, hkdfSync, randomBytes,
4
+ } from 'node:crypto';
5
+
6
+ // Bootstrap only; interoperable with https://c2sp.org/age and getsops/sops.
7
+ // See SOPS aes/cipher.go, sops.go, stores/dotenv/store.go, stores/flatten.go.
8
+ const ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
9
+ const GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
10
+ const PRIVATE_PREFIX = Buffer.from('302e020100300506032b656e04220420', 'hex');
11
+ const PUBLIC_PREFIX = Buffer.from('302a300506032b656e032100', 'hex');
12
+
13
+ function checksumStep(checksum, value) {
14
+ const top = checksum >>> 25;
15
+ let next = ((checksum & 0x1ffffff) << 5) ^ value;
16
+ for (let bit = 0; bit < 5; bit += 1) {
17
+ if ((top >>> bit) & 1) next ^= GENERATORS[bit];
18
+ }
19
+ return next >>> 0;
20
+ }
21
+
22
+ function checksumWords(prefix, words) {
23
+ const codes = [...prefix].map((character) => character.codePointAt(0));
24
+ const expanded = [...codes.map((code) => code >>> 5), 0, ...codes.map((code) => code & 31)];
25
+ return [...expanded, ...words].reduce(checksumStep, 1);
26
+ }
27
+
28
+ function convertBits(input, from, to, pad) {
29
+ let accumulator = 0;
30
+ let bits = 0;
31
+ const result = [];
32
+ const mask = (1 << to) - 1;
33
+ for (const value of input) {
34
+ accumulator = ((accumulator << from) | value) & 0xffff;
35
+ bits += from;
36
+ while (bits >= to) {
37
+ bits -= to;
38
+ result.push((accumulator >>> bits) & mask);
39
+ }
40
+ }
41
+ if (pad && bits) result.push((accumulator << (to - bits)) & mask);
42
+ if (!pad && (bits >= from || ((accumulator << (to - bits)) & mask))) {
43
+ throw new Error('Invalid age recipient encoding');
44
+ }
45
+ return result;
46
+ }
47
+
48
+ function bech32Encode(prefix, bytes) {
49
+ const words = convertBits(bytes, 8, 5, true);
50
+ const checksum = checksumWords(prefix, [...words, 0, 0, 0, 0, 0, 0]) ^ 1;
51
+ const suffix = [];
52
+ for (let index = 0; index < 6; index += 1) suffix.push((checksum >>> (5 * (5 - index))) & 31);
53
+ return prefix + '1' + [...words, ...suffix].map((word) => ALPHABET[word]).join('');
54
+ }
55
+
56
+ function decodeRecipient(recipient) {
57
+ if (typeof recipient !== 'string' || !recipient.startsWith('age1') || recipient.length !== 62) {
58
+ throw new Error('Bootstrap requires an X25519 age recipient');
59
+ }
60
+ const words = Array.from(recipient.slice(4), (character) => ALPHABET.indexOf(character));
61
+ if (words.includes(-1) || checksumWords('age', words) !== 1) {
62
+ throw new Error('Invalid age recipient checksum');
63
+ }
64
+ const decoded = Buffer.from(convertBits(words.slice(0, -6), 5, 8, false));
65
+ if (decoded.length !== 32) throw new Error('Invalid age recipient length');
66
+ return decoded;
67
+ }
68
+
69
+ function privateKey(bytes) {
70
+ const encoded = Buffer.concat([PRIVATE_PREFIX, bytes]);
71
+ try {
72
+ return createPrivateKey({ key: encoded, type: 'pkcs8', format: 'der' });
73
+ } finally {
74
+ encoded.fill(0);
75
+ }
76
+ }
77
+
78
+ function publicBytes(key) {
79
+ return createPublicKey(key).export({ type: 'spki', format: 'der' }).subarray(-32);
80
+ }
81
+
82
+ /** Return a fresh app identity in memory. The caller owns custody and zeroing. */
83
+ export function generateAppIdentity() {
84
+ const seed = randomBytes(32);
85
+ try {
86
+ const recipient = bech32Encode('age', publicBytes(privateKey(seed)));
87
+ const identity = Buffer.from(bech32Encode('age-secret-key-', seed).toUpperCase());
88
+ return { identity, recipient };
89
+ } finally {
90
+ seed.fill(0);
91
+ }
92
+ }
93
+
94
+ function deriveKey(secret, salt, info) {
95
+ return Buffer.from(hkdfSync('sha256', secret, salt, info, 32));
96
+ }
97
+
98
+ function chachaSeal(plaintext, key, nonce) {
99
+ const cipher = createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
100
+ return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
101
+ }
102
+
103
+ function rawBase64(bytes) {
104
+ return bytes.toString('base64').replaceAll('=', '');
105
+ }
106
+
107
+ function wrapFileKey(fileKey, recipient) {
108
+ const seed = randomBytes(32);
109
+ const ephemeral = privateKey(seed);
110
+ seed.fill(0);
111
+ const share = publicBytes(ephemeral);
112
+ const peer = createPublicKey({ key: Buffer.concat([PUBLIC_PREFIX, recipient]), type: 'spki', format: 'der' });
113
+ const sharedSecret = diffieHellman({ privateKey: ephemeral, publicKey: peer });
114
+ const wrapKey = deriveKey(sharedSecret, Buffer.concat([share, recipient]), 'age-encryption.org/v1/X25519');
115
+ try {
116
+ const body = rawBase64(chachaSeal(fileKey, wrapKey, Buffer.alloc(12)));
117
+ return '-> X25519 ' + rawBase64(share) + '\n' + body + '\n';
118
+ } finally {
119
+ sharedSecret.fill(0);
120
+ wrapKey.fill(0);
121
+ }
122
+ }
123
+
124
+ function ageSealDataKey(dataKey, recipient) {
125
+ const fileKey = randomBytes(16);
126
+ const nonce = randomBytes(16);
127
+ const headerKey = deriveKey(fileKey, Buffer.alloc(0), 'header');
128
+ const payloadKey = deriveKey(fileKey, nonce, 'payload');
129
+ try {
130
+ const header = 'age-encryption.org/v1\n' + wrapFileKey(fileKey, recipient) + '---';
131
+ const mac = rawBase64(createHmac('sha256', headerKey).update(header).digest());
132
+ const chunkNonce = Buffer.alloc(12);
133
+ chunkNonce[11] = 1;
134
+ const ciphertext = chachaSeal(dataKey, payloadKey, chunkNonce);
135
+ return Buffer.concat([Buffer.from(header + ' ' + mac + '\n'), nonce, ciphertext]);
136
+ } finally {
137
+ fileKey.fill(0);
138
+ headerKey.fill(0);
139
+ payloadKey.fill(0);
140
+ }
141
+ }
142
+
143
+ function armor(bytes) {
144
+ const encoded = bytes.toString('base64');
145
+ const lines = [];
146
+ for (let offset = 0; offset < encoded.length; offset += 64) lines.push(encoded.slice(offset, offset + 64));
147
+ return ['-----BEGIN AGE ENCRYPTED FILE-----', ...lines, '-----END AGE ENCRYPTED FILE-----', ''].join('\n');
148
+ }
149
+
150
+ function sopsEncrypt(value, dataKey, additionalData) {
151
+ const iv = randomBytes(32);
152
+ const cipher = createCipheriv('aes-256-gcm', dataKey, iv);
153
+ cipher.setAAD(Buffer.from(additionalData));
154
+ const data = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
155
+ return 'ENC[AES256_GCM,data:' + data.toString('base64') + ',iv:' + iv.toString('base64')
156
+ + ',tag:' + cipher.getAuthTag().toString('base64') + ',type:str]';
157
+ }
158
+
159
+ function bootstrapEntries(values) {
160
+ if (!values || typeof values !== 'object' || Array.isArray(values)) {
161
+ throw new Error('Bootstrap values must be a dotenv key/value object');
162
+ }
163
+ const entries = Object.entries(values);
164
+ if (!entries.length) throw new Error('Bootstrap requires at least one value');
165
+ for (const [name, value] of entries) {
166
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || name.startsWith('sops_') || name.endsWith('_unencrypted')) {
167
+ throw new Error('Bootstrap contains an invalid or reserved dotenv key');
168
+ }
169
+ if (typeof value !== 'string' || !value.length) throw new Error('Bootstrap values must be nonempty strings');
170
+ }
171
+ return entries;
172
+ }
173
+
174
+ /** Encrypt bootstrap values to the app's public key; no private key is needed. */
175
+ export function sealBootstrapValues({ values, recipient }) {
176
+ const entries = bootstrapEntries(values);
177
+ const recipientBytes = decodeRecipient(recipient);
178
+ const dataKey = randomBytes(32);
179
+ try {
180
+ const modified = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
181
+ const digest = createHash('sha512');
182
+ const lines = entries.map(([name, value]) => {
183
+ digest.update(value, 'utf8');
184
+ return name + '=' + sopsEncrypt(value, dataKey, name + ':');
185
+ });
186
+ const mac = sopsEncrypt(digest.digest('hex').toUpperCase(), dataKey, modified);
187
+ const wrapped = armor(ageSealDataKey(dataKey, recipientBytes)).replaceAll('\n', String.raw`\n`);
188
+ lines.push(
189
+ 'sops_age__list_0__map_enc=' + wrapped,
190
+ 'sops_age__list_0__map_recipient=' + recipient,
191
+ 'sops_lastmodified=' + modified,
192
+ 'sops_mac=' + mac,
193
+ 'sops_unencrypted_suffix=_unencrypted',
194
+ 'sops_version=3.9.4',
195
+ );
196
+ return lines.join('\n') + '\n';
197
+ } finally {
198
+ dataKey.fill(0);
199
+ }
200
+ }
@@ -0,0 +1,170 @@
1
+ // secret-edit.mjs - app secret unset|rename|remove: edit the key set of one sealed member without ever printing a value.
2
+ // Each verb is a dry run until --execute. The dry run reads only the envelope's public key names and recipients, so it needs no
3
+ // age identity. An executed edit decrypts the member in memory, re-seals it to the SAME recipients in the SAME sops format
4
+ // (the one exception is a binary `data` envelope, which a rename re-seals as a JSON envelope), checks the sealed result and
5
+ // publishes it atomically under the writer claim. Output names environments, members and keys only.
6
+ import fs from "node:fs";
7
+ import { isSopsEnvelope } from "../lib/sops-envelope.mjs";
8
+ import { envelopeOf, plaintextOf, validateKey, validateMap } from "./secret-document.mjs";
9
+ import { deleteSealed, fileOf, replaceSealed } from "./secret-store.mjs";
10
+ import { SecretError } from "./secret-errors.mjs";
11
+
12
+ const BINARY_PROPERTY = "data";
13
+ const NOT_PRESERVED = /\r|\\n/u;
14
+
15
+ const sameNames = (left, right) =>
16
+ left.length === right.length && left.every((name) => right.includes(name));
17
+
18
+ /** True for the sops binary store: a JSON envelope whose only property is `data` (the ssh key and known_hosts members). */
19
+ const isBinaryEnvelope = (envelope) =>
20
+ envelope.format === "json" &&
21
+ envelope.keys.length === 1 &&
22
+ envelope.keys[0] === BINARY_PROPERTY;
23
+
24
+ function inspectMember(file, label) {
25
+ let stat;
26
+ try {
27
+ stat = fs.lstatSync(file);
28
+ } catch {
29
+ throw new SecretError(`${label} is not a sealed member of this environment`);
30
+ }
31
+ if (!stat.isFile() || stat.isSymbolicLink())
32
+ throw new SecretError(`${label} must be an ordinary sealed file`);
33
+ const text = fs.readFileSync(file, "utf8");
34
+ if (!isSopsEnvelope(text))
35
+ throw new SecretError(`${label} is not a sops envelope`);
36
+ return envelopeOf(text);
37
+ }
38
+
39
+ function requireKey(envelope, key, label) {
40
+ if (!envelope.keys.includes(key))
41
+ throw new SecretError(`${label} has no KEY ${key}`);
42
+ }
43
+
44
+ /** Decrypt the member in memory and check that it holds exactly the keys its envelope declares. */
45
+ function openMap(file, envelope, sops) {
46
+ const map = sops.decrypt(file, envelope.format);
47
+ validateMap(map);
48
+ if (!sameNames(Object.keys(map), envelope.keys))
49
+ throw new SecretError("sops returned keys that differ from the envelope");
50
+ if (
51
+ envelope.format !== "dotenv" &&
52
+ Object.values(map).some((value) => NOT_PRESERVED.test(value))
53
+ )
54
+ throw new SecretError(
55
+ "a value holds a carriage return or literal backslash-n that this edit cannot preserve losslessly",
56
+ );
57
+ return map;
58
+ }
59
+
60
+ /** Seal the replacement map to the original recipients and prove the result before anything is published. */
61
+ function sealReplacement({ file, format, map, envelope, sops }) {
62
+ const sealed = sops.seal({
63
+ inputType: format,
64
+ plaintext: plaintextOf(format, map),
65
+ recipients: envelope.recipients,
66
+ filenameOverride: file,
67
+ });
68
+ let parsed;
69
+ try {
70
+ parsed = envelopeOf(sealed);
71
+ } catch {
72
+ throw new SecretError("sops returned an invalid sealed document");
73
+ }
74
+ const recipientsKept =
75
+ !envelope.recipients.length ||
76
+ sameNames(parsed.recipients, envelope.recipients);
77
+ if (
78
+ !isSopsEnvelope(sealed) ||
79
+ parsed.format !== format ||
80
+ !sameNames(parsed.keys, Object.keys(map))
81
+ )
82
+ throw new SecretError("sops returned an invalid sealed document");
83
+ if (!recipientsKept)
84
+ throw new SecretError("sops returned a document sealed to different recipients");
85
+ return sealed;
86
+ }
87
+
88
+ const keyCount = (count) => `${count} key${count === 1 ? "" : "s"}`;
89
+
90
+ function unsetKey({ file, label, key, execute, sops, stdout }) {
91
+ const envelope = inspectMember(file, label);
92
+ requireKey(envelope, key, label);
93
+ if (envelope.keys.length === 1)
94
+ throw new SecretError(
95
+ `${key} is the last KEY of ${label}; a sealed member cannot be empty, use remove ${label.replace("/", " ")} --force-nonempty to delete it`,
96
+ );
97
+ const remaining = envelope.keys.length - 1;
98
+ if (!execute) {
99
+ stdout(`dry run: would unset ${key} from ${label} (${keyCount(remaining)} remain); add --execute\n`);
100
+ return 0;
101
+ }
102
+ const seam = sops();
103
+ const map = openMap(file, envelope, seam);
104
+ delete map[key];
105
+ replaceSealed(file, sealReplacement({ file, format: envelope.format, map, envelope, sops: seam }));
106
+ stdout(`unset ${label} (${key}); ${keyCount(remaining)} remain\n`);
107
+ return 0;
108
+ }
109
+
110
+ /** The replacement map and format of a rename: the key keeps its position and value; a binary envelope becomes a JSON one. */
111
+ function renamedMap({ file, envelope, from, to, sops }) {
112
+ if (isBinaryEnvelope(envelope)) {
113
+ const value = sops.decryptBinary(file);
114
+ if (typeof value !== "string" || value === "")
115
+ throw new SecretError("the binary envelope holds no content");
116
+ return { format: "json", map: { [to]: value } };
117
+ }
118
+ const map = openMap(file, envelope, sops);
119
+ const entries = Object.entries(map).map(([name, value]) => [name === from ? to : name, value]);
120
+ return { format: envelope.format, map: Object.fromEntries(entries) };
121
+ }
122
+
123
+ function renameKey({ file, label, key, to, execute, sops, stdout }) {
124
+ const envelope = inspectMember(file, label);
125
+ requireKey(envelope, key, label);
126
+ if (key === to) throw new SecretError("the new KEY equals the old KEY");
127
+ if (envelope.keys.includes(to))
128
+ throw new SecretError(`${label} already has KEY ${to}`);
129
+ const binary = isBinaryEnvelope(envelope);
130
+ if (!execute) {
131
+ const note = binary ? "; the binary envelope is re-sealed as a JSON envelope" : "";
132
+ stdout(`dry run: would rename ${key} to ${to} in ${label}${note}; add --execute\n`);
133
+ return 0;
134
+ }
135
+ const seam = sops();
136
+ const { format, map } = renamedMap({ file, envelope, from: key, to, sops: seam });
137
+ replaceSealed(file, sealReplacement({ file, format, map, envelope, sops: seam }));
138
+ stdout(`renamed ${key} to ${to} in ${label}${binary ? " (re-sealed as a JSON envelope)" : ""}\n`);
139
+ return 0;
140
+ }
141
+
142
+ function removeMember({ file, label, execute, force, stdout }) {
143
+ const envelope = inspectMember(file, label);
144
+ if (envelope.keys.length && !force)
145
+ throw new SecretError(
146
+ `${label} still holds ${keyCount(envelope.keys.length)} (${envelope.keys.join(", ")}); unset them first, or pass --force-nonempty to delete the whole member`,
147
+ );
148
+ if (!execute) {
149
+ stdout(`dry run: would remove ${label}.enc (${keyCount(envelope.keys.length)}); add --execute\n`);
150
+ return 0;
151
+ }
152
+ deleteSealed(file);
153
+ stdout(`removed ${label}.enc (${keyCount(envelope.keys.length)})\n`);
154
+ return 0;
155
+ }
156
+
157
+ /** Validate every name of an edit verb before anything touches the file system. */
158
+ export function validateEditNames(verb, names) {
159
+ for (const name of names) validateKey(name);
160
+ if (verb === "remove" && names.length) throw new SecretError("remove takes no KEY");
161
+ }
162
+
163
+ /** unset, rename and remove; `sops` is a thunk so a dry run never needs the sops tool or an age identity. */
164
+ export function editSecret(verb, { directory, environment, slug, names, execute, force, sops, stdout }) {
165
+ const file = fileOf(directory, slug);
166
+ const common = { file, label: `${environment}/${slug}`, execute, sops, stdout };
167
+ if (verb === "unset") return unsetKey({ ...common, key: names[0] });
168
+ if (verb === "rename") return renameKey({ ...common, key: names[0], to: names[1] });
169
+ return removeMember({ ...common, force });
170
+ }
@@ -2,10 +2,15 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { parseYaml, stringifyYaml } from "../../engine/yaml.mjs";
5
+ import { secretEnv } from "../../engine/secrets.mjs";
5
6
  import { resolveSops } from "../api/sops/resolve-sops.mjs";
7
+ import { selectedAgeRecipient } from "../api/sops/selected-age-recipient.mjs";
6
8
  import { resolveRealTool } from "../api/process/resolve-real-tool.mjs";
7
9
  import { runProgram } from "../api/process/run-program.mjs";
8
10
  import { claimFile } from "../api/fs/claim-file.mjs";
11
+ import { generateAppIdentity } from "./secret-bootstrap-crypto.mjs";
12
+ import { materializeDeclaredPlaceholders } from "./sealed-members.mjs";
13
+ import { defaultSops } from "./secret-sops.mjs";
9
14
  import {
10
15
  outsideRepository,
11
16
  privateDirectory,
@@ -13,6 +18,68 @@ import {
13
18
  } from "./secret-files.mjs";
14
19
 
15
20
  const PATH_RULE = String.raw`\.starcistacks[/\\][^/\\]+[/\\]secrets[/\\][^/\\]+\.enc$`;
21
+ const AGE_IDENTITY_NAME = /^SOPS_AGE_KEY(?:_FILE)?$/i;
22
+
23
+ /** Bootstrap a new app with its own age identity; only its public recipient leaves this owner. */
24
+
25
+
26
+ /** Bootstrap a new app with its own age identity; only the public recipient leaves this owner. */
27
+ export function initializeAppIdentity(repoRoot, { write, tools } = {}) {
28
+ const privateDirectory = path.join(repoRoot, ".claude");
29
+ const privateFile = path.join(privateDirectory, "secret.env");
30
+ const directoryStat = fs.lstatSync(privateDirectory, { throwIfNoEntry: false });
31
+ if (directoryStat && (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()))
32
+ throw new Error("the app private identity directory must be an ordinary directory");
33
+ const privateStat = fs.lstatSync(privateFile, { throwIfNoEntry: false });
34
+ if (privateStat && (!privateStat.isFile() || privateStat.isSymbolicLink() || privateStat.nlink !== 1))
35
+ throw new Error("the app private identity must be an ordinary unlinked file");
36
+ let local;
37
+ try { local = privateStat ? (tools?.secretEnv ?? secretEnv)(privateDirectory, {}) : {}; }
38
+ catch { throw new Error("the app private environment could not be loaded safely"); }
39
+ if (Object.keys(local ?? {}).some((name) => AGE_IDENTITY_NAME.test(name)))
40
+ throw new Error("the app already has a private age identity; initialization must not rotate it");
41
+ const config = readInitializationConfig(path.join(repoRoot, ".sops.yaml"));
42
+ if ((config.creation_rules ?? []).some((rule) => rule?.age))
43
+ throw new Error("the app already has an age recipient; initialization must not rotate it");
44
+ const publish = write ?? ((file, content) => {
45
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
46
+ const append = file === privateFile || file === path.join(privateDirectory, ".gitignore");
47
+ fs.writeFileSync(file, content, { mode: 0o600, flag: append ? "a" : "w" });
48
+ });
49
+ const generated = generateAppIdentity();
50
+ let privateContent;
51
+ try {
52
+ const prefix = privateStat ? "\nSOPS_AGE_KEY=" : "SOPS_AGE_KEY=";
53
+ privateContent = Buffer.concat([Buffer.from(prefix), generated.identity, Buffer.from("\n")]);
54
+ publish(path.join(privateDirectory, ".gitignore"), "\nsecret.env\n");
55
+ publish(privateFile, privateContent);
56
+ if (process.platform !== "win32") fs.chmodSync(privateFile, 0o600);
57
+ config.creation_rules = [{ path_regex: PATH_RULE, age: generated.recipient }];
58
+ publish(path.join(repoRoot, ".sops.yaml"), stringifyYaml(config));
59
+ return { recipient: generated.recipient, files: [".claude/.gitignore", ".claude/secret.env", ".sops.yaml"] };
60
+ } finally {
61
+ privateContent?.fill(0);
62
+ generated.identity.fill(0);
63
+ }
64
+ }
65
+
66
+ /** Load host-local age selections below the explicit environment, without copying any other credentials. */
67
+ export function ageIdentityEnv(repoRoot, env, tools) {
68
+ const load = tools?.secretEnv ?? secretEnv;
69
+ let resolved = {};
70
+ try {
71
+ resolved = load(path.join(repoRoot, ".claude"), env) ?? {};
72
+ } catch {
73
+ return { ...env };
74
+ }
75
+ const own = new Set(Object.keys(env).map((name) => name.toUpperCase()));
76
+ const local = Object.fromEntries(
77
+ Object.entries(resolved).filter(
78
+ ([name]) => AGE_IDENTITY_NAME.test(name) && !own.has(name.toUpperCase()),
79
+ ),
80
+ );
81
+ return { ...env, ...local };
82
+ }
16
83
 
17
84
  /** Select the first matching public age rule, including for SOPS's isolated CI identity path. */
18
85
  export function configuredRecipients(repoRoot, file) {
@@ -93,19 +160,14 @@ function readInitializationConfig(configFile) {
93
160
  return config;
94
161
  }
95
162
 
96
- /** Reuse an existing matching age rule; initialization must not rotate an app's recipient. */
97
- export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
98
- const tool = secretTools(env, tools);
99
- const configFile = path.join(repoRoot, ".sops.yaml");
100
- const config = readInitializationConfig(configFile);
101
- const rules = config.creation_rules ?? [];
163
+ function existingInitializationRule(rules) {
102
164
  const canonical = rules.find(
103
165
  (rule) =>
104
166
  rule?.path_regex === PATH_RULE &&
105
167
  typeof rule.age === "string" &&
106
168
  rule.age.trim(),
107
169
  );
108
- const existing =
170
+ return (
109
171
  canonical ??
110
172
  rules.find((rule) => {
111
173
  if (typeof rule?.age !== "string" || !rule.age.trim()) return false;
@@ -119,15 +181,11 @@ export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
119
181
  } catch {
120
182
  return false;
121
183
  }
122
- });
123
- if (
124
- existing?.age
125
- .split(",")
126
- .some((value) => !/^age1[a-z0-9]+$/.test(value.trim()))
127
- )
128
- throw new Error(
129
- ".sops.yaml must name valid public age recipients; initialization never prints private key material",
130
- );
184
+ })
185
+ );
186
+ }
187
+
188
+ function legacyInitializationRecipient(repoRoot, env, platform, tool) {
131
189
  const identity = secretRoots(env, platform).identity;
132
190
  outsideRepository(repoRoot, identity);
133
191
  privateDirectory(repoRoot, path.dirname(identity));
@@ -136,7 +194,6 @@ export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
136
194
  throw new Error(
137
195
  "the age identity is being initialized; retry after that operation settles",
138
196
  );
139
- let recipient;
140
197
  try {
141
198
  if (!fs.existsSync(identity)) {
142
199
  const generated = tool.run(tool.keygen, ["-o", identity], {
@@ -170,10 +227,55 @@ export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
170
227
  recipients.some((value) => !/^age1[a-z0-9]+$/.test(value))
171
228
  )
172
229
  throw new Error("age-keygen returned an invalid public recipient");
173
- recipient = existing?.age ?? recipients.join(",");
230
+ return recipients.join(",");
174
231
  } finally {
175
232
  held.release();
176
233
  }
234
+ }
235
+
236
+ function initializationRecipient(repoRoot, env, platform, tool, tools) {
237
+ const selectedEnv = ageIdentityEnv(repoRoot, env, tools);
238
+ if (!Object.keys(selectedEnv).some((name) => /^SOPS_AGE_KEY$/i.test(name)))
239
+ return legacyInitializationRecipient(repoRoot, env, platform, tool);
240
+ const result = selectedAgeRecipient({
241
+ env: selectedEnv,
242
+ cwd: repoRoot,
243
+ invocation: {
244
+ runProgram: tool.run,
245
+ resolveRealTool: tools?.resolveRealTool ?? resolveRealTool,
246
+ },
247
+ });
248
+ if (!result.ok)
249
+ throw new Error(`SOPS_AGE_KEY public recipient could not be derived (${result.reason})`);
250
+ return result.publicRecipient;
251
+ }
252
+
253
+ /** Reuse an existing matching age rule; initialization must not rotate an app's recipient. */
254
+ export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
255
+ const configFile = path.join(repoRoot, ".sops.yaml");
256
+ const config = readInitializationConfig(configFile);
257
+ const rules = config.creation_rules ?? [];
258
+ const existing = existingInitializationRule(rules);
259
+ if (
260
+ existing?.age
261
+ .split(",")
262
+ .some((value) => !/^age1[a-z0-9]+$/.test(value.trim()))
263
+ )
264
+ throw new Error(
265
+ ".sops.yaml must name valid public age recipients; initialization never prints private key material",
266
+ );
267
+ const localEnv = ageIdentityEnv(repoRoot, {}, tools);
268
+ const selectedEnv = ageIdentityEnv(repoRoot, existing ? env : { ...env, ...localEnv }, tools);
269
+ if (!existing && !Object.keys(localEnv).some((name) => /^SOPS_AGE_KEY$/i.test(name))) {
270
+ const initialized = initializeAppIdentity(repoRoot, { tools });
271
+ materializeDeclaredPlaceholders(repoRoot, { recipient: initialized.recipient, processEnv: selectedEnv });
272
+ stdout('secrets initialized; public recipient: ' + initialized.recipient + '\n');
273
+ return 0;
274
+ }
275
+ const tool = secretTools(env, tools);
276
+ const inline = Object.keys(selectedEnv).some((name) => /^SOPS_AGE_KEY$/i.test(name));
277
+ const derived = existing && !inline ? existing.age : initializationRecipient(repoRoot, selectedEnv, platform, tool, tools);
278
+ const recipient = existing?.age ?? derived;
177
279
  const exact = rules.findIndex((rule) => rule?.path_regex === PATH_RULE);
178
280
  if (exact >= 0) rules[exact] = { ...rules[exact], age: recipient };
179
281
  // SOPS uses the first matching rule: keep any existing environment-specific recipients in force.
@@ -181,8 +283,14 @@ export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
181
283
  config.creation_rules = rules;
182
284
  fs.writeFileSync(
183
285
  configFile,
184
- `# Initialized by starci app secret init; private age identities stay outside the repo.\n${stringifyYaml(config)}`,
286
+ `# Initialized by starci app secret init; private age identities stay outside Git.\n${stringifyYaml(config)}`,
185
287
  );
288
+ materializeDeclaredPlaceholders(repoRoot, {
289
+ recipient,
290
+ recipientsForFile: (file) => configuredRecipients(repoRoot, file),
291
+ processEnv: selectedEnv,
292
+ sops: { seal: (request) => defaultSops(selectedEnv, repoRoot, platform, tools).seal(request) },
293
+ });
186
294
  stdout(`secrets initialized; public recipient: ${recipient}\n`);
187
295
  if (existing)
188
296
  stdout(
@@ -6,11 +6,13 @@ import { resolveRealTool } from "../api/process/resolve-real-tool.mjs";
6
6
  import { decrypt as sopsDecrypt } from "../api/sops/decrypt.mjs";
7
7
  import { seal as sopsSeal } from "../api/sops/seal.mjs";
8
8
  import { invocationDir } from "../lib/roots.mjs";
9
- import { configuredRecipients, secretTools } from "./secret-init.mjs";
9
+ import { envelopeOf } from "./secret-document.mjs";
10
+ import { ageIdentityEnv, configuredRecipients, secretTools } from "./secret-init.mjs";
10
11
  import { outsideRepository, secretRoots } from "./secret-files.mjs";
11
12
  import { SecretError, publicStorage } from "./secret-errors.mjs";
12
13
 
13
14
  const sopsInvocation = Object.freeze({ runProgram, resolveRealTool });
15
+ const AGE_IDENTITY_NAME = /^SOPS_AGE_KEY(?:_FILE)?$/i;
14
16
 
15
17
  /** The default sops seam: the runtime's sops api, which finds the binary on this machine. */
16
18
  export function defaultSops(
@@ -20,7 +22,11 @@ export function defaultSops(
20
22
  tools,
21
23
  ) {
22
24
  const { sops: binary } = publicStorage(() => secretTools(env, tools));
23
- const selectedEnv = { ...env };
25
+ const selectedEnv = ageIdentityEnv(repoRoot, env, tools);
26
+ const invocation = {
27
+ runProgram: tools?.runProgram ?? sopsInvocation.runProgram,
28
+ resolveRealTool: tools?.resolveRealTool ?? sopsInvocation.resolveRealTool,
29
+ };
24
30
  const checkIdentity = (file) =>
25
31
  publicStorage(() => {
26
32
  outsideRepository(repoRoot, file);
@@ -32,7 +38,7 @@ export function defaultSops(
32
38
  );
33
39
  }
34
40
  });
35
- for (const [name, value] of Object.entries(env))
41
+ for (const [name, value] of Object.entries(selectedEnv))
36
42
  if (/^SOPS_AGE_KEY_FILE$/i.test(name) && value) {
37
43
  if (!path.isAbsolute(value))
38
44
  throw new SecretError(
@@ -41,7 +47,7 @@ export function defaultSops(
41
47
  checkIdentity(value);
42
48
  }
43
49
  if (
44
- !Object.keys(env).some((name) => /^SOPS_AGE_KEY(?:_FILE)?$/i.test(name))
50
+ !Object.keys(selectedEnv).some((name) => AGE_IDENTITY_NAME.test(name))
45
51
  ) {
46
52
  const identity = secretRoots(env, platform).identity;
47
53
  checkIdentity(identity);
@@ -63,7 +69,7 @@ export function defaultSops(
63
69
  const result = sopsDecrypt(
64
70
  binary,
65
71
  ["decrypt", "--input-type", format, "--output-type", "dotenv", file],
66
- { env: selectedEnv, cwd: repoRoot, invocation: sopsInvocation },
72
+ { env: selectedEnv, cwd: repoRoot, invocation },
67
73
  );
68
74
  if (result.status !== 0)
69
75
  throw refuse(
@@ -80,6 +86,32 @@ export function defaultSops(
80
86
  }
81
87
  return map;
82
88
  },
89
+ /**
90
+ * The ssh key and known_hosts of the deploy custody. A classic sops BINARY envelope (one `data` property) yields the file itself;
91
+ * a JSON envelope under another property name (privateKey, knownHosts, ciPrivateKey) yields the JSON text of its properties,
92
+ * which the stack remote code unwraps by name.
93
+ */
94
+ decryptBinary(file) {
95
+ const envelope = envelopeOf(fs.readFileSync(file, "utf8"));
96
+ const classic =
97
+ envelope.format === "json" &&
98
+ envelope.keys.length === 1 &&
99
+ envelope.keys[0] === "data";
100
+ const types = classic
101
+ ? ["binary", "binary"]
102
+ : [envelope.format, "json"];
103
+ const result = sopsDecrypt(
104
+ binary,
105
+ ["decrypt", "--input-type", types[0], "--output-type", types[1], file],
106
+ { env: selectedEnv, cwd: repoRoot, invocation },
107
+ );
108
+ if (result.status !== 0)
109
+ throw refuse(
110
+ `decrypt ${path.basename(file)}; is the age identity installed?`,
111
+ result,
112
+ );
113
+ return String(result.stdout);
114
+ },
83
115
  seal(request) {
84
116
  const recipients = request.recipients.length
85
117
  ? request.recipients
@@ -89,7 +121,7 @@ export function defaultSops(
89
121
  const result = sopsSeal(
90
122
  binary,
91
123
  { ...request, recipients },
92
- { env: selectedEnv, cwd: repoRoot, invocation: sopsInvocation },
124
+ { env: selectedEnv, cwd: repoRoot, invocation },
93
125
  );
94
126
  if (result.status !== 0) throw refuse("seal the secret", result);
95
127
  return result.stdout;