@starci/hfs 5.1.0 → 5.3.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 (365) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +11 -2
  3. package/cache/hfs-family.d.mts +2 -3
  4. package/cache/hfs-family.mjs +4 -9
  5. package/cache/secret-records.d.mts +7 -0
  6. package/cache/secret-records.mjs +12 -0
  7. package/cache/session.mjs +4 -2
  8. package/codemod/account-doors.d.mts +36 -0
  9. package/codemod/account-doors.mjs +128 -0
  10. package/codemod/main.mjs +31 -1
  11. package/codemod/module-definition.d.mts +52 -0
  12. package/codemod/module-definition.mjs +242 -0
  13. package/codemod/quality-binding.d.mts +33 -0
  14. package/codemod/quality-binding.mjs +38 -0
  15. package/codemod/secrets-cache.d.mts +35 -0
  16. package/codemod/secrets-cache.mjs +70 -0
  17. package/codemod/sops-rules.d.mts +34 -0
  18. package/codemod/sops-rules.mjs +69 -0
  19. package/codemod/theme-tokens.mjs +48 -2
  20. package/db/postgres.mjs +9 -1
  21. package/lint/run.d.mts +6 -0
  22. package/lint/run.mjs +63 -10
  23. package/package.json +1 -1
  24. package/runtime/knowledge/hfs/canon-pins.yaml +65 -11
  25. package/runtime/knowledge/hfs/fe-stack.yaml +9 -9
  26. package/runtime/knowledge/hfs/fork-images.yaml +7 -0
  27. package/runtime/knowledge/hfs/rules.yaml +67 -15
  28. package/runtime/knowledge/hfs/slots.yaml +174 -30
  29. package/runtime/modules/kernel/failure-codes.yaml +67 -4
  30. package/runtime/modules/schemas/application-stacks.schema.yaml +6 -0
  31. package/runtime/scripts/hfs/architecture/config.d.mts +4 -0
  32. package/runtime/scripts/hfs/architecture/config.mjs +25 -1
  33. package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
  34. package/runtime/scripts/hfs/architecture/module-per-transport.d.mts +1 -1
  35. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +6 -1
  36. package/runtime/scripts/hfs/architecture/root-packages.d.mts +26 -0
  37. package/runtime/scripts/hfs/architecture/root-packages.mjs +78 -0
  38. package/runtime/scripts/hfs/architecture/transport-owner.d.mts +1 -2
  39. package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -6
  40. package/runtime/scripts/hfs/architecture/type-context.d.mts +4 -1
  41. package/runtime/scripts/hfs/architecture/type-context.mjs +30 -0
  42. package/runtime/scripts/hfs/architecture/typescript.d.mts +7 -1
  43. package/runtime/scripts/hfs/architecture/typescript.mjs +21 -1
  44. package/runtime/scripts/hfs/explicit-surface-rule.mjs +1 -1
  45. package/runtime/scripts/hfs/manifest-shape.d.mts +1 -0
  46. package/runtime/scripts/hfs/manifest-shape.mjs +3 -1
  47. package/runtime/scripts/hfs/rules/a15-findings.mjs +5 -0
  48. package/runtime/scripts/hfs/rules/database-access.mjs +39 -1
  49. package/runtime/scripts/hfs/rules/monorepo.d.mts +4 -0
  50. package/runtime/scripts/hfs/rules/monorepo.mjs +10 -5
  51. package/runtime/scripts/hfs/rules/secrets.mjs +11 -0
  52. package/runtime/scripts/hfs/rules/stacks-local.d.mts +7 -0
  53. package/runtime/scripts/hfs/rules/stacks-local.mjs +71 -0
  54. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +6 -1
  55. package/runtime/scripts/hfs/rules/stacks-service-images.d.mts +12 -0
  56. package/runtime/scripts/hfs/rules/stacks-service-images.mjs +120 -0
  57. package/runtime/scripts/hfs/secret-dev-init.d.mts +11 -0
  58. package/runtime/scripts/hfs/secret-dev-init.mjs +79 -0
  59. package/runtime/scripts/hfs/secret-errors.d.mts +3 -0
  60. package/runtime/scripts/hfs/secret-errors.mjs +8 -0
  61. package/runtime/scripts/hfs/secret-init.d.mts +6 -0
  62. package/runtime/scripts/hfs/secret-init.mjs +23 -0
  63. package/runtime/scripts/hfs/secret-sops.mjs +9 -1
  64. package/runtime/scripts/hfs/secret-standard.d.mts +20 -0
  65. package/runtime/scripts/hfs/secret-standard.mjs +106 -0
  66. package/runtime/scripts/hfs/secret.mjs +15 -8
  67. package/runtime/scripts/hfs/slots.mjs +1 -1
  68. package/runtime/scripts/hfs/stack-local-config.d.mts +101 -0
  69. package/runtime/scripts/hfs/stack-local-config.mjs +320 -0
  70. package/runtime/scripts/hfs/stacks-gate.mjs +1 -1
  71. package/runtime/scripts/hfs/view.d.mts +23 -0
  72. package/runtime/scripts/hfs/view.mjs +68 -0
  73. package/runtime/scripts/lib/declaration-surface.d.mts +4 -0
  74. package/runtime/scripts/lib/declaration-surface.mjs +55 -1
  75. package/runtime/scripts/lib/secret-records.d.mts +13 -0
  76. package/runtime/scripts/lib/secret-records.mjs +83 -0
  77. package/runtime/scripts/lib/swarm-stack.d.mts +2 -1
  78. package/runtime/scripts/lib/swarm-stack.mjs +2 -2
  79. package/scaffold/add-inbox-lite.mjs +2 -2
  80. package/scaffold/add-module.d.mts +16 -0
  81. package/scaffold/add-module.mjs +232 -0
  82. package/scaffold/add-queue-lite.mjs +50 -3
  83. package/scaffold/add.mjs +5 -0
  84. package/scaffold/app.d.mts +2 -2
  85. package/scaffold/app.mjs +16 -18
  86. package/scaffold/lite-exports.mjs +1 -15
  87. package/scaffold/module-registry.d.mts +19 -0
  88. package/scaffold/module-registry.mjs +84 -0
  89. package/scripts/sync-bundles.d.mts +8 -0
  90. package/src/main.mjs +23 -2
  91. package/stack/cli.mjs +6 -1
  92. package/stack/context.d.mts +15 -0
  93. package/stack/context.mjs +1 -0
  94. package/stack/images/openclaw-runtime/Dockerfile +18 -0
  95. package/stack/images/openclaw-runtime/entrypoint.sh +23 -0
  96. package/stack/images/openclaw-runtime/openclaw-init.mjs +101 -0
  97. package/stack/images/openclaw-runtime/openclaw-relay.mjs +25 -0
  98. package/stack/local-cli.d.mts +25 -0
  99. package/stack/local-cli.mjs +89 -0
  100. package/stack/local-config.d.mts +17 -0
  101. package/stack/local-config.mjs +37 -0
  102. package/stack/local-custody.d.mts +24 -0
  103. package/stack/local-custody.mjs +109 -0
  104. package/stack/local-doctor.d.mts +6 -0
  105. package/stack/local-doctor.mjs +49 -0
  106. package/stack/local-down.d.mts +8 -0
  107. package/stack/local-down.mjs +20 -0
  108. package/stack/local-host.d.mts +42 -0
  109. package/stack/local-host.mjs +163 -0
  110. package/stack/local-logs.d.mts +8 -0
  111. package/stack/local-logs.mjs +32 -0
  112. package/stack/local-ports.d.mts +7 -0
  113. package/stack/local-ports.mjs +39 -0
  114. package/stack/local-prepare.d.mts +22 -0
  115. package/stack/local-prepare.mjs +62 -0
  116. package/stack/local-run.d.mts +71 -0
  117. package/stack/local-run.mjs +200 -0
  118. package/stack/local-state.d.mts +35 -0
  119. package/stack/local-state.mjs +29 -0
  120. package/stack/local-status.d.mts +6 -0
  121. package/stack/local-status.mjs +36 -0
  122. package/stack/local-up.d.mts +11 -0
  123. package/stack/local-up.mjs +169 -0
  124. package/stack/local-verify.d.mts +6 -0
  125. package/stack/local-verify.mjs +21 -0
  126. package/stack/runtime-layers.d.mts +21 -0
  127. package/stack/runtime-layers.mjs +67 -0
  128. package/sync/hygiene.mjs +3 -1
  129. package/sync/index.d.mts +15 -12
  130. package/sync/index.mjs +114 -28
  131. package/templates/app/auth-doors.sql.partial +115 -0
  132. package/templates/app/ci-workflows/github/workflows/ci.yml +1 -1
  133. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -1
  134. package/templates/app/module/jest.config.js.tpl +8 -0
  135. package/templates/app/module/src/index.ts.tpl +2 -0
  136. package/templates/app/module/src/manifest/declaration.spec.ts.tpl +11 -0
  137. package/templates/app/module/src/manifest/declaration.ts.tpl +13 -0
  138. package/templates/app/module/src/manifest/index.ts.tpl +1 -0
  139. package/templates/app/module/src/public/index.ts.tpl +2 -0
  140. package/templates/app/module/src/template/__name__.template.spec.ts.tpl +13 -0
  141. package/templates/app/module/src/template/__name__.template.ts.tpl +41 -0
  142. package/templates/app/module/src/template/index.ts.tpl +1 -0
  143. package/templates/app/module/src/workbench/components/__pascal__WorkbenchView.tsx.tpl +16 -0
  144. package/templates/app/module/src/workbench/index.ts.tpl +2 -0
  145. package/templates/app/package-scripts/core.json +1 -1
  146. package/templates/app/package-scripts/package.json +1 -1
  147. package/templates/app/package-scripts-lite/package.json +1 -1
  148. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +85 -70
  149. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +24 -12
  150. package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +43 -0
  151. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +21 -26
  152. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +6 -0
  153. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +6 -1
  154. package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +76 -13
  155. package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +44 -0
  156. package/templates/be/auth-lite/src/features/api/auth/application/confirm.command.ts +11 -0
  157. package/templates/be/auth-lite/src/features/api/auth/application/confirm.handler.ts +22 -0
  158. package/templates/be/auth-lite/src/features/api/auth/application/forgot.command.ts +11 -0
  159. package/templates/be/auth-lite/src/features/api/auth/application/forgot.handler.ts +22 -0
  160. package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.handler.ts +23 -0
  161. package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.query.ts +12 -0
  162. package/templates/be/auth-lite/src/features/api/auth/application/google-begin.command.ts +11 -0
  163. package/templates/be/auth-lite/src/features/api/auth/application/google-begin.handler.ts +22 -0
  164. package/templates/be/auth-lite/src/features/api/auth/application/google-complete.command.ts +12 -0
  165. package/templates/be/auth-lite/src/features/api/auth/application/google-complete.handler.ts +23 -0
  166. package/templates/be/auth-lite/src/features/api/auth/application/login.command.ts +12 -0
  167. package/templates/be/auth-lite/src/features/api/auth/application/login.handler.ts +23 -0
  168. package/templates/be/auth-lite/src/features/api/auth/application/logout.command.ts +11 -0
  169. package/templates/be/auth-lite/src/features/api/auth/application/logout.handler.ts +22 -0
  170. package/templates/be/auth-lite/src/features/api/auth/application/refresh.command.ts +12 -0
  171. package/templates/be/auth-lite/src/features/api/auth/application/refresh.handler.ts +23 -0
  172. package/templates/be/auth-lite/src/features/api/auth/application/resend.command.ts +11 -0
  173. package/templates/be/auth-lite/src/features/api/auth/application/resend.handler.ts +22 -0
  174. package/templates/be/auth-lite/src/features/api/auth/application/reset.command.ts +11 -0
  175. package/templates/be/auth-lite/src/features/api/auth/application/reset.handler.ts +22 -0
  176. package/templates/be/auth-lite/src/features/api/auth/application/session.command.ts +12 -0
  177. package/templates/be/auth-lite/src/features/api/auth/application/session.handler.ts +23 -0
  178. package/templates/be/auth-lite/src/features/api/auth/application/signup.command.ts +11 -0
  179. package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +22 -0
  180. package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +6 -0
  181. package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +21 -0
  182. package/templates/be/auth-lite/src/features/api/auth/auth.options.ts +3 -0
  183. package/templates/be/auth-lite/src/features/api/auth/index.ts +2 -0
  184. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +6 -0
  185. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module.ts +9 -0
  186. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.options.ts +3 -0
  187. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +145 -0
  188. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/forgot.request.ts +16 -0
  189. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/google-start.request.ts +17 -0
  190. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/login.request.ts +16 -0
  191. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/resend.request.ts +16 -0
  192. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +18 -0
  193. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +22 -0
  194. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/token.request.ts +11 -0
  195. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +46 -0
  196. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +19 -0
  197. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +209 -0
  198. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +84 -0
  199. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.log-events.ts +6 -0
  200. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +233 -0
  201. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +176 -0
  202. package/templates/be/auth-lite/src/modules/domain/identity/identity-sign-in.log-events.ts +6 -0
  203. package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +98 -0
  204. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-attempt.entity.ts +18 -0
  205. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-email-token.entity.ts +34 -0
  206. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +34 -0
  207. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-session.entity.ts +30 -0
  208. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +10 -0
  209. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +86 -0
  210. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +45 -0
  211. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +97 -0
  212. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +81 -0
  213. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +95 -0
  214. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +93 -0
  215. package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +40 -0
  216. package/templates/be/inbox/module-definition.ts.tpl +2 -9
  217. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +2 -11
  218. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +2 -9
  219. package/templates/be/patterns/api/feature.module-definition.ts.tpl +3 -9
  220. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +3 -9
  221. package/templates/be/patterns/cli/group.module-definition.ts.tpl +2 -9
  222. package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +3 -10
  223. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +2 -9
  224. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +2 -9
  225. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +3 -9
  226. package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +3 -10
  227. package/templates/be/patterns/projections/projection.module-definition.ts.tpl +2 -9
  228. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +3 -10
  229. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +2 -9
  230. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +3 -9
  231. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +2 -9
  232. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +3 -9
  233. package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +3 -9
  234. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +3 -9
  235. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +3 -9
  236. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +2 -9
  237. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +3 -9
  238. package/templates/be/platform/graphql/graphql.module-definition.ts +2 -9
  239. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +3 -9
  240. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +3 -9
  241. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +2 -9
  242. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +3 -9
  243. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +2 -9
  244. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +2 -9
  245. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +2 -9
  246. package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +2 -9
  247. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +2 -9
  248. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +2 -0
  249. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +2 -9
  250. package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +3 -13
  251. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +4 -11
  252. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +4 -9
  253. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +5 -11
  254. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +4 -11
  255. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +2 -9
  256. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +2 -9
  257. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +2 -9
  258. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +4 -0
  259. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +3 -0
  260. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +6 -5
  261. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +3 -2
  262. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +12 -5
  263. package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +40 -0
  264. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +22 -1
  265. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +50 -2
  266. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +3 -0
  267. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity-mail.messages.ts +65 -0
  268. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +28 -0
  269. package/templates/be/skeleton-lite/src/modules/integrations/mail/index.ts +6 -0
  270. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.config.ts +33 -0
  271. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.decorators.ts +14 -0
  272. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module-definition.ts +12 -0
  273. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module.ts +27 -0
  274. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.options.ts +22 -0
  275. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +3 -3
  276. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +3 -1
  277. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +8 -4
  278. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +5 -0
  279. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module.ts +29 -0
  280. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +6 -31
  281. package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +18 -9
  282. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +35 -11
  283. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts +44 -8
  284. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +40 -18
  285. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +18 -6
  286. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.spec.ts +73 -0
  287. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.ts +26 -0
  288. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +24 -53
  289. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.registration.ts +31 -0
  290. package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +1 -1
  291. package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +2 -1
  292. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  293. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
  294. package/templates/be/skeleton-lite/src/modules/platform/http-security/rate-limit.guard.spec.ts +11 -0
  295. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +22 -4
  296. package/templates/be/table/api/feature.module-definition.ts.tpl +3 -9
  297. package/templates/be/table/api/graphql.module-definition.ts.tpl +3 -9
  298. package/templates/be/table/module-definition.ts.tpl +2 -9
  299. package/templates/be/test-world/e2e/serves-once-migrated.e2e-spec.ts.tpl +9 -9
  300. package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +4 -4
  301. package/templates/fe/auth-lite/apps/web/src/modules/auth/client.ts +26 -0
  302. package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +16 -0
  303. package/templates/fe/auth-lite/apps/web/src/modules/auth/index.ts +5 -0
  304. package/templates/fe/common/site-shell.tsx.partial +3 -3
  305. package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +33 -10
  306. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +1 -0
  307. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useViewerId.ts +11 -0
  308. package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +15 -12
  309. package/templates/fe/skeleton/apps/__app__/src/modules/viewer-cache/index.ts +5 -0
  310. package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +1 -1
  311. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +30 -67
  312. package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +6 -21
  313. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +10 -16
  314. package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +4 -7
  315. package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/index.tsx +2 -3
  316. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/confirm/page.tsx +9 -0
  317. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/forgot/page.tsx +9 -0
  318. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/resend/page.tsx +9 -0
  319. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/reset/page.tsx +9 -0
  320. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/signup/page.tsx +9 -0
  321. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +5 -9
  322. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +51 -0
  323. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +84 -0
  324. package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +4 -7
  325. package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/index.tsx +1 -2
  326. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppConfirmPage/index.tsx +25 -0
  327. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppForgotPage/index.tsx +21 -0
  328. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +11 -3
  329. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +4 -1
  330. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResendPage/index.tsx +21 -0
  331. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResetPage/index.tsx +25 -0
  332. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppSignupPage/index.tsx +21 -0
  333. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/emailAuth.shared.ts +12 -0
  334. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/index.ts +2 -0
  335. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/useEmailDoor.ts +32 -0
  336. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +2 -2
  337. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +50 -1
  338. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +50 -1
  339. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +7 -30
  340. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +13 -14
  341. package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +5 -0
  342. package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +4 -17
  343. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +0 -16
  344. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +0 -11
  345. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +0 -7
  346. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +0 -6
  347. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +0 -8
  348. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +0 -6
  349. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -186
  350. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +0 -22
  351. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +0 -25
  352. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.spec.ts +0 -104
  353. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +0 -34
  354. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.spec.ts +0 -28
  355. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +0 -11
  356. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +0 -88
  357. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +0 -69
  358. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +0 -41
  359. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +0 -40
  360. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +0 -63
  361. package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/classNames.ts +0 -4
  362. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +0 -30
  363. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +0 -50
  364. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +0 -31
  365. package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/classNames.ts +0 -4
