@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.
- package/CHANGELOG.md +20 -0
- package/README.md +10 -1
- package/db/postgres.mjs +9 -1
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +30 -9
- package/runtime/knowledge/hfs/fe-stack.yaml +7 -7
- package/runtime/knowledge/hfs/fork-images.yaml +7 -0
- package/runtime/knowledge/hfs/rules.yaml +55 -7
- package/runtime/knowledge/hfs/slots.yaml +75 -24
- package/runtime/modules/kernel/failure-codes.yaml +62 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +6 -0
- package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
- package/runtime/scripts/hfs/architecture/transport-owner.d.mts +1 -2
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -6
- package/runtime/scripts/hfs/manifest-shape.d.mts +1 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +3 -1
- package/runtime/scripts/hfs/rules/a15-findings.mjs +5 -0
- package/runtime/scripts/hfs/rules/stacks-local.d.mts +7 -0
- package/runtime/scripts/hfs/rules/stacks-local.mjs +71 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +6 -1
- package/runtime/scripts/hfs/rules/stacks-service-images.d.mts +12 -0
- package/runtime/scripts/hfs/rules/stacks-service-images.mjs +120 -0
- package/runtime/scripts/hfs/secret-dev-init.d.mts +11 -0
- package/runtime/scripts/hfs/secret-dev-init.mjs +79 -0
- package/runtime/scripts/hfs/secret.mjs +10 -7
- package/runtime/scripts/hfs/slots.mjs +1 -1
- package/runtime/scripts/hfs/stack-local-config.d.mts +101 -0
- package/runtime/scripts/hfs/stack-local-config.mjs +320 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.d.mts +2 -1
- package/runtime/scripts/lib/swarm-stack.mjs +2 -2
- package/scaffold/add-module.d.mts +16 -0
- package/scaffold/add-module.mjs +232 -0
- package/scaffold/add-queue-lite.mjs +49 -0
- package/scaffold/add.mjs +5 -0
- package/scaffold/app.mjs +7 -7
- package/scaffold/module-registry.d.mts +19 -0
- package/scaffold/module-registry.mjs +84 -0
- package/src/main.mjs +23 -2
- package/stack/cli.mjs +6 -1
- package/stack/context.d.mts +15 -0
- package/stack/context.mjs +1 -0
- package/stack/local-cli.d.mts +25 -0
- package/stack/local-cli.mjs +89 -0
- package/stack/local-config.d.mts +17 -0
- package/stack/local-config.mjs +37 -0
- package/stack/local-custody.d.mts +24 -0
- package/stack/local-custody.mjs +109 -0
- package/stack/local-doctor.d.mts +6 -0
- package/stack/local-doctor.mjs +49 -0
- package/stack/local-down.d.mts +8 -0
- package/stack/local-down.mjs +20 -0
- package/stack/local-host.d.mts +42 -0
- package/stack/local-host.mjs +163 -0
- package/stack/local-logs.d.mts +8 -0
- package/stack/local-logs.mjs +32 -0
- package/stack/local-ports.d.mts +7 -0
- package/stack/local-ports.mjs +39 -0
- package/stack/local-prepare.d.mts +22 -0
- package/stack/local-prepare.mjs +62 -0
- package/stack/local-run.d.mts +71 -0
- package/stack/local-run.mjs +200 -0
- package/stack/local-state.d.mts +35 -0
- package/stack/local-state.mjs +29 -0
- package/stack/local-status.d.mts +6 -0
- package/stack/local-status.mjs +36 -0
- package/stack/local-up.d.mts +11 -0
- package/stack/local-up.mjs +166 -0
- package/stack/local-verify.d.mts +6 -0
- package/stack/local-verify.mjs +21 -0
- package/sync/index.d.mts +13 -12
- package/sync/index.mjs +83 -28
- package/templates/app/module/jest.config.js.tpl +8 -0
- package/templates/app/module/src/index.ts.tpl +2 -0
- package/templates/app/module/src/manifest/declaration.spec.ts.tpl +11 -0
- package/templates/app/module/src/manifest/declaration.ts.tpl +13 -0
- package/templates/app/module/src/manifest/index.ts.tpl +1 -0
- package/templates/app/module/src/public/index.ts.tpl +2 -0
- package/templates/app/module/src/template/__name__.template.spec.ts.tpl +13 -0
- package/templates/app/module/src/template/__name__.template.ts.tpl +41 -0
- package/templates/app/module/src/template/index.ts.tpl +1 -0
- package/templates/app/module/src/workbench/components/__pascal__WorkbenchView.tsx.tpl +16 -0
- package/templates/app/module/src/workbench/index.ts.tpl +2 -0
- package/templates/app/package-scripts/package.json +1 -1
- package/templates/app/package-scripts-lite/package.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +76 -70
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +23 -13
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +25 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -26
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +1 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +2 -1
- package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +57 -14
- package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.query.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-begin.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-begin.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-complete.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-complete.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/login.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/login.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/logout.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/logout.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/refresh.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/refresh.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/session.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/session.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +13 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +16 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.options.ts +3 -0
- package/templates/be/auth-lite/src/features/api/auth/index.ts +2 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +13 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module.ts +9 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.options.ts +3 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +90 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/google-start.request.ts +17 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/login.request.ts +16 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +46 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +28 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +145 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +95 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts +38 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-user.entity.ts +54 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-attempt.entity.ts +18 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +34 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-session.entity.ts +30 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +17 -0
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +86 -0
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +45 -0
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +97 -0
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +81 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +78 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +87 -0
- package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +44 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +6 -5
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +3 -2
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +10 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +34 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +22 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +47 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +3 -1
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +8 -4
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module.ts +29 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +6 -31
- package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +18 -9
- package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +35 -11
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts +44 -8
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +40 -18
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +18 -6
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.spec.ts +73 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +24 -53
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.registration.ts +31 -0
- package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +11 -4
- package/templates/be/test-world/e2e/serves-once-migrated.e2e-spec.ts.tpl +9 -9
- package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +4 -4
- package/templates/fe/auth-lite/apps/web/src/modules/auth/client.ts +26 -0
- package/templates/fe/common/site-shell.tsx.partial +3 -3
- package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +33 -10
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useViewerId.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +15 -12
- package/templates/fe/skeleton/apps/__app__/src/modules/viewer-cache/index.ts +5 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +4 -7
- package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/index.tsx +2 -3
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +4 -7
- package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/index.tsx +1 -2
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +0 -16
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +0 -11
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +0 -7
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +0 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +0 -8
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +0 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -186
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +0 -22
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +0 -25
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.spec.ts +0 -104
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +0 -34
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.spec.ts +0 -28
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +0 -11
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +0 -88
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +0 -69
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +0 -41
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +0 -40
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +0 -63
- package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/classNames.ts +0 -4
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +0 -30
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +0 -50
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +0 -31
- 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
|
-
}
|
package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts
DELETED
|
@@ -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
|
-
}
|
package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts
DELETED
|
@@ -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
|
-
}
|
package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts
DELETED
|
@@ -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,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
|
-
}
|