@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,51 @@
1
+ import { Button, Form, Input, Link, Text } from "@starci/grammar/common"
2
+
3
+ /** One destination under the form: a sibling door or the sign-in page. */
4
+ export interface EmailDoorLink {
5
+ readonly href: string
6
+ readonly label: string
7
+ }
8
+
9
+ /** Copy, fields, state and action supplied by the connected email-door block. */
10
+ export interface EmailDoorFormBaseProps {
11
+ readonly props: {
12
+ readonly title: string
13
+ readonly description: string
14
+ /** The label of the email field, or undefined when the door takes none. */
15
+ readonly emailLabel: string | undefined
16
+ /** The label of the password field, or undefined when the door takes none. */
17
+ readonly passwordLabel: string | undefined
18
+ readonly submitLabel: string
19
+ readonly pending: boolean
20
+ /** The resolved message of the last refusal, or undefined. */
21
+ readonly errorMessage: string | undefined
22
+ /** The message shown once the back end accepted the request, or undefined while it has not. */
23
+ readonly doneMessage: string | undefined
24
+ readonly links: readonly EmailDoorLink[]
25
+ }
26
+ readonly on: { readonly submit: (input: FormData) => void }
27
+ }
28
+
29
+ /** Draws one email door (sign up, resend, forgot, reset) without reading navigation, copy or request state. */
30
+ export const EmailDoorFormBase = (props: EmailDoorFormBaseProps) => (
31
+ <>
32
+ {props.props.doneMessage === undefined ? (
33
+ <Form label={props.props.title} isPending={props.props.pending} onSubmit={props.on.submit}>
34
+ <Text>{props.props.description}</Text>
35
+ {props.props.emailLabel === undefined ? null : <Input id="email-door-email" name="email" label={props.props.emailLabel} kind="email" isRequired />}
36
+ {props.props.passwordLabel === undefined ? null : <Input id="email-door-password" name="password" label={props.props.passwordLabel} kind="password" isRequired />}
37
+ {props.props.errorMessage === undefined ? null : <Text live="assertive">{props.props.errorMessage}</Text>}
38
+ <Button type="submit" intent="commit" isPending={props.props.pending}>
39
+ {props.props.submitLabel}
40
+ </Button>
41
+ </Form>
42
+ ) : (
43
+ <Text live="polite">{props.props.doneMessage}</Text>
44
+ )}
45
+ {props.props.links.map((link) => (
46
+ <Link key={link.href} href={link.href}>
47
+ {link.label}
48
+ </Link>
49
+ ))}
50
+ </>
51
+ )
@@ -0,0 +1,84 @@
1
+ "use client"
2
+
3
+ import { useLocale, useTranslations } from "next-intl"
4
+ import { useEmailDoor } from "@/hooks/emailAuth"
5
+ import type { EmailDoorFailure } from "@/hooks/emailAuth"
6
+ import { APP_ROUTES } from "@/modules/routes"
7
+ import { EmailDoorFormBase } from "./component"
8
+ import type { EmailDoorLink } from "./component"
9
+
10
+ /** The doors the block draws; the confirmation is a button, so the mailbox is confirmed by a person and not by a mail scanner opening the link. */
11
+ type FormDoor = "signup" | "resend" | "forgot" | "reset" | "confirm"
12
+
13
+ /** The door the block draws and, for the two link doors, the token of the link. */
14
+ type EmailDoorFormProps = { readonly door: FormDoor; readonly token?: string | undefined }
15
+
16
+ /** The words of one door. */
17
+ interface DoorCopy {
18
+ readonly title: string
19
+ readonly description: string
20
+ readonly submit: string
21
+ readonly done: string
22
+ readonly email: string | undefined
23
+ readonly password: string | undefined
24
+ }
25
+
26
+ /** The value of a field of the submitted form, or undefined when it is missing. */
27
+ const fieldOf = (input: FormData, name: string): string | undefined => {
28
+ const value = input.get(name)
29
+ return typeof value === "string" ? value : undefined
30
+ }
31
+
32
+ /** The language a mail is written in: the page's, when the back end writes mail in it. */
33
+ const languageOf = (locale: string): "vi" | "en" | undefined => (locale === "vi" || locale === "en" ? locale : undefined)
34
+
35
+ /** One email door of the lite sign-in as a form: sign up, resend the confirmation, forgot the password, or choose a new one from a link. */
36
+ export const EmailDoorForm = (props: EmailDoorFormProps) => {
37
+ const t = useTranslations("app.emailAuth")
38
+ const locale = useLocale()
39
+ const { failure, done, pending, submit, reject } = useEmailDoor(props.door)
40
+ const copy = {
41
+ signup: { title: t("signup.title"), description: t("signup.description"), submit: t("signup.submit"), done: t("signup.done"), email: t("email"), password: t("password") },
42
+ resend: { title: t("resend.title"), description: t("resend.description"), submit: t("resend.submit"), done: t("resend.done"), email: t("email"), password: undefined },
43
+ forgot: { title: t("forgot.title"), description: t("forgot.description"), submit: t("forgot.submit"), done: t("forgot.done"), email: t("email"), password: undefined },
44
+ confirm: { title: t("confirm.title"), description: t("confirm.description"), submit: t("confirm.submit"), done: t("confirm.done"), email: undefined, password: undefined },
45
+ reset: { title: t("reset.title"), description: t("reset.description"), submit: t("reset.submit"), done: t("reset.done"), email: undefined, password: t("newPassword") },
46
+ } satisfies Record<FormDoor, DoorCopy>
47
+ const links = {
48
+ signup: [{ href: APP_ROUTES.home, label: t("toSignIn") }, { href: APP_ROUTES.resend, label: t("toResend") }],
49
+ resend: [{ href: APP_ROUTES.home, label: t("toSignIn") }],
50
+ forgot: [{ href: APP_ROUTES.home, label: t("toSignIn") }],
51
+ reset: [{ href: APP_ROUTES.home, label: t("toSignIn") }, { href: APP_ROUTES.forgot, label: t("toForgot") }],
52
+ confirm: [{ href: APP_ROUTES.home, label: t("toSignIn") }, { href: APP_ROUTES.resend, label: t("toResend") }],
53
+ } satisfies Record<FormDoor, readonly EmailDoorLink[]>
54
+ const failures = {
55
+ invalid: t("errors.invalid"),
56
+ password: t("errors.password"),
57
+ link: t("errors.link"),
58
+ tooMany: t("errors.tooMany"),
59
+ unavailable: t("errors.unavailable"),
60
+ } satisfies Record<EmailDoorFailure, string>
61
+ const chosen = copy[props.door]
62
+ const onSubmit = (input: FormData) => {
63
+ const email = fieldOf(input, "email")
64
+ const password = fieldOf(input, "password")
65
+ if ((chosen.email !== undefined && email === undefined) || (chosen.password !== undefined && password === undefined)) reject()
66
+ else submit({ email, password, token: props.token, language: languageOf(locale) })
67
+ }
68
+ return (
69
+ <EmailDoorFormBase
70
+ props={{
71
+ title: chosen.title,
72
+ description: chosen.description,
73
+ emailLabel: chosen.email,
74
+ passwordLabel: chosen.password,
75
+ submitLabel: chosen.submit,
76
+ pending,
77
+ errorMessage: failure === undefined ? undefined : failures[failure],
78
+ doneMessage: done ? chosen.done : undefined,
79
+ links: links[props.door],
80
+ }}
81
+ on={{ submit: onSubmit }}
82
+ />
83
+ )
84
+ }
@@ -1,4 +1,4 @@
1
- import { Heading, TextAction } from "@starci/grammar/common"
1
+ import { Link, Page } from "@starci/grammar/common"
2
2
 
