@starci/hfs 5.1.0 → 5.2.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 (199) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +10 -1
  3. package/db/postgres.mjs +9 -1
  4. package/package.json +1 -1
  5. package/runtime/knowledge/hfs/canon-pins.yaml +30 -9
  6. package/runtime/knowledge/hfs/fe-stack.yaml +7 -7
  7. package/runtime/knowledge/hfs/fork-images.yaml +7 -0
  8. package/runtime/knowledge/hfs/rules.yaml +55 -7
  9. package/runtime/knowledge/hfs/slots.yaml +75 -24
  10. package/runtime/modules/kernel/failure-codes.yaml +62 -0
  11. package/runtime/modules/schemas/application-stacks.schema.yaml +6 -0
  12. package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
  13. package/runtime/scripts/hfs/architecture/transport-owner.d.mts +1 -2
  14. package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -6
  15. package/runtime/scripts/hfs/manifest-shape.d.mts +1 -0
  16. package/runtime/scripts/hfs/manifest-shape.mjs +3 -1
  17. package/runtime/scripts/hfs/rules/a15-findings.mjs +5 -0
  18. package/runtime/scripts/hfs/rules/stacks-local.d.mts +7 -0
  19. package/runtime/scripts/hfs/rules/stacks-local.mjs +71 -0
  20. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +6 -1
  21. package/runtime/scripts/hfs/rules/stacks-service-images.d.mts +12 -0
  22. package/runtime/scripts/hfs/rules/stacks-service-images.mjs +120 -0
  23. package/runtime/scripts/hfs/secret-dev-init.d.mts +11 -0
  24. package/runtime/scripts/hfs/secret-dev-init.mjs +79 -0
  25. package/runtime/scripts/hfs/secret.mjs +10 -7
  26. package/runtime/scripts/hfs/slots.mjs +1 -1
  27. package/runtime/scripts/hfs/stack-local-config.d.mts +101 -0
  28. package/runtime/scripts/hfs/stack-local-config.mjs +320 -0
  29. package/runtime/scripts/hfs/stacks-gate.mjs +1 -1
  30. package/runtime/scripts/lib/swarm-stack.d.mts +2 -1
  31. package/runtime/scripts/lib/swarm-stack.mjs +2 -2
  32. package/scaffold/add-module.d.mts +16 -0
  33. package/scaffold/add-module.mjs +232 -0
  34. package/scaffold/add-queue-lite.mjs +49 -0
  35. package/scaffold/add.mjs +5 -0
  36. package/scaffold/app.mjs +7 -7
  37. package/scaffold/module-registry.d.mts +19 -0
  38. package/scaffold/module-registry.mjs +84 -0
  39. package/src/main.mjs +23 -2
  40. package/stack/cli.mjs +6 -1
  41. package/stack/context.d.mts +15 -0
  42. package/stack/context.mjs +1 -0
  43. package/stack/local-cli.d.mts +25 -0
  44. package/stack/local-cli.mjs +89 -0
  45. package/stack/local-config.d.mts +17 -0
  46. package/stack/local-config.mjs +37 -0
  47. package/stack/local-custody.d.mts +24 -0
  48. package/stack/local-custody.mjs +109 -0
  49. package/stack/local-doctor.d.mts +6 -0
  50. package/stack/local-doctor.mjs +49 -0
  51. package/stack/local-down.d.mts +8 -0
  52. package/stack/local-down.mjs +20 -0
  53. package/stack/local-host.d.mts +42 -0
  54. package/stack/local-host.mjs +163 -0
  55. package/stack/local-logs.d.mts +8 -0
  56. package/stack/local-logs.mjs +32 -0
  57. package/stack/local-ports.d.mts +7 -0
  58. package/stack/local-ports.mjs +39 -0
  59. package/stack/local-prepare.d.mts +22 -0
  60. package/stack/local-prepare.mjs +62 -0
  61. package/stack/local-run.d.mts +71 -0
  62. package/stack/local-run.mjs +200 -0
  63. package/stack/local-state.d.mts +35 -0
  64. package/stack/local-state.mjs +29 -0
  65. package/stack/local-status.d.mts +6 -0
  66. package/stack/local-status.mjs +36 -0
  67. package/stack/local-up.d.mts +11 -0
  68. package/stack/local-up.mjs +166 -0
  69. package/stack/local-verify.d.mts +6 -0
  70. package/stack/local-verify.mjs +21 -0
  71. package/sync/index.d.mts +13 -12
  72. package/sync/index.mjs +83 -28
  73. package/templates/app/module/jest.config.js.tpl +8 -0
  74. package/templates/app/module/src/index.ts.tpl +2 -0
  75. package/templates/app/module/src/manifest/declaration.spec.ts.tpl +11 -0
  76. package/templates/app/module/src/manifest/declaration.ts.tpl +13 -0
  77. package/templates/app/module/src/manifest/index.ts.tpl +1 -0
  78. package/templates/app/module/src/public/index.ts.tpl +2 -0
  79. package/templates/app/module/src/template/__name__.template.spec.ts.tpl +13 -0
  80. package/templates/app/module/src/template/__name__.template.ts.tpl +41 -0
  81. package/templates/app/module/src/template/index.ts.tpl +1 -0
  82. package/templates/app/module/src/workbench/components/__pascal__WorkbenchView.tsx.tpl +16 -0
  83. package/templates/app/module/src/workbench/index.ts.tpl +2 -0
  84. package/templates/app/package-scripts/package.json +1 -1
  85. package/templates/app/package-scripts-lite/package.json +1 -1
  86. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +76 -70
  87. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +23 -13
  88. package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +25 -0
  89. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -26
  90. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +1 -0
  91. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +2 -1
  92. package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +57 -14
  93. package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +11 -0
  94. package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.handler.ts +23 -0
  95. package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.query.ts +12 -0
  96. package/templates/be/auth-lite/src/features/api/auth/application/google-begin.command.ts +11 -0
  97. package/templates/be/auth-lite/src/features/api/auth/application/google-begin.handler.ts +22 -0
  98. package/templates/be/auth-lite/src/features/api/auth/application/google-complete.command.ts +12 -0
  99. package/templates/be/auth-lite/src/features/api/auth/application/google-complete.handler.ts +23 -0
  100. package/templates/be/auth-lite/src/features/api/auth/application/login.command.ts +12 -0
  101. package/templates/be/auth-lite/src/features/api/auth/application/login.handler.ts +23 -0
  102. package/templates/be/auth-lite/src/features/api/auth/application/logout.command.ts +11 -0
  103. package/templates/be/auth-lite/src/features/api/auth/application/logout.handler.ts +22 -0
  104. package/templates/be/auth-lite/src/features/api/auth/application/refresh.command.ts +12 -0
  105. package/templates/be/auth-lite/src/features/api/auth/application/refresh.handler.ts +23 -0
  106. package/templates/be/auth-lite/src/features/api/auth/application/session.command.ts +12 -0
  107. package/templates/be/auth-lite/src/features/api/auth/application/session.handler.ts +23 -0
  108. package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +13 -0
  109. package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +16 -0
  110. package/templates/be/auth-lite/src/features/api/auth/auth.options.ts +3 -0
  111. package/templates/be/auth-lite/src/features/api/auth/index.ts +2 -0
  112. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +13 -0
  113. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module.ts +9 -0
  114. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.options.ts +3 -0
  115. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +90 -0
  116. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/google-start.request.ts +17 -0
  117. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/login.request.ts +16 -0
  118. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +46 -0
  119. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +28 -0
  120. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +145 -0
  121. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +95 -0
  122. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts +38 -0
  123. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-user.entity.ts +54 -0
  124. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-attempt.entity.ts +18 -0
  125. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +34 -0
  126. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-session.entity.ts +30 -0
  127. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +17 -0
  128. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +86 -0
  129. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +45 -0
  130. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +97 -0
  131. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +81 -0
  132. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +78 -0
  133. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +87 -0
  134. package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +44 -0
  135. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +6 -5
  136. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +3 -2
  137. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +10 -5
  138. package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +34 -0
  139. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +22 -1
  140. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +47 -2
  141. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -0
  142. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +24 -0
  143. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +3 -3
  144. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +3 -1
  145. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +8 -4
  146. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +12 -0
  147. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module.ts +29 -0
  148. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +6 -31
  149. package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +18 -9
  150. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +35 -11
  151. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts +44 -8
  152. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +40 -18
  153. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +18 -6
  154. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.spec.ts +73 -0
  155. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.ts +26 -0
  156. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +24 -53
  157. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.registration.ts +31 -0
  158. package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +1 -1
  159. package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +2 -1
  160. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
  161. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +11 -4
  162. package/templates/be/test-world/e2e/serves-once-migrated.e2e-spec.ts.tpl +9 -9
  163. package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +4 -4
  164. package/templates/fe/auth-lite/apps/web/src/modules/auth/client.ts +26 -0
  165. package/templates/fe/common/site-shell.tsx.partial +3 -3
  166. package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +33 -10
  167. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +1 -0
  168. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useViewerId.ts +11 -0
  169. package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +15 -12
  170. package/templates/fe/skeleton/apps/__app__/src/modules/viewer-cache/index.ts +5 -0
  171. package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +1 -1
  172. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -1
  173. package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +4 -7
  174. package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/index.tsx +2 -3
  175. package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +4 -7
  176. package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/index.tsx +1 -2
  177. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +0 -16
  178. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +0 -11
  179. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +0 -7
  180. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +0 -6
  181. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +0 -8
  182. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +0 -6
  183. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -186
  184. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +0 -22
  185. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +0 -25
  186. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.spec.ts +0 -104
  187. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +0 -34
  188. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.spec.ts +0 -28
  189. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +0 -11
  190. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +0 -88
  191. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +0 -69
  192. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +0 -41
  193. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +0 -40
  194. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +0 -63
  195. package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/classNames.ts +0 -4
  196. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +0 -30
  197. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +0 -50
  198. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +0 -31
  199. package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/classNames.ts +0 -4
