@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
@@ -0,0 +1,86 @@
1
+ // Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
2
+ import "reflect-metadata"
3
+ import { Test } from "@nestjs/testing"
4
+ import { LessThanOrEqual, MoreThan } from "typeorm"
5
+ import { FakeClock, mockEntityManager, queueQueryBuilders } from "@starci/jest-preset"
6
+ import type { MockEntityManager } from "@starci/jest-preset"
7
+ import { CLOCK } from "@modules/platform/clock"
8
+ import { PRIMARY_ENTITY_MANAGER } from "@modules/platform/database"
9
+ import { IdentityAttemptEntity } from "./persistence/entities"
10
+ import { IdentityRateCounterService } from "./rate-counter.service"
11
+
12
+ const AT = "2026-02-03T04:05:06.000Z"
13
+ const NOW = new Date(AT)
14
+ const RESET_AT = new Date("2026-02-03T04:20:06.000Z")
15
+ const WINDOW_MS = 900_000
16
+
17
+ const isIncrement = (value: unknown): value is { readonly hits: () => string } =>
18
+ typeof value === "object" && value !== null && "hits" in value && typeof value.hits === "function"
19
+
20
+ const build = async (entityManager: MockEntityManager) => {
21
+ const moduleRef = await Test.createTestingModule({
22
+ providers: [
23
+ IdentityRateCounterService,
24
+ { provide: PRIMARY_ENTITY_MANAGER, useValue: entityManager },
25
+ { provide: CLOCK, useValue: new FakeClock(AT) },
26
+ ],
27
+ }).compile()
28
+ return moduleRef.get(IdentityRateCounterService)
29
+ }
30
+
31
+ describe("IdentityRateCounterService.consume", () => {
32
+ it("counts a hit in the live window and compares it with the limit", async () => {
33
+ const entityManager = mockEntityManager()
34
+ const [counting] = queueQueryBuilders(entityManager, [{ execute: { raw: [{ hits: 3 }] } }, { execute: { raw: [{ hits: 4 }] } }])
35
+ const subject = await build(entityManager)
36
+
37
+ expect(await subject.consume("login:abc", WINDOW_MS, 3)).toBe(true)
38
+ expect(await subject.consume("login:abc", WINDOW_MS, 3)).toBe(false)
39
+
40
+ expect(counting?.callsOf("where")[0]).toEqual([{ key: "login:abc", resetAt: MoreThan(NOW) }])
41
+ const [patch] = counting?.callsOf("set")[0] ?? []
42
+ expect(isIncrement(patch)).toBe(true)
43
+ if (isIncrement(patch)) expect(patch.hits()).toBe("hits + 1")
44
+ })
45
+
46
+ it("restarts an ended window at one hit", async () => {
47
+ const entityManager = mockEntityManager({ update: [IdentityAttemptEntity, { affected: 1 }] })
48
+ queueQueryBuilders(entityManager, [{ execute: { raw: [] } }])
49
+ const subject = await build(entityManager)
50
+
51
+ expect(await subject.consume("login:abc", WINDOW_MS, 3)).toBe(true)
52
+
53
+ expect(entityManager.update).toHaveBeenCalledWith(IdentityAttemptEntity, { key: "login:abc", resetAt: LessThanOrEqual(NOW) }, { hits: 1, resetAt: RESET_AT })
54
+ })
55
+
56
+ it("opens a window when the restart reports no affected count", async () => {
57
+ const entityManager = mockEntityManager({ update: [IdentityAttemptEntity, {}] })
58
+ const [, inserting] = queueQueryBuilders(entityManager, [{ execute: { raw: [] } }, { execute: { raw: [] } }])
59
+ const subject = await build(entityManager)
60
+
61
+ expect(await subject.consume("login:abc", WINDOW_MS, 3)).toBe(true)
62
+
63
+ expect(inserting?.callsOf("execute")).toHaveLength(1)
64
+ })
65
+
66
+ it("opens a missing window at one hit, and a limit of zero admits nobody", async () => {
67
+ const entityManager = mockEntityManager({ update: [IdentityAttemptEntity, { affected: 0 }] })
68
+ const [, inserting] = queueQueryBuilders(entityManager, [{ execute: { raw: [] } }, { execute: { raw: [] } }])
69
+ const subject = await build(entityManager)
70
+
71
+ expect(await subject.consume("login:abc", WINDOW_MS, 0)).toBe(false)
72
+
73
+ expect(inserting?.callsOf("values")[0]).toEqual([{ key: "login:abc", hits: 1, resetAt: RESET_AT }])
74
+ })
75
+ })
76
+
77
+ describe("IdentityRateCounterService.clear", () => {
78
+ it("forgets the window of a key", async () => {
79
+ const entityManager = mockEntityManager({ delete: [IdentityAttemptEntity, {}] })
80
+ const subject = await build(entityManager)
81
+
82
+ await subject.clear("login:abc")
83
+
84
+ expect(entityManager.delete).toHaveBeenCalledWith(IdentityAttemptEntity, { key: "login:abc" })
85
+ })
86
+ })
@@ -0,0 +1,45 @@
1
+ // Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
2
+ import { Injectable } from "@nestjs/common"
3
+ import { LessThanOrEqual, MoreThan } from "typeorm"
4
+ import type { EntityManager } from "typeorm"
5
+ import type { RateCounter } from "@modules/integrations/passport"
6
+ import { InjectClock } from "@modules/platform/clock"
7
+ import type { Clock } from "@modules/platform/clock"
8
+ import { InjectPrimaryEntityManager, returnedRows, rowShape } from "@modules/platform/database"
9
+ import { IdentityAttemptEntity } from "./persistence/entities"
10
+
11
+ const isCountedWindow = rowShape({ hits: "number" })
12
+
13
+ @Injectable()
14
+ /** The package's rate counter over identity_attempts: a fixed window per key, counted in one statement so every replica agrees. */
15
+ export class IdentityRateCounterService implements RateCounter {
16
+ constructor(
17
+ @InjectPrimaryEntityManager() private readonly entityManager: EntityManager,
18
+ @InjectClock() private readonly clock: Clock,
19
+ ) {}
20
+
21
+ /** Counts one hit in the window and says whether the key is still within the limit. */
22
+ async consume(key: string, windowMs: number, limit: number): Promise<boolean> {
23
+ const now = this.clock.now()
24
+ const counted = await this.entityManager
25
+ .createQueryBuilder(IdentityAttemptEntity, "attempt")
26
+ .update()
27
+ .set({ hits: () => "hits + 1" })
28
+ .where({ key, resetAt: MoreThan(now) })
29
+ .returning("hits")
30
+ .execute()
31
+ const [live] = returnedRows(counted.raw, isCountedWindow)
32
+ if (live !== undefined) return live.hits <= limit
33
+ const resetAt = new Date(now.getTime() + windowMs)
34
+ const restarted = await this.entityManager.update(IdentityAttemptEntity, { key, resetAt: LessThanOrEqual(now) }, { hits: 1, resetAt })
35
+ if ((restarted.affected ?? 0) === 0) {
36
+ await this.entityManager.createQueryBuilder(IdentityAttemptEntity, "attempt").insert().values({ key, hits: 1, resetAt }).orIgnore().execute()
37
+ }
38
+ return limit >= 1
39
+ }
40
+
41
+ /** Forgets the window of a key. */
42
+ async clear(key: string): Promise<void> {
43
+ await this.entityManager.delete(IdentityAttemptEntity, { key })
44
+ }
45
+ }
@@ -0,0 +1,97 @@
1
+ // Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
2
+ import "reflect-metadata"
3
+ import { Test } from "@nestjs/testing"
4
+ import { LessThan, Not } from "typeorm"
5
+ import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
6
+ import type { MockEntityManager } from "@starci/jest-preset"
7
+ import { PRIMARY_ENTITY_MANAGER } from "@modules/platform/database"
8
+ import { IdentityRefreshTokenEntity, IdentitySessionEntity } from "./persistence/entities"
9
+ import { IdentitySessionStoreService } from "./session-store.service"
10
+
11
+ const NOW = new Date("2026-02-03T04:05:06.000Z")
12
+ const LATER = new Date("2026-03-03T04:05:06.000Z")
13
+
14
+ const row = (usedAt: Date | null): IdentityRefreshTokenEntity => ({
15
+ id: "t-1",
16
+ sessionId: "s-1",
17
+ userId: "u-1",
18
+ tokenHash: "h-1",
19
+ createdAt: NOW,
20
+ expiresAt: LATER,
21
+ usedAt,
22
+ })
23
+
24
+ const build = async (entityManager: MockEntityManager = mockEntityManager()) => {
25
+ const transaction = fakeTransaction(entityManager)
26
+ const moduleRef = await Test.createTestingModule({
27
+ providers: [IdentitySessionStoreService, { provide: PRIMARY_ENTITY_MANAGER, useValue: entityManager }],
28
+ }).compile()
29
+ return { subject: moduleRef.get(IdentitySessionStoreService), entityManager, transaction }
30
+ }
31
+
32
+ describe("IdentitySessionStoreService.transaction", () => {
33
+ it("opens a session and stores a live token in one transaction", async () => {
34
+ const { subject, entityManager, transaction } = await build(mockEntityManager({ insert: [[IdentitySessionEntity, {}], [IdentityRefreshTokenEntity, {}]] }))
35
+
36
+ await subject.transaction(async (tx) => {
37
+ await tx.openSession({ id: "s-1", userId: "u-1", now: NOW, userAgent: "agent", ip: "203.0.113.9" })
38
+ await tx.insertToken({ id: "t-1", sessionId: "s-1", userId: "u-1", tokenHash: "h-1", expiresAt: LATER })
39
+ })
40
+
41
+ expect(entityManager.insert).toHaveBeenCalledWith(IdentitySessionEntity, { id: "s-1", userId: "u-1", createdAt: NOW, refreshedAt: NOW, userAgent: "agent", ip: "203.0.113.9" })
42
+ expect(entityManager.insert).toHaveBeenCalledWith(IdentityRefreshTokenEntity, { id: "t-1", sessionId: "s-1", userId: "u-1", tokenHash: "h-1", expiresAt: LATER, usedAt: null })
43
+ expect(transaction.commits).toBe(1)
44
+ })
45
+
46
+ it("reads the token for update, marks it used and drops the expired ones", async () => {
47
+ const { subject, entityManager } = await build(
48
+ mockEntityManager({
49
+ findOne: [[IdentityRefreshTokenEntity, row(NOW)], [IdentityRefreshTokenEntity, null]],
50
+ update: [IdentityRefreshTokenEntity, {}],
51
+ delete: [IdentityRefreshTokenEntity, {}],
52
+ }),
53
+ )
54
+
55
+ const found = await subject.transaction((tx) => tx.findTokenForUpdate("h-1"))
56
+ const missing = await subject.transaction((tx) => tx.findTokenForUpdate("h-2"))
57
+ await subject.transaction(async (tx) => {
58
+ await tx.markTokenUsed("t-1", NOW)
59
+ await tx.deleteExpiredTokens("u-1", NOW)
60
+ })
61
+
62
+ expect(found).toEqual({ id: "t-1", sessionId: "s-1", userId: "u-1", tokenHash: "h-1", expiresAt: LATER, usedAt: NOW })
63
+ expect(missing).toBeNull()
64
+ expect(entityManager.findOne).toHaveBeenCalledWith(IdentityRefreshTokenEntity, { where: { tokenHash: "h-1" }, lock: { mode: "pessimistic_write" } })
65
+ expect(entityManager.update).toHaveBeenCalledWith(IdentityRefreshTokenEntity, { id: "t-1" }, { usedAt: NOW })
66
+ expect(entityManager.delete).toHaveBeenCalledWith(IdentityRefreshTokenEntity, { userId: "u-1", expiresAt: LessThan(NOW) })
67
+ })
68
+
69
+ it("rolls the transaction back when the work throws", async () => {
70
+ const { subject, transaction } = await build()
71
+
72
+ await expect(subject.transaction(() => Promise.reject(new Error("disk full")))).rejects.toThrow("disk full")
73
+
74
+ expect(transaction.rollbacks).toBe(1)
75
+ })
76
+ })
77
+
78
+ describe("IdentitySessionStoreService", () => {
79
+ it("finds a token outside a transaction, or none", async () => {
80
+ const { subject } = await build(mockEntityManager({ findOne: [[IdentityRefreshTokenEntity, row(null)], [IdentityRefreshTokenEntity, null]] }))
81
+
82
+ expect(await subject.findToken("h-1")).toMatchObject({ id: "t-1", usedAt: null })
83
+ expect(await subject.findToken("h-2")).toBeNull()
84
+ })
85
+
86
+ it("touches, closes and closes the others of a session", async () => {
87
+ const { subject, entityManager } = await build(mockEntityManager({ update: [IdentitySessionEntity, {}], delete: [IdentitySessionEntity, {}] }))
88
+
89
+ await subject.touchSession("s-1", NOW)
90
+ await subject.closeSession("s-1")
91
+ await subject.closeOtherSessions("u-1", "s-1")
92
+
93
+ expect(entityManager.update).toHaveBeenCalledWith(IdentitySessionEntity, { id: "s-1" }, { refreshedAt: NOW })
94
+ expect(entityManager.delete).toHaveBeenCalledWith(IdentitySessionEntity, { id: "s-1" })
95
+ expect(entityManager.delete).toHaveBeenCalledWith(IdentitySessionEntity, { userId: "u-1", id: Not("s-1") })
96
+ })
97
+ })
@@ -0,0 +1,81 @@
1
+ // Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
2
+ import { Injectable } from "@nestjs/common"
3
+ import { LessThan, Not } from "typeorm"
4
+ import type { EntityManager } from "typeorm"
5
+ import type { NewSession, RefreshTokenRecord, SessionStore, SessionTransaction } from "@modules/integrations/passport"
6
+ import { InjectPrimaryEntityManager } from "@modules/platform/database"
7
+ import { IdentityRefreshTokenEntity, IdentitySessionEntity } from "./persistence/entities"
8
+
9
+ const recordOf = (row: IdentityRefreshTokenEntity): RefreshTokenRecord => ({
10
+ id: row.id,
11
+ sessionId: row.sessionId,
12
+ userId: row.userId,
13
+ tokenHash: row.tokenHash,
14
+ expiresAt: row.expiresAt,
15
+ usedAt: row.usedAt,
16
+ })
17
+
18
+ const transactionOf = (scoped: EntityManager): SessionTransaction => ({
19
+ openSession: async (session: NewSession) => {
20
+ await scoped.insert(IdentitySessionEntity, {
21
+ id: session.id,
22
+ userId: session.userId,
23
+ createdAt: session.now,
24
+ refreshedAt: session.now,
25
+ userAgent: session.userAgent,
26
+ ip: session.ip,
27
+ })
28
+ },
29
+ insertToken: async (token) => {
30
+ await scoped.insert(IdentityRefreshTokenEntity, {
31
+ id: token.id,
32
+ sessionId: token.sessionId,
33
+ userId: token.userId,
34
+ tokenHash: token.tokenHash,
35
+ expiresAt: token.expiresAt,
36
+ usedAt: null,
37
+ })
38
+ },
39
+ findTokenForUpdate: async (tokenHash) => {
40
+ const row = await scoped.findOne(IdentityRefreshTokenEntity, { where: { tokenHash }, lock: { mode: "pessimistic_write" } })
41
+ return row === null ? null : recordOf(row)
42
+ },
43
+ markTokenUsed: async (tokenId, at) => {
44
+ await scoped.update(IdentityRefreshTokenEntity, { id: tokenId }, { usedAt: at })
45
+ },
46
+ deleteExpiredTokens: async (userId, before) => {
47
+ await scoped.delete(IdentityRefreshTokenEntity, { userId, expiresAt: LessThan(before) })
48
+ },
49
+ })
50
+
51
+ @Injectable()
52
+ /** The package's session store over the entities of its tables: one transaction per rotation, the token row locked for update. */
53
+ export class IdentitySessionStoreService implements SessionStore {
54
+ constructor(@InjectPrimaryEntityManager() private readonly entityManager: EntityManager) {}
55
+
56
+ /** Runs the work in one database transaction. */
57
+ transaction<T>(work: (tx: SessionTransaction) => Promise<T>): Promise<T> {
58
+ return this.entityManager.transaction((scoped) => work(transactionOf(scoped)))
59
+ }
60
+
61
+ /** The token with this digest, or null. */
62
+ async findToken(tokenHash: string): Promise<RefreshTokenRecord | null> {
63
+ const row = await this.entityManager.findOne(IdentityRefreshTokenEntity, { where: { tokenHash } })
64
+ return row === null ? null : recordOf(row)
65
+ }
66
+
67
+ /** Marks a session refreshed. */
68
+ async touchSession(sessionId: string, now: Date): Promise<void> {
69
+ await this.entityManager.update(IdentitySessionEntity, { id: sessionId }, { refreshedAt: now })
70
+ }
71
+
72
+ /** Ends one session; its tokens go with it (the foreign key cascades). */
73
+ async closeSession(sessionId: string): Promise<void> {
74
+ await this.entityManager.delete(IdentitySessionEntity, { id: sessionId })
75
+ }
76
+
77
+ /** Ends every session of an account but one. */
78
+ async closeOtherSessions(userId: string, keepSessionId: string): Promise<void> {
79
+ await this.entityManager.delete(IdentitySessionEntity, { userId, id: Not(keepSessionId) })
80
+ }
81
+ }
@@ -0,0 +1,95 @@
1
+ // Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
2
+ import "reflect-metadata"
3
+ import { Test } from "@nestjs/testing"
4
+ import { mock } from "@starci/jest-preset"
5
+ import { AuthError, PassportError, PassportErrorCode } from "@modules/integrations/passport"
6
+ import type { AccessGrant, AuthDoors, AuthSessions, GoogleRedirect, HttpExchange } from "@modules/integrations/passport"
7
+ import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
8
+ import { IDENTITY_AUTH_DOORS, IDENTITY_AUTH_SESSIONS } from "./identity.decorators"
9
+ import { IdentitySignInService } from "./sign-in.service"
10
+
11
+ const GRANT: AccessGrant = { accessToken: "token", expiresIn: 900, expiresAt: "2026-02-03T04:20:06.000Z", user: { id: "u-1", email: "a@example.test" } }
12
+ const REDIRECT: GoogleRedirect = { url: "https://app.example.test/", statusCode: 303 }
13
+ const EXCHANGE = mock<HttpExchange>()
14
+
15
+ const build = async () => {
16
+ const doors = mock<AuthDoors>()
17
+ const sessions = mock<AuthSessions>()
18
+ const moduleRef = await Test.createTestingModule({
19
+ providers: [IdentitySignInService, { provide: IDENTITY_AUTH_DOORS, useValue: doors }, { provide: IDENTITY_AUTH_SESSIONS, useValue: sessions }],
20
+ }).compile()
21
+ return { subject: moduleRef.get(IdentitySignInService), doors, sessions }
22
+ }
23
+
24
+ describe("IdentitySignInService", () => {
25
+ it("hands each cookie door the exchange and answers what the package answers", async () => {
26
+ const { subject, doors } = await build()
27
+ doors.login.mockResolvedValue(GRANT)
28
+ doors.refresh.mockResolvedValue(GRANT)
29
+ doors.session.mockResolvedValue(GRANT)
30
+ doors.logout.mockResolvedValue()
31
+ doors.beginGoogle.mockResolvedValue()
32
+ doors.completeGoogle.mockResolvedValue(REDIRECT)
33
+
34
+ expect(await subject.login(EXCHANGE)).toBe(GRANT)
35
+ expect(await subject.refresh(EXCHANGE)).toBe(GRANT)
36
+ expect(await subject.session(EXCHANGE)).toBe(GRANT)
37
+ await subject.logout(EXCHANGE)
38
+ await subject.beginGoogle(EXCHANGE)
39
+ expect(await subject.completeGoogle(EXCHANGE)).toBe(REDIRECT)
40
+
41
+ for (const door of [doors.login, doors.refresh, doors.session, doors.logout, doors.beginGoogle, doors.completeGoogle]) expect(door).toHaveBeenCalledWith(EXCHANGE)
42
+ })
43
+
44
+ it("serves the public keys", async () => {
45
+ const { subject, sessions } = await build()
46
+ sessions.jwks.mockResolvedValue({ keys: [] })
47
+
48
+ expect(await subject.jwks()).toEqual({ keys: [] })
49
+ })
50
+
51
+ it("signs the caller out of every other device, keeping the session of the token", async () => {
52
+ const { subject, sessions } = await build()
53
+ sessions.endOthers.mockResolvedValue()
54
+
55
+ await subject.endOthers({ id: "u-1", role: "authenticated", sessionId: "s-1" })
56
+
57
+ expect(sessions.endOthers).toHaveBeenCalledWith("u-1", "s-1")
58
+ })
59
+
60
+ it("refuses to sign out the other devices of a token that names no session", async () => {
61
+ const { subject, sessions } = await build()
62
+ sessions.endOthers.mockRejectedValue(new AuthError("IDENTITY_UNAUTHENTICATED"))
63
+
64
+ await expect(subject.endOthers({ id: "u-1", role: "authenticated" })).rejects.toMatchObject({ code: IdentityErrorCode.Unauthenticated })
65
+ expect(sessions.endOthers).toHaveBeenCalledWith("u-1", undefined)
66
+ })
67
+
68
+ it("turns a refusal of the package into the error family that owns its code", async () => {
69
+ const { subject, doors } = await build()
70
+ doors.refresh.mockRejectedValue(new AuthError("IDENTITY_REFRESH_REUSED"))
71
+ doors.login.mockRejectedValue(new AuthError("PASSPORT_GOOGLE_REJECTED"))
72
+
73
+ const identity: unknown = await subject.refresh(EXCHANGE).catch((error: unknown) => error)
74
+ const passport: unknown = await subject.login(EXCHANGE).catch((error: unknown) => error)
75
+
76
+ expect(identity).toBeInstanceOf(IdentityError)
77
+ expect(identity).toMatchObject({ code: IdentityErrorCode.RefreshReused })
78
+ expect(passport).toBeInstanceOf(PassportError)
79
+ expect(passport).toMatchObject({ code: PassportErrorCode.GoogleRejected })
80
+ })
81
+
82
+ it("reads a code neither family names as unauthenticated", async () => {
83
+ const { subject, doors } = await build()
84
+ doors.login.mockRejectedValue(Object.assign(new AuthError("IDENTITY_REFRESH_REUSED"), { code: "NOT_A_CODE" }))
85
+
86
+ await expect(subject.login(EXCHANGE)).rejects.toMatchObject({ code: IdentityErrorCode.Unauthenticated })
87
+ })
88
+
89
+ it("rethrows a failure that is not a sign-in refusal as it is", async () => {
90
+ const { subject, doors } = await build()
91
+ doors.login.mockRejectedValue(new Error("boom"))
92
+
93
+ await expect(subject.login(EXCHANGE)).rejects.toThrow("boom")
94
+ })
95
+ })
@@ -0,0 +1,93 @@
1
+ // Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
2
+ import { Injectable } from "@nestjs/common"
3
+ import { AuthError, PassportError, PassportErrorCode } from "@modules/integrations/passport"
4
+ import type { Principal } from "@modules/platform/cqrs"
5
+ import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
6
+ import { InjectAuthDoors, InjectAuthSessions } from "./identity.decorators"
7
+ import type { AccessGrant, AuthDoors, AuthSessions, GoogleRedirect, HttpExchange, JwksDocument } from "@modules/integrations/passport"
8
+
9
+ /** The wire codes of the package that belong to the passport family (the token, key and Google legs). */
10
+ const PASSPORT_CODES: ReadonlyArray<PassportErrorCode> = [
11
+ PassportErrorCode.AccessTokenInvalid,
12
+ PassportErrorCode.KeyInvalid,
13
+ PassportErrorCode.MissingCredentials,
14
+ PassportErrorCode.GoogleStateInvalid,
15
+ PassportErrorCode.GoogleRejected,
16
+ PassportErrorCode.GoogleUnavailable,
17
+ ]
18
+
19
+ /** The wire codes of the package that belong to the identity family (the session and the sign-in refusals). */
20
+ const IDENTITY_CODES: ReadonlyArray<IdentityErrorCode> = [
21
+ IdentityErrorCode.RefreshInvalid,
22
+ IdentityErrorCode.RefreshReused,
23
+ IdentityErrorCode.AccountDisabled,
24
+ IdentityErrorCode.TooManyAttempts,
25
+ IdentityErrorCode.PairRefused,
26
+ IdentityErrorCode.EmailNotConfirmed,
27
+ ]
28
+
29
+ /** The error of the family that owns a wire code of the package; a code neither family names reads as unauthenticated. */
30
+ const familyOf = (error: AuthError): IdentityError | PassportError => {
31
+ const passport = PASSPORT_CODES.find((each) => each === error.code)
32
+ if (passport !== undefined) return new PassportError({ code: passport, cause: error })
33
+ const identity = IDENTITY_CODES.find((each) => each === error.code) ?? IdentityErrorCode.Unauthenticated
34
+ return new IdentityError({ code: identity, cause: error })
35
+ }
36
+
37
+ @Injectable()
38
+ /** The cookie doors of sign-in for the transport: the package's doors with its refusals turned into the passport error family the errors filter maps. */
39
+ export class IdentitySignInService {
40
+ constructor(
41
+ @InjectAuthDoors() private readonly doors: AuthDoors,
42
+ @InjectAuthSessions() private readonly sessions: AuthSessions,
43
+ ) {}
44
+
45
+ /** Email and password through the local strategy; the refresh token goes into the cookie. */
46
+ login(exchange: HttpExchange): Promise<AccessGrant> {
47
+ return this.refusing(() => this.doors.login(exchange))
48
+ }
49
+
50
+ /** Rotates the cookie token and answers a new access token. */
51
+ refresh(exchange: HttpExchange): Promise<AccessGrant> {
52
+ return this.refusing(() => this.doors.refresh(exchange))
53
+ }
54
+
55
+ /** Answers a fresh access token for the cookie session without rotating the cookie. */
56
+ session(exchange: HttpExchange): Promise<AccessGrant> {
57
+ return this.refusing(() => this.doors.session(exchange))
58
+ }
59
+
60
+ /** Ends the session of the cookie and clears it. */
61
+ logout(exchange: HttpExchange): Promise<void> {
62
+ return this.refusing(() => this.doors.logout(exchange))
63
+ }
64
+
65
+ /** Sends the browser to the Google consent screen. */
66
+ beginGoogle(exchange: HttpExchange): Promise<void> {
67
+ return this.refusing(() => this.doors.beginGoogle(exchange))
68
+ }
69
+
70
+ /** Finishes the Google round trip; a failure redirects to the sign-in page instead of throwing. */
71
+ completeGoogle(exchange: HttpExchange): Promise<GoogleRedirect> {
72
+ return this.refusing(() => this.doors.completeGoogle(exchange))
73
+ }
74
+
75
+ /** Signs the person out of every other device; the session of the caller stays. */
76
+ endOthers(principal: Principal): Promise<void> {
77
+ return this.refusing(() => this.sessions.endOthers(principal.id, principal.sessionId))
78
+ }
79
+
80
+ /** The public key document access tokens verify against. */
81
+ jwks(): Promise<JwksDocument> {
82
+ return this.refusing(() => this.sessions.jwks())
83
+ }
84
+
85
+ private async refusing<T>(work: () => Promise<T>): Promise<T> {
86
+ try {
87
+ return await work()
88
+ } catch (cause) {
89
+ if (cause instanceof AuthError) throw familyOf(cause)
90
+ throw cause
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,40 @@
1
+ // Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
2
+ const BEFORE = new Date("2026-02-03T04:00:00.000Z")
3
+
4
+ /** The kinds of account the sign-in reads by address. */
5
+ export type AccountByEmailVariant = "live" | "disabled" | "unconfirmed" | "passwordless" | "invited"
6
+
7
+ /** The kinds of account the sign-in reads by id. */
8
+ export type AccountByIdVariant = "live" | "disabled" | "addressless"
9
+
10
+ /** A row of the by-address door: `disabled` is what the database decided at its own now(). */
11
+ export interface AccountByEmailRow {
12
+ id: string
13
+ email: string
14
+ encrypted_password: string | null
15
+ email_confirmed_at: Date | null
16
+ disabled: boolean
17
+ }
18
+
19
+ /** A row of the by-id door. */
20
+ export interface AccountByIdRow {
21
+ id: string
22
+ email: string | null
23
+ disabled: boolean
24
+ }
25
+
26
+ /** The row the by-address door answers for a confirmed password account of the given variant. */
27
+ export const accountByEmailRow = (variant: AccountByEmailVariant = "live"): AccountByEmailRow => ({
28
+ id: "u-1",
29
+ email: "a@example.test",
30
+ encrypted_password: variant === "passwordless" || variant === "invited" ? null : "hash",
31
+ email_confirmed_at: variant === "unconfirmed" || variant === "invited" ? null : BEFORE,
32
+ disabled: variant === "disabled",
33
+ })
34
+
35
+ /** The row the by-id door answers for the account of the given variant; `id` is the account the spec stands for. */
36
+ export const accountByIdRow = (variant: AccountByIdVariant = "live", id = "u-1"): AccountByIdRow => ({
37
+ id,
38
+ email: variant === "addressless" ? null : "a@example.test",
39
+ disabled: variant === "disabled",
40
+ })
@@ -1,12 +1,5 @@
1
- import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
1
+ import { defineCapabilityModule } from "@starci/nest-kit"
2
2
  import type { @@Provider@@InboxOptions } from "./@@provider@@-inbox.options"
3
3
 
4
- const built = new ConfigurableModuleBuilder<@@Provider@@InboxOptions>()
5
- .setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
6
- .build()
7
- type Host = ConfigurableModuleHost<@@Provider@@InboxOptions, "register", "create", { isGlobal: boolean }>
8
-
9
4
  /** The configurable-module class the module extends. */
10
- export const ConfigurableModuleClass: Host["ConfigurableModuleClass"] = built.ConfigurableModuleClass
11
- /** The options of a synchronous registration, for the register signature. */
12
- export const OPTIONS_TYPE: Host["OPTIONS_TYPE"] = built.OPTIONS_TYPE
5
+ export const { ConfigurableModuleClass, OPTIONS_TYPE } = defineCapabilityModule<@@Provider@@InboxOptions>()
@@ -1,14 +1,5 @@
1
- import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
1
+ import { defineCapabilityModule } from "@starci/nest-kit"
2
2
  import type { EventBusOptions } from "./event-bus.options"
3
3
 
4
- const built = new ConfigurableModuleBuilder<EventBusOptions>()
5
- .setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
6
- .build()
7
- type Host = ConfigurableModuleHost<EventBusOptions, "register", "create", { isGlobal: boolean }>
8
-
9
4
  /** The configurable-module class the module extends. */
10
- export const ConfigurableModuleClass: Host["ConfigurableModuleClass"] = built.ConfigurableModuleClass
11
- /** The injection token of the registered options. */
12
- export const MODULE_OPTIONS_TOKEN: Host["MODULE_OPTIONS_TOKEN"] = built.MODULE_OPTIONS_TOKEN
13
- /** The options of a synchronous registration, for the register signature. */
14
- export const OPTIONS_TYPE: Host["OPTIONS_TYPE"] = built.OPTIONS_TYPE
5
+ export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } = defineCapabilityModule<EventBusOptions>()
@@ -1,12 +1,5 @@
1
- import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
1
+ import { defineCapabilityModule } from "@starci/nest-kit"
2
2
  import type { InboxOptions } from "./inbox.options"
3
3
 
4
- const built = new ConfigurableModuleBuilder<InboxOptions>()
5
- .setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
6
- .build()
7
- type Host = ConfigurableModuleHost<InboxOptions, "register", "create", { isGlobal: boolean }>
8
-
9
4
  /** The configurable-module class the module extends. */
10
- export const ConfigurableModuleClass: Host["ConfigurableModuleClass"] = built.ConfigurableModuleClass
11
- /** The options of a synchronous registration, for the register signature. */
12
- export const OPTIONS_TYPE: Host["OPTIONS_TYPE"] = built.OPTIONS_TYPE
5
+ export const { ConfigurableModuleClass, OPTIONS_TYPE } = defineCapabilityModule<InboxOptions>()
@@ -1,12 +1,6 @@
1
- import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
1
+ import { defineConfigurableModule } from "@starci/nest-kit"
2
2
  import type { @@Feature@@ApiOptions } from "./@@feature@@.options"
3
3
 
4
- const built = new ConfigurableModuleBuilder<@@Feature@@ApiOptions>().setClassMethodName("register").build()
5
- type Host = ConfigurableModuleHost<@@Feature@@ApiOptions, "register", "create", {}>
6
-
7
4
  /** The configurable-module base of the @@feature@@ module. */
8
- export const ConfigurableModuleClass: Host["ConfigurableModuleClass"] = built.ConfigurableModuleClass
9
- /** The injection token of the registered options. */
10
- export const MODULE_OPTIONS_TOKEN: Host["MODULE_OPTIONS_TOKEN"] = built.MODULE_OPTIONS_TOKEN
11
- /** The options of a synchronous registration, for the register signature. */
12
- export const OPTIONS_TYPE: Host["OPTIONS_TYPE"] = built.OPTIONS_TYPE
5
+ export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
6
+ defineConfigurableModule<@@Feature@@ApiOptions>()
@@ -1,12 +1,6 @@
1
- import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
1
+ import { defineConfigurableModule } from "@starci/nest-kit"
2
2
  import type { @@Feature@@GraphqlOptions } from "./@@feature@@-graphql.options"
3
3
 
4
- const built = new ConfigurableModuleBuilder<@@Feature@@GraphqlOptions>().setClassMethodName("register").build()
5
- type Host = ConfigurableModuleHost<@@Feature@@GraphqlOptions, "register", "create", {}>
6
-
7
4
  /** The configurable-module base of the @@feature@@-graphql module. */
8
- export const ConfigurableModuleClass: Host["ConfigurableModuleClass"] = built.ConfigurableModuleClass
9
- /** The injection token of the registered options. */
10
- export const MODULE_OPTIONS_TOKEN: Host["MODULE_OPTIONS_TOKEN"] = built.MODULE_OPTIONS_TOKEN
11
- /** The options of a synchronous registration, for the register signature. */
12
- export const OPTIONS_TYPE: Host["OPTIONS_TYPE"] = built.OPTIONS_TYPE
5
+ export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
6
+ defineConfigurableModule<@@Feature@@GraphqlOptions>()
@@ -1,12 +1,5 @@
1
- import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
1
+ import { defineConfigurableModule } from "@starci/nest-kit"
2
2
  import type { @@Group@@Options } from "./@@group@@.options"
3
3
 
4
- const built = new ConfigurableModuleBuilder<@@Group@@Options>().setClassMethodName("register").build()
5
- type Host = ConfigurableModuleHost<@@Group@@Options, "register", "create", {}>
6
-
7
4
  /** The configurable-module base of the @@group@@ module. */
8
- export const ConfigurableModuleClass: Host["ConfigurableModuleClass"] = built.ConfigurableModuleClass
9
- /** The injection token of the registered options. */
10
- export const MODULE_OPTIONS_TOKEN: Host["MODULE_OPTIONS_TOKEN"] = built.MODULE_OPTIONS_TOKEN
11
- /** The options of a synchronous registration, for the register signature. */
12
- export const OPTIONS_TYPE: Host["OPTIONS_TYPE"] = built.OPTIONS_TYPE
5
+ export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } = defineConfigurableModule<@@Group@@Options>()