@happyvertical/auth 0.103.3 → 0.103.5

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,761 @@
1
+ import { D as encode$1, S as JWTInvalid, T as concat, a as normalizeKey, b as JWTClaimValidationFailed, c as isDisjoint, d as JOSEAlgNotAllowed, i as validateCrit, n as JWTClaimsBuilder, p as JOSENotSupported, r as checkKeyType, s as sign, t as jwtVerify, u as assertNotSet, v as JWSInvalid, w as encode, x as JWTExpired, y as JWSSignatureVerificationFailed } from "../chunks/verify-CGYWoFss.js";
2
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
3
+ //#region ../../node_modules/.pnpm/jose@6.2.2/node_modules/jose/dist/webapi/jws/flattened/sign.js
4
+ var FlattenedSign = class {
5
+ #payload;
6
+ #protectedHeader;
7
+ #unprotectedHeader;
8
+ constructor(payload) {
9
+ if (!(payload instanceof Uint8Array)) throw new TypeError("payload must be an instance of Uint8Array");
10
+ this.#payload = payload;
11
+ }
12
+ setProtectedHeader(protectedHeader) {
13
+ assertNotSet(this.#protectedHeader, "setProtectedHeader");
14
+ this.#protectedHeader = protectedHeader;
15
+ return this;
16
+ }
17
+ setUnprotectedHeader(unprotectedHeader) {
18
+ assertNotSet(this.#unprotectedHeader, "setUnprotectedHeader");
19
+ this.#unprotectedHeader = unprotectedHeader;
20
+ return this;
21
+ }
22
+ async sign(key, options) {
23
+ if (!this.#protectedHeader && !this.#unprotectedHeader) throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()");
24
+ if (!isDisjoint(this.#protectedHeader, this.#unprotectedHeader)) throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
25
+ const joseHeader = {
26
+ ...this.#protectedHeader,
27
+ ...this.#unprotectedHeader
28
+ };
29
+ const extensions = validateCrit(JWSInvalid, /* @__PURE__ */ new Map([["b64", true]]), options?.crit, this.#protectedHeader, joseHeader);
30
+ let b64 = true;
31
+ if (extensions.has("b64")) {
32
+ b64 = this.#protectedHeader.b64;
33
+ if (typeof b64 !== "boolean") throw new JWSInvalid("The \"b64\" (base64url-encode payload) Header Parameter must be a boolean");
34
+ }
35
+ const { alg } = joseHeader;
36
+ if (typeof alg !== "string" || !alg) throw new JWSInvalid("JWS \"alg\" (Algorithm) Header Parameter missing or invalid");
37
+ checkKeyType(alg, key, "sign");
38
+ let payloadS;
39
+ let payloadB;
40
+ if (b64) {
41
+ payloadS = encode(this.#payload);
42
+ payloadB = encode$1(payloadS);
43
+ } else {
44
+ payloadB = this.#payload;
45
+ payloadS = "";
46
+ }
47
+ let protectedHeaderString;
48
+ let protectedHeaderBytes;
49
+ if (this.#protectedHeader) {
50
+ protectedHeaderString = encode(JSON.stringify(this.#protectedHeader));
51
+ protectedHeaderBytes = encode$1(protectedHeaderString);
52
+ } else {
53
+ protectedHeaderString = "";
54
+ protectedHeaderBytes = /* @__PURE__ */ new Uint8Array();
55
+ }
56
+ const data = concat(protectedHeaderBytes, encode$1("."), payloadB);
57
+ const jws = {
58
+ signature: encode(await sign(alg, await normalizeKey(key, alg), data)),
59
+ payload: payloadS
60
+ };
61
+ if (this.#unprotectedHeader) jws.header = this.#unprotectedHeader;
62
+ if (this.#protectedHeader) jws.protected = protectedHeaderString;
63
+ return jws;
64
+ }
65
+ };
66
+ //#endregion
67
+ //#region ../../node_modules/.pnpm/jose@6.2.2/node_modules/jose/dist/webapi/jws/compact/sign.js
68
+ var CompactSign = class {
69
+ #flattened;
70
+ constructor(payload) {
71
+ this.#flattened = new FlattenedSign(payload);
72
+ }
73
+ setProtectedHeader(protectedHeader) {
74
+ this.#flattened.setProtectedHeader(protectedHeader);
75
+ return this;
76
+ }
77
+ async sign(key, options) {
78
+ const jws = await this.#flattened.sign(key, options);
79
+ if (jws.payload === void 0) throw new TypeError("use the flattened module for creating JWS with b64: false");
80
+ return `${jws.protected}.${jws.payload}.${jws.signature}`;
81
+ }
82
+ };
83
+ //#endregion
84
+ //#region ../../node_modules/.pnpm/jose@6.2.2/node_modules/jose/dist/webapi/jwt/sign.js
85
+ var SignJWT = class {
86
+ #protectedHeader;
87
+ #jwt;
88
+ constructor(payload = {}) {
89
+ this.#jwt = new JWTClaimsBuilder(payload);
90
+ }
91
+ setIssuer(issuer) {
92
+ this.#jwt.iss = issuer;
93
+ return this;
94
+ }
95
+ setSubject(subject) {
96
+ this.#jwt.sub = subject;
97
+ return this;
98
+ }
99
+ setAudience(audience) {
100
+ this.#jwt.aud = audience;
101
+ return this;
102
+ }
103
+ setJti(jwtId) {
104
+ this.#jwt.jti = jwtId;
105
+ return this;
106
+ }
107
+ setNotBefore(input) {
108
+ this.#jwt.nbf = input;
109
+ return this;
110
+ }
111
+ setExpirationTime(input) {
112
+ this.#jwt.exp = input;
113
+ return this;
114
+ }
115
+ setIssuedAt(input) {
116
+ this.#jwt.iat = input;
117
+ return this;
118
+ }
119
+ setProtectedHeader(protectedHeader) {
120
+ this.#protectedHeader = protectedHeader;
121
+ return this;
122
+ }
123
+ async sign(key, options) {
124
+ const sig = new CompactSign(this.#jwt.data());
125
+ sig.setProtectedHeader(this.#protectedHeader);
126
+ if (Array.isArray(this.#protectedHeader?.crit) && this.#protectedHeader.crit.includes("b64") && this.#protectedHeader.b64 === false) throw new JWTInvalid("JWTs MUST NOT use unencoded payload");
127
+ return sig.sign(key, options);
128
+ }
129
+ };
130
+ //#endregion
131
+ //#region src/server/types.ts
132
+ /** OAuth errors safe to serialize at an endpoint or redirect to a registered callback. */
133
+ var OAuthServerError = class extends Error {
134
+ error;
135
+ description;
136
+ status;
137
+ constructor(error, description, status = 400) {
138
+ super(description);
139
+ this.error = error;
140
+ this.description = description;
141
+ this.status = status;
142
+ this.name = "OAuthServerError";
143
+ }
144
+ };
145
+ //#endregion
146
+ //#region src/server/memory.ts
147
+ /** Test-only reference store. Production callers must provide durable transactional persistence. */
148
+ var InMemoryOAuthAuthorizationStorage = class {
149
+ clients = /* @__PURE__ */ new Map();
150
+ codes = /* @__PURE__ */ new Map();
151
+ refreshes = /* @__PURE__ */ new Map();
152
+ usedCodes = /* @__PURE__ */ new Set();
153
+ usedRefreshes = /* @__PURE__ */ new Set();
154
+ revokedFamilies = /* @__PURE__ */ new Set();
155
+ revokedAccess = /* @__PURE__ */ new Map();
156
+ async getClient(clientId) {
157
+ return this.clients.get(clientId) ?? null;
158
+ }
159
+ async registerClient(client) {
160
+ this.clients.set(client.id, client);
161
+ }
162
+ async createAuthorizationCode(grant) {
163
+ this.codes.set(grant.id, grant);
164
+ }
165
+ async consumeAuthorizationCode(input) {
166
+ const grant = this.codes.get(input.id);
167
+ if (!grant || grant.codeHash !== input.codeHash) return { status: "invalid" };
168
+ if (this.usedCodes.has(grant.id)) return { status: "replayed" };
169
+ if (grant.expiresAt <= input.now) return { status: "expired" };
170
+ if (grant.clientId !== input.clientId || grant.redirectUri !== input.redirectUri || grant.resource !== input.resource) return { status: "invalid" };
171
+ this.usedCodes.add(grant.id);
172
+ return {
173
+ status: "consumed",
174
+ grant
175
+ };
176
+ }
177
+ async createRefreshGrant(grant) {
178
+ this.refreshes.set(grant.id, grant);
179
+ }
180
+ async narrowRefreshGrant(input) {
181
+ const grant = this.refreshes.get(input.id);
182
+ if (!grant || this.usedRefreshes.has(grant.id) || this.revokedFamilies.has(grant.familyId) || grant.tokenHash !== input.tokenHash || input.scopes.some((scope) => !grant.scopes.includes(scope))) throw new Error("Invalid refresh narrowing");
183
+ this.refreshes.set(input.id, {
184
+ ...grant,
185
+ scopes: [...input.scopes],
186
+ claims: input.claims
187
+ });
188
+ }
189
+ async rotateRefreshGrant(input) {
190
+ const grant = this.refreshes.get(input.id);
191
+ if (!grant || grant.tokenHash !== input.tokenHash) return { status: "invalid" };
192
+ if (this.revokedFamilies.has(grant.familyId)) return { status: "revoked" };
193
+ if (this.usedRefreshes.has(grant.id)) {
194
+ this.revokedFamilies.add(grant.familyId);
195
+ return { status: "replayed" };
196
+ }
197
+ if (grant.expiresAt <= input.now) return { status: "expired" };
198
+ if (grant.clientId !== input.clientId || grant.resource !== input.resource) return { status: "invalid" };
199
+ this.usedRefreshes.add(grant.id);
200
+ this.refreshes.set(input.replacement.id, {
201
+ ...grant,
202
+ ...input.replacement
203
+ });
204
+ return {
205
+ status: "rotated",
206
+ grant
207
+ };
208
+ }
209
+ async revokeRefreshGrant(input) {
210
+ const grant = this.refreshes.get(input.id);
211
+ if (grant?.tokenHash === input.tokenHash) this.revokedFamilies.add(grant.familyId);
212
+ }
213
+ async revokeAccessToken(input) {
214
+ this.revokedAccess.set(input.jti, input.expiresAt);
215
+ }
216
+ async isAccessTokenRevoked(jti, now) {
217
+ const expiry = this.revokedAccess.get(jti);
218
+ return Boolean(expiry && expiry > now);
219
+ }
220
+ };
221
+ //#endregion
222
+ //#region src/server/index.ts
223
+ var maxRegistrationBodyBytes = 16 * 1024;
224
+ var maxRedirectUris = 10;
225
+ var maxMetadataStringLength = 2048;
226
+ var maxClientNameLength = 256;
227
+ var maxLifetime = 3600 * 24 * 90;
228
+ var scopePattern = /^[\x21\x23-\x5b\x5d-\x7e]+$/u;
229
+ function opaque() {
230
+ const id = randomUUID();
231
+ const secret = randomBytes(32).toString("base64url");
232
+ return {
233
+ id,
234
+ secret,
235
+ value: `${id}.${secret}`,
236
+ hash: hash(secret)
237
+ };
238
+ }
239
+ function hash(value) {
240
+ return createHash("sha256").update(value).digest("base64url");
241
+ }
242
+ function parseOpaque(value) {
243
+ const [id, secret, extra] = value.split(".");
244
+ if (!id || !secret || extra || !/^[0-9a-f-]{36}$/iu.test(id) || !/^[A-Za-z0-9_-]{32,}$/u.test(secret)) return null;
245
+ return {
246
+ id,
247
+ hash: hash(secret)
248
+ };
249
+ }
250
+ function same(a, b) {
251
+ const left = Buffer.from(a);
252
+ const right = Buffer.from(b);
253
+ return left.length === right.length && timingSafeEqual(left, right);
254
+ }
255
+ function scopeList(value) {
256
+ if (!value) return [];
257
+ const scopes = value.split(" ").filter(Boolean);
258
+ if (!scopes.length || scopes.some((scope) => !scopePattern.test(scope))) throw new OAuthServerError("invalid_scope", "Invalid scope.");
259
+ return [...new Set(scopes)];
260
+ }
261
+ function endpoint(issuer, path) {
262
+ return new URL(path, issuer.endsWith("/") ? issuer : `${issuer}/`).href;
263
+ }
264
+ function json(body, status = 200) {
265
+ return new Response(JSON.stringify(body), {
266
+ status,
267
+ headers: {
268
+ "content-type": "application/json",
269
+ "cache-control": "no-store",
270
+ pragma: "no-cache"
271
+ }
272
+ });
273
+ }
274
+ function errorResponse(error) {
275
+ const value = error instanceof OAuthServerError ? error : new OAuthServerError("server_error", "The authorization server could not complete the request.", 500);
276
+ return json({
277
+ error: value.error,
278
+ error_description: value.description
279
+ }, value.status);
280
+ }
281
+ /** Framework-independent OAuth authorization server. Mount {@link handle} in any Fetch-compatible router. */
282
+ var OAuthAuthorizationServer = class {
283
+ issuer;
284
+ issuerUrl;
285
+ options;
286
+ constructor(options) {
287
+ this.issuerUrl = this.assertIssuer(options.issuer);
288
+ if (options.issueRefreshTokens !== false && typeof options.storage.narrowRefreshGrant !== "function") throw new TypeError("OAuth storage must implement narrowRefreshGrant when refresh tokens are issued.");
289
+ this.issuer = this.issuerUrl.href.replace(/\/$/u, "");
290
+ for (const lifetime of [
291
+ options.authorizationCodeLifetimeSeconds ?? 300,
292
+ options.accessTokenLifetimeSeconds ?? 300,
293
+ options.refreshTokenLifetimeSeconds ?? 3600 * 24 * 30
294
+ ]) if (!Number.isInteger(lifetime) || lifetime < 30 || lifetime > maxLifetime) throw new TypeError("OAuth lifetimes must be whole seconds between 30 and 90 days.");
295
+ this.options = {
296
+ ...options,
297
+ authorizationCodeLifetimeSeconds: options.authorizationCodeLifetimeSeconds ?? 300,
298
+ accessTokenLifetimeSeconds: options.accessTokenLifetimeSeconds ?? 300,
299
+ refreshTokenLifetimeSeconds: options.refreshTokenLifetimeSeconds ?? 3600 * 24 * 30,
300
+ issueRefreshTokens: options.issueRefreshTokens !== false,
301
+ dynamicClientRegistration: options.dynamicClientRegistration === true
302
+ };
303
+ }
304
+ /** Parse and validate an authorization request without authenticating a browser session. */
305
+ async parseAuthorizationRequest(input) {
306
+ if (input.get("response_type") !== "code") throw new OAuthServerError("unsupported_response_type", "Only authorization code is supported.");
307
+ const clientId = required(input, "client_id");
308
+ const redirectUri = required(input, "redirect_uri");
309
+ const client = await this.options.storage.getClient(clientId);
310
+ if (!client) throw new OAuthServerError("invalid_request", "Unknown client.");
311
+ this.assertRedirect(client, redirectUri);
312
+ const codeChallenge = required(input, "code_challenge");
313
+ if (!/^[A-Za-z0-9_-]{43,128}$/u.test(codeChallenge) || input.get("code_challenge_method") !== "S256") throw new OAuthServerError("invalid_request", "S256 PKCE is required.");
314
+ const scopes = scopeList(input.get("scope"));
315
+ this.assertScopes(client, scopes);
316
+ const resource = input.get("resource") ?? void 0;
317
+ this.assertResource(client, resource);
318
+ return {
319
+ clientId,
320
+ redirectUri,
321
+ responseType: "code",
322
+ scopes,
323
+ resource,
324
+ state: input.get("state") ?? void 0,
325
+ codeChallenge,
326
+ codeChallengeMethod: "S256"
327
+ };
328
+ }
329
+ /** Records an application-approved authorization decision and returns its redirect URI. */
330
+ async approve(request, consent) {
331
+ if (!consent.subject || consent.subject.length > 512) throw new OAuthServerError("access_denied", "Authorization denied.");
332
+ const client = await this.options.storage.getClient(request.clientId);
333
+ if (!client) throw new OAuthServerError("invalid_request", "Unknown client.");
334
+ this.assertRedirect(client, request.redirectUri);
335
+ this.assertScopes(client, request.scopes);
336
+ this.assertResource(client, request.resource);
337
+ const code = opaque();
338
+ const now = /* @__PURE__ */ new Date();
339
+ const grant = {
340
+ id: code.id,
341
+ codeHash: code.hash,
342
+ clientId: request.clientId,
343
+ subject: consent.subject,
344
+ ...consent.tenantId ? { tenantId: consent.tenantId } : {},
345
+ ...consent.claims ? { claims: consent.claims } : {},
346
+ redirectUri: request.redirectUri,
347
+ scopes: request.scopes,
348
+ ...request.resource ? { resource: request.resource } : {},
349
+ codeChallenge: request.codeChallenge,
350
+ expiresAt: new Date(now.getTime() + this.options.authorizationCodeLifetimeSeconds * 1e3)
351
+ };
352
+ await this.options.storage.createAuthorizationCode(this.applyConsent(grant, consent));
353
+ const redirect = new URL(request.redirectUri);
354
+ redirect.searchParams.set("code", code.value);
355
+ if (request.state) redirect.searchParams.set("state", request.state);
356
+ redirect.searchParams.set("iss", this.issuer);
357
+ return { redirectUri: redirect.href };
358
+ }
359
+ /** Exchanges an authorization code or a rotating refresh token for an access token. */
360
+ async token(input) {
361
+ const grantType = required(input, "grant_type");
362
+ if (grantType === "authorization_code") return this.exchangeCode(input);
363
+ if (grantType === "refresh_token") {
364
+ if (!this.options.issueRefreshTokens) throw new OAuthServerError("unsupported_grant_type", "Refresh tokens are not enabled.");
365
+ return this.refresh(input);
366
+ }
367
+ throw new OAuthServerError("unsupported_grant_type", "Unsupported grant_type.");
368
+ }
369
+ /** RFC 7009-style best-effort revocation; unknown token values deliberately succeed. */
370
+ async revoke(input) {
371
+ const token = required(input, "token");
372
+ const parsed = parseOpaque(token);
373
+ if (parsed) {
374
+ await this.options.storage.revokeRefreshGrant({
375
+ id: parsed.id,
376
+ tokenHash: parsed.hash,
377
+ now: /* @__PURE__ */ new Date()
378
+ });
379
+ return;
380
+ }
381
+ try {
382
+ await this.revokeAccessToken(token);
383
+ } catch (error) {
384
+ if (error instanceof OAuthServerError && error.error === "invalid_token") return;
385
+ if ([
386
+ JWTExpired,
387
+ JWTClaimValidationFailed,
388
+ JWTInvalid,
389
+ JWSInvalid,
390
+ JWSSignatureVerificationFailed,
391
+ JOSEAlgNotAllowed,
392
+ JOSENotSupported
393
+ ].some((ErrorType) => error instanceof ErrorType)) return;
394
+ throw error;
395
+ }
396
+ }
397
+ /** Immediately makes a valid, unexpired access JWT inactive through the injected revocation store. */
398
+ async revokeAccessToken(token) {
399
+ const payload = await this.verifyJwt(token);
400
+ if (typeof payload.jti !== "string" || typeof payload.exp !== "number") throw new OAuthServerError("invalid_token", "Invalid access token.", 401);
401
+ await this.options.storage.revokeAccessToken({
402
+ jti: payload.jti,
403
+ expiresAt: /* @__PURE__ */ new Date(payload.exp * 1e3)
404
+ });
405
+ }
406
+ /**
407
+ * Verifies issuer, signature, expiry, the caller's own resource audience and
408
+ * live server-side access-token revocation. Resource servers must always pass
409
+ * the audience (resource URI) they serve so tokens minted for another
410
+ * resource are rejected.
411
+ */
412
+ async verifyAccessToken(token, audience) {
413
+ if (!audience) throw new TypeError("verifyAccessToken requires the expected audience.");
414
+ return this.verifyJwt(token, audience);
415
+ }
416
+ async verifyJwt(token, audience) {
417
+ const { payload } = await jwtVerify(token, this.options.signingKey.publicKey, {
418
+ issuer: this.issuer,
419
+ typ: "at+jwt",
420
+ requiredClaims: [
421
+ "exp",
422
+ "iat",
423
+ "sub",
424
+ "client_id",
425
+ "scope",
426
+ "aud",
427
+ "jti"
428
+ ],
429
+ ...audience ? { audience } : {},
430
+ algorithms: [this.options.signingKey.algorithm]
431
+ });
432
+ if (typeof payload.jti !== "string" || !payload.jti || typeof payload.sub !== "string" || !payload.sub || typeof payload.client_id !== "string" || !payload.client_id || typeof payload.scope !== "string" || typeof payload.aud !== "string" || !payload.aud || typeof payload.iat !== "number" || !Number.isInteger(payload.iat) || typeof payload.exp !== "number" || !Number.isInteger(payload.exp) || payload.exp <= payload.iat || payload.tenant_id !== void 0 && (typeof payload.tenant_id !== "string" || !payload.tenant_id) || await this.options.storage.isAccessTokenRevoked(payload.jti, /* @__PURE__ */ new Date())) throw new OAuthServerError("invalid_token", "Invalid access token.", 401);
433
+ return payload;
434
+ }
435
+ /** Returns RFC 8414 authorization-server metadata. */
436
+ discovery() {
437
+ return {
438
+ issuer: this.issuer,
439
+ authorization_endpoint: endpoint(this.issuer, "authorize"),
440
+ token_endpoint: endpoint(this.issuer, "token"),
441
+ jwks_uri: endpoint(this.issuer, "jwks"),
442
+ ...this.options.dynamicClientRegistration ? { registration_endpoint: endpoint(this.issuer, "register") } : {},
443
+ response_types_supported: ["code"],
444
+ grant_types_supported: ["authorization_code", ...this.options.issueRefreshTokens ? ["refresh_token"] : []],
445
+ token_endpoint_auth_methods_supported: ["none", "client_secret_post"],
446
+ code_challenge_methods_supported: ["S256"],
447
+ authorization_response_iss_parameter_supported: true,
448
+ scopes_supported: [...this.options.scopes]
449
+ };
450
+ }
451
+ /** Returns the public signing JWK. */
452
+ jwks() {
453
+ return { keys: [{
454
+ ...this.options.signingKey.publicJwk,
455
+ kid: this.options.signingKey.keyId,
456
+ alg: this.options.signingKey.algorithm,
457
+ use: "sig"
458
+ }] };
459
+ }
460
+ /** Safely registers a public authorization-code client when DCR is explicitly enabled. */
461
+ async register(input) {
462
+ if (!this.options.dynamicClientRegistration) throw new OAuthServerError("invalid_request", "Dynamic client registration is disabled.", 404);
463
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new OAuthServerError("invalid_request", "Client metadata must be an object.");
464
+ const value = input;
465
+ if (value.client_name !== void 0 && (typeof value.client_name !== "string" || value.client_name.length > maxClientNameLength) || typeof value.scope === "string" && value.scope.length > maxMetadataStringLength) throw new OAuthServerError("invalid_request", "Client metadata exceeds supported bounds.");
466
+ for (const [field, allowed] of [["grant_types", ["authorization_code", ...this.options.issueRefreshTokens ? ["refresh_token"] : []]], ["response_types", ["code"]]]) {
467
+ const values = value[field];
468
+ if (values !== void 0 && (!Array.isArray(values) || !values.length || values.length > allowed.length || values.some((entry) => typeof entry !== "string" || !allowed.includes(entry)))) throw new OAuthServerError("invalid_request", "Unsupported client metadata.");
469
+ }
470
+ if (value.scope !== void 0 && (typeof value.scope !== "string" || scopeList(value.scope).some((scope) => !this.options.scopes.includes(scope)))) throw new OAuthServerError("invalid_scope", "Unsupported client scope.");
471
+ if (!Array.isArray(value.redirect_uris) || !value.redirect_uris.length || value.redirect_uris.length > maxRedirectUris || value.redirect_uris.some((uri) => typeof uri !== "string" || uri.length > maxMetadataStringLength)) throw new OAuthServerError("invalid_request", "redirect_uris is required.");
472
+ if (value.token_endpoint_auth_method !== void 0 && value.token_endpoint_auth_method !== "none") throw new OAuthServerError("invalid_request", "Only public clients are supported.");
473
+ const client = {
474
+ id: randomUUID(),
475
+ redirectUris: value.redirect_uris.map((uri) => this.assertPublicRedirect(uri)),
476
+ allowedScopes: typeof value.scope === "string" ? scopeList(value.scope) : this.options.scopes,
477
+ tokenEndpointAuthMethod: "none",
478
+ createdAt: /* @__PURE__ */ new Date()
479
+ };
480
+ await this.options.storage.registerClient(client);
481
+ const grantTypes = ["authorization_code", ...this.options.issueRefreshTokens ? ["refresh_token"] : []];
482
+ return {
483
+ client_id: client.id,
484
+ client_id_issued_at: Math.floor(client.createdAt.getTime() / 1e3),
485
+ redirect_uris: [...client.redirectUris],
486
+ token_endpoint_auth_method: "none",
487
+ grant_types: grantTypes,
488
+ response_types: ["code"],
489
+ scope: client.allowedScopes.join(" "),
490
+ ...typeof value.client_name === "string" ? { client_name: value.client_name } : {}
491
+ };
492
+ }
493
+ /** Fetch-native routing for discovery, JWKS, registration, token and revocation endpoints. Authorization approval stays application-owned. */
494
+ async handle(request) {
495
+ try {
496
+ const path = new URL(request.url).pathname;
497
+ const prefix = this.issuerUrl.pathname.replace(/\/$/u, "");
498
+ const route = path.startsWith(`${prefix}/`) ? path.slice(prefix.length) : path;
499
+ if (request.method === "GET" && (path === `/.well-known/oauth-authorization-server${prefix}` || route === "/.well-known/oauth-authorization-server")) return json(this.discovery());
500
+ if (request.method === "GET" && (path === `/.well-known/openid-configuration${prefix}` || route === "/.well-known/openid-configuration")) return json(this.discovery());
501
+ if (request.method === "GET" && route === "/jwks") return json(this.jwks());
502
+ if (request.method === "POST" && route === "/token") return json(await this.token(await form(request)));
503
+ if (request.method === "POST" && route === "/revoke") {
504
+ await this.revoke(await form(request));
505
+ return new Response(null, {
506
+ status: 200,
507
+ headers: { "cache-control": "no-store" }
508
+ });
509
+ }
510
+ if (request.method === "POST" && route === "/register") return json(await this.register(await registrationBody(request)), 201);
511
+ return new Response(null, { status: 404 });
512
+ } catch (error) {
513
+ return errorResponse(error);
514
+ }
515
+ }
516
+ async exchangeCode(input) {
517
+ const client = await this.client(input);
518
+ const raw = parseOpaque(required(input, "code"));
519
+ if (!raw) throw new OAuthServerError("invalid_grant", "Invalid authorization code.");
520
+ const redirectUri = required(input, "redirect_uri");
521
+ const verifier = required(input, "code_verifier");
522
+ if (!/^[A-Za-z0-9._~-]{43,128}$/u.test(verifier)) throw new OAuthServerError("invalid_grant", "Invalid PKCE verifier.");
523
+ const resource = input.get("resource") ?? void 0;
524
+ const result = await this.options.storage.consumeAuthorizationCode({
525
+ id: raw.id,
526
+ codeHash: raw.hash,
527
+ clientId: client.id,
528
+ redirectUri,
529
+ ...resource ? { resource } : {},
530
+ now: /* @__PURE__ */ new Date()
531
+ });
532
+ if (result.status !== "consumed") throw new OAuthServerError("invalid_grant", "Invalid authorization code.");
533
+ if (!same(hash(verifier), result.grant.codeChallenge)) throw new OAuthServerError("invalid_grant", "Invalid authorization code.");
534
+ const consent = await this.revalidate(result.grant);
535
+ return this.issue(this.applyConsent(result.grant, consent), true);
536
+ }
537
+ async refresh(input) {
538
+ const client = await this.client(input);
539
+ const raw = parseOpaque(required(input, "refresh_token"));
540
+ if (!raw) throw new OAuthServerError("invalid_grant", "Invalid refresh token.");
541
+ const resource = input.get("resource") ?? void 0;
542
+ const candidate = opaque();
543
+ const now = /* @__PURE__ */ new Date();
544
+ const replacement = {
545
+ id: candidate.id,
546
+ tokenHash: candidate.hash,
547
+ expiresAt: new Date(now.getTime() + this.options.refreshTokenLifetimeSeconds * 1e3)
548
+ };
549
+ const result = await this.options.storage.rotateRefreshGrant({
550
+ id: raw.id,
551
+ tokenHash: raw.hash,
552
+ clientId: client.id,
553
+ ...resource ? { resource } : {},
554
+ replacement,
555
+ now
556
+ });
557
+ if (result.status !== "rotated") throw new OAuthServerError("invalid_grant", "Invalid refresh token.");
558
+ try {
559
+ const requestedScope = input.get("scope");
560
+ const requestedScopes = requestedScope === null ? result.grant.scopes : [...new Set(requestedScope.split(" ").filter(Boolean))];
561
+ if (requestedScopes.some((scope) => !result.grant.scopes.includes(scope))) throw new OAuthServerError("invalid_scope", "Refresh scope expansion is not allowed.");
562
+ const consent = await this.revalidate(result.grant);
563
+ const authorized = this.applyConsent(result.grant, consent);
564
+ const grant = {
565
+ ...authorized,
566
+ scopes: authorized.scopes.filter((scope) => requestedScopes.includes(scope))
567
+ };
568
+ await this.options.storage.narrowRefreshGrant({
569
+ id: candidate.id,
570
+ tokenHash: candidate.hash,
571
+ scopes: grant.scopes,
572
+ claims: grant.claims
573
+ });
574
+ return await this.issue(grant, false, candidate.value);
575
+ } catch (error) {
576
+ await this.options.storage.revokeRefreshGrant({
577
+ id: raw.id,
578
+ tokenHash: raw.hash,
579
+ now
580
+ });
581
+ throw error;
582
+ }
583
+ }
584
+ async issue(grant, refresh = false, existingRefresh) {
585
+ const now = Math.floor(Date.now() / 1e3);
586
+ const jti = randomUUID();
587
+ const claims = grant.claims ?? {};
588
+ for (const reserved of [
589
+ "scope",
590
+ "client_id",
591
+ "tenant_id",
592
+ "aud",
593
+ "iss",
594
+ "sub",
595
+ "jti",
596
+ "exp",
597
+ "iat",
598
+ "nbf"
599
+ ]) if (reserved in claims) throw new OAuthServerError("invalid_request", "Reserved token claim.");
600
+ const token = await new SignJWT({
601
+ scope: grant.scopes.join(" "),
602
+ client_id: grant.clientId,
603
+ ...grant.tenantId ? { tenant_id: grant.tenantId } : {},
604
+ ...claims
605
+ }).setProtectedHeader({
606
+ alg: this.options.signingKey.algorithm,
607
+ kid: this.options.signingKey.keyId,
608
+ typ: "at+jwt"
609
+ }).setIssuer(this.issuer).setSubject(grant.subject).setJti(jti).setIssuedAt(now).setExpirationTime(now + this.options.accessTokenLifetimeSeconds).setAudience(grant.resource ?? this.issuer).sign(this.options.signingKey.privateKey);
610
+ let refreshToken;
611
+ if (this.options.issueRefreshTokens) if (existingRefresh) refreshToken = existingRefresh;
612
+ else {
613
+ const raw = opaque();
614
+ await this.options.storage.createRefreshGrant({
615
+ id: raw.id,
616
+ tokenHash: raw.hash,
617
+ familyId: randomUUID(),
618
+ clientId: grant.clientId,
619
+ subject: grant.subject,
620
+ ...grant.tenantId ? { tenantId: grant.tenantId } : {},
621
+ ...grant.claims ? { claims: grant.claims } : {},
622
+ scopes: grant.scopes,
623
+ ...grant.resource ? { resource: grant.resource } : {},
624
+ expiresAt: new Date(Date.now() + this.options.refreshTokenLifetimeSeconds * 1e3)
625
+ });
626
+ refreshToken = raw.value;
627
+ }
628
+ return {
629
+ access_token: token,
630
+ token_type: "Bearer",
631
+ expires_in: this.options.accessTokenLifetimeSeconds,
632
+ scope: grant.scopes.join(" "),
633
+ ...refreshToken ? { refresh_token: refreshToken } : {}
634
+ };
635
+ }
636
+ async client(input) {
637
+ const id = required(input, "client_id");
638
+ const client = await this.options.storage.getClient(id);
639
+ if (!client) throw new OAuthServerError("invalid_client", "Invalid client.", 401);
640
+ if (client.tokenEndpointAuthMethod === "client_secret_post") {
641
+ const secret = required(input, "client_secret");
642
+ if (!client.secretHash || !same(hash(secret), client.secretHash)) throw new OAuthServerError("invalid_client", "Invalid client.", 401);
643
+ }
644
+ return client;
645
+ }
646
+ assertRedirect(client, redirectUri) {
647
+ if (!client.redirectUris.includes(redirectUri)) throw new OAuthServerError("invalid_request", "Invalid redirect_uri.");
648
+ }
649
+ assertScopes(client, scopes) {
650
+ if (scopes.some((scope) => !this.options.scopes.includes(scope) || !client.allowedScopes.includes(scope))) throw new OAuthServerError("invalid_scope", "Requested scope is not allowed.");
651
+ }
652
+ assertResource(client, resource) {
653
+ if (!resource) return;
654
+ if (!this.options.resources?.includes(resource) || client.allowedResources && !client.allowedResources.includes(resource)) throw new OAuthServerError("invalid_request", "Requested resource is not allowed.");
655
+ }
656
+ assertIssuer(issuer) {
657
+ let url;
658
+ try {
659
+ url = new URL(issuer);
660
+ } catch {
661
+ throw new TypeError("issuer must be an absolute URL.");
662
+ }
663
+ if (url.protocol !== "https:" || url.search || url.hash || url.username || url.password) throw new TypeError("issuer must be an HTTPS origin or path without query, fragment, or credentials.");
664
+ return url;
665
+ }
666
+ async revalidate(grant) {
667
+ const context = {
668
+ subject: grant.subject,
669
+ ...grant.tenantId ? { tenantId: grant.tenantId } : {},
670
+ ...grant.claims ? { claims: grant.claims } : {}
671
+ };
672
+ const result = this.options.identity.revalidateConsent ? await this.options.identity.revalidateConsent(context, grant) : await this.options.identity.refreshConsent?.(context, grant);
673
+ if (!result) throw new OAuthServerError("invalid_grant", "Authorization is no longer active.");
674
+ return result;
675
+ }
676
+ applyConsent(grant, consent) {
677
+ if (!consent.subject || consent.subject !== grant.subject || consent.tenantId !== grant.tenantId) throw new OAuthServerError("invalid_grant", "Live authorization changed identity or tenant.");
678
+ const scopes = [...new Set(consent.scopes ?? grant.scopes)];
679
+ if (scopes.some((scope) => !grant.scopes.includes(scope))) throw new OAuthServerError("invalid_scope", "Live authorization expanded scope.");
680
+ return {
681
+ ...grant,
682
+ subject: consent.subject,
683
+ ...consent.tenantId ? { tenantId: consent.tenantId } : {},
684
+ claims: consent.claims,
685
+ scopes
686
+ };
687
+ }
688
+ assertPublicRedirect(uri) {
689
+ let url;
690
+ try {
691
+ url = new URL(uri);
692
+ } catch {
693
+ throw new OAuthServerError("invalid_request", "Invalid redirect URI.");
694
+ }
695
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.hash || url.search || url.protocol === "http:" && (!this.options.allowLoopbackRedirects || ![
696
+ "localhost",
697
+ "127.0.0.1",
698
+ "[::1]"
699
+ ].includes(url.hostname))) throw new OAuthServerError("invalid_request", "Invalid redirect URI.");
700
+ return url.href;
701
+ }
702
+ };
703
+ /** Creates a framework-independent OAuth server; callers must provide durable storage in production. */
704
+ function createAuthorizationServer(options) {
705
+ return new OAuthAuthorizationServer(options);
706
+ }
707
+ function required(params, name) {
708
+ const value = params.get(name);
709
+ if (!value) throw new OAuthServerError("invalid_request", `${name} is required.`);
710
+ return value;
711
+ }
712
+ async function form(request) {
713
+ if (!(request.headers.get("content-type") ?? "").startsWith("application/x-www-form-urlencoded")) throw new OAuthServerError("invalid_request", "Form-encoded request required.");
714
+ return new URLSearchParams(new TextDecoder().decode(await boundedBody(request)));
715
+ }
716
+ /** Bound bytes while streaming before endpoint parsing or persistence. */
717
+ async function boundedBody(request) {
718
+ const tooLarge = () => new OAuthServerError("invalid_request", "Request body exceeds 16384 bytes.", 413);
719
+ const declaredLength = request.headers.get("content-length");
720
+ if (declaredLength !== null && Number(declaredLength) > maxRegistrationBodyBytes) {
721
+ await request.body?.cancel().catch(() => {});
722
+ throw tooLarge();
723
+ }
724
+ const reader = request.body?.getReader();
725
+ if (!reader) return /* @__PURE__ */ new Uint8Array();
726
+ const chunks = [];
727
+ let length = 0;
728
+ try {
729
+ while (true) {
730
+ const next = await reader.read();
731
+ if (next.done) break;
732
+ length += next.value.byteLength;
733
+ if (length > maxRegistrationBodyBytes) {
734
+ await reader.cancel().catch(() => {});
735
+ throw tooLarge();
736
+ }
737
+ chunks.push(next.value);
738
+ }
739
+ } finally {
740
+ reader.releaseLock();
741
+ }
742
+ const bytes = new Uint8Array(length);
743
+ let offset = 0;
744
+ for (const chunk of chunks) {
745
+ bytes.set(chunk, offset);
746
+ offset += chunk.byteLength;
747
+ }
748
+ return bytes;
749
+ }
750
+ async function registrationBody(request) {
751
+ const bytes = await boundedBody(request);
752
+ try {
753
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
754
+ } catch {
755
+ throw new OAuthServerError("invalid_request", "Client metadata must be valid JSON.");
756
+ }
757
+ }
758
+ //#endregion
759
+ export { InMemoryOAuthAuthorizationStorage, OAuthAuthorizationServer, OAuthServerError, createAuthorizationServer };
760
+
761
+ //# sourceMappingURL=index.js.map