@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
@@ -43,3 +43,26 @@ export declare const declaredHfsView: ({ runtimeRoot, declaration, repoRoot, sid
43
43
  repoRoot: string;
44
44
  side: string;
45
45
  }) => HfsView;
46
+ /**
47
+ * The view of the root packages of the app at `repoRoot` (the app root, where the root `eslint.config.mjs` lives), for the laws of `side`.
48
+ *
49
+ * @param {{ runtimeRoot: string, repoRoot: string, side: string }} input - Where the manifest copy lives, the app root, and the side whose laws apply.
50
+ * @returns {object} The frozen view.
51
+ */
52
+ export declare const openPackagesHfsView: ({ runtimeRoot, repoRoot, side }: {
53
+ runtimeRoot: string;
54
+ repoRoot: string;
55
+ side: "be" | "fe";
56
+ }) => HfsView;
57
+ /**
58
+ * The view of the root packages of an in-memory app declaration (rule tests): no file is read except the manifest.
59
+ *
60
+ * @param {{ runtimeRoot: string, declaration: object, repoRoot: string, side: string }} input - Manifest root, the app hfs.json object, the app root and the side.
61
+ * @returns {object} The frozen view.
62
+ */
63
+ export declare const declaredPackagesHfsView: ({ runtimeRoot, declaration, repoRoot, side }: {
64
+ runtimeRoot: string;
65
+ declaration: Parsed;
66
+ repoRoot: string;
67
+ side: "be" | "fe";
68
+ }) => HfsView;
@@ -5,6 +5,7 @@
5
5
  import path from 'node:path';
6
6
  import { allowsFile } from './allows.mjs';
7
7
  import { openHfs } from './slots.mjs';
8
+ import { ROOT_PACKAGE_SLOTS } from './architecture/root-packages.mjs';
8
9
  const posix = (p) => String(p).replaceAll('\\', '/');
9
10
  const isAbsoluteAny = (text) => path.isAbsolute(text) || /^[A-Za-z]:\//.test(text);
10
11
  /**
@@ -69,3 +70,70 @@ export const openHfsView = ({ runtimeRoot, repoRoot }) => hfsView(openHfs({ root
69
70
  * @returns {object} The frozen view.
70
71
  */
71
72
  export const declaredHfsView = ({ runtimeRoot, declaration, repoRoot, side }) => hfsView(openHfs({ root: runtimeRoot, declaration, side }), repoRoot);