@@ -0,0 +1,44 @@
1
+ // Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
2
+ const BEFORE = new Date("2026-02-03T04:00:00.000Z")
3
+ const AFTER = new Date("2026-02-03T05:00:00.000Z")
4
+
5
+ /** The kinds of account a sign-in spec needs. */
6
+ export type AuthUserVariant = "live" | "banned" | "ban-ended" | "deleted" | "unconfirmed" | "addressless" | "passwordless" | "created"
7
+
8
+ /** A row of auth.users as the sign-in reads it (the shape of AuthUserEntity). */
9
+ export interface AuthUserRow {
10
+ id: string
11
+ aud: string | null
12
+ role: string | null
13
+ email: string | null
14
+ encryptedPassword: string | null
15
+ emailConfirmedAt: Date | null
16
+ lastSignInAt: Date | null
17
+ bannedUntil: Date | null
18
+ deletedAt: Date | null
19
+ isAnonymous: boolean
20
+ createdAt: Date | null
21
+ updatedAt: Date | null
22
+ }
23
+
24
+ /** Until when an account of this variant is banned, or null. */
25
+ const bannedUntilOf = (variant: AuthUserVariant): Date | null => {
26
+ if (variant === "banned") return AFTER
27
+ return variant === "ban-ended" ? BEFORE : null
28
+ }
29
+
30
+ /** A confirmed, live password account of the given variant; \`created\` is the account the first Google sign-in makes (the first id the fake ids hand out). */
31
+ export const authUserRow = (variant: AuthUserVariant = "live"): AuthUserRow => ({
32
+ id: variant === "created" ? "00000000-0000-4000-8000-000000000001" : "u-1",
33
+ aud: "authenticated",
34
+ role: "authenticated",
35
+ email: variant === "addressless" ? null : "a@example.test",
36
+ encryptedPassword: variant === "passwordless" || variant === "created" ? null : "hash",
37
+ emailConfirmedAt: variant === "unconfirmed" ? null : BEFORE,
38
+ lastSignInAt: null,
39
+ bannedUntil: bannedUntilOf(variant),
40
+ deletedAt: variant === "deleted" ? BEFORE : null,
41
+ isAnonymous: false,
42
+ createdAt: BEFORE,
43
+ updatedAt: BEFORE,
44
+ })
@@ -2,7 +2,7 @@ import { Module } from "@nestjs/common"
2
2
  import type { DynamicModule } from "@nestjs/common"