3
3
  /** The words of a missing page and the way home. */
4
4
  type NotFoundNoticeProps = {
@@ -9,10 +9,7 @@ type NotFoundNoticeProps = {
9
9
 
10
10
  /** A missing page with one destination back to the app's front door. */
11
11
  export const NotFoundNotice = (props: NotFoundNoticeProps) => (
12
- <>
13
- <Heading level={1}>{props.title}</Heading>
14
- <TextAction appearance="inline" href={props.homeHref}>
15
- {props.homeLabel}
16
- </TextAction>
17
- </>
12
+ <Page header={{ title: props.title }} measure="reading">
13
+ <Link href={props.homeHref}>{props.homeLabel}</Link>
14
+ </Page>
18
15
  )
@@ -1,6 +1,5 @@
1
1
  import type { ReactNode } from "react"
2
- import { GrammarRoot, PageContainer, TextAction, TopBar, WorkspaceShell } from "@starci/grammar/common"
3
- import { siteShellClassNames } from "./classNames"
2
+ import { GrammarRoot, Link, TopBar, WorkspaceShell } from "@starci/grammar/common"
4
3
 
5
4
  /** What the app chrome hands the shell. */
6
5
  type SiteShellProps = {
@@ -0,0 +1,25 @@
1
+ import type { ReactElement } from "react"
2
+ import type { Metadata } from "next"
3
+ import { Page } from "@starci/grammar/common"
4
+ import { getTranslations } from "next-intl/server"
5
+ import { EmailDoorForm } from "@/components/blocks/EmailDoorForm"
6
+
7
+ /** The document title of the confirm page. */
8
+ export const appConfirmMetadata = async (): Promise<Metadata> => {
9
+ const t = await getTranslations("app.emailAuth.confirm")
10
+ return { title: t("title") }
11
+ }
12
+
13
+ /** The query of the page: the link of the mail carries its token there. */
14
+ type AppConfirmPageProps = { readonly searchParams: Promise<{ readonly token?: string | readonly string[] }> }
15
+
16
+ /** The confirm door of the lite sign-in, drawn as one Grammar Page (R325). */
17
+ export const AppConfirmPage = async (props: AppConfirmPageProps): Promise<ReactElement> => {
18
+ const { token } = await props.searchParams
19
+ const t = await getTranslations("app.emailAuth.confirm")
20
+ return (
21
+ <Page header={{ title: t("title") }} measure="reading">
22
+ <EmailDoorForm door="confirm" token={typeof token === "string" ? token : token?.[0]} />
23
+ </Page>
24
+ )
25
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactElement } from "react"
2
+ import type { Metadata } from "next"
3
+ import { Page } from "@starci/grammar/common"
4
+ import { getTranslations } from "next-intl/server"
5
+ import { EmailDoorForm } from "@/components/blocks/EmailDoorForm"
6
+
7
+ /** The document title of the forgot page. */
8
+ export const appForgotMetadata = async (): Promise<Metadata> => {
9
+ const t = await getTranslations("app.emailAuth.forgot")
10
+ return { title: t("title") }
11
+ }
12
+
13
+ /** The forgot door of the lite sign-in, drawn as one Grammar Page (R325). */
14
+ export const AppForgotPage = async (_props: Record<string, never>): Promise<ReactElement> => {
15
+ const t = await getTranslations("app.emailAuth.forgot")
16
+ return (
17
+ <Page header={{ title: t("title") }} measure="reading">
18
+ <EmailDoorForm door="forgot" />
19
+ </Page>
20
+ )
21
+ }
@@ -1,16 +1,24 @@
1
- import { Page } from "@starci/grammar/common"
1
+ import { Link, Page } from "@starci/grammar/common"
2
2
  import { SignInForm } from "@/components/blocks/SignInForm"
3
3
  import { SignOutButton } from "@/components/blocks/SignOutButton"
4
4
 
5
- /** Props for the home page: its title. */
5
+ /** Props for the home page: its title and the two doors beside the sign-in form. */
6
6
  export type AppHomePageBaseProps = {
7
- readonly props: { readonly title: string }
7
+ readonly props: {
8
+ readonly title: string
9
+ readonly signupLabel: string
10
+ readonly signupHref: string
11
+ readonly forgotLabel: string
12
+ readonly forgotHref: string
13
+ }
8
14
  }
9
15
 
10
16
  /** Draws the front door as one Grammar Page (R325): the title heads the band, the body the sign-in form or the reader and sign-out. */
11
17
  export const AppHomePageBase = (props: AppHomePageBaseProps) => (
12
18
  <Page header={{ title: props.props.title }}>
13
19
  <SignInForm />
20
+ <Link href={props.props.signupHref}>{props.props.signupLabel}</Link>
21
+ <Link href={props.props.forgotHref}>{props.props.forgotLabel}</Link>
14
22
  <SignOutButton />
15
23
  </Page>
16
24
  )
@@ -2,6 +2,7 @@ import type { ReactElement } from "react"
2
2
  import type { Metadata } from "next"
3
3
  import { getTranslations } from "next-intl/server"
4
4
  import { AppGuideOverlay } from "@/features/overlays/AppGuideOverlay"
5
+ import { APP_ROUTES } from "@/modules/routes"
5
6
  import { AppHomePageBase } from "./component"
6
7
 
7
8
  /** The document title of the home page. */
@@ -15,7 +16,9 @@ export const AppHomePage = async (): Promise<ReactElement> => {
15
16
  const t = await getTranslations("app.home")
16
17
  return (
17
18
  <>
18
- <AppHomePageBase props={{ title: t("title") }} />
19
+ <AppHomePageBase
20
+ props={{ title: t("title"), signupLabel: t("signup"), signupHref: APP_ROUTES.signup, forgotLabel: t("forgot"), forgotHref: APP_ROUTES.forgot }}
21
+ />
19
22
  <AppGuideOverlay />
20
23
  </>
21
24
  )
@@ -0,0 +1,21 @@
1
+ import type { ReactElement } from "react"
2
+ import type { Metadata } from "next"
3
+ import { Page } from "@starci/grammar/common"
4
+ import { getTranslations } from "next-intl/server"
5
+ import { EmailDoorForm } from "@/components/blocks/EmailDoorForm"
6
+
7
+ /** The document title of the resend page. */
8
+ export const appResendMetadata = async (): Promise<Metadata> => {
9
+ const t = await getTranslations("app.emailAuth.resend")
10
+ return { title: t("title") }
11
+ }
12
+
13
+ /** The resend door of the lite sign-in, drawn as one Grammar Page (R325). */
14
+ export const AppResendPage = async (_props: Record<string, never>): Promise<ReactElement> => {
15
+ const t = await getTranslations("app.emailAuth.resend")
16
+ return (
17
+ <Page header={{ title: t("title") }} measure="reading">
18
+ <EmailDoorForm door="resend" />
19
+ </Page>
20
+ )
21
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactElement } from "react"
2
+ import type { Metadata } from "next"
3
+ import { Page } from "@starci/grammar/common"
4
+ import { getTranslations } from "next-intl/server"
5
+ import { EmailDoorForm } from "@/components/blocks/EmailDoorForm"
6
+
7
+ /** The document title of the reset page. */
8
+ export const appResetMetadata = async (): Promise<Metadata> => {
9
+ const t = await getTranslations("app.emailAuth.reset")
10
+ return { title: t("title") }
11
+ }
12
+
13
+ /** The query of the page: the link of the mail carries its token there. */
14
+ type AppResetPageProps = { readonly searchParams: Promise<{ readonly token?: string | readonly string[] }> }
15
+
16
+ /** The reset door of the lite sign-in, drawn as one Grammar Page (R325). */
17
+ export const AppResetPage = async (props: AppResetPageProps): Promise<ReactElement> => {
18
+ const { token } = await props.searchParams
19
+ const t = await getTranslations("app.emailAuth.reset")
20
+ return (
21
+ <Page header={{ title: t("title") }} measure="reading">
22
+ <EmailDoorForm door="reset" token={typeof token === "string" ? token : token?.[0]} />
23
+ </Page>
24
+ )
25
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactElement } from "react"
2
+ import type { Metadata } from "next"
3
+ import { Page } from "@starci/grammar/common"
4
+ import { getTranslations } from "next-intl/server"
5
+ import { EmailDoorForm } from "@/components/blocks/EmailDoorForm"
6
+
7
+ /** The document title of the signup page. */
8
+ export const appSignupMetadata = async (): Promise<Metadata> => {
9
+ const t = await getTranslations("app.emailAuth.signup")
10
+ return { title: t("title") }
11
+ }
12
+
13
+ /** The signup door of the lite sign-in, drawn as one Grammar Page (R325). */
14
+ export const AppSignupPage = async (_props: Record<string, never>): Promise<ReactElement> => {
15
+ const t = await getTranslations("app.emailAuth.signup")
16
+ return (
17
+ <Page header={{ title: t("title") }} measure="reading">
18
+ <EmailDoorForm door="signup" />
19
+ </Page>
20
+ )
21
+ }
@@ -0,0 +1,12 @@
1
+ import type { AuthFailure } from "@/modules/auth"
2
+
3
+ /** Why an email door refused: input the back end rejected, a weak password, a dead link, too many tries or the back end unreachable. */
4
+ export type EmailDoorFailure = "invalid" | "password" | "link" | "tooMany" | "unavailable"
5
+
6
+ /** The failure a door's wire code and kind stand for. */
7
+ export const failureOf = (failure: AuthFailure): EmailDoorFailure => {
8
+ if (failure.code === "IDENTITY_PASSWORD_INVALID") return "password"
9
+ if (failure.code === "IDENTITY_RESET_TOKEN_INVALID") return "link"
10
+ if (failure.code === "IDENTITY_TOO_MANY_ATTEMPTS" || failure.code === "429") return "tooMany"
11
+ return failure.kind === "invalid" ? "invalid" : "unavailable"
12
+ }
@@ -0,0 +1,2 @@
1
+ export { useEmailDoor } from "./useEmailDoor"
2
+ export type { EmailDoorFailure } from "./useEmailDoor"
@@ -0,0 +1,32 @@
1
+ "use client"
2
+
3
+ import { useState, useTransition } from "react"
4
+ import { sendEmailDoor } from "@/modules/auth"
5
+ import type { EmailDoorName } from "@/modules/auth"
6
+ import { failureOf } from "./emailAuth.shared"
7
+ import type { EmailDoorFailure } from "./emailAuth.shared"
8
+
9
+ export type { EmailDoorFailure } from "./emailAuth.shared"
10
+
11
+ /** What an email door hook answers: whether a request runs, whether the back end accepted it, why it refused, the call itself and `reject`. */
12
+ export interface EmailDoor {
13
+ readonly failure: EmailDoorFailure | undefined
14
+ readonly done: boolean
15
+ readonly pending: boolean
16
+ readonly submit: (body: Readonly<Record<string, string | undefined>>) => void
17
+ readonly reject: () => void
18
+ }
19
+
20
+ /** Calls one email door of the back end through modules/auth; a success sets `done`, a refusal its failure. */
21
+ export const useEmailDoor = (door: EmailDoorName): EmailDoor => {
22
+ const [failure, setFailure] = useState<EmailDoorFailure>()
23
+ const [done, setDone] = useState(false)
24
+ const [pending, startTransition] = useTransition()
25
+ const submit = (body: Readonly<Record<string, string | undefined>>): void =>
26
+ startTransition(async () => {
27
+ const failed = await sendEmailDoor(door, body)
28
+ setFailure(failed === undefined ? undefined : failureOf(failed))
29
+ setDone(failed === undefined)
30
+ })
31
+ return { failure, done, pending, submit, reject: (): void => setFailure("invalid") }
32
+ }
@@ -1,2 +1,2 @@
1
- /** The locale served at the unprefixed path and used by the global error boundary. */
2
- export { DEFAULT_LOCALE, PRODUCT_TIME_ZONE, routing } from "./routing"
1
+ /** The web app's description of its languages and the locale served at the unprefixed path (the global error boundary). */
2
+ export { DEFAULT_LOCALE, i18n } from "./routing"
@@ -5,7 +5,9 @@
5
5
  "brand": "{{project}}"
6
6
  },
7
7
  "home": {
8
- "title": "Home"
8
+ "title": "Home",
9
+ "signup": "Create an account",
10
+ "forgot": "Forgot your password?"
9
11
  },
10
12
  "signIn": {
11
13
  "title": "Sign in",
@@ -52,6 +54,53 @@
52
54
  "title": "Getting started",
53
55
  "description": "Sign in to use the application. You can close this guide and open it again at any time.",
54
56
  "close": "Close"
57
+ },
58
+ "emailAuth": {
59
+ "email": "Email",
60
+ "password": "Password",
61
+ "newPassword": "New password",
62
+ "toSignIn": "Back to sign in",
63
+ "toResend": "Send the confirmation again",
64
+ "toForgot": "Request a new reset link",
65
+ "toSignup": "Create an account",
66
+ "toForgotPassword": "Forgot your password?",
67
+ "signup": {
68
+ "title": "Create an account",
69
+ "description": "Choose a password of at least 8 characters. We email a link to confirm your address.",
70
+ "submit": "Create account",
71
+ "done": "Check your inbox: we sent a link to confirm your email."
72
+ },
73
+ "resend": {
74
+ "title": "Confirm your email again",
75
+ "description": "Enter your address and we email a new confirmation link if the account is waiting for one.",
76
+ "submit": "Send the link",
77
+ "done": "If that address is waiting for confirmation, a new link is on its way."
78
+ },
79
+ "forgot": {
80
+ "title": "Forgot your password",
81
+ "description": "Enter your address and we email a link to choose a new password.",
82
+ "submit": "Send the link",
83
+ "done": "If that address has an account, a reset link is on its way."
84
+ },
85
+ "reset": {
86
+ "title": "Choose a new password",
87
+ "description": "The link works once. Every device is signed out when the password changes.",
88
+ "submit": "Save password",
89
+ "done": "Your password is changed. Sign in with the new one."
90
+ },
91
+ "confirm": {
92
+ "title": "Confirm your email",
93
+ "description": "Press the button to confirm your email address.",
94
+ "submit": "Confirm email",
95
+ "done": "Your email is confirmed. You can sign in."
96
+ },
97
+ "errors": {
98
+ "invalid": "Enter a valid value in every field.",
99
+ "password": "The password needs at least 8 characters and at most 72 bytes.",
100
+ "link": "The link is invalid or has expired. Request a new one.",
101
+ "tooMany": "Too many attempts. Try again later.",
102
+ "unavailable": "This is unavailable. Try again shortly."
103
+ }
55
104
  }
56
105
  }
57
106
  }
@@ -5,7 +5,9 @@
5
5
  "brand": "{{project}}"
6
6
  },
