@pafi-dev/issuer 0.36.0 → 0.38.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.
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/auth-client/index.ts
21
+ var auth_client_exports = {};
22
+ __export(auth_client_exports, {
23
+ PafiAuthClient: () => PafiAuthClient,
24
+ PafiAuthError: () => PafiAuthError,
25
+ signClientAssertion: () => signClientAssertion
26
+ });
27
+ module.exports = __toCommonJS(auth_client_exports);
28
+
29
+ // src/auth-client/pafi-auth-client.ts
30
+ var import_node_crypto2 = require("crypto");
31
+
32
+ // src/auth-client/sign-client-assertion.ts
33
+ var import_jose = require("jose");
34
+ var import_node_crypto = require("crypto");
35
+ async function signClientAssertion(args) {
36
+ const alg = args.alg ?? args.privateJwk.alg ?? "ES256";
37
+ const key = await (0, import_jose.importJWK)(args.privateJwk, alg);
38
+ 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
+ }
41
+
42
+ // src/auth-client/types.ts
43
+ 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
+ }
51
+ status;
52
+ code;
53
+ correlationId;
54
+ };
55
+
56
+ // src/auth-client/pafi-auth-client.ts
57
+ var PafiAuthClient = class {
58
+ constructor(opts) {
59
+ this.opts = opts;
60
+ if (!opts.clientPrivateJwk.kid) {
61
+ throw new Error(
62
+ "PafiAuthClient: clientPrivateJwk.kid is required (gateway uses kid to look up the verification key)"
63
+ );
64
+ }
65
+ this.fetchImpl = opts.fetchImpl ?? fetch;
66
+ this.tokenExchangeAud = `${opts.gatewayUrl}/v1/token-exchange`;
67
+ }
68
+ opts;
69
+ fetchImpl;
70
+ tokenExchangeAud;
71
+ // ───────────────────────────────────────────────────────────────
72
+ // EMAIL OTP — 2-step
73
+ // ───────────────────────────────────────────────────────────────
74
+ /**
75
+ * Step 1: ask the gateway to send the user an OTP. Returns the
76
+ * `challengeId` to echo back on {@link verifyEmail}.
77
+ */
78
+ 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
+ );
87
+ return {
88
+ challengeId: res.challenge_id,
89
+ expiresInSec: res.expires_in
90
+ };
91
+ }
92
+ /**
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
+ */
98
+ 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
+ );
107
+ return mapAuthSuccess(res);
108
+ }
109
+ // ───────────────────────────────────────────────────────────────
110
+ // GOOGLE — 1-step exchange
111
+ // ───────────────────────────────────────────────────────────────
112
+ /**
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
+ */
117
+ 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
+ );
126
+ return mapAuthSuccess(res);
127
+ }
128
+ // ───────────────────────────────────────────────────────────────
129
+ // KAKAO — 1-step exchange (authorization code)
130
+ // ───────────────────────────────────────────────────────────────
131
+ /**
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
+ */
140
+ 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
+ );
150
+ return mapAuthSuccess(res);
151
+ }
152
+ // ───────────────────────────────────────────────────────────────
153
+ async post(path, body, correlationId) {
154
+ const assertion = await signClientAssertion({
155
+ gatewayUrl: this.opts.gatewayUrl,
156
+ clientId: this.opts.clientId,
157
+ privateJwk: this.opts.clientPrivateJwk,
158
+ alg: this.opts.alg
159
+ });
160
+ const finalCorrelationId = correlationId ?? `iss-${(0, import_node_crypto2.randomUUID)()}`;
161
+ const res = await this.fetchImpl(`${this.opts.gatewayUrl}${path}`, {
162
+ method: "POST",
163
+ headers: {
164
+ Authorization: `Bearer ${assertion}`,
165
+ "Content-Type": "application/json",
166
+ "X-Correlation-Id": finalCorrelationId
167
+ },
168
+ body: JSON.stringify(body)
169
+ });
170
+ const text = await res.text();
171
+ let parsed;
172
+ try {
173
+ parsed = text ? JSON.parse(text) : {};
174
+ } 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
+ );
181
+ }
182
+ if (!res.ok) {
183
+ 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
+ );
190
+ }
191
+ return parsed;
192
+ }
193
+ };
194
+ function mapAuthSuccess(res) {
195
+ return {
196
+ pafiSessionToken: res.pafi_session_token,
197
+ pafiJwt: res.pafi_jwt,
198
+ canonicalId: res.canonical_id,
199
+ expiresAt: res.expires_at,
200
+ isFirstLogin: res.is_first_login,
201
+ ...res.verified_email ? { verifiedEmail: res.verified_email } : {}
202
+ };
203
+ }
204
+ // Annotate the CommonJS export names for ESM import in node:
205
+ 0 && (module.exports = {
206
+ PafiAuthClient,
207
+ PafiAuthError,
208
+ signClientAssertion
209
+ });
210
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,171 @@
1
+ import { JWK } from 'jose';
2
+
3
+ /**
4
+ * Constructor params for {@link PafiAuthClient}. One instance per
5
+ * issuer backend — wraps the issuer's gateway credentials (client_id +
6
+ * private JWK) plus the static config (gateway URL, audience).
7
+ */
8
+ interface PafiAuthClientOptions {
9
+ /** Base URL of the PAFI gateway (e.g. `https://id-dev.pacificfinance.org`). */
10
+ gatewayUrl: string;
11
+ /** Issuer identifier registered with the gateway (e.g. `gg56`). */
12
+ issuerId: string;
13
+ /**
14
+ * Gateway client_id assigned at issuer onboarding. Also acts as the
15
+ * `iss`/`sub` of the client_assertion JWT (RFC 7523 §3).
16
+ */
17
+ clientId: string;
18
+ /**
19
+ * Private JWK the issuer uses to sign client_assertion. MUST include
20
+ * `kid` — gateway looks up the matching public JWK by kid.
21
+ */
22
+ clientPrivateJwk: JWK & {
23
+ kid: string;
24
+ };
25
+ /**
26
+ * Optional fetch override — useful for tests / Node env without
27
+ * global fetch (Node ≥ 18 has it built-in).
28
+ */
29
+ fetchImpl?: typeof fetch;
30
+ /**
31
+ * Optional algorithm override for the client assertion JWT. Default
32
+ * `ES256`. Must match what the gateway expects for this client.
33
+ */
34
+ alg?: "ES256" | "ES384" | "RS256" | "RS384" | "RS512" | "EdDSA";
35
+ }
36
+ interface AuthSuccess {
37
+ pafiSessionToken: string;
38
+ pafiJwt: string;
39
+ canonicalId: string;
40
+ expiresAt: number;
41
+ isFirstLogin: boolean;
42
+ verifiedEmail?: string;
43
+ }
44
+ interface EmailChallenge {
45
+ challengeId: string;
46
+ expiresInSec: number;
47
+ }
48
+ /**
49
+ * Thrown when the gateway rejects the call. `code` is the gateway's
50
+ * structured `error` field (e.g. `invalid_otp`, `too_many_attempts`,
51
+ * `expired`, `email_not_verified`) — issuers can branch on it to drive
52
+ * UX (e.g. show "Resend code" button on `expired`).
53
+ */
54
+ declare class PafiAuthError extends Error {
55
+ readonly status: number;
56
+ readonly code: string;
57
+ readonly correlationId?: string | undefined;
58
+ constructor(message: string, status: number, code: string, correlationId?: string | undefined);
59
+ }
60
+
61
+ /**
62
+ * Issuer-side client for the PAFI gateway's direct-auth endpoints. Use
63
+ * one instance per issuer backend — the constructor binds gateway URL +
64
+ * issuer id + signing credentials, methods just route to specific
65
+ * endpoints.
66
+ *
67
+ * Each method signs a fresh client_assertion (RFC 7523, 60s TTL) so
68
+ * a leaked one is useless beyond the window + replay-protected by jti.
69
+ *
70
+ * The gateway endpoints invoked here are owned by PAFI — the issuer
71
+ * never sees OTP codes, never holds OAuth client_secrets, never
72
+ * verifies signatures itself. Gateway is the sole authority; this
73
+ * client is purely a transport.
74
+ *
75
+ * Usage in a NestJS issuer backend:
76
+ *
77
+ * @Injectable()
78
+ * export class PafiAuthClientProvider {
79
+ * readonly client: PafiAuthClient;
80
+ * constructor(config: ConfigService) {
81
+ * this.client = new PafiAuthClient({
82
+ * gatewayUrl: config.getOrThrow('PAFI_GATEWAY_URL'),
83
+ * issuerId: config.getOrThrow('PAFI_GATEWAY_ISSUER_ID'),
84
+ * clientId: config.getOrThrow('PAFI_GATEWAY_CLIENT_ID'),
85
+ * clientPrivateJwk: JSON.parse(
86
+ * config.getOrThrow('PAFI_GATEWAY_CLIENT_PRIVATE_JWK_JSON'),
87
+ * ),
88
+ * });
89
+ * }
90
+ * }
91
+ */
92
+ declare class PafiAuthClient {
93
+ private readonly opts;
94
+ private readonly fetchImpl;
95
+ private readonly tokenExchangeAud;
96
+ constructor(opts: PafiAuthClientOptions);
97
+ /**
98
+ * Step 1: ask the gateway to send the user an OTP. Returns the
99
+ * `challengeId` to echo back on {@link verifyEmail}.
100
+ */
101
+ startEmail(args: {
102
+ email: string;
103
+ correlationId?: string;
104
+ }): Promise<EmailChallenge>;
105
+ /**
106
+ * Step 2: submit the OTP the user received. On success returns
107
+ * {@link AuthSuccess} containing BOTH the long-lived
108
+ * pafi_session_token (issuer verifies via gateway JWKS) AND the
109
+ * short-lived pafi_jwt (issuer FE feeds to Privy).
110
+ */
111
+ verifyEmail(args: {
112
+ challengeId: string;
113
+ otpCode: string;
114
+ correlationId?: string;
115
+ }): Promise<AuthSuccess>;
116
+ /**
117
+ * Hand the gateway an id_token the issuer FE obtained from Google
118
+ * Identity Services (using PAFI's shared client_id). Gateway verifies
119
+ * signature + audience + `email_verified` before resolving identity.
120
+ */
121
+ exchangeGoogle(args: {
122
+ idToken: string;
123
+ correlationId?: string;
124
+ }): Promise<AuthSuccess>;
125
+ /**
126
+ * Hand the gateway the authorization code returned by Kakao's
127
+ * redirect. Gateway exchanges with Kakao (server-to-server using
128
+ * PAFI's client_secret), verifies id_token, resolves identity.
129
+ *
130
+ * `redirectUri` must match the URL the FE used when starting the
131
+ * Kakao flow. Falls back to the gateway's KAKAO_REDIRECT_URI when
132
+ * omitted — pass an explicit value for multi-environment FEs.
133
+ */
134
+ exchangeKakao(args: {
135
+ code: string;
136
+ redirectUri?: string;
137
+ correlationId?: string;
138
+ }): Promise<AuthSuccess>;
139
+ private post;
140
+ }
141
+
142
+ /**
143
+ * Mint the RFC 7523 client_assertion JWT the gateway expects in the
144
+ * `Authorization: Bearer …` header of every direct-auth call.
145
+ *
146
+ * Claims:
147
+ * - iss / sub = clientId (RFC 7523 §3: same value for client auth)
148
+ * - aud = `${gatewayUrl}/v1/token-exchange` (exact endpoint URL)
149
+ * — NOTE the gateway also accepts this same audience
150
+ * for the direct-auth endpoints because they live on
151
+ * the same client-auth boundary. Single audience keeps
152
+ * one client_assertion reusable across all gateway
153
+ * endpoints for the duration of its short lifetime.
154
+ * - iat / exp = 60-second window (replay-protected by jti)
155
+ * - jti = random UUID
156
+ *
157
+ * 60-second lifetime is a deliberate trade-off: long enough to absorb
158
+ * clock skew + slow networks, short enough that a stolen assertion is
159
+ * usable only briefly. The gateway's per-jti replay cache means even
160
+ * within that window an assertion is single-use.
161
+ */
162
+ declare function signClientAssertion(args: {
163
+ gatewayUrl: string;
164
+ clientId: string;
165
+ privateJwk: JWK & {
166
+ kid: string;
167
+ };
168
+ alg?: string;
169
+ }): Promise<string>;
170
+
171
+ export { type AuthSuccess, type EmailChallenge, PafiAuthClient, type PafiAuthClientOptions, PafiAuthError, signClientAssertion };
@@ -0,0 +1,171 @@
1
+ import { JWK } from 'jose';
2
+
3
+ /**
4
+ * Constructor params for {@link PafiAuthClient}. One instance per
5
+ * issuer backend — wraps the issuer's gateway credentials (client_id +
6
+ * private JWK) plus the static config (gateway URL, audience).
7
+ */
8
+ interface PafiAuthClientOptions {
9
+ /** Base URL of the PAFI gateway (e.g. `https://id-dev.pacificfinance.org`). */
10
+ gatewayUrl: string;
11
+ /** Issuer identifier registered with the gateway (e.g. `gg56`). */
12
+ issuerId: string;
13
+ /**
14
+ * Gateway client_id assigned at issuer onboarding. Also acts as the
15
+ * `iss`/`sub` of the client_assertion JWT (RFC 7523 §3).
16
+ */
17
+ clientId: string;
18
+ /**
19
+ * Private JWK the issuer uses to sign client_assertion. MUST include
20
+ * `kid` — gateway looks up the matching public JWK by kid.
21
+ */
22
+ clientPrivateJwk: JWK & {
23
+ kid: string;
24
+ };
25
+ /**
26
+ * Optional fetch override — useful for tests / Node env without
27
+ * global fetch (Node ≥ 18 has it built-in).
28
+ */
29
+ fetchImpl?: typeof fetch;
30
+ /**
31
+ * Optional algorithm override for the client assertion JWT. Default
32
+ * `ES256`. Must match what the gateway expects for this client.
33
+ */
34
+ alg?: "ES256" | "ES384" | "RS256" | "RS384" | "RS512" | "EdDSA";
35
+ }
36
+ interface AuthSuccess {
37
+ pafiSessionToken: string;
38
+ pafiJwt: string;
39
+ canonicalId: string;
40
+ expiresAt: number;
41
+ isFirstLogin: boolean;
42
+ verifiedEmail?: string;
43
+ }
44
+ interface EmailChallenge {
45
+ challengeId: string;
46
+ expiresInSec: number;
47
+ }
48
+ /**
49
+ * Thrown when the gateway rejects the call. `code` is the gateway's
50
+ * structured `error` field (e.g. `invalid_otp`, `too_many_attempts`,
51
+ * `expired`, `email_not_verified`) — issuers can branch on it to drive
52
+ * UX (e.g. show "Resend code" button on `expired`).
53
+ */
54
+ declare class PafiAuthError extends Error {
55
+ readonly status: number;
56
+ readonly code: string;
57
+ readonly correlationId?: string | undefined;
58
+ constructor(message: string, status: number, code: string, correlationId?: string | undefined);
59
+ }
60
+
61
+ /**
62
+ * Issuer-side client for the PAFI gateway's direct-auth endpoints. Use
63
+ * one instance per issuer backend — the constructor binds gateway URL +
64
+ * issuer id + signing credentials, methods just route to specific
65
+ * endpoints.
66
+ *
67
+ * Each method signs a fresh client_assertion (RFC 7523, 60s TTL) so
68
+ * a leaked one is useless beyond the window + replay-protected by jti.
69
+ *
70
+ * The gateway endpoints invoked here are owned by PAFI — the issuer
71
+ * never sees OTP codes, never holds OAuth client_secrets, never
72
+ * verifies signatures itself. Gateway is the sole authority; this
73
+ * client is purely a transport.
74
+ *
75
+ * Usage in a NestJS issuer backend:
76
+ *
77
+ * @Injectable()
78
+ * export class PafiAuthClientProvider {
79
+ * readonly client: PafiAuthClient;
80
+ * constructor(config: ConfigService) {
81
+ * this.client = new PafiAuthClient({
82
+ * gatewayUrl: config.getOrThrow('PAFI_GATEWAY_URL'),
83
+ * issuerId: config.getOrThrow('PAFI_GATEWAY_ISSUER_ID'),
84
+ * clientId: config.getOrThrow('PAFI_GATEWAY_CLIENT_ID'),
85
+ * clientPrivateJwk: JSON.parse(
86
+ * config.getOrThrow('PAFI_GATEWAY_CLIENT_PRIVATE_JWK_JSON'),
87
+ * ),
88
+ * });
89
+ * }
90
+ * }
91
+ */
92
+ declare class PafiAuthClient {
93
+ private readonly opts;
94
+ private readonly fetchImpl;
95
+ private readonly tokenExchangeAud;
96
+ constructor(opts: PafiAuthClientOptions);
97
+ /**
98
+ * Step 1: ask the gateway to send the user an OTP. Returns the
99
+ * `challengeId` to echo back on {@link verifyEmail}.
100
+ */
101
+ startEmail(args: {
102
+ email: string;
103
+ correlationId?: string;
104
+ }): Promise<EmailChallenge>;
105
+ /**
106
+ * Step 2: submit the OTP the user received. On success returns
107
+ * {@link AuthSuccess} containing BOTH the long-lived
108
+ * pafi_session_token (issuer verifies via gateway JWKS) AND the
109
+ * short-lived pafi_jwt (issuer FE feeds to Privy).
110
+ */
111
+ verifyEmail(args: {
112
+ challengeId: string;
113
+ otpCode: string;
114
+ correlationId?: string;
115
+ }): Promise<AuthSuccess>;
116
+ /**
117
+ * Hand the gateway an id_token the issuer FE obtained from Google
118
+ * Identity Services (using PAFI's shared client_id). Gateway verifies
119
+ * signature + audience + `email_verified` before resolving identity.
120
+ */
121
+ exchangeGoogle(args: {
122
+ idToken: string;
123
+ correlationId?: string;
124
+ }): Promise<AuthSuccess>;
125
+ /**
126
+ * Hand the gateway the authorization code returned by Kakao's
127
+ * redirect. Gateway exchanges with Kakao (server-to-server using
128
+ * PAFI's client_secret), verifies id_token, resolves identity.
129
+ *
130
+ * `redirectUri` must match the URL the FE used when starting the
131
+ * Kakao flow. Falls back to the gateway's KAKAO_REDIRECT_URI when
132
+ * omitted — pass an explicit value for multi-environment FEs.
133
+ */
134
+ exchangeKakao(args: {
135
+ code: string;
136
+ redirectUri?: string;
137
+ correlationId?: string;
138
+ }): Promise<AuthSuccess>;
139
+ private post;
140
+ }
141
+
142
+ /**
143
+ * Mint the RFC 7523 client_assertion JWT the gateway expects in the
144
+ * `Authorization: Bearer …` header of every direct-auth call.
145
+ *
146
+ * Claims:
147
+ * - iss / sub = clientId (RFC 7523 §3: same value for client auth)
148
+ * - aud = `${gatewayUrl}/v1/token-exchange` (exact endpoint URL)
149
+ * — NOTE the gateway also accepts this same audience
150
+ * for the direct-auth endpoints because they live on
151
+ * the same client-auth boundary. Single audience keeps
152
+ * one client_assertion reusable across all gateway
153
+ * endpoints for the duration of its short lifetime.
154
+ * - iat / exp = 60-second window (replay-protected by jti)
155
+ * - jti = random UUID
156
+ *
157
+ * 60-second lifetime is a deliberate trade-off: long enough to absorb
158
+ * clock skew + slow networks, short enough that a stolen assertion is
159
+ * usable only briefly. The gateway's per-jti replay cache means even
160
+ * within that window an assertion is single-use.
161
+ */
162
+ declare function signClientAssertion(args: {
163
+ gatewayUrl: string;
164
+ clientId: string;
165
+ privateJwk: JWK & {
166
+ kid: string;
167
+ };
168
+ alg?: string;
169
+ }): Promise<string>;
170
+
171
+ export { type AuthSuccess, type EmailChallenge, PafiAuthClient, type PafiAuthClientOptions, PafiAuthError, signClientAssertion };
@@ -0,0 +1,183 @@
1
+ import "../chunk-BRKEJJFQ.js";
2
+
3
+ // src/auth-client/pafi-auth-client.ts
4
+ import { randomUUID as randomUUID2 } from "crypto";
5
+
6
+ // src/auth-client/sign-client-assertion.ts
7
+ import { importJWK, SignJWT } from "jose";
8
+ import { randomUUID } from "crypto";
9
+ async function signClientAssertion(args) {
10
+ const alg = args.alg ?? args.privateJwk.alg ?? "ES256";
11
+ const key = await importJWK(args.privateJwk, alg);
12
+ const now = Math.floor(Date.now() / 1e3);
13
+ 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);
14
+ }
15
+
16
+ // src/auth-client/types.ts
17
+ var PafiAuthError = class extends Error {
18
+ constructor(message, status, code, correlationId) {
19
+ super(message);
20
+ this.status = status;
21
+ this.code = code;
22
+ this.correlationId = correlationId;
23
+ this.name = "PafiAuthError";
24
+ }
25
+ status;
26
+ code;
27
+ correlationId;
28
+ };
29
+
30
+ // src/auth-client/pafi-auth-client.ts
31
+ var PafiAuthClient = class {
32
+ constructor(opts) {
33
+ this.opts = opts;
34
+ if (!opts.clientPrivateJwk.kid) {
35
+ throw new Error(
36
+ "PafiAuthClient: clientPrivateJwk.kid is required (gateway uses kid to look up the verification key)"
37
+ );
38
+ }
39
+ this.fetchImpl = opts.fetchImpl ?? fetch;
40
+ this.tokenExchangeAud = `${opts.gatewayUrl}/v1/token-exchange`;
41
+ }
42
+ opts;
43
+ fetchImpl;
44
+ tokenExchangeAud;
45
+ // ───────────────────────────────────────────────────────────────
46
+ // EMAIL OTP — 2-step
47
+ // ───────────────────────────────────────────────────────────────
48
+ /**
49
+ * Step 1: ask the gateway to send the user an OTP. Returns the
50
+ * `challengeId` to echo back on {@link verifyEmail}.
51
+ */
52
+ async startEmail(args) {
53
+ const res = await this.post(
54
+ "/v1/auth/email/start",
55
+ {
56
+ issuer_id: this.opts.issuerId,
57
+ email: args.email
58
+ },
59
+ args.correlationId
60
+ );
61
+ return {
62
+ challengeId: res.challenge_id,
63
+ expiresInSec: res.expires_in
64
+ };
65
+ }
66
+ /**
67
+ * Step 2: submit the OTP the user received. On success returns
68
+ * {@link AuthSuccess} containing BOTH the long-lived
69
+ * pafi_session_token (issuer verifies via gateway JWKS) AND the
70
+ * short-lived pafi_jwt (issuer FE feeds to Privy).
71
+ */
72
+ async verifyEmail(args) {
73
+ const res = await this.post(
74
+ "/v1/auth/email/verify",
75
+ {
76
+ challenge_id: args.challengeId,
77
+ otp_code: args.otpCode
78
+ },
79
+ args.correlationId
80
+ );
81
+ return mapAuthSuccess(res);
82
+ }
83
+ // ───────────────────────────────────────────────────────────────
84
+ // GOOGLE — 1-step exchange
85
+ // ───────────────────────────────────────────────────────────────
86
+ /**
87
+ * Hand the gateway an id_token the issuer FE obtained from Google
88
+ * Identity Services (using PAFI's shared client_id). Gateway verifies
89
+ * signature + audience + `email_verified` before resolving identity.
90
+ */
91
+ async exchangeGoogle(args) {
92
+ const res = await this.post(
93
+ "/v1/auth/google/exchange",
94
+ {
95
+ issuer_id: this.opts.issuerId,
96
+ id_token: args.idToken
97
+ },
98
+ args.correlationId
99
+ );
100
+ return mapAuthSuccess(res);
101
+ }
102
+ // ───────────────────────────────────────────────────────────────
103
+ // KAKAO — 1-step exchange (authorization code)
104
+ // ───────────────────────────────────────────────────────────────
105
+ /**
106
+ * Hand the gateway the authorization code returned by Kakao's
107
+ * redirect. Gateway exchanges with Kakao (server-to-server using
108
+ * PAFI's client_secret), verifies id_token, resolves identity.
109
+ *
110
+ * `redirectUri` must match the URL the FE used when starting the
111
+ * Kakao flow. Falls back to the gateway's KAKAO_REDIRECT_URI when
112
+ * omitted — pass an explicit value for multi-environment FEs.
113
+ */
114
+ async exchangeKakao(args) {
115
+ const res = await this.post(
116
+ "/v1/auth/kakao/exchange",
117
+ {
118
+ issuer_id: this.opts.issuerId,
119
+ code: args.code,
120
+ ...args.redirectUri ? { redirect_uri: args.redirectUri } : {}
121
+ },
122
+ args.correlationId
123
+ );
124
+ return mapAuthSuccess(res);
125
+ }
126
+ // ───────────────────────────────────────────────────────────────
127
+ async post(path, body, correlationId) {
128
+ const assertion = await signClientAssertion({
129
+ gatewayUrl: this.opts.gatewayUrl,
130
+ clientId: this.opts.clientId,
131
+ privateJwk: this.opts.clientPrivateJwk,
132
+ alg: this.opts.alg
133
+ });
134
+ const finalCorrelationId = correlationId ?? `iss-${randomUUID2()}`;
135
+ const res = await this.fetchImpl(`${this.opts.gatewayUrl}${path}`, {
136
+ method: "POST",
137
+ headers: {
138
+ Authorization: `Bearer ${assertion}`,
139
+ "Content-Type": "application/json",
140
+ "X-Correlation-Id": finalCorrelationId
141
+ },
142
+ body: JSON.stringify(body)
143
+ });
144
+ const text = await res.text();
145
+ let parsed;
146
+ try {
147
+ parsed = text ? JSON.parse(text) : {};
148
+ } catch {
149
+ throw new PafiAuthError(
150
+ `Non-JSON response from gateway (${path}): ${text.slice(0, 120)}`,
151
+ res.status,
152
+ "non_json_response",
153
+ finalCorrelationId
154
+ );
155
+ }
156
+ if (!res.ok) {
157
+ const err = parsed;
158
+ throw new PafiAuthError(
159
+ err.error_description ?? err.error ?? `Gateway returned HTTP ${res.status}`,
160
+ res.status,
161
+ err.error ?? "unknown_error",
162
+ err.correlation_id ?? finalCorrelationId
163
+ );
164
+ }
165
+ return parsed;
166
+ }
167
+ };
168
+ function mapAuthSuccess(res) {
169
+ return {
170
+ pafiSessionToken: res.pafi_session_token,
171
+ pafiJwt: res.pafi_jwt,
172
+ canonicalId: res.canonical_id,
173
+ expiresAt: res.expires_at,
174
+ isFirstLogin: res.is_first_login,
175
+ ...res.verified_email ? { verifiedEmail: res.verified_email } : {}
176
+ };
177
+ }
178
+ export {
179
+ PafiAuthClient,
180
+ PafiAuthError,
181
+ signClientAssertion
182
+ };
183
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth-client/pafi-auth-client.ts","../../src/auth-client/sign-client-assertion.ts","../../src/auth-client/types.ts"],"sourcesContent":["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":["randomUUID","randomUUID"]}
package/dist/index.cjs CHANGED
@@ -5421,7 +5421,7 @@ var MemoryRedemptionHistoryStore = class {
5421
5421
  };