73
+ /**
74
+ * The view of the root workspace packages of an app (`packages/<pkg>/src/**`, slots app.packages and app.module-registry), as the lint of one
75
+ * side sees them: the rule parameters, the apps and the Grammar of `side` (a package is judged by the laws of the side whose subpath a file
76
+ * sits in), and the paths of the APP ROOT (`repoRoot` is the app root, not a side folder). The slots answer for the root paths; a file of a
77
+ * package is of the tier `package` and has no owner: the owner laws (one `index.ts` per owner root) are the module folders', a package's public
78
+ * surface being the `exports` of its manifest.
79
+ *
80
+ * @param {object} opened - The result of `openHfs` for the app root (profile app, both sides resolved).
81
+ * @param {string} repoRoot - The absolute app root.
82
+ * @param {string} side - `be` or `fe`: the side whose laws the file is judged by.
83
+ * @returns {object} The frozen view: the profile of the side over the paths of the app root.
84
+ */
85
+ function packagesView(opened, repoRoot, side) {
86
+ const scope = opened.sides?.[side];
87
+ const sideRepo = opened.repo.sides?.[side];
88
+ if (scope === undefined || sideRepo === undefined)
89
+ throw new Error('the view of the root packages needs the declaration of an app with both sides');
90
+ const rel = (file) => {
91
+ const text = posix(file);
92
+ return isAbsoluteAny(text) ? posix(path.relative(repoRoot, file)) : text.replace(/^\.\//, '');
93
+ };
94
+ const cache = new Map();
95
+ const classify = (file) => {
96
+ const key = rel(file);
97
+ if (!cache.has(key))
98
+ cache.set(key, opened.classifyPath(key));
99
+ return cache.get(key);
100
+ };
101
+ const packageOf = (file) => {
102
+ const slot = classify(file).slot;
103
+ const relative = rel(file);
104
+ return slot !== undefined && ROOT_PACKAGE_SLOTS.has(slot) ? relative.split('/').slice(0, 2).join('/') : null;
105
+ };
106
+ return Object.freeze({
107
+ repoRoot,
108
+ profile: side,
109
+ side,
110
+ edition: opened.repo.edition ?? 'full',
111
+ ...(sideRepo.grammar === undefined ? {} : { grammar: sideRepo.grammar }),
112
+ apps: sideRepo.apps,
113
+ connections: sideRepo.connections,
114
+ patterns: Object.freeze([...(sideRepo.patterns ?? [])]),
115
+ ruleParams: scope.ruleParams(),
116
+ relative: rel,
117
+ classify,
118
+ slotOf: (file) => classify(file).slot ?? null,
119
+ tierOf: (file) => (packageOf(file) === null ? null : 'package'),
120
+ // A package is no owner of the module laws (one index.ts at the owner root): its public surface is the `exports` of its manifest, so no file of it has an owner.
121
+ ownerOf: () => null,
122
+ allows: (file) => allowsFile(opened, rel(file)),
123
+ slot: (id) => opened.slot(id),
124
+ });
125
+ }
126
+ /**
127
+ * The view of the root packages of the app at `repoRoot` (the app root, where the root `eslint.config.mjs` lives), for the laws of `side`.
128
+ *
129
+ * @param {{ runtimeRoot: string, repoRoot: string, side: string }} input - Where the manifest copy lives, the app root, and the side whose laws apply.
130
+ * @returns {object} The frozen view.
131
+ */
132
+ export const openPackagesHfsView = ({ runtimeRoot, repoRoot, side }) => packagesView(openHfs({ root: runtimeRoot, repoRoot }), repoRoot, side);
133
+ /**
134
+ * The view of the root packages of an in-memory app declaration (rule tests): no file is read except the manifest.
135
+ *
136
+ * @param {{ runtimeRoot: string, declaration: object, repoRoot: string, side: string }} input - Manifest root, the app hfs.json object, the app root and the side.
137
+ * @returns {object} The frozen view.
138
+ */
139
+ export const declaredPackagesHfsView = ({ runtimeRoot, declaration, repoRoot, side }) => packagesView(openHfs({ root: runtimeRoot, declaration }), repoRoot, side);
@@ -26,6 +26,10 @@ export declare const DECLARATION: RegExp;
26
26
  export declare const TYPESCRIPT: RegExp;
27
27
  /** Whether `file` is a TypeScript source (not a declaration file) the declaration emit reads. */
28
28
  export declare const isEmittableSource: (file: string) => boolean;
29
+ /** The package whose module-definition helpers return a type they state in their own declaration (R368 accepts a destructured export of their call). */
30
+ export declare const STATED_SURFACE_PACKAGE = "@starci/nest-kit";
31
+ /** The helpers of that package: each is called with the options type as a type argument and returns the explicitly typed definition. */
32
+ export declare const STATED_SURFACE_HELPERS: ReadonlySet<string>;
29
33
  /**
30
34
  * The declaration emit of one TypeScript source and the exports it could not write explicitly (a type inferred from bodies and other files), or null
31
35
  * when the compiler has no `transpileDeclaration` (before TypeScript 5.5). A declaration whose only diagnostics are components returning JSX has the
@@ -55,6 +55,52 @@ function returnsOnlyJsx(ts, source, start) {
55
55
  ts.forEachChild(fn.body, visit);
56
56
  return only;
57
57
  }
58
+ /** The package whose module-definition helpers return a type they state in their own declaration (R368 accepts a destructured export of their call). */
59
+ export const STATED_SURFACE_PACKAGE = '@starci/nest-kit';
60
+ /** The helpers of that package: each is called with the options type as a type argument and returns the explicitly typed definition. */
61
+ export const STATED_SURFACE_HELPERS = new Set(['defineConfigurableModule', 'defineCapabilityModule']);
62
+ /** The local names under which `source` imports a stated-surface helper from its package (`import { defineConfigurableModule as define }` gives `define`). */
63
+ function statedHelperNames(ts, source) {
64
+ const names = new Set();
65
+ for (const statement of source.statements) {
66
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== STATED_SURFACE_PACKAGE)
67
+ continue;
68
+ const bindings = statement.importClause?.namedBindings;
69
+ if (bindings === undefined || !ts.isNamedImports(bindings))
70
+ continue;
71
+ for (const element of bindings.elements)
72
+ if (STATED_SURFACE_HELPERS.has((element.propertyName ?? element.name).text))
73
+ names.add(element.name.text);
74
+ }
75
+ return names;
76
+ }
77
+ /**
78
+ * Whether the destructured export the diagnostic at `start` points at is `export const { A, B } = helper<Options>()` for a stated-surface helper:
79
+ * each name has the type the helper's declaration gives it, written there, so the file states its surface by naming the helper and the options
80
+ * type, exactly as `export const A: Host["A"] = built.A` states it through `Host`.
81
+ */
82
+ function statesSurfaceThroughHelper(ts, source, start) {
83
+ const helpers = statedHelperNames(ts, source);
84
+ if (helpers.size === 0)
85
+ return null;
86
+ let innermost = source;
87
+ const descend = (node) => {
88
+ if (start < node.getFullStart() || start >= node.end)
89
+ return;
90
+ innermost = node;
91
+ ts.forEachChild(node, descend);
92
+ };
93
+ ts.forEachChild(source, descend);
94
+ for (let node = innermost; node !== undefined && node !== source; node = node.parent) {
95
+ if (!ts.isVariableDeclaration(node))
96
+ continue;
97
+ const call = node.initializer;
98
+ const stated = ts.isObjectBindingPattern(node.name) && call !== undefined && ts.isCallExpression(call) && ts.isIdentifier(call.expression) && helpers.has(call.expression.text)
99
+ && (call.typeArguments?.length ?? 0) > 0 && node.name.elements.every((element) => ts.isIdentifier(element.name) && element.dotDotDotToken === undefined);
100
+ return stated && call !== undefined ? call.getText(source) : null;
101
+ }
102
+ return null;
103
+ }
58
104
  /** What to annotate for each isolated-declaration diagnostic code; any other code asks for the exported value's type. */
