@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
@@ -1,6 +1,6 @@
1
1
  // monorepo.mjs - every product is a monorepo, even a single app: the app root holds one package.json and lockfile whose npm
2
2
  // workspaces are the fe apps and packages (turbo runs their tasks), and the back end is a Nest monorepo of be/apps/<app>.
3
- // HFS_MONO_WORKSPACES the root package.json: `workspaces` exactly fe/apps/* and fe/packages/*, `packageManager` npm at an
3
+ // HFS_MONO_WORKSPACES the root package.json: `workspaces` exactly fe/apps/* and fe/packages/* (and packages/* once the app tracks a root workspace package, slot app.packages), `packageManager` npm at an
4
4
  // exact version, turbo a root devDependency (its version is the canon pin); turbo.json is app.task-graph
5
5
  // HFS_MONO_FE_WORKSPACE every fe app's package.json is @<project>/<app>, private, with exactly the workspace scripts; every
6
6
  // fe package's package.json is private with build and typecheck scripts and the workspace lint
@@ -22,6 +22,10 @@ const MONO_NEST_PROJECTS = 'HFS_MONO_NEST_PROJECTS';
22
22
  const MONO_WORKSPACE_DEP = 'HFS_MONO_WORKSPACE_DEP';
23
23
  /** The npm workspaces of every app, in this order. */
24
24
  export const WORKSPACES = Object.freeze(['fe/apps/*', 'fe/packages/*']);
25
+ /** The workspace of the app's own root packages (slot app.packages: packages/agent-sdk, packages/module-<name>), added after the fe ones once one is tracked. */
26
+ export const ROOT_PACKAGES_WORKSPACE = 'packages/*';
27
+ /** The workspaces the tracked tree `files` calls for: the fe ones, then `packages/*` when a root package tracks its package.json. */
28
+ export const workspacesFor = (files) => (files.some((file) => /^packages\/[^/]+\/package\.json$/.test(file)) ? [...WORKSPACES, ROOT_PACKAGES_WORKSPACE] : WORKSPACES);
25
29
  /** The scripts of every fe app workspace, exactly: turbo runs build, dev and typecheck; start serves a build. The lint is the root's (`starci app check --gate lint`), never a workspace script. */
26
30
  export const FE_APP_SCRIPTS = Object.freeze({ build: 'next build', dev: 'next dev', start: 'next start', typecheck: 'tsc --noEmit' });
27
31
  /** The scripts every fe package workspace has (its build and typecheck commands are its own); it has no lint script. */
@@ -49,13 +53,14 @@ function workspacesOf(files) {
49
53
  return [...folders].sort(byCodeUnit);
50
54
  }
51
55
  /** R143: the root package.json declares the workspaces, the package manager and turbo. */