5422
5422
 
5423
5423
  // src/index.ts
5424
- var PAFI_ISSUER_SDK_VERSION = true ? "0.35.1" : "dev";
5424
+ var PAFI_ISSUER_SDK_VERSION = true ? "0.38.0" : "dev";
5425
5425
  // Annotate the CommonJS export names for ESM import in node:
5426
5426
  0 && (module.exports = {
5427
5427
  AdapterMisconfiguredError,
package/dist/index.js CHANGED
@@ -5237,7 +5237,7 @@ var MemoryRedemptionHistoryStore = class {
5237
5237
  };
5238
5238
 
5239
5239
  // src/index.ts
5240
- var PAFI_ISSUER_SDK_VERSION = true ? "0.35.1" : "dev";
5240
+ var PAFI_ISSUER_SDK_VERSION = true ? "0.38.0" : "dev";
5241
5241
  export {
5242
5242
  AdapterMisconfiguredError,
5243
5243
  AuthError,
@@ -1,6 +1,6 @@
1
1
  import { ExceptionFilter, ArgumentsHost, DynamicModule, FactoryProvider } from '@nestjs/common';
2
2
  import { PafiErrorEnvelope } from '../http/index.cjs';
3
- import { a as IssuerPublicJwk } from '../types-CxVXRHLy.cjs';
3
+ import { b as IssuerPublicJwk } from '../types-DPqLTJk-.cjs';
4
4
  import '@pafi-dev/core';
5
5
  import 'jose';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { ExceptionFilter, ArgumentsHost, DynamicModule, FactoryProvider } from '@nestjs/common';
2
2
  import { PafiErrorEnvelope } from '../http/index.js';
3
- import { a as IssuerPublicJwk } from '../types-CxVXRHLy.js';
3
+ import { b as IssuerPublicJwk } from '../types-DPqLTJk-.js';
4
4
  import '@pafi-dev/core';
5
5
  import 'jose';
6
6
 
@@ -23,6 +23,31 @@ interface IssuerPrivateJwk extends JWK {
23
23
  kid: string;
24
24
  alg?: string;
25
25
  }
26
+ /**
27
+ * Strong identifier the issuer has verified the user owns at the moment
28
+ * of this login. Drives cross-issuer wallet unification in the gateway:
29
+ * `canonical_pafi_user_id = HMAC(pepper, type + ":" + normalized_value)`,
30
+ * so the same email or wallet address used at two different issuers
31
+ * resolves to the same Privy embedded wallet.
32
+ *
33
+ * Issuers MUST only attach an attribute they have actually verified
34
+ * (email OTP confirmed, SIWE signature accepted, OAuth provider returned
35
+ * `email_verified=true`, etc.). The gateway trusts the issuer's claim —
36
+ * attaching unverified values lets users hijack wallets.
37
+ *
38
+ * Normalization rules (gateway enforces, but issuers should mirror to
39
+ * avoid surprising hash mismatches):
40
+ * - `email`: lowercase + trim. No Gmail dot-strip, no plus-tag strip.
41
+ * - `eth_wallet`: lowercase 0x-prefixed 20-byte hex.
42
+ *
43
+ * Omit entirely on logins where no strong attribute is verifiable —
44
+ * gateway falls back to legacy isolated-per-issuer canonical_id.
45
+ */
46
+ type AuthAttributeType = "email" | "eth_wallet";
47
+ interface AuthAttribute {
48
+ type: AuthAttributeType;
49
+ value: string;
50
+ }
26
51
  /**
27
52
  * Parameters for minting an issuer JWT to send to the PAFI gateway.
28
53
  */
@@ -54,11 +79,20 @@ interface SignIssuerJwtParams {
54
79
  * uses this for replay protection — must be unique per request.
55
80
  */
56
81
  jti?: string;
82
+ /**
83
+ * Optional strong identifier the user proved ownership of during this
84
+ * login. Same attribute across issuers = same wallet. See
85
+ * {@link AuthAttribute}. Omit for SIWE-pure issuers, OAuth providers
86
+ * that don't expose verified contacts, etc. — gateway then falls back
87
+ * to legacy per-issuer canonical_id isolation for this login.
88
+ */
89
+ authAttribute?: AuthAttribute;
57
90
  /**
58
91
  * Optional additional claims merged into the payload. Use sparingly —
59
- * PAFI gateway only reads `iss`, `sub`, `aud`, `iat`, `exp`, `jti`.
92
+ * PAFI gateway only reads `iss`, `sub`, `aud`, `iat`, `exp`, `jti`,
93
+ * and `auth_attribute`.
60
94
  */
61
95
  extra?: Record<string, unknown>;
62
96
  }
63
97
 
64
- export type { IssuerPrivateJwk as I, SignIssuerJwtParams as S, IssuerPublicJwk as a };
98
+ export type { AuthAttribute as A, IssuerPrivateJwk as I, SignIssuerJwtParams as S, AuthAttributeType as a, IssuerPublicJwk as b };
@@ -23,6 +23,31 @@ interface IssuerPrivateJwk extends JWK {
23
23
  kid: string;
24
24
  alg?: string;
25
25
  }
26
+ /**
27
+ * Strong identifier the issuer has verified the user owns at the moment
28
+ * of this login. Drives cross-issuer wallet unification in the gateway:
29
+ * `canonical_pafi_user_id = HMAC(pepper, type + ":" + normalized_value)`,
30
+ * so the same email or wallet address used at two different issuers
31
+ * resolves to the same Privy embedded wallet.
32
+ *
33
+ * Issuers MUST only attach an attribute they have actually verified
34
+ * (email OTP confirmed, SIWE signature accepted, OAuth provider returned
35
+ * `email_verified=true`, etc.). The gateway trusts the issuer's claim —
36
+ * attaching unverified values lets users hijack wallets.
37
+ *
38
+ * Normalization rules (gateway enforces, but issuers should mirror to
39
+ * avoid surprising hash mismatches):
40
+ * - `email`: lowercase + trim. No Gmail dot-strip, no plus-tag strip.
41
+ * - `eth_wallet`: lowercase 0x-prefixed 20-byte hex.
42
+ *
43
+ * Omit entirely on logins where no strong attribute is verifiable —
44
+ * gateway falls back to legacy isolated-per-issuer canonical_id.
45
+ */
46
+ type AuthAttributeType = "email" | "eth_wallet";
47
+ interface AuthAttribute {
48
+ type: AuthAttributeType;
49
+ value: string;
50
+ }
26
51
  /**
27
52
  * Parameters for minting an issuer JWT to send to the PAFI gateway.
28
53
  */
@@ -54,11 +79,20 @@ interface SignIssuerJwtParams {
54
79
  * uses this for replay protection — must be unique per request.
55
80
  */
56
81
  jti?: string;
82
+ /**
83
+ * Optional strong identifier the user proved ownership of during this
84
+ * login. Same attribute across issuers = same wallet. See
85
+ * {@link AuthAttribute}. Omit for SIWE-pure issuers, OAuth providers
86
+ * that don't expose verified contacts, etc. — gateway then falls back
87
+ * to legacy per-issuer canonical_id isolation for this login.
88
+ */
89
+ authAttribute?: AuthAttribute;
57
90
  /**
58
91
  * Optional additional claims merged into the payload. Use sparingly —
59
- * PAFI gateway only reads `iss`, `sub`, `aud`, `iat`, `exp`, `jti`.
92
+ * PAFI gateway only reads `iss`, `sub`, `aud`, `iat`, `exp`, `jti`,
93
+ * and `auth_attribute`.
60
94
  */
61
95
  extra?: Record<string, unknown>;
62
96
  }
63
97
 
64
- export type { IssuerPrivateJwk as I, SignIssuerJwtParams as S, IssuerPublicJwk as a };
98
+ export type { AuthAttribute as A, IssuerPrivateJwk as I, SignIssuerJwtParams as S, AuthAttributeType as a, IssuerPublicJwk as b };
@@ -38,7 +38,14 @@ async function signIssuerJwt(params) {
38
38
  const key = await (0, import_jose.importJWK)(params.privateJwk, alg);
39
39
  const now = Math.floor(Date.now() / 1e3);
40
40
  const lifetime = params.expiresInSec ?? 60;
41
- const builder = new import_jose.SignJWT({ ...params.extra ?? {} }).setProtectedHeader({ alg, typ: "JWT", kid }).setIssuer(params.iss).setSubject(params.sub).setAudience(params.aud).setIssuedAt(now).setExpirationTime(now + lifetime).setJti(params.jti ?? (0, import_node_crypto.randomUUID)());
41
+ const payload = { ...params.extra ?? {} };
42
+ if (params.authAttribute) {
43
+ payload.auth_attribute = {
44
+ type: params.authAttribute.type,
45
+ value: params.authAttribute.value
46
+ };
47
+ }
48
+ const builder = new import_jose.SignJWT(payload).setProtectedHeader({ alg, typ: "JWT", kid }).setIssuer(params.iss).setSubject(params.sub).setAudience(params.aud).setIssuedAt(now).setExpirationTime(now + lifetime).setJti(params.jti ?? (0, import_node_crypto.randomUUID)());
42
49
  return builder.sign(key);
43
50
  }
44
51
  // Annotate the CommonJS export names for ESM import in node:
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/wallet-auth/index.ts","../../src/wallet-auth/signIssuerJwt.ts"],"sourcesContent":["/**\n * Wallet-auth helpers — for issuer backends that integrate with the\n * PAFI Wallet Auth Gateway.\n *\n * Sub-modules:\n * - signIssuerJwt : mint an issuer JWT to send to the gateway\n * - types : IssuerPublicJwk, IssuerPrivateJwk, SignIssuerJwtParams\n *\n * NestJS-specific helpers (drop-in JWKS publishing controller) live\n * under `@pafi-dev/issuer/nestjs`.\n */\n\nexport { signIssuerJwt } from \"./signIssuerJwt\";\nexport type {\n IssuerPublicJwk,\n IssuerPrivateJwk,\n SignIssuerJwtParams,\n} from \"./types\";\n","import { importJWK, SignJWT } from \"jose\";\nimport { randomUUID } from \"node:crypto\";\nimport type { SignIssuerJwtParams } from \"./types\";\n\n/**\n * Mint an issuer JWT for the PAFI Wallet Auth Gateway.\n *\n * Returns a compact JWT (header.payload.signature) ready to send as\n * the `issuer_jwt` body field in `POST /v1/token-exchange`.\n *\n * The matching public key must be reachable at the issuer's\n * `/.well-known/jwks.json` (see `WalletAuthJwksModule`) so the\n * gateway can verify the signature.\n *\n * @example\n * import { signIssuerJwt } from '@pafi-dev/issuer/wallet-auth';\n *\n * const jwt = await signIssuerJwt({\n * privateJwk: JSON.parse(process.env.ISSUER_PRIVATE_JWK_JSON!),\n * iss: 'https://gg56.com',\n * sub: 'gg56_user_99',\n * aud: 'pafi-gateway-prod',\n * });\n *\n * // POST { issuer_id: 'GG56', issuer_jwt: jwt } to gateway\n */\nexport async function signIssuerJwt(\n params: SignIssuerJwtParams,\n): Promise<string> {\n const alg = params.alg ?? params.privateJwk.alg ?? \"ES256\";\n const kid = params.privateJwk.kid;\n\n if (!kid) {\n throw new Error(\n \"signIssuerJwt: privateJwk.kid is required (gateway uses kid to look up the verification key)\",\n );\n }\n\n const key = await importJWK(params.privateJwk, alg);\n const now = Math.floor(Date.now() / 1000);\n const lifetime = params.expiresInSec ?? 60;\n\n const builder = new SignJWT({ ...(params.extra ?? {}) })\n .setProtectedHeader({ alg, typ: \"JWT\", kid })\n .setIssuer(params.iss)\n .setSubject(params.sub)\n .setAudience(params.aud)\n .setIssuedAt(now)\n .setExpirationTime(now + lifetime)\n .setJti(params.jti ?? randomUUID());\n\n return builder.sign(key);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAAmC;AACnC,yBAA2B;AAyB3B,eAAsB,cACpB,QACiB;AACjB,QAAM,MAAM,OAAO,OAAO,OAAO,WAAW,OAAO;AACnD,QAAM,MAAM,OAAO,WAAW;AAE9B,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,UAAM,uBAAU,OAAO,YAAY,GAAG;AAClD,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,WAAW,OAAO,gBAAgB;AAExC,QAAM,UAAU,IAAI,oBAAQ,EAAE,GAAI,OAAO,SAAS,CAAC,EAAG,CAAC,EACpD,mBAAmB,EAAE,KAAK,KAAK,OAAO,IAAI,CAAC,EAC3C,UAAU,OAAO,GAAG,EACpB,WAAW,OAAO,GAAG,EACrB,YAAY,OAAO,GAAG,EACtB,YAAY,GAAG,EACf,kBAAkB,MAAM,QAAQ,EAChC,OAAO,OAAO,WAAO,+BAAW,CAAC;AAEpC,SAAO,QAAQ,KAAK,GAAG;AACzB;","names":[]}
1
+ {"version":3,"sources":["../../src/wallet-auth/index.ts","../../src/wallet-auth/signIssuerJwt.ts"],"sourcesContent":["/**\n * Wallet-auth helpers — for issuer backends that integrate with the\n * PAFI Wallet Auth Gateway.\n *\n * Sub-modules:\n * - signIssuerJwt : mint an issuer JWT to send to the gateway\n * - types : IssuerPublicJwk, IssuerPrivateJwk, SignIssuerJwtParams\n *\n * NestJS-specific helpers (drop-in JWKS publishing controller) live\n * under `@pafi-dev/issuer/nestjs`.\n */\n\nexport { signIssuerJwt } from \"./signIssuerJwt\";\nexport type {\n AuthAttribute,\n AuthAttributeType,\n IssuerPublicJwk,\n IssuerPrivateJwk,\n SignIssuerJwtParams,\n} from \"./types\";\n","import { importJWK, SignJWT } from \"jose\";\nimport { randomUUID } from \"node:crypto\";\nimport type { SignIssuerJwtParams } from \"./types\";\n\n/**\n * Mint an issuer JWT for the PAFI Wallet Auth Gateway.\n *\n * Returns a compact JWT (header.payload.signature) ready to send as\n * the `issuer_jwt` body field in `POST /v1/token-exchange`.\n *\n * The matching public key must be reachable at the issuer's\n * `/.well-known/jwks.json` (see `WalletAuthJwksModule`) so the\n * gateway can verify the signature.\n *\n * @example\n * import { signIssuerJwt } from '@pafi-dev/issuer/wallet-auth';\n *\n * const jwt = await signIssuerJwt({\n * privateJwk: JSON.parse(process.env.ISSUER_PRIVATE_JWK_JSON!),\n * iss: 'https://gg56.com',\n * sub: 'gg56_user_99',\n * aud: 'pafi-gateway-prod',\n * });\n *\n * // POST { issuer_id: 'GG56', issuer_jwt: jwt } to gateway\n */\nexport async function signIssuerJwt(\n params: SignIssuerJwtParams,\n): Promise<string> {\n const alg = params.alg ?? params.privateJwk.alg ?? \"ES256\";\n const kid = params.privateJwk.kid;\n\n if (!kid) {\n throw new Error(\n \"signIssuerJwt: privateJwk.kid is required (gateway uses kid to look up the verification key)\",\n );\n }\n\n const key = await importJWK(params.privateJwk, alg);\n const now = Math.floor(Date.now() / 1000);\n const lifetime = params.expiresInSec ?? 60;\n\n const payload: Record<string, unknown> = { ...(params.extra ?? {}) };\n if (params.authAttribute) {\n payload.auth_attribute = {\n type: params.authAttribute.type,\n value: params.authAttribute.value,\n };\n }\n\n const builder = new SignJWT(payload)\n .setProtectedHeader({ alg, typ: \"JWT\", kid })\n .setIssuer(params.iss)\n .setSubject(params.sub)\n .setAudience(params.aud)\n .setIssuedAt(now)\n .setExpirationTime(now + lifetime)\n .setJti(params.jti ?? randomUUID());\n\n return builder.sign(key);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAAmC;AACnC,yBAA2B;AAyB3B,eAAsB,cACpB,QACiB;AACjB,QAAM,MAAM,OAAO,OAAO,OAAO,WAAW,OAAO;AACnD,QAAM,MAAM,OAAO,WAAW;AAE9B,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,UAAM,uBAAU,OAAO,YAAY,GAAG;AAClD,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,WAAW,OAAO,gBAAgB;AAExC,QAAM,UAAmC,EAAE,GAAI,OAAO,SAAS,CAAC,EAAG;AACnE,MAAI,OAAO,eAAe;AACxB,YAAQ,iBAAiB;AAAA,MACvB,MAAM,OAAO,cAAc;AAAA,MAC3B,OAAO,OAAO,cAAc;AAAA,IAC9B;AAAA,EACF;AAEA,QAAM,UAAU,IAAI,oBAAQ,OAAO,EAChC,mBAAmB,EAAE,KAAK,KAAK,OAAO,IAAI,CAAC,EAC3C,UAAU,OAAO,GAAG,EACpB,WAAW,OAAO,GAAG,EACrB,YAAY,OAAO,GAAG,EACtB,YAAY,GAAG,EACf,kBAAkB,MAAM,QAAQ,EAChC,OAAO,OAAO,WAAO,+BAAW,CAAC;AAEpC,SAAO,QAAQ,KAAK,GAAG;AACzB;","names":[]}
@@ -1,5 +1,5 @@
1
- import { S as SignIssuerJwtParams } from '../types-CxVXRHLy.cjs';
2
- export { I as IssuerPrivateJwk, a as IssuerPublicJwk } from '../types-CxVXRHLy.cjs';
1
+ import { S as SignIssuerJwtParams } from '../types-DPqLTJk-.cjs';
2
+ export { A as AuthAttribute, a as AuthAttributeType, I as IssuerPrivateJwk, b as IssuerPublicJwk } from '../types-DPqLTJk-.cjs';
3
3
  import 'jose';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { S as SignIssuerJwtParams } from '../types-CxVXRHLy.js';
2
- export { I as IssuerPrivateJwk, a as IssuerPublicJwk } from '../types-CxVXRHLy.js';
1
+ import { S as SignIssuerJwtParams } from '../types-DPqLTJk-.js';
2
+ export { A as AuthAttribute, a as AuthAttributeType, I as IssuerPrivateJwk, b as IssuerPublicJwk } from '../types-DPqLTJk-.js';
3
3
  import 'jose';
4
4
 
5
5
  /**
@@ -14,7 +14,14 @@ async function signIssuerJwt(params) {
14
14
  const key = await importJWK(params.privateJwk, alg);
15
15
  const now = Math.floor(Date.now() / 1e3);
16
16
  const lifetime = params.expiresInSec ?? 60;
17
- const builder = new SignJWT({ ...params.extra ?? {} }).setProtectedHeader({ alg, typ: "JWT", kid }).setIssuer(params.iss).setSubject(params.sub).setAudience(params.aud).setIssuedAt(now).setExpirationTime(now + lifetime).setJti(params.jti ?? randomUUID());
17
+ const payload = { ...params.extra ?? {} };
18
+ if (params.authAttribute) {
19
+ payload.auth_attribute = {
20
+ type: params.authAttribute.type,
21
+ value: params.authAttribute.value
22
+ };
23
+ }
24
+ const builder = new SignJWT(payload).setProtectedHeader({ alg, typ: "JWT", kid }).setIssuer(params.iss).setSubject(params.sub).setAudience(params.aud).setIssuedAt(now).setExpirationTime(now + lifetime).setJti(params.jti ?? randomUUID());
18
25
  return builder.sign(key);
19
26
  }
20
27
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/wallet-auth/signIssuerJwt.ts"],"sourcesContent":["import { importJWK, SignJWT } from \"jose\";\nimport { randomUUID } from \"node:crypto\";\nimport type { SignIssuerJwtParams } from \"./types\";\n\n/**\n * Mint an issuer JWT for the PAFI Wallet Auth Gateway.\n *\n * Returns a compact JWT (header.payload.signature) ready to send as\n * the `issuer_jwt` body field in `POST /v1/token-exchange`.\n *\n * The matching public key must be reachable at the issuer's\n * `/.well-known/jwks.json` (see `WalletAuthJwksModule`) so the\n * gateway can verify the signature.\n *\n * @example\n * import { signIssuerJwt } from '@pafi-dev/issuer/wallet-auth';\n *\n * const jwt = await signIssuerJwt({\n * privateJwk: JSON.parse(process.env.ISSUER_PRIVATE_JWK_JSON!),\n * iss: 'https://gg56.com',\n * sub: 'gg56_user_99',\n * aud: 'pafi-gateway-prod',\n * });\n *\n * // POST { issuer_id: 'GG56', issuer_jwt: jwt } to gateway\n */\nexport async function signIssuerJwt(\n params: SignIssuerJwtParams,\n): Promise<string> {\n const alg = params.alg ?? params.privateJwk.alg ?? \"ES256\";\n const kid = params.privateJwk.kid;\n\n if (!kid) {\n throw new Error(\n \"signIssuerJwt: privateJwk.kid is required (gateway uses kid to look up the verification key)\",\n );\n }\n\n const key = await importJWK(params.privateJwk, alg);\n const now = Math.floor(Date.now() / 1000);\n const lifetime = params.expiresInSec ?? 60;\n\n const builder = new SignJWT({ ...(params.extra ?? {}) })\n .setProtectedHeader({ alg, typ: \"JWT\", kid })\n .setIssuer(params.iss)\n .setSubject(params.sub)\n .setAudience(params.aud)\n .setIssuedAt(now)\n .setExpirationTime(now + lifetime)\n .setJti(params.jti ?? randomUUID());\n\n return builder.sign(key);\n}\n"],"mappings":";;;AAAA,SAAS,WAAW,eAAe;AACnC,SAAS,kBAAkB;AAyB3B,eAAsB,cACpB,QACiB;AACjB,QAAM,MAAM,OAAO,OAAO,OAAO,WAAW,OAAO;AACnD,QAAM,MAAM,OAAO,WAAW;AAE9B,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,MAAM,UAAU,OAAO,YAAY,GAAG;AAClD,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,WAAW,OAAO,gBAAgB;AAExC,QAAM,UAAU,IAAI,QAAQ,EAAE,GAAI,OAAO,SAAS,CAAC,EAAG,CAAC,EACpD,mBAAmB,EAAE,KAAK,KAAK,OAAO,IAAI,CAAC,EAC3C,UAAU,OAAO,GAAG,EACpB,WAAW,OAAO,GAAG,EACrB,YAAY,OAAO,GAAG,EACtB,YAAY,GAAG,EACf,kBAAkB,MAAM,QAAQ,EAChC,OAAO,OAAO,OAAO,WAAW,CAAC;AAEpC,SAAO,QAAQ,KAAK,GAAG;AACzB;","names":[]}
1
+ {"version":3,"sources":["../../src/wallet-auth/signIssuerJwt.ts"],"sourcesContent":["import { importJWK, SignJWT } from \"jose\";\nimport { randomUUID } from \"node:crypto\";\nimport type { SignIssuerJwtParams } from \"./types\";\n\n/**\n * Mint an issuer JWT for the PAFI Wallet Auth Gateway.\n *\n * Returns a compact JWT (header.payload.signature) ready to send as\n * the `issuer_jwt` body field in `POST /v1/token-exchange`.\n *\n * The matching public key must be reachable at the issuer's\n * `/.well-known/jwks.json` (see `WalletAuthJwksModule`) so the\n * gateway can verify the signature.\n *\n * @example\n * import { signIssuerJwt } from '@pafi-dev/issuer/wallet-auth';\n *\n * const jwt = await signIssuerJwt({\n * privateJwk: JSON.parse(process.env.ISSUER_PRIVATE_JWK_JSON!),\n * iss: 'https://gg56.com',\n * sub: 'gg56_user_99',\n * aud: 'pafi-gateway-prod',\n * });\n *\n * // POST { issuer_id: 'GG56', issuer_jwt: jwt } to gateway\n */\nexport async function signIssuerJwt(\n params: SignIssuerJwtParams,\n): Promise<string> {\n const alg = params.alg ?? params.privateJwk.alg ?? \"ES256\";\n const kid = params.privateJwk.kid;\n\n if (!kid) {\n throw new Error(\n \"signIssuerJwt: privateJwk.kid is required (gateway uses kid to look up the verification key)\",\n );\n }\n\n const key = await importJWK(params.privateJwk, alg);\n const now = Math.floor(Date.now() / 1000);\n const lifetime = params.expiresInSec ?? 60;\n\n const payload: Record<string, unknown> = { ...(params.extra ?? {}) };\n if (params.authAttribute) {\n payload.auth_attribute = {\n type: params.authAttribute.type,\n value: params.authAttribute.value,\n };\n }\n\n const builder = new SignJWT(payload)\n .setProtectedHeader({ alg, typ: \"JWT\", kid })\n .setIssuer(params.iss)\n .setSubject(params.sub)\n .setAudience(params.aud)\n .setIssuedAt(now)\n .setExpirationTime(now + lifetime)\n .setJti(params.jti ?? randomUUID());\n\n return builder.sign(key);\n}\n"],"mappings":";;;AAAA,SAAS,WAAW,eAAe;AACnC,SAAS,kBAAkB;AAyB3B,eAAsB,cACpB,QACiB;AACjB,QAAM,MAAM,OAAO,OAAO,OAAO,WAAW,OAAO;AACnD,QAAM,MAAM,OAAO,WAAW;AAE9B,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,MAAM,UAAU,OAAO,YAAY,GAAG;AAClD,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,WAAW,OAAO,gBAAgB;AAExC,QAAM,UAAmC,EAAE,GAAI,OAAO,SAAS,CAAC,EAAG;AACnE,MAAI,OAAO,eAAe;AACxB,YAAQ,iBAAiB;AAAA,MACvB,MAAM,OAAO,cAAc;AAAA,MAC3B,OAAO,OAAO,cAAc;AAAA,IAC9B;AAAA,EACF;AAEA,QAAM,UAAU,IAAI,QAAQ,OAAO,EAChC,mBAAmB,EAAE,KAAK,KAAK,OAAO,IAAI,CAAC,EAC3C,UAAU,OAAO,GAAG,EACpB,WAAW,OAAO,GAAG,EACrB,YAAY,OAAO,GAAG,EACtB,YAAY,GAAG,EACf,kBAAkB,MAAM,QAAQ,EAChC,OAAO,OAAO,OAAO,WAAW,CAAC;AAEpC,SAAO,QAAQ,KAAK,GAAG;AACzB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pafi-dev/issuer",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "Issuer backend API and services for the PAFI point token system",
5
5
  "repository": {
6
6
  "type": "git",
@@ -55,6 +55,16 @@
55
55
  "types": "./dist/wallet-auth/index.d.cts",
56
56
  "default": "./dist/wallet-auth/index.cjs"
57
57
  }
58
+ },
59
+ "./auth-client": {
60
+ "import": {
61
+ "types": "./dist/auth-client/index.d.ts",
62
+ "default": "./dist/auth-client/index.js"
63
+ },
64
+ "require": {
65
+ "types": "./dist/auth-client/index.d.cts",
66
+ "default": "./dist/auth-client/index.cjs"
67
+ }
58
68
  }
59
69
  },
60
70
  "typesVersions": {
@@ -67,15 +77,25 @@
67
77
  ],
68
78
  "wallet-auth": [
69
79
  "./dist/wallet-auth/index.d.ts"
80
+ ],
81
+ "auth-client": [
82
+ "./dist/auth-client/index.d.ts"
70
83
  ]
71
84
  }
72
85
  },
73
86
  "files": [
74
87
  "dist"
75
88
  ],
89
+ "scripts": {
90
+ "build": "tsup",
91
+ "test": "vitest run --passWithNoTests",
92
+ "test:watch": "vitest",
93
+ "test:cov": "vitest run --coverage --passWithNoTests",
94
+ "typecheck": "tsc --noEmit"
95
+ },
76
96
  "dependencies": {
77
- "jose": "^5.9.0",
78
- "@pafi-dev/core": "0.24.0"
97
+ "@pafi-dev/core": "workspace:*",
98
+ "jose": "^5.9.0"
79
99
  },
80
100
  "peerDependencies": {
81
101
  "@nestjs/common": "^10.0.0",
@@ -94,12 +114,5 @@
94
114
  "viem": "^2.21.0",
95
115
  "vitest": "^2.0.0"
96
116
  },
97
- "license": "Apache-2.0",
98
- "scripts": {
99
- "build": "tsup",
100
- "test": "vitest run --passWithNoTests",
101
- "test:watch": "vitest",
102
- "test:cov": "vitest run --coverage --passWithNoTests",
103
- "typecheck": "tsc --noEmit"
104
- }
105
- }
117
+ "license": "Apache-2.0"
118
+ }