@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
@@ -1,69 +0,0 @@
1
- import { createLocalJWKSet, exportJWK, importPKCS8, jwtVerify, SignJWT } from "jose"
2
- import type { JSONWebKeySet } from "jose"
3
- import { ok, refused } from "@modules/platform/primitives"
4
- import { PassportErrorCode } from "./errors/passport.error"
5
- import type { IssueAccessToken, OwnPrincipal, TokenOptions, VerifyAccessToken } from "./passport.options"
6
-
7
- const ALGORITHM = "ES256"
8
- const ROLE = "authenticated"
9
-
10
- /** The app's own token kit: the signer, the verifier over its own key set, and the public JWKS the API serves. */
11
- export interface TokenKit {
12
- /** Signs one access token. */
13
- readonly issue: IssueAccessToken
14
- /** Verifies one access token against the app's own keys. */
15
- readonly verify: VerifyAccessToken
16
- /** The public key set served at `/.well-known/jwks.json`. */
17
- readonly jwks: JSONWebKeySet
18
- }
19
-
20
- /** Builds the token kit from the ES256 private key; the verifier checks the app's own key set, never a remote one. */
21
- export const createTokenKit = async (options: TokenOptions): Promise<TokenKit> => {
22
- const privateKey = await importPKCS8(options.privateKey.reveal(), ALGORITHM, { extractable: true })
23
- const { d: _private, ...publicJwk } = await exportJWK(privateKey)
24
- const jwks: JSONWebKeySet = { keys: [{ ...publicJwk, kid: options.keyId, alg: ALGORITHM, use: "sig" }] }
25
- const keys = createLocalJWKSet(jwks)
26
- const issue: IssueAccessToken = (user) =>
27
- new SignJWT({ role: ROLE, ...(user.email === undefined ? {} : { email: user.email }) })
28
- .setProtectedHeader({ alg: ALGORITHM, kid: options.keyId })
29
- .setSubject(user.id)
30
- .setIssuer(options.issuer)
31
- .setAudience(options.audience)
32
- .setIssuedAt()
33
- .setExpirationTime(`${String(options.accessTtlSeconds)}s`)
34
- .sign(privateKey)
35
- const verify: VerifyAccessToken = async (token) => {
36
- try {
37
- const { payload } = await jwtVerify(token, keys, {
38
- algorithms: ["ES256"],
39
- audience: options.audience,
40
- issuer: options.issuer,
41
- })
42
- if (typeof payload.sub !== "string" || typeof payload.exp !== "number" || payload.role !== ROLE) {
43
- return refused(PassportErrorCode.AccessTokenInvalid)
44
- }
45
- const principal: OwnPrincipal = {
46
- id: payload.sub,
47
- role: ROLE,
48
- ...(typeof payload.email === "string" ? { email: payload.email } : {}),
49
- }
50
- return ok(principal)
51
- } catch (cause) {
52
- return refused(PassportErrorCode.AccessTokenInvalid, { cause: String(cause) })
53
- }
54
- }
55
- return { issue, verify, jwks }
56
- }
57
-
58
- /**
59
- * The token kit of a composition root that registers synchronously: each call awaits the one key import started here, so a
60
- * bad key fails the first verification instead of hiding behind a default.
61
- */
62
- export const tokenKit = (options: TokenOptions): { readonly issue: IssueAccessToken; readonly verify: VerifyAccessToken; readonly jwks: () => Promise<JSONWebKeySet> } => {
63
- const kit = createTokenKit(options)
64
- return {
65
- issue: async (user) => (await kit).issue(user),
66
- verify: async (token) => (await kit).verify(token),
67
- jwks: async () => (await kit).jwks,
68
- }
69
- }
@@ -1,41 +0,0 @@
1
- import { Injectable } from "@nestjs/common"
2
- import type { Type } from "@nestjs/common"
3
- import { PassportStrategy } from "@nestjs/passport"
4
- import { Strategy } from "passport-google-oauth20"
5
- import type { Profile } from "passport-google-oauth20"
6
- import { InjectPassportOptions } from "../passport.decorators"
7
- import type { PassportOptions } from "../passport.options"
8
- import { CookieStateStore } from "./oauth-state.store"
9
-
10
- /** The Google identity a callback proves: the provider subject and the verified email when Google states one. */
11
- export interface GoogleIdentity {
12
- /** Google's stable subject id (auth.identities.provider_id). */
13
- readonly providerId: string
14
- /** The verified email, when present. */
15
- readonly email?: string
16
- }
17
-
18
- /** The Passport strategy base of the google scheme, typed as the class the strategy extends. */
19
- const GoogleStrategyBase: Type<object> = PassportStrategy(Strategy, "google")
20
-
21
- @Injectable()
22
- /** Google OAuth 2.0 (authorization code with state and PKCE): it proves the Google identity and never signs the user in itself. */
23
- export class GoogleStrategy extends GoogleStrategyBase {
24
- constructor(@InjectPassportOptions() options: PassportOptions) {
25
- super({
26
- clientID: options.google.clientId,
27
- clientSecret: options.google.clientSecret.reveal(),
28
- callbackURL: options.google.callbackUrl,
29
- scope: ["openid", "email"],
30
- pkce: true,
31
- state: true,
32
- store: new CookieStateStore(new URL(options.google.callbackUrl).pathname, options.refresh.cookieDomain),
33
- })
34
- }
35
-
36
- /** The identity of the callback; the identity domain links it to a user and issues the session. */
37
- validate(_accessToken: string, _refreshToken: string, profile: Profile): GoogleIdentity {
38
- const email = profile.emails?.find((entry) => entry.verified)?.value
39
- return { providerId: profile.id, ...(email === undefined ? {} : { email }) }
40
- }
41
- }
@@ -1,40 +0,0 @@
1
- import { Injectable } from "@nestjs/common"
2
- import type { Type } from "@nestjs/common"
3
- import { PassportStrategy } from "@nestjs/passport"
4
- import { Strategy } from "passport-local"
5
- import { PassportError, PassportErrorCode } from "../errors/passport.error"
6
- import { InjectPasswordAccounts } from "../passport.decorators"
7
- import { verifyPassword } from "../passport.password"
8
-
9
- /** The account a password sign-in proves. */
10
- export interface PasswordAccount {
11
- /** The user id (auth.users.id). */
12
- readonly id: string
13
- /** The account email. */
14
- readonly email: string
15
- /** The stored bcrypt hash (auth.users.encrypted_password), null for an account without a password. */
16
- readonly passwordHash: string | null
17
- }
18
-
19
- /** The lookup the identity domain provides: the account of one email, or undefined. */
20
- export type FindPasswordAccount = (email: string) => Promise<PasswordAccount | undefined>
21
-
22
- /** The Passport strategy base of the local scheme, typed as the class the strategy extends. */
23
- const LocalStrategyBase: Type<object> = PassportStrategy(Strategy, "local")
24
-
25
- @Injectable()
26
- /** Email and password sign-in: bcrypt against the stored hash; a miss and a wrong password fail alike. */
27
- export class LocalStrategy extends LocalStrategyBase {
28
- constructor(@InjectPasswordAccounts() private readonly findAccount: FindPasswordAccount) {
29
- super({ usernameField: "email", passwordField: "password" })
30
- }
31
-
32
- /** The proven account; every refusal is the same error so an email's existence never leaks. */
33
- async validate(email: string, password: string): Promise<{ readonly id: string; readonly email: string }> {
34
- const account = await this.findAccount(email.trim().toLowerCase())
35
- if (!(await verifyPassword(password, account?.passwordHash ?? null)) || account === undefined) {
36
- throw new PassportError({ code: PassportErrorCode.CredentialsRefused })
37
- }
38
- return { id: account.id, email: account.email }
39
- }
40
- }
@@ -1,63 +0,0 @@
1
- import { randomBytes, timingSafeEqual } from "node:crypto"
2
- import type { Request, Response } from "express"
3
- import { domainAttribute } from "../passport.cookie"
4
-
5
- const COOKIE = "oauth_state"
6
- const MAX_AGE_MS = 600_000
7
-
8
- type Stored = { readonly state: string; readonly verifier: string }
9
- type StoreCallback = (error: Error | null, state?: string) => void
10
- type VerifyCallback = (error: Error | null, verifier?: string | false, state?: string) => void
11
-
12
- /** The response of the request a passport-oauth2 store is called with: passport keeps it on `req.res`. */
13
- const responseOf = (request: Request): Response | undefined => request.res
14
-
15
- /** The value of the state cookie in the request's Cookie header (no cookie middleware is installed). */
16
- const cookieOf = (request: Request): string | undefined =>
17
- request.headers.cookie
18
- ?.split(";")
19
- .map((part) => part.trim())
20
- .find((part) => part.startsWith(`${COOKIE}=`))
21
- ?.slice(COOKIE.length + 1)
22
-
23
- const readStored = (request: Request): Stored | undefined => {
24
- const raw = cookieOf(request)
25
- if (raw === undefined) return undefined
26
- const [state, verifier, extra] = raw.split(".")
27
- return state && verifier && extra === undefined ? { state, verifier } : undefined
28
- }
29
-
30
- const sameText = (left: string, right: string): boolean => {
31
- const a = Buffer.from(left)
32
- const b = Buffer.from(right)
33
- return a.length === b.length && timingSafeEqual(a, b)
34
- }
35
-
36
- /**
37
- * The OAuth state and PKCE verifier store of the Google strategy without a server session: both live in one short-lived
38
- * httpOnly, Secure, SameSite=Lax cookie scoped to the callback path, and the callback must present the same state.
39
- */
40
- export class CookieStateStore {
41
- constructor(
42
- private readonly callbackPath: string,
43
- private readonly cookieDomain?: string,
44
- ) {}
45
-
46
- /** Stores a fresh state with the PKCE verifier and hands the state to the authorization redirect. */
47
- store(request: Request, verifier: string, _state: unknown, _meta: unknown, callback: StoreCallback): void {
48
- const state = randomBytes(24).toString("base64url")
49
- responseOf(request)?.cookie(COOKIE, `${state}.${verifier}`, { httpOnly: true, secure: true, sameSite: "lax", path: this.callbackPath, maxAge: MAX_AGE_MS, ...domainAttribute(this.cookieDomain) })
50
- callback(null, state)
51
- }
52
-
53
- /** Answers the stored verifier only when the callback's state equals the stored one; the cookie is cleared either way. */
54
- verify(request: Request, provided: string, callback: VerifyCallback): void {
55
- const stored = readStored(request)
56
- responseOf(request)?.clearCookie(COOKIE, { httpOnly: true, secure: true, sameSite: "lax", path: this.callbackPath, ...domainAttribute(this.cookieDomain) })
57
- if (stored === undefined || !sameText(stored.state, provided)) {
58
- callback(null, false)
59
- return
60
- }
61
- callback(null, stored.verifier, stored.state)
62
- }
63
- }
@@ -1,4 +0,0 @@
1
- /** The shell's frame: a full-height column; colours come from Grammar's root. */
2
- export const siteShellClassNames = {
3
- frame: "flex min-h-screen flex-col",
4
- } as const
@@ -1,30 +0,0 @@
1
- import axios from "axios"
2
- import { apiUrl } from "@/modules/config"
3
-
4
- /** What one auth door answered: its status and its body, unread (the client narrows it). */
5
- export interface DoorAnswer {
6
- readonly status: number
7
- readonly body: unknown
8
- }
9
-
10
- /** The auth doors of the app's own back end; the refresh token rides in their httpOnly cookie, never in a body. */
11
- export type AuthDoor = "sign-in" | "sign-up" | "refresh" | "sign-out" | "password-reset" | "resend-verification"
12
-
13
- const TIMEOUT_MS = 10_000
14
-
15
- /**
16
- * The one auth door of the front end: a POST to `<api>/auth/<door>` with the cookies attached. A network failure answers
17
- * status 0, so the client maps it to "unavailable" instead of throwing.
18
- */
19
- export const callAuthDoor = async (door: AuthDoor, body?: Readonly<Record<string, string>>): Promise<DoorAnswer> => {
20
- try {
21
- const response = await axios.post<unknown>(`${apiUrl()}/auth/${door}`, body, {
22
- withCredentials: true,
23
- validateStatus: () => true,
24
- signal: AbortSignal.timeout(TIMEOUT_MS),
25
- })
26
- return { status: response.status, body: response.data }
27
- } catch {
28
- return { status: 0, body: undefined }
29
- }
30
- }
@@ -1,50 +0,0 @@
1
- import { callAuthDoor } from "./auth-door"
2
- import type { DoorAnswer } from "./auth-door"
3
- import type { AuthClient, AuthFailure, AuthSession } from "./contracts"
4
- import { heldSession, heldToken, hold, listen } from "./token-store"
5
-
6
- const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> => typeof value === "object" && value !== null
7
-
8
- /** The token and session an auth door issued, narrowed from its unread body; undefined when the body is not one. */
9
- const issuedOf = (body: unknown): { readonly token: string; readonly session: AuthSession } | undefined => {
10
- if (!isRecord(body) || typeof body.accessToken !== "string" || !isRecord(body.user) || typeof body.user.id !== "string") return undefined
11
- const email = typeof body.user.email === "string" ? body.user.email : undefined
12
- return { token: body.accessToken, session: { userId: body.user.id, ...(email === undefined ? {} : { email }) } }
13
- }
14
-
15
- /** The failure kind of a door's status: refused credentials, rejected input, or the API unavailable. */
16
- const kindOf = (status: number): string => {
17
- if (status === 400 || status === 401 || status === 403) return "refused"
18
- return status === 422 ? "invalid" : "unavailable"
19
- }
20
-
21
- const failureOf = (answer: DoorAnswer): AuthFailure => ({ code: `AUTH_${String(answer.status)}`, kind: kindOf(answer.status), message: "" })
22
-
23
- const ok = (answer: DoorAnswer): boolean => answer.status >= 200 && answer.status < 300
24
-
25
- /** Rotates the refresh cookie and holds the new access token; signs out when the API refuses the cookie. */
26
- const refresh = async (): Promise<string | undefined> => {
27
- const answer = await callAuthDoor("refresh")
28
- hold(ok(answer) ? issuedOf(answer.body) : undefined)
29
- return heldToken()
30
- }
31
-
32
- /** The lite session issuer's client: the app's own auth doors through `auth-door.ts`, the token in `token-store.ts`. */
33
- export const authClient: AuthClient = {
34
- session: async () => heldSession() ?? ((await refresh()) === undefined ? undefined : heldSession()),
35
- accessToken: async () => heldToken() ?? refresh(),
36
- refreshAccessToken: refresh,
37
- signInWithPassword: async (email, password) => {
38
- const answer = await callAuthDoor("sign-in", { email, password })
39
- const issued = ok(answer) ? issuedOf(answer.body) : undefined
40
- if (issued === undefined) return failureOf(answer)
41
- hold(issued)
42
- return undefined
43
- },
44
- signOut: async () => {
45
- const answer = await callAuthDoor("sign-out")
46
- hold(undefined)
47
- return ok(answer) ? undefined : failureOf(answer)
48
- },
49
- onChange: listen,
50
- }
@@ -1,31 +0,0 @@
1
- import type { AuthSession } from "./contracts"
2
-
3
- /** The signed-in reader and the access token, in memory only; the refresh token never reaches the browser's script. */
4
- interface Held {
5
- readonly token: string
6
- readonly session: AuthSession
7
- }
8
-
9
- let held: Held | undefined
10
- const listeners = new Set<(session: AuthSession | undefined) => void>()
11
-
12
- /** The held access token, or undefined when signed out. */
13
- export const heldToken = (): string | undefined => held?.token
14
-
15
- /** The held session, or undefined when signed out. */
16
- export const heldSession = (): AuthSession | undefined => held?.session
17
-
18
- /** Replaces the held token and session (undefined signs out) and tells every listener. */
19
- export const hold = (next: Held | undefined): void => {
20
- held = next
21
- for (const listener of listeners) listener(held?.session)
22
- }
23
-
24
- /** Subscribes to session changes; the listener hears the current session first. */
25
- export const listen = (listener: (session: AuthSession | undefined) => void): (() => void) => {
26
- listeners.add(listener)
27
- listener(held?.session)
28
- return () => {
29
- listeners.delete(listener)
30
- }
31
- }
@@ -1,4 +0,0 @@
1
- /** The shell's frame: a full-height column; colours come from Grammar's root. */
2
- export const siteShellClassNames = {
3
- frame: "flex min-h-screen flex-col",
4
- } as const