@pafi-dev/issuer 0.39.2 → 0.40.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.
@@ -3,6 +3,7 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
7
  var __export = (target, all) => {
7
8
  for (var name in all)
8
9
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -36,117 +37,109 @@ async function signClientAssertion(args) {
36
37
  const alg = args.alg ?? args.privateJwk.alg ?? "ES256";
37
38
  const key = await (0, import_jose.importJWK)(args.privateJwk, alg);
38
39
  const now = Math.floor(Date.now() / 1e3);
39
- return new import_jose.SignJWT({}).setProtectedHeader({ alg, typ: "JWT", kid: args.privateJwk.kid }).setIssuer(args.clientId).setSubject(args.clientId).setAudience(`${args.gatewayUrl}/v1/token-exchange`).setIssuedAt(now).setExpirationTime(now + 60).setJti((0, import_node_crypto.randomUUID)()).sign(key);
40
+ return new import_jose.SignJWT({}).setProtectedHeader({
41
+ alg,
42
+ typ: "JWT",
43
+ kid: args.privateJwk.kid
44
+ }).setIssuer(args.clientId).setSubject(args.clientId).setAudience(`${args.gatewayUrl}/v1/token-exchange`).setIssuedAt(now).setExpirationTime(now + 60).setJti((0, import_node_crypto.randomUUID)()).sign(key);
40
45
  }
46
+ __name(signClientAssertion, "signClientAssertion");
41
47
 
42
48
  // src/auth-client/types.ts
43
49
  var PafiAuthError = class extends Error {
44
- constructor(message, status, code, correlationId) {
45
- super(message);
46
- this.status = status;
47
- this.code = code;
48
- this.correlationId = correlationId;
49
- this.name = "PafiAuthError";
50
+ static {
51
+ __name(this, "PafiAuthError");
50
52
  }
51
53
  status;
52
54
  code;
53
55
  correlationId;
56
+ constructor(message, status, code, correlationId) {
57
+ super(message), this.status = status, this.code = code, this.correlationId = correlationId;
58
+ this.name = "PafiAuthError";
59
+ }
54
60
  };
55
61
 
56
62
  // src/auth-client/pafi-auth-client.ts
57
63
  var PafiAuthClient = class {
64
+ static {
65
+ __name(this, "PafiAuthClient");
66
+ }
67
+ opts;
68
+ fetchImpl;
69
+ tokenExchangeAud;
58
70
  constructor(opts) {
59
71
  this.opts = opts;
60
72
  if (!opts.clientPrivateJwk.kid) {
61
- throw new Error(
62
- "PafiAuthClient: clientPrivateJwk.kid is required (gateway uses kid to look up the verification key)"
63
- );
73
+ throw new Error("PafiAuthClient: clientPrivateJwk.kid is required (gateway uses kid to look up the verification key)");
64
74
  }
65
75
  this.fetchImpl = opts.fetchImpl ?? fetch;
66
76
  this.tokenExchangeAud = `${opts.gatewayUrl}/v1/token-exchange`;
67
77
  }
68
- opts;
69
- fetchImpl;
70
- tokenExchangeAud;
71
78
  // ───────────────────────────────────────────────────────────────
72
79
  // EMAIL OTP — 2-step
73
80
  // ───────────────────────────────────────────────────────────────
74
81
  /**
75
- * Step 1: ask the gateway to send the user an OTP. Returns the
76
- * `challengeId` to echo back on {@link verifyEmail}.
77
- */
82
+ * Step 1: ask the gateway to send the user an OTP. Returns the
83
+ * `challengeId` to echo back on {@link verifyEmail}.
84
+ */
78
85
  async startEmail(args) {
79
- const res = await this.post(
80
- "/v1/auth/email/start",
81
- {
82
- issuer_id: this.opts.issuerId,
83
- email: args.email
84
- },
85
- args.correlationId
86
- );
86
+ const res = await this.post("/v1/auth/email/start", {
87
+ issuer_id: this.opts.issuerId,
88
+ email: args.email
89
+ }, args.correlationId);
87
90
  return {
88
91
  challengeId: res.challenge_id,
89
92
  expiresInSec: res.expires_in
90
93
  };
91
94
  }
92
95
  /**
93
- * Step 2: submit the OTP the user received. On success returns
94
- * {@link AuthSuccess} containing BOTH the long-lived
95
- * pafi_session_token (issuer verifies via gateway JWKS) AND the
96
- * short-lived pafi_jwt (issuer FE feeds to Privy).
97
- */
96
+ * Step 2: submit the OTP the user received. On success returns
97
+ * {@link AuthSuccess} containing BOTH the long-lived
98
+ * pafi_session_token (issuer verifies via gateway JWKS) AND the
99
+ * short-lived pafi_jwt (issuer FE feeds to Privy).
100
+ */
98
101
  async verifyEmail(args) {
99
- const res = await this.post(
100
- "/v1/auth/email/verify",
101
- {
102
- challenge_id: args.challengeId,
103
- otp_code: args.otpCode
104
- },
105
- args.correlationId
106
- );
102
+ const res = await this.post("/v1/auth/email/verify", {
103
+ challenge_id: args.challengeId,
104
+ otp_code: args.otpCode
105
+ }, args.correlationId);
107
106
  return mapAuthSuccess(res);
108
107
  }
109
108
  // ───────────────────────────────────────────────────────────────
110
109
  // GOOGLE — 1-step exchange
111
110
  // ───────────────────────────────────────────────────────────────
112
111
  /**
113
- * Hand the gateway an id_token the issuer FE obtained from Google
114
- * Identity Services (using PAFI's shared client_id). Gateway verifies
115
- * signature + audience + `email_verified` before resolving identity.
116
- */
112
+ * Hand the gateway an id_token the issuer FE obtained from Google
113
+ * Identity Services (using PAFI's shared client_id). Gateway verifies
114
+ * signature + audience + `email_verified` before resolving identity.
115
+ */
117
116
  async exchangeGoogle(args) {
118
- const res = await this.post(
119
- "/v1/auth/google/exchange",
120
- {
121
- issuer_id: this.opts.issuerId,
122
- id_token: args.idToken
123
- },
124
- args.correlationId
125
- );
117
+ const res = await this.post("/v1/auth/google/exchange", {
118
+ issuer_id: this.opts.issuerId,
119
+ id_token: args.idToken
120
+ }, args.correlationId);
126
121
  return mapAuthSuccess(res);
127
122
  }
128
123
  // ───────────────────────────────────────────────────────────────
129
124
  // KAKAO — 1-step exchange (authorization code)
130
125
  // ───────────────────────────────────────────────────────────────
131
126
  /**
132
- * Hand the gateway the authorization code returned by Kakao's
133
- * redirect. Gateway exchanges with Kakao (server-to-server using
134
- * PAFI's client_secret), verifies id_token, resolves identity.
135
- *
136
- * `redirectUri` must match the URL the FE used when starting the
137
- * Kakao flow. Falls back to the gateway's KAKAO_REDIRECT_URI when
138
- * omitted — pass an explicit value for multi-environment FEs.
139
- */
127
+ * Hand the gateway the authorization code returned by Kakao's
128
+ * redirect. Gateway exchanges with Kakao (server-to-server using
129
+ * PAFI's client_secret), verifies id_token, resolves identity.
130
+ *
131
+ * `redirectUri` must match the URL the FE used when starting the
132
+ * Kakao flow. Falls back to the gateway's KAKAO_REDIRECT_URI when
133
+ * omitted — pass an explicit value for multi-environment FEs.
134
+ */
140
135
  async exchangeKakao(args) {
141
- const res = await this.post(
142
- "/v1/auth/kakao/exchange",
143
- {
144
- issuer_id: this.opts.issuerId,
145
- code: args.code,
146
- ...args.redirectUri ? { redirect_uri: args.redirectUri } : {}
147
- },
148
- args.correlationId
149
- );
136
+ const res = await this.post("/v1/auth/kakao/exchange", {
137
+ issuer_id: this.opts.issuerId,
138
+ code: args.code,
139
+ ...args.redirectUri ? {
140
+ redirect_uri: args.redirectUri
141
+ } : {}
142
+ }, args.correlationId);
150
143
  return mapAuthSuccess(res);
151
144
  }
152
145
  // ───────────────────────────────────────────────────────────────
@@ -172,21 +165,11 @@ var PafiAuthClient = class {
172
165
  try {
173
166
  parsed = text ? JSON.parse(text) : {};
174
167
  } catch {
175
- throw new PafiAuthError(
176
- `Non-JSON response from gateway (${path}): ${text.slice(0, 120)}`,
177
- res.status,
178
- "non_json_response",
179
- finalCorrelationId
180
- );
168
+ throw new PafiAuthError(`Non-JSON response from gateway (${path}): ${text.slice(0, 120)}`, res.status, "non_json_response", finalCorrelationId);
181
169
  }
182
170
  if (!res.ok) {
183
171
  const err = parsed;
184
- throw new PafiAuthError(
185
- err.error_description ?? err.error ?? `Gateway returned HTTP ${res.status}`,
186
- res.status,
187
- err.error ?? "unknown_error",
188
- err.correlation_id ?? finalCorrelationId
189
- );
172
+ throw new PafiAuthError(err.error_description ?? err.error ?? `Gateway returned HTTP ${res.status}`, res.status, err.error ?? "unknown_error", err.correlation_id ?? finalCorrelationId);
190
173
  }
191
174
  return parsed;
192
175
  }
@@ -198,9 +181,12 @@ function mapAuthSuccess(res) {
198
181
  canonicalId: res.canonical_id,
199
182
  expiresAt: res.expires_at,
200
183
  isFirstLogin: res.is_first_login,
201
- ...res.verified_email ? { verifiedEmail: res.verified_email } : {}
184
+ ...res.verified_email ? {
185
+ verifiedEmail: res.verified_email
186
+ } : {}
202
187
  };
203
188
  }
189
+ __name(mapAuthSuccess, "mapAuthSuccess");
204
190
  // Annotate the CommonJS export names for ESM import in node:
205
191
  0 && (module.exports = {
206
192
  PafiAuthClient,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth-client/index.ts","../../src/auth-client/pafi-auth-client.ts","../../src/auth-client/sign-client-assertion.ts","../../src/auth-client/types.ts"],"sourcesContent":["export { PafiAuthClient } from \"./pafi-auth-client\";\nexport { PafiAuthError } from \"./types\";\nexport type {\n AuthSuccess,\n EmailChallenge,\n PafiAuthClientOptions,\n} from \"./types\";\nexport { signClientAssertion } from \"./sign-client-assertion\";\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"],"mappings":"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","names":["import_node_crypto"]}
1
+ {"version":3,"sources":["../../src/auth-client/index.ts","../../src/auth-client/pafi-auth-client.ts","../../src/auth-client/sign-client-assertion.ts","../../src/auth-client/types.ts"],"sourcesContent":["export { PafiAuthClient } from \"./pafi-auth-client\";\nexport { PafiAuthError } from \"./types\";\nexport type {\n AuthSuccess,\n EmailChallenge,\n PafiAuthClientOptions,\n} from \"./types\";\nexport { signClientAssertion } from \"./sign-client-assertion\";\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"],"mappings":"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","names":["import_node_crypto","signClientAssertion","args","alg","privateJwk","key","importJWK","now","Math","floor","Date","SignJWT","setProtectedHeader","typ","kid","setIssuer","clientId","setSubject","setAudience","gatewayUrl","setIssuedAt","setExpirationTime","setJti","randomUUID","sign","PafiAuthError","Error","message","status","code","correlationId","name","PafiAuthClient","fetchImpl","tokenExchangeAud","opts","clientPrivateJwk","kid","Error","fetch","gatewayUrl","startEmail","args","res","post","issuer_id","issuerId","email","correlationId","challengeId","challenge_id","expiresInSec","expires_in","verifyEmail","otp_code","otpCode","mapAuthSuccess","exchangeGoogle","id_token","idToken","exchangeKakao","code","redirectUri","redirect_uri","path","body","assertion","signClientAssertion","clientId","privateJwk","alg","finalCorrelationId","randomUUID","method","headers","Authorization","JSON","stringify","text","parsed","parse","PafiAuthError","slice","status","ok","err","error_description","error","correlation_id","pafiSessionToken","pafi_session_token","pafiJwt","pafi_jwt","canonicalId","canonical_id","expiresAt","expires_at","isFirstLogin","is_first_login","verified_email","verifiedEmail"]}
@@ -2,8 +2,8 @@ import {
2
2
  PafiAuthClient,
3
3
  PafiAuthError,
4
4
  signClientAssertion
5
- } from "../chunk-7VEYSL2C.js";
6
- import "../chunk-BRKEJJFQ.js";
5
+ } from "../chunk-2Z3M2KQG.js";
6
+ import "../chunk-7QVYU63E.js";
7
7
  export {
8
8
  PafiAuthClient,
9
9
  PafiAuthError,
@@ -1,3 +1,7 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-7QVYU63E.js";
4
+
1
5
  // src/auth-client/sign-client-assertion.ts
2
6
  import { importJWK, SignJWT } from "jose";
3
7
  import { randomUUID } from "crypto";
@@ -5,118 +9,110 @@ async function signClientAssertion(args) {
5
9
  const alg = args.alg ?? args.privateJwk.alg ?? "ES256";
6
10
  const key = await importJWK(args.privateJwk, alg);
7
11
  const now = Math.floor(Date.now() / 1e3);
8
- return new SignJWT({}).setProtectedHeader({ alg, typ: "JWT", kid: args.privateJwk.kid }).setIssuer(args.clientId).setSubject(args.clientId).setAudience(`${args.gatewayUrl}/v1/token-exchange`).setIssuedAt(now).setExpirationTime(now + 60).setJti(randomUUID()).sign(key);
12
+ return new SignJWT({}).setProtectedHeader({
13
+ alg,
14
+ typ: "JWT",
15
+ kid: args.privateJwk.kid
16
+ }).setIssuer(args.clientId).setSubject(args.clientId).setAudience(`${args.gatewayUrl}/v1/token-exchange`).setIssuedAt(now).setExpirationTime(now + 60).setJti(randomUUID()).sign(key);
9
17
  }
18
+ __name(signClientAssertion, "signClientAssertion");
10
19
 
11
20
  // src/auth-client/types.ts
12
21
  var PafiAuthError = class extends Error {
13
- constructor(message, status, code, correlationId) {
14
- super(message);
15
- this.status = status;
16
- this.code = code;
17
- this.correlationId = correlationId;
18
- this.name = "PafiAuthError";
22
+ static {
23
+ __name(this, "PafiAuthError");
19
24
  }
20
25
  status;
21
26
  code;
22
27
  correlationId;
28
+ constructor(message, status, code, correlationId) {
29
+ super(message), this.status = status, this.code = code, this.correlationId = correlationId;
30
+ this.name = "PafiAuthError";
31
+ }
23
32
  };
24
33
 
25
34
  // src/auth-client/pafi-auth-client.ts
26
35
  import { randomUUID as randomUUID2 } from "crypto";
27
36
  var PafiAuthClient = class {
37
+ static {
38
+ __name(this, "PafiAuthClient");
39
+ }
40
+ opts;
41
+ fetchImpl;
42
+ tokenExchangeAud;
28
43
  constructor(opts) {
29
44
  this.opts = opts;
30
45
  if (!opts.clientPrivateJwk.kid) {
31
- throw new Error(
32
- "PafiAuthClient: clientPrivateJwk.kid is required (gateway uses kid to look up the verification key)"
33
- );
46
+ throw new Error("PafiAuthClient: clientPrivateJwk.kid is required (gateway uses kid to look up the verification key)");
34
47
  }
35
48
  this.fetchImpl = opts.fetchImpl ?? fetch;
36
49
  this.tokenExchangeAud = `${opts.gatewayUrl}/v1/token-exchange`;
37
50
  }
38
- opts;
39
- fetchImpl;
40
- tokenExchangeAud;
41
51
  // ───────────────────────────────────────────────────────────────
42
52
  // EMAIL OTP — 2-step
43
53
  // ───────────────────────────────────────────────────────────────
44
54
  /**
45
- * Step 1: ask the gateway to send the user an OTP. Returns the
46
- * `challengeId` to echo back on {@link verifyEmail}.
47
- */
55
+ * Step 1: ask the gateway to send the user an OTP. Returns the
56
+ * `challengeId` to echo back on {@link verifyEmail}.
57
+ */
48
58
  async startEmail(args) {
49
- const res = await this.post(
50
- "/v1/auth/email/start",
51
- {
52
- issuer_id: this.opts.issuerId,
53
- email: args.email
54
- },
55
- args.correlationId
56
- );
59
+ const res = await this.post("/v1/auth/email/start", {
60
+ issuer_id: this.opts.issuerId,
61
+ email: args.email
62
+ }, args.correlationId);
57
63
  return {
58
64
  challengeId: res.challenge_id,
59
65
  expiresInSec: res.expires_in
60
66
  };
61
67
  }
62
68
  /**
63
- * Step 2: submit the OTP the user received. On success returns
64
- * {@link AuthSuccess} containing BOTH the long-lived
65
- * pafi_session_token (issuer verifies via gateway JWKS) AND the
66
- * short-lived pafi_jwt (issuer FE feeds to Privy).
67
- */
69
+ * Step 2: submit the OTP the user received. On success returns
70
+ * {@link AuthSuccess} containing BOTH the long-lived
71
+ * pafi_session_token (issuer verifies via gateway JWKS) AND the
72
+ * short-lived pafi_jwt (issuer FE feeds to Privy).
73
+ */
68
74
  async verifyEmail(args) {
69
- const res = await this.post(
70
- "/v1/auth/email/verify",
71
- {
72
- challenge_id: args.challengeId,
73
- otp_code: args.otpCode
74
- },
75
- args.correlationId
76
- );
75
+ const res = await this.post("/v1/auth/email/verify", {
76
+ challenge_id: args.challengeId,
77
+ otp_code: args.otpCode
78
+ }, args.correlationId);
77
79
  return mapAuthSuccess(res);
78
80
  }
79
81
  // ───────────────────────────────────────────────────────────────
80
82
  // GOOGLE — 1-step exchange
81
83
  // ───────────────────────────────────────────────────────────────
82
84
  /**
83
- * Hand the gateway an id_token the issuer FE obtained from Google
84
- * Identity Services (using PAFI's shared client_id). Gateway verifies
85
- * signature + audience + `email_verified` before resolving identity.
86
- */
85
+ * Hand the gateway an id_token the issuer FE obtained from Google
86
+ * Identity Services (using PAFI's shared client_id). Gateway verifies
87
+ * signature + audience + `email_verified` before resolving identity.
88
+ */
87
89
  async exchangeGoogle(args) {
88
- const res = await this.post(
89
- "/v1/auth/google/exchange",
90
- {
91
- issuer_id: this.opts.issuerId,
92
- id_token: args.idToken
93
- },
94
- args.correlationId
95
- );
90
+ const res = await this.post("/v1/auth/google/exchange", {
91
+ issuer_id: this.opts.issuerId,
92
+ id_token: args.idToken
93
+ }, args.correlationId);
96
94
  return mapAuthSuccess(res);
97
95
  }
98
96
  // ───────────────────────────────────────────────────────────────
99
97
  // KAKAO — 1-step exchange (authorization code)
100
98
  // ───────────────────────────────────────────────────────────────
101
99
  /**
102
- * Hand the gateway the authorization code returned by Kakao's
103
- * redirect. Gateway exchanges with Kakao (server-to-server using
104
- * PAFI's client_secret), verifies id_token, resolves identity.
105
- *
106
- * `redirectUri` must match the URL the FE used when starting the
107
- * Kakao flow. Falls back to the gateway's KAKAO_REDIRECT_URI when
108
- * omitted — pass an explicit value for multi-environment FEs.
109
- */
100
+ * Hand the gateway the authorization code returned by Kakao's
101
+ * redirect. Gateway exchanges with Kakao (server-to-server using
102
+ * PAFI's client_secret), verifies id_token, resolves identity.
103
+ *
104
+ * `redirectUri` must match the URL the FE used when starting the
105
+ * Kakao flow. Falls back to the gateway's KAKAO_REDIRECT_URI when
106
+ * omitted — pass an explicit value for multi-environment FEs.
107
+ */
110
108
  async exchangeKakao(args) {
111
- const res = await this.post(
112
- "/v1/auth/kakao/exchange",
113
- {
114
- issuer_id: this.opts.issuerId,
115
- code: args.code,
116
- ...args.redirectUri ? { redirect_uri: args.redirectUri } : {}
117
- },
118
- args.correlationId
119
- );
109
+ const res = await this.post("/v1/auth/kakao/exchange", {
110
+ issuer_id: this.opts.issuerId,
111
+ code: args.code,
112
+ ...args.redirectUri ? {
113
+ redirect_uri: args.redirectUri
114
+ } : {}
115
+ }, args.correlationId);
120
116
  return mapAuthSuccess(res);
121
117
  }
122
118
  // ───────────────────────────────────────────────────────────────
@@ -142,21 +138,11 @@ var PafiAuthClient = class {
142
138
  try {
143
139
  parsed = text ? JSON.parse(text) : {};
144
140
  } catch {
145
- throw new PafiAuthError(
146
- `Non-JSON response from gateway (${path}): ${text.slice(0, 120)}`,
147
- res.status,
148
- "non_json_response",
149
- finalCorrelationId
150
- );
141
+ throw new PafiAuthError(`Non-JSON response from gateway (${path}): ${text.slice(0, 120)}`, res.status, "non_json_response", finalCorrelationId);
151
142
  }
152
143
  if (!res.ok) {
153
144
  const err = parsed;
154
- throw new PafiAuthError(
155
- err.error_description ?? err.error ?? `Gateway returned HTTP ${res.status}`,
156
- res.status,
157
- err.error ?? "unknown_error",
158
- err.correlation_id ?? finalCorrelationId
159
- );
145
+ throw new PafiAuthError(err.error_description ?? err.error ?? `Gateway returned HTTP ${res.status}`, res.status, err.error ?? "unknown_error", err.correlation_id ?? finalCorrelationId);
160
146
  }
161
147
  return parsed;
162
148
  }
@@ -168,13 +154,16 @@ function mapAuthSuccess(res) {
168
154
  canonicalId: res.canonical_id,
169
155
  expiresAt: res.expires_at,
170
156
  isFirstLogin: res.is_first_login,
171
- ...res.verified_email ? { verifiedEmail: res.verified_email } : {}
157
+ ...res.verified_email ? {
158
+ verifiedEmail: res.verified_email
159
+ } : {}
172
160
  };
173
161
  }
162
+ __name(mapAuthSuccess, "mapAuthSuccess");
174
163
 
175
164
  export {
176
165
  signClientAssertion,
177
166
  PafiAuthError,
178
167
  PafiAuthClient
179
168
  };
180
- //# sourceMappingURL=chunk-7VEYSL2C.js.map
169
+ //# sourceMappingURL=chunk-2Z3M2KQG.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/auth-client/sign-client-assertion.ts","../src/auth-client/types.ts","../src/auth-client/pafi-auth-client.ts"],"sourcesContent":["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 { 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"],"mappings":"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","names":["randomUUID","randomUUID"]}
1
+ {"version":3,"sources":["../src/auth-client/sign-client-assertion.ts","../src/auth-client/types.ts","../src/auth-client/pafi-auth-client.ts"],"sourcesContent":["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 { 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"],"mappings":"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","names":["importJWK","SignJWT","randomUUID","signClientAssertion","args","alg","privateJwk","key","importJWK","now","Math","floor","Date","SignJWT","setProtectedHeader","typ","kid","setIssuer","clientId","setSubject","setAudience","gatewayUrl","setIssuedAt","setExpirationTime","setJti","randomUUID","sign","PafiAuthError","Error","message","status","code","correlationId","name","randomUUID","PafiAuthClient","fetchImpl","tokenExchangeAud","opts","clientPrivateJwk","kid","Error","fetch","gatewayUrl","startEmail","args","res","post","issuer_id","issuerId","email","correlationId","challengeId","challenge_id","expiresInSec","expires_in","verifyEmail","otp_code","otpCode","mapAuthSuccess","exchangeGoogle","id_token","idToken","exchangeKakao","code","redirectUri","redirect_uri","path","body","assertion","signClientAssertion","clientId","privateJwk","alg","finalCorrelationId","randomUUID","method","headers","Authorization","JSON","stringify","text","parsed","parse","PafiAuthError","slice","status","ok","err","error_description","error","correlation_id","pafiSessionToken","pafi_session_token","pafiJwt","pafi_jwt","canonicalId","canonical_id","expiresAt","expires_at","isFirstLogin","is_first_login","verified_email","verifiedEmail"]}
@@ -0,0 +1,7 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ export {
5
+ __name
6
+ };
7
+ //# sourceMappingURL=chunk-7QVYU63E.js.map