7
7
  "home": {
8
- "title": "Trang chủ"
8
+ "title": "Trang chủ",
9
+ "signup": "Tạo tài khoản",
10
+ "forgot": "Quên mật khẩu?"
9
11
  },
10
12
  "signIn": {
11
13
  "title": "Đăng nhập",
@@ -52,6 +54,53 @@
52
54
  "title": "Bắt đầu",
53
55
  "description": "Đăng nhập để sử dụng ứng dụng. Bạn có thể đóng hướng dẫn này và mở lại bất cứ lúc nào.",
54
56
  "close": "Đóng"
57
+ },
58
+ "emailAuth": {
59
+ "email": "Email",
60
+ "password": "Mật khẩu",
61
+ "newPassword": "Mật khẩu mới",
62
+ "toSignIn": "Quay lại đăng nhập",
63
+ "toResend": "Gửi lại thư xác nhận",
64
+ "toForgot": "Yêu cầu liên kết đặt lại mới",
65
+ "toSignup": "Tạo tài khoản",
66
+ "toForgotPassword": "Quên mật khẩu?",
67
+ "signup": {
68
+ "title": "Tạo tài khoản",
69
+ "description": "Chọn mật khẩu ít nhất 8 ký tự. Chúng tôi gửi email một liên kết để xác nhận địa chỉ của bạn.",
70
+ "submit": "Tạo tài khoản",
71
+ "done": "Hãy kiểm tra hộp thư: chúng tôi đã gửi liên kết xác nhận email."
72
+ },
73
+ "resend": {
74
+ "title": "Xác nhận email lại",
75
+ "description": "Nhập địa chỉ của bạn, chúng tôi gửi liên kết xác nhận mới nếu tài khoản đang chờ xác nhận.",
76
+ "submit": "Gửi liên kết",
77
+ "done": "Nếu địa chỉ đó đang chờ xác nhận, một liên kết mới đang được gửi."
78
+ },
79
+ "forgot": {
80
+ "title": "Quên mật khẩu",
81
+ "description": "Nhập địa chỉ của bạn, chúng tôi gửi email một liên kết để chọn mật khẩu mới.",
82
+ "submit": "Gửi liên kết",
83
+ "done": "Nếu địa chỉ đó có tài khoản, một liên kết đặt lại đang được gửi."
84
+ },
85
+ "reset": {
86
+ "title": "Chọn mật khẩu mới",
87
+ "description": "Liên kết chỉ dùng được một lần. Mọi thiết bị sẽ bị đăng xuất khi đổi mật khẩu.",
88
+ "submit": "Lưu mật khẩu",
89
+ "done": "Mật khẩu đã được đổi. Hãy đăng nhập bằng mật khẩu mới."
90
+ },
91
+ "confirm": {
92
+ "title": "Xác nhận email",
93
+ "description": "Nhấn nút để xác nhận địa chỉ email của bạn.",
94
+ "submit": "Xác nhận email",
95
+ "done": "Email đã được xác nhận. Bạn có thể đăng nhập."
96
+ },
97
+ "errors": {
98
+ "invalid": "Hãy nhập giá trị hợp lệ ở mọi ô.",
99
+ "password": "Mật khẩu cần ít nhất 8 ký tự và tối đa 72 byte.",
100
+ "link": "Liên kết không hợp lệ hoặc đã hết hạn. Hãy yêu cầu liên kết mới.",
101
+ "tooMany": "Quá nhiều lần thử. Hãy thử lại sau.",
102
+ "unavailable": "Chưa thể thực hiện. Hãy thử lại sau."
103
+ }
55
104
  }