3
3
  import { APP_FILTER, APP_GUARD } from "@nestjs/core"
4
4
  import { AuthGuard, CredentialGuard } from "@modules/domain/identity"
5
- import { tokenKit } from "@modules/integrations/passport"
5
+ import { passportKit } from "@modules/integrations/passport"
6
6
  import { ErrorsFilter } from "@modules/platform/errors"
7
7
  import { OriginGuard, RateLimitGuard } from "@modules/platform/http-security"
8
8
  import { platformModules } from "./compose/platform"
@@ -10,7 +10,7 @@ import { domainModules } from "./compose/domain"
10
10
  import { systemModules } from "./compose/system"
11
11
  import { apiModules } from "./compose/api"
12
12
  import { webhooksModules } from "./compose/webhooks"
13
- import { integrationProviders } from "./compose/integrations"
13
+ import { integrationModules, integrationProviders } from "./compose/integrations"
14
14
  import type { ApiOptions } from "./api.options"
15
15
 
16
16
  @Module({})
@@ -18,18 +18,19 @@ import type { ApiOptions } from "./api.options"
18
18
  export class AppModule {
19
19
  /** Builds the module from options parsed once at boot; nothing here reads the environment. */
20
20
  static register(options: ApiOptions): DynamicModule {
21
- const tokens = tokenKit(options.passport.tokens)
21
+ const passport = passportKit(options.passport)
22
22
  return {
23
23
  module: AppModule,
24
24
  imports: [
25
25
  ...platformModules(options),
26
- ...domainModules(tokens.verify),
26
+ ...integrationModules(options),
27
+ ...domainModules(passport.verify),
27
28
  ...systemModules(),
28
29
  ...apiModules(),
29
30
  ...webhooksModules(),
30
31
  ],
31
32
  providers: [
32
- ...integrationProviders(options, tokens.verify),
33
+ ...integrationProviders(options, passport.verify),
33
34
  { provide: APP_FILTER, useClass: ErrorsFilter },
34
35
  { provide: APP_GUARD, useClass: RateLimitGuard },
35
36
  { provide: APP_GUARD, useClass: OriginGuard },
@@ -1,4 +1,5 @@
1
1
  import type { DynamicModule, Type } from "@nestjs/common"
2
+ import { AuthHttpModule } from "@features/api/auth"
2
3
 
3
- /** The API transports of the app; `starci app add api` registers each generated door here. */
4
- export const apiModules = (): Array<Type<unknown> | DynamicModule> => []
4
+ /** The API transports of the app: the sign-in doors; `starci app add api` registers each generated door here. */
5
+ export const apiModules = (): Array<Type<unknown> | DynamicModule> => [AuthHttpModule.register({})]
@@ -1,12 +1,17 @@
1
- import type { Provider } from "@nestjs/common"
2
- import { OWN_TOKEN_VERIFIER, PASSPORT_OPTIONS } from "@modules/integrations/passport"
1
+ import type { DynamicModule, Provider } from "@nestjs/common"
2
+ import { OWN_TOKEN_VERIFIER, PASSPORT_OPTIONS, passportModule } from "@modules/integrations/passport"
3
3
  import type { VerifyAccessToken } from "@modules/integrations/passport"
4
- import { OBJECT_STORAGE, createObjectStorage } from "@modules/integrations/object-storage"
4
+ import { ObjectStorageModule } from "@modules/integrations/object-storage"
5
5
  import type { ApiOptions } from "../api.options"
6
6
 
7
- /** The passport and object-storage integration ports bound once for this app. */
7
+ /** The sign-in and object storage packages of the app, each registered once with the app's typed settings. */
8
+ export const integrationModules = (options: ApiOptions): Array<DynamicModule> => [
9
+ passportModule(options.passport, { isGlobal: true }),
10
+ ObjectStorageModule.register({ ...options.objectStorage, isGlobal: true }),
11
+ ]
12
+
13
+ /** The passport integration ports bound once for this app. */
8
14
  export const integrationProviders = (options: ApiOptions, verifyAccessToken: VerifyAccessToken): Array<Provider> => [
9
15
  { provide: PASSPORT_OPTIONS, useValue: options.passport },
10
16
  { provide: OWN_TOKEN_VERIFIER, useValue: verifyAccessToken },
11
- { provide: OBJECT_STORAGE, useValue: createObjectStorage(options.objectStorage) },
12
17
  ]
@@ -0,0 +1,34 @@
1
+ import { DomainError } from "@modules/platform/errors"
2
+ import type { ErrorKind } from "@modules/platform/errors"
3
+
4
+ /** Codes of the identity capability: the sign-in refusals travel under these wire codes, the same on both sides of the doors. */
5
+ export enum IdentityErrorCode {
6
+ /** The door needs a signed-in caller and the request establishes none. */
7
+ Unauthenticated = "IDENTITY_UNAUTHENTICATED",
8
+ /** The refresh token is unknown, expired or already spent. */
9
+ RefreshInvalid = "IDENTITY_REFRESH_INVALID",
10
+ /** A rotated refresh token was presented again: the session was ended. */
11
+ RefreshReused = "IDENTITY_REFRESH_REUSED",
12
+ /** The account is deleted or banned. */
13
+ AccountDisabled = "IDENTITY_ACCOUNT_DISABLED",
14
+ /** Too many failed attempts for this address; try again later. */
15
+ TooManyAttempts = "IDENTITY_TOO_MANY_ATTEMPTS",
16
+ /** The pair of address and password is wrong (the same answer for an unknown address). */
17
+ PairRefused = "IDENTITY_INVALID_CREDENTIALS",
18
+ /** The mailbox was never confirmed. */
19
+ EmailNotConfirmed = "IDENTITY_EMAIL_NOT_CONFIRMED",
20
+ }
21
+
22
+ /** How each identity code travels. */
23
+ export const IDENTITY_ERROR_KINDS: Record<IdentityErrorCode, ErrorKind> = {
24
+ [IdentityErrorCode.Unauthenticated]: "unauthenticated",
25
+ [IdentityErrorCode.RefreshInvalid]: "unauthenticated",
26
+ [IdentityErrorCode.RefreshReused]: "unauthenticated",
27
+ [IdentityErrorCode.AccountDisabled]: "forbidden",
28
+ [IdentityErrorCode.TooManyAttempts]: "rate-limited",
29
+ [IdentityErrorCode.PairRefused]: "unauthenticated",
30
+ [IdentityErrorCode.EmailNotConfirmed]: "forbidden",
31
+ }
32
+
33
+ /** The one error class of the identity capability. */
34
+ export class IdentityError extends DomainError<IdentityErrorCode> {}
@@ -2,7 +2,7 @@ import { SetMetadata, applyDecorators, createParamDecorator } from "@nestjs/comm
2
2
  import type { ExecutionContext } from "@nestjs/common"
3
3
  import { injector } from "@modules/platform/composition"
4
4
  import type { DoorDecorator, TypedParameterDecorator } from "@modules/platform/composition"
5
- import type { VerifyAccessToken } from "@modules/integrations/passport"
5
+ import type { AuthDoors, AuthPasswords, AuthSessions, SignInBudget, VerifyAccessToken } from "@modules/integrations/passport"
6
6
  import { ServerToServer, requestOf } from "@modules/platform/http-security"
7
7
  import type { Principal } from "@modules/platform/cqrs"
8
8
  import type { CredentialAuthenticator, CredentialMetadata } from "./credential.contracts"
@@ -10,6 +10,27 @@ import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
10
10
  import { PublicReason } from "./identity.contracts"
11
11
  import type { PublicMetadata } from "./identity.contracts"
12
12
 
13
+ /** Token of the sign-in doors of the package, provided by the identity module from the passport integration. */
14
+ export const IDENTITY_AUTH_DOORS: unique symbol = Symbol("domain.identity.auth-doors")
15
+ /** Token of the rotating sessions of the package. */
16
+ export const IDENTITY_AUTH_SESSIONS: unique symbol = Symbol("domain.identity.auth-sessions")
17
+ /** Token of the password hasher of the package. */
18
+ export const IDENTITY_AUTH_PASSWORDS: unique symbol = Symbol("domain.identity.auth-passwords")
19
+ /** Token of the failures-only sign-in budget of the package. */
20
+ export const IDENTITY_SIGN_IN_BUDGET: unique symbol = Symbol("domain.identity.sign-in-budget")
21
+
22
+ /** Injects the sign-in doors. Parameter type: AuthDoors. */
23
+ export const InjectAuthDoors = (): TypedParameterDecorator<AuthDoors> => injector<AuthDoors>(IDENTITY_AUTH_DOORS)
24
+
25
+ /** Injects the rotating sessions. Parameter type: AuthSessions. */
26
+ export const InjectAuthSessions = (): TypedParameterDecorator<AuthSessions> => injector<AuthSessions>(IDENTITY_AUTH_SESSIONS)
27
+
28
+ /** Injects the password hasher. Parameter type: AuthPasswords. */
29
+ export const InjectAuthPasswords = (): TypedParameterDecorator<AuthPasswords> => injector<AuthPasswords>(IDENTITY_AUTH_PASSWORDS)
30
+
31
+ /** Injects the sign-in budget. Parameter type: SignInBudget. */
32
+ export const InjectSignInBudget = (): TypedParameterDecorator<SignInBudget> => injector<SignInBudget>(IDENTITY_SIGN_IN_BUDGET)
33
+
13
34
  /** Token of the access-token verifier the composition root hands to identity. */
14
35
  export const ACCESS_TOKEN_VERIFIER: unique symbol = Symbol("domain.identity.access-token-verifier")
15
36
 
@@ -1,10 +1,32 @@
1
1
  import { Module } from "@nestjs/common"
2
2
  import type { DynamicModule, Provider } from "@nestjs/common"
3
+ import {
4
+ PASSPORT_ACCOUNTS,
5
+ PASSPORT_AUTHENTICATOR,
6
+ PASSPORT_DOORS,
7
+ PASSPORT_PASSWORDS,
8
+ PASSPORT_RATE_COUNTER,
9
+ PASSPORT_SESSIONS,
10
+ PASSPORT_SESSION_STORE,
11
+ PASSPORT_SIGN_IN_BUDGET,
12
+ } from "@modules/integrations/passport"
13
+ import { IdentityAccountDirectoryService } from "./account-directory.service"
3
14
  import { AuthGuard } from "./auth.guard"
15
+ import { IdentityAuthenticatorService } from "./authenticator.service"
4
16
  import { CredentialGuard } from "./credential.guard"
5
17
  import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
6
- import { ACCESS_TOKEN_VERIFIER, CREDENTIAL_AUTHENTICATOR } from "./identity.decorators"
18
+ import {
19
+ ACCESS_TOKEN_VERIFIER,
20
+ CREDENTIAL_AUTHENTICATOR,
21
+ IDENTITY_AUTH_DOORS,
22
+ IDENTITY_AUTH_PASSWORDS,
23
+ IDENTITY_AUTH_SESSIONS,
24
+ IDENTITY_SIGN_IN_BUDGET,
25
+ } from "./identity.decorators"
7
26
  import { ConfigurableModuleClass, OPTIONS_TYPE } from "./identity.module-definition"
27
+ import { IdentityRateCounterService } from "./rate-counter.service"
28
+ import { IdentitySessionStoreService } from "./session-store.service"
29
+ import { IdentitySignInService } from "./sign-in.service"
8
30
 
9
31
  const refusingAuthenticator: Provider = {
10
32
  provide: CREDENTIAL_AUTHENTICATOR,
@@ -34,8 +56,31 @@ export class IdentityModule extends ConfigurableModuleClass {
34
56
  authenticator,
35
57
  CredentialGuard,
36
58
  AuthGuard,
59
+ IdentitySessionStoreService,
60
+ IdentityAccountDirectoryService,
61
+ IdentityRateCounterService,
62
+ IdentityAuthenticatorService,
63
+ IdentitySignInService,
64
+ { provide: IDENTITY_AUTH_DOORS, useExisting: PASSPORT_DOORS },
65
+ { provide: IDENTITY_AUTH_SESSIONS, useExisting: PASSPORT_SESSIONS },
66
+ { provide: IDENTITY_AUTH_PASSWORDS, useExisting: PASSPORT_PASSWORDS },
67
+ { provide: IDENTITY_SIGN_IN_BUDGET, useExisting: PASSPORT_SIGN_IN_BUDGET },
68
+ { provide: PASSPORT_SESSION_STORE, useExisting: IdentitySessionStoreService },
69
+ { provide: PASSPORT_ACCOUNTS, useExisting: IdentityAccountDirectoryService },
70
+ { provide: PASSPORT_RATE_COUNTER, useExisting: IdentityRateCounterService },
71
+ { provide: PASSPORT_AUTHENTICATOR, useExisting: IdentityAuthenticatorService },
72
+ ],
73
+ exports: [
74
+ CredentialGuard,
75
+ CREDENTIAL_AUTHENTICATOR,
76
+ AuthGuard,
77
+ ACCESS_TOKEN_VERIFIER,
78
+ IdentitySignInService,
79
+ PASSPORT_SESSION_STORE,
80
+ PASSPORT_ACCOUNTS,
81
+ PASSPORT_RATE_COUNTER,
82
+ PASSPORT_AUTHENTICATOR,
37
83
  ],
38
- exports: [CredentialGuard, CREDENTIAL_AUTHENTICATOR, AuthGuard, ACCESS_TOKEN_VERIFIER],
39
84
  }
40
85
  }
41
86
  }
@@ -5,3 +5,5 @@ export { IDENTITY_MESSAGES } from "./messages/identity.messages"
5
5
  export { PublicReason } from "./identity.contracts"
6
6
  export { ACCESS_TOKEN_VERIFIER, Public } from "./identity.decorators"
7
7
  export { IdentityModule } from "./identity.module"
8
+ export { identityEntities } from "./persistence/entities"
9
+ export { IdentitySignInService } from "./sign-in.service"
@@ -0,0 +1,24 @@
1
+ import type { MessageBundle } from "@modules/platform/i18n"
2
+
3
+ /** Display text of the identity codes, Vietnamese and English. */
4
+ export const IDENTITY_MESSAGES: MessageBundle = {
5
+ vi: {
6
+ "errors.IDENTITY_UNAUTHENTICATED":
7
+ "Bạn cần đăng nhập để thực hiện thao tác này.",
8
+ "errors.IDENTITY_REFRESH_INVALID": "Phiên đăng nhập không hợp lệ hoặc đã hết hạn.",
9
+ "errors.IDENTITY_REFRESH_REUSED": "Phiên đăng nhập đã kết thúc vì bị dùng lại. Hãy đăng nhập lại.",
10
+ "errors.IDENTITY_ACCOUNT_DISABLED": "Tài khoản này đã bị khóa.",
11
+ "errors.IDENTITY_TOO_MANY_ATTEMPTS": "Quá nhiều lần thử. Hãy thử lại sau.",
12
+ "errors.IDENTITY_INVALID_CREDENTIALS": "Email hoặc mật khẩu không đúng.",
13
+ "errors.IDENTITY_EMAIL_NOT_CONFIRMED": "Hãy xác nhận email trước khi đăng nhập.",
14
+ },
15
+ en: {
16
+ "errors.IDENTITY_UNAUTHENTICATED": "You need to sign in to do this.",
17
+ "errors.IDENTITY_REFRESH_INVALID": "The session is invalid or has expired.",
18
+ "errors.IDENTITY_REFRESH_REUSED": "The session ended because it was used again. Please sign in again.",
19
+ "errors.IDENTITY_ACCOUNT_DISABLED": "This account is disabled.",
20
+ "errors.IDENTITY_TOO_MANY_ATTEMPTS": "Too many attempts. Try again later.",
21
+ "errors.IDENTITY_INVALID_CREDENTIALS": "The email or password is wrong.",
22
+ "errors.IDENTITY_EMAIL_NOT_CONFIRMED": "Confirm your email before signing in.",
23
+ },
24
+ }
@@ -1,4 +1,4 @@
1
- export { createObjectStorage } from "./object-storage.client"
1
+ export { OBJECT_STORAGE } from "@starci/object-storage"
2
2
  export { parseObjectStorageConfig } from "./object-storage.config"
3
- export { InjectObjectStorage, OBJECT_STORAGE } from "./object-storage.decorators"
4
- export type { ObjectStorage, PresignedRequest, ObjectStorageOptions } from "./object-storage.options"
3
+ export { ObjectStorageModule } from "./object-storage.module"
4
+ export type { ObjectStorageOptions } from "./object-storage.options"
@@ -1,6 +1,8 @@
1
1
  import type { EnvSource } from "@modules/platform/config"
2
2
  import type { ObjectStorageOptions } from "./object-storage.options"
3
3
 
4
+ const PRESIGN_TTL_SECONDS = 300
5
+
4
6
  /** Reads the object storage configuration with no credential or endpoint defaults; secrets read `<KEY>_FILE` first. */
5
7
  export const parseObjectStorageConfig = (env: EnvSource): ObjectStorageOptions => ({
6
8
  endpoint: env.url("STORAGE_ENDPOINT"),
@@ -8,5 +10,5 @@ export const parseObjectStorageConfig = (env: EnvSource): ObjectStorageOptions =
8
10
  bucket: env.string("STORAGE_BUCKET"),
9
11
  accessKeyId: env.secret("STORAGE_ACCESS_KEY_ID"),
10
12
  secretAccessKey: env.secret("STORAGE_SECRET_ACCESS_KEY"),
11
- presignTtlSeconds: env.int("STORAGE_PRESIGN_TTL_SECONDS", 300),
13
+ defaultExpiresIn: env.int("STORAGE_PRESIGN_TTL_SECONDS", PRESIGN_TTL_SECONDS),
12
14
  })
@@ -1,9 +1,13 @@
1
+ import { OBJECT_STORAGE } from "@starci/object-storage"
2
+ import type { ObjectStorage } from "@starci/object-storage"
1
3
  import { injector } from "@modules/platform/composition"
2
4
  import type { TypedParameterDecorator } from "@modules/platform/composition"
3
- import type { ObjectStorage } from "./object-storage.options"
4
5
 
5
- /** Token of the object storage port. */
6
- export const OBJECT_STORAGE: unique symbol = Symbol("integrations.object-storage")
6
+ /** Token of the bucket of the app's objects. */
7
+ export const OBJECT_STORAGE_BUCKET: unique symbol = Symbol("integrations.object-storage.bucket")
7
8
 
8
- /** Injects the object storage port. Parameter type: ObjectStorage. */
9
+ /** Injects the object storage port of `@starci/object-storage`. Parameter type: ObjectStorage. */
9
10
  export const InjectObjectStorage = (): TypedParameterDecorator<ObjectStorage> => injector<ObjectStorage>(OBJECT_STORAGE)
11
+
12
+ /** Injects the bucket of the app's objects. Parameter type: string. */
13
+ export const InjectObjectStorageBucket = (): TypedParameterDecorator<string> => injector<string>(OBJECT_STORAGE_BUCKET)
@@ -0,0 +1,12 @@
1
+ import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
2
+ import type { ObjectStorageOptions } from "./object-storage.options"
3
+
4
+ const built = new ConfigurableModuleBuilder<ObjectStorageOptions>()
5
+ .setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
6
+ .build()
7
+ type Host = ConfigurableModuleHost<ObjectStorageOptions, "register", "create", { isGlobal: boolean }>
8
+
9
+ /** The configurable-module base of the object storage glue; `isGlobal` is decided by the app root. */
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
@@ -0,0 +1,29 @@
1
+ import { ObjectStorageModule as StorageModule } from "@starci/object-storage"
2
+ import type { ObjectStorageOptions as StorageSettings } from "@starci/object-storage"
3
+ import { Module } from "@nestjs/common"
4
+ import type { DynamicModule } from "@nestjs/common"
5
+ import { CLOCK } from "@modules/platform/clock"
6
+ import type { Clock } from "@modules/platform/clock"
7
+ import { OBJECT_STORAGE_BUCKET } from "./object-storage.decorators"
8
+ import { ConfigurableModuleClass, OPTIONS_TYPE } from "./object-storage.module-definition"
9
+
10
+ @Module({})
11
+ /** Composes `@starci/object-storage` with the app's typed settings and clock, and provides the bucket of the app's objects. */
12
+ export class ObjectStorageModule extends ConfigurableModuleClass {
13
+ /** Registers the capability once per app. */
14
+ static register(options: typeof OPTIONS_TYPE): DynamicModule {
15
+ const { bucket, isGlobal, ...settings } = options
16
+ const base = super.register(options)
17
+ const storage = StorageModule.registerAsync({
18
+ inject: [CLOCK],
19
+ useFactory: (clock: Clock): StorageSettings => ({ ...settings, clock }),
20
+ })
21
+ return {
22
+ ...base,
23
+ imports: [...(base.imports ?? []), storage],
24
+ providers: [...(base.providers ?? []), { provide: OBJECT_STORAGE_BUCKET, useValue: bucket }],
25
+ exports: [...(base.exports ?? []), storage, OBJECT_STORAGE_BUCKET],
26
+ global: isGlobal === true,
27
+ }
28
+ }
29
+ }
@@ -1,35 +1,10 @@
1
- import type { Secret } from "@modules/platform/config"
1
+ import type { ObjectStorageOptions as StorageSettings } from "@starci/object-storage"
2
2
 
3
- /** S3-compatible object storage (MinIO in lite) parsed once at API boot. */
4
- export interface ObjectStorageOptions {
5
- /** The S3 endpoint origin. */
6
- readonly endpoint: string
7
- /** The region the endpoint signs for. */
8
- readonly region: string
3
+ /**
4
+ * S3-compatible object storage (MinIO in lite) parsed once at API boot: the settings of `@starci/object-storage` (its clock is
5
+ * the app's `Clock`) and the one bucket of the app's objects. The key layout belongs to the domain that stores the object.
6
+ */
7
+ export interface ObjectStorageOptions extends Omit<StorageSettings, "clock"> {
9
8
  /** The bucket of the app's objects. */
10
9
  readonly bucket: string
11
- /** The access key id. */
12
- readonly accessKeyId: Secret
13
- /** The secret access key. */
14
- readonly secretAccessKey: Secret
15
- /** The lifetime of a presigned URL in seconds. */
16
- readonly presignTtlSeconds: number
17
- }
18
-
19
- /** One presigned request: the URL the browser calls and when it stops working. */
20
- export interface PresignedRequest {
21
- /** The presigned URL. */
22
- readonly url: string
23
- /** The expiry instant, ISO-8601. */
24
- readonly expiresAt: string
25
- }
26
-
27
- /** The object storage port: presigned PUT and GET of one key and a server-side delete. */
28
- export interface ObjectStorage {
29
- /** A presigned PUT of `key` with its content type fixed. */
30
- presignPut(key: string, contentType: string): Promise<PresignedRequest>
31
- /** A presigned GET of `key`. */
32
- presignGet(key: string): Promise<PresignedRequest>
33
- /** Deletes `key`. */
34
- remove(key: string): Promise<void>
35
10
  }
@@ -1,21 +1,30 @@
1
1
  import { DomainError } from "@modules/platform/errors"
2
2
  import type { ErrorKind } from "@modules/platform/errors"
3
3
 
4
- /** Codes of the passport integration. */
4
+ /** Codes of the passport integration: the wire codes @starci/auth names for the token, key and Google legs (the kit spec holds the parity). */
5
5
  export enum PassportErrorCode {
6
- /** The access token failed signature or required-claim verification. */
6
+ /** The access token failed signature, issuer, audience or expiry verification. */
7
7
  AccessTokenInvalid = "PASSPORT_ACCESS_TOKEN_INVALID",
8
- /** The email and password do not match an account. */
9
- CredentialsRefused = "PASSPORT_CREDENTIALS_REFUSED",
10
- /** The OAuth state or PKCE verifier of a callback does not match the request that started it. */
11
- OAuthStateInvalid = "PASSPORT_OAUTH_STATE_INVALID",
8
+ /** The signing key set is unusable. */
9
+ KeyInvalid = "PASSPORT_KEY_INVALID",
10
+ /** The request carried no email or password. */
11
+ MissingCredentials = "PASSPORT_MISSING_CREDENTIALS",
12
+ /** The Google round trip could not be proven to be ours (state, PKCE or cookie). */
13
+ GoogleStateInvalid = "PASSPORT_GOOGLE_STATE_INVALID",
14
+ /** Google refused the consent or answered no usable account. */
15
+ GoogleRejected = "PASSPORT_GOOGLE_REJECTED",
16
+ /** This deployment has no Google client. */
17
+ GoogleUnavailable = "PASSPORT_GOOGLE_UNAVAILABLE",
12
18
  }
13
19
 
14
- /** How each passport integration code travels. */
20
+ /** How each passport code travels. */
15
21
  export const PASSPORT_ERROR_KINDS: Record<PassportErrorCode, ErrorKind> = {
16
22
  [PassportErrorCode.AccessTokenInvalid]: "unauthenticated",
17
- [PassportErrorCode.CredentialsRefused]: "unauthenticated",
18
- [PassportErrorCode.OAuthStateInvalid]: "unauthenticated",
23
+ [PassportErrorCode.KeyInvalid]: "internal",
24
+ [PassportErrorCode.MissingCredentials]: "invalid",
25
+ [PassportErrorCode.GoogleStateInvalid]: "unauthenticated",
26
+ [PassportErrorCode.GoogleRejected]: "unauthenticated",
27
+ [PassportErrorCode.GoogleUnavailable]: "unavailable",
19
28
  }
20
29
 
21
30
  /** The one error class of the passport integration. */
@@ -1,12 +1,36 @@
1
+ export { AuthError, isPresentablePassword, normalizeEmail } from "@starci/auth/server"
2
+ export type {
3
+ AccountDirectory,
4
+ AccountStanding,
5
+ AuthAuthenticator,
6
+ AuthDoors,
7
+ AuthPasswords,
8
+ AuthSessions,
9
+ AuthSubject,
10
+ GoogleProfile,
11
+ HttpExchange,
12
+ NewSession,
13
+ RateCounter,
14
+ RefreshTokenRecord,
15
+ SessionStore,
16
+ SessionTransaction,
17
+ SignInBudget,
18
+ } from "@starci/auth/server"
19
+ export type { AccessGrant, GoogleRedirect, JwksDocument } from "@starci/auth/contracts"
1
20
  export { parsePassportConfig } from "./passport.config"
2
- export { OWN_TOKEN_VERIFIER, PASSPORT_OPTIONS, PASSWORD_ACCOUNTS } from "./passport.decorators"
3
- export { PASSPORT_ERROR_KINDS, PassportErrorCode } from "./errors/passport.error"
4
- export { createTokenKit, tokenKit } from "./passport.tokens"
5
- export type { TokenKit } from "./passport.tokens"
6
- export { hashPassword, verifyPassword } from "./passport.password"
7
- export { clearRefreshCookie, newRefreshToken, refreshTokenHash, setRefreshCookie } from "./passport.cookie"
8
- export { GoogleStrategy } from "./strategies/google.strategy"
9
- export type { GoogleIdentity } from "./strategies/google.strategy"
10
- export { LocalStrategy } from "./strategies/local.strategy"
11
- export type { FindPasswordAccount, PasswordAccount } from "./strategies/local.strategy"
12
- export type { OwnPrincipal, PassportOptions, VerifyAccessToken } from "./passport.options"
21
+ export {
22
+ OWN_TOKEN_VERIFIER,
23
+ PASSPORT_ACCOUNTS,
24
+ PASSPORT_AUTHENTICATOR,
25
+ PASSPORT_DOORS,
26
+ PASSPORT_OPTIONS,
27
+ PASSPORT_PASSWORDS,
28
+ PASSPORT_RATE_COUNTER,
29
+ PASSPORT_SESSION_STORE,
30
+ PASSPORT_SESSIONS,
31
+ PASSPORT_SIGN_IN_BUDGET,
32
+ } from "./passport.decorators"
33
+ export { PASSPORT_ERROR_KINDS, PassportError, PassportErrorCode } from "./errors/passport.error"
34
+ export { passportKit } from "./passport.kit"
35
+ export { passportModule } from "./passport.registration"
36
+ export type { PassportOptions, VerifyAccessToken } from "./passport.options"
@@ -5,13 +5,9 @@ import { parsePassportConfig } from "./passport.config"
5
5
  const CONFIG: unique symbol = Symbol("passport.config")
6
6
 
7
7
  const REQUIRED = {
8
- GOOGLE_CLIENT_ID: "client",
9
- GOOGLE_CLIENT_SECRET: "client-secret",
10
- GOOGLE_CALLBACK_URL: "https://api.example.com/auth/google/callback",
11
8
  AUTH_JWT_ISSUER: "https://api.example.com",
12
- AUTH_JWT_AUDIENCE: "app",
13
9
  AUTH_JWT_PRIVATE_KEY: "key",
14
- AUTH_JWT_KEY_ID: "kid-1",
10
+ AUTH_WEB_ORIGIN: "https://app.example.com/home",
15
11
  }
16
12
 
17
13
  const build = async () => {
@@ -21,14 +17,54 @@ const build = async () => {
21
17
 
22
18
  const parse = async (extra: Record<string, string>) => (await build()).parsePassportConfig(new EnvSource({ ...REQUIRED, ...extra }))
23
19
 
20
+ describe("the required keys", () => {
21
+ it("reads the issuer, the web origin and the signing key, and defaults the lifetimes and the budget", async () => {
22
+ const options = await parse({})
23
+ expect(options.issuer).toBe("https://api.example.com")
24
+ expect(options.returnOrigin).toBe("https://app.example.com")
25
+ expect(options.keys).toHaveLength(1)
26
+ expect(options.audience).toBeUndefined()
27
+ expect(options).toMatchObject({
28
+ accessTtlSeconds: 900,
29
+ refreshTtlSeconds: 2_592_000,
30
+ bcryptCost: 10,
31
+ google: null,
32
+ cookie: { name: "refresh_token" },
33
+ budget: { loginAttempts: 10, resetAttempts: 5, windowMs: 900_000 },
34
+ })
35
+ })
36
+
37
+ it("refuses a boot without the signing key", async () => {
38
+ const subject = await build()
39
+ const { AUTH_JWT_PRIVATE_KEY: _key, ...without } = REQUIRED
40
+ expect(() => subject.parsePassportConfig(new EnvSource(without))).toThrow()
41
+ })
42
+ })
43
+
44
+ describe("the Google client", () => {
45
+ it("is declared by its client id and then needs its secret and callback", async () => {
46
+ const options = await parse({ GOOGLE_CLIENT_ID: "client", GOOGLE_CLIENT_SECRET: "secret", GOOGLE_CALLBACK_URL: "https://api.example.com/auth/google/callback" })
47
+ expect(options.google).toMatchObject({ clientId: "client", callbackUrl: "https://api.example.com/auth/google/callback" })
48
+ await expect(parse({ GOOGLE_CLIENT_ID: "client" })).rejects.toBeDefined()
49
+ })
50
+ })
51
+
52
+ describe("the key set", () => {
53
+ it("keeps the previous public key beside the signing key while a rotation runs", async () => {
54
+ const options = await parse({ AUTH_JWT_KEY_ID: "new", AUTH_JWT_PREVIOUS_PUBLIC_KEY: "previous-pem", AUTH_JWT_PREVIOUS_KEY_ID: "old" })
55
+ expect(options.keys.map((key) => key.keyId)).toEqual(["new", "old"])
56
+ expect(options.keys[1]?.publicKey).toBe("previous-pem")
57
+ })
58
+ })
59
+
24
60
  describe("AUTH_COOKIE_DOMAIN", () => {
25
61
  it("leaves the refresh cookie host-only when the key is not declared", async () => {
26
- expect((await parse({})).refresh.cookieDomain).toBeUndefined()
62
+ expect((await parse({})).cookie.domain).toBeUndefined()
27
63
  })
28
64
 
29
65
  it("reads a bare registrable domain", async () => {
30
- expect((await parse({ AUTH_COOKIE_DOMAIN: "example.com" })).refresh.cookieDomain).toBe("example.com")
31
- expect((await parse({ AUTH_COOKIE_DOMAIN: "Sub.Example.CO.uk" })).refresh.cookieDomain).toBe("sub.example.co.uk")
66
+ expect((await parse({ AUTH_COOKIE_DOMAIN: "example.com" })).cookie.domain).toBe("example.com")
67
+ expect((await parse({ AUTH_COOKIE_DOMAIN: "Sub.Example.CO.uk" })).cookie.domain).toBe("sub.example.co.uk")
32
68
  })
33
69
 
34
70
  it.each(["https://example.com", "example.com:443", "example.com/path", ".example.com", "localhost", "127.0.0.1", "exa mple.com", "-a.example.com"])(