52
- function rootFindings(repoRoot) {
56
+ function rootFindings(repoRoot, files) {
53
57
  const pkg = readJson(repoRoot, MANIFEST);
54
58
  if (!pkg)
55
59
  return [];
56
60
  const findings = [];
57
- if (!sameList(pkg.workspaces, WORKSPACES))
58
- findings.push(found(MONO_WORKSPACES, MANIFEST, `${MANIFEST} declares workspaces ${JSON.stringify(pkg.workspaces ?? null)}; every app is a monorepo whose npm workspaces are exactly ${JSON.stringify(WORKSPACES)} (each fe app and package has its own package.json; be has none).`, { workspaces: pkg.workspaces ?? null }));
61
+ const wanted = workspacesFor(files);
62
+ if (!sameList(pkg.workspaces, wanted))
63
+ findings.push(found(MONO_WORKSPACES, MANIFEST, `${MANIFEST} declares workspaces ${JSON.stringify(pkg.workspaces ?? null)}; every app is a monorepo whose npm workspaces are exactly ${JSON.stringify(wanted)} (each fe app and package has its own package.json; be has none; ${ROOT_PACKAGES_WORKSPACE} is listed when the app tracks a root package).`, { workspaces: pkg.workspaces ?? null }));
59
64
  if (!PACKAGE_MANAGER_FORM.test(String(pkg.packageManager ?? '')))
60
65
  findings.push(found(MONO_WORKSPACES, MANIFEST, `${MANIFEST} declares packageManager ${JSON.stringify(pkg.packageManager ?? null)}; turbo needs \`npm@<exact version>\` there.`, { packageManager: pkg.packageManager ?? null }));
61
66
  if (typeof pkg.devDependencies?.turbo !== 'string')
@@ -234,7 +239,7 @@ export function monorepoFindings({ repoRoot, files, repo }) {
234
239
  const workspaces = workspacesOf(files);
235
240
  const beApps = repo.sides.be.apps;
236
241
  return [
237
- ...rootFindings(repoRoot),
242
+ ...rootFindings(repoRoot, files),
238
243
  ...workspaceManifestFindings(repoRoot, repo.project, workspaces),
239
244
  ...nestFindings(repoRoot, files, beApps),
240
245
  ...workspaceDepFindings(repoRoot, files, workspaces),
@@ -8,6 +8,7 @@
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { FORBIDDEN_FILES, secretHitsInFile } from '../../lib/secret-patterns.mjs';
11
+ import { SECRETS_CACHE_FILE, secretRecordFaults } from '../../lib/secret-records.mjs';
11
12
  import { isSopsEnvelope } from '../../lib/sops-envelope.mjs';
12
13
  import { found, readText } from './read.mjs';
13
14
  const PLAINTEXT_SECRET = 'HFS_PLAINTEXT_SECRET';
@@ -28,6 +29,14 @@ export function untrackedSecretFindings({ repoRoot, files, repo }) {
28
29
  }
29
30
  return [found(PLAINTEXT_SECRET, 'secrets.env', 'secrets.env is an untracked plaintext secret at the lite app root; seal it with starci app secret set --env <env> into .starcistacks/<env>/secrets/<slug>.enc and remove the plaintext file; read materialized values by *_FILE', { level: 'warning', pattern: 'untracked-secrets-env' })];
30
31
  }
32
+ /** The findings of a file of the committed secrets cache: it is judged by what it holds (codes, paths, positions and digests, secret-records.mjs), never by the `secrets/` folder it sits in. */
33
+ function cacheRecordFindings(file, text) {
34
+ const faults = secretRecordFaults(text);
35
+ if (faults.length === 0)
36
+ return [];
37
+ const more = faults.length > 1 ? ` (and ${faults.length - 1} more)` : '';
38
+ return [found(PLAINTEXT_SECRET, file, `${file}: ${faults[0]}${more}; the secrets cache holds codes, paths, positions and digests only, so a file that holds more may carry secret material; regenerate it with starci app check --gate secrets, or migrate it with starci app codemod secrets-cache --execute`, { pattern: 'secrets-cache-record' })];
39
+ }
31
40
  /**
32
41
  * The findings of R06 for one tracked file that no forbidden slot claims: a secret by being (an env file, a key file), an `.enc` that is
33
42
  * no sops envelope, or a line that matches a secret pattern. `text` is the file's text, or null when it cannot be read (binary, too
@@ -39,6 +48,8 @@ export function secretFileFindings({ file, text, manifestText }) {
39
48
  return [found(PLAINTEXT_SECRET, file, `${file} is a plaintext secret by being (${being.name}); a secret is committed only as a sops envelope at .starcistacks/<env>/secrets/<slug>.enc`, { pattern: being.name })];
40
49
  if (text === null || text === undefined || NOT_SCANNED.test(file))
41
50
  return [];
51
+ if (SECRETS_CACHE_FILE.test(file))
52
+ return cacheRecordFindings(file, text);
42
53
  if (/\.enc(?:\.yaml)?$/.test(file))
43
54
  return isSopsEnvelope(text) ? [] : [found(PLAINTEXT_SECRET, file, `${file} is named like a sealed secret but is not a sops envelope; encrypt the value with sops and commit only the envelope`, { pattern: 'sops-not-envelope' })];
44
55
  const findings = [];
@@ -0,0 +1,7 @@
1
+ import type { Repo } from './rule-types.mjs';
2
+ /** Findings of the dev environments of the app root. */
3
+ export declare function stacksLocalFindings({ repoRoot, repo, isRoot }: {
4
+ repoRoot: string;
5
+ repo: Repo;
6
+ isRoot: boolean;
7
+ }): import("./rule-types.mjs").Finding[];
@@ -0,0 +1,71 @@
1
+ // stacks-local.mjs - R372 and R373: the dev environment of an app is declared runnable and its runbook names the harness verb.
2
+ // R372 STACKS_LOCAL_CONFIG_INVALID every docker-compose environment carries a `local` block that the dev verbs of `starci app stack`
3
+ // normalise (scripts/hfs/stack-local-config.mjs), and its compose services exist in the compose files;
4
+ // STACKS_DEV_UNDECLARED a lite app declares at least one docker-compose environment (the dev kind of PROJECT-STACKS-3).
5
+ // R373 STACKS_RUNBOOK_DOCKER_COMMAND the runbook of a docker-compose environment names `starci app stack up --env <env>` and carries no
6
+ // `docker build|run|compose|create|start|stop|pull|push` or `docker stack deploy` command (rules.md rule 8).
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { isPlainObject as plain } from '../../../engine/plain-object.mjs';
10
+ import { findStackDeclaration, readDeclaration, STACK_ROOT } from '../../lib/stack-declaration.mjs';
11
+ import { normalizeLocal } from '../stack-local-config.mjs';
12
+ import { found, readJson } from './read.mjs';
13
+ const DECLARATION = `${STACK_ROOT}/application-stacks.yaml`;
14
+ const ENV_NAME = /^[a-z0-9][a-z0-9-]*$/u;
15
+ const DOCKER_COMMAND = /^\s*(?:[$>]\s*)?(?:\w+=\S+\s+)*docker(?:-compose)?\s+(?:build|buildx|run|compose|create|start|stop|rm|pull|push|stack\s+deploy|stack\s+rm|service)\b/u;
16
+ const mapping = (value) => (plain(value) ? value : {});
17
+ function composeServices(envDir, spec) {
18
+ const names = new Set();
19
+ for (const relative of Array.isArray(spec.composeFiles) ? spec.composeFiles : []) {
20
+ if (typeof relative !== 'string' || path.isAbsolute(relative) || relative.split(/[\\/]/u).includes('..'))
21
+ continue;
22
+ for (const name of Object.keys(mapping(readDeclaration(path.join(envDir, relative)).doc?.services)))
23
+ names.add(name);
24
+ }
25
+ return names;
26
+ }
27
+ function localFindings({ repoRoot, project, environment, spec }) {
28
+ const at = `environments.${environment}.local`;
29
+ if (spec.local === undefined)
30
+ return [found('STACKS_LOCAL_CONFIG_INVALID', DECLARATION, `environments.${environment} runs docker-compose and declares no local block: declare the ports, services, processes and probes \`starci app stack up --env ${environment}\` runs`)];
31
+ const { config, problems } = normalizeLocal(spec.local, { project, env: environment });
32
+ const findings = problems.map((problem) => found('STACKS_LOCAL_CONFIG_INVALID', DECLARATION, `${at}.${problem.at}: ${problem.message}`));
33
+ if (config) {
34
+ const services = composeServices(path.join(repoRoot, STACK_ROOT, environment), spec);
35
+ for (const service of config.services)
36
+ if (!services.has(service.name))
37
+ findings.push(found('STACKS_LOCAL_CONFIG_INVALID', DECLARATION, `${at}.services: ${service.name} is not a service of the compose files of ${environment}`));
38
+ }
39
+ return findings;
40
+ }
41
+ function runbookFindings({ repoRoot, environment, spec }) {
42
+ const relative = typeof spec.runbook === 'string' ? spec.runbook : 'README.md';
43
+ const file = path.join(repoRoot, STACK_ROOT, environment, relative);
44
+ const display = path.relative(repoRoot, file).split(path.sep).join('/');
45
+ let text;
46
+ try {
47
+ text = fs.readFileSync(file, 'utf8');
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ const findings = text.split(/\r?\n/u).map((line, index) => ({ line, index })).filter(({ line }) => DOCKER_COMMAND.test(line) || /`docker\s+(?:build|run|compose|create|start|stop|pull|push|stack)\b/u.test(line))
53
+ .map(({ index }) => found('STACKS_RUNBOOK_DOCKER_COMMAND', `${display}:${index + 1}`, `the runbook of ${environment} carries a docker command; Docker is run by the harness: name \`starci app stack up --env ${environment}\` instead`));
54
+ if (!text.includes(`starci app stack up --env ${environment}`))
55
+ findings.push(found('STACKS_RUNBOOK_DOCKER_COMMAND', display, `the runbook of ${environment} does not name \`starci app stack up --env ${environment}\`, the one command that brings the environment up`));
56
+ return findings;
57
+ }
58
+ /** Findings of the dev environments of the app root. */
59
+ export function stacksLocalFindings({ repoRoot, repo, isRoot }) {
60
+ if (repo?.kind !== 'app' || !['lite', 'full'].some((value) => value === repo.edition) || isRoot === false)
61
+ return [];
62
+ const { doc } = findStackDeclaration(repoRoot);
63
+ if (!doc)
64
+ return [];
65
+ const project = String(readJson(repoRoot, 'hfs.json')?.project ?? 'app');
66
+ const judged = Object.entries(mapping(mapping(doc).environments)).filter(([environment, spec]) => ENV_NAME.test(environment) && plain(spec) && spec.runtime === 'docker-compose');
67
+ const findings = judged.flatMap(([environment, spec]) => [...localFindings({ repoRoot, project, environment, spec }), ...runbookFindings({ repoRoot, environment, spec })]);
68
+ if (repo.edition === 'lite' && judged.length === 0)
69
+ findings.push(found('STACKS_DEV_UNDECLARED', DECLARATION, 'a lite app declares its dev environment: an environment with runtime docker-compose and a local block, run by `starci app stack up --env dev` (PROJECT-STACKS-3, PROJECT-STACKS-6)'));
70
+ return findings;
71
+ }
@@ -99,6 +99,9 @@ function liteEnvironmentFindings({ doc, environments, deployments }) {
99
99
  for (const [environment, spec] of Object.entries(environments)) {
100
100
  if (!Array.isArray(spec?.composeFiles) || !spec.composeFiles.length)
101
101
  continue;
102
+ // The dev kind (PROJECT-STACKS-3): docker-compose sources under infra/compose/ with the local block `starci app stack up` runs; judged by R372.
103
+ if (spec.runtime === 'docker-compose' && spec.local !== undefined && spec.composeFiles.every((file) => typeof file === 'string' && file.startsWith('infra/compose/')))
104
+ continue;
102
105
  if (spec.runtime !== 'docker-swarm' || spec.composeFiles.some((file) => typeof file !== 'string' || !file.startsWith('infra/swarm/')))
103
106
  findings.push(found(CODE, DECLARATION, `${DECLARATION}: environments.${environment} lite delivery must use runtime: docker-swarm and infra/swarm sources`));
104
107
  }
@@ -111,11 +114,13 @@ function undeclaredSwarmFindings(deployments, environments, sources) {
111
114
  return found(CODE, file, `${file}: Swarm deployment ${reason}`);
112
115
  });
113
116
  }
117
+ /** The component ids a docker-swarm environment binds: the only ones the lite refusal of backing images judges (a dev compose environment runs its own MinIO, Redis and agent runtime). */
118
+ const swarmBoundComponents = (environments) => new Set(Object.values(environments).filter((spec) => spec?.runtime === 'docker-swarm').flatMap((spec) => Object.keys(mapping(spec.components))));
114
119
  /** Declared Swarm sources are checked for both editions; lite additionally refuses local managed backing images. */
115
120
  export function stacksRuntimeFindings({ repoRoot, files, doc, edition = 'full' }) {
116
121
  const findings = [], lite = edition === 'lite', components = mapping(doc?.components), environments = mapping(doc?.environments);
117
122
  const deployments = swarmDeploymentFiles(repoRoot, files), sources = new Set();
118
- findings.push(...swarmInventoryIssues({ components, lite }).map((issue) => issueFinding(DECLARATION, issue)));
123
+ findings.push(...swarmInventoryIssues({ components, lite, swarm: swarmBoundComponents(environments) }).map((issue) => issueFinding(DECLARATION, issue)));
119
124
  if (lite)
120
125
  findings.push(...liteEnvironmentFindings({ doc, environments, deployments }));
121
126
  for (const [environment, entry] of Object.entries(environments)) {
@@ -0,0 +1,12 @@
1
+ import { found } from './read.mjs';
2
+ import type { Repo } from './rule-types.mjs';
3
+ type Finding = ReturnType<typeof found>;
4
+ /** The fork repositories the harness owns, by service name. */
5
+ export declare function forkImages(): Record<string, string>;
6
+ /** Findings of R374 for the app root. */
7
+ export declare function stacksServiceImageFindings({ repoRoot, repo, isRoot }: {
8
+ repoRoot: string;
9
+ repo: Repo;
10
+ isRoot: boolean;
11
+ }): Finding[];
12
+ export {};
@@ -0,0 +1,120 @@
1
+ // stacks-service-images.mjs - R374: a customised service runs from an image the app builds, never from an upstream image with code mounted into it.
2
+ // STACKS_CODE_BIND_MOUNT a stack or compose file bind-mounts code, a script, a plugin or a writable repository path into a service container.
3
+ // Allowed: a named volume, a secret or config grant (not volumes), a bind under the data root (`${data}`, `${STACK_DATA_DIR}`) or a least grant (`${STACK_GRANT_<NAME>}`),
4
+ // the docker socket read-only, and a read-only plain data file (json, yaml, toml, conf, ini, properties, txt, pem, crt, csv).
5
+ // STACKS_IMAGE_BASE_UNPINNED a Dockerfile of `.starcistacks/images/<name>/` has a FROM that is not digest-pinned.
6
+ // STACKS_IMAGE_BASE_NOT_FORK the image folder of a service the harness owns a fork image for (knowledge/hfs/fork-images.yaml) does not end FROM that fork.
7
+ // STACKS_IMAGE_ENV_DIVERGES a service that has an image folder runs a literal third-party image in some environment: every environment runs the
8
+ // image built from the folder (compose `build.context`, or a project-owned or variable image reference), so dev and production share it.
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { isPlainObject as plain } from '../../../engine/plain-object.mjs';
12
+ import { parseYaml } from '../../../engine/yaml.mjs';
13
+ import { findStackDeclaration, readDeclaration, STACK_ROOT } from '../../lib/stack-declaration.mjs';
14
+ import { found, readJson } from './read.mjs';
15
+ const FORKS = new URL('../../../knowledge/hfs/fork-images.yaml', import.meta.url);
16
+ const ENV_NAME = /^[a-z0-9][a-z0-9-]*$/u;
17
+ const DATA_ROOT = /^(?:\$\{(?:data|STACK_DATA_DIR|STACK_GRANT_[A-Z0-9_]+)\}|\$(?:data|STACK_DATA_DIR|STACK_GRANT_[A-Z0-9_]+))(?:[\\/]|$)/u;
18
+ const DATA_FILE = /\.(?:json|ya?ml|toml|conf|cnf|cfg|ini|properties|txt|pem|crt|csv)$/iu;
19
+ const SOCKET = '/var/run/docker.sock';
20
+ const mapping = (value) => (plain(value) ? value : {});
21
+ /** The fork repositories the harness owns, by service name. */
22
+ export function forkImages() {
23
+ return mapping(mapping(parseYaml(fs.readFileSync(FORKS, 'utf8'))).images);
24
+ }
25
+ /** One volume entry as { source, readOnly } when it is a bind mount (a host path), else null. */
26
+ function bindOf(entry) {
27
+ if (typeof entry === 'string') {
28
+ const [source = '', , mode = ''] = entry.split(':');
29
+ if (!/^(?:\.|\/|~|\$\{|\$[A-Za-z])/u.test(source))
30
+ return null;
31
+ return { source, readOnly: mode.split(',').includes('ro') };
32
+ }
33
+ const long = mapping(entry);
34
+ if (long.type !== 'bind' || typeof long.source !== 'string')
35
+ return null;
36
+ return { source: long.source, readOnly: long.read_only === true };
37
+ }
38
+ /** Whether a bind mount is a data volume or plain read-only config data, the line R374 draws. */
39
+ const allowedBind = ({ source, readOnly }) => DATA_ROOT.test(source) || (readOnly && (source === SOCKET || DATA_FILE.test(source)));
40
+ const baseOf = (reference) => reference.replace(/@sha256:[0-9a-f]+$/iu, '').replace(/:[^/:]+$/u, '');
41
+ function bindFindings(display, environment, services) {
42
+ return Object.entries(services).flatMap(([name, service]) => (Array.isArray(mapping(service).volumes) ? mapping(service).volumes : [])
43
+ .map(bindOf).filter((bind) => bind !== null && !allowedBind(bind))
44
+ .map((bind) => found('STACKS_CODE_BIND_MOUNT', display, `${environment}: service ${name} bind-mounts ${bind.source}; code, plugins and scripts are baked into an image built from .starcistacks/images/${name}/ (FROM the pinned base, or the harness fork image), and only data volumes, secrets and read-only plain config data are mounted`)));
45
+ }
46
+ function dockerfileFindings(repoRoot, forks) {
47
+ const images = path.join(repoRoot, STACK_ROOT, 'images');
48
+ if (!fs.existsSync(images))
49
+ return [];
50
+ return fs.readdirSync(images, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
51
+ const display = `${STACK_ROOT}/images/${entry.name}/Dockerfile`;
52
+ let text;
53
+ try {
54
+ text = fs.readFileSync(path.join(images, entry.name, 'Dockerfile'), 'utf8');
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ const stages = new Set();
60
+ const bases = [];
61
+ const findings = [];
62
+ for (const line of text.split(/\r?\n/u)) {
63
+ const from = /^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)(?:\s+AS\s+(\S+))?/iu.exec(line);
64
+ if (!from)
65
+ continue;
66
+ const ref = from[1];
67
+ if (from[2])
68
+ stages.add(from[2].toLowerCase());
69
+ if (stages.has(ref.toLowerCase()) || ref === 'scratch')
70
+ continue;
71
+ bases.push(ref);
72
+ if (!/@sha256:[0-9a-f]{64}/iu.test(ref))
73
+ findings.push(found('STACKS_IMAGE_BASE_UNPINNED', display, `FROM ${ref} is not pinned by digest (name@sha256:...)`));
74
+ }
75
+ const fork = forks[entry.name];
76
+ const last = bases.at(-1);
77
+ if (fork !== undefined && last !== undefined && baseOf(last) !== fork)
78
+ findings.push(found('STACKS_IMAGE_BASE_NOT_FORK', display, `the harness owns the StarCi fork image ${fork} for ${entry.name}; the final stage must be FROM ${fork}@sha256:..., not ${baseOf(last)}`));
79
+ return findings;
80
+ });
81
+ }
82
+ /** Whether a service's image/build is the one built from its image folder, or a project-owned or variable reference. */
83
+ function builtFromFolder(service, name, project) {
84
+ const build = service.build;
85
+ const context = typeof build === 'string' ? build : typeof mapping(build).context === 'string' ? String(mapping(build).context) : '';
86
+ if (context !== '' && context.replaceAll('\\', '/').includes(`images/${name}`))
87
+ return true;
88
+ const image = typeof service.image === 'string' ? service.image : '';
89
+ return image.includes('${') || image.startsWith(`${project}/`) || image.startsWith(`${project}-`) || image.includes(`/${project}/`);
90
+ }
91
+ /** Findings of R374 for the app root. */
92
+ export function stacksServiceImageFindings({ repoRoot, repo, isRoot }) {
93
+ if (repo?.kind !== 'app' || !['lite', 'full'].some((value) => value === repo.edition) || isRoot === false)
94
+ return [];
95
+ const { doc } = findStackDeclaration(repoRoot);
96
+ if (!doc)
97
+ return [];
98
+ const project = String(readJson(repoRoot, 'hfs.json')?.project ?? 'app');
99
+ const folders = path.join(repoRoot, STACK_ROOT, 'images');
100
+ const withFolder = new Set(fs.existsSync(folders) ? fs.readdirSync(folders, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : []);
101
+ const findings = dockerfileFindings(repoRoot, forkImages());
102
+ for (const [environment, spec] of Object.entries(mapping(mapping(doc).environments))) {
103
+ if (!ENV_NAME.test(environment) || !plain(spec))
104
+ continue;
105
+ const files = Array.isArray(spec.composeFiles) ? spec.composeFiles : [];
106
+ for (const relative of files) {
107
+ if (typeof relative !== 'string' || path.isAbsolute(relative) || relative.split(/[\\/]/u).includes('..'))
108
+ continue;
109
+ const file = path.join(repoRoot, STACK_ROOT, environment, relative);
110
+ const display = path.relative(repoRoot, file).split(path.sep).join('/');
111
+ const services = mapping(readDeclaration(file).doc?.services);
112
+ findings.push(...bindFindings(display, environment, services));
113
+ for (const [name, service] of Object.entries(services)) {
114
+ if (withFolder.has(name) && !builtFromFolder(mapping(service), name, project))
115
+ findings.push(found('STACKS_IMAGE_ENV_DIVERGES', display, `${environment}: service ${name} has an image folder but runs ${String(mapping(service).image)}; every environment runs the image built from .starcistacks/images/${name}/ so dev and production share one image`));
116
+ }
117
+ }
118
+ }
119
+ return findings;
120
+ }
@@ -0,0 +1,11 @@
1
+ import type { SecretSops } from "./secret-sops.mjs";
2
+ import type { CustodyGenerator } from "./stack-local-config.mjs";
3
+ /** One generated value (or a private/public pair) by its kind; the `password` kind is URL- and SQL-safe. */
4
+ export declare function generateValues(entry: CustodyGenerator, random?: (bytes: number) => Buffer): Record<string, string>;
5
+ /** Seals the missing dev values of `environment`; a dry run unless `execute`. Returns the exit code. */
6
+ export declare function seedDevCustody(repoRoot: string, environment: string, { execute, sops, stdout, random }: {
7
+ execute: boolean;
8
+ sops: () => SecretSops;
9
+ stdout: (text: string) => unknown;
10
+ random?: (bytes: number) => Buffer;
11
+ }): number;
@@ -0,0 +1,79 @@
1
+ // secret-dev-init.mjs - `starci app secret init --env <dev env> [--execute]`: creates the dev-only values the environment's `local.custody`
2
+ // declares (PROJECT-STACKS-5: sealed only, owner-armed, created once, never replaced). It refuses an environment that is not a docker-compose
3
+ // dev environment (vps and production never get generated values), skips a key its member already holds, and refuses a half-present pair
4
+ // instead of minting a replacement. Values are generated in memory and sealed in one write per member; names are all it prints.
5
+ import fs from "node:fs";
6
+ import { generateKeyPairSync, randomBytes, randomUUID } from "node:crypto";
7
+ import { envelopeOf } from "./secret-document.mjs";
8
+ import { SecretError } from "./secret-errors.mjs";
9
+ import { fileOf, secretsDirectory, writeSecret } from "./secret-store.mjs";
10
+ import { declareKeys } from "./secret-standard.mjs";
11
+ import { isLocalEnvironment, loadLocalConfig } from "../../../stack/local-config.mjs";
12
+ /** One generated value (or a private/public pair) by its kind; the `password` kind is URL- and SQL-safe. */
13
+ export function generateValues(entry, random = randomBytes) {
14
+ if (entry.kind === "hex")
15
+ return { [entry.key]: random(entry.bytes).toString("hex") };
16
+ if (entry.kind === "base64")
17
+ return { [entry.key]: random(entry.bytes).toString("base64") };
18
+ if (entry.kind === "password")
19
+ return { [entry.key]: random(entry.bytes).toString("base64url") };
20
+ if (entry.kind === "uuid")
21
+ return { [entry.key]: randomUUID() };
22
+ if (entry.kind === "rsa-pem" || entry.kind === "ec-pem" || entry.kind === "ed25519-pem") {
23
+ const pair = entry.kind === "rsa-pem"
24
+ ? generateKeyPairSync("rsa", { modulusLength: 2048, privateKeyEncoding: { type: "pkcs8", format: "pem" }, publicKeyEncoding: { type: "spki", format: "pem" } })
25
+ : entry.kind === "ec-pem"
26
+ ? generateKeyPairSync("ec", { namedCurve: "P-256", privateKeyEncoding: { type: "pkcs8", format: "pem" }, publicKeyEncoding: { type: "spki", format: "pem" } })
27
+ : generateKeyPairSync("ed25519", { privateKeyEncoding: { type: "pkcs8", format: "pem" }, publicKeyEncoding: { type: "spki", format: "pem" } });
28
+ return { [entry.key]: pair.privateKey, ...(entry.publicKey ? { [entry.publicKey]: pair.publicKey } : {}) };
29
+ }
30
+ throw new SecretError(`the generator ${entry.kind} is unknown`);
31
+ }
32
+ /** Seals the missing dev values of `environment`; a dry run unless `execute`. Returns the exit code. */
33
+ export function seedDevCustody(repoRoot, environment, { execute, sops, stdout, random = randomBytes }) {
34
+ if (!isLocalEnvironment(repoRoot, environment))
35
+ throw new SecretError(`secret init --env ${environment}: only a docker-compose dev environment gets generated values; a vps or production value is set by the owner with secret set (PROJECT-STACKS-5)`);
36
+ const local = loadLocalConfig({ repoRoot, env: environment });
37
+ if (local.custody.length === 0)
38
+ throw new SecretError(`environment ${environment} declares no local.custody: nothing to generate; the owner seals its values with secret set`);
39
+ const directory = secretsDirectory(repoRoot, environment);
40
+ const byMember = new Map();
41
+ for (const entry of local.custody)
42
+ byMember.set(entry.member, [...(byMember.get(entry.member) ?? []), entry]);
43
+ const writes = [];
44
+ for (const [member, entries] of byMember) {
45
+ const file = fileOf(directory, member);
46
+ const held = fs.existsSync(file) ? envelopeOf(fs.readFileSync(file, "utf8")).keys : [];
47
+ const values = {};
48
+ for (const entry of entries) {
49
+ const names = [entry.key, ...(entry.publicKey ? [entry.publicKey] : [])];
50
+ const have = names.filter((name) => held.includes(name));
51
+ if (have.length === names.length) {
52
+ stdout(`kept ${environment}/${member} ${entry.key} (already sealed, never replaced)\n`);
53
+ continue;
54
+ }
55
+ if (have.length > 0)
56
+ throw new SecretError(`${environment}/${member} holds ${have.join(", ")} but not the rest of its pair ${names.join(", ")}: a replacement is never minted for an existing sealed value; the owner removes the pair with secret unset and runs init again`);
57
+ Object.assign(values, generateValues(entry, random));
58
+ }
59
+ if (Object.keys(values).length)
60
+ writes.push({ file, member, values });
61
+ }
62
+ for (const write of writes) {
63
+ const names = Object.keys(write.values);
64
+ if (!execute) {
65
+ stdout(`would seal ${environment}/${write.member} (${names.join(", ")})\n`);
66
+ continue;
67
+ }
68
+ writeSecret({ file: write.file, values: write.values, age: [], sops: sops() });
69
+ stdout(`sealed ${environment}/${write.member} (${names.join(", ")})\n`);
70
+ const declaration = declareKeys(repoRoot, environment, write.member, names);
71
+ if (declaration.declared.length)
72
+ stdout(`declared ${declaration.declared.join(", ")} in environments.${environment}.secrets of application-stacks.yaml\n`);
73
+ }
74
+ if (writes.length === 0)
75
+ stdout("nothing to create: every declared dev value is sealed\n");
76
+ else if (!execute)
77
+ stdout("dry run: nothing was written; arm it with --execute (the owner's yes)\n");
78
+ return 0;
79
+ }
@@ -1,4 +1,7 @@
1
+ /** A refusal with a message safe to print; `code` names it when a caller or a spec keys on it. */
1
2
  export declare class SecretError extends Error {
3
+ code: string | undefined;
4
+ constructor(message: string, code?: string);
2
5
  }
3
6
  export declare const publicStorage: <T>(action: () => T) => T;
4
7
  export declare function secretErrorMessage(caught: unknown): string;
@@ -1,11 +1,19 @@
1
1
  // secret-errors.mjs - sanitized public errors shared by app secret operations.
2
+ /** A refusal with a message safe to print; `code` names it when a caller or a spec keys on it. */
2
3
  export class SecretError extends Error {
4
+ code;
5
+ constructor(message, code) {
6
+ super(message);
7
+ this.code = code;
8
+ }
3
9
  }
4
10
  export const publicStorage = (action) => {
5
11
  try {
6
12
  return action();
7
13
  }
8
14
  catch (caught) {
15
+ if (caught instanceof SecretError)
16
+ throw caught;
9
17
  // reason: a thrown value is untyped; storage failures are Node errors carrying an optional code.
10
18
  const error = caught;
11
19
  throw new SecretError(error.code ? `private storage failed (${error.code})` : error.message);
@@ -72,6 +72,12 @@ export declare function decryptionIdentity(repoRoot: string, env: NodeJS.Process
72
72
  export declare function ageIdentityEnv(repoRoot: string, env: NodeJS.ProcessEnv, tools?: SecretToolOverrides): NodeJS.ProcessEnv;
73
73
  /** Select the first matching public age rule, including for SOPS's isolated CI identity path. */
74
74
  export declare function configuredRecipients(repoRoot: string, file: string): string[];
75
+ /**
76
+ * Whether sops itself finds a creation rule for `file`: the first rule whose path_regex matches the path as written (sops reads the file's own
77
+ * name, so a `.enc.yaml` member is not matched by a rule written for the legacy `.enc` spelling, which configuredRecipients tolerates when it
78
+ * only reads recipients). A rule with no path_regex matches everything.
79
+ */
80
+ export declare function sopsFindsRule(repoRoot: string, file: string): boolean;
75
81
  /** Resolve required tools without exposing subprocess output (which can contain private data). */
76
82
  export declare function secretTools(env: NodeJS.ProcessEnv, tools?: SecretToolOverrides): SecretTools;
77
83
  /**
@@ -154,6 +154,29 @@ export function configuredRecipients(repoRoot, file) {
154
154
  throw new Error(".sops.yaml must supply public age recipients for this secret; run starci app secret init");
155
155
  return recipients;
156
156
  }
157
+ /**
158
+ * Whether sops itself finds a creation rule for `file`: the first rule whose path_regex matches the path as written (sops reads the file's own
159
+ * name, so a `.enc.yaml` member is not matched by a rule written for the legacy `.enc` spelling, which configuredRecipients tolerates when it
160
+ * only reads recipients). A rule with no path_regex matches everything.
161
+ */
162
+ export function sopsFindsRule(repoRoot, file) {
163
+ let config; // reason: parsed YAML boundary, read from disk
164
+ try {
165
+ config = parseYaml(fs.readFileSync(path.join(repoRoot, ".sops.yaml"), "utf8"));
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ const relative = path.relative(repoRoot, file).split(path.sep).join("/");
171
+ return (config?.creation_rules ?? []).some((entry) => {
172
+ try {
173
+ return !entry.path_regex || new RegExp(entry.path_regex).test(relative) || new RegExp(entry.path_regex).test(relative.replaceAll("/", "\\"));
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ });
179
+ }
157
180
  /** Resolve required tools without exposing subprocess output (which can contain private data). */
158
181
  export function secretTools(env, tools = {}) {
159
182
  const sops = (tools.resolveSops ?? resolveSops)(env);
@@ -10,7 +10,7 @@ import { updateKeys as sopsUpdateKeys } from "../api/sops/update-keys.mjs";
10
10
  import { isLegacyName } from "../lib/sealed-member.mjs";
11
11
  import { invocationDir } from "../lib/roots.mjs";
12
12
  import { envelopeOf } from "./secret-document.mjs";
13
- import { configuredRecipients, decryptionIdentity, secretTools, } from "./secret-init.mjs";
13
+ import { configuredRecipients, sopsFindsRule, decryptionIdentity, secretTools, } from "./secret-init.mjs";
14
14
  import { outsideRepository } from "./secret-files.mjs";
15
15
  import { SecretError, publicStorage } from "./secret-errors.mjs";
16
16
  const sopsInvocation = Object.freeze({ runProgram, resolveRealTool });
@@ -83,9 +83,17 @@ function identityMissing(repoRoot, identity, file) {
83
83
  * the existing member is sealed to, so no rewrite (set, migrate, split, an edit) drops a recipient such as the deploy server's.
84
84
  * `ruled` says whether a rule of the app's .sops.yaml matches the file. Refused, as the rule refuses, when none names one.
85
85
  */
86
+ /** The refusal for a member no creation rule of `.sops.yaml` reaches, naming the path and the way to the per-environment rules. */
87
+ function unruledMessage(repoRoot, file) {
88
+ const relative = path.relative(repoRoot, file).split(path.sep).join("/");
89
+ const environment = /^\.starcistacks\/([^/]+)\//.exec(relative)?.[1] ?? "<env>";
90
+ return `SECRET_SOPS_RULE_MISSING: no creation rule of .sops.yaml matches ${relative} (a rule written for the legacy .enc spelling does not match .enc.yaml), so sops cannot seal it; run starci app secret init --env ${environment}, which writes the per-environment rules, or starci app codemod sops-rules --execute to migrate the file`;
91
+ }
86
92
  function sealRecipients(repoRoot, file, named) {
87
93
  try {
88
94
  const rule = configuredRecipients(repoRoot, file);
95
+ if (!sopsFindsRule(repoRoot, file))
96
+ throw new SecretError(unruledMessage(repoRoot, file), "SECRET_SOPS_RULE_MISSING");
89
97
  return { recipients: named.length ? named : rule, ruled: true };
90
98
  }
91
99
  catch (error) {
@@ -59,6 +59,25 @@ export declare function sshTransportMembers(spec: EnvironmentSpec | undefined):
59
59
  * RECIPIENT_DRIFT, UNDECLARED_KEY (only where application-stacks.yaml declares the environment), UNTRACKED. Names only.
60
60
  */
61
61
  export declare function auditSecrets(repoRoot: string, environment?: string | null): AuditFinding[];
62
+ /** What `secret set` did with a member that is a service credential (tooling, never an app runtime secret): the keys it bound, or the remedy to apply by hand. */
63
+ export interface ToolingOutcome {
64
+ bound: string[];
65
+ remedy: string;
66
+ }
67
+ /** One sealed tooling member `bindToolingMembers` judged: what it bound, the remedy when it could not, and the runtime entries an earlier `secret set` declared for it. */
68
+ export interface ToolingMember {
69
+ environment: string;
70
+ member: string;
71
+ service: string;
72
+ outcome: ToolingOutcome;
73
+ runtimeEntries: string[];
74
+ }
75
+ /**
76
+ * 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
77
+ * 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
78
+ * the member are named, not removed: the owner deletes them (CI custody refuses the token while one stands).
79
+ */
80
+ export declare function bindToolingMembers(repoRoot: string, write: boolean): ToolingMember[];
62
81
  /**
63
82
  * Declares each of `keys` of `member` in environment `environment`'s `secrets:` of application-stacks.yaml, one entry per key
64
83
  * (`name: <kebab of the key>, member, key, where: secrets/<member>`), so the key is materialized as its own `<KEY>_FILE`. A key
@@ -68,6 +87,7 @@ export declare function auditSecrets(repoRoot: string, environment?: string | nu
68
87
  export declare function declareKeys(repoRoot: string, environment: string, member: string, keys: string[]): {
69
88
  declared: string[];
70
89
  byHand: string[];
90
+ tooling?: ToolingOutcome;
71
91
  };
72
92
  /**
73
93
  * Records `recipient` as `environments.<environment>.deploy.remote.serverRecipient` of application-stacks.yaml: the public age