56
105
  }
57
106
  }
@@ -1,35 +1,12 @@
1
- import "server-only"
2
- import { hasLocale } from "next-intl"
3
- import type { AbstractIntlMessages } from "next-intl"
4
- import { getRequestConfig } from "next-intl/server"
5
- import { headers } from "next/headers"
6
- import { PRODUCT_TIME_ZONE, routing } from "./routing"
7
-
8
- /** The message tree a catalog module exports as its default: the module is read as `unknown` and checked, never trusted as `any`. */
9
- const isMessages = (value: unknown): value is AbstractIntlMessages => typeof value === "object" && value !== null
10
-
11
- const messagesOf = (loaded: unknown): AbstractIntlMessages => {
12
- const messages = typeof loaded === "object" && loaded !== null && "default" in loaded ? loaded.default : undefined
13
- if (!isMessages(messages)) throw new TypeError("A message catalog module must export its messages as default")
14
- return messages
15
- }
16
-
17
- /** The header the next-intl middleware sets on the request with the locale it negotiated. */
18
- const LOCALE_HEADER = "x-next-intl-locale"
1
+ import { createRequestConfig } from "@starci/next-i18n/request"
2
+ import type { I18nRequestConfig } from "@starci/next-i18n/request"
3
+ import { i18n } from "./routing"
19
4
 
