@pafi-dev/issuer 0.38.0 → 0.39.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/dist/auth-client/index.d.cts +2 -140
- package/dist/auth-client/index.d.ts +2 -140
- package/dist/auth-client/index.js +5 -176
- package/dist/auth-client/index.js.map +1 -1
- package/dist/chunk-7VEYSL2C.js +180 -0
- package/dist/chunk-7VEYSL2C.js.map +1 -0
- package/dist/direct-auth/index.cjs +657 -0
- package/dist/direct-auth/index.cjs.map +1 -0
- package/dist/direct-auth/index.d.cts +405 -0
- package/dist/direct-auth/index.d.ts +405 -0
- package/dist/direct-auth/index.js +458 -0
- package/dist/direct-auth/index.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/pafi-auth-client-DzHd_Ts_.d.cts +142 -0
- package/dist/pafi-auth-client-DzHd_Ts_.d.ts +142 -0
- package/package.json +44 -12
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/direct-auth/index.ts","../../src/direct-auth/pafi-direct-auth.module.ts","../../src/direct-auth/services/pafi-auth-client.provider.ts","../../src/auth-client/pafi-auth-client.ts","../../src/auth-client/sign-client-assertion.ts","../../src/auth-client/types.ts","../../src/direct-auth/pafi-direct-auth.module-options.ts","../../src/direct-auth/services/pafi-session-verifier.service.ts","../../src/direct-auth/services/pafi-direct-auth.service.ts","../../src/direct-auth/interfaces/user-store.interface.ts","../../src/direct-auth/interfaces/session-token-minter.interface.ts","../../src/direct-auth/pafi-direct-auth.controller.ts","../../src/direct-auth/pafi-direct-auth.dto.ts"],"sourcesContent":["/**\n * `@pafi-dev/issuer/direct-auth` — NestJS module for the PAFI gateway's\n * direct-auth surface (2026-06-30 architecture pivot).\n *\n * Replaces ~626 lines of per-issuer boilerplate (PafiAuthClientProvider,\n * PafiSessionVerifier, PafiDirectAuthService, controller, DTOs) with a\n * single `PafiDirectAuthModule.forRoot(options)` import + two issuer-\n * implemented adapter classes (`IUserStore`, `ISessionTokenMinter`).\n *\n * Quick start:\n *\n * ```ts\n * import { PafiDirectAuthModule } from '@pafi-dev/issuer/direct-auth';\n * import { Gg56UserStore } from './gg56-user-store';\n * import { Gg56SessionMinter } from './gg56-session-minter';\n *\n * @Module({\n * imports: [\n * PafiDirectAuthModule.forRoot({\n * gatewayUrl: process.env.PAFI_GATEWAY_URL!,\n * issuerId: process.env.PAFI_GATEWAY_ISSUER_ID!,\n * clientId: process.env.PAFI_GATEWAY_CLIENT_ID!,\n * clientPrivateJwk: JSON.parse(\n * process.env.PAFI_GATEWAY_CLIENT_PRIVATE_JWK_JSON!,\n * ),\n * userStore: Gg56UserStore,\n * sessionTokenMinter: Gg56SessionMinter,\n * }),\n * ],\n * providers: [Gg56UserStore, Gg56SessionMinter],\n * })\n * export class AuthModule {}\n * ```\n *\n * See `docs/wallet-auth-gateway/ISSUER_INTEGRATION_GUIDE.md` for the\n * full integration walkthrough.\n */\nexport { PafiDirectAuthModule } from \"./pafi-direct-auth.module\";\nexport {\n PAFI_DIRECT_AUTH_MODULE_OPTIONS,\n type PafiDirectAuthModuleOptions,\n} from \"./pafi-direct-auth.module-options\";\nexport { PafiDirectAuthService, type DirectAuthResult } from \"./services/pafi-direct-auth.service\";\nexport { PafiAuthClientProvider } from \"./services/pafi-auth-client.provider\";\nexport {\n PafiSessionVerifierService,\n type PafiSessionClaims,\n} from \"./services/pafi-session-verifier.service\";\nexport { PafiDirectAuthController } from \"./pafi-direct-auth.controller\";\nexport {\n USER_STORE,\n type IUserStore,\n} from \"./interfaces/user-store.interface\";\nexport {\n SESSION_TOKEN_MINTER,\n type ISessionTokenMinter,\n} from \"./interfaces/session-token-minter.interface\";\nexport {\n EmailStartRequestDto,\n EmailVerifyRequestDto,\n GoogleExchangeRequestDto,\n KakaoExchangeRequestDto,\n EmailStartResponseDto,\n PafiAuthSuccessDto,\n} from \"./pafi-direct-auth.dto\";\n","import { Module, type DynamicModule } from \"@nestjs/common\";\n\nimport { PafiAuthClientProvider } from \"./services/pafi-auth-client.provider\";\nimport { PafiSessionVerifierService } from \"./services/pafi-session-verifier.service\";\nimport { PafiDirectAuthService } from \"./services/pafi-direct-auth.service\";\nimport { PafiDirectAuthController } from \"./pafi-direct-auth.controller\";\nimport {\n PAFI_DIRECT_AUTH_MODULE_OPTIONS,\n type PafiDirectAuthModuleOptions,\n} from \"./pafi-direct-auth.module-options\";\nimport { USER_STORE } from \"./interfaces/user-store.interface\";\nimport { SESSION_TOKEN_MINTER } from \"./interfaces/session-token-minter.interface\";\n\n/**\n * NestJS module wiring the direct-auth surface (`/auth/v2/*`\n * controller, orchestrator service, gateway client, session\n * verifier).\n *\n * Issuer backends mount this once at the root module with their\n * gateway credentials + their `IUserStore` and `ISessionTokenMinter`\n * implementations:\n *\n * ```ts\n * @Module({\n * imports: [\n * ConfigModule.forRoot(),\n * TypeOrmModule.forFeature([UserEntity]),\n * PafiDirectAuthModule.forRoot({\n * gatewayUrl: process.env.PAFI_GATEWAY_URL!,\n * issuerId: process.env.PAFI_GATEWAY_ISSUER_ID!,\n * clientId: process.env.PAFI_GATEWAY_CLIENT_ID!,\n * clientPrivateJwk: JSON.parse(\n * process.env.PAFI_GATEWAY_CLIENT_PRIVATE_JWK_JSON!,\n * ) as JWK & { kid: string },\n * userStore: Gg56UserStore,\n * sessionTokenMinter: Gg56SessionMinter,\n * }),\n * ],\n * providers: [Gg56UserStore, Gg56SessionMinter],\n * })\n * export class AuthModule {}\n * ```\n *\n * The `IUserStore` + `ISessionTokenMinter` classes MUST be provided\n * separately in the host module's `providers:` array (NestJS needs to\n * see them to instantiate via DI). This module wires them to the\n * orchestrator via the `USER_STORE` + `SESSION_TOKEN_MINTER` injection\n * tokens.\n *\n * For ConfigService-based async setup, use `forRootAsync()` (TODO).\n */\n@Module({})\nexport class PafiDirectAuthModule {\n static forRoot(options: PafiDirectAuthModuleOptions): DynamicModule {\n return {\n module: PafiDirectAuthModule,\n controllers: [PafiDirectAuthController],\n providers: [\n {\n provide: PAFI_DIRECT_AUTH_MODULE_OPTIONS,\n useValue: options,\n },\n // Bridge user-supplied implementations into the injection\n // tokens the orchestrator uses. The class itself MUST also be\n // listed in the host's providers array (NestJS only instantiates\n // classes it sees in providers; useExisting requires that).\n {\n provide: USER_STORE,\n useExisting: options.userStore,\n },\n {\n provide: SESSION_TOKEN_MINTER,\n useExisting: options.sessionTokenMinter,\n },\n PafiAuthClientProvider,\n PafiSessionVerifierService,\n PafiDirectAuthService,\n ],\n exports: [\n PafiAuthClientProvider,\n PafiSessionVerifierService,\n PafiDirectAuthService,\n ],\n };\n }\n}\n","import { Injectable, Inject, type OnModuleInit } from \"@nestjs/common\";\nimport type { JWK } from \"jose\";\n\nimport { PafiAuthClient } from \"../../auth-client/pafi-auth-client\";\nimport {\n PAFI_DIRECT_AUTH_MODULE_OPTIONS,\n type PafiDirectAuthModuleOptions,\n} from \"../pafi-direct-auth.module-options\";\n\n/**\n * Thin DI wrapper around {@link PafiAuthClient}. Constructs a single\n * client at boot using the issuer's gateway credentials and exposes it\n * to services that need to call the direct-auth endpoints.\n *\n * One instance per process — the SDK client is stateless and the\n * client_assertion is signed on every call (no warm-up needed).\n *\n * Config comes from `PafiDirectAuthModule.forRoot(options)` (see\n * `pafi-direct-auth.module.ts`). The host app supplies `gatewayUrl`,\n * `issuerId`, `clientId`, and `clientPrivateJwk` either as literal\n * values OR as a factory that resolves them from NestJS ConfigService\n * (`forRootAsync` variant).\n */\n@Injectable()\nexport class PafiAuthClientProvider implements OnModuleInit {\n private _client!: PafiAuthClient;\n\n constructor(\n @Inject(PAFI_DIRECT_AUTH_MODULE_OPTIONS)\n private readonly options: PafiDirectAuthModuleOptions,\n ) {}\n\n onModuleInit(): void {\n const jwk = this.options.clientPrivateJwk as JWK;\n if (!jwk.kid) {\n throw new Error(\n \"PafiDirectAuthModule: clientPrivateJwk.kid is required — gateway uses kid for key lookup\",\n );\n }\n this._client = new PafiAuthClient({\n gatewayUrl: this.options.gatewayUrl,\n issuerId: this.options.issuerId,\n clientId: this.options.clientId,\n clientPrivateJwk: jwk as JWK & { kid: string },\n });\n }\n\n get client(): PafiAuthClient {\n return this._client;\n }\n}\n","import { randomUUID } from \"node:crypto\";\nimport { signClientAssertion } from \"./sign-client-assertion\";\nimport {\n PafiAuthError,\n type AuthSuccess,\n type EmailChallenge,\n type PafiAuthClientOptions,\n} from \"./types\";\n\ninterface GatewayErrorResponse {\n error?: string;\n error_description?: string;\n correlation_id?: string;\n}\n\ninterface GatewayAuthSuccessResponse {\n pafi_session_token: string;\n pafi_jwt: string;\n canonical_id: string;\n expires_at: number;\n is_first_login: boolean;\n verified_email?: string;\n}\n\ninterface GatewayChallengeResponse {\n challenge_id: string;\n expires_in: number;\n}\n\n/**\n * Issuer-side client for the PAFI gateway's direct-auth endpoints. Use\n * one instance per issuer backend — the constructor binds gateway URL +\n * issuer id + signing credentials, methods just route to specific\n * endpoints.\n *\n * Each method signs a fresh client_assertion (RFC 7523, 60s TTL) so\n * a leaked one is useless beyond the window + replay-protected by jti.\n *\n * The gateway endpoints invoked here are owned by PAFI — the issuer\n * never sees OTP codes, never holds OAuth client_secrets, never\n * verifies signatures itself. Gateway is the sole authority; this\n * client is purely a transport.\n *\n * Usage in a NestJS issuer backend:\n *\n * @Injectable()\n * export class PafiAuthClientProvider {\n * readonly client: PafiAuthClient;\n * constructor(config: ConfigService) {\n * this.client = new PafiAuthClient({\n * gatewayUrl: config.getOrThrow('PAFI_GATEWAY_URL'),\n * issuerId: config.getOrThrow('PAFI_GATEWAY_ISSUER_ID'),\n * clientId: config.getOrThrow('PAFI_GATEWAY_CLIENT_ID'),\n * clientPrivateJwk: JSON.parse(\n * config.getOrThrow('PAFI_GATEWAY_CLIENT_PRIVATE_JWK_JSON'),\n * ),\n * });\n * }\n * }\n */\nexport class PafiAuthClient {\n private readonly fetchImpl: typeof fetch;\n private readonly tokenExchangeAud: string;\n\n constructor(private readonly opts: PafiAuthClientOptions) {\n if (!opts.clientPrivateJwk.kid) {\n throw new Error(\n \"PafiAuthClient: clientPrivateJwk.kid is required (gateway uses kid to look up the verification key)\",\n );\n }\n this.fetchImpl = opts.fetchImpl ?? fetch;\n this.tokenExchangeAud = `${opts.gatewayUrl}/v1/token-exchange`;\n }\n\n // ───────────────────────────────────────────────────────────────\n // EMAIL OTP — 2-step\n // ───────────────────────────────────────────────────────────────\n\n /**\n * Step 1: ask the gateway to send the user an OTP. Returns the\n * `challengeId` to echo back on {@link verifyEmail}.\n */\n async startEmail(args: {\n email: string;\n correlationId?: string;\n }): Promise<EmailChallenge> {\n const res = await this.post<GatewayChallengeResponse>(\n \"/v1/auth/email/start\",\n {\n issuer_id: this.opts.issuerId,\n email: args.email,\n },\n args.correlationId,\n );\n return {\n challengeId: res.challenge_id,\n expiresInSec: res.expires_in,\n };\n }\n\n /**\n * Step 2: submit the OTP the user received. On success returns\n * {@link AuthSuccess} containing BOTH the long-lived\n * pafi_session_token (issuer verifies via gateway JWKS) AND the\n * short-lived pafi_jwt (issuer FE feeds to Privy).\n */\n async verifyEmail(args: {\n challengeId: string;\n otpCode: string;\n correlationId?: string;\n }): Promise<AuthSuccess> {\n const res = await this.post<GatewayAuthSuccessResponse>(\n \"/v1/auth/email/verify\",\n {\n challenge_id: args.challengeId,\n otp_code: args.otpCode,\n },\n args.correlationId,\n );\n return mapAuthSuccess(res);\n }\n\n // ───────────────────────────────────────────────────────────────\n // GOOGLE — 1-step exchange\n // ───────────────────────────────────────────────────────────────\n\n /**\n * Hand the gateway an id_token the issuer FE obtained from Google\n * Identity Services (using PAFI's shared client_id). Gateway verifies\n * signature + audience + `email_verified` before resolving identity.\n */\n async exchangeGoogle(args: {\n idToken: string;\n correlationId?: string;\n }): Promise<AuthSuccess> {\n const res = await this.post<GatewayAuthSuccessResponse>(\n \"/v1/auth/google/exchange\",\n {\n issuer_id: this.opts.issuerId,\n id_token: args.idToken,\n },\n args.correlationId,\n );\n return mapAuthSuccess(res);\n }\n\n // ───────────────────────────────────────────────────────────────\n // KAKAO — 1-step exchange (authorization code)\n // ───────────────────────────────────────────────────────────────\n\n /**\n * Hand the gateway the authorization code returned by Kakao's\n * redirect. Gateway exchanges with Kakao (server-to-server using\n * PAFI's client_secret), verifies id_token, resolves identity.\n *\n * `redirectUri` must match the URL the FE used when starting the\n * Kakao flow. Falls back to the gateway's KAKAO_REDIRECT_URI when\n * omitted — pass an explicit value for multi-environment FEs.\n */\n async exchangeKakao(args: {\n code: string;\n redirectUri?: string;\n correlationId?: string;\n }): Promise<AuthSuccess> {\n const res = await this.post<GatewayAuthSuccessResponse>(\n \"/v1/auth/kakao/exchange\",\n {\n issuer_id: this.opts.issuerId,\n code: args.code,\n ...(args.redirectUri ? { redirect_uri: args.redirectUri } : {}),\n },\n args.correlationId,\n );\n return mapAuthSuccess(res);\n }\n\n // ───────────────────────────────────────────────────────────────\n\n private async post<T>(\n path: string,\n body: unknown,\n correlationId: string | undefined,\n ): Promise<T> {\n const assertion = await signClientAssertion({\n gatewayUrl: this.opts.gatewayUrl,\n clientId: this.opts.clientId,\n privateJwk: this.opts.clientPrivateJwk,\n alg: this.opts.alg,\n });\n const finalCorrelationId = correlationId ?? `iss-${randomUUID()}`;\n const res = await this.fetchImpl(`${this.opts.gatewayUrl}${path}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${assertion}`,\n \"Content-Type\": \"application/json\",\n \"X-Correlation-Id\": finalCorrelationId,\n },\n body: JSON.stringify(body),\n });\n const text = await res.text();\n let parsed: unknown;\n try {\n parsed = text ? JSON.parse(text) : {};\n } catch {\n throw new PafiAuthError(\n `Non-JSON response from gateway (${path}): ${text.slice(0, 120)}`,\n res.status,\n \"non_json_response\",\n finalCorrelationId,\n );\n }\n if (!res.ok) {\n const err = parsed as GatewayErrorResponse;\n throw new PafiAuthError(\n err.error_description ??\n err.error ??\n `Gateway returned HTTP ${res.status}`,\n res.status,\n err.error ?? \"unknown_error\",\n err.correlation_id ?? finalCorrelationId,\n );\n }\n return parsed as T;\n }\n}\n\nfunction mapAuthSuccess(res: GatewayAuthSuccessResponse): AuthSuccess {\n return {\n pafiSessionToken: res.pafi_session_token,\n pafiJwt: res.pafi_jwt,\n canonicalId: res.canonical_id,\n expiresAt: res.expires_at,\n isFirstLogin: res.is_first_login,\n ...(res.verified_email ? { verifiedEmail: res.verified_email } : {}),\n };\n}\n","import { importJWK, SignJWT, type JWK } from \"jose\";\nimport { randomUUID } from \"node:crypto\";\n\n/**\n * Mint the RFC 7523 client_assertion JWT the gateway expects in the\n * `Authorization: Bearer …` header of every direct-auth call.\n *\n * Claims:\n * - iss / sub = clientId (RFC 7523 §3: same value for client auth)\n * - aud = `${gatewayUrl}/v1/token-exchange` (exact endpoint URL)\n * — NOTE the gateway also accepts this same audience\n * for the direct-auth endpoints because they live on\n * the same client-auth boundary. Single audience keeps\n * one client_assertion reusable across all gateway\n * endpoints for the duration of its short lifetime.\n * - iat / exp = 60-second window (replay-protected by jti)\n * - jti = random UUID\n *\n * 60-second lifetime is a deliberate trade-off: long enough to absorb\n * clock skew + slow networks, short enough that a stolen assertion is\n * usable only briefly. The gateway's per-jti replay cache means even\n * within that window an assertion is single-use.\n */\nexport async function signClientAssertion(args: {\n gatewayUrl: string;\n clientId: string;\n privateJwk: JWK & { kid: string };\n alg?: string;\n}): Promise<string> {\n const alg = args.alg ?? args.privateJwk.alg ?? \"ES256\";\n const key = await importJWK(args.privateJwk, alg);\n const now = Math.floor(Date.now() / 1000);\n return new SignJWT({})\n .setProtectedHeader({ alg, typ: \"JWT\", kid: args.privateJwk.kid })\n .setIssuer(args.clientId)\n .setSubject(args.clientId)\n .setAudience(`${args.gatewayUrl}/v1/token-exchange`)\n .setIssuedAt(now)\n .setExpirationTime(now + 60)\n .setJti(randomUUID())\n .sign(key);\n}\n","import type { JWK } from \"jose\";\n\n/**\n * Constructor params for {@link PafiAuthClient}. One instance per\n * issuer backend — wraps the issuer's gateway credentials (client_id +\n * private JWK) plus the static config (gateway URL, audience).\n */\nexport interface PafiAuthClientOptions {\n /** Base URL of the PAFI gateway (e.g. `https://id-dev.pacificfinance.org`). */\n gatewayUrl: string;\n /** Issuer identifier registered with the gateway (e.g. `gg56`). */\n issuerId: string;\n /**\n * Gateway client_id assigned at issuer onboarding. Also acts as the\n * `iss`/`sub` of the client_assertion JWT (RFC 7523 §3).\n */\n clientId: string;\n /**\n * Private JWK the issuer uses to sign client_assertion. MUST include\n * `kid` — gateway looks up the matching public JWK by kid.\n */\n clientPrivateJwk: JWK & { kid: string };\n /**\n * Optional fetch override — useful for tests / Node env without\n * global fetch (Node ≥ 18 has it built-in).\n */\n fetchImpl?: typeof fetch;\n /**\n * Optional algorithm override for the client assertion JWT. Default\n * `ES256`. Must match what the gateway expects for this client.\n */\n alg?: \"ES256\" | \"ES384\" | \"RS256\" | \"RS384\" | \"RS512\" | \"EdDSA\";\n}\n\nexport interface AuthSuccess {\n pafiSessionToken: string;\n pafiJwt: string;\n canonicalId: string;\n expiresAt: number;\n isFirstLogin: boolean;\n verifiedEmail?: string;\n}\n\nexport interface EmailChallenge {\n challengeId: string;\n expiresInSec: number;\n}\n\n/**\n * Thrown when the gateway rejects the call. `code` is the gateway's\n * structured `error` field (e.g. `invalid_otp`, `too_many_attempts`,\n * `expired`, `email_not_verified`) — issuers can branch on it to drive\n * UX (e.g. show \"Resend code\" button on `expired`).\n */\nexport class PafiAuthError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly code: string,\n public readonly correlationId?: string,\n ) {\n super(message);\n this.name = \"PafiAuthError\";\n }\n}\n","import type { JWK } from \"jose\";\nimport type { Type } from \"@nestjs/common\";\n\nimport type { IUserStore } from \"./interfaces/user-store.interface\";\nimport type { ISessionTokenMinter } from \"./interfaces/session-token-minter.interface\";\n\n/**\n * Options consumed by `PafiDirectAuthModule.forRoot(options)`. The\n * host (issuer backend) supplies:\n *\n * - Gateway credentials (URL, issuer_id, client_id, private JWK)\n * - Issuer-specific implementations of IUserStore + ISessionTokenMinter\n *\n * Typically read from `ConfigService` in the issuer backend. Use\n * `forRootAsync()` (planned) when config needs `inject: [ConfigService]`.\n */\nexport interface PafiDirectAuthModuleOptions {\n /** Base URL of the PAFI gateway (e.g. `https://id-dev.pacificfinance.org`). */\n gatewayUrl: string;\n /** Issuer identifier registered with the gateway (e.g. `gg56`). */\n issuerId: string;\n /**\n * Gateway client_id assigned at issuer onboarding. Also acts as the\n * `iss`/`sub` of the client_assertion JWT (RFC 7523 §3).\n */\n clientId: string;\n /**\n * Private JWK the issuer uses to sign client_assertion. MUST include\n * `kid` — gateway looks up the matching public JWK by kid.\n */\n clientPrivateJwk: JWK & { kid: string };\n /**\n * Issuer-implemented user-row upsert. SDK calls this on every\n * successful direct-auth flow to ensure the local users row exists\n * keyed by canonical_id. See `IUserStore` for contract details.\n */\n userStore: Type<IUserStore>;\n /**\n * Issuer-implemented session-token minter. SDK calls this once per\n * successful direct-auth flow to wrap the gateway-returned canonical\n * + verified email into the issuer's own session JWT. See\n * `ISessionTokenMinter` for contract details.\n */\n sessionTokenMinter: Type<ISessionTokenMinter>;\n}\n\n/** DI token for the resolved options. Use with `@Inject(PAFI_DIRECT_AUTH_MODULE_OPTIONS)`. */\nexport const PAFI_DIRECT_AUTH_MODULE_OPTIONS = Symbol(\n \"PAFI_DIRECT_AUTH_MODULE_OPTIONS\",\n);\n","import {\n Injectable,\n Inject,\n UnauthorizedException,\n} from \"@nestjs/common\";\nimport { createRemoteJWKSet, jwtVerify, type JWTPayload } from \"jose\";\n\nimport {\n PAFI_DIRECT_AUTH_MODULE_OPTIONS,\n type PafiDirectAuthModuleOptions,\n} from \"../pafi-direct-auth.module-options\";\n\nexport interface PafiSessionClaims {\n sub: string;\n scope: \"pafi-session\";\n /** Verified attribute the gateway used to derive canonical_id. */\n verifiedAttribute?: { type: string; valueHash?: string };\n /** Issuer that originated the auth (must equal our PAFI_GATEWAY_ISSUER_ID). */\n issuerId?: string;\n exp: number;\n iat: number;\n raw: JWTPayload;\n}\n\n/**\n * Verifies the long-lived `pafi_session_token` returned by the\n * gateway's direct-auth endpoints. Defense-in-depth: the gateway just\n * minted this token, so the chance of forgery is near zero — BUT\n * verifying it locally guarantees we never trust an attacker-supplied\n * \"fake gateway response\" that bypassed the network call (e.g. a\n * compromised inbound proxy injecting an arbitrary JSON body). One\n * audit line for a millisecond of crypto work.\n *\n * Uses the gateway's published JWKS (`/.well-known/jwks.json`) — same\n * key set Privy uses when verifying the short-lived `pafi_jwt`. The\n * remote JWKS is cached + auto-refreshed by jose.\n */\n@Injectable()\nexport class PafiSessionVerifierService {\n private readonly jwks: ReturnType<typeof createRemoteJWKSet>;\n private readonly expectedIssuer: string;\n\n constructor(\n @Inject(PAFI_DIRECT_AUTH_MODULE_OPTIONS)\n options: PafiDirectAuthModuleOptions,\n ) {\n this.jwks = createRemoteJWKSet(\n new URL(`${options.gatewayUrl}/.well-known/jwks.json`),\n );\n this.expectedIssuer = options.gatewayUrl;\n }\n\n async verify(token: string): Promise<PafiSessionClaims> {\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, this.jwks, {\n issuer: this.expectedIssuer,\n }));\n } catch (err) {\n throw new UnauthorizedException(\n `Invalid pafi_session_token: ${(err as Error).message}`,\n );\n }\n\n if (payload.scope !== \"pafi-session\") {\n throw new UnauthorizedException(\n `pafi_session_token has wrong scope: ${String(payload.scope)}`,\n );\n }\n if (typeof payload.sub !== \"string\") {\n throw new UnauthorizedException(\"pafi_session_token missing sub\");\n }\n if (typeof payload.exp !== \"number\" || typeof payload.iat !== \"number\") {\n throw new UnauthorizedException(\"pafi_session_token missing iat/exp\");\n }\n\n const verifiedAttribute = parseVerifiedAttribute(\n (payload as { verified_attribute?: unknown }).verified_attribute,\n );\n return {\n sub: payload.sub,\n scope: \"pafi-session\",\n verifiedAttribute,\n issuerId:\n typeof (payload as { issuer_id?: unknown }).issuer_id === \"string\"\n ? ((payload as { issuer_id?: string }).issuer_id as string)\n : undefined,\n exp: payload.exp,\n iat: payload.iat,\n raw: payload,\n };\n }\n}\n\nfunction parseVerifiedAttribute(\n raw: unknown,\n): PafiSessionClaims[\"verifiedAttribute\"] {\n if (!raw || typeof raw !== \"object\") return undefined;\n const obj = raw as { type?: unknown; value_hash?: unknown };\n if (typeof obj.type !== \"string\") return undefined;\n return {\n type: obj.type,\n valueHash:\n typeof obj.value_hash === \"string\" ? obj.value_hash : undefined,\n };\n}\n","import { Injectable, Inject, Logger } from \"@nestjs/common\";\n\nimport { PafiAuthClientProvider } from \"./pafi-auth-client.provider\";\nimport { PafiSessionVerifierService } from \"./pafi-session-verifier.service\";\nimport {\n USER_STORE,\n type IUserStore,\n} from \"../interfaces/user-store.interface\";\nimport {\n SESSION_TOKEN_MINTER,\n type ISessionTokenMinter,\n} from \"../interfaces/session-token-minter.interface\";\n\nexport interface DirectAuthResult {\n sessionToken: string;\n sessionExpiresAt: string;\n pafiJwt: string;\n pafiSessionToken: string;\n canonicalId: string;\n isFirstLogin: boolean;\n verifiedEmail?: string;\n}\n\n/**\n * Orchestrates the gateway-owned auth flows for an issuer backend. The\n * gateway is the sole verifier (it ran the OTP check / verified the\n * Google id_token / exchanged the Kakao code); this service is a thin\n * adapter that:\n *\n * 1. Forwards the call via {@link PafiAuthClient} (RFC 7523\n * client_assertion signed by the issuer's private JWK)\n * 2. Verifies the returned `pafi_session_token` against the gateway's\n * JWKS (defense in depth — see {@link PafiSessionVerifierService})\n * 3. Upserts the local user row via {@link IUserStore} (issuer\n * implements; SDK doesn't know the DB schema)\n * 4. Mints an issuer-native session token via\n * {@link ISessionTokenMinter} (issuer implements; SDK doesn't see\n * the issuer's JWT_SECRET)\n *\n * The returned `sessionToken.sub` is the canonical_id — the same value\n * the gateway uses for the PAFI JWT's `sub`. This means downstream\n * services that need a stable user id can use it without caring which\n * auth method produced it.\n *\n * Extracted from per-issuer boilerplate code in 2026-06-30 SDK refactor.\n * Previously copy-pasted (~260 lines per issuer); now ~60 lines per\n * issuer (UserStore + SessionMinter + module wiring).\n */\n@Injectable()\nexport class PafiDirectAuthService {\n private readonly logger = new Logger(PafiDirectAuthService.name);\n\n constructor(\n private readonly clientProvider: PafiAuthClientProvider,\n private readonly sessionVerifier: PafiSessionVerifierService,\n @Inject(USER_STORE) private readonly userStore: IUserStore,\n @Inject(SESSION_TOKEN_MINTER)\n private readonly sessionTokenMinter: ISessionTokenMinter,\n ) {}\n\n // ── Email OTP ────────────────────────────────────────────────────\n\n async startEmail(args: {\n email: string;\n correlationId?: string;\n }): Promise<{ challengeId: string; expiresInSec: number }> {\n return this.clientProvider.client.startEmail({\n email: args.email,\n correlationId: args.correlationId,\n });\n }\n\n async verifyEmail(args: {\n challengeId: string;\n otpCode: string;\n correlationId?: string;\n }): Promise<DirectAuthResult> {\n const success = await this.clientProvider.client.verifyEmail({\n challengeId: args.challengeId,\n otpCode: args.otpCode,\n correlationId: args.correlationId,\n });\n await this.sessionVerifier.verify(success.pafiSessionToken);\n return this.finalize(success);\n }\n\n // ── Google ───────────────────────────────────────────────────────\n\n async exchangeGoogle(args: {\n idToken: string;\n correlationId?: string;\n }): Promise<DirectAuthResult> {\n const success = await this.clientProvider.client.exchangeGoogle({\n idToken: args.idToken,\n correlationId: args.correlationId,\n });\n await this.sessionVerifier.verify(success.pafiSessionToken);\n return this.finalize(success);\n }\n\n // ── Kakao ────────────────────────────────────────────────────────\n\n async exchangeKakao(args: {\n code: string;\n redirectUri?: string;\n correlationId?: string;\n }): Promise<DirectAuthResult> {\n const success = await this.clientProvider.client.exchangeKakao({\n code: args.code,\n redirectUri: args.redirectUri,\n correlationId: args.correlationId,\n });\n await this.sessionVerifier.verify(success.pafiSessionToken);\n return this.finalize(success);\n }\n\n // ── Internal: upsert user + mint issuer session token ───────────\n\n private async finalize(success: {\n canonicalId: string;\n pafiJwt: string;\n pafiSessionToken: string;\n isFirstLogin: boolean;\n verifiedEmail?: string;\n }): Promise<DirectAuthResult> {\n await this.userStore.upsertByCanonicalAndEmail({\n canonicalId: success.canonicalId,\n verifiedEmail: success.verifiedEmail,\n });\n\n const { token, expiresAt } = await this.sessionTokenMinter.mint({\n canonicalId: success.canonicalId,\n verifiedEmail: success.verifiedEmail,\n });\n\n return {\n sessionToken: token,\n sessionExpiresAt: expiresAt,\n pafiJwt: success.pafiJwt,\n pafiSessionToken: success.pafiSessionToken,\n canonicalId: success.canonicalId,\n isFirstLogin: success.isFirstLogin,\n ...(success.verifiedEmail ? { verifiedEmail: success.verifiedEmail } : {}),\n };\n }\n}\n","/**\n * Issuer-side abstraction for \"make sure the local user row exists,\n * keyed by the canonical_pafi_user_id the gateway just resolved.\"\n *\n * The SDK can't own this because each issuer has a different\n * `users` table schema (gg56 has `wallet_address`, lotteria doesn't,\n * future issuers might add `phone_number` / `kyc_status` / etc.).\n * Each consuming issuer backend implements `IUserStore` once and\n * passes the class to `PafiDirectAuthModule.forRoot({ userStore })`.\n *\n * Idempotency contract: callers invoke this on every successful\n * direct-auth flow (email OTP / Google / Kakao). Implementation MUST\n * be idempotent — re-calling with the same args is a no-op, NOT an\n * error.\n *\n * Lookup precedence (recommended): canonical_id first, fall back to\n * email. Same canonical across issuers = cross-issuer wallet merge.\n *\n * Race-safety: parallel requests for the same brand-new canonical\n * should be tolerated. The SDK assumes the underlying DB enforces a\n * unique constraint on canonical_id; on conflict, treat as \"row\n * already exists\" rather than re-throwing.\n *\n * @example\n * ```ts\n * @Injectable()\n * class Gg56UserStore implements IUserStore {\n * constructor(\n * @InjectRepository(UserEntity)\n * private readonly users: Repository<UserEntity>,\n * ) {}\n *\n * async upsertByCanonicalAndEmail(args: {\n * canonicalId: string;\n * verifiedEmail?: string;\n * }): Promise<void> {\n * const byCanonical = await this.users.findOne({\n * where: { canonicalId: args.canonicalId },\n * });\n * if (byCanonical) return;\n * // ... fall back to email, create row, race-handle, etc.\n * }\n * }\n * ```\n */\nexport interface IUserStore {\n upsertByCanonicalAndEmail(args: {\n /** canonical_pafi_user_id the gateway just minted. Always present. */\n canonicalId: string;\n /**\n * Verified email when the auth method exposed one (email OTP +\n * Google always; Kakao only if user shared email at consent).\n * Issuer may use to display profile, NOT for primary user lookup\n * (canonicalId is the stable id).\n */\n verifiedEmail?: string;\n }): Promise<void>;\n}\n\n/** DI token for IUserStore. Use with `@Inject(USER_STORE)`. */\nexport const USER_STORE = Symbol(\"USER_STORE\");\n","/**\n * Issuer-side abstraction for \"mint an issuer-native session token\n * the FE will Bearer-auth with for all subsequent /api calls.\"\n *\n * The SDK can't own this because:\n * - Each issuer signs with their own `JWT_SECRET` (HS256) OR with\n * their own RSA/ECDSA key — SDK shouldn't see secrets\n * - Token shape may differ (sub format, scope claims, extra claims\n * like role/tier specific to the issuer's app)\n * - Lifetime may differ (24h default, but some issuers want shorter)\n * - Existing JwtGuard / SessionTokenGuard in the issuer backend\n * expects a specific shape — SDK can't predict it\n *\n * The SDK orchestrator calls `mint()` once per successful direct-auth\n * flow, BEFORE returning the bundled response to the issuer's HTTP\n * controller. The minter is responsible for:\n * 1. Producing a token the issuer's existing guards accept\n * 2. Embedding `authAttribute: {type, value}` if the issuer's\n * /wallet/exchange-pafi-jwt refresh path needs it (most do — see\n * ISSUER_INTEGRATION_GUIDE.md §9 refresh path)\n * 3. Computing absolute expiry timestamp the FE can show\n *\n * @example\n * ```ts\n * @Injectable()\n * class Gg56SessionMinter implements ISessionTokenMinter {\n * private readonly secret: Uint8Array;\n * constructor(config: ConfigService) {\n * this.secret = new TextEncoder().encode(\n * config.getOrThrow('JWT_SECRET'),\n * );\n * }\n *\n * async mint(args: {\n * canonicalId: string;\n * verifiedEmail?: string;\n * }): Promise<{ token: string; expiresAt: string }> {\n * const now = Math.floor(Date.now() / 1000);\n * const exp = now + 86400;\n * const token = await new SignJWT({\n * userKey: args.canonicalId,\n * loginType: 'pafi-direct',\n * scope: 'pafi-session',\n * ...(args.verifiedEmail\n * ? { authAttribute: { type: 'email', value: args.verifiedEmail } }\n * : {}),\n * })\n * .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })\n * .setSubject(args.canonicalId)\n * .setIssuedAt(now)\n * .setExpirationTime(exp)\n * .sign(this.secret);\n * return { token, expiresAt: new Date(exp * 1000).toISOString() };\n * }\n * }\n * ```\n */\nexport interface ISessionTokenMinter {\n mint(args: {\n /** canonical_pafi_user_id, use as `sub` so downstream identifies user uniformly. */\n canonicalId: string;\n /**\n * Verified email when available. Implementations SHOULD embed this\n * as `authAttribute: { type: 'email', value }` in the JWT body so\n * the legacy /wallet/exchange-pafi-jwt refresh path keeps deriving\n * the same canonical_id from the same attribute across refreshes.\n */\n verifiedEmail?: string;\n }): Promise<{\n /** The signed session token (any JWT shape — issuer guards must accept). */\n token: string;\n /** ISO 8601 absolute expiry timestamp. Returned to FE so it can show \"expires at X\". */\n expiresAt: string;\n }>;\n}\n\n/** DI token for ISessionTokenMinter. Use with `@Inject(SESSION_TOKEN_MINTER)`. */\nexport const SESSION_TOKEN_MINTER = Symbol(\"SESSION_TOKEN_MINTER\");\n","import {\n Body,\n Controller,\n HttpCode,\n HttpStatus,\n Post,\n} from \"@nestjs/common\";\nimport { ApiOkResponse, ApiOperation, ApiTags } from \"@nestjs/swagger\";\n\nimport {\n EmailStartRequestDto,\n EmailStartResponseDto,\n EmailVerifyRequestDto,\n GoogleExchangeRequestDto,\n KakaoExchangeRequestDto,\n PafiAuthSuccessDto,\n} from \"./pafi-direct-auth.dto\";\nimport { PafiDirectAuthService } from \"./services/pafi-direct-auth.service\";\n\n/**\n * `/auth/v2/*` — gateway-owned auth endpoints (2026-06-30 direct-auth\n * surface, shipped by `@pafi-dev/issuer/direct-auth`).\n *\n * Issuer backend no longer verifies OTP codes, OAuth id_tokens, or\n * holds OAuth client_secrets. All of that lives at the PAFI gateway.\n * These endpoints are thin proxies that forward user input via\n * {@link PafiDirectAuthService}, then upsert the local user row (via\n * the issuer-provided {@link IUserStore}) + mint an issuer-native\n * session token (via the issuer-provided {@link ISessionTokenMinter})\n * wrapping the gateway-assigned canonical_id.\n *\n * Mounted automatically by `PafiDirectAuthModule.forRoot(options)`.\n * Issuer apps that want to layer extra auth-error handling (e.g. a\n * custom `AuthErrorFilter`) can wrap the controller's routes via a\n * NestJS global filter — no per-route `@UseFilters` needed here.\n */\n@ApiTags(\"pafi-auth-v2\")\n@Controller(\"auth/v2\")\nexport class PafiDirectAuthController {\n constructor(private readonly directAuth: PafiDirectAuthService) {}\n\n @Post(\"email/start\")\n @HttpCode(HttpStatus.OK)\n @ApiOperation({\n summary: \"Step 1: ask gateway to send an OTP to the user email.\",\n description:\n \"Gateway generates the OTP, sends it via its configured email provider, and returns an opaque challenge_id. The FE echoes that challenge_id back on step 2 along with the code the user typed.\",\n })\n @ApiOkResponse({ type: EmailStartResponseDto })\n async startEmail(\n @Body() body: EmailStartRequestDto,\n ): Promise<EmailStartResponseDto> {\n const res = await this.directAuth.startEmail({ email: body.email });\n return {\n challengeId: res.challengeId,\n expiresInSec: res.expiresInSec,\n };\n }\n\n @Post(\"email/verify\")\n @HttpCode(HttpStatus.OK)\n @ApiOperation({\n summary: \"Step 2: submit the OTP to complete email sign-in.\",\n description:\n \"Gateway verifies the OTP, derives canonical_id from the verified email, and mints both a pafi_session_token (24h, gateway-signed) and pafi_jwt (60s, for Privy.loginWithCustomAuth). Issuer wraps these in a session token of its own (sub = canonical_id) so existing guards keep working.\",\n })\n @ApiOkResponse({ type: PafiAuthSuccessDto })\n async verifyEmail(\n @Body() body: EmailVerifyRequestDto,\n ): Promise<PafiAuthSuccessDto> {\n return this.directAuth.verifyEmail({\n challengeId: body.challengeId,\n otpCode: body.otpCode,\n });\n }\n\n @Post(\"google/exchange\")\n @HttpCode(HttpStatus.OK)\n @ApiOperation({\n summary: \"Sign in with Google.\",\n description:\n \"Hand the gateway a Google-issued id_token (FE obtains via Google Identity Services using PAFI's shared client_id). Gateway verifies signature + email_verified, derives canonical_id from the email, returns the same token bundle as /email/verify.\",\n })\n @ApiOkResponse({ type: PafiAuthSuccessDto })\n async exchangeGoogle(\n @Body() body: GoogleExchangeRequestDto,\n ): Promise<PafiAuthSuccessDto> {\n return this.directAuth.exchangeGoogle({ idToken: body.idToken });\n }\n\n @Post(\"kakao/exchange\")\n @HttpCode(HttpStatus.OK)\n @ApiOperation({\n summary: \"Sign in with Kakao.\",\n description:\n \"Hand the gateway the authorization code Kakao redirected back to the FE. Gateway exchanges with Kakao server-to-server (using PAFI-held client_secret), verifies the id_token, and returns the same token bundle as /email/verify. canonical_id derives from email when present, else from the Kakao sub.\",\n })\n @ApiOkResponse({ type: PafiAuthSuccessDto })\n async exchangeKakao(\n @Body() body: KakaoExchangeRequestDto,\n ): Promise<PafiAuthSuccessDto> {\n return this.directAuth.exchangeKakao({\n code: body.code,\n redirectUri: body.redirectUri,\n });\n }\n}\n","import { ApiProperty } from \"@nestjs/swagger\";\nimport {\n IsEmail,\n IsNotEmpty,\n IsOptional,\n IsString,\n IsUrl,\n Length,\n MaxLength,\n} from \"class-validator\";\n\n// ── Requests ────────────────────────────────────────────────────────\n\nexport class EmailStartRequestDto {\n @ApiProperty({ example: \"user1@example.com\" })\n @IsEmail()\n @MaxLength(320)\n email!: string;\n}\n\nexport class EmailVerifyRequestDto {\n @ApiProperty({\n description:\n \"Challenge id returned by POST /auth/v2/email/start. Opaque to the FE; echo verbatim.\",\n })\n @IsString()\n @IsNotEmpty()\n @MaxLength(128)\n challengeId!: string;\n\n @ApiProperty({ example: \"123456\" })\n @IsString()\n @Length(4, 10)\n otpCode!: string;\n}\n\nexport class GoogleExchangeRequestDto {\n @ApiProperty({\n description:\n \"Google-issued ID token (JWS). Obtain on FE via Google Identity Services using PAFI's Google OAuth client_id.\",\n })\n @IsString()\n @IsNotEmpty()\n @MaxLength(8192)\n idToken!: string;\n}\n\nexport class KakaoExchangeRequestDto {\n @ApiProperty({\n description:\n \"Authorization code returned by Kakao to the FE redirect URL.\",\n })\n @IsString()\n @IsNotEmpty()\n @MaxLength(2048)\n code!: string;\n\n @ApiProperty({\n description:\n \"Redirect URI the FE used when initiating the Kakao flow. Optional — gateway falls back to its own KAKAO_REDIRECT_URI env.\",\n required: false,\n })\n @IsOptional()\n @IsUrl({ require_tld: false, require_protocol: true })\n @MaxLength(2048)\n redirectUri?: string;\n}\n\n// ── Responses ───────────────────────────────────────────────────────\n\nexport class EmailStartResponseDto {\n @ApiProperty()\n challengeId!: string;\n\n @ApiProperty({ description: \"Seconds until the challenge expires.\" })\n expiresInSec!: number;\n}\n\nexport class PafiAuthSuccessDto {\n @ApiProperty({\n description:\n \"Issuer-native session token (typically HS256, minted by ISessionTokenMinter) — Bearer-auth for subsequent issuer API calls.\",\n })\n sessionToken!: string;\n\n @ApiProperty({ description: \"Issuer session token expiration (ISO 8601).\" })\n sessionExpiresAt!: string;\n\n @ApiProperty({\n description:\n \"Short-lived PAFI JWT (60s) — FE feeds verbatim to Privy.loginWithCustomAuth() to provision the embedded wallet.\",\n })\n pafiJwt!: string;\n\n @ApiProperty({\n description:\n \"Long-lived PAFI session token (24h) — opaque to FE; keep alongside sessionToken if you ever need to call the gateway directly.\",\n })\n pafiSessionToken!: string;\n\n @ApiProperty({ description: \"canonical_pafi_user_id assigned by the gateway.\" })\n canonicalId!: string;\n\n @ApiProperty({\n description: \"True the first time the user appears at the gateway.\",\n })\n isFirstLogin!: boolean;\n\n @ApiProperty({\n description:\n \"Verified email (when the auth method exposed one — email OTP and Google always; Kakao only if the user shared their email).\",\n required: false,\n })\n verifiedEmail?: string;\n}\n"],"mappings":"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","names":["import_common","import_node_crypto","import_common","import_jose","import_common","import_common","import_swagger"]}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { Type, DynamicModule, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { JWK, JWTPayload } from 'jose';
|
|
3
|
+
import { P as PafiAuthClient } from '../pafi-auth-client-DzHd_Ts_.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Issuer-side abstraction for "make sure the local user row exists,
|
|
7
|
+
* keyed by the canonical_pafi_user_id the gateway just resolved."
|
|
8
|
+
*
|
|
9
|
+
* The SDK can't own this because each issuer has a different
|
|
10
|
+
* `users` table schema (gg56 has `wallet_address`, lotteria doesn't,
|
|
11
|
+
* future issuers might add `phone_number` / `kyc_status` / etc.).
|
|
12
|
+
* Each consuming issuer backend implements `IUserStore` once and
|
|
13
|
+
* passes the class to `PafiDirectAuthModule.forRoot({ userStore })`.
|
|
14
|
+
*
|
|
15
|
+
* Idempotency contract: callers invoke this on every successful
|
|
16
|
+
* direct-auth flow (email OTP / Google / Kakao). Implementation MUST
|
|
17
|
+
* be idempotent — re-calling with the same args is a no-op, NOT an
|
|
18
|
+
* error.
|
|
19
|
+
*
|
|
20
|
+
* Lookup precedence (recommended): canonical_id first, fall back to
|
|
21
|
+
* email. Same canonical across issuers = cross-issuer wallet merge.
|
|
22
|
+
*
|
|
23
|
+
* Race-safety: parallel requests for the same brand-new canonical
|
|
24
|
+
* should be tolerated. The SDK assumes the underlying DB enforces a
|
|
25
|
+
* unique constraint on canonical_id; on conflict, treat as "row
|
|
26
|
+
* already exists" rather than re-throwing.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* @Injectable()
|
|
31
|
+
* class Gg56UserStore implements IUserStore {
|
|
32
|
+
* constructor(
|
|
33
|
+
* @InjectRepository(UserEntity)
|
|
34
|
+
* private readonly users: Repository<UserEntity>,
|
|
35
|
+
* ) {}
|
|
36
|
+
*
|
|
37
|
+
* async upsertByCanonicalAndEmail(args: {
|
|
38
|
+
* canonicalId: string;
|
|
39
|
+
* verifiedEmail?: string;
|
|
40
|
+
* }): Promise<void> {
|
|
41
|
+
* const byCanonical = await this.users.findOne({
|
|
42
|
+
* where: { canonicalId: args.canonicalId },
|
|
43
|
+
* });
|
|
44
|
+
* if (byCanonical) return;
|
|
45
|
+
* // ... fall back to email, create row, race-handle, etc.
|
|
46
|
+
* }
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
interface IUserStore {
|
|
51
|
+
upsertByCanonicalAndEmail(args: {
|
|
52
|
+
/** canonical_pafi_user_id the gateway just minted. Always present. */
|
|
53
|
+
canonicalId: string;
|
|
54
|
+
/**
|
|
55
|
+
* Verified email when the auth method exposed one (email OTP +
|
|
56
|
+
* Google always; Kakao only if user shared email at consent).
|
|
57
|
+
* Issuer may use to display profile, NOT for primary user lookup
|
|
58
|
+
* (canonicalId is the stable id).
|
|
59
|
+
*/
|
|
60
|
+
verifiedEmail?: string;
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
/** DI token for IUserStore. Use with `@Inject(USER_STORE)`. */
|
|
64
|
+
declare const USER_STORE: unique symbol;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Issuer-side abstraction for "mint an issuer-native session token
|
|
68
|
+
* the FE will Bearer-auth with for all subsequent /api calls."
|
|
69
|
+
*
|
|
70
|
+
* The SDK can't own this because:
|
|
71
|
+
* - Each issuer signs with their own `JWT_SECRET` (HS256) OR with
|
|
72
|
+
* their own RSA/ECDSA key — SDK shouldn't see secrets
|
|
73
|
+
* - Token shape may differ (sub format, scope claims, extra claims
|
|
74
|
+
* like role/tier specific to the issuer's app)
|
|
75
|
+
* - Lifetime may differ (24h default, but some issuers want shorter)
|
|
76
|
+
* - Existing JwtGuard / SessionTokenGuard in the issuer backend
|
|
77
|
+
* expects a specific shape — SDK can't predict it
|
|
78
|
+
*
|
|
79
|
+
* The SDK orchestrator calls `mint()` once per successful direct-auth
|
|
80
|
+
* flow, BEFORE returning the bundled response to the issuer's HTTP
|
|
81
|
+
* controller. The minter is responsible for:
|
|
82
|
+
* 1. Producing a token the issuer's existing guards accept
|
|
83
|
+
* 2. Embedding `authAttribute: {type, value}` if the issuer's
|
|
84
|
+
* /wallet/exchange-pafi-jwt refresh path needs it (most do — see
|
|
85
|
+
* ISSUER_INTEGRATION_GUIDE.md §9 refresh path)
|
|
86
|
+
* 3. Computing absolute expiry timestamp the FE can show
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* @Injectable()
|
|
91
|
+
* class Gg56SessionMinter implements ISessionTokenMinter {
|
|
92
|
+
* private readonly secret: Uint8Array;
|
|
93
|
+
* constructor(config: ConfigService) {
|
|
94
|
+
* this.secret = new TextEncoder().encode(
|
|
95
|
+
* config.getOrThrow('JWT_SECRET'),
|
|
96
|
+
* );
|
|
97
|
+
* }
|
|
98
|
+
*
|
|
99
|
+
* async mint(args: {
|
|
100
|
+
* canonicalId: string;
|
|
101
|
+
* verifiedEmail?: string;
|
|
102
|
+
* }): Promise<{ token: string; expiresAt: string }> {
|
|
103
|
+
* const now = Math.floor(Date.now() / 1000);
|
|
104
|
+
* const exp = now + 86400;
|
|
105
|
+
* const token = await new SignJWT({
|
|
106
|
+
* userKey: args.canonicalId,
|
|
107
|
+
* loginType: 'pafi-direct',
|
|
108
|
+
* scope: 'pafi-session',
|
|
109
|
+
* ...(args.verifiedEmail
|
|
110
|
+
* ? { authAttribute: { type: 'email', value: args.verifiedEmail } }
|
|
111
|
+
* : {}),
|
|
112
|
+
* })
|
|
113
|
+
* .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
|
|
114
|
+
* .setSubject(args.canonicalId)
|
|
115
|
+
* .setIssuedAt(now)
|
|
116
|
+
* .setExpirationTime(exp)
|
|
117
|
+
* .sign(this.secret);
|
|
118
|
+
* return { token, expiresAt: new Date(exp * 1000).toISOString() };
|
|
119
|
+
* }
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
interface ISessionTokenMinter {
|
|
124
|
+
mint(args: {
|
|
125
|
+
/** canonical_pafi_user_id, use as `sub` so downstream identifies user uniformly. */
|
|
126
|
+
canonicalId: string;
|
|
127
|
+
/**
|
|
128
|
+
* Verified email when available. Implementations SHOULD embed this
|
|
129
|
+
* as `authAttribute: { type: 'email', value }` in the JWT body so
|
|
130
|
+
* the legacy /wallet/exchange-pafi-jwt refresh path keeps deriving
|
|
131
|
+
* the same canonical_id from the same attribute across refreshes.
|
|
132
|
+
*/
|
|
133
|
+
verifiedEmail?: string;
|
|
134
|
+
}): Promise<{
|
|
135
|
+
/** The signed session token (any JWT shape — issuer guards must accept). */
|
|
136
|
+
token: string;
|
|
137
|
+
/** ISO 8601 absolute expiry timestamp. Returned to FE so it can show "expires at X". */
|
|
138
|
+
expiresAt: string;
|
|
139
|
+
}>;
|
|
140
|
+
}
|
|
141
|
+
/** DI token for ISessionTokenMinter. Use with `@Inject(SESSION_TOKEN_MINTER)`. */
|
|
142
|
+
declare const SESSION_TOKEN_MINTER: unique symbol;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Options consumed by `PafiDirectAuthModule.forRoot(options)`. The
|
|
146
|
+
* host (issuer backend) supplies:
|
|
147
|
+
*
|
|
148
|
+
* - Gateway credentials (URL, issuer_id, client_id, private JWK)
|
|
149
|
+
* - Issuer-specific implementations of IUserStore + ISessionTokenMinter
|
|
150
|
+
*
|
|
151
|
+
* Typically read from `ConfigService` in the issuer backend. Use
|
|
152
|
+
* `forRootAsync()` (planned) when config needs `inject: [ConfigService]`.
|
|
153
|
+
*/
|
|
154
|
+
interface PafiDirectAuthModuleOptions {
|
|
155
|
+
/** Base URL of the PAFI gateway (e.g. `https://id-dev.pacificfinance.org`). */
|
|
156
|
+
gatewayUrl: string;
|
|
157
|
+
/** Issuer identifier registered with the gateway (e.g. `gg56`). */
|
|
158
|
+
issuerId: string;
|
|
159
|
+
/**
|
|
160
|
+
* Gateway client_id assigned at issuer onboarding. Also acts as the
|
|
161
|
+
* `iss`/`sub` of the client_assertion JWT (RFC 7523 §3).
|
|
162
|
+
*/
|
|
163
|
+
clientId: string;
|
|
164
|
+
/**
|
|
165
|
+
* Private JWK the issuer uses to sign client_assertion. MUST include
|
|
166
|
+
* `kid` — gateway looks up the matching public JWK by kid.
|
|
167
|
+
*/
|
|
168
|
+
clientPrivateJwk: JWK & {
|
|
169
|
+
kid: string;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Issuer-implemented user-row upsert. SDK calls this on every
|
|
173
|
+
* successful direct-auth flow to ensure the local users row exists
|
|
174
|
+
* keyed by canonical_id. See `IUserStore` for contract details.
|
|
175
|
+
*/
|
|
176
|
+
userStore: Type<IUserStore>;
|
|
177
|
+
/**
|
|
178
|
+
* Issuer-implemented session-token minter. SDK calls this once per
|
|
179
|
+
* successful direct-auth flow to wrap the gateway-returned canonical
|
|
180
|
+
* + verified email into the issuer's own session JWT. See
|
|
181
|
+
* `ISessionTokenMinter` for contract details.
|
|
182
|
+
*/
|
|
183
|
+
sessionTokenMinter: Type<ISessionTokenMinter>;
|
|
184
|
+
}
|
|
185
|
+
/** DI token for the resolved options. Use with `@Inject(PAFI_DIRECT_AUTH_MODULE_OPTIONS)`. */
|
|
186
|
+
declare const PAFI_DIRECT_AUTH_MODULE_OPTIONS: unique symbol;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* NestJS module wiring the direct-auth surface (`/auth/v2/*`
|
|
190
|
+
* controller, orchestrator service, gateway client, session
|
|
191
|
+
* verifier).
|
|
192
|
+
*
|
|
193
|
+
* Issuer backends mount this once at the root module with their
|
|
194
|
+
* gateway credentials + their `IUserStore` and `ISessionTokenMinter`
|
|
195
|
+
* implementations:
|
|
196
|
+
*
|
|
197
|
+
* ```ts
|
|
198
|
+
* @Module({
|
|
199
|
+
* imports: [
|
|
200
|
+
* ConfigModule.forRoot(),
|
|
201
|
+
* TypeOrmModule.forFeature([UserEntity]),
|
|
202
|
+
* PafiDirectAuthModule.forRoot({
|
|
203
|
+
* gatewayUrl: process.env.PAFI_GATEWAY_URL!,
|
|
204
|
+
* issuerId: process.env.PAFI_GATEWAY_ISSUER_ID!,
|
|
205
|
+
* clientId: process.env.PAFI_GATEWAY_CLIENT_ID!,
|
|
206
|
+
* clientPrivateJwk: JSON.parse(
|
|
207
|
+
* process.env.PAFI_GATEWAY_CLIENT_PRIVATE_JWK_JSON!,
|
|
208
|
+
* ) as JWK & { kid: string },
|
|
209
|
+
* userStore: Gg56UserStore,
|
|
210
|
+
* sessionTokenMinter: Gg56SessionMinter,
|
|
211
|
+
* }),
|
|
212
|
+
* ],
|
|
213
|
+
* providers: [Gg56UserStore, Gg56SessionMinter],
|
|
214
|
+
* })
|
|
215
|
+
* export class AuthModule {}
|
|
216
|
+
* ```
|
|
217
|
+
*
|
|
218
|
+
* The `IUserStore` + `ISessionTokenMinter` classes MUST be provided
|
|
219
|
+
* separately in the host module's `providers:` array (NestJS needs to
|
|
220
|
+
* see them to instantiate via DI). This module wires them to the
|
|
221
|
+
* orchestrator via the `USER_STORE` + `SESSION_TOKEN_MINTER` injection
|
|
222
|
+
* tokens.
|
|
223
|
+
*
|
|
224
|
+
* For ConfigService-based async setup, use `forRootAsync()` (TODO).
|
|
225
|
+
*/
|
|
226
|
+
declare class PafiDirectAuthModule {
|
|
227
|
+
static forRoot(options: PafiDirectAuthModuleOptions): DynamicModule;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Thin DI wrapper around {@link PafiAuthClient}. Constructs a single
|
|
232
|
+
* client at boot using the issuer's gateway credentials and exposes it
|
|
233
|
+
* to services that need to call the direct-auth endpoints.
|
|
234
|
+
*
|
|
235
|
+
* One instance per process — the SDK client is stateless and the
|
|
236
|
+
* client_assertion is signed on every call (no warm-up needed).
|
|
237
|
+
*
|
|
238
|
+
* Config comes from `PafiDirectAuthModule.forRoot(options)` (see
|
|
239
|
+
* `pafi-direct-auth.module.ts`). The host app supplies `gatewayUrl`,
|
|
240
|
+
* `issuerId`, `clientId`, and `clientPrivateJwk` either as literal
|
|
241
|
+
* values OR as a factory that resolves them from NestJS ConfigService
|
|
242
|
+
* (`forRootAsync` variant).
|
|
243
|
+
*/
|
|
244
|
+
declare class PafiAuthClientProvider implements OnModuleInit {
|
|
245
|
+
private readonly options;
|
|
246
|
+
private _client;
|
|
247
|
+
constructor(options: PafiDirectAuthModuleOptions);
|
|
248
|
+
onModuleInit(): void;
|
|
249
|
+
get client(): PafiAuthClient;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interface PafiSessionClaims {
|
|
253
|
+
sub: string;
|
|
254
|
+
scope: "pafi-session";
|
|
255
|
+
/** Verified attribute the gateway used to derive canonical_id. */
|
|
256
|
+
verifiedAttribute?: {
|
|
257
|
+
type: string;
|
|
258
|
+
valueHash?: string;
|
|
259
|
+
};
|
|
260
|
+
/** Issuer that originated the auth (must equal our PAFI_GATEWAY_ISSUER_ID). */
|
|
261
|
+
issuerId?: string;
|
|
262
|
+
exp: number;
|
|
263
|
+
iat: number;
|
|
264
|
+
raw: JWTPayload;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Verifies the long-lived `pafi_session_token` returned by the
|
|
268
|
+
* gateway's direct-auth endpoints. Defense-in-depth: the gateway just
|
|
269
|
+
* minted this token, so the chance of forgery is near zero — BUT
|
|
270
|
+
* verifying it locally guarantees we never trust an attacker-supplied
|
|
271
|
+
* "fake gateway response" that bypassed the network call (e.g. a
|
|
272
|
+
* compromised inbound proxy injecting an arbitrary JSON body). One
|
|
273
|
+
* audit line for a millisecond of crypto work.
|
|
274
|
+
*
|
|
275
|
+
* Uses the gateway's published JWKS (`/.well-known/jwks.json`) — same
|
|
276
|
+
* key set Privy uses when verifying the short-lived `pafi_jwt`. The
|
|
277
|
+
* remote JWKS is cached + auto-refreshed by jose.
|
|
278
|
+
*/
|
|
279
|
+
declare class PafiSessionVerifierService {
|
|
280
|
+
private readonly jwks;
|
|
281
|
+
private readonly expectedIssuer;
|
|
282
|
+
constructor(options: PafiDirectAuthModuleOptions);
|
|
283
|
+
verify(token: string): Promise<PafiSessionClaims>;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
interface DirectAuthResult {
|
|
287
|
+
sessionToken: string;
|
|
288
|
+
sessionExpiresAt: string;
|
|
289
|
+
pafiJwt: string;
|
|
290
|
+
pafiSessionToken: string;
|
|
291
|
+
canonicalId: string;
|
|
292
|
+
isFirstLogin: boolean;
|
|
293
|
+
verifiedEmail?: string;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Orchestrates the gateway-owned auth flows for an issuer backend. The
|
|
297
|
+
* gateway is the sole verifier (it ran the OTP check / verified the
|
|
298
|
+
* Google id_token / exchanged the Kakao code); this service is a thin
|
|
299
|
+
* adapter that:
|
|
300
|
+
*
|
|
301
|
+
* 1. Forwards the call via {@link PafiAuthClient} (RFC 7523
|
|
302
|
+
* client_assertion signed by the issuer's private JWK)
|
|
303
|
+
* 2. Verifies the returned `pafi_session_token` against the gateway's
|
|
304
|
+
* JWKS (defense in depth — see {@link PafiSessionVerifierService})
|
|
305
|
+
* 3. Upserts the local user row via {@link IUserStore} (issuer
|
|
306
|
+
* implements; SDK doesn't know the DB schema)
|
|
307
|
+
* 4. Mints an issuer-native session token via
|
|
308
|
+
* {@link ISessionTokenMinter} (issuer implements; SDK doesn't see
|
|
309
|
+
* the issuer's JWT_SECRET)
|
|
310
|
+
*
|
|
311
|
+
* The returned `sessionToken.sub` is the canonical_id — the same value
|
|
312
|
+
* the gateway uses for the PAFI JWT's `sub`. This means downstream
|
|
313
|
+
* services that need a stable user id can use it without caring which
|
|
314
|
+
* auth method produced it.
|
|
315
|
+
*
|
|
316
|
+
* Extracted from per-issuer boilerplate code in 2026-06-30 SDK refactor.
|
|
317
|
+
* Previously copy-pasted (~260 lines per issuer); now ~60 lines per
|
|
318
|
+
* issuer (UserStore + SessionMinter + module wiring).
|
|
319
|
+
*/
|
|
320
|
+
declare class PafiDirectAuthService {
|
|
321
|
+
private readonly clientProvider;
|
|
322
|
+
private readonly sessionVerifier;
|
|
323
|
+
private readonly userStore;
|
|
324
|
+
private readonly sessionTokenMinter;
|
|
325
|
+
private readonly logger;
|
|
326
|
+
constructor(clientProvider: PafiAuthClientProvider, sessionVerifier: PafiSessionVerifierService, userStore: IUserStore, sessionTokenMinter: ISessionTokenMinter);
|
|
327
|
+
startEmail(args: {
|
|
328
|
+
email: string;
|
|
329
|
+
correlationId?: string;
|
|
330
|
+
}): Promise<{
|
|
331
|
+
challengeId: string;
|
|
332
|
+
expiresInSec: number;
|
|
333
|
+
}>;
|
|
334
|
+
verifyEmail(args: {
|
|
335
|
+
challengeId: string;
|
|
336
|
+
otpCode: string;
|
|
337
|
+
correlationId?: string;
|
|
338
|
+
}): Promise<DirectAuthResult>;
|
|
339
|
+
exchangeGoogle(args: {
|
|
340
|
+
idToken: string;
|
|
341
|
+
correlationId?: string;
|
|
342
|
+
}): Promise<DirectAuthResult>;
|
|
343
|
+
exchangeKakao(args: {
|
|
344
|
+
code: string;
|
|
345
|
+
redirectUri?: string;
|
|
346
|
+
correlationId?: string;
|
|
347
|
+
}): Promise<DirectAuthResult>;
|
|
348
|
+
private finalize;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
declare class EmailStartRequestDto {
|
|
352
|
+
email: string;
|
|
353
|
+
}
|
|
354
|
+
declare class EmailVerifyRequestDto {
|
|
355
|
+
challengeId: string;
|
|
356
|
+
otpCode: string;
|
|
357
|
+
}
|
|
358
|
+
declare class GoogleExchangeRequestDto {
|
|
359
|
+
idToken: string;
|
|
360
|
+
}
|
|
361
|
+
declare class KakaoExchangeRequestDto {
|
|
362
|
+
code: string;
|
|
363
|
+
redirectUri?: string;
|
|
364
|
+
}
|
|
365
|
+
declare class EmailStartResponseDto {
|
|
366
|
+
challengeId: string;
|
|
367
|
+
expiresInSec: number;
|
|
368
|
+
}
|
|
369
|
+
declare class PafiAuthSuccessDto {
|
|
370
|
+
sessionToken: string;
|
|
371
|
+
sessionExpiresAt: string;
|
|
372
|
+
pafiJwt: string;
|
|
373
|
+
pafiSessionToken: string;
|
|
374
|
+
canonicalId: string;
|
|
375
|
+
isFirstLogin: boolean;
|
|
376
|
+
verifiedEmail?: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* `/auth/v2/*` — gateway-owned auth endpoints (2026-06-30 direct-auth
|
|
381
|
+
* surface, shipped by `@pafi-dev/issuer/direct-auth`).
|
|
382
|
+
*
|
|
383
|
+
* Issuer backend no longer verifies OTP codes, OAuth id_tokens, or
|
|
384
|
+
* holds OAuth client_secrets. All of that lives at the PAFI gateway.
|
|
385
|
+
* These endpoints are thin proxies that forward user input via
|
|
386
|
+
* {@link PafiDirectAuthService}, then upsert the local user row (via
|
|
387
|
+
* the issuer-provided {@link IUserStore}) + mint an issuer-native
|
|
388
|
+
* session token (via the issuer-provided {@link ISessionTokenMinter})
|
|
389
|
+
* wrapping the gateway-assigned canonical_id.
|
|
390
|
+
*
|
|
391
|
+
* Mounted automatically by `PafiDirectAuthModule.forRoot(options)`.
|
|
392
|
+
* Issuer apps that want to layer extra auth-error handling (e.g. a
|
|
393
|
+
* custom `AuthErrorFilter`) can wrap the controller's routes via a
|
|
394
|
+
* NestJS global filter — no per-route `@UseFilters` needed here.
|
|
395
|
+
*/
|
|
396
|
+
declare class PafiDirectAuthController {
|
|
397
|
+
private readonly directAuth;
|
|
398
|
+
constructor(directAuth: PafiDirectAuthService);
|
|
399
|
+
startEmail(body: EmailStartRequestDto): Promise<EmailStartResponseDto>;
|
|
400
|
+
verifyEmail(body: EmailVerifyRequestDto): Promise<PafiAuthSuccessDto>;
|
|
401
|
+
exchangeGoogle(body: GoogleExchangeRequestDto): Promise<PafiAuthSuccessDto>;
|
|
402
|
+
exchangeKakao(body: KakaoExchangeRequestDto): Promise<PafiAuthSuccessDto>;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export { type DirectAuthResult, EmailStartRequestDto, EmailStartResponseDto, EmailVerifyRequestDto, GoogleExchangeRequestDto, type ISessionTokenMinter, type IUserStore, KakaoExchangeRequestDto, PAFI_DIRECT_AUTH_MODULE_OPTIONS, PafiAuthClientProvider, PafiAuthSuccessDto, PafiDirectAuthController, PafiDirectAuthModule, type PafiDirectAuthModuleOptions, PafiDirectAuthService, type PafiSessionClaims, PafiSessionVerifierService, SESSION_TOKEN_MINTER, USER_STORE };
|