@starci/hfs 5.2.0 → 5.4.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 (223) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +2 -1
  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 +134 -0
  10. package/codemod/app-prettier.d.mts +5 -0
  11. package/codemod/app-prettier.mjs +27 -0
  12. package/codemod/explicit-surface.mjs +1 -25
  13. package/codemod/main.mjs +35 -1
  14. package/codemod/module-definition.d.mts +59 -0
  15. package/codemod/module-definition.mjs +257 -0
  16. package/codemod/quality-binding.d.mts +33 -0
  17. package/codemod/quality-binding.mjs +38 -0
  18. package/codemod/secrets-cache.d.mts +35 -0
  19. package/codemod/secrets-cache.mjs +70 -0
  20. package/codemod/sops-rules.d.mts +34 -0
  21. package/codemod/sops-rules.mjs +69 -0
  22. package/codemod/theme-tokens.mjs +48 -2
  23. package/lint/run.d.mts +6 -0
  24. package/lint/run.mjs +63 -10
  25. package/package.json +1 -1
  26. package/runtime/knowledge/hfs/canon-pins.yaml +43 -10
  27. package/runtime/knowledge/hfs/fe-stack.yaml +3 -3
  28. package/runtime/knowledge/hfs/rules.yaml +13 -9
  29. package/runtime/knowledge/hfs/slots.yaml +105 -12
  30. package/runtime/modules/kernel/failure-codes.yaml +5 -4
  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/module-per-transport.d.mts +1 -1
  34. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +6 -1
  35. package/runtime/scripts/hfs/architecture/root-packages.d.mts +26 -0
  36. package/runtime/scripts/hfs/architecture/root-packages.mjs +78 -0
  37. package/runtime/scripts/hfs/architecture/type-context.d.mts +4 -1
  38. package/runtime/scripts/hfs/architecture/type-context.mjs +30 -0
  39. package/runtime/scripts/hfs/architecture/typescript.d.mts +7 -1
  40. package/runtime/scripts/hfs/architecture/typescript.mjs +21 -1
  41. package/runtime/scripts/hfs/explicit-surface-rule.mjs +1 -1
  42. package/runtime/scripts/hfs/rules/database-access.mjs +39 -1
  43. package/runtime/scripts/hfs/rules/monorepo.d.mts +4 -0
  44. package/runtime/scripts/hfs/rules/monorepo.mjs +10 -5
  45. package/runtime/scripts/hfs/rules/secrets.mjs +11 -0
  46. package/runtime/scripts/hfs/secret-errors.d.mts +3 -0
  47. package/runtime/scripts/hfs/secret-errors.mjs +8 -0
  48. package/runtime/scripts/hfs/secret-init.d.mts +6 -0
  49. package/runtime/scripts/hfs/secret-init.mjs +23 -0
  50. package/runtime/scripts/hfs/secret-sops.mjs +9 -1
  51. package/runtime/scripts/hfs/secret-standard.d.mts +20 -0
  52. package/runtime/scripts/hfs/secret-standard.mjs +106 -0
  53. package/runtime/scripts/hfs/secret.mjs +5 -1
  54. package/runtime/scripts/hfs/view.d.mts +23 -0
  55. package/runtime/scripts/hfs/view.mjs +68 -0
  56. package/runtime/scripts/lib/declaration-surface.d.mts +4 -0
  57. package/runtime/scripts/lib/declaration-surface.mjs +55 -1
  58. package/runtime/scripts/lib/secret-records.d.mts +13 -0
  59. package/runtime/scripts/lib/secret-records.mjs +83 -0
  60. package/scaffold/add-inbox-lite.mjs +2 -2
  61. package/scaffold/add-queue-lite.mjs +1 -3
  62. package/scaffold/app.d.mts +2 -2
  63. package/scaffold/app.mjs +10 -12
  64. package/scaffold/lite-exports.mjs +1 -15
  65. package/scripts/sync-bundles.d.mts +8 -0
  66. package/stack/images/openclaw-runtime/Dockerfile +18 -0
  67. package/stack/images/openclaw-runtime/entrypoint.sh +23 -0
  68. package/stack/images/openclaw-runtime/openclaw-init.mjs +101 -0
  69. package/stack/images/openclaw-runtime/openclaw-relay.mjs +25 -0
  70. package/stack/local-up.mjs +3 -0
  71. package/stack/runtime-layers.d.mts +21 -0
  72. package/stack/runtime-layers.mjs +67 -0
  73. package/sync/compile-guard.d.mts +36 -0
  74. package/sync/compile-guard.mjs +189 -0
  75. package/sync/hygiene.mjs +3 -1
  76. package/sync/index.d.mts +11 -1
  77. package/sync/index.mjs +61 -10
  78. package/templates/app/auth-doors.sql.partial +115 -0
  79. package/templates/app/ci-workflows/github/workflows/ci.yml +1 -1
  80. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -1
  81. package/templates/app/package-scripts/core.json +1 -1
  82. package/templates/app/package-scripts/package.json +1 -1
  83. package/templates/app/package-scripts-lite/package.json +1 -1
  84. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +9 -0
  85. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +3 -1
  86. package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +19 -1
  87. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +3 -0
  88. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +5 -0
  89. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +4 -0
  90. package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +27 -5
  91. package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +33 -0
  92. package/templates/be/auth-lite/src/features/api/auth/application/confirm.command.ts +11 -0
  93. package/templates/be/auth-lite/src/features/api/auth/application/confirm.handler.ts +22 -0
  94. package/templates/be/auth-lite/src/features/api/auth/application/forgot.command.ts +11 -0
  95. package/templates/be/auth-lite/src/features/api/auth/application/forgot.handler.ts +22 -0
  96. package/templates/be/auth-lite/src/features/api/auth/application/resend.command.ts +11 -0
  97. package/templates/be/auth-lite/src/features/api/auth/application/resend.handler.ts +22 -0
  98. package/templates/be/auth-lite/src/features/api/auth/application/reset.command.ts +11 -0
  99. package/templates/be/auth-lite/src/features/api/auth/application/reset.handler.ts +22 -0
  100. package/templates/be/auth-lite/src/features/api/auth/application/signup.command.ts +11 -0
  101. package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +22 -0
  102. package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +3 -10
  103. package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +6 -1
  104. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +3 -10
  105. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +56 -1
  106. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/forgot.request.ts +16 -0
  107. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/resend.request.ts +16 -0
  108. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +18 -0
  109. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +22 -0
  110. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/token.request.ts +11 -0
  111. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +30 -30
  112. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +7 -16
  113. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +113 -49
  114. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +42 -53
  115. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.log-events.ts +6 -0
  116. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +233 -0
  117. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +176 -0
  118. package/templates/be/auth-lite/src/modules/domain/identity/identity-sign-in.log-events.ts +6 -0
  119. package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +98 -0
  120. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-email-token.entity.ts +34 -0
  121. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +4 -0
  122. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +3 -10
  123. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +6 -3
  124. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +6 -1
  125. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +17 -0
  126. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +6 -0
  127. package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +29 -33
  128. package/templates/be/inbox/module-definition.ts.tpl +2 -9
  129. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +2 -11
  130. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +2 -9
  131. package/templates/be/patterns/api/feature.module-definition.ts.tpl +3 -9
  132. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +3 -9
  133. package/templates/be/patterns/cli/group.module-definition.ts.tpl +2 -9
  134. package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +3 -10
  135. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +2 -9
  136. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +2 -9
  137. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +3 -9
  138. package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +3 -10
  139. package/templates/be/patterns/projections/projection.module-definition.ts.tpl +2 -9
  140. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +3 -10
  141. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +2 -9
  142. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +3 -9
  143. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +2 -9
  144. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +3 -9
  145. package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +3 -9
  146. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +3 -9
  147. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +3 -9
  148. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +2 -9
  149. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +3 -9
  150. package/templates/be/platform/graphql/graphql.module-definition.ts +2 -9
  151. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +3 -9
  152. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +3 -9
  153. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +2 -9
  154. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +3 -9
  155. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +2 -9
  156. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +2 -9
  157. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +2 -9
  158. package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +2 -9
  159. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +2 -9
  160. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +2 -0
  161. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +2 -9
  162. package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +3 -13
  163. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +4 -11
  164. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +4 -9
  165. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +5 -11
  166. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +4 -11
  167. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +2 -9
  168. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +2 -9
  169. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +2 -9
  170. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +4 -0
  171. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +3 -0
  172. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +3 -1
  173. package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +6 -0
  174. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +3 -0
  175. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +1 -0
  176. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity-mail.messages.ts +65 -0
  177. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +4 -0
  178. package/templates/be/skeleton-lite/src/modules/integrations/mail/index.ts +6 -0
  179. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.config.ts +33 -0
  180. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.decorators.ts +14 -0
  181. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module-definition.ts +12 -0
  182. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module.ts +27 -0
  183. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.options.ts +22 -0
  184. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +2 -9
  185. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +1 -1
  186. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  187. package/templates/be/skeleton-lite/src/modules/platform/http-security/rate-limit.guard.spec.ts +11 -0
  188. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +11 -0
  189. package/templates/be/table/api/feature.module-definition.ts.tpl +3 -9
  190. package/templates/be/table/api/graphql.module-definition.ts.tpl +3 -9
  191. package/templates/be/table/module-definition.ts.tpl +2 -9
  192. package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +16 -0
  193. package/templates/fe/auth-lite/apps/web/src/modules/auth/index.ts +5 -0
  194. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +30 -67
  195. package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +6 -21
  196. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +10 -16
  197. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/confirm/page.tsx +9 -0
  198. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/forgot/page.tsx +9 -0
  199. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/resend/page.tsx +9 -0
  200. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/reset/page.tsx +9 -0
  201. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/signup/page.tsx +9 -0
  202. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +5 -9
  203. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +51 -0
  204. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +84 -0
  205. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppConfirmPage/index.tsx +25 -0
  206. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppForgotPage/index.tsx +21 -0
  207. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +11 -3
  208. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +4 -1
  209. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResendPage/index.tsx +21 -0
  210. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResetPage/index.tsx +25 -0
  211. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppSignupPage/index.tsx +21 -0
  212. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/emailAuth.shared.ts +12 -0
  213. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/index.ts +2 -0
  214. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/useEmailDoor.ts +32 -0
  215. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +2 -2
  216. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +50 -1
  217. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +50 -1
  218. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +7 -30
  219. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +13 -14
  220. package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +5 -0
  221. package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +4 -17
  222. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts +0 -38
  223. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-user.entity.ts +0 -54