20
5
  /**
21
- * The request config next-intl's plugin loads. The locale is the one the middleware negotiated: this module is reachable from the proxy
22
- * through the navigation, and `next/root-params` can only be imported inside the app directory.
6
+ * The request config next-intl's plugin loads: the app's own catalogs over the shared stack. The locale is the one the proxy
7
+ * negotiated (a route the proxy does not negotiate reads the cookie, then the default); `next/root-params` can only be imported
8
+ * inside the app directory, so no route reader is passed.
23
9
  */
24
- const requestConfig: ReturnType<typeof getRequestConfig> = getRequestConfig(async () => {
25
- const requested = (await headers()).get(LOCALE_HEADER)
26
- const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale
27
- return {
28
- locale,
29
- timeZone: PRODUCT_TIME_ZONE,
30
- messages: messagesOf(await import(`./messages/${locale}.json`)),
31
- getMessageFallback: ({ namespace, key }) => `web:${namespace ?? ""}.${key}`,
32
- }
33
- })
10
+ const requestConfig: I18nRequestConfig = createRequestConfig(i18n, { app: "web", loadMessages: (locale) => import(`./messages/${locale}.json`) })
34
11
 
35
12
  export default requestConfig
@@ -1,18 +1,17 @@
1
+ import { defineI18n } from "@starci/next-i18n"
2
+ import type { I18n } from "@starci/next-i18n"
3
+
1
4
  const LOCALES = ["vi", "en"] as const