59
105
  const ANNOTATE = new Map([
60
106
  [9007, 'return type'], [9008, 'return type'], [9009, 'accessor type'], [9010, 'variable type'], [9011, 'parameter type'], [9012, 'property type'],
@@ -98,7 +144,14 @@ export function declarationSurface(ts, file, text, module) {
98
144
  return { outputText: declared.outputText, problems: [] };
99
145
  const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, file.endsWith('x') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
100
146
  const problems = [];
147
+ // The emit writes `any` for a destructured export; the call that states its type joins the digest, so a change of the options type reaches the importers.
148
+ const stated = new Set();
101
149
  for (const diagnostic of diagnostics) {
150
+ const call = diagnostic.code === 9019 && diagnostic.start !== undefined ? statesSurfaceThroughHelper(ts, source, diagnostic.start) : null;
151
+ if (call !== null) {
152
+ stated.add(call);
153
+ continue;
154
+ }
102
155
  const exempt = (diagnostic.code === 9007 || diagnostic.code === 9008) && diagnostic.start !== undefined && returnsOnlyJsx(ts, source, diagnostic.start);
103
156
  if (exempt)
104
157
  continue;
@@ -108,5 +161,6 @@ export function declarationSurface(ts, file, text, module) {
108
161
  reason: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n').split('\n')[0] ?? '',
109
162
  });
110
163
  }
111
- return { outputText: declared.outputText, problems };
164
+ const outputText = stated.size === 0 ? declared.outputText : `${declared.outputText}${[...stated].sort().map((call) => `// stated by ${call}\n`).join('')}`;
165
+ return { outputText, problems };
112
166
  }
@@ -0,0 +1,13 @@
1
+ /** The cache folder of the secrets family, relative to the cache directory. */
2
+ export declare const SECRETS_FOLDER = "check/secrets";
3
+ /** A file of the secrets folder in a tracked tree: `.starcistacks/cache/check/secrets/<area>.jsonl` (an area is a folder path, so the file may sit in subfolders), at the app root or below one. */
4
+ export declare const SECRETS_CACHE_FILE: RegExp;
5
+ /** The finding codes of the secrets family: plaintext secrets, sealed custody, untracked secret files. */
6
+ export declare const SECRET_CODES: ReadonlySet<string>;
7
+ /** The stored form of a finding message: the digest of the message. A message already in that form is kept. */
8
+ export declare function messageDigest(message: string): string;
9
+ /**
10
+ * The faults of the text of a file of the secrets folder: every line is a record that holds codes, paths, positions and digests, and nothing
11
+ * else. An empty list means the file carries no secret material by construction.
12
+ */
13
+ export declare function secretRecordFaults(text: string): string[];
@@ -0,0 +1,83 @@
1
+ // secret-records.mjs - the contract of the secrets family's records in the committed check cache (contexts/project/check-cache.md, "The secrets
2
+ // records"). The cache is committed, so what the secrets family writes may never be secret material: a record holds codes, paths, positions
3
+ // and hashes. The finding's rule is one of the family's codes, its message is the sha256 prefix of the message the run printed (the identity
4
+ // the new / known / fixed comparison needs), and the record carries no `data`. The cache writer (hfs cache/secret-records.mjs) projects its
5
+ // records to this form; the secret judgement of R06 (scripts/hfs/rules/secrets.mjs, shared by `starci app check` and the commit hook) judges a
6
+ // staged or tracked file of the folder by `secretRecordFaults`, so the file is accepted by what it holds and never by where it sits.
7
+ import { createHash } from 'node:crypto';
8
+ /** The cache folder of the secrets family, relative to the cache directory. */
9
+ export const SECRETS_FOLDER = 'check/secrets';
10
+ /** A file of the secrets folder in a tracked tree: `.starcistacks/cache/check/secrets/<area>.jsonl` (an area is a folder path, so the file may sit in subfolders), at the app root or below one. */
11
+ export const SECRETS_CACHE_FILE = /(?:^|\/)\.starcistacks\/cache\/check\/secrets\/.+\.jsonl$/;
12
+ /** The finding codes of the secrets family: plaintext secrets, sealed custody, untracked secret files. */
13
+ export const SECRET_CODES = new Set([
14
+ 'HFS_PLAINTEXT_SECRET', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_LITE_SECRET_CUSTODY', 'HFS_IDENTITY_CUSTODY',
15
+ 'HFS_AGE_KEY_CUSTODY', 'RT_SECRET_TRACKED', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'SEALED_CUSTODY_LOCATION',
16
+ 'APP_SECRET_IN_CI', 'CI_SECRET_SEALED', 'BE_LOG_SECRET', 'BE_SECRET_DEFAULT', 'HFS_DOCKER_SECRETS',
17
+ ]);
18
+ /** The form a stored message takes: `sha256:` and the first 16 hex digits of the sha256 of the message. */
19
+ const DIGEST = /^sha256:[0-9a-f]{16}$/;
20
+ /** A path a record may name: a relative posix path without control characters, backslashes or colons, never absolute and never leaving the app. */
21
+ const SAFE_PATH = /^(?!\/)(?!(?:.*\/)?\.\.(?:\/|$))[^\u0000-\u001f\\:]*$/u;
22
+ /** The stored form of a finding message: the digest of the message. A message already in that form is kept. */
23
+ export function messageDigest(message) {
24
+ return DIGEST.test(message) ? message : `sha256:${createHash('sha256').update(message).digest('hex').slice(0, 16)}`;
25
+ }
26
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
27
+ const isPath = (value) => typeof value === 'string' && SAFE_PATH.test(value);
28
+ /** The faults of one parsed record of the secrets folder; an empty list is a record that carries no secret material. */
29
+ function recordFaults(value, at) {
30
+ if (!isObject(value))
31
+ return [`${at} is not a record`];
32
+ const faults = [];
33
+ const extra = Object.keys(value).filter((key) => !['path', 'step', 'hash', 'findings'].includes(key));
34
+ if (extra.length > 0)
35
+ faults.push(`${at} carries ${extra.join(', ')}; a secrets record holds path, step, hash and findings only`);
36
+ if (!isPath(value['path']))
37
+ faults.push(`${at} path is not a relative posix path`);
38
+ if (value['step'] !== 'secrets')
39
+ faults.push(`${at} step is not "secrets"`);
40
+ if (typeof value['hash'] !== 'string' || !/^(?:[0-9a-f]{64}|absent)$/.test(value['hash']))
41
+ faults.push(`${at} hash is not a sha256`);
42
+ const findings = value['findings'];
43
+ if (!Array.isArray(findings))
44
+ return [...faults, `${at} findings is not a list`];
45
+ for (const finding of findings) {
46
+ if (!isObject(finding)) {
47
+ faults.push(`${at} holds a finding that is not an object`);
48
+ continue;
49
+ }
50
+ const keys = Object.keys(finding).sort().join(',');
51
+ if (keys !== 'column,line,message,path,rule')
52
+ faults.push(`${at} holds a finding with the keys ${keys}; a finding holds path, line, column, rule and message only`);
53
+ if (typeof finding['rule'] !== 'string' || !SECRET_CODES.has(finding['rule']))
54
+ faults.push(`${at} holds a finding whose rule is not a code of the secrets family`);
55
+ if (typeof finding['message'] !== 'string' || !DIGEST.test(finding['message']))
56
+ faults.push(`${at} holds a finding whose message is not a digest (sha256:<16 hex>)`);
57
+ if (!isPath(finding['path']))
58
+ faults.push(`${at} holds a finding whose path is not a relative posix path`);
59
+ if (!Number.isInteger(finding['line']) || !Number.isInteger(finding['column']))
60
+ faults.push(`${at} holds a finding whose position is not a pair of integers`);
61
+ }
62
+ return faults;
63
+ }
64
+ /**
65
+ * The faults of the text of a file of the secrets folder: every line is a record that holds codes, paths, positions and digests, and nothing
66
+ * else. An empty list means the file carries no secret material by construction.
67
+ */
68
+ export function secretRecordFaults(text) {
69
+ const faults = [];
70
+ text.split('\n').forEach((raw, index) => {
71
+ const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
72
+ if (line.trim() === '')
73
+ return;
74
+ const at = `line ${index + 1}`;
75
+ try {
76
+ faults.push(...recordFaults(JSON.parse(line), at));
77
+ }
78
+ catch {
79
+ faults.push(`${at} is not JSON`);
80
+ }
81
+ });
82
+ return faults;
83
+ }
@@ -10,9 +10,10 @@ export declare function composeIncludePaths(doc: Mapping): {
10
10
  errors: string[];
11
11
  };
12
12
  /** Inventory rules are independent of a selected environment or rendered deployment model. */
13
- export declare function swarmInventoryIssues({ components, lite }: {
13
+ export declare function swarmInventoryIssues({ components, lite, swarm }: {
14
14
  components: unknown;
15
15
  lite?: boolean;
16
+ swarm?: ReadonlySet<string>;
16
17
  }): SwarmIssue[];
17
18
  /** Validate the same deployment model in HFS and the deployment evidence gate. */
18
19
  export declare function swarmModelIssues({ model, components, bindings, lite }: {
@@ -49,12 +49,12 @@ function jobDeclarationIssues(id, component, components, add) {
49
49
  add(`components.${id}.schedule`, 'replicated-job must declare its cron schedule');
50
50
  }
51
51
  /** Inventory rules are independent of a selected environment or rendered deployment model. */
52
- export function swarmInventoryIssues({ components, lite = false }) {
52
+ export function swarmInventoryIssues({ components, lite = false, swarm }) {
53
53
  const errors = [], catalog = mapping(components);
54
54
  const add = (at, message) => { errors.push({ at, message }); };
55
55
  for (const [id, entry] of Object.entries(catalog)) {
56
56
  const component = mapping(entry);
57
- if (lite && present(component.image) && BACKING_IMAGE.test(repositoryOf(component.image)))
57
+ if (lite && (swarm === undefined || swarm.has(id)) && present(component.image) && BACKING_IMAGE.test(repositoryOf(component.image)))
58
58
  add(`components.${id}.image`, 'lite runs no backing image; its db component runs the image .starcistacks/images/db/Dockerfile builds, storage is the external S3 endpoint, identity the API');
59
59
  jobDeclarationIssues(id, component, catalog, add);
60
60
  }
@@ -218,8 +218,8 @@ export function addLiteWebhookInbox({ root, repo, noun, provider, values, now =
218
218
  const hasHandoffStorage = migrations.some((file) => /create\s+function\s+public\.event_bus_send\b/i.test(fs.readFileSync(path.join(migrationsDir, file), "utf8")));
219
219
  if (!hasHandoffStorage)
220
220
  planned[0].text = `${template("platform/migration.sql.tpl", renderValues)}\n${template("platform/client-operation-outcomes.sql.tpl", renderValues)}\n${planned[0].text}`; // the migration is the first planned file
221
- // The sql tag and the database-function call (with its spec) the inbox and the event bus use; the lite starter leaves them out.
222
- for (const file of ["database.sql.ts", "database.client.ts", "database.client.spec.ts"]) {
221
+ // The sql tag the inbox and the event bus use; the lite starter leaves it out.
222
+ for (const file of ["database.sql.ts"]) {
223
223
  const relative = `be/src/modules/platform/database/${file}`;
224
224
  if (fs.existsSync(path.join(root, ...relative.split("/"))))
225
225
  continue;
@@ -0,0 +1,16 @@
1
+ /** What a module verb answers: every app-relative path it wrote, rewrote or deleted. */
2
+ export interface ModuleResult {
3
+ readonly created: string[];
4
+ readonly updated: string[];
5
+ readonly removed: string[];
6
+ }
7
+ /** Adds the module package `name`, registers it and re-renders the registry. Refused when the app has no agent SDK package or the module exists. */
8
+ export declare function addModule({ root, name }: {
9
+ root: string;
10
+ name: string;
11
+ }): ModuleResult;
12
+ /** Removes the module package `name`, its registrations and its entry in the registry; the last module takes the registry package with it. */
13
+ export declare function removeModule({ root, name }: {
14
+ root: string;
15
+ name: string;
16
+ }): ModuleResult;
@@ -0,0 +1,232 @@
1
+ // starci app add module <name> / starci app remove module <name> - a module package of the product's agent SDK and its registry.
2
+ //
3
+ // A module is a workspace package `packages/module-<name>` (`@<project>/module-<name>`) built on the product's agent SDK (`packages/agent-sdk`): its
4
+ // template and workbench declaration (React-free, for the back end), and its workbench (drawn with Grammar alone). `add` writes the package from
5
+ // templates/app/module, registers it (root `workspaces`, the app dependencies on the registry and the module) and re-renders the managed registry
6
+ // package `packages/modules` (module-registry.mts, the same render `starci app sync` checks); `remove` reverses every one of those steps.
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { TEMPLATES_DIR } from '../sync/index.mjs';
10
+ import { readRepoDeclaration, loadSlotManifest } from '../runtime/scripts/hfs/slots.mjs';
11
+ import { byCodeUnit } from '../report/order.mjs';
12
+ import { ScaffoldError, pascalOf, camelOf } from './service.mjs';
13
+ import { packageJsonText } from './app.mjs';
14
+ import { MODULE_PREFIX, REGISTRY_DIR, SDK_DIR, moduleNames, registryFiles, upperOf } from './module-registry.mjs';
15
+ const NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
16
+ const TEMPLATE_DIR = path.join(TEMPLATES_DIR, 'app', 'module');
17
+ const WORKSPACE_GLOB = 'packages/*';
18
+ const read = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
19
+ const sortedDependencies = (dependencies) => Object.fromEntries(Object.entries(dependencies).sort(([left], [right]) => byCodeUnit(left, right)));
20
+ const listFiles = (dir, base = dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
21
+ const full = path.join(dir, entry.name);
22
+ return entry.isDirectory() ? listFiles(full, base) : [path.relative(base, full).split(path.sep).join('/')];
23
+ });
24
+ /** A template's text with `{{project}}`, `{{name}}`, `{{pascal}}`, `{{camel}}` and `{{upper}}` filled; any other placeholder is a defect of the template. */
25
+ function fill(text, values, file) {
26
+ return text.replace(/\{\{([A-Za-z]\w*)\}\}/g, (whole, key) => {
27
+ const value = values[key];
28
+ if (value === undefined)
29
+ throw new ScaffoldError('HFS_ADD_PLACEHOLDER_UNKNOWN', `module template ${file} uses ${whole}, which add module does not provide`);
30
+ return value;
31
+ }).replace(/\r\n/g, '\n');
32
+ }
33
+ /** The package files written from code: the manifests are never template files. */
34
+ function manifests(project, name, versions) {
35
+ const sdk = `@${project}/agent-sdk`;
36
+ const exports = {
37
+ '.': { types: './dist/index.d.ts', default: './dist/index.js' },
38
+ './template': { types: './dist/template/index.d.ts', default: './dist/template/index.js' },
39
+ './manifest': { types: './dist/manifest/index.d.ts', default: './dist/manifest/index.js' },
40
+ './workbench': { types: './dist/esm/workbench/index.d.ts', default: './dist/esm/workbench/index.js' },
41
+ './public': { types: './dist/esm/public/index.d.ts', default: './dist/esm/public/index.js' },
42
+ };
43
+ const packageJson = {
44
+ name: `@${project}/${MODULE_PREFIX}${name}`,
45
+ version: '0.0.0',
46
+ private: true,
47
+ description: `The ${pascalOf(name)} module on the ${project} agent SDK: its template (./template) and workbench declaration (./manifest), both React-free for the back end, its Grammar-only workbench (./workbench) and its public face (./public) for the front end.`,
48
+ exports,
49
+ scripts: { build: 'tsc -p tsconfig.build.json && tsc -p tsconfig.build.workbench.json', typecheck: 'tsc --noEmit --pretty false', test: 'jest --coverage' },
50
+ dependencies: { [sdk]: '*' },
51
+ peerDependencies: { '@starci/grammar': versions['@starci/grammar'], react: versions['react'] },
52
+ devDependencies: sortedDependencies({ '@types/jest': versions['@types/jest'] ?? '', '@types/react': versions['@types/react'] ?? '', jest: versions['jest'] ?? '', 'ts-jest': versions['ts-jest'] ?? '', typescript: versions['typescript'] ?? '' }),
53
+ };
54
+ const build = { rootDir: 'src', noEmit: false, declaration: true, types: [] };
55
+ return [
56
+ { relative: 'package.json', body: packageJsonText(packageJson) },
57
+ { relative: 'tsconfig.json', body: packageJsonText({ extends: '@starci/tsconfig/next.json', compilerOptions: { incremental: false, types: ['jest', 'node'] }, include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.json'], exclude: ['node_modules', 'dist'] }) },
58
+ { relative: 'tsconfig.build.json', body: packageJsonText({ extends: './tsconfig.json', compilerOptions: { ...build, outDir: 'dist', module: 'node16', moduleResolution: 'node16' }, exclude: ['node_modules', 'dist', 'src/**/*.spec.ts', 'src/workbench/**', 'src/public/**'] }) },
59
+ { relative: 'tsconfig.build.workbench.json', body: packageJsonText({ extends: './tsconfig.json', compilerOptions: { ...build, outDir: 'dist/esm' }, include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.json'], exclude: ['node_modules', 'dist', 'src/**/*.spec.ts'] }) },
60
+ ];
61
+ }
62
+ /** The versions a module package declares, read from the app's own manifests so it installs the one version of each. */
63
+ function versionsOf(root, feAppFiles) {
64
+ const rootPackage = read(path.join(root, 'package.json'));
65
+ const all = { ...rootPackage.devDependencies, ...rootPackage.dependencies };
66
+ for (const file of feAppFiles)
67
+ Object.assign(all, read(file).dependencies);
68
+ const wanted = ['@starci/grammar', 'react', '@types/jest', '@types/react', 'jest', 'ts-jest', 'typescript'];
69
+ const missing = wanted.filter((name) => all[name] === undefined);
70
+ if (missing.length > 0)
71
+ throw new ScaffoldError('HFS_ADD_MODULE_PINS', `add module reads the versions of ${missing.join(', ')} from the app's manifests, which declare none of them`);
72
+ return Object.fromEntries(wanted.map((name) => [name, all[name]]));
73
+ }
74
+ /** The package.json of every front-end Next app of the app. */
75
+ const feManifests = (root) => {
76
+ const apps = path.join(root, 'fe', 'apps');
77
+ if (!fs.existsSync(apps))
78
+ return [];
79
+ return fs.readdirSync(apps, { withFileTypes: true }).filter((entry) => entry.isDirectory() && fs.existsSync(path.join(apps, entry.name, 'package.json'))).map((entry) => path.join(apps, entry.name, 'package.json')).sort(byCodeUnit);
80
+ };
81
+ /** Everything a verb changes, snapshotted first so a failure leaves the tree as it was. */
82
+ class Transaction {
83
+ before = new Map();
84
+ root;
85
+ constructor(root) { this.root = root; }
86
+ write(relative, body) {
87
+ const file = path.join(this.root, ...relative.split('/'));
88
+ if (!this.before.has(file))
89
+ this.before.set(file, fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null);
90
+ fs.mkdirSync(path.dirname(file), { recursive: true });
91
+ fs.writeFileSync(file, body);
92
+ }
93
+ remove(relative) {
94
+ const file = path.join(this.root, ...relative.split('/'));
95
+ if (!fs.existsSync(file))
96
+ return;
97
+ for (const inner of fs.statSync(file).isDirectory() ? listFiles(file).map((name) => path.join(file, ...name.split('/'))) : [file])
98
+ this.before.set(inner, fs.readFileSync(inner, 'utf8'));
99
+ fs.rmSync(file, { recursive: true, force: true });
100
+ }
101
+ rollback() {
102
+ for (const [file, text] of this.before) {
103
+ if (text === null)
104
+ fs.rmSync(file, { force: true });
105
+ else {
106
+ fs.mkdirSync(path.dirname(file), { recursive: true });
107
+ fs.writeFileSync(file, text);
108
+ }
109
+ }
110
+ }
111
+ }
112
+ function context(root, name) {
113
+ if (!NAME.test(String(name)))
114
+ throw new ScaffoldError('HFS_ADD_NAME_INVALID', `module name ${name} must be kebab-case (letters, digits and single dashes)`);
115
+ const declarationFile = path.join(root, 'hfs.json');
116
+ const packageFile = path.join(root, 'package.json');
117
+ if (!fs.existsSync(declarationFile) || !fs.existsSync(packageFile))
118
+ throw new ScaffoldError('HFS_ADD_NOT_AN_APP', 'a module verb runs at the app root containing hfs.json and package.json');
119
+ const repo = readRepoDeclaration(loadSlotManifest(), root);
120
+ return { project: repo.project, declarationFile, packageFile };
121
+ }
122
+ /** Sets or deletes one dependency of a manifest file, keeping the section sorted; answers whether the text changed. */
123
+ function setDependency(tx, root, file, name, version) {
124
+ const manifest = read(file);
125
+ const current = manifest.dependencies ?? {};
126
+ if ((version === null && current[name] === undefined) || current[name] === version)
127
+ return false;
128
+ const next = { ...current };
129
+ if (version === null)
130
+ Reflect.deleteProperty(next, name);
131
+ else
132
+ next[name] = version;
133
+ manifest.dependencies = sortedDependencies(next);
134
+ tx.write(path.relative(root, file).split(path.sep).join('/'), packageJsonText(manifest));
135
+ return true;
136
+ }
137
+ /** Registers the modules on the app: the workspace glob, and the registry and module dependencies of the back end (the root manifest) and every front-end app. */
138
+ function register(tx, root, project, packageFile, names, changed) {
139
+ const rootPackage = read(packageFile);
140
+ const workspaces = new Set(rootPackage.workspaces ?? []);
141
+ const others = fs.existsSync(path.join(root, 'packages')) && fs.readdirSync(path.join(root, 'packages')).some((entry) => !entry.startsWith(MODULE_PREFIX) && entry !== 'modules');
142
+ if (names.length > 0)
143
+ workspaces.add(WORKSPACE_GLOB);
144
+ else if (!others)
145
+ workspaces.delete(WORKSPACE_GLOB);
146
+ if ([...workspaces].join('|') !== (rootPackage.workspaces ?? []).join('|')) {
147
+ rootPackage.workspaces = [...workspaces];
148
+ tx.write('package.json', packageJsonText(rootPackage));
149
+ changed.push('package.json');
150
+ }
151
+ const registry = `@${project}/modules`;
152
+ if (setDependency(tx, root, packageFile, registry, names.length > 0 ? '*' : null))
153
+ changed.push('package.json');
154
+ for (const file of feManifests(root)) {
155
+ let touched = setDependency(tx, root, file, registry, names.length > 0 ? '*' : null);
156
+ const present = Object.keys(read(file).dependencies ?? {}).filter((dependency) => dependency.startsWith(`@${project}/${MODULE_PREFIX}`));
157
+ for (const dependency of present)
158
+ if (!names.some((name) => `@${project}/${MODULE_PREFIX}${name}` === dependency))
159
+ touched = setDependency(tx, root, file, dependency, null) || touched;
160
+ for (const name of names)
161
+ touched = setDependency(tx, root, file, `@${project}/${MODULE_PREFIX}${name}`, '*') || touched;
162
+ if (touched)
163
+ changed.push(path.relative(root, file).split(path.sep).join('/'));
164
+ }
165
+ }
166
+ /** Replaces the registry package by the render of `names`: files written, and a registry of no module deleted. */
167
+ function writeRegistry(tx, root, project, names, result) {
168
+ const files = registryFiles(project, names);
169
+ if (files.length === 0) {
170
+ if (fs.existsSync(path.join(root, REGISTRY_DIR))) {
171
+ tx.remove(REGISTRY_DIR);
172
+ result.removed.push(REGISTRY_DIR);
173
+ }
174
+ return;
175
+ }
176
+ for (const file of files) {
177
+ (fs.existsSync(path.join(root, file.path)) ? result.updated : result.created).push(file.path);
178
+ tx.write(file.path, file.content);
179
+ }
180
+ }
181
+ /** Adds the module package `name`, registers it and re-renders the registry. Refused when the app has no agent SDK package or the module exists. */
182
+ export function addModule({ root, name }) {
183
+ const { project, packageFile } = context(root, name);
184
+ if (!fs.existsSync(path.join(root, SDK_DIR, 'package.json')))
185
+ throw new ScaffoldError('HFS_ADD_MODULE_SDK_MISSING', `a module is built on the product's agent SDK, which must exist as ${SDK_DIR}/ (a workspace package named @${project}/agent-sdk)`);
186
+ const folder = `packages/${MODULE_PREFIX}${name}`;
187
+ if (fs.existsSync(path.join(root, folder)))
188
+ throw new ScaffoldError('HFS_ADD_EXISTS', `module ${name} already exists: ${folder}`);
189
+ const values = { project, name, pascal: pascalOf(name), camel: camelOf(name), upper: upperOf(name) };
190
+ const planned = [
191
+ ...manifests(project, name, versionsOf(root, feManifests(root))),
192
+ ...listFiles(TEMPLATE_DIR).map((file) => ({
193
+ relative: file.replace(/\.tpl$/, '').split('/').map((segment) => segment.replace('__name__', name).replace('__pascal__', values.pascal)).join('/'),
194
+ body: fill(fs.readFileSync(path.join(TEMPLATE_DIR, ...file.split('/')), 'utf8'), values, file),
195
+ })),
196
+ ];
197
+ const result = { created: planned.map((file) => `${folder}/${file.relative}`), updated: [], removed: [] };
198
+ const tx = new Transaction(root);
199
+ try {
200
+ for (const file of planned)
201
+ tx.write(`${folder}/${file.relative}`, file.body);
202
+ const names = moduleNames(root);
203
+ register(tx, root, project, packageFile, names, result.updated);
204
+ writeRegistry(tx, root, project, names, result);
205
+ }
206
+ catch (error) {
207
+ tx.rollback();
208
+ fs.rmSync(path.join(root, folder), { recursive: true, force: true });
209
+ throw error;
210
+ }
211
+ return result;
212
+ }
213
+ /** Removes the module package `name`, its registrations and its entry in the registry; the last module takes the registry package with it. */
214
+ export function removeModule({ root, name }) {
215
+ const { project, packageFile } = context(root, name);
216
+ const folder = `packages/${MODULE_PREFIX}${name}`;
217
+ if (!fs.existsSync(path.join(root, folder)))
218
+ throw new ScaffoldError('HFS_REMOVE_NOT_FOUND', `module ${name} does not exist: ${folder}`);
219
+ const result = { created: [], updated: [], removed: [folder] };
220
+ const tx = new Transaction(root);
221
+ try {
222
+ tx.remove(folder);
223
+ const names = moduleNames(root);
224
+ register(tx, root, project, packageFile, names, result.updated);
225
+ writeRegistry(tx, root, project, names, result);
226
+ }
227
+ catch (error) {
228
+ tx.rollback();
229
+ throw error;
230
+ }
231
+ return result;
232
+ }