@@ -46,6 +46,17 @@ export const buildWiringProof = (): WiringProof => ({
46
46
  secretAccessKey: new Secret("fixture-only"),
47
47
  defaultExpiresIn: 300,
48
48
  },
49
+ mail: {
50
+ host: "127.0.0.1",
51
+ port: 2525,
52
+ security: "none",
53
+ from: { name: "Fixture", address: "no-reply@fixture.example" },
54
+ webOrigin: "https://app.fixture.example",
55
+ brandName: "Fixture",
56
+ defaultLanguage: "en",
57
+ confirmTtlSeconds: 86400,
58
+ resetTtlSeconds: 3600,
59
+ },
49
60
  },
50
61
  overrides: [
51
62
  { token: PRIMARY_DATA_SOURCE, value: { manager: {} } },
@@ -1,12 +1,6 @@
1
- import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
1
+ import { defineConfigurableModule } from "@starci/nest-kit"
2
2
  import type { {{Name}}ApiOptions } from "./{{name}}.options"
3
3
 
4
- const built = new ConfigurableModuleBuilder<{{Name}}ApiOptions>().setClassMethodName("register").build()
5
- type Host = ConfigurableModuleHost<{{Name}}ApiOptions, "register", "create", {}>
6
-
7
4
  /** The configurable-module base of the {{name}} 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<{{Name}}ApiOptions>()
@@ -1,12 +1,6 @@
1
- import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
1
+ import { defineConfigurableModule } from "@starci/nest-kit"
2
2
  import type { {{Name}}GraphqlOptions } from "./{{name}}-graphql.options"
3
3
 
4
- const built = new ConfigurableModuleBuilder<{{Name}}GraphqlOptions>().setClassMethodName("register").build()
5
- type Host = ConfigurableModuleHost<{{Name}}GraphqlOptions, "register", "create", {}>
6
-
7
4
  /** The configurable-module base of the {{name}}-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<{{Name}}GraphqlOptions>()
@@ -1,12 +1,5 @@
1
- import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
1
+ import { defineCapabilityModule } from "@starci/nest-kit"
2
2
  import type { {{Name}}Options } from "./{{name}}.options"
3
3
 
4
- const built = new ConfigurableModuleBuilder<{{Name}}Options>()
5
- .setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
6
- .build()
7
- type Host = ConfigurableModuleHost<{{Name}}Options, "register", "create", { isGlobal: boolean }>
8
-
9
4
  /** The configurable-module base of the {{name}} capability. */
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<{{Name}}Options>()
@@ -0,0 +1,16 @@
1
+ // Managed by starci app sync (slot fe.modules.auth.client); do not edit: run "npx starci app sync --execute".
2
+ import { createAuthClient } from "@starci/auth/client"
3
+ import { apiUrl } from "@/modules/config"
4
+ import type { AuthFailure } from "./contracts"
5
+
6
+ /** The email doors of the lite back end; the full edition's identity provider owns its own sign-up and recovery flows. */
7
+ export type EmailDoorName = "signup" | "resend" | "confirm" | "forgot" | "reset"
8
+
9
+ /** The anonymous doors need no session, so they use a client of their own over the same back-end origin. */
10
+ const doors = createAuthClient({ apiOrigin: () => apiUrl() })
11
+
12
+ /** Calls one email door with its posted fields; answers its failure (the wire code and its kind), or undefined when the back end accepted it. */
13
+ export const sendEmailDoor = async (door: EmailDoorName, body: Readonly<Record<string, string | undefined>>): Promise<AuthFailure | undefined> => {
14
+ const outcome = await doors.send(door, body)
15
+ return outcome.success ? undefined : { code: outcome.error, kind: outcome.message, message: "" }
16
+ }
@@ -0,0 +1,5 @@
1
+ // Managed by starci app sync (slot fe.modules.auth.client); do not edit: run "npx starci app sync --execute".
2
+ export { authClient } from "./client"
3
+ export { sendEmailDoor } from "./email-doors"
4
+ export type { EmailDoorName } from "./email-doors"
5
+ export type { AuthFailure, AuthSession } from "./contracts"
@@ -1,81 +1,44 @@
1
1
  import "server-only"
2
- import { hasLocale } from "next-intl"
3
- import type { AbstractIntlMessages } from "next-intl"
4
- import { createNavigation } from "next-intl/navigation"
5
- import { getMessages, getRequestConfig } from "next-intl/server"
6
- import { notFound } from "next/navigation"
2
+ import { createReadLocaleSegment } from "@starci/next-i18n/layout"
3
+ import type { ReadLocaleSegment } from "@starci/next-i18n/layout"
4
+ import { createI18nNavigation } from "@starci/next-i18n/navigation"
5
+ import type { I18nNavigation } from "@starci/next-i18n/navigation"
6
+ import { createRequestConfig } from "@starci/next-i18n/request"
7
+ import type { I18nRequestConfig } from "@starci/next-i18n/request"
8
+ import type { LoadCatalog } from "@starci/next-i18n"
7
9
  import { locale as routeLocale } from "next/root-params"
8
- import { PRODUCT_TIME_ZONE, routing } from "./routing"
9
- import type { ProductRouting } from "./routing"
10
+ import { i18n } from "./routing"
11
+ import type { ProductI18n } from "./routing"
10
12
 
11
- /** Loads one app's own catalog for a served locale. */
12
- type LoadMessages = (locale: string) => Promise<unknown>
13
+ /** The locales the product serves, as the catalog loader of an app receives them. */
14
+ type ProductLocale = ProductI18n["locales"][number]
13
15
 
14
- /** The missing message next-intl asks a fallback for. */
15
- type MissingMessage = { readonly namespace?: string; readonly key: string }
16
-
17
- /** The message tree a catalog module exports as its default: the module is read as `unknown` and checked, never trusted as `any`. */
18
- const isMessages = (value: unknown): value is AbstractIntlMessages => typeof value === "object" && value !== null
19
-
20
- const messagesOf = (loaded: unknown): AbstractIntlMessages => {
21
- const messages = typeof loaded === "object" && loaded !== null && "default" in loaded ? loaded.default : undefined
22
- if (!isMessages(messages)) throw new TypeError("A message catalog module must export its messages as default")
23
- return messages
16
+ /** What one app of the product reads of its next-intl stack. */
17
+ export interface AppI18n {
18
+ readonly navigation: I18nNavigation<ProductI18n["locales"], ProductI18n["localePrefix"]>
19
+ readonly requestConfig: I18nRequestConfig
20
+ readonly readLocaleSegment: ReadLocaleSegment<ProductLocale>
24
21
  }
25
22
 
26
23
  /** `next/root-params` is typed only after Next generates its types, so the reader is checked before it is called. */
27
24
  const isLocaleReader = (value: unknown): value is () => Promise<unknown> => typeof value === "function"
28
25
 
26
+ /**
27
+ * The `[locale]` route param, read through `next/root-params` (`experimental.rootParams` in the app's next.config.ts), never the
28
+ * deprecated `requestLocale`. `next/root-params` is importable only where Next bundles it, which is why this workspace package, not
29
+ * `@starci/next-i18n`, reads it.
30
+ */
29
31
  const readRouteLocale = async (): Promise<unknown> => (isLocaleReader(routeLocale) ? routeLocale() : undefined)
30
32
 
31
- /** The language the address states, its catalog and the product time zone. */
32
- interface LocaleSegment {
33
- readonly locale: string
34
- readonly messages: AbstractIntlMessages
35
- readonly timeZone: string
36
- }
37
-
38
- /** The navigation helpers of the product's routing (links, redirects, the locale-aware router). */
39
- type ProductNavigation = ReturnType<typeof createNavigation<ProductRouting["locales"], "as-needed">>
40
-
41
- /** What one app of the product reads of its next-intl stack. */
42
- export interface AppI18n {
43
- readonly navigation: ProductNavigation
44
- readonly requestConfig: ReturnType<typeof getRequestConfig>
45
- readonly readLocaleSegment: (params: Promise<{ readonly locale: string }>) => Promise<LocaleSegment>
46
- }
47
-
48
33
  /**
49
- * The next-intl stack of one app of the product, written once here and called once by each app's `modules/i18n` (R59): the
50
- * product's one routing with its locale-aware navigation, the request config next-intl's plugin loads (the app's
51
- * `modules/i18n/request.ts` default-exports it) and what the app's `[locale]` layout resolves before it mounts the shell. `app` names
52
- * the app in the text of a message its catalog lacks (`<app>:<namespace>.<key>`); `loadMessages` loads its own catalogs. A server
53
- * module: a client component reads the default locale from `./routing`.
34
+ * What one app of the product calls from its `modules/i18n` (R59): the locale-aware navigation, the request config next-intl's
35
+ * plugin loads (the app's `modules/i18n/request.ts` default-exports it) and what the app's `[locale]` layout resolves before it
36
+ * mounts the shell, all built by `@starci/next-i18n` over the product's one description of its languages. `app` names the app in the
37
+ * text of a message its catalog lacks (`<app>:<namespace>.<key>`); `loadMessages` loads its own catalogs. A server module: a client
38
+ * component reads the default locale from `./routing`.
54
39
  */
55
- export const createAppI18n = (app: string, loadMessages: LoadMessages): AppI18n => ({
56
- navigation: createNavigation<ProductRouting["locales"], "as-needed">(routing),
57
- /**
58
- * The locale is the `[locale]` route param read through `next/root-params` (`experimental.rootParams` in the app's next.config.ts,
59
- * FE-I18N-1), never the deprecated `requestLocale`. An unrecognised locale resolves to the default before the loader runs, so a
60
- * missing catalog file is never imported.
61
- */
62
- requestConfig: getRequestConfig(async () => {
63
- const requested = await readRouteLocale()
64
- const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale
65
- return {
66
- locale,
67
- timeZone: PRODUCT_TIME_ZONE,
68
- messages: messagesOf(await loadMessages(locale)),
69
- getMessageFallback: ({ namespace, key }: MissingMessage) => `${app}:${namespace ?? ""}.${key}`,
70
- }
71
- }),
72
- /**
73
- * The language the address states, its catalog and the product time zone. An unknown segment is a route that does not exist,
74
- * not a request to fall back silently.
75
- */
76
- readLocaleSegment: async (params: Promise<{ readonly locale: string }>) => {
77
- const { locale } = await params
78
- if (!hasLocale(routing.locales, locale)) notFound()
79
- return { locale, messages: await getMessages(), timeZone: PRODUCT_TIME_ZONE }
80
- },
40
+ export const createAppI18n = (app: string, loadMessages: LoadCatalog<ProductLocale>): AppI18n => ({
41
+ navigation: createI18nNavigation(i18n),
42
+ requestConfig: createRequestConfig(i18n, { app, loadMessages, readRouteLocale }),
43
+ readLocaleSegment: createReadLocaleSegment(i18n),
81
44
  })
@@ -1,24 +1,9 @@
1
- import "server-only"
2
- import createMiddleware from "next-intl/middleware"
3
- import { NextResponse, type NextRequest } from "next/server"
4
- import { routing } from "./routing"
5
-
6
- /** The first path segments of the API, the health probe and the framework: never locale-negotiated. */
7
- const UNNEGOTIATED_ROOTS: ReadonlySet<string> = new Set(["api", "health", "_next", "_vercel"])
8
-
9
- /** Whether the path belongs to the API, the health probe, the framework or a file with an extension. */
10
- const isUnnegotiated = (pathname: string): boolean => {
11
- const segments = pathname.split("/")
12
- const last = segments.at(-1) ?? ""
13
- return UNNEGOTIATED_ROOTS.has(segments[1] ?? "") || (last.includes(".") && !last.endsWith("."))
14
- }
15
-
16
- const negotiate = createMiddleware(routing)
1
+ import { createProxy } from "@starci/next-i18n/proxy"
2
+ import type { LocaleProxy } from "@starci/next-i18n/proxy"
3
+ import { i18n } from "./routing"
17
4
 
18
5
  /**
19
- * The one thing that runs before a route exists: which language it is in. next-intl's middleware reads the cookie, falls back to
20
- * the default and serves the default locale (vi) without a prefix. One routing table for every app; each app's `src/proxy.ts`
21
- * re-exports this.
6
+ * The one thing that runs before a route exists: which language it is in. The product's one description of its languages, read by
7
+ * the package's proxy; each app's `src/proxy.ts` re-exports this.
22
8
  */
23
- export const proxy = (request: NextRequest): NextResponse =>
24
- isUnnegotiated(request.nextUrl.pathname) ? NextResponse.next() : negotiate(request)
9
+ export const proxy: LocaleProxy = createProxy(i18n)
@@ -1,24 +1,18 @@
1
- import { defineRouting } from "next-intl/routing"
2
- import type { RoutingConfig } from "next-intl/routing"
1
+ import { defineI18n } from "@starci/next-i18n"
2
+ import type { I18n } from "@starci/next-i18n"
3
3
 
4
4
  /** The locales every app of the product serves; every catalog of an app has the same keys in each. */
5
5
  const LOCALES = ["vi", "en"] as const
6
6
 
7
- /** The locale served at the unprefixed path, and the language of a document no locale reached (the global error boundary). */
8
- export const DEFAULT_LOCALE = "vi"
9
-
10
- /** The product's time zone: fixed, not inferred, so the server and the hydrated client format the same instant the same way. */
11
- export const PRODUCT_TIME_ZONE = "Asia/Ho_Chi_Minh"
7
+ /** The description of the product's languages: the locales and the `as-needed` prefix mode that `defineI18n` types. */
8
+ export type ProductI18n = I18n<typeof LOCALES, "as-needed">
12
9
 
13
- /** The type of the product's routing table: the served locales, no locale prefix for the default one, no localized pathnames or domains. */
14
- export type ProductRouting = RoutingConfig<typeof LOCALES, "as-needed", never, never>
10
+ /** The locale served at the unprefixed path, and the language of a document no locale reached (the global error boundary). */
11
+ export const DEFAULT_LOCALE: ProductI18n["defaultLocale"] = "vi"
15
12
 
16
13
  /**
17
- * The one routing table of the product, for the proxy, the navigation and the request config of every app: vi default, a prefix
18
- * only when the locale is not the default, so `/vi` means the same thing on every app.
14
+ * The one description of the product's languages, for the proxy, the navigation and the request config of every app: vi default,
15
+ * a prefix only when the locale is not the default, and a fixed time zone so the server and the hydrated client format the same
16
+ * instant the same way. The stack that reads it is `@starci/next-i18n`'s; this package only states the languages (FE-I18N-1).
19
17
  */
20
- export const routing: ProductRouting = defineRouting({
21
- locales: LOCALES,
22
- defaultLocale: DEFAULT_LOCALE,
23
- localePrefix: "as-needed",
24
- })
18
+ export const i18n: ProductI18n = defineI18n({ locales: LOCALES, defaultLocale: DEFAULT_LOCALE, timeZone: "Asia/Ho_Chi_Minh" })
@@ -0,0 +1,9 @@
1
+ import { AppConfirmPage } from "@/features/pages/AppConfirmPage"
2
+
3
+ /** The document title of this page. */
4
+ export { appConfirmMetadata as generateMetadata } from "@/features/pages/AppConfirmPage"
5
+
6
+ /** The confirm page slot: Next hands the query over as a promise, which the page reads for the token of the mail link. */
7
+ const Page = (props: Parameters<typeof AppConfirmPage>[0]) => <AppConfirmPage {...props} />
8
+
9
+ export default Page
@@ -0,0 +1,9 @@
1
+ import { AppForgotPage } from "@/features/pages/AppForgotPage"
2
+
3
+ /** The document title of this page. */
4
+ export { appForgotMetadata as generateMetadata } from "@/features/pages/AppForgotPage"
5
+
6
+ /** The forgot page slot. */
7
+ const Page = () => <AppForgotPage />
8
+
9
+ export default Page
@@ -0,0 +1,9 @@
1
+ import { AppResendPage } from "@/features/pages/AppResendPage"
2
+
3
+ /** The document title of this page. */
4
+ export { appResendMetadata as generateMetadata } from "@/features/pages/AppResendPage"
5
+
6
+ /** The resend page slot. */
7
+ const Page = () => <AppResendPage />
8
+
9
+ export default Page
@@ -0,0 +1,9 @@
1
+ import { AppResetPage } from "@/features/pages/AppResetPage"
2
+
3
+ /** The document title of this page. */
4
+ export { appResetMetadata as generateMetadata } from "@/features/pages/AppResetPage"
5
+
6
+ /** The reset page slot: Next hands the query over as a promise, which the page reads for the token of the mail link. */
7
+ const Page = (props: Parameters<typeof AppResetPage>[0]) => <AppResetPage {...props} />
8
+
9
+ export default Page
@@ -0,0 +1,9 @@
1
+ import { AppSignupPage } from "@/features/pages/AppSignupPage"
2
+
3
+ /** The document title of this page. */
4
+ export { appSignupMetadata as generateMetadata } from "@/features/pages/AppSignupPage"
5
+
6
+ /** The signup page slot. */
7
+ const Page = () => <AppSignupPage />
8
+
9
+ export default Page
@@ -1,10 +1,8 @@
1
- import { hasLocale } from "next-intl"
2
- import { getMessages } from "next-intl/server"
3
- import { notFound } from "next/navigation"
1
+ import { createReadLocaleSegment } from "@starci/next-i18n/layout"
4
2
  import type { Metadata } from "next"
5
3
  import type { ReactElement, ReactNode } from "react"
6
4
  import { AppLayout, appLayoutMetadata } from "@/features/layouts/AppLayout"
7
- import { PRODUCT_TIME_ZONE, routing } from "@/modules/i18n"
5
+ import { i18n } from "@/modules/i18n"
8
6
  import { Providers } from "./providers"
9
7
  import "../globals.css"
10
8
  import "@/modules/brand"
@@ -17,16 +15,14 @@ type LayoutProps = {
17
15
  /** The localized document title of this route segment. */
18
16
  export const generateMetadata = async (props: LayoutProps): Promise<Metadata> => appLayoutMetadata((await props.params).locale)
19
17
 
20
- /** The locale segment's document, providers and app chrome. */
18
+ /** The locale segment's document, providers and app chrome; an unknown locale is a 404. */
21
19
  const Layout = async (props: LayoutProps): Promise<ReactElement> => {
22
- const { locale } = await props.params
23
- if (!hasLocale(routing.locales, locale)) notFound()
24
- const messages = await getMessages()
20
+ const { locale, messages, timeZone } = await createReadLocaleSegment(i18n)(props.params)
25
21
  return (
26
22
  <AppLayout
27
23
  locale={locale}
28
24
  content={
29
- <Providers locale={locale} messages={messages} timeZone={PRODUCT_TIME_ZONE}>
25
+ <Providers locale={locale} messages={messages} timeZone={timeZone}>
30
26
  {props.children}
31
27
  </Providers>
32
28
  }
@@ -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
+ }
@@ -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
+ }