2
5
 
3
- /** The locale served at the unprefixed path. */
4
- export const DEFAULT_LOCALE = "vi" as const
6
+ /** The description of the web app's languages: the locales and the `as-needed` prefix mode that `defineI18n` types. */
7
+ export type WebI18n = I18n<typeof LOCALES, "as-needed">
5
8
 
6
- /** The fixed product time zone used by the server and hydrated client. */
7
- export const PRODUCT_TIME_ZONE = "Asia/Ho_Chi_Minh"
9
+ /** The locale served at the unprefixed path. */
10
+ export const DEFAULT_LOCALE: WebI18n["defaultLocale"] = "vi"
8
11
 
9
- /** The one locale-routing table of the web app. */
10
- export const routing: {
11
- readonly locales: typeof LOCALES
12
- readonly defaultLocale: typeof DEFAULT_LOCALE
13
- readonly localePrefix: "as-needed"
14
- } = {
15
- locales: LOCALES,
16
- defaultLocale: DEFAULT_LOCALE,
17
- localePrefix: "as-needed" as const,
18
- }
12
+ /**
13
+ * The one description of the web app's languages, read by the proxy, the request config and the locale layout: vi default, a prefix
14
+ * only when the locale is not the default, and a fixed time zone so the server and the hydrated client format the same instant the
15
+ * same way. The stack that reads it is `@starci/next-i18n`'s (FE-I18N-1).
16
+ */
17
+ export const i18n: WebI18n = defineI18n({ locales: LOCALES, defaultLocale: DEFAULT_LOCALE, timeZone: "Asia/Ho_Chi_Minh" })
@@ -1,4 +1,9 @@
1
1
  /** Every internal destination the web app links to, named once. */
