@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,57 +1,17 @@
1
- // identity.mjs - the back-end identity machine. Under lite the one token verifier is be.integrations.passport: jose's jwtVerify
2
- // there checks the app's OWN key set (createLocalJWKSet over the keys it signs with, never createRemoteJWKSet), pins issuer,
3
- // audience and algorithms, and no file decodes a token unverified or reads authorization claims from a request body (R329).
4
- // In both editions every jwtVerify call holds the JWT policy (R260, jwtPolicyProblems).
1
+ // identity.mjs - the back-end identity machine. Under lite the one token verifier is @starci/auth, composed by be.integrations.passport:
2
+ // no app file calls jose's jwtVerify, decodes a token unverified or reads authorization claims from a request body (R329), and the
3
+ // passport integration imports @starci/auth/server. In both editions every jwtVerify call holds the JWT policy (R260, jwtPolicyProblems).
5
4
  import path from 'node:path';
6
5
  import { machineKit } from './machine-ast.mjs';
7
- import { fromConfig, importedFrom, jwtPolicyProblems, reportAt, unwrap } from './jwt-ast.mjs';
6
+ import { jwtPolicyProblems, reportAt, unwrap } from './jwt-ast.mjs';
8
7
  import { accessName } from '../../lib/ts-ast.mjs';
9
8
  const PASSPORT_SLOT = 'be.integrations.passport';
9
+ const PACKAGE_ENTRY = '@starci/auth/server';
10
10
  const BE_JWT_VERIFIED = 'BE_AUTH_JWT_OWN_JWKS';
11
11
  /** The rule ids the back-end identity machine reports. */
12
12
  export const IDENTITY_RULE_IDS = Object.freeze(['BE_AUTH_JWT_OWN_JWKS', 'BE_JWT_ALG_PINNED']);
13
13
  const JWT_MODULES = new Set(['jose', 'jsonwebtoken', 'jwt-decode']);
14
14
  const CLAIM_NAMES = new Set(['role', 'roles', 'membership', 'memberships']);
15
- const objectLiteral = (kit, checker, node) => {
16
- const current = unwrap(kit.ts, node);
17
- if (!current)
18
- return null;
19
- if (kit.ts.isObjectLiteralExpression(current))
20
- return current;
21
- if (!kit.ts.isIdentifier(current))
22
- return null;
23
- for (const declaration of kit.declarationsOf(checker, current)) {
24
- if (kit.ts.isVariableDeclaration(declaration) && declaration.initializer) {
25
- const value = unwrap(kit.ts, declaration.initializer);
26
- if (kit.ts.isObjectLiteralExpression(value))
27
- return value;
28
- }
29
- }
30
- return null;
31
- };
32
- const staticStrings = (kit, checker, node) => {
33
- const current = unwrap(kit.ts, node);
34
- if (kit.ts.isStringLiteralLike(current))
35
- return [current.text];
36
- if (kit.ts.isArrayLiteralExpression(current)) {
37
- const literals = current.elements.filter((element) => kit.ts.isStringLiteralLike(element));
38
- if (literals.length === current.elements.length)
39
- return literals.map((element) => element.text);
40
- }
41
- const value = kit.stringValue(checker, current);
42
- return value === null ? null : [value];
43
- };
44
- const jwksArgument = (kit, checker, node) => {
45
- const current = unwrap(kit.ts, node);
46
- if (!current)
47
- return false;
48
- if (kit.ts.isCallExpression(current))
49
- return importedFrom(kit, checker, current.expression, new Set(['jose']), new Set(['createLocalJWKSet']));
50
- if (!kit.ts.isIdentifier(current))
51
- return false;
52
- return kit.declarationsOf(checker, current).some((declaration) => kit.ts.isVariableDeclaration(declaration) && declaration.initializer
53
- && jwksArgument(kit, checker, declaration.initializer));
54
- };
55
15
  const importedCall = (kit, checker, call) => {
56
16
  const direct = kit.importBinding(checker, call.expression);
57
17
  if (direct?.module === 'jwt-decode' && direct.name === 'default')
@@ -84,41 +44,16 @@ const fromRequestBody = (kit, checker, node, seen = new Set()) => {
84
44
  return kit.declarationsOf(checker, current).some(declaration => kit.ts.isVariableDeclaration(declaration) && declaration.initializer
85
45
  && fromRequestBody(kit, checker, declaration.initializer, seen));
86
46
  };
87
- function verifyJwtCall(kit, checker, file, node, counters, violations) {
88
- counters.verifyCalls += 1;
89
- if (file.slot !== PASSPORT_SLOT) {
90
- reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `jwtVerify is called outside ${PASSPORT_SLOT}; the integration is the one token verifier.`);
91
- return true;
92
- }
93
- const options = objectLiteral(kit, checker, node.arguments[2]);
94
- const property = (name) => options ? kit.propertyOf(options, name) : null;
95
- const values = (name) => {
96
- const entry = property(name);
97
- return entry ? staticStrings(kit, checker, kit.valueOfProperty(entry)) : null;
98
- };
99
- const issuer = property('issuer');
100
- const audience = values('audience');
101
- const algorithms = values('algorithms');
102
- // The audience is pinned by a literal or, as R260 accepts, by typed configuration (the skeleton's TokenOptions.audience).
103
- const audienceEntry = property('audience');
104
- const pinnedAudience = (audience !== null && audience.length > 0)
105
- || (audienceEntry != null && fromConfig(kit.ts, checker, kit.valueOfProperty(audienceEntry)));
106
- const good = jwksArgument(kit, checker, node.arguments[1]) && issuer != null && pinnedAudience
107
- && algorithms !== null && algorithms.length > 0;
108
- if (!good) {
109
- reportAt(violations, kit, BE_JWT_VERIFIED, file, node, 'jwtVerify must check the app\'s own key set (createLocalJWKSet(...) over the keys integrations/passport signs with) and pin issuer, audience and a non-empty algorithms list; jwtVerify then enforces signature and exp.');
110
- }
111
- else
112
- counters.verifiedCalls += 1;
113
- return false;
114
- }
115
47
  function inspectJwtCall(kit, checker, file, node, counters, violations) {
116
48
  const binding = importedCall(kit, checker, node);
117
49
  if (binding && JWT_MODULES.has(binding.module) && ['decode', 'decodeJwt', 'jwtDecode'].includes(binding.name)) {
118
50
  reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `${binding.module}.${binding.name} decodes a token without proving its signature; verify it through integrations/passport.`, { module: binding.module, method: binding.name });
119
51
  }