@@ -160,6 +160,107 @@ function childLine(lines, block, key) {
160
160
  return at;
161
161
  return -1;
162
162
  }
163
+ /** The service of `services:` that `member` is the sealed credential of: named like the member (`sonar`), or one whose credential custody path is the member's file. */
164
+ function toolingServiceOf(document, member) {
165
+ const services = (document["services"] ?? {});
166
+ for (const [name, service] of Object.entries(services)) {
167
+ if (service === null || typeof service !== "object")
168
+ continue;
169
+ const owns = (service.credentials ?? []).some((credential) => new RegExp(`/${member}\\.enc(?:\\.yaml)?$`, "u").test(credential?.custody?.path ?? ""));
170
+ if (name === member || owns)
171
+ return { name, service };
172
+ }
173
+ return null;
174
+ }
175
+ /**
176
+ * A tooling member (sonar.enc.yaml: the analysis token a CI job reads) is a credential of a service, not a secret the app reads at runtime, so it never
177
+ * becomes an `environments.<env>.secrets` entry (CI custody refuses a runtime-granted CI secret, APP_SECRET_IN_CI). Its binding is the service's
178
+ * `credentials` entry (custody names the sealed file) and the `ci.secrets` entry that reads it. A missing binding of the shape the template renders
179
+ * (`credentials: []`) is written; any other shape returns the exact binding to add by hand.
180
+ */
181
+ function bindTooling(file, text, document, repository, environment, member, keys, found) {
182
+ const bound = new Set((found.service.credentials ?? []).map((credential) => credential.key));
183
+ const missing = keys.filter((key) => !bound.has(key));
184
+ if (!missing.length)
185
+ return { bound: [], remedy: "" };
186
+ const project = found.service.projects?.[0]?.key ?? repository;
187
+ const custodyPath = `.starcistacks/${environment}/secrets/${member}.enc.yaml`;
188
+ const first = !(found.service.credentials ?? []).length;
189
+ const credentials = missing.map((key, index) => ({ id: first && index === 0 ? "analysis" : key.toLowerCase().replaceAll("_", "-"), project, key, custody: { repository, path: custodyPath } }));
190
+ const ciSecrets = credentials.map((credential) => ({ name: credential.key, credential: credential.id }));
191
+ const remedy = `${member} is a tooling credential of services.${found.name}, not an app runtime secret, so it is not declared in environments.${environment}.secrets; bind it by hand: services.${found.name}.credentials: ${JSON.stringify(credentials)} and services.${found.name}.ci.secrets: ${JSON.stringify(ciSecrets)}`;
192
+ const newline = text.includes("\r\n") ? "\r\n" : "\n";
193
+ const lines = text.replace(/\r\n/gu, "\n").split("\n");
194
+ const root = lines.findIndex((line) => /^services:\s*(?:#.*)?$/u.test(line));
195
+ const serviceLine = root < 0 ? -1 : childLine(lines, blockOf(lines, root), found.name);
196
+ const credentialsLine = serviceLine < 0 ? -1 : childLine(lines, blockOf(lines, serviceLine), "credentials");
197
+ if (credentialsLine < 0 || !/^ *credentials:\s*\[\s*\]\s*(?:#.*)?$/u.test(lines[credentialsLine]) || !first)
198
+ return { bound: [], remedy };
199
+ const pad = indentOf(lines[credentialsLine]);
200
+ const ciLine = childLine(lines, blockOf(lines, serviceLine), "ci");
201
+ const expected = structuredClone(document);
202
+ const target = expected["services"][found.name];
203
+ target.credentials = credentials;
204
+ if (target.ci !== undefined)
205
+ target.ci.secrets = [...(target.ci.secrets ?? []), ...ciSecrets];
206
+ if (target.ci !== undefined) {
207
+ const ci = ciLine < 0 ? "" : lines[ciLine];
208
+ const secrets = `[${ciSecrets.map((entry) => `{name: ${entry.name}, credential: ${entry.credential}}`).join(", ")}]`;
209
+ if (/secrets:\s*\[\s*\]/u.test(ci))
210
+ lines[ciLine] = ci.replace(/secrets:\s*\[\s*\]/u, `secrets: ${secrets}`);
211
+ else if (/^ *ci:\s*\{.*\}\s*$/u.test(ci) && !/secrets:/u.test(ci))
212
+ lines[ciLine] = ci.replace(/\}\s*$/u, `, secrets: ${secrets}}`);
213
+ else
214
+ return { bound: [], remedy };
215
+ }
216
+ const block = credentials.flatMap((credential) => [`${" ".repeat(pad + 2)}- id: ${credential.id}`, `${" ".repeat(pad + 4)}project: ${JSON.stringify(credential.project)}`, `${" ".repeat(pad + 4)}key: ${credential.key}`, `${" ".repeat(pad + 4)}custody: {repository: ${JSON.stringify(credential.custody.repository)}, path: ${credential.custody.path}}`]);
217
+ lines.splice(credentialsLine, 1, `${" ".repeat(pad)}credentials:`, ...block);
218
+ const edited = lines.join(newline);
219
+ let parsed;
220
+ try {
221
+ parsed = parseYaml(edited);
222
+ }
223
+ catch {
224
+ parsed = undefined;
225
+ }
226
+ if (JSON.stringify(parsed) !== JSON.stringify(expected))
227
+ return { bound: [], remedy };
228
+ fs.writeFileSync(file, edited);
229
+ return { bound: missing, remedy: "" };
230
+ }
231
+ /**
232
+ * The tooling members of the app (a sealed `.starcistacks/<env>/secrets/<member>.enc.yaml` that a service of application-stacks.yaml owns): each is bound
233
+ * to its service when `write` and the binding is missing in the shape the template renders. The runtime `secrets:` entries an earlier `secret set` declared for
234
+ * the member are named, not removed: the owner deletes them (CI custody refuses the token while one stands).
235
+ */
236
+ export function bindToolingMembers(repoRoot, write) {
237
+ const file = path.join(repoRoot, ".starcistacks", "application-stacks.yaml");
238
+ if (!fs.existsSync(file))
239
+ return [];
240
+ const text = fs.readFileSync(file, "utf8");
241
+ const document = parseYaml(text) ?? {}; // reason: parsed YAML boundary, read from disk
242
+ const repository = repositoryName(repoRoot, { env: identityQueryEnvironment(process.env, serviceCredentialNames(document)) });
243
+ const found = [];
244
+ for (const environment of Object.keys(document.environments ?? {})) {
245
+ const directory = path.join(repoRoot, ".starcistacks", environment, "secrets");
246
+ if (!fs.existsSync(directory))
247
+ continue;
248
+ for (const name of fs.readdirSync(directory).filter((entry) => entry.endsWith(MEMBER_SUFFIX)).sort()) {
249
+ const member = name.slice(0, -MEMBER_SUFFIX.length);
250
+ const service = toolingServiceOf(document, member);
251
+ if (service === null)
252
+ continue;
253
+ const keys = envelopeOf(fs.readFileSync(path.join(directory, name), "utf8")).keys;
254
+ const entries = Array.isArray(document.environments?.[environment]?.secrets) ? document.environments?.[environment]?.secrets : [];
255
+ const runtimeEntries = entries.filter((entry) => memberOfEntry(entry) === member).map((entry) => String(entry.name ?? entry.key ?? member));
256
+ const outcome = write
257
+ ? bindTooling(file, fs.readFileSync(file, "utf8"), parseYaml(fs.readFileSync(file, "utf8")) ?? {}, repository, environment, member, keys, service)
258
+ : { bound: [], remedy: (service.service.credentials ?? []).length ? "" : `services.${service.name} has no credential for ${member}` };
259
+ found.push({ environment, member, service: service.name, outcome, runtimeEntries });
260
+ }
261
+ }
262
+ return found;
263
+ }
163
264
  /**
164
265
  * Declares each of `keys` of `member` in environment `environment`'s `secrets:` of application-stacks.yaml, one entry per key
165
266
  * (`name: <kebab of the key>, member, key, where: secrets/<member>`), so the key is materialized as its own `<KEY>_FILE`. A key
@@ -175,6 +276,11 @@ export function declareKeys(repoRoot, environment, member, keys) {
175
276
  const spec = document.environments?.[environment];
176
277
  if (!spec)
177
278
  return { declared: [], byHand: [] };
279
+ const tooling = toolingServiceOf(document, member);
280
+ if (tooling !== null) {
281
+ const outcome = bindTooling(file, text, document, repositoryName(repoRoot, { env: identityQueryEnvironment(process.env, serviceCredentialNames(document)) }), environment, member, keys, tooling);
282
+ return outcome.bound.length || outcome.remedy ? { declared: [], byHand: [], tooling: outcome } : { declared: [], byHand: [] };
283
+ }
178
284
  const serviceKeys = declaredServiceKeys(document, repositoryName(repoRoot, { env: identityQueryEnvironment(process.env, serviceCredentialNames(document)) }), environment, member);
179
285
  const missing = keys.filter((key) => !declaredKeys(spec, member, keys).includes(key) && !serviceKeys.includes(key));
180
286
  if (!missing.length)
@@ -24,6 +24,7 @@ import { setValues } from "./secret-prompt.mjs";
24
24
  import { stackContext } from "../../../stack/context.mjs";
25
25
  import { StackError } from "../../../stack/errors.mjs";
26
26
  import { serverKey } from "../../../stack/server-key.mjs";
27
+ import { seedDevCustody } from "./secret-dev-init.mjs";
27
28
  export { envelopeOf, formatOf, plaintextOf } from "./secret-document.mjs";
28
29
  export { setSecretValues } from "./secret-store.mjs";
29
30
  /** The actions of `starci app secret`, in usage order. */
@@ -76,7 +77,7 @@ function parse(argv) {
76
77
  /** The operands and flags of one secret action, as the usage of `starci app secret` prints them. */
77
78
  export function secretUsage(verb) {
78
79
  if (verb === "init")
79
- return "";
80
+ return "[--env <dev env> [--execute]]";
80
81
  if (verb === "set")
81
82
  return "--env <env> <slug> <KEY> [KEY ...] [--generate <bytes>] [--from-file KEY=path ...]";
82
83
  if (verb === "get")
@@ -100,8 +101,8 @@ export function secretUsage(verb) {
100
101
  return "--env <env>";
101
102
  }
102
103
  function editOptionsFor(verb, opts) {
103
- if (opts.execute !== undefined && !EDIT_VERBS.has(verb) && !MOVE_VERBS.has(verb) && verb !== "server-key" && verb !== "recipients")
104
- throw new SecretError("--execute is only valid for unset, rename, remove, recipients, migrate, split and server-key");
104
+ if (opts.execute !== undefined && !EDIT_VERBS.has(verb) && !MOVE_VERBS.has(verb) && verb !== "server-key" && verb !== "recipients" && verb !== "init")
105
+ throw new SecretError("--execute is only valid for init --env, unset, rename, remove, recipients, migrate, split and server-key");
105
106
  if (opts.execute !== undefined && verb === "recipients" && !opts.add && !opts.remove)
106
107
  throw new SecretError("--execute is only valid for recipients with --add or --remove");
107
108
  if ((opts.rotate !== undefined || opts.backup !== undefined || opts.restore !== undefined) && verb !== "server-key")
@@ -128,8 +129,8 @@ function outputOptionsFor(verb, opts) {
128
129
  }
129
130
  /** The environment leads the operands (`--env <env> <slug> ...` is checked and read as `<env> <slug> ...`); init takes none, audit may name one. */
130
131
  function environmentOf(verb, opts) {
131
- if (verb === "init" && opts.env !== undefined)
132
- throw new SecretError("--env is not valid for init");
132
+ if (verb === "init" && opts.env === undefined && opts.execute !== undefined)
133
+ throw new SecretError("--execute is only valid for init --env <dev env>");
133
134
  if (verb !== "init" && verb !== "audit" && opts.env === undefined)
134
135
  throw new SecretError(`usage: starci app secret ${verb} ${secretUsage(verb)}`);
135
136
  if (verb === "audit" && opts.positional.length)
@@ -141,7 +142,7 @@ function optionsFor(verb, opts) {
141
142
  environmentOf(verb, opts);
142
143
  const count = opts.positional.length;
143
144
  const bounds = {
144
- init: [0, 0],
145
+ init: [0, 1],
145
146
  list: [1, 1],
146
147
  materialize: [1, 1],
147
148
  verify: [1, 1],
@@ -192,7 +193,11 @@ function fromFiles(entries, keys) {
192
193
  return files;
193
194
  }
194
195
  /** What `set` declared in the environment's stack `secrets:`, or the keys to declare by hand; nothing when all were declared. */
195
- function declarationLine(environment, { declared, byHand }) {
196
+ function declarationLine(environment, { declared, byHand, tooling }) {
197
+ if (tooling?.bound.length)
198
+ return `bound ${tooling.bound.join(", ")} to the service credentials and ci.secrets of application-stacks.yaml (a tooling member is not a runtime secret)\n`;
199
+ if (tooling?.remedy)
200
+ return `${tooling.remedy}\n`;
196
201
  if (declared.length)
197
202
  return `declared ${declared.join(", ")} in environments.${environment}.secrets of application-stacks.yaml\n`;
198
203
  if (byHand.length)
@@ -253,10 +258,12 @@ export async function secretMain(argv, { stdout = (s) => process.stdout.write(s)
253
258
  const [verb, ...rest] = argv;
254
259
  try {
255
260
  if (!VERBS.has(verb))
256
- throw new SecretError("usage: starci app secret init | set --env <env> <slug> <KEY> [KEY ...] | get --env <env> <slug> [KEY] --to-file | unset --env <env> <slug> <KEY> | rename --env <env> <slug> <OLD> <NEW> | remove --env <env> <slug> | materialize --env <env> | list --env <env> | verify --env <env> | recipients --env <env> [--add|--remove ...] [--execute] | migrate --env <env> <slug> [--to <slug>] [--into-existing] | split --env <env> <slug> [--group KEY=member] [--group-file <yaml>] [--into-existing] | audit [--env <env>] | server-key --env <env> [--rotate | --backup github | --restore github] [--execute]");
261
+ throw new SecretError("usage: starci app secret init [--env <dev env> [--execute]] | set --env <env> <slug> <KEY> [KEY ...] | get --env <env> <slug> [KEY] --to-file | unset --env <env> <slug> <KEY> | rename --env <env> <slug> <OLD> <NEW> | remove --env <env> <slug> | materialize --env <env> | list --env <env> | verify --env <env> | recipients --env <env> [--add|--remove ...] [--execute] | migrate --env <env> <slug> [--to <slug>] [--into-existing] | split --env <env> <slug> [--group KEY=member] [--group-file <yaml>] [--into-existing] | audit [--env <env>] | server-key --env <env> [--rotate | --backup github | --restore github] [--execute]");
257
262
  const opts = parse(rest);
258
263
  optionsFor(verb, opts);
259
264
  const repoRoot = path.resolve(cwd, opts.cwd ?? ".");
265
+ if (verb === "init" && opts.env !== undefined)
266
+ return seedDevCustody(repoRoot, opts.env, { execute: opts.execute === true, sops: () => sops ?? defaultSops(env, repoRoot, platform, tools, hostIdentity), stdout, random });
260
267
  if (verb === "init")
261
268
  return publicStorage(() => initializeSecrets(repoRoot, { env, platform, tools, stdout, host: hostIdentity }));
262
269
  if (verb === "audit")
@@ -232,7 +232,7 @@ export function readRepoDeclaration(manifest, repoRoot) {
232
232
  function createScopeResolver(manifest, repo) {
233
233
  const profile = repo.profile;
234
234
  const edition = repo.edition ?? 'full';
235
- const slots = manifest.slots.filter((s) => s.profiles.includes(profile) && slotInEdition(manifest, s, edition)).map((s) => effectiveSlot(s, profile, edition));
235
+ const slots = manifest.slots.filter((s) => s.managedOnly !== true && s.profiles.includes(profile) && slotInEdition(manifest, s, edition)).map((s) => effectiveSlot(s, profile, edition));
236
236
  const scope = { manifest, repo, profile, slots, byId: new Map(slots.map((s) => [s.id, s])), variants: slots.flatMap(variantsOf), appKind: new Map(repo.apps.map((a) => [a.name, a.kind])) };
237
237
  const classifyPath = (input) => classifyIn(scope, input);
238
238
  /** tracked | ignored | external for the slot owning `p`, or null when no slot owns it. */
@@ -0,0 +1,101 @@
1
+ import type { StackProblem } from './stack-config-kit.mjs';
2
+ export declare const LOCAL_CONFIG_CODE = "STACKS_LOCAL_CONFIG";
3
+ /** The generators `starci app secret init --env <env>` knows (PROJECT-STACKS-5: dev-only values, created once, never replaced). */
4
+ export declare const GENERATORS: readonly string[];
5
+ /** A key of a sealed member of the environment. */
6
+ export interface CustodyRef {
7
+ member: string;
8
+ key: string;
9
+ }
10
+ /** A command the verbs run: an argv list (never a shell line), run from `cwd` (app-root relative). */
11
+ export interface LocalCommand {
12
+ name: string;
13
+ argv: string[];
14
+ cwd: string;
15
+ heavy: 'install' | 'build' | 'none';
16
+ skipIfExists: string | null;
17
+ env: Record<string, string>;
18
+ files: string[];
19
+ /** Seed only: `migrate` runs it once the database is migrated, `ready` once the processes answer (a seed that signs up through the api). */
20
+ after: 'migrate' | 'ready';
21
+ }
22
+ /** A compose service the environment starts and how its readiness is observed. */
23
+ export interface LocalService {
24
+ name: string;
25
+ wait: 'healthy' | 'started' | 'tcp';
26
+ port: string | null;
27
+ }
28
+ /** A native app process. */
29
+ export interface LocalProcess extends LocalCommand {
30
+ port: string | null;
31
+ health: HttpProbe | null;
32
+ }
33
+ /** One HTTP probe: the readiness of a process, a `ready` row or a `verify` row. */
34
+ export interface HttpProbe {
35
+ name: string;
36
+ url: string;
37
+ method: string;
38
+ status: number;
39
+ contains: string | null;
40
+ }
41
+ /** The per-environment database: its compose service, its own login role and where the app reads its URL. */
42
+ export interface LocalDatabase {
43
+ service: string;
44
+ port: string;
45
+ name: string;
46
+ role: string;
47
+ urlEnv: string;
48
+ urlAs: 'env' | 'file';
49
+ adminUser: string;
50
+ password: CustodyRef;
51
+ adminPassword: CustodyRef;
52
+ appRole: string | null;
53
+ appPassword: CustodyRef | null;
54
+ }
55
+ /** One dev-only value the owner-armed `secret init` creates once. */
56
+ export interface CustodyGenerator extends CustodyRef {
57
+ kind: string;
58
+ bytes: number;
59
+ publicKey: string | null;
60
+ }
61
+ /** The normalised `local` block of one environment. */
62
+ export interface LocalBlock {
63
+ project: string;
64
+ ports: Record<string, number>;
65
+ services: LocalService[];
66
+ database: LocalDatabase | null;
67
+ composeEnv: Array<CustodyRef & {
68
+ name: string;
69
+ }>;
70
+ /** Least grants: the keys one compose service may read, mounted from `STACK_GRANT_<NAME>` (a directory holding only those keys). */
71
+ grants: Array<{
72
+ name: string;
73
+ keys: string[];
74
+ }>;
75
+ /** Data directories under the state base (`${data}/<name>`, `STACK_DATA_DIR`), created by `up` before anything starts: bind mounts and file stores of native processes. */
76
+ dirs: string[];
77
+ custody: CustodyGenerator[];
78
+ setup: LocalCommand[];
79
+ seed: LocalCommand[];
80
+ processes: LocalProcess[];
81
+ ready: HttpProbe[];
82
+ verify: HttpProbe[];
83
+ origins: Array<{
84
+ name: string;
85
+ url: string;
86
+ }>;
87
+ resources: {
88
+ diskGb: number;
89
+ ramMb: number;
90
+ };
91
+ }
92
+ /** The normalised `local` block, or the problems that stop it. `project` and `env` name the defaults (`<project>-<env>`, the role). */
93
+ export declare function normalizeLocal(raw: unknown, { project, env }: {
94
+ project: string;
95
+ env: string;
96
+ }): {
97
+ config: LocalBlock | null;
98
+ problems: StackProblem[];
99
+ };
100
+ /** Replaces `${port.<name>}` with the chosen host port. */
101
+ export declare const expandPorts: (value: string, ports: Readonly<Record<string, number>>) => string;
@@ -0,0 +1,320 @@
1
+ // stack-local-config.mjs - reads and judges `environments.<env>.local`, the declaration of a dev environment (runtime docker-compose) that
2
+ // `starci app stack up|down|doctor|prepare|status|logs|verify --env <env>` runs (PROJECT-STACKS-6). It holds names, ports, argv lists and
3
+ // shapes only, never a value: custody keys are references into .starcistacks/<env>/secrets (PROJECT-STACKS-5). The authority for the fields is
4
+ // this module; the same normaliser serves `starci app check` (rule R340) and the verbs, so a block the check passes is a block the verbs run.
5
+ import path from 'node:path';
6
+ import { isPlainObject as plain } from '../../engine/plain-object.mjs';
7
+ import { closedFields, collector, patternField } from './stack-config-kit.mjs';
8
+ export const LOCAL_CONFIG_CODE = 'STACKS_LOCAL_CONFIG';
9
+ const NAME = /^[a-z][a-z0-9-]{0,40}$/u;
10
+ const KEY = /^[A-Z][A-Z0-9_]{0,63}$/u;
11
+ const PROJECT = /^[a-z0-9][a-z0-9_-]{0,62}$/u;
12
+ const NAME_PATTERN = /^[a-z][a-z0-9-]{0,40}$/u;
13
+ const SQL_NAME = /^[a-z][a-z0-9_]{0,62}$/u;
14
+ /** The generators `starci app secret init --env <env>` knows (PROJECT-STACKS-5: dev-only values, created once, never replaced). */
15
+ export const GENERATORS = Object.freeze(['hex', 'base64', 'password', 'uuid', 'rsa-pem', 'ec-pem', 'ed25519-pem']);
16
+ const TOP = ['project', 'ports', 'services', 'database', 'composeEnv', 'grants', 'dirs', 'custody', 'setup', 'seed', 'processes', 'ready', 'verify', 'origins', 'resources'];
17
+ const COMMAND = ['name', 'argv', 'cwd', 'heavy', 'skipIfExists', 'env', 'files'];
18
+ const SEED = [...COMMAND, 'after'];
19
+ const PROCESS = [...COMMAND, 'port', 'health'];
20
+ const text = (value) => typeof value === 'string' && value.trim() !== '';
21
+ /** A relative path inside the app root: no drive, no `..`, forward slashes. */
22
+ function relative(c, at, value) {
23
+ if (typeof value !== 'string' || value === '' || path.posix.isAbsolute(value) || /^[A-Za-z]:/u.test(value) || value.includes('\\') || path.posix.normalize(value).split('/').includes('..')) {
24
+ c.problem(at, 'must be a relative path inside the app root');
25
+ return '.';
26
+ }
27
+ return value;
28
+ }
29
+ function ref(c, at, value) {
30
+ if (!closedFields(c, at, value, ['member', 'key'], 'local'))
31
+ return { member: '', key: '' };
32
+ const entry = value;
33
+ patternField(c, `${at}.member`, entry.member, /^[a-z0-9][a-z0-9-]*$/u);
34
+ patternField(c, `${at}.key`, entry.key, KEY);
35
+ return { member: String(entry.member), key: String(entry.key) };
36
+ }
37
+ function strings(c, at, value, pattern = null) {
38
+ if (value === undefined)
39
+ return [];
40
+ if (!Array.isArray(value) || value.some((item) => !text(item))) {
41
+ c.problem(at, 'must be a list of non-empty strings');
42
+ return [];
43
+ }
44
+ if (pattern)
45
+ value.forEach((item, index) => patternField(c, `${at}[${index}]`, item, pattern));
46
+ return value;
47
+ }
48
+ function command(c, at, value, allowed, names) {
49
+ const empty = { name: '', argv: [], cwd: '.', heavy: 'none', skipIfExists: null, env: {}, files: [], after: 'migrate' };
50
+ if (!closedFields(c, at, value, allowed, 'local'))
51
+ return empty;
52
+ const entry = value;
53
+ const name = String(entry.name ?? '');
54
+ if (patternField(c, `${at}.name`, entry.name, NAME)) {
55
+ if (names.has(name))
56
+ c.problem(`${at}.name`, `${name} is declared twice`);
57
+ names.add(name);
58
+ }
59
+ const argv = strings(c, `${at}.argv`, entry.argv);
60
+ if (argv.length === 0)
61
+ c.problem(`${at}.argv`, 'must hold the command and its arguments (an argv list, never a shell line)');
62
+ const heavy = entry.heavy ?? 'none';
63
+ if (!['install', 'build', 'none'].includes(String(heavy)))
64
+ c.problem(`${at}.heavy`, 'must be install, build or none (the heavy-command governor class of the command)');
65
+ let env = {};
66
+ if (entry.env !== undefined) {
67
+ if (!plain(entry.env))
68
+ c.problem(`${at}.env`, 'must be a mapping of non-secret values');
69
+ else {
70
+ env = entry.env;
71
+ for (const [key, item] of Object.entries(env)) {
72
+ patternField(c, `${at}.env.${key}`, key, KEY);
73
+ if (typeof item !== 'string')
74
+ c.problem(`${at}.env.${key}`, 'must be a string');
75
+ }
76
+ }
77
+ }
78
+ return {
79
+ name, argv, cwd: entry.cwd === undefined ? '.' : relative(c, `${at}.cwd`, entry.cwd), heavy: heavy,
80
+ skipIfExists: entry.skipIfExists === undefined ? null : relative(c, `${at}.skipIfExists`, entry.skipIfExists), env,
81
+ files: strings(c, `${at}.files`, entry.files, KEY), after: afterOf(c, at, entry.after),
82
+ };
83
+ }
84
+ function afterOf(c, at, value) {
85
+ if (value === undefined)
86
+ return 'migrate';
87
+ if (value !== 'migrate' && value !== 'ready')
88
+ c.problem(`${at}.after`, 'must be migrate or ready');
89
+ return value === 'ready' ? 'ready' : 'migrate';
90
+ }
91
+ function probe(c, at, value) {
92
+ const empty = { name: '', url: '', method: 'GET', status: 200, contains: null };
93
+ if (!closedFields(c, at, value, ['name', 'url', 'method', 'status', 'contains'], 'local'))
94
+ return empty;
95
+ const entry = value;
96
+ patternField(c, `${at}.name`, entry.name, NAME);
97
+ const url = String(entry.url ?? '');
98
+ if (!/^http:\/\/(localhost|127\.0\.0\.1|[a-z0-9-]+\.localhost)(:\$\{port\.[a-z][a-z0-9-]*\}|:[0-9]+)?(\/.*)?$/u.test(url))
99
+ c.problem(`${at}.url`, 'must be an http URL on the loopback (localhost, 127.0.0.1 or *.localhost), its port a literal or ${port.<name>}');
100
+ const method = String(entry.method ?? 'GET');
101
+ if (!['GET', 'HEAD', 'POST'].includes(method))
102
+ c.problem(`${at}.method`, 'must be GET, HEAD or POST');
103
+ const status = entry.status === undefined ? 200 : entry.status;
104
+ if (!Number.isInteger(status) || status < 100 || status > 599)
105
+ c.problem(`${at}.status`, 'must be an HTTP status');
106
+ return { name: String(entry.name ?? ''), url, method, status: status, contains: text(entry.contains) ? entry.contains : null };
107
+ }
108
+ function portsOf(c, value) {
109
+ const ports = {};
110
+ if (!plain(value) || Object.keys(value).length === 0) {
111
+ c.problem('ports', 'must name at least one port: the host ports the environment publishes or listens on');
112
+ return ports;
113
+ }
114
+ const seen = new Map();
115
+ for (const [name, port] of Object.entries(value)) {
116
+ patternField(c, `ports.${name}`, name, NAME);
117
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
118
+ c.problem(`ports.${name}`, 'must be a port from 1024 to 65535');
119
+ continue;
120
+ }
121
+ if (seen.has(port))
122
+ c.problem(`ports.${name}`, `is the port of ${seen.get(port)} too`);
123
+ seen.set(port, name);
124
+ ports[name] = port;
125
+ }
126
+ return ports;
127
+ }
128
+ function servicesOf(c, value, ports) {
129
+ if (!Array.isArray(value) || value.length === 0) {
130
+ c.problem('services', 'must list the compose services `up` starts, dependencies first');
131
+ return [];
132
+ }
133
+ return value.map((item, index) => {
134
+ const at = `services[${index}]`;
135
+ if (!closedFields(c, at, item, ['name', 'wait', 'port'], 'local'))
136
+ return { name: '', wait: 'started', port: null };
137
+ const entry = item;
138
+ patternField(c, `${at}.name`, entry.name, NAME);
139
+ const wait = String(entry.wait ?? 'healthy');
140
+ if (!['healthy', 'started', 'tcp'].includes(wait))
141
+ c.problem(`${at}.wait`, 'must be healthy (the compose healthcheck), tcp (the port accepts) or started');
142
+ const port = entry.port === undefined ? null : String(entry.port);
143
+ if (wait === 'tcp' && (port === null || !Object.hasOwn(ports, port)))
144
+ c.problem(`${at}.port`, 'must name a declared port when wait is tcp');
145
+ if (port !== null && !Object.hasOwn(ports, port))
146
+ c.problem(`${at}.port`, `${port} is not a declared port`);
147
+ return { name: String(entry.name ?? ''), wait: wait, port };
148
+ });
149
+ }
150
+ function databaseOf(c, value, ports, services, role) {
151
+ if (value === undefined)
152
+ return null;
153
+ if (!closedFields(c, 'database', value, ['service', 'port', 'name', 'role', 'urlEnv', 'urlAs', 'adminUser', 'password', 'adminPassword', 'appRole', 'appPassword'], 'local'))
154
+ return null;
155
+ const entry = value;
156
+ patternField(c, 'database.service', entry.service, NAME);
157
+ if (!services.some((service) => service.name === entry.service))
158
+ c.problem('database.service', 'must be one of the started services');
159
+ if (!Object.hasOwn(ports, String(entry.port)))
160
+ c.problem('database.port', 'must name a declared port');
161
+ const own = entry.role === undefined ? role : String(entry.role);
162
+ patternField(c, 'database.role', own, SQL_NAME);
163
+ const name = entry.name === undefined ? own : String(entry.name);
164
+ patternField(c, 'database.name', name, SQL_NAME);
165
+ patternField(c, 'database.urlEnv', entry.urlEnv, KEY);
166
+ const urlAs = String(entry.urlAs ?? 'file');
167
+ if (!['env', 'file'].includes(urlAs))
168
+ c.problem('database.urlAs', 'must be env (the URL in the process environment) or file (<KEY>_FILE, preferred)');
169
+ const adminUser = String(entry.adminUser ?? 'postgres');
170
+ patternField(c, 'database.adminUser', adminUser, SQL_NAME);
171
+ if (own === adminUser || own === 'postgres')
172
+ c.problem('database.role', 'must be a role of this environment alone, never the superuser and never a role another environment uses');
173
+ const appRole = entry.appRole === undefined ? null : String(entry.appRole);
174
+ if (appRole !== null)
175
+ patternField(c, 'database.appRole', appRole, SQL_NAME);
176
+ if ((appRole === null) !== (entry.appPassword === undefined))
177
+ c.problem('database.appPassword', 'is declared with appRole, both or neither: the app role the migrations create gets its own password per environment');
178
+ if (appRole !== null && (appRole === own || appRole === adminUser))
179
+ c.problem('database.appRole', 'must be a role other than the owner role and the superuser');
180
+ return {
181
+ appRole, appPassword: entry.appPassword === undefined ? null : ref(c, 'database.appPassword', entry.appPassword),
182
+ service: String(entry.service), port: String(entry.port), name, role: own, urlEnv: String(entry.urlEnv), urlAs: urlAs, adminUser,
183
+ password: ref(c, 'database.password', entry.password), adminPassword: ref(c, 'database.adminPassword', entry.adminPassword),
184
+ };
185
+ }
186
+ function custodyOf(c, value) {
187
+ if (value === undefined)
188
+ return [];
189
+ if (!Array.isArray(value)) {
190
+ c.problem('custody', 'must be a list of generated dev values');
191
+ return [];
192
+ }
193
+ return value.map((item, index) => {
194
+ const at = `custody[${index}]`;
195
+ if (!closedFields(c, at, item, ['member', 'key', 'kind', 'bytes', 'publicKey'], 'local'))
196
+ return { member: '', key: '', kind: 'hex', bytes: 32, publicKey: null };
197
+ const entry = item;
198
+ const base = ref(c, at, { member: entry.member, key: entry.key });
199
+ if (!GENERATORS.includes(String(entry.kind)))
200
+ c.problem(`${at}.kind`, `must be one of ${GENERATORS.join(', ')}`);
201
+ const bytes = entry.bytes === undefined ? 32 : entry.bytes;
202
+ if (!Number.isInteger(bytes) || bytes < 8 || bytes > 1024)
203
+ c.problem(`${at}.bytes`, 'must be a whole byte count from 8 to 1024');
204
+ if (entry.publicKey !== undefined) {
205
+ if (entry.kind === 'rsa-pem' || entry.kind === 'ec-pem' || entry.kind === 'ed25519-pem')
206
+ patternField(c, `${at}.publicKey`, entry.publicKey, KEY);
207
+ else
208
+ c.problem(`${at}.publicKey`, 'is only for kind rsa-pem, ec-pem or ed25519-pem');
209
+ }
210
+ return { ...base, kind: String(entry.kind), bytes: bytes, publicKey: text(entry.publicKey) ? entry.publicKey : null };
211
+ });
212
+ }
213
+ function listOf(c, at, value, each) {
214
+ if (value === undefined)
215
+ return [];
216
+ if (!Array.isArray(value)) {
217
+ c.problem(at, 'must be a list');
218
+ return [];
219
+ }
220
+ return value.map((item, index) => each(item, index));
221
+ }
222
+ function referencedPorts(c, block) {
223
+ const known = new Set(Object.keys(block.ports));
224
+ const check = (at, value) => {
225
+ for (const match of value.matchAll(/\$\{port\.([a-z][a-z0-9-]*)\}/gu))
226
+ if (!known.has(match[1]))
227
+ c.problem(at, `names the undeclared port ${match[1]}`);
228
+ };
229
+ for (const item of [...block.ready, ...block.verify])
230
+ check(`url of ${item.name}`, item.url);
231
+ for (const origin of block.origins)
232
+ check(`origin ${origin.name}`, origin.url);
233
+ for (const run of [...block.setup, ...block.seed, ...block.processes]) {
234
+ for (const part of run.argv)
235
+ check(`argv of ${run.name}`, part);
236
+ for (const value of Object.values(run.env))
237
+ check(`env of ${run.name}`, value);
238
+ }
239
+ for (const process of block.processes) {
240
+ if (process.port !== null && !known.has(process.port))
241
+ c.problem(`processes.${process.name}.port`, `${process.port} is not a declared port`);
242
+ if (process.health)
243
+ check(`health of ${process.name}`, process.health.url);
244
+ }
245
+ }
246
+ /** The normalised `local` block, or the problems that stop it. `project` and `env` name the defaults (`<project>-<env>`, the role). */
247
+ export function normalizeLocal(raw, { project, env }) {
248
+ const c = collector();
249
+ if (!closedFields(c, 'local', raw, TOP, 'local'))
250
+ return { config: null, problems: c.problems };
251
+ const entry = raw;
252
+ const compose = String(entry.project ?? `${project}-${env}`).toLowerCase().replaceAll(/[^a-z0-9_-]+/gu, '-');
253
+ patternField(c, 'project', compose, PROJECT);
254
+ const ports = portsOf(c, entry.ports);
255
+ const services = servicesOf(c, entry.services, ports);
256
+ const role = `${project}_${env}`.toLowerCase().replaceAll(/[^a-z0-9]+/gu, '_').replace(/^[^a-z]+/u, 'a');
257
+ const names = new Set();
258
+ const block = {
259
+ project: compose, ports, services,
260
+ database: databaseOf(c, entry.database, ports, services, role),
261
+ composeEnv: listOf(c, 'composeEnv', entry.composeEnv, (item, index) => {
262
+ const at = `composeEnv[${index}]`;
263
+ if (!closedFields(c, at, item, ['name', 'member', 'key'], 'local'))
264
+ return { name: '', member: '', key: '' };
265
+ const value = item;
266
+ patternField(c, `${at}.name`, value.name, KEY);
267
+ return { name: String(value.name), ...ref(c, at, { member: value.member, key: value.key }) };
268
+ }),
269
+ grants: listOf(c, 'grants', entry.grants, (item, index) => {
270
+ const at = `grants[${index}]`;
271
+ if (!closedFields(c, at, item, ['name', 'keys'], 'local'))
272
+ return { name: '', keys: [] };
273
+ const value = item;
274
+ patternField(c, `${at}.name`, value.name, NAME);
275
+ const keys = strings(c, `${at}.keys`, value.keys, KEY);
276
+ if (keys.length === 0)
277
+ c.problem(`${at}.keys`, 'must list the keys the service reads');
278
+ return { name: String(value.name), keys };
279
+ }),
280
+ dirs: strings(c, 'dirs', entry.dirs, NAME_PATTERN),
281
+ custody: custodyOf(c, entry.custody),
282
+ setup: listOf(c, 'setup', entry.setup, (item, index) => command(c, `setup[${index}]`, item, COMMAND, names)),
283
+ seed: listOf(c, 'seed', entry.seed, (item, index) => command(c, `seed[${index}]`, item, SEED, names)),
284
+ processes: listOf(c, 'processes', entry.processes, (item, index) => {
285
+ const at = `processes[${index}]`;
286
+ const base = command(c, at, item, PROCESS, names);
287
+ const value = plain(item) ? item : {};
288
+ return { ...base, port: value.port === undefined ? null : String(value.port), health: value.health === undefined ? null : probe(c, `${at}.health`, { name: base.name || 'health', ...value.health }) };
289
+ }),
290
+ ready: listOf(c, 'ready', entry.ready, (item, index) => probe(c, `ready[${index}]`, item)),
291
+ verify: listOf(c, 'verify', entry.verify, (item, index) => probe(c, `verify[${index}]`, item)),
292
+ origins: listOf(c, 'origins', entry.origins, (item, index) => {
293
+ const at = `origins[${index}]`;
294
+ if (!closedFields(c, at, item, ['name', 'url'], 'local'))
295
+ return { name: '', url: '' };
296
+ const value = item;
297
+ patternField(c, `${at}.name`, value.name, NAME);
298
+ return { name: String(value.name), url: String(value.url ?? '') };
299
+ }),
300
+ resources: { diskGb: 10, ramMb: 4096 },
301
+ };
302
+ if (entry.resources !== undefined && closedFields(c, 'resources', entry.resources, ['diskGb', 'ramMb'], 'local')) {
303
+ const value = entry.resources;
304
+ for (const [key, floor] of [['diskGb', 1], ['ramMb', 512]]) {
305
+ if (value[key] === undefined)
306
+ continue;
307
+ if (!Number.isInteger(value[key]) || value[key] < floor)
308
+ c.problem(`resources.${key}`, `must be a whole number from ${floor}`);
309
+ else
310
+ block.resources[key] = value[key];
311
+ }
312
+ }
313
+ referencedPorts(c, block);
314
+ const generated = new Set(block.custody.flatMap((item) => [`${item.member}/${item.key}`, ...(item.publicKey ? [`${item.member}/${item.publicKey}`] : [])]));
315
+ if (generated.size < block.custody.length)
316
+ c.problem('custody', 'declares a key twice');
317
+ return c.problems.length === 0 ? { config: block, problems: [] } : { config: null, problems: c.problems };
318
+ }
319
+ /** Replaces `${port.<name>}` with the chosen host port. */
320
+ export const expandPorts = (value, ports) => value.replaceAll(/\$\{port\.([a-z][a-z0-9-]*)\}/gu, (_whole, name) => String(ports[name] ?? ''));
@@ -535,7 +535,7 @@ const checkStacksManifest = (manifest, model, { stacksRoot, environment, root, r
535
535
  const lite = repo.edition === 'lite';
536
536
  checkStacksTree({ stacksRoot, manifest, environmentNames, add, lite });
537
537
  checkKubernetesDeclaration(manifest, add);
538
- for (const issue of swarmInventoryIssues({ components: manifest.components, lite }))
538
+ for (const issue of swarmInventoryIssues({ components: manifest.components, lite, swarm: new Set(Object.values(object(manifest.environments) ? manifest.environments : {}).filter((spec) => spec?.runtime === 'docker-swarm').flatMap((spec) => Object.keys(object(spec.components) ? spec.components : {}))) }))
539
539
  add('HFS_STACKS_SHAPE', issue.at, issue.message);
540
540
  checkSelectedEnvironment({ environment, environmentNames, environments, stacksRoot, ids, root, model, manifest, lite, repo, add });
541
541
  };