2
2
  export const APP_ROUTES = {
3
3
  home: "/",
4
+ signup: "/auth/signup",
5
+ resend: "/auth/resend",
6
+ forgot: "/auth/forgot",
7
+ confirm: "/auth/confirm",
8
+ reset: "/auth/reset",
4
9
  } as const
@@ -1,25 +1,12 @@
1
- import createMiddleware from "next-intl/middleware"
2
- import { NextResponse } from "next/server"
3
- import type { NextRequest } from "next/server"
4
- import { routing } from "@/modules/i18n"
5
-
6
- /** The paths outside locale routing: the liveness route and the framework's own assets. */
7
- const UNNEGOTIATED_ROOTS: ReadonlySet<string> = new Set(["health", "_next", "_vercel"])
8
- const negotiate = createMiddleware(routing)
9
-
10
- const firstSegment = (pathname: string): string => pathname.split("/")[1] ?? ""
11
-
12
- const isUnnegotiated = (pathname: string): boolean => {
13
- const last = pathname.split("/").at(-1) ?? ""
14
- return UNNEGOTIATED_ROOTS.has(firstSegment(pathname)) || (last.includes(".") && !last.endsWith("."))
15
- }
1
+ import { createProxy } from "@starci/next-i18n/proxy"
2
+ import type { LocaleProxy } from "@starci/next-i18n/proxy"
3
+ import { i18n } from "@/modules/i18n"
16
4
 
17
5
  /**
18
6
  * Locale routing only. The session lives in the browser (modules/auth) and the back end verifies the access token of
19
7
  * every call, so the proxy neither reads nor refreshes it.
20
8
  */
21
- export const proxy = (request: NextRequest): NextResponse =>
22
- isUnnegotiated(request.nextUrl.pathname) ? NextResponse.next() : negotiate(request)
9
+ export const proxy: LocaleProxy = createProxy(i18n)
23
10
 
24
11
  /** Routes that pass through locale negotiation. */
25
12
  export const config = {