120
- if (binding?.module === 'jose' && binding.name === 'jwtVerify')
121
- return verifyJwtCall(kit, checker, file, node, counters, violations);
52
+ if (binding?.module === 'jose' && binding.name === 'jwtVerify') {
53
+ counters.verifyCalls += 1;
54
+ reportAt(violations, kit, BE_JWT_VERIFIED, file, node, 'jwtVerify is called in the app; @starci/auth verifies the own access tokens, reached through ' + PASSPORT_SLOT + '.');
55
+ return true;
56
+ }
122
57
  return false;
123
58
  }
124
59
  function inspectRequestClaims(kit, file, node, checker, violations) {
@@ -146,18 +81,18 @@ function checkOwnJwks(input) {
146
81
  const violations = [];
147
82
  const integrations = [...input.graph.files.values()].filter((file) => file.slot === PASSPORT_SLOT);
148
83
  if (!integrations.length)
149
- return { violations, coverage: { status: 'not-applicable', integrationFiles: 0, verifiedCalls: 0 } };
150
- const counters = { verifyCalls: 0, verifiedCalls: 0 };
84
+ return { violations, coverage: { status: 'not-applicable', integrationFiles: 0, verifyCalls: 0 } };
85
+ const counters = { verifyCalls: 0 };
151
86
  for (const file of input.graph.files.values()) {
152
87
  const checker = kit.checkerOf(file.sourceFile);
153
88
  kit.walk(file.sourceFile, (node) => inspectBackendNode(kit, checker, file, node, counters, violations));
154
89
  }
155
- if (counters.verifyCalls === 0) {
156
- const anchor = integrations.find((file) => path.posix.basename(file.rel).endsWith('.tokens.ts')) ?? integrations[0];
157
- if (anchor)
158
- reportAt(violations, kit, BE_JWT_VERIFIED, anchor, anchor.sourceFile, `${PASSPORT_SLOT} has no jose.jwtVerify call backed by createLocalJWKSet over the app's own keys.`);
159
- }
160
- return { violations, coverage: { status: 'checked', integrationFiles: integrations.length, verifyCalls: counters.verifyCalls, verifiedCalls: counters.verifiedCalls } };
90
+ const composes = integrations.some((file) => file.sourceFile.statements.some((statement) => kit.ts.isImportDeclaration(statement)
91
+ && kit.ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === PACKAGE_ENTRY));
92
+ const anchor = integrations.find((file) => path.posix.basename(file.rel) === 'index.ts') ?? integrations[0];
93
+ if (!composes && anchor)
94
+ reportAt(violations, kit, BE_JWT_VERIFIED, anchor, anchor.sourceFile, PASSPORT_SLOT + ' does not import ' + PACKAGE_ENTRY + ', the one verifier of the own access tokens.');
95
+ return { violations, coverage: { status: 'checked', integrationFiles: integrations.length, verifyCalls: counters.verifyCalls, composesPackage: composes } };
161
96
  }
162
97
  /** R260 BE_JWT_ALG_PINNED: every jose jwtVerify call pins a literal secure algorithm list and a configured issuer, and binds the principal by a configured audience or the authenticated role. */
163
98
  export function checkJwtPolicy(input) {
@@ -9,7 +9,7 @@ import type { MachineContext } from './machine-ast.mjs';
9
9
  *
10
10
  * - a `*.module.ts` file below a transport folder other than `<f>-<protocol>.module.ts`, a `*.module-definition.ts` that is
11
11
  * not the sibling of the application module or of the transport module of its own folder, and an `@Module` class (or a
12
- * `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use) in a feature file that is neither the application module, the
12
+ * `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use or a `defineConfigurableModule`/`defineCapabilityModule` call of `@starci/nest-kit`) in a feature file that is neither the application module, the
13
13
  * transport module of its own folder nor that module's definition are refused;
14
14
  * - apps import only transport modules, of the kinds their app kind allows: the `composedBy` list of each transport slot
15
15
  * in knowledge/hfs/slots.yaml (api: graphql, http, websocket; worker: schedule, message). An app root (app.module.ts
@@ -11,7 +11,7 @@ import { trimTrailingSlashes } from '../trailing-slashes.mjs';
11
11
  *
12
12
  * - a `*.module.ts` file below a transport folder other than `<f>-<protocol>.module.ts`, a `*.module-definition.ts` that is
13
13
  * not the sibling of the application module or of the transport module of its own folder, and an `@Module` class (or a
14
- * `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use) in a feature file that is neither the application module, the
14
+ * `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use or a `defineConfigurableModule`/`defineCapabilityModule` call of `@starci/nest-kit`) in a feature file that is neither the application module, the
15
15
  * transport module of its own folder nor that module's definition are refused;
16
16
  * - apps import only transport modules, of the kinds their app kind allows: the `composedBy` list of each transport slot
17
17
  * in knowledge/hfs/slots.yaml (api: graphql, http, websocket; worker: schedule, message). An app root (app.module.ts
@@ -99,6 +99,8 @@ function reportModuleClassLocation(file, name, place, composedModuleOf, report)
99
99
  if (file.rel !== own)
100
100
  report(file, name, `${name.text} is a Nest module declared in ${file.rel}; a feature declares exactly its application module (${place.feature}.module.ts) and one module per transport folder (${place.feature}-<protocol>.module.ts), never a module per operation.`, { feature: place.feature });
101
101
  }
102
+ /** The module-definition helpers of @starci/nest-kit: each builds the definition of one module, like `ConfigurableModuleBuilder`. */
103
+ const DEFINITION_HELPERS = ['defineConfigurableModule', 'defineCapabilityModule'];
102
104
  function reportConfigurableModuleUses(file, checker, kit, ts, place, env) {
103
105
  const own = ownModuleOf(place, file.rel, env.composedModuleOf);
104
106
  const isDefinition = file.rel === definitionOf(own);
@@ -106,6 +108,9 @@ function reportConfigurableModuleUses(file, checker, kit, ts, place, env) {
106
108
  if (ts.isNewExpression(node) && kit.isImportOf(checker, node.expression, 'ConfigurableModuleBuilder', '@nestjs/common') && !isDefinition) {
107
109
  env.report(file, node, `ConfigurableModuleBuilder builds the module definition of one module; in feature ${place.feature} it is written only in ${path.posix.basename(definitionOf(own))}, beside ${path.posix.basename(own)}.`, { feature: place.feature });
108
110
  }
111
+ else if (ts.isCallExpression(node) && !isDefinition && DEFINITION_HELPERS.some(name => kit.isImportOf(checker, node.expression, name, '@starci/nest-kit'))) {
112
+ env.report(file, node, `${node.expression.getText(file.sourceFile)} builds the module definition of one module; in feature ${place.feature} it is written only in ${path.posix.basename(definitionOf(own))}, beside ${path.posix.basename(own)}.`, { feature: place.feature });
113
+ }
109
114
  else if (ts.isHeritageClause(node) && file.rel !== own) {
110
115
  for (const type of node.types)
111
116
  if (kit.isImportOf(checker, type.expression, 'ConfigurableModuleClass', '@nestjs/common')) {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A workspace package of the app root: a first-party package that both sides import through its package entry points and never past
3
+ * them. `root` is the real path of its folder, `relative` the folder from the app root, `exports` the `exports` of its manifest.
4
+ */
5
+ export interface RootPackage {
6
+ root: string;
7
+ relative: string;
8
+ name: string;
9
+ exports: unknown;
10
+ manifest: Record<string, unknown>;
11
+ workspace: true;
12
+ app: false;
13
+ }
14
+ /** The slots whose directories at the app root are first-party packages of the app (agent SDK, module packages, the managed registry). */
15
+ export declare const ROOT_PACKAGE_SLOTS: ReadonlySet<string>;
16
+ /** A package folder with a regular package.json (a link is not one). */
17
+ export declare function packageDirectory(absolute: string): boolean;
18
+ /** The folders the `workspaces` of the app root's package.json name that no side owns, as absolute paths in code-unit order. */
19
+ export declare function rootWorkspaceDirectories(packageRoot: string): string[];
20
+ /**
21
+ * The record of one package folder of the app root, or null when it is no named package inside the app root.
22
+ * `canonical` is the file-identity function of the caller (the real path), so a record agrees with the files the graph reads.
23
+ */
24
+ export declare function rootPackageRecord(packageRoot: string, directory: string, canonical: (file: string) => string): RootPackage | null;
25
+ /** The names of the workspace packages of the app root: what the back-end canon must not read as third-party libraries. */
26
+ export declare function rootPackageNames(packageRoot: string): Set<string>;
@@ -0,0 +1,78 @@
1
+ // root-packages.mjs - the workspace packages of an app ROOT (packages/agent-sdk, packages/module-<name>, the managed packages/modules).
2
+ // They lie outside both side folders and belong to no side; both sides import them by package name, through their package entry points
3
+ // (the `exports` of the manifest), so the import graph reads them as first-party packages and the back-end canon never reads them as
4
+ // a third-party library. The app root's one package.json lists them in `workspaces`; the patterns that start with a side folder
5
+ // (`fe/apps/*`) are that side's own workspaces and are not read here.
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { readJsonFile as readJson } from '../../lib/json.mjs';
9
+ import { byCodeUnit } from '../../lib/list.mjs';
10
+ import { isInside } from '../../lib/walk.mjs';
11
+ import { slash } from '../../lib/path-key.mjs';
12
+ /** The slots whose directories at the app root are first-party packages of the app (agent SDK, module packages, the managed registry). */
13
+ export const ROOT_PACKAGE_SLOTS = new Set(['app.packages', 'app.module-registry']);
14
+ const SIDE_PREFIX = /^(?:be|fe)\//;
15
+ /** A workspace pattern this reader expands (a literal path, or `*` as a whole segment); the others are judged by the workspace rules. */
16
+ const expandablePattern = (written) => !path.isAbsolute(written) && written.split('/').every(segment => segment !== '..' && (!segment.includes('*') || segment === '*'));
17
+ function expandSegment(base, segment) {
18
+ if (segment !== '*')
19
+ return [path.join(base, segment)];
20
+ if (!fs.existsSync(base) || !fs.lstatSync(base).isDirectory())
21
+ return [];
22
+ return fs.readdirSync(base, { withFileTypes: true }).filter(item => item.isDirectory()).sort((a, b) => byCodeUnit(a.name, b.name)).map(item => path.join(base, item.name));
23
+ }
24
+ /** A package folder with a regular package.json (a link is not one). */
25
+ export function packageDirectory(absolute) {
26
+ try {
27
+ if (!fs.lstatSync(absolute).isDirectory())
28
+ return false;
29
+ const manifest = fs.lstatSync(path.join(absolute, 'package.json'));
30
+ return manifest.isFile() && !manifest.isSymbolicLink();
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ /** The folders the `workspaces` of the app root's package.json name that no side owns, as absolute paths in code-unit order. */
37
+ export function rootWorkspaceDirectories(packageRoot) {
38
+ const manifest = readJson(path.join(packageRoot, 'package.json'));
39
+ const declared = manifest?.workspaces;
40
+ const patterns = Array.isArray(declared) ? declared : declared?.packages;
41
+ const found = new Set();
42
+ for (const pattern of Array.isArray(patterns) ? patterns : []) {
43
+ if (typeof pattern !== 'string' || !pattern.trim())
44
+ continue;
45
+ const written = slash(pattern.trim()).replace(/^\.\//, '');
46
+ if (SIDE_PREFIX.test(written) || !expandablePattern(written))
47
+ continue;
48
+ let bases = [packageRoot];
49
+ for (const segment of written.split('/'))
50
+ bases = bases.flatMap(base => expandSegment(base, segment));
51
+ for (const base of bases)
52
+ found.add(path.resolve(base));
53
+ }
54
+ return [...found].sort(byCodeUnit);
55
+ }
56
+ /**
57
+ * The record of one package folder of the app root, or null when it is no named package inside the app root.
58
+ * `canonical` is the file-identity function of the caller (the real path), so a record agrees with the files the graph reads.
59
+ */
60
+ export function rootPackageRecord(packageRoot, directory, canonical) {
61
+ if (!packageDirectory(directory) || !isInside(packageRoot, directory))
62
+ return null;
63
+ const manifest = readJson(path.join(directory, 'package.json'));
64
+ const name = manifest?.['name'];
65
+ if (manifest === null || typeof name !== 'string' || !name)
66
+ return null;
67
+ return { root: canonical(directory), relative: slash(path.relative(packageRoot, directory)), name, exports: manifest['exports'], manifest, workspace: true, app: false };
68
+ }
69
+ /** The names of the workspace packages of the app root: what the back-end canon must not read as third-party libraries. */
70
+ export function rootPackageNames(packageRoot) {
71
+ const names = new Set();
72
+ for (const directory of rootWorkspaceDirectories(packageRoot)) {
73
+ const record = rootPackageRecord(packageRoot, directory, (file) => file);
74
+ if (record !== null)
75
+ names.add(record.name);
76
+ }
77
+ return names;
78
+ }
@@ -15,8 +15,7 @@ import type { ArchitectureViolation, CheckInput } from './hfs-graph.mjs';
15
15
  * - no file outside the client folder and the stream reader folder (`fe.transport.stream`, `modules/api/sse/clients/`) references the global `fetch`: a call, `globalThis.fetch`, `window.fetch`, or a
16
16
  * reference passed as a value (`useSWR(key, fetch)`, `const send = fetch`), all of which a spelling-based rule misses; with no
17
17
  * client at all every such reference is a finding that says the repository has no client;
18
- * - axios is imported only by the api owner (modules/api, the api package) for its rest calls and by the auth door
19
- * (modules/auth/auth-door.ts) for the app's own auth doors; every other HTTP library is a
18
+ * - axios is imported only by the api owner (modules/api, the api package) for its rest calls; every other HTTP library is a
20
19
  * second transport wherever it is imported, by static import, `import()` or `require()`;
21
20
  * - an `ApolloClient` is constructed only inside the client folder;
22
21
  * - every GraphQL operation (`graphql/queries/query-*.ts`, `graphql/mutations/mutation-*.ts`, `graphql/subscriptions/subscription-*.ts`) imports the client (or the api
@@ -17,8 +17,7 @@ import { propertyValue } from './function-spec-providers.mjs';
17
17
  * - no file outside the client folder and the stream reader folder (`fe.transport.stream`, `modules/api/sse/clients/`) references the global `fetch`: a call, `globalThis.fetch`, `window.fetch`, or a
18
18
  * reference passed as a value (`useSWR(key, fetch)`, `const send = fetch`), all of which a spelling-based rule misses; with no
19
19
  * client at all every such reference is a finding that says the repository has no client;
20
- * - axios is imported only by the api owner (modules/api, the api package) for its rest calls and by the auth door
21
- * (modules/auth/auth-door.ts) for the app's own auth doors; every other HTTP library is a
20
+ * - axios is imported only by the api owner (modules/api, the api package) for its rest calls; every other HTTP library is a
22
21
  * second transport wherever it is imported, by static import, `import()` or `require()`;
23
22
  * - an `ApolloClient` is constructed only inside the client folder;
24
23
  * - every GraphQL operation (`graphql/queries/query-*.ts`, `graphql/mutations/mutation-*.ts`, `graphql/subscriptions/subscription-*.ts`) imports the client (or the api
@@ -105,17 +104,15 @@ function isApolloClientConstruction(ts, kit, checker, node) {
105
104
  const binding = kit.importBinding(checker, node.expression);
106
105
  return Boolean(binding?.name === 'ApolloClient' && binding.module.startsWith('@apollo/client'));
107
106
  }
108
- /** The one auth door of the session issuer's client (`modules/auth/auth-door.ts`, slot fe.modules.auth): it calls the app's own auth doors with axios. */
109
- const isAuthDoor = (file) => file.slot === 'fe.modules.auth' && path.posix.basename(file.rel) === 'auth-door.ts';
110
107
  function inspectLibraryImport({ ts, file, node, transport, report }) {
111
108
  const specifier = moduleSpecifierOf(ts, node);
112
109
  if (!specifier || !ts.isStringLiteralLike(specifier))
113
110
  return;
114
111
  const library = specifier.text.split('/')[0];
115
- if (!HTTP_LIBRARIES.has(library) || (library === REST_LIBRARY && (API_OWNER_SLOTS.has(file.slot) || isAuthDoor(file))))
112
+ if (!HTTP_LIBRARIES.has(library) || (library === REST_LIBRARY && (API_OWNER_SLOTS.has(file.slot))))
116
113
  return;
117
114
  const where = library === REST_LIBRARY
118
- ? 'axios belongs to the api owner (modules/api/rest, the api package) and the auth door (modules/auth/auth-door.ts) only: call their functions'
115
+ ? 'axios belongs to the api owner (modules/api/rest, the api package) only: call their functions'
119
116
  : `the transport is Apollo GraphQL (${transport}) with axios for the rest calls of modules/api/rest`;
120
117
  report(file, node, `${specifier.text} is a second HTTP transport in ${file.rel}; ${where}.`, { library: specifier.text });
121
118
  }
@@ -1,5 +1,5 @@
1
1
  import type { CompilerOptions, CreateProgramOptions, Diagnostic, ModuleResolutionCache, ModuleResolutionHost, Node, Program, ProjectReference, SourceFile, TypeChecker } from 'typescript';
2
- import type { ArchitectureConfig, TypeScriptApi } from './config.mjs';
2
+ import type { ArchitectureConfig, RootPackage, TypeScriptApi } from './config.mjs';
3
3
  import type { ProjectRead } from '../typescript-programs.mjs';
4
4
  import type { ArchitectureError, ImportEdge, LoadedTypeScript, ModuleReferences, WorkspaceRecord } from './typescript.mjs';
5
5
  /** One parsed tsconfig with its compiler program. */
@@ -56,5 +56,8 @@ export interface TypeContextHelpers {
56
56
  relativePath(root: string, fileName: string): string;
57
57
  packageExported(ts: TypeScriptApi, workspace: WorkspaceRecord, specifier: string, actualTarget: string): boolean;
58
58
  boundaryViolation(root: string, edge: ImportEdge, fromWorkspace: WorkspaceRecord | null, toWorkspace: WorkspaceRecord | null, ruleId: string, message: string): ArchitectureError;
59
+ rootPackageOf(config: Pick<ArchitectureConfig, 'rootPackages'>, specifier: string): RootPackage | null;
60
+ rootPackageEntryDeclared(item: RootPackage, specifier: string): boolean;
61
+ rootPackageEntries(item: RootPackage): string[];
59
62
  }
60
63
  export declare function contextFromProjects(config: ArchitectureConfig, loaded: LoadedTypeScript, paths: string[], helpers: TypeContextHelpers): TypeContext;
@@ -241,8 +241,38 @@ function appendEdge(from, sourceFile, project, owningWorkspace, reference, actua
241
241
  errors.push(helpers.boundaryViolation(config.root, edge, owningWorkspace, targetWorkspace, 'ARCH_PACKAGE_EXPORT_BYPASS', 'Cross-package imports must use the target package name and a declared package export.'));
242
242
  }
243
243
  }
244
+ /**
245
+ * An import of one of the app's root workspace packages (`@scope/agent-sdk`, `@scope/agent-sdk/workbench`). The package lies outside the side
246
+ * folder, yet it is first-party source both sides may import: through its package entry points (the `exports` of its manifest) and never past
247
+ * them. An entry point is no import that leaves the side and adds no edge (the package is not part of this side's graph; the package's own
248
+ * lint judges its files); a path into the package's source is refused. Returns true when the reference was settled here.
249
+ */
250
+ function rootPackageReference(sourceFile, project, reference, state) {
251
+ const { config, errors, helpers, host, ts } = state;
252
+ if (!config.rootPackages?.length)
253
+ return false;
254
+ const item = helpers.rootPackageOf(config, reference.specifier);
255
+ if (item === null)
256
+ return false;
257
+ if (!helpers.rootPackageEntryDeclared(item, reference.specifier)) {
258
+ errors.push({
259
+ ruleId: 'ARCH_INTERNAL_IMPORT_OUTSIDE',
260
+ project: project.relative,
261
+ path: helpers.relativePath(config.root, sourceFile.fileName),
262
+ ...helpers.sourceLocation(sourceFile, reference.node),
263
+ specifier: reference.specifier,
264
+ message: `Internal import ${reference.specifier} reaches into the package ${item.name} past its package entry points; import ${helpers.rootPackageEntries(item).join(', ')} instead.`,
265
+ });
266
+ return true;
267
+ }
268
+ // An entry point that a path alias sends outside the package is no entry point: the ordinary boundary judges it.
269
+ const resolved = helpers.resolveTypeScriptModule(ts, reference.specifier, sourceFile.fileName, project.options, host, resolutionCache(state, project));
270
+ return resolved === undefined || helpers.isInside(item.root, helpers.canonical(resolved));
271
+ }
244
272
  function processReference(from, sourceFile, project, owningWorkspace, reference, state) {
245
273
  const { config, ts, workspaceNames, workspaces, host, errors, helpers } = state;
274
+ if (rootPackageReference(sourceFile, project, reference, state))
275
+ return;
246
276
  // A workspace package is read at its source, never at its build: the same import resolves whether or not dist exists.
247
277
  const workspaceTarget = workspaces.filter(item => item.root !== helpers.canonical(config.root)).map(item => helpers.workspaceSourceEntry(ts, item, reference.specifier)).find(Boolean);
248
278
  const resolvedName = workspaceTarget ?? helpers.resolveTypeScriptModule(ts, reference.specifier, sourceFile.fileName, project.options, host, resolutionCache(state, project));
@@ -1,6 +1,6 @@
1
1
  import type { TypeContext } from './type-context.mjs';
2
2
  import type { Node, SourceFile, TypeChecker } from 'typescript';
3
- import type { ArchitectureConfig, TypeScriptApi } from './config.mjs';
3
+ import type { ArchitectureConfig, RootPackage, TypeScriptApi } from './config.mjs';
4
4
  export { unwrapExpression } from '../../lib/ts-analysis.mjs';
5
5
  /** The target repository's loaded TypeScript: the API, where it resolved from and its version. */
6
6
  export interface LoadedTypeScript {
@@ -82,6 +82,12 @@ export declare function exportTargetStrings(value: unknown): string[];
82
82
  export declare function workspaceSourceEntry(ts: TypeScriptApi, workspace: WorkspaceRecord, specifier: string): string | null;
83
83
  /** Every source file the package.json `exports` of a workspace package maps (each non-wildcard subpath, dist back to source): its public entries. */
84
84
  export declare function workspaceExportSources(ts: TypeScriptApi, workspace: WorkspaceRecord): string[];
85
+ /** The root workspace package of the app (`packages/agent-sdk`) a specifier names, its name or a subpath of it; the longest name when several match. */
86
+ export declare function rootPackageOf(config: Pick<ArchitectureConfig, 'rootPackages'>, specifier: string): RootPackage | null;
87
+ /** Whether a specifier into a root package names one of its entry points: a subpath its `exports` declare, or its root through `exports`, `main`/`types` or `src/index`. A path into its source is not an entry point. */
88
+ export declare function rootPackageEntryDeclared(item: RootPackage, specifier: string): boolean;
89
+ /** The subpaths a root package declares for import (`.` first), for the message of a refused deep import. */
90
+ export declare function rootPackageEntries(item: RootPackage): string[];
85
91
  /** Parse configured tsconfigs, then build only selected programs and their project-reference closure. */
86
92
  export declare function buildTypeScriptContext(config: ArchitectureConfig, injectedTypeScript?: TypeScriptApi | null, paths?: string[]): TypeContext;
87
93
  export declare function relativePath(root: string, fileName: string): string;
@@ -385,6 +385,26 @@ function packageExported(ts, workspace, specifier, actualTarget) {
385
385
  return expected === canonical(actualTarget) || sourceOfTarget(ts, workspace, target) === canonical(actualTarget);
386
386
  });
387
387
  }
388
+ /** The root workspace package of the app (`packages/agent-sdk`) a specifier names, its name or a subpath of it; the longest name when several match. */
389
+ export function rootPackageOf(config, specifier) {
390
+ let found = null;
391
+ for (const item of config.rootPackages ?? [])
392
+ if (sameOrUnder(specifier, item.name) && (found === null || item.name.length > found.name.length))
393
+ found = item;
394
+ return found;
395
+ }
396
+ /** Whether a specifier into a root package names one of its entry points: a subpath its `exports` declare, or its root through `exports`, `main`/`types` or `src/index`. A path into its source is not an entry point. */
397
+ export function rootPackageEntryDeclared(item, specifier) {
398
+ if (exportCandidates(item, specifier).length > 0)
399
+ return true;
400
+ return specifier === item.name && !item.exports && (manifestEntryTargets(item.manifest).length > 0 || packageIndexSource(item) !== null);
401
+ }
402
+ /** The subpaths a root package declares for import (`.` first), for the message of a refused deep import. */
403
+ export function rootPackageEntries(item) {
404
+ const declaration = item.exports;
405
+ const keys = declaration && typeof declaration === 'object' && !Array.isArray(declaration) ? Object.keys(declaration).filter(key => key.startsWith('.')) : ['.'];
406
+ return (keys.length ? keys : ['.']).map(key => (key === '.' ? item.name : `${item.name}${key.slice(1)}`));
407
+ }
388
408
  function boundaryViolation(root, edge, fromWorkspace, toWorkspace, ruleId, message) {
389
409
  return {
390
410
  ruleId,
@@ -413,7 +433,7 @@ function typeScriptContext(config, loaded, paths) {
413
433
  canonical, compilerError, isInside, slash, readTypeScriptProject, typeScriptProjectReferencePath,
414
434
  sameOrUnder, createTypeScriptProgram, isProductionSource, sourceLocation, workspaceMetadata, workspaceOf,
415
435
  pathAliasMatches, moduleReferences, workspaceSourceEntry, resolveTypeScriptModule, crossesSide, assetExtension: ASSET_EXTENSION,
416
- relativePath, packageExported, boundaryViolation,
436
+ relativePath, packageExported, boundaryViolation, rootPackageOf, rootPackageEntryDeclared, rootPackageEntries,
417
437
  });
418
438
  }
419
439
  export function relativePath(root, fileName) {
@@ -6,7 +6,7 @@ const HINTS = {
6
6
  'parameter type': 'Give the parameter a type: `(value: Input)`.',
7
7
  'property type': 'Give the property a type: `name: Result = ...`.',
8
8
  'accessor type': 'Give the accessor a type: `get name(): Result` (a setter takes it from its parameter).',
9
- 'destructured export': 'A destructuring pattern cannot carry a type per name: export each name as its own variable with a type, `export const name: Type = source.name`.',
9
+ 'destructured export': 'A destructuring pattern cannot carry a type per name: export each name as its own variable with a type, `export const name: Type = source.name`; a module definition is `export const { ConfigurableModuleClass, OPTIONS_TYPE } = defineCapabilityModule<XOptions>()` (or `defineConfigurableModule`) of @starci/nest-kit, which states the types (`starci app codemod module-definition` writes it).',
10
10
  'exported value': 'Declare the type (an interface or a type alias) and annotate the export with it: `export const name: Shape = ...`.',
11
11
  };
12
12
  /** The `--isolatedDeclarations` flag name TypeScript ends its reasons with says nothing to the reader. */
@@ -64,6 +64,7 @@ export interface Slot {
64
64
  role?: string;
65
65
  importedBy?: string[];
66
66
  rewrittenBy?: string;
67
+ managedOnly?: boolean;
67
68
  logic?: string[];
68
69
  generatedBy?: string;
69
70
  }
@@ -121,7 +121,7 @@ export const MANIFEST_KINDS = [APP_KIND, RUNTIME_KIND];
121
121
  /** The kind of a parsed manifest: `kind`, or app when absent (knowledge/hfs/slots.yaml carries none). */
122
122
  export const manifestKind = (m) => (isPlainObject(m) && m.kind !== undefined ? m.kind : APP_KIND);
123
123
  const strList = (v) => stringList(v);
124
- const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exemplifiedBy', 'role', 'importedBy', 'rewrittenBy', 'logic'];
124
+ const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exemplifiedBy', 'role', 'importedBy', 'rewrittenBy', 'logic', 'managedOnly'];
125
125
  /** The roles a file slot may play that a rule reads (`role`): the entity registry of persistence.md BE-PERSISTENCE-3 (architecture/entity-registry.mjs). */
126
126
  const SLOT_ROLES = ['entity-registry'];
127
127
  /** The fields a slot's `lite` overlay may hold: the same keys it would carry in the slot body, resolved under edition lite. */
@@ -235,6 +235,8 @@ function slotListProblems(slot, at) {
235
235
  bad.push(`${at}: composedBy must be a non-empty list of unique app kinds`);
236
236
  if (slot.budget !== undefined && !budgetValid(slot.budget))
237
237
  bad.push(`${at}: budget must map names to positive integers`);
238
+ if (slot.managedOnly !== undefined && (slot.managedOnly !== true || slot.managedBy === undefined))
239
+ bad.push(`${at}: managedOnly is true and needs managedBy: the slot only lists files sync renders, and the slot owning the path judges them`);
238
240
  for (const key of ['managedBy', 'liteManagedBy'])
239
241
  if (slot[key] !== undefined && !NAME.test(String(slot[key])))
240
242
  bad.push(`${at}: ${key} must be a template id`);
@@ -33,6 +33,8 @@ import { stacksIngressFindings } from './stacks-ingress.mjs';
33
33
  import { stacksLiteBackingFindings } from './stacks-lite-backing.mjs';
34
34
  import { stackToolingFindings } from './stack-tooling.mjs';
35
35
  import { stacksDeployFindings } from './stacks-deploy.mjs';
36
+ import { stacksLocalFindings } from './stacks-local.mjs';
37
+ import { stacksServiceImageFindings } from './stacks-service-images.mjs';
36
38
  import { stacksAuthCookieDomainFindings } from './stacks-auth-cookie-domain.mjs';
37
39
  import { ciCustodyFindings } from './ci-custody.mjs';
38
40
  import { workflowContextsFindings } from './workflow-contexts.mjs';
@@ -49,6 +51,7 @@ export const A15_CODES = [
49
51
  'HFS_WORKFLOW_CONTEXT',
50
52
  'CI_STANDARD',
51
53
  'CI_SECRET_SEALED', 'APP_SECRET_IN_CI', 'CI_DECLARATION_DRIFT',
54
+ 'STACKS_LOCAL_CONFIG_INVALID', 'STACKS_DEV_UNDECLARED', 'STACKS_RUNBOOK_DOCKER_COMMAND',
52
55
  'BACKUP_PROOF_MISSING', 'BACKUP_UNDECLARED', 'BE_APP_BOOT_PROOF_MISSING', 'BOOT_PROOF_TIMEOUT', 'BE_APP_ENV_REQUIRED', 'BE_APP_MODULE_LIST_INLINE', 'BE_LITE_PROOF_LANE_MISSING', 'DB_ARCHIVE_MUTATED', 'DB_BASELINE_SHAPE', 'DB_EXTENSION_ALLOWED', 'DB_MIGRATION_DESTRUCTIVE', 'DB_PGMQ_DECLARED', 'DB_VAULT_WRITE', 'ENV_DUPLICATE', 'ENV_EXAMPLE_DRIFT', 'ENV_PUBLIC_SEALED', 'ENV_REQUIRED_UNSET', 'ENV_SEALED_UNREAD', 'ENV_VALUE_SHAPE', 'FE_STACK_CORE_MISSING', 'FE_STACK_UNLISTED', 'HFS_AGE_KEY_CUSTODY', 'HFS_APP_LEXICON', 'HFS_APP_WORKFLOW_TRIGGER', 'HFS_ASSET_KIND', 'HFS_ASSET_OVERSIZE', 'HFS_AUDIT_STEP_MISSING', 'HFS_AUTH_COOKIE_DOMAIN_MISSING', 'HFS_CANON_COPY_DRIFT', 'HFS_CI_TOOL_INSTALL', 'HFS_COVERAGE_BELOW', 'HFS_DB_DEPLOY_UNGUARDED', 'HFS_DEPENDABOT_MISSING', 'HFS_DEPLOY_COMPOSE_ON_VPS', 'HFS_DEPLOY_NO_LOGIC', 'HFS_DEPLOY_SCHEDULER', 'HFS_DEPLOY_WORKFLOW', 'HFS_DOC_COMMAND_UNKNOWN', 'HFS_DOC_ENV_UNKNOWN', 'HFS_DOC_PATH_MISSING', 'HFS_DOC_RULE_UNKNOWN', 'HFS_FEATURE_FILE_BUDGET', 'HFS_HOST_CRON', 'HFS_RELEASE_TAG_MISMATCH', 'HFS_SBOM_MISSING', 'HFS_SCHEDULE_UNDECLARED', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_SCRIPT_XTRACE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SEED_PII', 'HFS_UNUSED_SYMBOL', 'HFS_WORKFLOW_PIN', 'STACKS_DEPLOY_CONFIG_INVALID', 'STACKS_DNS_UNDECLARED', 'STACKS_HEALTHCHECK_MISSING', 'STACKS_HOST_UNROUTED', 'STACKS_INGRESS_UNDECLARED', 'STACKS_INGRESS_UNKNOWN', 'STACKS_LITE_REDIS_PERSISTENCE', 'STACKS_LITE_SELFHOSTED_DB', 'STACKS_MODEL_UNAVAILABLE', 'STACKS_PLACEMENT_MISSING', 'STACKS_PORT_PUBLISHED', 'STACKS_REALTIME_SCALED', 'STACKS_RELEASE_UNDECLARED', 'STACKS_SECOND_INGRESS', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'STACKS_SIDECAR_DB_UNSUPPORTED', 'STACKS_SIDECAR_VOLUME_UNDECLARED', 'STACKS_TLS_UNDECLARED', 'STACKS_UPDATE_POLICY_MISSING', 'STACK_TOOLING_REIMPLEMENTED',
53
56
  // codes-end
54
57
  ];
@@ -87,6 +90,8 @@ export const FINDERS = [
87
90
  stacksLiteBackingFindings,
88
91
  stackToolingFindings,
89
92
  stacksDeployFindings,
93
+ stacksLocalFindings,
94
+ stacksServiceImageFindings,
90
95
  stacksAuthCookieDomainFindings,
91
96
  ciCustodyFindings,
92
97
  // finders-end
@@ -1,4 +1,4 @@
1
- // R255-R256 inspect the final database access state: the application role, forced RLS, policies, views and storage prefixes.
1
+ // R255-R256 inspect the final database access state: the application role (its attributes and every grant on schema auth), forced RLS, policies, views and storage prefixes.
2
2
  import { found } from './read.mjs';
3
3
  import { parseSql } from '../sql/pg-parse.mjs';
4
4
  import { eachInOrder } from '../../lib/in-order.mjs';
@@ -20,6 +20,10 @@ const AND = ['AND', 'EXPR'].join('_');
20
20
  const OR = ['OR', 'EXPR'].join('_');
21
21
  const NOT = ['NOT', 'EXPR'].join('_');
22
22
  const SEQUENCE = ['OBJECT', 'SEQUENCE'].join('_');
23
+ const SCHEMA = ['OBJECT', 'SCHEMA'].join('_');
24
+ const TABLE = ['OBJECT', 'TABLE'].join('_');
25
+ const ALL_IN_SCHEMA = ['ACL', 'TARGET', 'ALL', 'IN', 'SCHEMA'].join('_');
26
+ const ACCOUNT_SCHEMA = 'auth';
23
27
  const MATVIEW = ['OBJECT', 'MATVIEW'].join('_');
24
28
  const note = (code, source, message, extra = {}) => found(code, source.file, `${source.file}:${source.line} ${message}`, { line: source.line, ...extra });
25
29
  function someNode(node, predicate) {
@@ -157,6 +161,39 @@ function roleFindings(analyses, appRole) {
157
161
  }
158
162
  return findings;
159
163
  }
164
+ /** What one grant statement names inside schema auth: the schema, a table or sequence of it, every table or sequence in it. */
165
+ function authTargets(grant) {
166
+ if (grant.objtype === SCHEMA)
167
+ return grant.objects.filter((object) => sval(object) === ACCOUNT_SCHEMA).map(() => `schema ${ACCOUNT_SCHEMA}`);
168
+ if (![TABLE, SEQUENCE].includes(grant.objtype))
169
+ return [];
170
+ if (grant.targtype === ALL_IN_SCHEMA)
171
+ return grant.objects.filter((object) => sval(object) === ACCOUNT_SCHEMA).map(() => `all ${grant.objtype === TABLE ? 'tables' : 'sequences'} in schema ${ACCOUNT_SCHEMA}`);
172
+ return grant.objects.filter((object) => object.RangeVar?.schemaname === ACCOUNT_SCHEMA).map((object) => relationName(object.RangeVar));
173
+ }
174
+ /**
175
+ * R255: the application role holds no privilege on schema auth, on a table or sequence in it, or by default privilege for the
176
+ * schema. The migrations are applied in order, so a grant that a later migration revokes (the contract step of an app that moved
177
+ * to the identity account doors) is not held; what remains at the end is reported at the grant that left it.
178
+ */
179
+ function authSchemaFindings(analyses, appRole) {
180
+ const held = new Map();
181
+ for (const { file, facts } of analyses) {
182
+ const statements = [
183
+ ...facts.grants.map((grant) => ({ grant, targets: authTargets(grant) })),
184
+ ...facts.defaultPrivileges.map((grant) => ({ grant, targets: grant.schemas.includes(ACCOUNT_SCHEMA) ? [`default privileges in schema ${ACCOUNT_SCHEMA}`] : [] })),
185
+ ].filter(({ grant }) => grant.grantees.includes(appRole));
186
+ for (const { grant, targets } of statements) {
187
+ for (const target of targets) {
188
+ const state = held.get(target) ?? { target, privileges: new Set() };
189
+ updatePrivileges(state, grant.grant, grant.all ? ['all'] : grant.privileges);
190
+ held.set(target, { ...state, file, line: grant.line });
191
+ }
192
+ }
193
+ }
194
+ return [...held.values()].filter((state) => state.privileges.size > 0)
195
+ .map((state) => note(DB_APP_ROLE, state, `${appRole} holds a grant on ${state.target}; reach accounts through the identity account doors (SECURITY DEFINER functions), never through a grant on schema ${ACCOUNT_SCHEMA}; an app on the old sign-in glue runs "starci app codemod account-doors", "starci app sync --execute" and "starci app codemod account-doors --contract" in one release`, { target: state.target }));
196
+ }
160
197
  function roleStatementFindings(statement, appRole) {
161
198
  const { type, node } = statement;
162
199
  if (!['CreateRoleStmt', 'AlterRoleStmt'].includes(type))
@@ -299,6 +336,7 @@ export async function databaseAccessFindings({ analyses, exposed, db = {} }) {
299
336
  return [
300
337
  ...policyFindings(policies, appRole, helpers),
301
338
  ...roleFindings(analyses, appRole),
339
+ ...authSchemaFindings(analyses, appRole),
302
340
  ...forceFindings(analyses, declared, appRole),
303
341
  ...viewFindings(analyses, grants, exposed),
304
342
  ...storagePrefixFindings(policies, helpers),
@@ -1,6 +1,10 @@
1
1
  import type { Parsed, Repo } from './rule-types.mjs';
2
2
  /** The npm workspaces of every app, in this order. */
3
3
  export declare const WORKSPACES: readonly string[];
4
+ /** 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. */
5
+ export declare const ROOT_PACKAGES_WORKSPACE = "packages/*";
6
+ /** The workspaces the tracked tree `files` calls for: the fe ones, then `packages/*` when a root package tracks its package.json. */
7
+ export declare const workspacesFor: (files: readonly string[]) => readonly string[];
4
8
  /** 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. */
5
9
  export declare const FE_APP_SCRIPTS: Readonly<Record<string, string>>;
6
10
  /** The package manager every app root declares (turbo reads it to parse the lockfile); the starci app scaffold writes it. */