@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
@@ -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 = {
@@ -1,38 +0,0 @@
1
- // Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
2
- import { Column, Entity, PrimaryColumn } from "typeorm"
3
-
4
- @Entity({ schema: "auth", name: "identities" })
5
- /** One external identity of an account (a Google subject), mapped from auth.identities of db/bootstrap.sql. */
6
- export class AuthIdentityEntity {
7
- /** The row id. */
8
- @PrimaryColumn({ name: "id", type: "uuid" })
9
- id!: string
10
-
11
- /** The provider's id of the person (the Google subject). */
12
- @Column({ name: "provider_id", type: "text" })
13
- providerId!: string
14
-
15
- /** The account the identity belongs to. */
16
- @Column({ name: "user_id", type: "uuid" })
17
- userId!: string
18
-
19
- /** What the provider told us, as JSON. */
20
- @Column({ name: "identity_data", type: "jsonb" })
21
- identityData!: Record<string, string | boolean | null>
22
-
23
- /** The provider name. */
24
- @Column({ name: "provider", type: "text" })
25
- provider!: string
26
-
27
- /** The last sign-in through this identity, or null. */
28
- @Column({ name: "last_sign_in_at", type: "timestamptz", nullable: true })
29
- lastSignInAt!: Date | null
30
-
31
- /** When the identity was linked. */
32
- @Column({ name: "created_at", type: "timestamptz", nullable: true })
33
- createdAt!: Date | null
34
-
35
- /** When the identity last changed. */
36
- @Column({ name: "updated_at", type: "timestamptz", nullable: true })
37
- updatedAt!: Date | null
38
- }
@@ -1,54 +0,0 @@
1
- // Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
2
- import { Column, Entity, PrimaryColumn } from "typeorm"
3
-
4
- @Entity({ schema: "auth", name: "users" })
5
- /** One person who can sign in, mapped from auth.users of db/bootstrap.sql (the GoTrue column set; only the columns sign-in reads or writes). */
6
- export class AuthUserEntity {
7
- /** The account id. */
8
- @PrimaryColumn({ name: "id", type: "uuid" })
9
- id!: string
10
-
11
- /** The audience the row carries. */
12
- @Column({ name: "aud", type: "varchar", length: 255, nullable: true })
13
- aud!: string | null
14
-
15
- /** The database role the row carries. */
16
- @Column({ name: "role", type: "varchar", length: 255, nullable: true })
17
- role!: string | null
18
-
19
- /** The sign-in address, lower-cased. */
20
- @Column({ name: "email", type: "varchar", length: 255, nullable: true })
21
- email!: string | null
22
-
23
- /** The bcrypt hash of the password, or null for an account that signs in with Google only. */
24
- @Column({ name: "encrypted_password", type: "varchar", length: 255, nullable: true })
25
- encryptedPassword!: string | null
26
-
27
- /** When the mailbox was confirmed, or null. */
28
- @Column({ name: "email_confirmed_at", type: "timestamptz", nullable: true })
29
- emailConfirmedAt!: Date | null
30
-
31
- /** The last sign-in, or null. */
32
- @Column({ name: "last_sign_in_at", type: "timestamptz", nullable: true })
33
- lastSignInAt!: Date | null
34
-
35
- /** Until when the account is banned, or null. */
36
- @Column({ name: "banned_until", type: "timestamptz", nullable: true })
37
- bannedUntil!: Date | null
38
-
39
- /** When the account was deleted, or null. */
40
- @Column({ name: "deleted_at", type: "timestamptz", nullable: true })
41
- deletedAt!: Date | null
42
-
43
- /** True for an anonymous account, which never signs in with an address. */
44
- @Column({ name: "is_anonymous", type: "boolean" })
45
- isAnonymous!: boolean
46
-
47
- /** When the account was created. */
48
- @Column({ name: "created_at", type: "timestamptz", nullable: true })
49
- createdAt!: Date | null
50
-
51
- /** When the account last changed. */
52
- @Column({ name: "updated_at", type: "timestamptz", nullable: true })
53
- updatedAt!: Date | null
54
- }