@tdacorp/identity-client 0.1.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,320 @@
1
+ // src/discovery.ts
2
+ import { createRemoteJWKSet } from "jose";
3
+ var DISCOVERY_CACHE_TTL_MS = 6e5;
4
+ var CACHE_MAX_ENTRIES = 100;
5
+ var discoveryCache = /* @__PURE__ */ new Map();
6
+ var discoveryPending = /* @__PURE__ */ new Map();
7
+ var jwksCache = /* @__PURE__ */ new Map();
8
+ function touchLru(map, key) {
9
+ const value = map.get(key);
10
+ if (value === void 0) return;
11
+ map.delete(key);
12
+ map.set(key, value);
13
+ }
14
+ function setWithLruEviction(map, key, value, maxEntries) {
15
+ map.delete(key);
16
+ map.set(key, value);
17
+ if (map.size > maxEntries) {
18
+ const oldestKey = map.keys().next().value;
19
+ if (oldestKey !== void 0) map.delete(oldestKey);
20
+ }
21
+ }
22
+ function discoveryUrl(issuerUrl) {
23
+ return `${issuerUrl.replace(/\/+$/, "")}/.well-known/openid-configuration`;
24
+ }
25
+ function isLoopbackHost(hostname) {
26
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1";
27
+ }
28
+ function assertHttps(url, label) {
29
+ const parsed = new URL(url);
30
+ if (parsed.protocol !== "https:" && !isLoopbackHost(parsed.hostname)) {
31
+ throw new Error(`Discovery: ${label} "${url}" must be https (loopback hosts are the only http exception)`);
32
+ }
33
+ }
34
+ function validateDiscoveryDocument(document, issuerUrl) {
35
+ assertHttps(issuerUrl, "issuer");
36
+ assertHttps(document.jwks_uri, "jwks_uri");
37
+ assertHttps(document.token_endpoint, "token_endpoint");
38
+ if (document.issuer !== issuerUrl) {
39
+ throw new Error(`Discovery: document issuer "${document.issuer}" does not match the configured issuer "${issuerUrl}"`);
40
+ }
41
+ const issuerOrigin = new URL(issuerUrl).origin;
42
+ if (new URL(document.jwks_uri).origin !== issuerOrigin) {
43
+ throw new Error(`Discovery: jwks_uri "${document.jwks_uri}" is not same-origin as issuer "${issuerUrl}"`);
44
+ }
45
+ if (new URL(document.token_endpoint).origin !== issuerOrigin) {
46
+ throw new Error(`Discovery: token_endpoint "${document.token_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
47
+ }
48
+ }
49
+ async function fetchDiscovery(issuerUrl) {
50
+ const cached = discoveryCache.get(issuerUrl);
51
+ if (cached && cached.expiresAt > Date.now()) {
52
+ touchLru(discoveryCache, issuerUrl);
53
+ return cached.document;
54
+ }
55
+ const pending = discoveryPending.get(issuerUrl);
56
+ if (pending) return pending;
57
+ const promise = fetch(discoveryUrl(issuerUrl), { headers: { Accept: "application/json" } }).then(
58
+ async (response) => {
59
+ if (!response.ok) {
60
+ throw new Error(`Discovery fetch for ${issuerUrl} failed: HTTP ${response.status}`);
61
+ }
62
+ const document = await response.json();
63
+ validateDiscoveryDocument(document, issuerUrl);
64
+ return document;
65
+ }
66
+ );
67
+ discoveryPending.set(issuerUrl, promise);
68
+ try {
69
+ const document = await promise;
70
+ discoveryPending.delete(issuerUrl);
71
+ setWithLruEviction(discoveryCache, issuerUrl, { document, expiresAt: Date.now() + DISCOVERY_CACHE_TTL_MS }, CACHE_MAX_ENTRIES);
72
+ return document;
73
+ } catch (error) {
74
+ discoveryPending.delete(issuerUrl);
75
+ throw error;
76
+ }
77
+ }
78
+ function getRemoteJwksForUri(jwksUri) {
79
+ const cached = jwksCache.get(jwksUri);
80
+ if (cached) {
81
+ touchLru(jwksCache, jwksUri);
82
+ return cached;
83
+ }
84
+ const jwks = createRemoteJWKSet(new URL(jwksUri));
85
+ setWithLruEviction(jwksCache, jwksUri, jwks, CACHE_MAX_ENTRIES);
86
+ return jwks;
87
+ }
88
+
89
+ // src/pkce.ts
90
+ var CODE_VERIFIER_BYTE_LENGTH = 32;
91
+ var STATE_BYTE_LENGTH = 32;
92
+ function base64UrlEncode(bytes) {
93
+ let binary = "";
94
+ for (const byte of bytes) binary += String.fromCharCode(byte);
95
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
96
+ }
97
+ function randomBase64Url(byteLength) {
98
+ const bytes = new Uint8Array(byteLength);
99
+ crypto.getRandomValues(bytes);
100
+ return base64UrlEncode(bytes);
101
+ }
102
+ function generateCodeVerifier() {
103
+ return randomBase64Url(CODE_VERIFIER_BYTE_LENGTH);
104
+ }
105
+ async function generateCodeChallenge(verifier) {
106
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
107
+ return base64UrlEncode(new Uint8Array(digest));
108
+ }
109
+ function generateState() {
110
+ return randomBase64Url(STATE_BYTE_LENGTH);
111
+ }
112
+
113
+ // src/token.ts
114
+ var TokenRequestError = class extends Error {
115
+ status;
116
+ error;
117
+ errorDescription;
118
+ constructor(details) {
119
+ super(
120
+ details.errorDescription ?? details.error ?? `Token request failed${details.status ? ` with HTTP ${details.status}` : ""}`
121
+ );
122
+ this.name = "TokenRequestError";
123
+ this.status = details.status;
124
+ this.error = details.error;
125
+ this.errorDescription = details.errorDescription;
126
+ }
127
+ };
128
+ function toTokenSet(body) {
129
+ return {
130
+ accessToken: body.access_token,
131
+ tokenType: body.token_type ?? "Bearer",
132
+ refreshToken: body.refresh_token,
133
+ idToken: body.id_token,
134
+ expiresIn: body.expires_in,
135
+ scope: body.scope
136
+ };
137
+ }
138
+ async function postTokenRequest(tokenEndpoint, clientId, clientSecret, params) {
139
+ const body = new URLSearchParams(params);
140
+ const headers = {
141
+ "Content-Type": "application/x-www-form-urlencoded",
142
+ Accept: "application/json"
143
+ };
144
+ if (clientSecret) {
145
+ headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
146
+ } else {
147
+ body.set("client_id", clientId);
148
+ }
149
+ return fetch(tokenEndpoint, { method: "POST", headers, body: body.toString() });
150
+ }
151
+ async function readTokenResponseBody(response) {
152
+ return await response.json().catch(() => null);
153
+ }
154
+ async function exchangeAuthorizationCode(options) {
155
+ const discovery = await fetchDiscovery(options.issuer);
156
+ const response = await postTokenRequest(discovery.token_endpoint, options.clientId, options.clientSecret, {
157
+ grant_type: "authorization_code",
158
+ code: options.code,
159
+ redirect_uri: options.redirectUri,
160
+ code_verifier: options.codeVerifier
161
+ });
162
+ const body = await readTokenResponseBody(response);
163
+ if (!response.ok || !body || body.error || typeof body.access_token !== "string") {
164
+ throw new TokenRequestError({ status: response.status, error: body?.error, errorDescription: body?.error_description });
165
+ }
166
+ return toTokenSet(body);
167
+ }
168
+ async function clientCredentialsGrant(options) {
169
+ const discovery = await fetchDiscovery(options.issuer);
170
+ const response = await postTokenRequest(discovery.token_endpoint, options.clientId, options.clientSecret, {
171
+ grant_type: "client_credentials",
172
+ ...options.scope ? { scope: options.scope } : {}
173
+ });
174
+ const body = await readTokenResponseBody(response);
175
+ if (!response.ok || !body || body.error || typeof body.access_token !== "string") {
176
+ throw new TokenRequestError({ status: response.status, error: body?.error, errorDescription: body?.error_description });
177
+ }
178
+ return toTokenSet(body);
179
+ }
180
+ var TRANSIENT_HTTP_STATUSES = /* @__PURE__ */ new Set([408, 429]);
181
+ function isTransientStatus(status) {
182
+ return TRANSIENT_HTTP_STATUSES.has(status) || status >= 500;
183
+ }
184
+ async function refreshTokens(options) {
185
+ let response;
186
+ try {
187
+ const discovery = await fetchDiscovery(options.issuer);
188
+ response = await postTokenRequest(discovery.token_endpoint, options.clientId, options.clientSecret, {
189
+ grant_type: "refresh_token",
190
+ refresh_token: options.refreshToken
191
+ });
192
+ } catch (error) {
193
+ return { outcome: "transient", error };
194
+ }
195
+ if (isTransientStatus(response.status)) {
196
+ return { outcome: "transient", error: new TokenRequestError({ status: response.status }) };
197
+ }
198
+ const body = await readTokenResponseBody(response);
199
+ if (response.ok && body && typeof body.access_token === "string") {
200
+ return { outcome: "success", tokens: toTokenSet(body) };
201
+ }
202
+ const tokenError = new TokenRequestError({
203
+ status: response.status,
204
+ error: body?.error,
205
+ errorDescription: body?.error_description
206
+ });
207
+ const isConfirmedRejection = response.status === 400 && body?.error === "invalid_grant";
208
+ return isConfirmedRejection ? { outcome: "terminal", error: tokenError } : { outcome: "transient", error: tokenError };
209
+ }
210
+
211
+ // src/verify.ts
212
+ import { jwtVerify, errors as joseErrors } from "jose";
213
+ var CLOCK_SKEW_TOLERANCE_SECONDS = 60;
214
+ function joseErrorCode(error) {
215
+ return error instanceof joseErrors.JOSEError ? error.code : void 0;
216
+ }
217
+ function classifyJoseError(error) {
218
+ const code = joseErrorCode(error);
219
+ const message = error instanceof Error ? error.message : "Token verification failed";
220
+ switch (code) {
221
+ case "ERR_JWT_EXPIRED":
222
+ return { code: "expired", message };
223
+ case "ERR_JWT_CLAIM_VALIDATION_FAILED": {
224
+ const claim = error instanceof joseErrors.JWTClaimValidationFailed ? error.claim : void 0;
225
+ if (claim === "aud") return { code: "invalid-audience", message };
226
+ if (claim === "iss") return { code: "invalid-issuer", message };
227
+ return { code: "malformed-claims", message };
228
+ }
229
+ case "ERR_JWS_SIGNATURE_VERIFICATION_FAILED":
230
+ case "ERR_JWS_INVALID":
231
+ case "ERR_JWKS_NO_MATCHING_KEY":
232
+ case "ERR_JWKS_MULTIPLE_MATCHING_KEYS":
233
+ return { code: "invalid-signature", message };
234
+ case "ERR_JWKS_TIMEOUT":
235
+ return { code: "jwks-unavailable", message };
236
+ case "ERR_JWT_INVALID":
237
+ default:
238
+ return { code: "malformed-token", message };
239
+ }
240
+ }
241
+ function checkAuthorizedParty(payload, authorizedParties) {
242
+ const party = typeof payload.azp === "string" ? payload.azp : typeof payload.aud === "string" ? payload.aud : void 0;
243
+ if (party === void 0) {
244
+ return { code: "unauthorized-party", message: "Token has no azp claim and no single-string aud to check against authorizedParties" };
245
+ }
246
+ if (!authorizedParties.includes(party)) {
247
+ return { code: "unauthorized-party", message: `Token's authorized party "${party}" is not in the configured allowlist` };
248
+ }
249
+ return null;
250
+ }
251
+ function requireConfig(options) {
252
+ if (!options.issuer) throw new Error("verify: options.issuer is required");
253
+ if (!options.audience) throw new Error("verify: options.audience is required");
254
+ }
255
+ async function verifyAgainstIssuer(token, options) {
256
+ requireConfig(options);
257
+ let jwksUri;
258
+ try {
259
+ jwksUri = (await fetchDiscovery(options.issuer)).jwks_uri;
260
+ } catch (error) {
261
+ return { errors: [{ code: "jwks-unavailable", message: error instanceof Error ? error.message : "Discovery fetch failed" }] };
262
+ }
263
+ const jwks = getRemoteJwksForUri(jwksUri);
264
+ try {
265
+ const { payload } = await jwtVerify(token, jwks, {
266
+ issuer: options.issuer,
267
+ audience: options.audience,
268
+ algorithms: ["EdDSA"],
269
+ clockTolerance: CLOCK_SKEW_TOLERANCE_SECONDS
270
+ });
271
+ if (options.authorizedParties) {
272
+ const authorizedPartyError = checkAuthorizedParty(payload, options.authorizedParties);
273
+ if (authorizedPartyError) return { errors: [authorizedPartyError] };
274
+ }
275
+ return { payload };
276
+ } catch (error) {
277
+ return { errors: [classifyJoseError(error)] };
278
+ }
279
+ }
280
+ async function verifyIdToken(idToken, options) {
281
+ const result = await verifyAgainstIssuer(idToken, options);
282
+ if ("errors" in result) return { success: false, errors: result.errors };
283
+ const { payload } = result;
284
+ if (typeof payload.sub !== "string") {
285
+ return { success: false, errors: [{ code: "malformed-claims", message: "id_token has no sub claim" }] };
286
+ }
287
+ return { success: true, data: payload };
288
+ }
289
+ async function verifyAccessToken(accessToken, options) {
290
+ const result = await verifyAgainstIssuer(accessToken, options);
291
+ if ("errors" in result) return { success: false, errors: result.errors };
292
+ const { payload } = result;
293
+ const claims = payload;
294
+ const roles = "roles" in claims ? claims.roles : void 0;
295
+ if (typeof payload.sub === "string") {
296
+ return { success: true, data: { kind: "user", sub: payload.sub, roles, claims } };
297
+ }
298
+ if (typeof payload.azp !== "string") {
299
+ return {
300
+ success: false,
301
+ errors: [{ code: "malformed-claims", message: "Access token has neither a sub (user) nor an azp (machine client id)" }]
302
+ };
303
+ }
304
+ return { success: true, data: { kind: "machine", clientId: payload.azp, claims } };
305
+ }
306
+
307
+ export {
308
+ fetchDiscovery,
309
+ getRemoteJwksForUri,
310
+ generateCodeVerifier,
311
+ generateCodeChallenge,
312
+ generateState,
313
+ TokenRequestError,
314
+ exchangeAuthorizationCode,
315
+ clientCredentialsGrant,
316
+ refreshTokens,
317
+ CLOCK_SKEW_TOLERANCE_SECONDS,
318
+ verifyIdToken,
319
+ verifyAccessToken
320
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1,357 @@
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/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CLOCK_SKEW_TOLERANCE_SECONDS: () => CLOCK_SKEW_TOLERANCE_SECONDS,
24
+ TokenRequestError: () => TokenRequestError,
25
+ clientCredentialsGrant: () => clientCredentialsGrant,
26
+ exchangeAuthorizationCode: () => exchangeAuthorizationCode,
27
+ fetchDiscovery: () => fetchDiscovery,
28
+ generateCodeChallenge: () => generateCodeChallenge,
29
+ generateCodeVerifier: () => generateCodeVerifier,
30
+ generateState: () => generateState,
31
+ getRemoteJwksForUri: () => getRemoteJwksForUri,
32
+ refreshTokens: () => refreshTokens,
33
+ verifyAccessToken: () => verifyAccessToken,
34
+ verifyIdToken: () => verifyIdToken
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/discovery.ts
39
+ var import_jose = require("jose");
40
+ var DISCOVERY_CACHE_TTL_MS = 6e5;
41
+ var CACHE_MAX_ENTRIES = 100;
42
+ var discoveryCache = /* @__PURE__ */ new Map();
43
+ var discoveryPending = /* @__PURE__ */ new Map();
44
+ var jwksCache = /* @__PURE__ */ new Map();
45
+ function touchLru(map, key) {
46
+ const value = map.get(key);
47
+ if (value === void 0) return;
48
+ map.delete(key);
49
+ map.set(key, value);
50
+ }
51
+ function setWithLruEviction(map, key, value, maxEntries) {
52
+ map.delete(key);
53
+ map.set(key, value);
54
+ if (map.size > maxEntries) {
55
+ const oldestKey = map.keys().next().value;
56
+ if (oldestKey !== void 0) map.delete(oldestKey);
57
+ }
58
+ }
59
+ function discoveryUrl(issuerUrl) {
60
+ return `${issuerUrl.replace(/\/+$/, "")}/.well-known/openid-configuration`;
61
+ }
62
+ function isLoopbackHost(hostname) {
63
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1";
64
+ }
65
+ function assertHttps(url, label) {
66
+ const parsed = new URL(url);
67
+ if (parsed.protocol !== "https:" && !isLoopbackHost(parsed.hostname)) {
68
+ throw new Error(`Discovery: ${label} "${url}" must be https (loopback hosts are the only http exception)`);
69
+ }
70
+ }
71
+ function validateDiscoveryDocument(document, issuerUrl) {
72
+ assertHttps(issuerUrl, "issuer");
73
+ assertHttps(document.jwks_uri, "jwks_uri");
74
+ assertHttps(document.token_endpoint, "token_endpoint");
75
+ if (document.issuer !== issuerUrl) {
76
+ throw new Error(`Discovery: document issuer "${document.issuer}" does not match the configured issuer "${issuerUrl}"`);
77
+ }
78
+ const issuerOrigin = new URL(issuerUrl).origin;
79
+ if (new URL(document.jwks_uri).origin !== issuerOrigin) {
80
+ throw new Error(`Discovery: jwks_uri "${document.jwks_uri}" is not same-origin as issuer "${issuerUrl}"`);
81
+ }
82
+ if (new URL(document.token_endpoint).origin !== issuerOrigin) {
83
+ throw new Error(`Discovery: token_endpoint "${document.token_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
84
+ }
85
+ }
86
+ async function fetchDiscovery(issuerUrl) {
87
+ const cached = discoveryCache.get(issuerUrl);
88
+ if (cached && cached.expiresAt > Date.now()) {
89
+ touchLru(discoveryCache, issuerUrl);
90
+ return cached.document;
91
+ }
92
+ const pending = discoveryPending.get(issuerUrl);
93
+ if (pending) return pending;
94
+ const promise = fetch(discoveryUrl(issuerUrl), { headers: { Accept: "application/json" } }).then(
95
+ async (response) => {
96
+ if (!response.ok) {
97
+ throw new Error(`Discovery fetch for ${issuerUrl} failed: HTTP ${response.status}`);
98
+ }
99
+ const document = await response.json();
100
+ validateDiscoveryDocument(document, issuerUrl);
101
+ return document;
102
+ }
103
+ );
104
+ discoveryPending.set(issuerUrl, promise);
105
+ try {
106
+ const document = await promise;
107
+ discoveryPending.delete(issuerUrl);
108
+ setWithLruEviction(discoveryCache, issuerUrl, { document, expiresAt: Date.now() + DISCOVERY_CACHE_TTL_MS }, CACHE_MAX_ENTRIES);
109
+ return document;
110
+ } catch (error) {
111
+ discoveryPending.delete(issuerUrl);
112
+ throw error;
113
+ }
114
+ }
115
+ function getRemoteJwksForUri(jwksUri) {
116
+ const cached = jwksCache.get(jwksUri);
117
+ if (cached) {
118
+ touchLru(jwksCache, jwksUri);
119
+ return cached;
120
+ }
121
+ const jwks = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri));
122
+ setWithLruEviction(jwksCache, jwksUri, jwks, CACHE_MAX_ENTRIES);
123
+ return jwks;
124
+ }
125
+
126
+ // src/pkce.ts
127
+ var CODE_VERIFIER_BYTE_LENGTH = 32;
128
+ var STATE_BYTE_LENGTH = 32;
129
+ function base64UrlEncode(bytes) {
130
+ let binary = "";
131
+ for (const byte of bytes) binary += String.fromCharCode(byte);
132
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
133
+ }
134
+ function randomBase64Url(byteLength) {
135
+ const bytes = new Uint8Array(byteLength);
136
+ crypto.getRandomValues(bytes);
137
+ return base64UrlEncode(bytes);
138
+ }
139
+ function generateCodeVerifier() {
140
+ return randomBase64Url(CODE_VERIFIER_BYTE_LENGTH);
141
+ }
142
+ async function generateCodeChallenge(verifier) {
143
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
144
+ return base64UrlEncode(new Uint8Array(digest));
145
+ }
146
+ function generateState() {
147
+ return randomBase64Url(STATE_BYTE_LENGTH);
148
+ }
149
+
150
+ // src/token.ts
151
+ var TokenRequestError = class extends Error {
152
+ status;
153
+ error;
154
+ errorDescription;
155
+ constructor(details) {
156
+ super(
157
+ details.errorDescription ?? details.error ?? `Token request failed${details.status ? ` with HTTP ${details.status}` : ""}`
158
+ );
159
+ this.name = "TokenRequestError";
160
+ this.status = details.status;
161
+ this.error = details.error;
162
+ this.errorDescription = details.errorDescription;
163
+ }
164
+ };
165
+ function toTokenSet(body) {
166
+ return {
167
+ accessToken: body.access_token,
168
+ tokenType: body.token_type ?? "Bearer",
169
+ refreshToken: body.refresh_token,
170
+ idToken: body.id_token,
171
+ expiresIn: body.expires_in,
172
+ scope: body.scope
173
+ };
174
+ }
175
+ async function postTokenRequest(tokenEndpoint, clientId, clientSecret, params) {
176
+ const body = new URLSearchParams(params);
177
+ const headers = {
178
+ "Content-Type": "application/x-www-form-urlencoded",
179
+ Accept: "application/json"
180
+ };
181
+ if (clientSecret) {
182
+ headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
183
+ } else {
184
+ body.set("client_id", clientId);
185
+ }
186
+ return fetch(tokenEndpoint, { method: "POST", headers, body: body.toString() });
187
+ }
188
+ async function readTokenResponseBody(response) {
189
+ return await response.json().catch(() => null);
190
+ }
191
+ async function exchangeAuthorizationCode(options) {
192
+ const discovery = await fetchDiscovery(options.issuer);
193
+ const response = await postTokenRequest(discovery.token_endpoint, options.clientId, options.clientSecret, {
194
+ grant_type: "authorization_code",
195
+ code: options.code,
196
+ redirect_uri: options.redirectUri,
197
+ code_verifier: options.codeVerifier
198
+ });
199
+ const body = await readTokenResponseBody(response);
200
+ if (!response.ok || !body || body.error || typeof body.access_token !== "string") {
201
+ throw new TokenRequestError({ status: response.status, error: body?.error, errorDescription: body?.error_description });
202
+ }
203
+ return toTokenSet(body);
204
+ }
205
+ async function clientCredentialsGrant(options) {
206
+ const discovery = await fetchDiscovery(options.issuer);
207
+ const response = await postTokenRequest(discovery.token_endpoint, options.clientId, options.clientSecret, {
208
+ grant_type: "client_credentials",
209
+ ...options.scope ? { scope: options.scope } : {}
210
+ });
211
+ const body = await readTokenResponseBody(response);
212
+ if (!response.ok || !body || body.error || typeof body.access_token !== "string") {
213
+ throw new TokenRequestError({ status: response.status, error: body?.error, errorDescription: body?.error_description });
214
+ }
215
+ return toTokenSet(body);
216
+ }
217
+ var TRANSIENT_HTTP_STATUSES = /* @__PURE__ */ new Set([408, 429]);
218
+ function isTransientStatus(status) {
219
+ return TRANSIENT_HTTP_STATUSES.has(status) || status >= 500;
220
+ }
221
+ async function refreshTokens(options) {
222
+ let response;
223
+ try {
224
+ const discovery = await fetchDiscovery(options.issuer);
225
+ response = await postTokenRequest(discovery.token_endpoint, options.clientId, options.clientSecret, {
226
+ grant_type: "refresh_token",
227
+ refresh_token: options.refreshToken
228
+ });
229
+ } catch (error) {
230
+ return { outcome: "transient", error };
231
+ }
232
+ if (isTransientStatus(response.status)) {
233
+ return { outcome: "transient", error: new TokenRequestError({ status: response.status }) };
234
+ }
235
+ const body = await readTokenResponseBody(response);
236
+ if (response.ok && body && typeof body.access_token === "string") {
237
+ return { outcome: "success", tokens: toTokenSet(body) };
238
+ }
239
+ const tokenError = new TokenRequestError({
240
+ status: response.status,
241
+ error: body?.error,
242
+ errorDescription: body?.error_description
243
+ });
244
+ const isConfirmedRejection = response.status === 400 && body?.error === "invalid_grant";
245
+ return isConfirmedRejection ? { outcome: "terminal", error: tokenError } : { outcome: "transient", error: tokenError };
246
+ }
247
+
248
+ // src/verify.ts
249
+ var import_jose2 = require("jose");
250
+ var CLOCK_SKEW_TOLERANCE_SECONDS = 60;
251
+ function joseErrorCode(error) {
252
+ return error instanceof import_jose2.errors.JOSEError ? error.code : void 0;
253
+ }
254
+ function classifyJoseError(error) {
255
+ const code = joseErrorCode(error);
256
+ const message = error instanceof Error ? error.message : "Token verification failed";
257
+ switch (code) {
258
+ case "ERR_JWT_EXPIRED":
259
+ return { code: "expired", message };
260
+ case "ERR_JWT_CLAIM_VALIDATION_FAILED": {
261
+ const claim = error instanceof import_jose2.errors.JWTClaimValidationFailed ? error.claim : void 0;
262
+ if (claim === "aud") return { code: "invalid-audience", message };
263
+ if (claim === "iss") return { code: "invalid-issuer", message };
264
+ return { code: "malformed-claims", message };
265
+ }
266
+ case "ERR_JWS_SIGNATURE_VERIFICATION_FAILED":
267
+ case "ERR_JWS_INVALID":
268
+ case "ERR_JWKS_NO_MATCHING_KEY":
269
+ case "ERR_JWKS_MULTIPLE_MATCHING_KEYS":
270
+ return { code: "invalid-signature", message };
271
+ case "ERR_JWKS_TIMEOUT":
272
+ return { code: "jwks-unavailable", message };
273
+ case "ERR_JWT_INVALID":
274
+ default:
275
+ return { code: "malformed-token", message };
276
+ }
277
+ }
278
+ function checkAuthorizedParty(payload, authorizedParties) {
279
+ const party = typeof payload.azp === "string" ? payload.azp : typeof payload.aud === "string" ? payload.aud : void 0;
280
+ if (party === void 0) {
281
+ return { code: "unauthorized-party", message: "Token has no azp claim and no single-string aud to check against authorizedParties" };
282
+ }
283
+ if (!authorizedParties.includes(party)) {
284
+ return { code: "unauthorized-party", message: `Token's authorized party "${party}" is not in the configured allowlist` };
285
+ }
286
+ return null;
287
+ }
288
+ function requireConfig(options) {
289
+ if (!options.issuer) throw new Error("verify: options.issuer is required");
290
+ if (!options.audience) throw new Error("verify: options.audience is required");
291
+ }
292
+ async function verifyAgainstIssuer(token, options) {
293
+ requireConfig(options);
294
+ let jwksUri;
295
+ try {
296
+ jwksUri = (await fetchDiscovery(options.issuer)).jwks_uri;
297
+ } catch (error) {
298
+ return { errors: [{ code: "jwks-unavailable", message: error instanceof Error ? error.message : "Discovery fetch failed" }] };
299
+ }
300
+ const jwks = getRemoteJwksForUri(jwksUri);
301
+ try {
302
+ const { payload } = await (0, import_jose2.jwtVerify)(token, jwks, {
303
+ issuer: options.issuer,
304
+ audience: options.audience,
305
+ algorithms: ["EdDSA"],
306
+ clockTolerance: CLOCK_SKEW_TOLERANCE_SECONDS
307
+ });
308
+ if (options.authorizedParties) {
309
+ const authorizedPartyError = checkAuthorizedParty(payload, options.authorizedParties);
310
+ if (authorizedPartyError) return { errors: [authorizedPartyError] };
311
+ }
312
+ return { payload };
313
+ } catch (error) {
314
+ return { errors: [classifyJoseError(error)] };
315
+ }
316
+ }
317
+ async function verifyIdToken(idToken, options) {
318
+ const result = await verifyAgainstIssuer(idToken, options);
319
+ if ("errors" in result) return { success: false, errors: result.errors };
320
+ const { payload } = result;
321
+ if (typeof payload.sub !== "string") {
322
+ return { success: false, errors: [{ code: "malformed-claims", message: "id_token has no sub claim" }] };
323
+ }
324
+ return { success: true, data: payload };
325
+ }
326
+ async function verifyAccessToken(accessToken, options) {
327
+ const result = await verifyAgainstIssuer(accessToken, options);
328
+ if ("errors" in result) return { success: false, errors: result.errors };
329
+ const { payload } = result;
330
+ const claims = payload;
331
+ const roles = "roles" in claims ? claims.roles : void 0;
332
+ if (typeof payload.sub === "string") {
333
+ return { success: true, data: { kind: "user", sub: payload.sub, roles, claims } };
334
+ }
335
+ if (typeof payload.azp !== "string") {
336
+ return {
337
+ success: false,
338
+ errors: [{ code: "malformed-claims", message: "Access token has neither a sub (user) nor an azp (machine client id)" }]
339
+ };
340
+ }
341
+ return { success: true, data: { kind: "machine", clientId: payload.azp, claims } };
342
+ }
343
+ // Annotate the CommonJS export names for ESM import in node:
344
+ 0 && (module.exports = {
345
+ CLOCK_SKEW_TOLERANCE_SECONDS,
346
+ TokenRequestError,
347
+ clientCredentialsGrant,
348
+ exchangeAuthorizationCode,
349
+ fetchDiscovery,
350
+ generateCodeChallenge,
351
+ generateCodeVerifier,
352
+ generateState,
353
+ getRemoteJwksForUri,
354
+ refreshTokens,
355
+ verifyAccessToken,
356
+ verifyIdToken
357
+ });