@getcirrus/oauth-provider 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/dist/index.d.ts +90 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +158 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# @getcirrus/oauth-provider
|
|
2
2
|
|
|
3
|
-
> **🚨 This package has been renamed to `@getcirrus/oauth-provider`**
|
|
4
|
-
>
|
|
5
|
-
> This package is deprecated and will no longer receive updates. Please migrate to [`@getcirrus/oauth-provider`](https://www.npmjs.com/package/@getcirrus/oauth-provider) for the latest features and bug fixes.
|
|
6
|
-
|
|
7
3
|
AT Protocol OAuth 2.1 Authorization Server for Cloudflare Workers.
|
|
8
4
|
|
|
9
5
|
A complete OAuth 2.1 provider implementation that enables "Login with Bluesky" functionality for your PDS. Built specifically for Cloudflare Workers with Durable Objects.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JWK } from "jose";
|
|
1
|
+
import { JWK as JWK$1, JWTPayload } from "jose";
|
|
2
2
|
import { OAuthClientMetadata, OAuthParResponse, OAuthTokenResponse } from "@atproto/oauth-types";
|
|
3
3
|
|
|
4
4
|
//#region src/storage.d.ts
|
|
@@ -49,6 +49,21 @@ interface TokenData {
|
|
|
49
49
|
/** Whether the token has been revoked */
|
|
50
50
|
revoked?: boolean;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* JSON Web Key for client authentication
|
|
54
|
+
*/
|
|
55
|
+
interface JWK {
|
|
56
|
+
kty: string;
|
|
57
|
+
use?: string;
|
|
58
|
+
key_ops?: string[];
|
|
59
|
+
alg?: string;
|
|
60
|
+
kid?: string;
|
|
61
|
+
crv?: string;
|
|
62
|
+
x?: string;
|
|
63
|
+
y?: string;
|
|
64
|
+
n?: string;
|
|
65
|
+
e?: string;
|
|
66
|
+
}
|
|
52
67
|
/**
|
|
53
68
|
* OAuth client metadata (discovered from DID document)
|
|
54
69
|
*/
|
|
@@ -63,6 +78,14 @@ interface ClientMetadata {
|
|
|
63
78
|
logoUri?: string;
|
|
64
79
|
/** Client homepage URI (optional) */
|
|
65
80
|
clientUri?: string;
|
|
81
|
+
/** Token endpoint auth method ("none" for public, "private_key_jwt" for confidential) */
|
|
82
|
+
tokenEndpointAuthMethod?: "none" | "private_key_jwt";
|
|
83
|
+
/** JSON Web Key Set for confidential client authentication */
|
|
84
|
+
jwks?: {
|
|
85
|
+
keys: JWK[];
|
|
86
|
+
};
|
|
87
|
+
/** URI to fetch JWKS from (alternative to inline jwks) */
|
|
88
|
+
jwksUri?: string;
|
|
66
89
|
/** When the metadata was cached (Unix ms) */
|
|
67
90
|
cachedAt?: number;
|
|
68
91
|
}
|
|
@@ -359,7 +382,7 @@ interface DpopProof {
|
|
|
359
382
|
/** Key thumbprint (JWK thumbprint of the proof key) */
|
|
360
383
|
jkt: string;
|
|
361
384
|
/** The public JWK from the proof */
|
|
362
|
-
jwk: JWK;
|
|
385
|
+
jwk: JWK$1;
|
|
363
386
|
}
|
|
364
387
|
/**
|
|
365
388
|
* DPoP verification options
|
|
@@ -546,11 +569,17 @@ declare function isTokenValid(tokenData: TokenData): boolean;
|
|
|
546
569
|
* - default-src 'none': Deny all by default
|
|
547
570
|
* - style-src 'unsafe-inline': Allow inline styles (our CSS is inline)
|
|
548
571
|
* - img-src https: data:: Allow images from HTTPS URLs (client logos) and data URIs
|
|
549
|
-
* - form-action 'self': Form can only POST to same origin
|
|
550
572
|
* - frame-ancestors 'none': Prevent clickjacking by disallowing framing
|
|
551
573
|
* - base-uri 'none': Prevent base tag injection
|
|
574
|
+
*
|
|
575
|
+
* Note: form-action is intentionally omitted. Browser behavior for blocking
|
|
576
|
+
* redirects after form submission is inconsistent - Chrome blocks redirects
|
|
577
|
+
* to URLs not in form-action, while Firefox does not. Since OAuth requires
|
|
578
|
+
* redirecting to the client's callback URL after form submission, we cannot
|
|
579
|
+
* use form-action without breaking the flow in Chrome.
|
|
580
|
+
* See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/form-action
|
|
552
581
|
*/
|
|
553
|
-
declare const CONSENT_UI_CSP = "default-src 'none'; style-src 'unsafe-inline'; img-src https: data:;
|
|
582
|
+
declare const CONSENT_UI_CSP = "default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; frame-ancestors 'none'; base-uri 'none'";
|
|
554
583
|
/**
|
|
555
584
|
* Options for rendering the consent UI
|
|
556
585
|
*/
|
|
@@ -587,5 +616,61 @@ declare function renderConsentUI(options: ConsentUIOptions): string;
|
|
|
587
616
|
*/
|
|
588
617
|
declare function renderErrorPage(error: string, description: string, redirectUri?: string): string;
|
|
589
618
|
//#endregion
|
|
590
|
-
|
|
619
|
+
//#region src/client-auth.d.ts
|
|
620
|
+
/** Expected assertion type for private_key_jwt */
|
|
621
|
+
declare const JWT_BEARER_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
622
|
+
/**
|
|
623
|
+
* Client authentication error
|
|
624
|
+
*/
|
|
625
|
+
declare class ClientAuthError extends Error {
|
|
626
|
+
readonly code: string;
|
|
627
|
+
constructor(message: string, code: string);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Result of client authentication
|
|
631
|
+
*/
|
|
632
|
+
interface ClientAuthResult {
|
|
633
|
+
/** Whether client authentication was performed */
|
|
634
|
+
authenticated: boolean;
|
|
635
|
+
/** The client ID from the assertion (if authenticated) */
|
|
636
|
+
clientId?: string;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Options for client authentication
|
|
640
|
+
*/
|
|
641
|
+
interface ClientAuthOptions {
|
|
642
|
+
/** Token endpoint URL (for audience validation) */
|
|
643
|
+
tokenEndpoint: string;
|
|
644
|
+
/** Fetch function for fetching remote JWKS (for testing) */
|
|
645
|
+
fetch?: typeof globalThis.fetch;
|
|
646
|
+
/** Check if a JTI has been used (for replay prevention) */
|
|
647
|
+
checkJti?: (jti: string) => Promise<boolean>;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Parse client assertion from request parameters
|
|
651
|
+
*/
|
|
652
|
+
declare function parseClientAssertion(params: Record<string, string>): {
|
|
653
|
+
assertionType?: string;
|
|
654
|
+
assertion?: string;
|
|
655
|
+
};
|
|
656
|
+
/**
|
|
657
|
+
* Verify a client assertion JWT
|
|
658
|
+
* @param assertion The JWT assertion
|
|
659
|
+
* @param client The client metadata (with JWKS)
|
|
660
|
+
* @param options Verification options
|
|
661
|
+
* @returns The verified JWT payload
|
|
662
|
+
* @throws ClientAuthError if verification fails
|
|
663
|
+
*/
|
|
664
|
+
declare function verifyClientAssertion(assertion: string, client: ClientMetadata, options: ClientAuthOptions): Promise<JWTPayload>;
|
|
665
|
+
/**
|
|
666
|
+
* Authenticate a client from request parameters
|
|
667
|
+
* @param params Request parameters containing client_id, client_assertion_type, client_assertion
|
|
668
|
+
* @param getClient Function to resolve client metadata
|
|
669
|
+
* @param options Authentication options
|
|
670
|
+
* @returns Authentication result
|
|
671
|
+
* @throws ClientAuthError if authentication fails
|
|
672
|
+
*/
|
|
673
|
+
declare function authenticateClient(params: Record<string, string>, getClient: (clientId: string) => Promise<ClientMetadata | null>, options: ClientAuthOptions): Promise<ClientAuthResult>;
|
|
674
|
+
//#endregion
|
|
675
|
+
export { ACCESS_TOKEN_TTL, ATProtoOAuthProvider, AUTH_CODE_TTL, type AuthCodeData, CONSENT_UI_CSP, ClientAuthError, type ClientAuthOptions, type ClientAuthResult, type ClientMetadata, ClientResolutionError, ClientResolver, type ClientResolverOptions, type ConsentUIOptions, DpopError, type DpopProof, type DpopVerifyOptions, type GenerateTokensOptions, type GeneratedTokens, InMemoryOAuthStorage, type JWK, JWT_BEARER_ASSERTION_TYPE, type OAuthClientMetadata, type OAuthErrorResponse, type OAuthParResponse, type OAuthProviderConfig, type OAuthStorage, type PARData, PARHandler, REFRESH_TOKEN_TTL, RequestBodyError, type TokenData, authenticateClient, buildTokenResponse, createClientResolver, extractAccessToken, generateAuthCode, generateDpopNonce, generateRandomToken, generateTokens, isTokenValid, parseClientAssertion, parseRequestBody, refreshTokens, renderConsentUI, renderErrorPage, verifyClientAssertion, verifyDpopProof, verifyPkceChallenge };
|
|
591
676
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/storage.ts","../src/client-resolver.ts","../src/provider.ts","../src/pkce.ts","../src/dpop.ts","../src/par.ts","../src/tokens.ts","../src/ui.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAQA;AAoBA;AAwBA;AAkBiB,UA9DA,YAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/storage.ts","../src/client-resolver.ts","../src/provider.ts","../src/pkce.ts","../src/dpop.ts","../src/par.ts","../src/tokens.ts","../src/ui.ts","../src/client-auth.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EmbeddedJWK, base64url, calculateJwkThumbprint, errors, jwtVerify } from "jose";
|
|
1
|
+
import { EmbeddedJWK, base64url, calculateJwkThumbprint, createRemoteJWKSet, errors, importJWK, jwtVerify } from "jose";
|
|
2
2
|
import { ensureValidDid } from "@atproto/syntax";
|
|
3
3
|
import { oauthClientMetadataSchema } from "@atproto/oauth-types";
|
|
4
4
|
|
|
@@ -53,7 +53,7 @@ function randomString(byteLength = 32) {
|
|
|
53
53
|
* DPoP (Demonstrating Proof of Possession) verification
|
|
54
54
|
* Implements RFC 9449 using jose library for JWT operations
|
|
55
55
|
*/
|
|
56
|
-
const { JOSEError } = errors;
|
|
56
|
+
const { JOSEError: JOSEError$1 } = errors;
|
|
57
57
|
/**
|
|
58
58
|
* DPoP verification error
|
|
59
59
|
*/
|
|
@@ -110,7 +110,7 @@ async function verifyDpopProof(request, options = {}) {
|
|
|
110
110
|
protectedHeader = result.protectedHeader;
|
|
111
111
|
payload = result.payload;
|
|
112
112
|
} catch (err) {
|
|
113
|
-
if (err instanceof JOSEError) throw new DpopError(`DPoP verification failed: ${err.message}`, "invalid_dpop", { cause: err });
|
|
113
|
+
if (err instanceof JOSEError$1) throw new DpopError(`DPoP verification failed: ${err.message}`, "invalid_dpop", { cause: err });
|
|
114
114
|
throw new DpopError("DPoP verification failed", "invalid_dpop", { cause: err });
|
|
115
115
|
}
|
|
116
116
|
if (!payload.jti || typeof payload.jti !== "string") throw new DpopError("DPoP \"jti\" missing", "invalid_dpop");
|
|
@@ -368,6 +368,9 @@ var ClientResolver = class {
|
|
|
368
368
|
redirectUris: doc.redirect_uris,
|
|
369
369
|
logoUri: doc.logo_uri,
|
|
370
370
|
clientUri: doc.client_uri,
|
|
371
|
+
tokenEndpointAuthMethod: doc.token_endpoint_auth_method ?? "none",
|
|
372
|
+
jwks: doc.jwks,
|
|
373
|
+
jwksUri: doc.jwks_uri,
|
|
371
374
|
cachedAt: Date.now()
|
|
372
375
|
};
|
|
373
376
|
if (this.storage) await this.storage.saveClient(clientId, metadata);
|
|
@@ -534,11 +537,17 @@ function isTokenValid(tokenData) {
|
|
|
534
537
|
* - default-src 'none': Deny all by default
|
|
535
538
|
* - style-src 'unsafe-inline': Allow inline styles (our CSS is inline)
|
|
536
539
|
* - img-src https: data:: Allow images from HTTPS URLs (client logos) and data URIs
|
|
537
|
-
* - form-action 'self': Form can only POST to same origin
|
|
538
540
|
* - frame-ancestors 'none': Prevent clickjacking by disallowing framing
|
|
539
541
|
* - base-uri 'none': Prevent base tag injection
|
|
542
|
+
*
|
|
543
|
+
* Note: form-action is intentionally omitted. Browser behavior for blocking
|
|
544
|
+
* redirects after form submission is inconsistent - Chrome blocks redirects
|
|
545
|
+
* to URLs not in form-action, while Firefox does not. Since OAuth requires
|
|
546
|
+
* redirecting to the client's callback URL after form submission, we cannot
|
|
547
|
+
* use form-action without breaking the flow in Chrome.
|
|
548
|
+
* See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/form-action
|
|
540
549
|
*/
|
|
541
|
-
const CONSENT_UI_CSP = "default-src 'none'; style-src 'unsafe-inline'; img-src https: data:;
|
|
550
|
+
const CONSENT_UI_CSP = "default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; frame-ancestors 'none'; base-uri 'none'";
|
|
542
551
|
/**
|
|
543
552
|
* Escape HTML to prevent XSS
|
|
544
553
|
*/
|
|
@@ -907,6 +916,112 @@ function renderErrorPage(error, description, redirectUri) {
|
|
|
907
916
|
</html>`;
|
|
908
917
|
}
|
|
909
918
|
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/client-auth.ts
|
|
921
|
+
/**
|
|
922
|
+
* Client authentication for confidential clients using private_key_jwt
|
|
923
|
+
* Implements RFC 7523 (JWT Bearer Client Authentication)
|
|
924
|
+
*/
|
|
925
|
+
const { JOSEError } = errors;
|
|
926
|
+
/** Expected assertion type for private_key_jwt */
|
|
927
|
+
const JWT_BEARER_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
928
|
+
/**
|
|
929
|
+
* Client authentication error
|
|
930
|
+
*/
|
|
931
|
+
var ClientAuthError = class extends Error {
|
|
932
|
+
constructor(message, code) {
|
|
933
|
+
super(message);
|
|
934
|
+
this.code = code;
|
|
935
|
+
this.name = "ClientAuthError";
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
/**
|
|
939
|
+
* Parse client assertion from request parameters
|
|
940
|
+
*/
|
|
941
|
+
function parseClientAssertion(params) {
|
|
942
|
+
return {
|
|
943
|
+
assertionType: params.client_assertion_type,
|
|
944
|
+
assertion: params.client_assertion
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Verify a client assertion JWT
|
|
949
|
+
* @param assertion The JWT assertion
|
|
950
|
+
* @param client The client metadata (with JWKS)
|
|
951
|
+
* @param options Verification options
|
|
952
|
+
* @returns The verified JWT payload
|
|
953
|
+
* @throws ClientAuthError if verification fails
|
|
954
|
+
*/
|
|
955
|
+
async function verifyClientAssertion(assertion, client, options) {
|
|
956
|
+
const { tokenEndpoint, fetch: fetchFn = globalThis.fetch.bind(globalThis), checkJti } = options;
|
|
957
|
+
let keyResolver;
|
|
958
|
+
if (client.jwks && client.jwks.keys.length > 0) keyResolver = async (header) => {
|
|
959
|
+
const keys = client.jwks.keys;
|
|
960
|
+
let key;
|
|
961
|
+
if (header.kid) key = keys.find((k) => k.kid === header.kid);
|
|
962
|
+
if (!key) key = keys.find((k) => !k.alg || k.alg === header.alg);
|
|
963
|
+
if (!key) key = keys[0];
|
|
964
|
+
if (!key) throw new ClientAuthError("No suitable key found in client JWKS", "invalid_client");
|
|
965
|
+
const alg = key.alg ?? header.alg;
|
|
966
|
+
return importJWK(key, alg);
|
|
967
|
+
};
|
|
968
|
+
else if (client.jwksUri) keyResolver = createRemoteJWKSet(new URL(client.jwksUri), { [Symbol.for("fetch")]: fetchFn });
|
|
969
|
+
else throw new ClientAuthError("Client has no JWKS configured", "invalid_client");
|
|
970
|
+
let payload;
|
|
971
|
+
try {
|
|
972
|
+
payload = (await jwtVerify(assertion, keyResolver, {
|
|
973
|
+
algorithms: ["ES256"],
|
|
974
|
+
clockTolerance: 30,
|
|
975
|
+
maxTokenAge: "5m"
|
|
976
|
+
})).payload;
|
|
977
|
+
} catch (err) {
|
|
978
|
+
if (err instanceof JOSEError) throw new ClientAuthError(`JWT verification failed: ${err.message}`, "invalid_client");
|
|
979
|
+
throw new ClientAuthError(`JWT verification failed: ${err instanceof Error ? err.message : String(err)}`, "invalid_client");
|
|
980
|
+
}
|
|
981
|
+
if (payload.iss !== client.clientId) throw new ClientAuthError(`JWT issuer mismatch: expected ${client.clientId}, got ${payload.iss}`, "invalid_client");
|
|
982
|
+
if (payload.sub !== client.clientId) throw new ClientAuthError(`JWT subject mismatch: expected ${client.clientId}, got ${payload.sub}`, "invalid_client");
|
|
983
|
+
if (!(Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : []).includes(tokenEndpoint)) throw new ClientAuthError(`JWT audience must include token endpoint: ${tokenEndpoint}`, "invalid_client");
|
|
984
|
+
if (!payload.jti) throw new ClientAuthError("JWT must include jti claim", "invalid_client");
|
|
985
|
+
if (checkJti) {
|
|
986
|
+
if (!await checkJti(payload.jti)) throw new ClientAuthError("JWT has already been used (replay detected)", "invalid_client");
|
|
987
|
+
}
|
|
988
|
+
if (!payload.iat) throw new ClientAuthError("JWT must include iat claim", "invalid_client");
|
|
989
|
+
return payload;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Authenticate a client from request parameters
|
|
993
|
+
* @param params Request parameters containing client_id, client_assertion_type, client_assertion
|
|
994
|
+
* @param getClient Function to resolve client metadata
|
|
995
|
+
* @param options Authentication options
|
|
996
|
+
* @returns Authentication result
|
|
997
|
+
* @throws ClientAuthError if authentication fails
|
|
998
|
+
*/
|
|
999
|
+
async function authenticateClient(params, getClient, options) {
|
|
1000
|
+
const clientId = params.client_id;
|
|
1001
|
+
if (!clientId) throw new ClientAuthError("Missing client_id", "invalid_request");
|
|
1002
|
+
const { assertionType, assertion } = parseClientAssertion(params);
|
|
1003
|
+
const client = await getClient(clientId);
|
|
1004
|
+
if (!client) throw new ClientAuthError(`Unknown client: ${clientId}`, "invalid_client");
|
|
1005
|
+
const authMethod = client.tokenEndpointAuthMethod ?? "none";
|
|
1006
|
+
if (authMethod === "none") {
|
|
1007
|
+
if (assertion || assertionType) throw new ClientAuthError("Client assertion not expected for public client", "invalid_request");
|
|
1008
|
+
return {
|
|
1009
|
+
authenticated: false,
|
|
1010
|
+
clientId
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
if (authMethod === "private_key_jwt") {
|
|
1014
|
+
if (!assertionType || !assertion) throw new ClientAuthError("Client assertion required for confidential client", "invalid_client");
|
|
1015
|
+
if (assertionType !== JWT_BEARER_ASSERTION_TYPE) throw new ClientAuthError(`Unsupported assertion type: ${assertionType}. Expected: ${JWT_BEARER_ASSERTION_TYPE}`, "invalid_client");
|
|
1016
|
+
await verifyClientAssertion(assertion, client, options);
|
|
1017
|
+
return {
|
|
1018
|
+
authenticated: true,
|
|
1019
|
+
clientId
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
throw new ClientAuthError(`Unsupported auth method: ${authMethod}`, "invalid_client");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
910
1025
|
//#endregion
|
|
911
1026
|
//#region src/provider.ts
|
|
912
1027
|
/**
|
|
@@ -1040,7 +1155,7 @@ var ATProtoOAuthProvider = class {
|
|
|
1040
1155
|
const password = params.password ?? null;
|
|
1041
1156
|
const redirectUri = params.redirect_uri;
|
|
1042
1157
|
const state = params.state;
|
|
1043
|
-
const responseMode = params.response_mode ?? "
|
|
1158
|
+
const responseMode = params.response_mode ?? "query";
|
|
1044
1159
|
if (action === "deny") {
|
|
1045
1160
|
const errorUrl = new URL(redirectUri);
|
|
1046
1161
|
if (responseMode === "fragment") {
|
|
@@ -1132,6 +1247,22 @@ var ATProtoOAuthProvider = class {
|
|
|
1132
1247
|
"redirect_uri",
|
|
1133
1248
|
"code_verifier"
|
|
1134
1249
|
]) if (!params[param]) return oauthError("invalid_request", `Missing required parameter: ${param}`);
|
|
1250
|
+
try {
|
|
1251
|
+
await authenticateClient(params, async (clientId) => {
|
|
1252
|
+
if (this.clientResolver) try {
|
|
1253
|
+
return await this.clientResolver.resolveClient(clientId);
|
|
1254
|
+
} catch {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
return this.storage.getClient(clientId);
|
|
1258
|
+
}, {
|
|
1259
|
+
tokenEndpoint: `${this.issuer}/oauth/token`,
|
|
1260
|
+
checkJti: async (jti) => this.storage.checkAndSaveNonce(jti)
|
|
1261
|
+
});
|
|
1262
|
+
} catch (e) {
|
|
1263
|
+
if (e instanceof ClientAuthError) return oauthError(e.code, e.message);
|
|
1264
|
+
return oauthError("invalid_client", "Client authentication failed");
|
|
1265
|
+
}
|
|
1135
1266
|
const codeData = await this.storage.getAuthCode(params.code);
|
|
1136
1267
|
if (!codeData) return oauthError("invalid_grant", "Invalid or expired authorization code");
|
|
1137
1268
|
await this.storage.deleteAuthCode(params.code);
|
|
@@ -1192,6 +1323,22 @@ var ATProtoOAuthProvider = class {
|
|
|
1192
1323
|
async handleRefreshTokenGrant(request, params) {
|
|
1193
1324
|
const refreshToken = params.refresh_token;
|
|
1194
1325
|
if (!refreshToken) return oauthError("invalid_request", "Missing refresh_token parameter");
|
|
1326
|
+
if (params.client_id) try {
|
|
1327
|
+
await authenticateClient(params, async (clientId) => {
|
|
1328
|
+
if (this.clientResolver) try {
|
|
1329
|
+
return await this.clientResolver.resolveClient(clientId);
|
|
1330
|
+
} catch {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
return this.storage.getClient(clientId);
|
|
1334
|
+
}, {
|
|
1335
|
+
tokenEndpoint: `${this.issuer}/oauth/token`,
|
|
1336
|
+
checkJti: async (jti) => this.storage.checkAndSaveNonce(jti)
|
|
1337
|
+
});
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
if (e instanceof ClientAuthError) return oauthError(e.code, e.message);
|
|
1340
|
+
return oauthError("invalid_client", "Client authentication failed");
|
|
1341
|
+
}
|
|
1195
1342
|
const existingData = await this.storage.getTokenByRefresh(refreshToken);
|
|
1196
1343
|
if (!existingData) return oauthError("invalid_grant", "Invalid refresh token");
|
|
1197
1344
|
if (existingData.revoked) return oauthError("invalid_grant", "Token has been revoked");
|
|
@@ -1230,11 +1377,12 @@ var ATProtoOAuthProvider = class {
|
|
|
1230
1377
|
issuer: this.issuer,
|
|
1231
1378
|
authorization_endpoint: `${this.issuer}/oauth/authorize`,
|
|
1232
1379
|
token_endpoint: `${this.issuer}/oauth/token`,
|
|
1380
|
+
userinfo_endpoint: `${this.issuer}/oauth/userinfo`,
|
|
1233
1381
|
response_types_supported: ["code"],
|
|
1234
1382
|
response_modes_supported: ["fragment", "query"],
|
|
1235
1383
|
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
1236
1384
|
code_challenge_methods_supported: ["S256"],
|
|
1237
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
1385
|
+
token_endpoint_auth_methods_supported: ["none", "private_key_jwt"],
|
|
1238
1386
|
scopes_supported: [
|
|
1239
1387
|
"atproto",
|
|
1240
1388
|
"transition:generic",
|
|
@@ -1243,14 +1391,12 @@ var ATProtoOAuthProvider = class {
|
|
|
1243
1391
|
subject_types_supported: ["public"],
|
|
1244
1392
|
authorization_response_iss_parameter_supported: true,
|
|
1245
1393
|
client_id_metadata_document_supported: true,
|
|
1394
|
+
token_endpoint_auth_signing_alg_values_supported: ["ES256"],
|
|
1246
1395
|
...this.enablePAR && {
|
|
1247
1396
|
pushed_authorization_request_endpoint: `${this.issuer}/oauth/par`,
|
|
1248
1397
|
require_pushed_authorization_requests: false
|
|
1249
1398
|
},
|
|
1250
|
-
...this.dpopRequired && {
|
|
1251
|
-
dpop_signing_alg_values_supported: ["ES256"],
|
|
1252
|
-
token_endpoint_auth_signing_alg_values_supported: ["ES256"]
|
|
1253
|
-
}
|
|
1399
|
+
...this.dpopRequired && { dpop_signing_alg_values_supported: ["ES256"] }
|
|
1254
1400
|
};
|
|
1255
1401
|
return new Response(JSON.stringify(metadata), {
|
|
1256
1402
|
status: 200,
|
|
@@ -1391,5 +1537,5 @@ var InMemoryOAuthStorage = class {
|
|
|
1391
1537
|
};
|
|
1392
1538
|
|
|
1393
1539
|
//#endregion
|
|
1394
|
-
export { ACCESS_TOKEN_TTL, ATProtoOAuthProvider, AUTH_CODE_TTL, CONSENT_UI_CSP, ClientResolutionError, ClientResolver, DpopError, InMemoryOAuthStorage, PARHandler, REFRESH_TOKEN_TTL, RequestBodyError, buildTokenResponse, createClientResolver, extractAccessToken, generateAuthCode, generateDpopNonce, generateRandomToken, generateTokens, isTokenValid, parseRequestBody, refreshTokens, renderConsentUI, renderErrorPage, verifyDpopProof, verifyPkceChallenge };
|
|
1540
|
+
export { ACCESS_TOKEN_TTL, ATProtoOAuthProvider, AUTH_CODE_TTL, CONSENT_UI_CSP, ClientAuthError, ClientResolutionError, ClientResolver, DpopError, InMemoryOAuthStorage, JWT_BEARER_ASSERTION_TYPE, PARHandler, REFRESH_TOKEN_TTL, RequestBodyError, authenticateClient, buildTokenResponse, createClientResolver, extractAccessToken, generateAuthCode, generateDpopNonce, generateRandomToken, generateTokens, isTokenValid, parseClientAssertion, parseRequestBody, refreshTokens, renderConsentUI, renderErrorPage, verifyClientAssertion, verifyDpopProof, verifyPkceChallenge };
|
|
1395
1541
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["url: URL","protectedHeader: { alg: string; jwk?: JWK }","payload: {\n\t\tjti?: string;\n\t\thtm?: string;\n\t\thtu?: string;\n\t\tiat?: number;\n\t\tath?: string;\n\t\tnonce?: string;\n\t}","params: Record<string, string>","parData: PARData","response: OAuthParResponse","body: OAuthErrorResponse","code: string","response: Response","doc: OAuthClientMetadata","metadata: ClientMetadata","tokenData: TokenData","descriptions: string[]","params: Record<string, string>","client: ClientMetadata","user: { sub: string; handle: string } | null","authCodeData: AuthCodeData","dpopJkt: string | undefined","metadata: OAuthAuthorizationServerMetadata"],"sources":["../src/pkce.ts","../src/encoding.ts","../src/dpop.ts","../src/par.ts","../src/client-resolver.ts","../src/tokens.ts","../src/ui.ts","../src/provider.ts","../src/storage.ts"],"sourcesContent":["/**\n * PKCE (Proof Key for Code Exchange) verification\n * Implements RFC 7636 with S256 challenge method\n */\n\nimport { base64url } from \"jose\";\n\n/**\n * Generate the S256 code challenge from a verifier\n * challenge = BASE64URL(SHA256(verifier))\n */\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst data = encoder.encode(verifier);\n\tconst hash = await crypto.subtle.digest(\"SHA-256\", data);\n\treturn base64url.encode(new Uint8Array(hash));\n}\n\n/**\n * Verify a PKCE code challenge against a verifier\n * @param verifier The code verifier from the token request\n * @param challenge The code challenge from the authorization request\n * @param method The challenge method (only S256 supported for AT Protocol)\n * @returns true if the verifier matches the challenge\n */\nexport async function verifyPkceChallenge(\n\tverifier: string,\n\tchallenge: string,\n\tmethod: \"S256\"\n): Promise<boolean> {\n\tif (method !== \"S256\") {\n\t\tthrow new Error(\"Only S256 challenge method is supported\");\n\t}\n\n\t// Validate verifier format (RFC 7636 Section 4.1)\n\t// Must be 43-128 characters, unreserved characters only\n\tif (verifier.length < 43 || verifier.length > 128) {\n\t\treturn false;\n\t}\n\tif (!/^[A-Za-z0-9._~-]+$/.test(verifier)) {\n\t\treturn false;\n\t}\n\n\tconst expectedChallenge = await generateCodeChallenge(verifier);\n\treturn expectedChallenge === challenge;\n}\n","/**\n * Shared encoding utilities for OAuth provider\n */\n\nimport { base64url } from \"jose\";\n\n/**\n * Generate a cryptographically random string\n *\n * @param byteLength Number of random bytes (default: 32 = 256 bits)\n * @returns Base64URL-encoded random string\n */\nexport function randomString(byteLength: number = 32): string {\n\tconst buffer = new Uint8Array(byteLength);\n\tcrypto.getRandomValues(buffer);\n\treturn base64url.encode(buffer);\n}\n","/**\n * DPoP (Demonstrating Proof of Possession) verification\n * Implements RFC 9449 using jose library for JWT operations\n */\n\nimport { jwtVerify, EmbeddedJWK, calculateJwkThumbprint, errors, base64url } from \"jose\";\nimport type { JWK } from \"jose\";\nimport { randomString } from \"./encoding.js\";\n\nconst { JOSEError } = errors;\n\n/**\n * Verified DPoP proof data\n */\nexport interface DpopProof {\n\t/** HTTP method from the proof */\n\thtm: string;\n\t/** HTTP URI from the proof (without query/fragment) */\n\thtu: string;\n\t/** Unique proof identifier (for replay prevention) */\n\tjti: string;\n\t/** Access token hash (if present) */\n\tath?: string;\n\t/** Key thumbprint (JWK thumbprint of the proof key) */\n\tjkt: string;\n\t/** The public JWK from the proof */\n\tjwk: JWK;\n}\n\n/**\n * DPoP verification options\n */\nexport interface DpopVerifyOptions {\n\t/** Access token to verify ath claim against (optional) */\n\taccessToken?: string;\n\t/** Allowed signature algorithms (default: ['ES256']) */\n\tallowedAlgorithms?: string[];\n\t/** Expected nonce value (optional, for nonce binding) */\n\texpectedNonce?: string;\n\t/** Max token age in seconds (default: 60) */\n\tmaxTokenAge?: number;\n}\n\n/**\n * DPoP verification error\n */\nexport class DpopError extends Error {\n\treadonly code: string;\n\tconstructor(message: string, code: string, options?: ErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.name = \"DpopError\";\n\t\tthis.code = code;\n\t}\n}\n\n/**\n * Normalize URI for HTU comparison\n * Removes query string and fragment per RFC 9449\n */\nfunction normalizeHtuUrl(url: URL): string {\n\treturn url.origin + url.pathname;\n}\n\n/**\n * Parse and validate HTU claim\n */\nfunction parseHtu(htu: string): string {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(htu);\n\t} catch {\n\t\tthrow new DpopError('DPoP \"htu\" is not a valid URL', \"invalid_dpop\");\n\t}\n\n\tif (url.password || url.username) {\n\t\tthrow new DpopError('DPoP \"htu\" must not contain credentials', \"invalid_dpop\");\n\t}\n\n\tif (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n\t\tthrow new DpopError('DPoP \"htu\" must be http or https', \"invalid_dpop\");\n\t}\n\n\treturn normalizeHtuUrl(url);\n}\n\n/**\n * Verify a DPoP proof from a request\n * Uses jose library for JWT verification\n * @param request The HTTP request containing the DPoP header\n * @param options Verification options\n * @returns The verified proof data\n * @throws DpopError if verification fails\n */\nexport async function verifyDpopProof(\n\trequest: Request,\n\toptions: DpopVerifyOptions = {}\n): Promise<DpopProof> {\n\tconst { allowedAlgorithms = [\"ES256\"], accessToken, expectedNonce, maxTokenAge = 60 } = options;\n\n\tconst dpopHeader = request.headers.get(\"DPoP\");\n\tif (!dpopHeader) {\n\t\tthrow new DpopError(\"Missing DPoP header\", \"missing_dpop\");\n\t}\n\n\tlet protectedHeader: { alg: string; jwk?: JWK };\n\tlet payload: {\n\t\tjti?: string;\n\t\thtm?: string;\n\t\thtu?: string;\n\t\tiat?: number;\n\t\tath?: string;\n\t\tnonce?: string;\n\t};\n\n\ttry {\n\t\tconst result = await jwtVerify(dpopHeader, EmbeddedJWK, {\n\t\t\ttyp: \"dpop+jwt\",\n\t\t\talgorithms: allowedAlgorithms,\n\t\t\tmaxTokenAge,\n\t\t\tclockTolerance: 10,\n\t\t});\n\t\tprotectedHeader = result.protectedHeader as typeof protectedHeader;\n\t\tpayload = result.payload as typeof payload;\n\t} catch (err) {\n\t\tif (err instanceof JOSEError) {\n\t\t\tthrow new DpopError(`DPoP verification failed: ${err.message}`, \"invalid_dpop\", { cause: err });\n\t\t}\n\t\tthrow new DpopError(\"DPoP verification failed\", \"invalid_dpop\", { cause: err });\n\t}\n\n\tif (!payload.jti || typeof payload.jti !== \"string\") {\n\t\tthrow new DpopError('DPoP \"jti\" missing', \"invalid_dpop\");\n\t}\n\n\tif (!payload.htm || typeof payload.htm !== \"string\") {\n\t\tthrow new DpopError('DPoP \"htm\" missing', \"invalid_dpop\");\n\t}\n\n\tif (!payload.htu || typeof payload.htu !== \"string\") {\n\t\tthrow new DpopError('DPoP \"htu\" missing', \"invalid_dpop\");\n\t}\n\n\tif (payload.htm !== request.method) {\n\t\tthrow new DpopError('DPoP \"htm\" mismatch', \"invalid_dpop\");\n\t}\n\n\tconst requestUrl = new URL(request.url);\n\tconst expectedHtu = normalizeHtuUrl(requestUrl);\n\tconst proofHtu = parseHtu(payload.htu);\n\tif (proofHtu !== expectedHtu) {\n\t\tthrow new DpopError('DPoP \"htu\" mismatch', \"invalid_dpop\");\n\t}\n\n\tif (expectedNonce !== undefined && payload.nonce !== expectedNonce) {\n\t\tthrow new DpopError('DPoP \"nonce\" mismatch', \"use_dpop_nonce\");\n\t}\n\n\t// Verify ath (access token hash) binding per RFC 9449 Section 4.3\n\tif (accessToken) {\n\t\tif (!payload.ath) {\n\t\t\tthrow new DpopError('DPoP \"ath\" missing when access token provided', \"invalid_dpop\");\n\t\t}\n\n\t\tconst tokenHash = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(accessToken));\n\t\tconst expectedAth = base64url.encode(new Uint8Array(tokenHash));\n\n\t\tif (payload.ath !== expectedAth) {\n\t\t\tthrow new DpopError('DPoP \"ath\" mismatch', \"invalid_dpop\");\n\t\t}\n\t} else if (payload.ath !== undefined) {\n\t\tthrow new DpopError('DPoP \"ath\" claim not allowed without access token', \"invalid_dpop\");\n\t}\n\n\tconst jwk = protectedHeader.jwk!;\n\tconst jkt = await calculateJwkThumbprint(jwk, \"sha256\");\n\n\treturn Object.freeze({\n\t\thtm: payload.htm,\n\t\thtu: payload.htu,\n\t\tjti: payload.jti,\n\t\tath: payload.ath,\n\t\tjkt,\n\t\tjwk,\n\t});\n}\n\n/**\n * Generate a random DPoP nonce\n * @returns A base64url-encoded random nonce (16 bytes)\n */\nexport function generateDpopNonce(): string {\n\treturn randomString(16);\n}\n","/**\n * PAR (Pushed Authorization Requests) handler\n * Implements RFC 9126\n */\n\nimport type { OAuthParResponse } from \"@atproto/oauth-types\";\nimport type { OAuthStorage, PARData } from \"./storage.js\";\nimport { randomString } from \"./encoding.js\";\nimport { parseRequestBody } from \"./provider.js\";\n\nexport type { OAuthParResponse };\n\n/** PAR request URI prefix per RFC 9126 */\nconst REQUEST_URI_PREFIX = \"urn:ietf:params:oauth:request_uri:\";\n\n/** Default PAR expiration in seconds (90 seconds per RFC recommendation) */\nconst DEFAULT_EXPIRES_IN = 90;\n\n/**\n * OAuth error response\n */\nexport interface OAuthErrorResponse {\n\terror: string;\n\terror_description?: string;\n}\n\n/**\n * Generate a unique request URI\n */\nfunction generateRequestUri(): string {\n\treturn REQUEST_URI_PREFIX + randomString(32);\n}\n\n/**\n * Required OAuth parameters for authorization request\n */\nconst REQUIRED_PARAMS = [\"client_id\", \"redirect_uri\", \"response_type\", \"code_challenge\", \"code_challenge_method\", \"state\"];\n\n/**\n * Handler for Pushed Authorization Requests (PAR)\n */\nexport class PARHandler {\n\tprivate storage: OAuthStorage;\n\tprivate issuer: string;\n\tprivate expiresIn: number;\n\n\t/**\n\t * Create a PAR handler\n\t * @param storage OAuth storage implementation\n\t * @param issuer The OAuth issuer URL\n\t * @param expiresIn PAR expiration time in seconds (default: 90)\n\t */\n\tconstructor(storage: OAuthStorage, issuer: string, expiresIn: number = DEFAULT_EXPIRES_IN) {\n\t\tthis.storage = storage;\n\t\tthis.issuer = issuer;\n\t\tthis.expiresIn = expiresIn;\n\t}\n\n\t/**\n\t * Handle a PAR push request\n\t * POST /oauth/par\n\t * @param request The HTTP request\n\t * @returns Response with request_uri or error\n\t */\n\tasync handlePushRequest(request: Request): Promise<Response> {\n\t\tlet params: Record<string, string>;\n\t\ttry {\n\t\t\tparams = await parseRequestBody(request);\n\t\t} catch (e) {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\te instanceof Error ? e.message : \"Invalid request\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tconst clientId = params.client_id;\n\t\tif (!clientId) {\n\t\t\treturn this.errorResponse(\"invalid_request\", \"Missing client_id parameter\", 400);\n\t\t}\n\n\t\tfor (const param of REQUIRED_PARAMS) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn this.errorResponse(\"invalid_request\", `Missing required parameter: ${param}`, 400);\n\t\t\t}\n\t\t}\n\n\t\tif (params.response_type !== \"code\") {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"unsupported_response_type\",\n\t\t\t\t\"Only response_type=code is supported\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tif (params.code_challenge_method !== \"S256\") {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t\"Only code_challenge_method=S256 is supported\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tconst codeChallenge = params.code_challenge!;\n\t\tif (!/^[A-Za-z0-9_-]{43}$/.test(codeChallenge)) {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t\"Invalid code_challenge format\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\tnew URL(params.redirect_uri!);\n\t\t} catch {\n\t\t\treturn this.errorResponse(\"invalid_request\", \"Invalid redirect_uri\", 400);\n\t\t}\n\n\t\tconst requestUri = generateRequestUri();\n\t\tconst expiresAt = Date.now() + this.expiresIn * 1000;\n\n\t\tconst parData: PARData = {\n\t\t\tclientId,\n\t\t\tparams,\n\t\t\texpiresAt,\n\t\t};\n\n\t\tawait this.storage.savePAR(requestUri, parData);\n\n\t\tconst response: OAuthParResponse = {\n\t\t\trequest_uri: requestUri,\n\t\t\texpires_in: this.expiresIn,\n\t\t};\n\n\t\treturn new Response(JSON.stringify(response), {\n\t\t\tstatus: 201,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Retrieve and consume PAR parameters\n\t * Called during authorization request handling\n\t * @param requestUri The request URI from the authorization request\n\t * @param clientId The client_id from the authorization request (for verification)\n\t * @returns The stored parameters or null if not found/expired\n\t */\n\tasync retrieveParams(\n\t\trequestUri: string,\n\t\tclientId: string\n\t): Promise<Record<string, string> | null> {\n\t\tif (!requestUri.startsWith(REQUEST_URI_PREFIX)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst parData = await this.storage.getPAR(requestUri);\n\t\tif (!parData) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (parData.clientId !== clientId) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// One-time use: delete after retrieval\n\t\tawait this.storage.deletePAR(requestUri);\n\n\t\treturn parData.params;\n\t}\n\n\t/**\n\t * Check if a request_uri is valid format\n\t */\n\tstatic isRequestUri(value: string): boolean {\n\t\treturn value.startsWith(REQUEST_URI_PREFIX);\n\t}\n\n\t/**\n\t * Create an OAuth error response\n\t */\n\tprivate errorResponse(\n\t\terror: string,\n\t\tdescription: string,\n\t\tstatus: number = 400\n\t): Response {\n\t\tconst body: OAuthErrorResponse = {\n\t\t\terror,\n\t\t\terror_description: description,\n\t\t};\n\t\treturn new Response(JSON.stringify(body), {\n\t\t\tstatus,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n}\n","/**\n * Client resolver for DID-based client discovery\n * Resolves OAuth client metadata from DIDs for AT Protocol\n */\n\nimport { ensureValidDid } from \"@atproto/syntax\";\nimport {\n\toauthClientMetadataSchema,\n\ttype OAuthClientMetadata,\n} from \"@atproto/oauth-types\";\nimport type { ClientMetadata, OAuthStorage } from \"./storage.js\";\n\nexport type { OAuthClientMetadata };\n\n/**\n * Client resolution error\n */\nexport class ClientResolutionError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly code: string\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"ClientResolutionError\";\n\t}\n}\n\n/**\n * Options for client resolution\n */\nexport interface ClientResolverOptions {\n\t/** Storage for caching client metadata */\n\tstorage?: OAuthStorage;\n\t/** Cache TTL in milliseconds (default: 1 hour) */\n\tcacheTtl?: number;\n\t/** Fetch function for making HTTP requests (for testing) */\n\tfetch?: typeof globalThis.fetch;\n}\n\n/**\n * Check if a string is a valid HTTPS URL\n */\nfunction isHttpsUrl(value: string): boolean {\n\ttry {\n\t\tconst url = new URL(value);\n\t\treturn url.protocol === \"https:\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Validate that a string is a valid DID using @atproto/syntax\n */\nfunction isValidDid(value: string): boolean {\n\ttry {\n\t\tensureValidDid(value);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Get the client metadata URL from a client ID\n * Supports both URL-based and DID-based client IDs\n */\nfunction getClientMetadataUrl(clientId: string): string | null {\n\t// URL-based client ID: the URL itself is the metadata endpoint\n\tif (isHttpsUrl(clientId)) {\n\t\treturn clientId;\n\t}\n\n\t// DID-based client ID: derive the metadata URL\n\tif (clientId.startsWith(\"did:web:\")) {\n\t\t// did:web:example.com -> https://example.com/.well-known/oauth-client-metadata\n\t\t// did:web:example.com:path -> https://example.com/path/.well-known/oauth-client-metadata\n\t\tconst parts = clientId.slice(8).split(\":\");\n\t\tconst host = parts[0]!.replace(/%3A/g, \":\");\n\t\tconst path = parts.slice(1).join(\"/\");\n\t\tconst baseUrl = `https://${host}${path ? \"/\" + path : \"\"}`;\n\t\treturn `${baseUrl}/.well-known/oauth-client-metadata`;\n\t}\n\n\t// Unsupported client ID format\n\treturn null;\n}\n\n/**\n * Resolve client metadata from a DID\n */\nexport class ClientResolver {\n\tprivate storage?: OAuthStorage;\n\tprivate cacheTtl: number;\n\tprivate fetchFn: typeof globalThis.fetch;\n\n\tconstructor(options: ClientResolverOptions = {}) {\n\t\tthis.storage = options.storage;\n\t\tthis.cacheTtl = options.cacheTtl ?? 60 * 60 * 1000; // 1 hour default\n\t\tthis.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);\n\t}\n\n\t/**\n\t * Resolve client metadata from a client ID (URL or DID)\n\t * @param clientId The client ID (HTTPS URL or DID)\n\t * @returns The client metadata\n\t * @throws ClientResolutionError if resolution fails\n\t */\n\tasync resolveClient(clientId: string): Promise<ClientMetadata> {\n\t\tif (!isHttpsUrl(clientId) && !isValidDid(clientId)) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Invalid client ID format: ${clientId}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tif (this.storage) {\n\t\t\tconst cached = await this.storage.getClient(clientId);\n\t\t\tif (cached && cached.cachedAt && Date.now() - cached.cachedAt < this.cacheTtl) {\n\t\t\t\treturn cached;\n\t\t\t}\n\t\t}\n\n\t\tconst metadataUrl = getClientMetadataUrl(clientId);\n\t\tif (!metadataUrl) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Unsupported client ID format: ${clientId}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await this.fetchFn(metadataUrl, {\n\t\t\t\theaders: {\n\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (e) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Failed to fetch client metadata: ${e}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Client metadata fetch failed with status ${response.status}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tlet doc: OAuthClientMetadata;\n\t\ttry {\n\t\t\tconst json = await response.json();\n\t\t\tdoc = oauthClientMetadataSchema.parse(json);\n\t\t} catch (e) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Invalid client metadata: ${e instanceof Error ? e.message : \"validation failed\"}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tif (doc.client_id !== clientId) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Client ID mismatch: expected ${clientId}, got ${doc.client_id}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tconst metadata: ClientMetadata = {\n\t\t\tclientId: doc.client_id,\n\t\t\tclientName: doc.client_name ?? clientId,\n\t\t\tredirectUris: doc.redirect_uris,\n\t\t\tlogoUri: doc.logo_uri,\n\t\t\tclientUri: doc.client_uri,\n\t\t\tcachedAt: Date.now(),\n\t\t};\n\n\t\tif (this.storage) {\n\t\t\tawait this.storage.saveClient(clientId, metadata);\n\t\t}\n\n\t\treturn metadata;\n\t}\n\n\t/**\n\t * Validate that a redirect URI is allowed for a client\n\t * @param clientId The client DID\n\t * @param redirectUri The redirect URI to validate\n\t * @returns true if the redirect URI is allowed\n\t */\n\tasync validateRedirectUri(clientId: string, redirectUri: string): Promise<boolean> {\n\t\ttry {\n\t\t\tconst metadata = await this.resolveClient(clientId);\n\t\t\treturn metadata.redirectUris.includes(redirectUri);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/**\n * Create a client resolver with optional caching\n */\nexport function createClientResolver(options: ClientResolverOptions = {}): ClientResolver {\n\treturn new ClientResolver(options);\n}\n","/**\n * Token generation and validation\n * Generates opaque tokens (not JWTs) that are stored in the database\n */\n\nimport type { OAuthTokenResponse } from \"@atproto/oauth-types\";\nimport type { TokenData } from \"./storage.js\";\nimport { randomString } from \"./encoding.js\";\n\n/** Default access token TTL: 1 hour */\nexport const ACCESS_TOKEN_TTL = 60 * 60 * 1000;\n\n/** Default refresh token TTL: 90 days */\nexport const REFRESH_TOKEN_TTL = 90 * 24 * 60 * 60 * 1000;\n\n/** Authorization code TTL: 5 minutes */\nexport const AUTH_CODE_TTL = 5 * 60 * 1000;\n\n/**\n * Generate a cryptographically random token\n * @param bytes Number of random bytes (default: 32)\n * @returns Base64URL-encoded token\n */\nexport function generateRandomToken(bytes: number = 32): string {\n\treturn randomString(bytes);\n}\n\n/**\n * Generate an authorization code\n * @returns A random authorization code\n */\nexport function generateAuthCode(): string {\n\treturn generateRandomToken(32);\n}\n\n/**\n * Token generation result\n */\nexport interface GeneratedTokens {\n\t/** Opaque access token */\n\taccessToken: string;\n\t/** Opaque refresh token */\n\trefreshToken: string;\n\t/** Access token type (Bearer or DPoP) */\n\ttokenType: \"Bearer\" | \"DPoP\";\n\t/** Access token expiration in seconds */\n\texpiresIn: number;\n\t/** Scope granted */\n\tscope: string;\n\t/** Subject (user DID) */\n\tsub: string;\n}\n\n/**\n * Options for token generation\n */\nexport interface GenerateTokensOptions {\n\t/** User DID */\n\tsub: string;\n\t/** Client DID */\n\tclientId: string;\n\t/** Scope granted */\n\tscope: string;\n\t/** DPoP key thumbprint (if using DPoP) */\n\tdpopJkt?: string;\n\t/** Custom access token TTL in ms (default: 1 hour) */\n\taccessTokenTtl?: number;\n\t/** Custom refresh token TTL in ms (default: 90 days) */\n\trefreshTokenTtl?: number;\n}\n\n/**\n * Generate access and refresh tokens\n * Tokens are opaque - their meaning comes from the database entry\n * @param options Token generation options\n * @returns Generated tokens and metadata\n */\nexport function generateTokens(options: GenerateTokensOptions): {\n\ttokens: GeneratedTokens;\n\ttokenData: TokenData;\n} {\n\tconst {\n\t\tsub,\n\t\tclientId,\n\t\tscope,\n\t\tdpopJkt,\n\t\taccessTokenTtl = ACCESS_TOKEN_TTL,\n\t} = options;\n\n\tconst accessToken = generateRandomToken(32);\n\tconst refreshToken = generateRandomToken(32);\n\tconst now = Date.now();\n\n\tconst tokenData: TokenData = {\n\t\taccessToken,\n\t\trefreshToken,\n\t\tclientId,\n\t\tsub,\n\t\tscope,\n\t\tdpopJkt,\n\t\tissuedAt: now,\n\t\texpiresAt: now + accessTokenTtl,\n\t\trevoked: false,\n\t};\n\n\tconst tokens: GeneratedTokens = {\n\t\taccessToken,\n\t\trefreshToken,\n\t\ttokenType: dpopJkt ? \"DPoP\" : \"Bearer\",\n\t\texpiresIn: Math.floor(accessTokenTtl / 1000),\n\t\tscope,\n\t\tsub,\n\t};\n\n\treturn { tokens, tokenData };\n}\n\n/**\n * Refresh tokens - generates new access token, optionally rotates refresh token\n * @param existingData The existing token data\n * @param rotateRefreshToken Whether to generate a new refresh token\n * @param accessTokenTtl Custom access token TTL in ms\n * @returns Updated tokens and token data\n */\nexport function refreshTokens(\n\texistingData: TokenData,\n\trotateRefreshToken: boolean = false,\n\taccessTokenTtl: number = ACCESS_TOKEN_TTL\n): {\n\ttokens: GeneratedTokens;\n\ttokenData: TokenData;\n} {\n\tconst accessToken = generateRandomToken(32);\n\tconst refreshToken = rotateRefreshToken ? generateRandomToken(32) : existingData.refreshToken;\n\tconst now = Date.now();\n\n\tconst tokenData: TokenData = {\n\t\t...existingData,\n\t\taccessToken,\n\t\trefreshToken,\n\t\tissuedAt: now,\n\t\texpiresAt: now + accessTokenTtl,\n\t};\n\n\tconst tokens: GeneratedTokens = {\n\t\taccessToken,\n\t\trefreshToken,\n\t\ttokenType: existingData.dpopJkt ? \"DPoP\" : \"Bearer\",\n\t\texpiresIn: Math.floor(accessTokenTtl / 1000),\n\t\tscope: existingData.scope,\n\t\tsub: existingData.sub,\n\t};\n\n\treturn { tokens, tokenData };\n}\n\n/**\n * Build token response for OAuth token endpoint\n * @param tokens The generated tokens\n * @returns JSON-serializable token response\n */\nexport function buildTokenResponse(tokens: GeneratedTokens): OAuthTokenResponse {\n\treturn {\n\t\taccess_token: tokens.accessToken,\n\t\ttoken_type: tokens.tokenType,\n\t\texpires_in: tokens.expiresIn,\n\t\trefresh_token: tokens.refreshToken,\n\t\tscope: tokens.scope,\n\t\tsub: tokens.sub,\n\t};\n}\n\n/**\n * Extract access token from Authorization header\n * Supports both Bearer and DPoP token types\n * @param request The HTTP request\n * @returns The access token and type, or null if not found\n */\nexport function extractAccessToken(\n\trequest: Request\n): { token: string; type: \"Bearer\" | \"DPoP\" } | null {\n\tconst authHeader = request.headers.get(\"Authorization\");\n\tif (!authHeader) {\n\t\treturn null;\n\t}\n\n\tif (authHeader.startsWith(\"Bearer \")) {\n\t\treturn {\n\t\t\ttoken: authHeader.slice(7),\n\t\t\ttype: \"Bearer\",\n\t\t};\n\t}\n\n\tif (authHeader.startsWith(\"DPoP \")) {\n\t\treturn {\n\t\t\ttoken: authHeader.slice(5),\n\t\t\ttype: \"DPoP\",\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Validate that a token is not expired or revoked\n * @param tokenData The token data from storage\n * @returns true if the token is valid\n */\nexport function isTokenValid(tokenData: TokenData): boolean {\n\tif (tokenData.revoked) {\n\t\treturn false;\n\t}\n\tif (Date.now() > tokenData.expiresAt) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n","/**\n * Authorization consent UI\n * Renders the HTML page for user consent during OAuth authorization\n */\n\nimport type { ClientMetadata } from \"./storage.js\";\n\n/**\n * Content Security Policy for the consent UI\n *\n * - default-src 'none': Deny all by default\n * - style-src 'unsafe-inline': Allow inline styles (our CSS is inline)\n * - img-src https: data:: Allow images from HTTPS URLs (client logos) and data URIs\n * - form-action 'self': Form can only POST to same origin\n * - frame-ancestors 'none': Prevent clickjacking by disallowing framing\n * - base-uri 'none': Prevent base tag injection\n */\nexport const CONSENT_UI_CSP =\n\t\"default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; form-action 'self'; frame-ancestors 'none'; base-uri 'none'\";\n\n/**\n * Escape HTML to prevent XSS\n */\nfunction escapeHtml(text: string): string {\n\treturn text\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n\n/**\n * Parse scope string into human-readable descriptions\n */\nfunction getScopeDescriptions(scope: string): string[] {\n\tconst scopes = scope.split(\" \").filter(Boolean);\n\tconst descriptions: string[] = [];\n\n\tfor (const s of scopes) {\n\t\tswitch (s) {\n\t\t\tcase \"atproto\":\n\t\t\t\tdescriptions.push(\"Access your AT Protocol account\");\n\t\t\t\tbreak;\n\t\t\tcase \"transition:generic\":\n\t\t\t\tdescriptions.push(\"Perform account operations\");\n\t\t\t\tbreak;\n\t\t\tcase \"transition:chat.bsky\":\n\t\t\t\tdescriptions.push(\"Access chat functionality\");\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t// Don't show unknown scopes to avoid confusion\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t// If no recognized scopes, show a generic message\n\tif (descriptions.length === 0) {\n\t\tdescriptions.push(\"Access your account on your behalf\");\n\t}\n\n\treturn descriptions;\n}\n\n/**\n * Options for rendering the consent UI\n */\nexport interface ConsentUIOptions {\n\t/** The OAuth client metadata */\n\tclient: ClientMetadata;\n\t/** The requested scope */\n\tscope: string;\n\t/** URL to POST the consent form to */\n\tauthorizeUrl: string;\n\t/** State parameter to include in the form */\n\tstate: string;\n\t/** OAuth parameters to include as hidden fields */\n\toauthParams: Record<string, string>;\n\t/** User's handle (for display) */\n\tuserHandle?: string;\n\t/** Whether to show a login form instead of consent */\n\tshowLogin?: boolean;\n\t/** Error message to display */\n\terror?: string;\n}\n\n/**\n * Render the consent UI HTML\n * @param options Consent UI options\n * @returns HTML string\n */\nexport function renderConsentUI(options: ConsentUIOptions): string {\n\tconst { client, scope, authorizeUrl, oauthParams, userHandle, showLogin, error } = options;\n\n\tconst clientName = escapeHtml(client.clientName);\n\tconst scopeDescriptions = getScopeDescriptions(scope);\n\tconst logoHtml = client.logoUri\n\t\t? `<img src=\"${escapeHtml(client.logoUri)}\" alt=\"${clientName} logo\" class=\"app-logo\" />`\n\t\t: `<div class=\"app-logo-placeholder\">${clientName.charAt(0).toUpperCase()}</div>`;\n\n\tconst errorHtml = error\n\t\t? `<div class=\"error-message\">${escapeHtml(error)}</div>`\n\t\t: \"\";\n\n\tconst loginFormHtml = showLogin\n\t\t? `\n\t\t\t<div class=\"login-form\">\n\t\t\t\t<p>Sign in to continue</p>\n\t\t\t\t<input type=\"password\" name=\"password\" placeholder=\"Password\" required autocomplete=\"current-password\" />\n\t\t\t</div>\n\t\t`\n\t\t: \"\";\n\n\t// Render OAuth params as hidden form fields\n\tconst hiddenFieldsHtml = Object.entries(oauthParams)\n\t\t.map(([key, value]) => `<input type=\"hidden\" name=\"${escapeHtml(key)}\" value=\"${escapeHtml(value)}\" />`)\n\t\t.join(\"\\n\\t\\t\\t\");\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>Authorize ${clientName}</title>\n\t<style>\n\t\t* {\n\t\t\tbox-sizing: border-box;\n\t\t\tmargin: 0;\n\t\t\tpadding: 0;\n\t\t}\n\n\t\tbody {\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n\t\t\tbackground: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n\t\t\tmin-height: 100vh;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tpadding: 20px;\n\t\t\tcolor: #e0e0e0;\n\t\t}\n\n\t\t.container {\n\t\t\tbackground: #1e1e30;\n\t\t\tborder-radius: 16px;\n\t\t\tpadding: 32px;\n\t\t\tmax-width: 400px;\n\t\t\twidth: 100%;\n\t\t\tbox-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t}\n\n\t\t.header {\n\t\t\ttext-align: center;\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.app-logo {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tborder-radius: 12px;\n\t\t\tmargin-bottom: 16px;\n\t\t\tobject-fit: cover;\n\t\t}\n\n\t\t.app-logo-placeholder {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tborder-radius: 12px;\n\t\t\tmargin: 0 auto 16px;\n\t\t\tbackground: linear-gradient(135deg, #3b82f6, #8b5cf6);\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tfont-size: 28px;\n\t\t\tfont-weight: 600;\n\t\t\tcolor: white;\n\t\t}\n\n\t\th1 {\n\t\t\tfont-size: 20px;\n\t\t\tfont-weight: 600;\n\t\t\tmargin-bottom: 8px;\n\t\t}\n\n\t\t.client-name {\n\t\t\tcolor: #60a5fa;\n\t\t}\n\n\t\t.user-info {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t}\n\n\t\t.permissions {\n\t\t\tbackground: rgba(255, 255, 255, 0.05);\n\t\t\tborder-radius: 12px;\n\t\t\tpadding: 16px;\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.permissions-title {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t\tmargin-bottom: 12px;\n\t\t}\n\n\t\t.permissions-list {\n\t\t\tlist-style: none;\n\t\t}\n\n\t\t.permissions-list li {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: 10px;\n\t\t\tpadding: 8px 0;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\t.permissions-list li::before {\n\t\t\tcontent: \"\";\n\t\t\twidth: 8px;\n\t\t\theight: 8px;\n\t\t\tbackground: #22c55e;\n\t\t\tborder-radius: 50%;\n\t\t\tflex-shrink: 0;\n\t\t}\n\n\t\t.buttons {\n\t\t\tdisplay: flex;\n\t\t\tgap: 12px;\n\t\t}\n\n\t\tbutton {\n\t\t\tflex: 1;\n\t\t\tpadding: 12px 20px;\n\t\t\tborder-radius: 8px;\n\t\t\tfont-size: 14px;\n\t\t\tfont-weight: 500;\n\t\t\tcursor: pointer;\n\t\t\ttransition: all 0.2s;\n\t\t\tborder: none;\n\t\t}\n\n\t\t.btn-deny {\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t\tcolor: #e0e0e0;\n\t\t}\n\n\t\t.btn-deny:hover {\n\t\t\tbackground: rgba(255, 255, 255, 0.15);\n\t\t}\n\n\t\t.btn-allow {\n\t\t\tbackground: linear-gradient(135deg, #3b82f6, #2563eb);\n\t\t\tcolor: white;\n\t\t}\n\n\t\t.btn-allow:hover {\n\t\t\tbackground: linear-gradient(135deg, #2563eb, #1d4ed8);\n\t\t}\n\n\t\t.info {\n\t\t\tmargin-top: 16px;\n\t\t\tfont-size: 12px;\n\t\t\tcolor: #6b7280;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.error-message {\n\t\t\tbackground: rgba(239, 68, 68, 0.1);\n\t\t\tborder: 1px solid rgba(239, 68, 68, 0.3);\n\t\t\tcolor: #f87171;\n\t\t\tpadding: 12px;\n\t\t\tborder-radius: 8px;\n\t\t\tmargin-bottom: 16px;\n\t\t\tfont-size: 14px;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.login-form {\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.login-form p {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t\tmargin-bottom: 12px;\n\t\t}\n\n\t\t.login-form input {\n\t\t\twidth: 100%;\n\t\t\tpadding: 12px;\n\t\t\tborder-radius: 8px;\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t\tbackground: rgba(255, 255, 255, 0.05);\n\t\t\tcolor: #e0e0e0;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\t.login-form input:focus {\n\t\t\toutline: none;\n\t\t\tborder-color: #3b82f6;\n\t\t}\n\n\t\t.login-form input::placeholder {\n\t\t\tcolor: #6b7280;\n\t\t}\n\n\t\t.client-uri {\n\t\t\tfont-size: 12px;\n\t\t\tcolor: #6b7280;\n\t\t\tmargin-top: 4px;\n\t\t}\n\n\t\t.client-uri a {\n\t\t\tcolor: #60a5fa;\n\t\t\ttext-decoration: none;\n\t\t}\n\n\t\t.client-uri a:hover {\n\t\t\ttext-decoration: underline;\n\t\t}\n\t</style>\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<div class=\"header\">\n\t\t\t${logoHtml}\n\t\t\t<h1>Authorize <span class=\"client-name\">${clientName}</span></h1>\n\t\t\t${userHandle ? `<p class=\"user-info\">as @${escapeHtml(userHandle)}</p>` : \"\"}\n\t\t\t${client.clientUri ? `<p class=\"client-uri\"><a href=\"${escapeHtml(client.clientUri)}\" target=\"_blank\" rel=\"noopener\">${escapeHtml(new URL(client.clientUri).hostname)}</a></p>` : \"\"}\n\t\t</div>\n\n\t\t${errorHtml}\n\n\t\t<form method=\"POST\" action=\"${escapeHtml(authorizeUrl)}\">\n\t\t\t${hiddenFieldsHtml}\n\n\t\t\t${loginFormHtml}\n\n\t\t\t<div class=\"permissions\">\n\t\t\t\t<p class=\"permissions-title\">This app wants to:</p>\n\t\t\t\t<ul class=\"permissions-list\">\n\t\t\t\t\t${scopeDescriptions.map((desc) => `<li>${escapeHtml(desc)}</li>`).join(\"\")}\n\t\t\t\t</ul>\n\t\t\t</div>\n\n\t\t\t<div class=\"buttons\">\n\t\t\t\t<button type=\"submit\" name=\"action\" value=\"deny\" class=\"btn-deny\">Deny</button>\n\t\t\t\t<button type=\"submit\" name=\"action\" value=\"allow\" class=\"btn-allow\">Allow</button>\n\t\t\t</div>\n\t\t</form>\n\n\t\t<p class=\"info\">You can revoke access anytime in your account settings.</p>\n\t</div>\n</body>\n</html>`;\n}\n\n/**\n * Render an error page\n * @param error Error code\n * @param description Error description\n * @param redirectUri Optional redirect URI for the error\n * @returns HTML string\n */\nexport function renderErrorPage(\n\terror: string,\n\tdescription: string,\n\tredirectUri?: string\n): string {\n\tconst escapedError = escapeHtml(error);\n\tconst escapedDescription = escapeHtml(description);\n\n\tconst redirectHtml = redirectUri\n\t\t? `<p style=\"margin-top: 16px;\"><a href=\"${escapeHtml(redirectUri)}\" style=\"color: #60a5fa;\">Return to application</a></p>`\n\t\t: \"\";\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>Authorization Error</title>\n\t<style>\n\t\tbody {\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n\t\t\tbackground: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n\t\t\tmin-height: 100vh;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tpadding: 20px;\n\t\t\tcolor: #e0e0e0;\n\t\t\tmargin: 0;\n\t\t}\n\n\t\t.container {\n\t\t\tbackground: #1e1e30;\n\t\t\tborder-radius: 16px;\n\t\t\tpadding: 32px;\n\t\t\tmax-width: 400px;\n\t\t\twidth: 100%;\n\t\t\tbox-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.error-icon {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tbackground: rgba(239, 68, 68, 0.1);\n\t\t\tborder-radius: 50%;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tmargin: 0 auto 16px;\n\t\t\tfont-size: 32px;\n\t\t}\n\n\t\th1 {\n\t\t\tfont-size: 20px;\n\t\t\tmargin-bottom: 8px;\n\t\t\tcolor: #f87171;\n\t\t}\n\n\t\tp {\n\t\t\tcolor: #9ca3af;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\tcode {\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t\tpadding: 2px 6px;\n\t\t\tborder-radius: 4px;\n\t\t\tfont-size: 12px;\n\t\t}\n\t</style>\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<div class=\"error-icon\">!</div>\n\t\t<h1>Authorization Error</h1>\n\t\t<p>${escapedDescription}</p>\n\t\t<p style=\"margin-top: 8px;\"><code>${escapedError}</code></p>\n\t\t${redirectHtml}\n\t</div>\n</body>\n</html>`;\n}\n","/**\n * Core OAuth 2.1 Provider with AT Protocol extensions\n * Orchestrates authorization code flow with PKCE, DPoP, and PAR\n */\n\nimport type { OAuthAuthorizationServerMetadata } from \"@atproto/oauth-types\";\nimport type { OAuthStorage, AuthCodeData, TokenData, ClientMetadata } from \"./storage.js\";\nimport { verifyPkceChallenge } from \"./pkce.js\";\nimport { verifyDpopProof, DpopError, generateDpopNonce } from \"./dpop.js\";\nimport { PARHandler } from \"./par.js\";\nimport { ClientResolver } from \"./client-resolver.js\";\nimport {\n\tgenerateAuthCode,\n\tgenerateTokens,\n\trefreshTokens,\n\tbuildTokenResponse,\n\textractAccessToken,\n\tisTokenValid,\n\tAUTH_CODE_TTL,\n} from \"./tokens.js\";\nimport { renderConsentUI, renderErrorPage, CONSENT_UI_CSP } from \"./ui.js\";\n\n/**\n * OAuth provider configuration\n */\nexport interface OAuthProviderConfig {\n\t/** OAuth storage implementation */\n\tstorage: OAuthStorage;\n\t/** The OAuth issuer URL (e.g., https://your-pds.com) */\n\tissuer: string;\n\t/** Whether DPoP is required for all tokens (default: true for AT Protocol) */\n\tdpopRequired?: boolean;\n\t/** Whether PAR is enabled (default: true) */\n\tenablePAR?: boolean;\n\t/** Client resolver for DID-based discovery */\n\tclientResolver?: ClientResolver;\n\t/** Callback to verify user credentials */\n\tverifyUser?: (password: string) => Promise<{ sub: string; handle: string } | null>;\n\t/** Get the current user (if already authenticated) */\n\tgetCurrentUser?: () => Promise<{ sub: string; handle: string } | null>;\n}\n\n/**\n * OAuth error response builder\n */\nfunction oauthError(error: string, description: string, status: number = 400): Response {\n\treturn new Response(\n\t\tJSON.stringify({\n\t\t\terror,\n\t\t\terror_description: description,\n\t\t}),\n\t\t{\n\t\t\tstatus,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t}\n\t);\n}\n\n/**\n * Error thrown when request body parsing fails\n */\nexport class RequestBodyError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"RequestBodyError\";\n\t}\n}\n\n/**\n * Parse request body from JSON or form-urlencoded\n * @throws RequestBodyError if content type is unsupported or parsing fails\n */\nexport async function parseRequestBody(request: Request): Promise<Record<string, string>> {\n\tconst contentType = request.headers.get(\"Content-Type\") ?? \"\";\n\n\ttry {\n\t\tif (contentType.includes(\"application/json\")) {\n\t\t\tconst json = await request.json();\n\t\t\treturn Object.fromEntries(\n\t\t\t\tObject.entries(json as Record<string, unknown>).map(([k, v]) => [k, String(v)])\n\t\t\t);\n\t\t} else if (contentType.includes(\"application/x-www-form-urlencoded\")) {\n\t\t\tconst body = await request.text();\n\t\t\treturn Object.fromEntries(new URLSearchParams(body).entries());\n\t\t} else {\n\t\t\tthrow new RequestBodyError(\n\t\t\t\t\"Content-Type must be application/json or application/x-www-form-urlencoded\"\n\t\t\t);\n\t\t}\n\t} catch (e) {\n\t\tif (e instanceof RequestBodyError) {\n\t\t\tthrow e;\n\t\t}\n\t\tthrow new RequestBodyError(\"Failed to parse request body\");\n\t}\n}\n\n/**\n * AT Protocol OAuth 2.1 Provider\n */\nexport class ATProtoOAuthProvider {\n\tprivate storage: OAuthStorage;\n\tprivate issuer: string;\n\tprivate dpopRequired: boolean;\n\tprivate enablePAR: boolean;\n\tprivate parHandler: PARHandler;\n\tprivate clientResolver: ClientResolver;\n\tprivate verifyUser?: (password: string) => Promise<{ sub: string; handle: string } | null>;\n\tprivate getCurrentUser?: () => Promise<{ sub: string; handle: string } | null>;\n\n\tconstructor(config: OAuthProviderConfig) {\n\t\tthis.storage = config.storage;\n\t\tthis.issuer = config.issuer;\n\t\tthis.dpopRequired = config.dpopRequired ?? true;\n\t\tthis.enablePAR = config.enablePAR ?? true;\n\t\tthis.parHandler = new PARHandler(config.storage, config.issuer);\n\t\tthis.clientResolver = config.clientResolver ?? new ClientResolver({ storage: config.storage });\n\t\tthis.verifyUser = config.verifyUser;\n\t\tthis.getCurrentUser = config.getCurrentUser;\n\t}\n\n\t/**\n\t * Handle authorization request (GET/POST /oauth/authorize)\n\t */\n\tasync handleAuthorize(request: Request): Promise<Response> {\n\t\tconst url = new URL(request.url);\n\n\t\t// Parse OAuth params from query string (GET) or form data (POST)\n\t\tlet params: Record<string, string>;\n\n\t\tif (request.method === \"POST\") {\n\t\t\t// POST: parse from form data (includes hidden fields with OAuth params)\n\t\t\tconst formData = await request.formData();\n\t\t\tparams = {};\n\t\t\tfor (const [key, value] of formData.entries()) {\n\t\t\t\tif (typeof value === \"string\") {\n\t\t\t\t\tparams[key] = value;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// GET: check for PAR or query params\n\t\t\tconst requestUri = url.searchParams.get(\"request_uri\");\n\t\t\tconst clientId = url.searchParams.get(\"client_id\");\n\n\t\t\tif (requestUri && this.enablePAR) {\n\t\t\t\tif (!clientId) {\n\t\t\t\t\treturn this.renderError(\"invalid_request\", \"client_id required with request_uri\");\n\t\t\t\t}\n\t\t\t\tconst parParams = await this.parHandler.retrieveParams(requestUri, clientId);\n\t\t\t\tif (!parParams) {\n\t\t\t\t\treturn this.renderError(\"invalid_request\", \"Invalid or expired request_uri\");\n\t\t\t\t}\n\t\t\t\tparams = parParams;\n\t\t\t} else {\n\t\t\t\t// Parse query parameters\n\t\t\t\tparams = Object.fromEntries(url.searchParams.entries());\n\t\t\t}\n\t\t}\n\n\t\t// Validate required parameters\n\t\tconst required = [\"client_id\", \"redirect_uri\", \"response_type\", \"code_challenge\", \"state\"];\n\t\tfor (const param of required) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn this.renderError(\"invalid_request\", `Missing required parameter: ${param}`);\n\t\t\t}\n\t\t}\n\n\t\t// Validate response_type\n\t\tif (params.response_type !== \"code\") {\n\t\t\treturn this.renderError(\"unsupported_response_type\", \"Only response_type=code is supported\");\n\t\t}\n\n\t\t// Validate code_challenge_method\n\t\tif (params.code_challenge_method && params.code_challenge_method !== \"S256\") {\n\t\t\treturn this.renderError(\"invalid_request\", \"Only code_challenge_method=S256 is supported\");\n\t\t}\n\n\t\t// Resolve client metadata\n\t\tlet client: ClientMetadata;\n\t\ttry {\n\t\t\tclient = await this.clientResolver.resolveClient(params.client_id!);\n\t\t} catch (e) {\n\t\t\treturn this.renderError(\"invalid_client\", `Failed to resolve client: ${e}`);\n\t\t}\n\n\t\t// Validate redirect_uri\n\t\tif (!client.redirectUris.includes(params.redirect_uri!)) {\n\t\t\treturn this.renderError(\"invalid_request\", \"Invalid redirect_uri for this client\");\n\t\t}\n\n\t\t// Handle POST (form submission)\n\t\tif (request.method === \"POST\") {\n\t\t\treturn this.handleAuthorizePost(request, params, client);\n\t\t}\n\n\t\t// Check if user is authenticated\n\t\tlet user: { sub: string; handle: string } | null = null;\n\t\tif (this.getCurrentUser) {\n\t\t\tuser = await this.getCurrentUser();\n\t\t}\n\n\t\t// Show consent UI\n\t\tconst scope = params.scope ?? \"atproto\";\n\t\tconst html = renderConsentUI({\n\t\t\tclient,\n\t\t\tscope,\n\t\t\tauthorizeUrl: url.pathname,\n\t\t\tstate: params.state!,\n\t\t\toauthParams: params,\n\t\t\tuserHandle: user?.handle,\n\t\t\tshowLogin: !user && !!this.verifyUser,\n\t\t});\n\n\t\treturn new Response(html, {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Handle authorization form POST\n\t */\n\tprivate async handleAuthorizePost(\n\t\trequest: Request,\n\t\tparams: Record<string, string>,\n\t\tclient: ClientMetadata\n\t): Promise<Response> {\n\t\t// Form data was already parsed in handleAuthorize - extract action and password\n\t\tconst action = params.action;\n\t\tconst password = params.password ?? null;\n\n\t\tconst redirectUri = params.redirect_uri!;\n\t\tconst state = params.state!;\n\t\tconst responseMode = params.response_mode ?? \"fragment\";\n\n\t\t// Handle deny\n\t\tif (action === \"deny\") {\n\t\t\tconst errorUrl = new URL(redirectUri);\n\n\t\t\tif (responseMode === \"fragment\") {\n\t\t\t\tconst hashParams = new URLSearchParams();\n\t\t\t\thashParams.set(\"error\", \"access_denied\");\n\t\t\t\thashParams.set(\"error_description\", \"User denied authorization\");\n\t\t\t\thashParams.set(\"state\", state);\n\t\t\t\thashParams.set(\"iss\", this.issuer);\n\t\t\t\terrorUrl.hash = hashParams.toString();\n\t\t\t} else {\n\t\t\t\terrorUrl.searchParams.set(\"error\", \"access_denied\");\n\t\t\t\terrorUrl.searchParams.set(\"error_description\", \"User denied authorization\");\n\t\t\t\terrorUrl.searchParams.set(\"state\", state);\n\t\t\t\terrorUrl.searchParams.set(\"iss\", this.issuer);\n\t\t\t}\n\n\t\t\treturn Response.redirect(errorUrl.toString(), 302);\n\t\t}\n\n\t\t// Get or verify user\n\t\tlet user: { sub: string; handle: string } | null = null;\n\n\t\tif (this.getCurrentUser) {\n\t\t\tuser = await this.getCurrentUser();\n\t\t}\n\n\t\tif (!user && password && this.verifyUser) {\n\t\t\tuser = await this.verifyUser(password);\n\t\t}\n\n\t\tif (!user) {\n\t\t\t// Show login form with error\n\t\t\tconst url = new URL(request.url);\n\t\t\tconst scope = params.scope ?? \"atproto\";\n\t\t\tconst html = renderConsentUI({\n\t\t\t\tclient,\n\t\t\t\tscope,\n\t\t\t\tauthorizeUrl: url.pathname,\n\t\t\t\tstate,\n\t\t\t\toauthParams: params,\n\t\t\t\tshowLogin: true,\n\t\t\t\terror: \"Invalid password\",\n\t\t\t});\n\t\t\treturn new Response(html, {\n\t\t\t\tstatus: 401,\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Generate authorization code\n\t\tconst code = generateAuthCode();\n\t\tconst scope = params.scope ?? \"atproto\";\n\n\t\tconst authCodeData: AuthCodeData = {\n\t\t\tclientId: params.client_id!,\n\t\t\tredirectUri,\n\t\t\tcodeChallenge: params.code_challenge!,\n\t\t\tcodeChallengeMethod: \"S256\",\n\t\t\tscope,\n\t\t\tsub: user.sub,\n\t\t\texpiresAt: Date.now() + AUTH_CODE_TTL,\n\t\t};\n\n\t\tawait this.storage.saveAuthCode(code, authCodeData);\n\n\t\t// Redirect with code (using fragment mode if requested)\n\t\tconst successUrl = new URL(redirectUri);\n\n\t\tif (responseMode === \"fragment\") {\n\t\t\t// Put params in hash fragment\n\t\t\tconst hashParams = new URLSearchParams();\n\t\t\thashParams.set(\"code\", code);\n\t\t\thashParams.set(\"state\", state);\n\t\t\thashParams.set(\"iss\", this.issuer);\n\t\t\tsuccessUrl.hash = hashParams.toString();\n\t\t} else {\n\t\t\t// Put params in query string\n\t\t\tsuccessUrl.searchParams.set(\"code\", code);\n\t\t\tsuccessUrl.searchParams.set(\"state\", state);\n\t\t\tsuccessUrl.searchParams.set(\"iss\", this.issuer);\n\t\t}\n\n\t\treturn Response.redirect(successUrl.toString(), 302);\n\t}\n\n\t/**\n\t * Handle token request (POST /oauth/token)\n\t */\n\tasync handleToken(request: Request): Promise<Response> {\n\t\tlet params: Record<string, string>;\n\t\ttry {\n\t\t\tparams = await parseRequestBody(request);\n\t\t} catch (e) {\n\t\t\treturn oauthError(\"invalid_request\", e instanceof Error ? e.message : \"Invalid request\");\n\t\t}\n\n\t\tconst grantType = params.grant_type;\n\n\t\tif (grantType === \"authorization_code\") {\n\t\t\treturn this.handleAuthorizationCodeGrant(request, params);\n\t\t} else if (grantType === \"refresh_token\") {\n\t\t\treturn this.handleRefreshTokenGrant(request, params);\n\t\t} else {\n\t\t\treturn oauthError(\"unsupported_grant_type\", `Unsupported grant_type: ${grantType}`);\n\t\t}\n\t}\n\n\t/**\n\t * Handle authorization code grant\n\t */\n\tprivate async handleAuthorizationCodeGrant(\n\t\trequest: Request,\n\t\tparams: Record<string, string>\n\t): Promise<Response> {\n\t\t// Validate required parameters\n\t\tconst required = [\"code\", \"client_id\", \"redirect_uri\", \"code_verifier\"];\n\t\tfor (const param of required) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn oauthError(\"invalid_request\", `Missing required parameter: ${param}`);\n\t\t\t}\n\t\t}\n\n\t\t// Get authorization code data\n\t\tconst codeData = await this.storage.getAuthCode(params.code!);\n\t\tif (!codeData) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Invalid or expired authorization code\");\n\t\t}\n\n\t\t// Delete code (one-time use)\n\t\tawait this.storage.deleteAuthCode(params.code!);\n\n\t\t// Verify client_id matches\n\t\tif (codeData.clientId !== params.client_id) {\n\t\t\treturn oauthError(\"invalid_grant\", \"client_id mismatch\");\n\t\t}\n\n\t\t// Verify redirect_uri matches\n\t\tif (codeData.redirectUri !== params.redirect_uri) {\n\t\t\treturn oauthError(\"invalid_grant\", \"redirect_uri mismatch\");\n\t\t}\n\n\t\t// Verify PKCE\n\t\tconst pkceValid = await verifyPkceChallenge(\n\t\t\tparams.code_verifier!,\n\t\t\tcodeData.codeChallenge,\n\t\t\tcodeData.codeChallengeMethod\n\t\t);\n\t\tif (!pkceValid) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Invalid code_verifier\");\n\t\t}\n\n\t\t// Verify DPoP if required\n\t\tlet dpopJkt: string | undefined;\n\t\tif (this.dpopRequired) {\n\t\t\ttry {\n\t\t\t\tconst dpopProof = await verifyDpopProof(request);\n\n\t\t\t\t// Verify jti is unique (replay prevention)\n\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP proof replay detected\");\n\t\t\t\t}\n\n\t\t\t\tdpopJkt = dpopProof.jkt;\n\t\t\t} catch (e) {\n\t\t\t\tif (e instanceof DpopError) {\n\t\t\t\t\t// Check if we need to send a nonce\n\t\t\t\t\tif (e.code === \"use_dpop_nonce\") {\n\t\t\t\t\t\tconst nonce = generateDpopNonce();\n\t\t\t\t\t\treturn new Response(\n\t\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\t\terror: \"use_dpop_nonce\",\n\t\t\t\t\t\t\t\terror_description: \"DPoP nonce required\",\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tstatus: 400,\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\"DPoP-Nonce\": nonce,\n\t\t\t\t\t\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", e.message);\n\t\t\t\t}\n\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP verification failed\");\n\t\t\t}\n\t\t} else {\n\t\t\t// Check if DPoP header is present (optional but binding)\n\t\t\tconst dpopHeader = request.headers.get(\"DPoP\");\n\t\t\tif (dpopHeader) {\n\t\t\t\ttry {\n\t\t\t\t\tconst dpopProof = await verifyDpopProof(request);\n\t\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP proof replay detected\");\n\t\t\t\t\t}\n\t\t\t\t\tdpopJkt = dpopProof.jkt;\n\t\t\t\t} catch (e) {\n\t\t\t\t\tif (e instanceof DpopError) {\n\t\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", e.message);\n\t\t\t\t\t}\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP verification failed\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Generate tokens\n\t\tconst { tokens, tokenData } = generateTokens({\n\t\t\tsub: codeData.sub,\n\t\t\tclientId: codeData.clientId,\n\t\t\tscope: codeData.scope,\n\t\t\tdpopJkt,\n\t\t});\n\n\t\t// Save tokens\n\t\tawait this.storage.saveTokens(tokenData);\n\n\t\t// Return token response\n\t\treturn new Response(JSON.stringify(buildTokenResponse(tokens)), {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Handle refresh token grant\n\t */\n\tprivate async handleRefreshTokenGrant(\n\t\trequest: Request,\n\t\tparams: Record<string, string>\n\t): Promise<Response> {\n\t\tconst refreshToken = params.refresh_token;\n\t\tif (!refreshToken) {\n\t\t\treturn oauthError(\"invalid_request\", \"Missing refresh_token parameter\");\n\t\t}\n\n\t\t// Get token data\n\t\tconst existingData = await this.storage.getTokenByRefresh(refreshToken);\n\t\tif (!existingData) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Invalid refresh token\");\n\t\t}\n\n\t\t// Check if token was revoked\n\t\tif (existingData.revoked) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Token has been revoked\");\n\t\t}\n\n\t\t// Verify client_id if provided\n\t\tif (params.client_id && params.client_id !== existingData.clientId) {\n\t\t\treturn oauthError(\"invalid_grant\", \"client_id mismatch\");\n\t\t}\n\n\t\t// Verify DPoP if token was DPoP-bound\n\t\tif (existingData.dpopJkt) {\n\t\t\ttry {\n\t\t\t\tconst dpopProof = await verifyDpopProof(request);\n\n\t\t\t\t// Verify key thumbprint matches\n\t\t\t\tif (dpopProof.jkt !== existingData.dpopJkt) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP key mismatch\");\n\t\t\t\t}\n\n\t\t\t\t// Verify jti is unique\n\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP proof replay detected\");\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tif (e instanceof DpopError) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", e.message);\n\t\t\t\t}\n\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP verification failed\");\n\t\t\t}\n\t\t}\n\n\t\t// Revoke old tokens\n\t\tawait this.storage.revokeToken(existingData.accessToken);\n\n\t\t// Generate new tokens (with refresh token rotation)\n\t\tconst { tokens, tokenData } = refreshTokens(existingData, true);\n\n\t\t// Save new tokens\n\t\tawait this.storage.saveTokens(tokenData);\n\n\t\t// Return token response\n\t\treturn new Response(JSON.stringify(buildTokenResponse(tokens)), {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Handle PAR request (POST /oauth/par)\n\t */\n\tasync handlePAR(request: Request): Promise<Response> {\n\t\tif (!this.enablePAR) {\n\t\t\treturn oauthError(\"invalid_request\", \"PAR is not enabled\");\n\t\t}\n\t\treturn this.parHandler.handlePushRequest(request);\n\t}\n\n\t/**\n\t * Handle metadata request (GET /.well-known/oauth-authorization-server)\n\t */\n\thandleMetadata(): Response {\n\t\t// URLs are built dynamically so we cast to the schema type\n\t\tconst metadata: OAuthAuthorizationServerMetadata = {\n\t\t\tissuer: this.issuer,\n\t\t\tauthorization_endpoint: `${this.issuer}/oauth/authorize`,\n\t\t\ttoken_endpoint: `${this.issuer}/oauth/token`,\n\t\t\tresponse_types_supported: [\"code\"],\n\t\t\tresponse_modes_supported: [\"fragment\", \"query\"],\n\t\t\tgrant_types_supported: [\"authorization_code\", \"refresh_token\"],\n\t\t\tcode_challenge_methods_supported: [\"S256\"],\n\t\t\ttoken_endpoint_auth_methods_supported: [\"none\"],\n\t\t\tscopes_supported: [\"atproto\", \"transition:generic\", \"transition:chat.bsky\"],\n\t\t\tsubject_types_supported: [\"public\"],\n\t\t\tauthorization_response_iss_parameter_supported: true,\n\t\t\tclient_id_metadata_document_supported: true,\n\t\t\t...(this.enablePAR && {\n\t\t\t\tpushed_authorization_request_endpoint: `${this.issuer}/oauth/par`,\n\t\t\t\trequire_pushed_authorization_requests: false,\n\t\t\t}),\n\t\t\t...(this.dpopRequired && {\n\t\t\t\tdpop_signing_alg_values_supported: [\"ES256\"],\n\t\t\t\ttoken_endpoint_auth_signing_alg_values_supported: [\"ES256\"],\n\t\t\t}),\n\t\t} as OAuthAuthorizationServerMetadata;\n\n\t\treturn new Response(JSON.stringify(metadata), {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"max-age=3600\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Verify an access token from a request\n\t * @param request The HTTP request\n\t * @param requiredScope Optional scope to require\n\t * @returns Token data if valid\n\t */\n\tasync verifyAccessToken(\n\t\trequest: Request,\n\t\trequiredScope?: string\n\t): Promise<TokenData | null> {\n\t\t// Extract token from Authorization header\n\t\tconst tokenInfo = extractAccessToken(request);\n\t\tif (!tokenInfo) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Lookup token\n\t\tconst tokenData = await this.storage.getTokenByAccess(tokenInfo.token);\n\t\tif (!tokenData) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Check validity\n\t\tif (!isTokenValid(tokenData)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Check token type matches\n\t\tif (tokenData.dpopJkt && tokenInfo.type !== \"DPoP\") {\n\t\t\treturn null; // DPoP-bound token must use DPoP header\n\t\t}\n\n\t\t// Verify DPoP if token is bound\n\t\tif (tokenData.dpopJkt) {\n\t\t\ttry {\n\t\t\t\tconst dpopProof = await verifyDpopProof(request, {\n\t\t\t\t\taccessToken: tokenInfo.token,\n\t\t\t\t});\n\n\t\t\t\t// Verify key thumbprint matches\n\t\t\t\tif (dpopProof.jkt !== tokenData.dpopJkt) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\t// Verify jti is unique\n\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\t// Check scope if required\n\t\tif (requiredScope) {\n\t\t\tconst scopes = tokenData.scope.split(\" \");\n\t\t\tif (!scopes.includes(requiredScope)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\treturn tokenData;\n\t}\n\n\t/**\n\t * Render an error page\n\t */\n\tprivate renderError(error: string, description: string): Response {\n\t\tconst html = renderErrorPage(error, description);\n\t\treturn new Response(html, {\n\t\t\tstatus: 400,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n}\n","/**\n * OAuth storage interface and types\n * Defines the storage abstraction for auth codes, tokens, clients, etc.\n */\n\n/**\n * Data stored with an authorization code\n */\nexport interface AuthCodeData {\n\t/** Client DID that requested the code */\n\tclientId: string;\n\t/** Redirect URI used in the authorization request */\n\tredirectUri: string;\n\t/** PKCE code challenge */\n\tcodeChallenge: string;\n\t/** PKCE challenge method (always S256 for AT Protocol) */\n\tcodeChallengeMethod: \"S256\";\n\t/** Authorized scope */\n\tscope: string;\n\t/** User DID that authorized the request */\n\tsub: string;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n}\n\n/**\n * Data stored with access and refresh tokens\n */\nexport interface TokenData {\n\t/** Opaque access token */\n\taccessToken: string;\n\t/** Opaque refresh token */\n\trefreshToken: string;\n\t/** Client DID that received the token */\n\tclientId: string;\n\t/** User DID the token is for */\n\tsub: string;\n\t/** Authorized scope */\n\tscope: string;\n\t/** DPoP key thumbprint (for token binding) */\n\tdpopJkt?: string;\n\t/** Issuance timestamp (Unix ms) */\n\tissuedAt: number;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n\t/** Whether the token has been revoked */\n\trevoked?: boolean;\n}\n\n/**\n * OAuth client metadata (discovered from DID document)\n */\nexport interface ClientMetadata {\n\t/** Client DID */\n\tclientId: string;\n\t/** Human-readable client name */\n\tclientName: string;\n\t/** Allowed redirect URIs */\n\tredirectUris: string[];\n\t/** Client logo URI (optional) */\n\tlogoUri?: string;\n\t/** Client homepage URI (optional) */\n\tclientUri?: string;\n\t/** When the metadata was cached (Unix ms) */\n\tcachedAt?: number;\n}\n\n/**\n * Data stored for Pushed Authorization Requests (PAR)\n */\nexport interface PARData {\n\t/** Client DID that pushed the request */\n\tclientId: string;\n\t/** All OAuth parameters from the push request */\n\tparams: Record<string, string>;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n}\n\n/**\n * Storage interface for OAuth data\n * Implementations should handle TTL-based expiration\n */\nexport interface OAuthStorage {\n\t// ============================================\n\t// Authorization Codes (5 min TTL)\n\t// ============================================\n\n\t/**\n\t * Save an authorization code\n\t * @param code The authorization code\n\t * @param data Associated data\n\t */\n\tsaveAuthCode(code: string, data: AuthCodeData): Promise<void>;\n\n\t/**\n\t * Get authorization code data\n\t * @param code The authorization code\n\t * @returns The data or null if not found/expired\n\t */\n\tgetAuthCode(code: string): Promise<AuthCodeData | null>;\n\n\t/**\n\t * Delete an authorization code (after use)\n\t * @param code The authorization code\n\t */\n\tdeleteAuthCode(code: string): Promise<void>;\n\n\t// ============================================\n\t// Tokens\n\t// ============================================\n\n\t/**\n\t * Save token data\n\t * @param data The token data\n\t */\n\tsaveTokens(data: TokenData): Promise<void>;\n\n\t/**\n\t * Get token data by access token\n\t * @param accessToken The access token\n\t * @returns The data or null if not found/expired/revoked\n\t */\n\tgetTokenByAccess(accessToken: string): Promise<TokenData | null>;\n\n\t/**\n\t * Get token data by refresh token\n\t * @param refreshToken The refresh token\n\t * @returns The data or null if not found/expired/revoked\n\t */\n\tgetTokenByRefresh(refreshToken: string): Promise<TokenData | null>;\n\n\t/**\n\t * Revoke a token by access token\n\t * @param accessToken The access token to revoke\n\t */\n\trevokeToken(accessToken: string): Promise<void>;\n\n\t/**\n\t * Revoke all tokens for a user (for logout)\n\t * @param sub The user DID\n\t */\n\trevokeAllTokens?(sub: string): Promise<void>;\n\n\t// ============================================\n\t// Clients (DID-based, cached)\n\t// ============================================\n\n\t/**\n\t * Save client metadata (cached from DID document)\n\t * @param clientId The client DID\n\t * @param metadata The client metadata\n\t */\n\tsaveClient(clientId: string, metadata: ClientMetadata): Promise<void>;\n\n\t/**\n\t * Get cached client metadata\n\t * @param clientId The client DID\n\t * @returns The metadata or null if not cached\n\t */\n\tgetClient(clientId: string): Promise<ClientMetadata | null>;\n\n\t// ============================================\n\t// PAR Requests (90 sec TTL)\n\t// ============================================\n\n\t/**\n\t * Save PAR request data\n\t * @param requestUri The unique request URI\n\t * @param data The PAR data\n\t */\n\tsavePAR(requestUri: string, data: PARData): Promise<void>;\n\n\t/**\n\t * Get PAR request data\n\t * @param requestUri The request URI\n\t * @returns The data or null if not found/expired\n\t */\n\tgetPAR(requestUri: string): Promise<PARData | null>;\n\n\t/**\n\t * Delete PAR request (after use - one-time use)\n\t * @param requestUri The request URI\n\t */\n\tdeletePAR(requestUri: string): Promise<void>;\n\n\t// ============================================\n\t// DPoP Nonces (5 min TTL, replay prevention)\n\t// ============================================\n\n\t/**\n\t * Check if a nonce has been used and save it if not\n\t * Used for DPoP replay prevention\n\t * @param nonce The nonce to check\n\t * @returns true if the nonce is new (valid), false if already used\n\t */\n\tcheckAndSaveNonce(nonce: string): Promise<boolean>;\n}\n\n/**\n * In-memory storage implementation for testing\n */\nexport class InMemoryOAuthStorage implements OAuthStorage {\n\tprivate authCodes = new Map<string, AuthCodeData>();\n\tprivate tokens = new Map<string, TokenData>();\n\tprivate refreshTokenIndex = new Map<string, string>(); // refreshToken -> accessToken\n\tprivate clients = new Map<string, ClientMetadata>();\n\tprivate parRequests = new Map<string, PARData>();\n\tprivate nonces = new Set<string>();\n\n\tasync saveAuthCode(code: string, data: AuthCodeData): Promise<void> {\n\t\tthis.authCodes.set(code, data);\n\t}\n\n\tasync getAuthCode(code: string): Promise<AuthCodeData | null> {\n\t\tconst data = this.authCodes.get(code);\n\t\tif (!data) return null;\n\t\tif (Date.now() > data.expiresAt) {\n\t\t\tthis.authCodes.delete(code);\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync deleteAuthCode(code: string): Promise<void> {\n\t\tthis.authCodes.delete(code);\n\t}\n\n\tasync saveTokens(data: TokenData): Promise<void> {\n\t\tthis.tokens.set(data.accessToken, data);\n\t\tthis.refreshTokenIndex.set(data.refreshToken, data.accessToken);\n\t}\n\n\tasync getTokenByAccess(accessToken: string): Promise<TokenData | null> {\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (!data) return null;\n\t\tif (data.revoked || Date.now() > data.expiresAt) {\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync getTokenByRefresh(refreshToken: string): Promise<TokenData | null> {\n\t\tconst accessToken = this.refreshTokenIndex.get(refreshToken);\n\t\tif (!accessToken) return null;\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (!data) return null;\n\t\tif (data.revoked) return null;\n\t\t// Refresh tokens don't use accessToken expiresAt\n\t\treturn data;\n\t}\n\n\tasync revokeToken(accessToken: string): Promise<void> {\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (data) {\n\t\t\tdata.revoked = true;\n\t\t}\n\t}\n\n\tasync revokeAllTokens(sub: string): Promise<void> {\n\t\tfor (const [, data] of this.tokens) {\n\t\t\tif (data.sub === sub) {\n\t\t\t\tdata.revoked = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync saveClient(clientId: string, metadata: ClientMetadata): Promise<void> {\n\t\tthis.clients.set(clientId, metadata);\n\t}\n\n\tasync getClient(clientId: string): Promise<ClientMetadata | null> {\n\t\treturn this.clients.get(clientId) ?? null;\n\t}\n\n\tasync savePAR(requestUri: string, data: PARData): Promise<void> {\n\t\tthis.parRequests.set(requestUri, data);\n\t}\n\n\tasync getPAR(requestUri: string): Promise<PARData | null> {\n\t\tconst data = this.parRequests.get(requestUri);\n\t\tif (!data) return null;\n\t\tif (Date.now() > data.expiresAt) {\n\t\t\tthis.parRequests.delete(requestUri);\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync deletePAR(requestUri: string): Promise<void> {\n\t\tthis.parRequests.delete(requestUri);\n\t}\n\n\tasync checkAndSaveNonce(nonce: string): Promise<boolean> {\n\t\tif (this.nonces.has(nonce)) {\n\t\t\treturn false;\n\t\t}\n\t\tthis.nonces.add(nonce);\n\t\t// Note: No auto-cleanup in test implementation - use clear() between tests\n\t\t// Production SQLite storage handles TTL-based cleanup properly\n\t\treturn true;\n\t}\n\n\t/** Clear all stored data (for testing) */\n\tclear(): void {\n\t\tthis.authCodes.clear();\n\t\tthis.tokens.clear();\n\t\tthis.refreshTokenIndex.clear();\n\t\tthis.clients.clear();\n\t\tthis.parRequests.clear();\n\t\tthis.nonces.clear();\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["url: URL","protectedHeader: { alg: string; jwk?: JWK }","payload: {\n\t\tjti?: string;\n\t\thtm?: string;\n\t\thtu?: string;\n\t\tiat?: number;\n\t\tath?: string;\n\t\tnonce?: string;\n\t}","JOSEError","params: Record<string, string>","parData: PARData","response: OAuthParResponse","body: OAuthErrorResponse","code: string","response: Response","doc: OAuthClientMetadata","metadata: ClientMetadata","tokenData: TokenData","descriptions: string[]","code: string","keyResolver: Parameters<typeof jwtVerify>[1]","key: JWK | undefined","payload: JWTPayload","params: Record<string, string>","client: ClientMetadata","user: { sub: string; handle: string } | null","authCodeData: AuthCodeData","dpopJkt: string | undefined","metadata: OAuthAuthorizationServerMetadata"],"sources":["../src/pkce.ts","../src/encoding.ts","../src/dpop.ts","../src/par.ts","../src/client-resolver.ts","../src/tokens.ts","../src/ui.ts","../src/client-auth.ts","../src/provider.ts","../src/storage.ts"],"sourcesContent":["/**\n * PKCE (Proof Key for Code Exchange) verification\n * Implements RFC 7636 with S256 challenge method\n */\n\nimport { base64url } from \"jose\";\n\n/**\n * Generate the S256 code challenge from a verifier\n * challenge = BASE64URL(SHA256(verifier))\n */\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst data = encoder.encode(verifier);\n\tconst hash = await crypto.subtle.digest(\"SHA-256\", data);\n\treturn base64url.encode(new Uint8Array(hash));\n}\n\n/**\n * Verify a PKCE code challenge against a verifier\n * @param verifier The code verifier from the token request\n * @param challenge The code challenge from the authorization request\n * @param method The challenge method (only S256 supported for AT Protocol)\n * @returns true if the verifier matches the challenge\n */\nexport async function verifyPkceChallenge(\n\tverifier: string,\n\tchallenge: string,\n\tmethod: \"S256\"\n): Promise<boolean> {\n\tif (method !== \"S256\") {\n\t\tthrow new Error(\"Only S256 challenge method is supported\");\n\t}\n\n\t// Validate verifier format (RFC 7636 Section 4.1)\n\t// Must be 43-128 characters, unreserved characters only\n\tif (verifier.length < 43 || verifier.length > 128) {\n\t\treturn false;\n\t}\n\tif (!/^[A-Za-z0-9._~-]+$/.test(verifier)) {\n\t\treturn false;\n\t}\n\n\tconst expectedChallenge = await generateCodeChallenge(verifier);\n\treturn expectedChallenge === challenge;\n}\n","/**\n * Shared encoding utilities for OAuth provider\n */\n\nimport { base64url } from \"jose\";\n\n/**\n * Generate a cryptographically random string\n *\n * @param byteLength Number of random bytes (default: 32 = 256 bits)\n * @returns Base64URL-encoded random string\n */\nexport function randomString(byteLength: number = 32): string {\n\tconst buffer = new Uint8Array(byteLength);\n\tcrypto.getRandomValues(buffer);\n\treturn base64url.encode(buffer);\n}\n","/**\n * DPoP (Demonstrating Proof of Possession) verification\n * Implements RFC 9449 using jose library for JWT operations\n */\n\nimport { jwtVerify, EmbeddedJWK, calculateJwkThumbprint, errors, base64url } from \"jose\";\nimport type { JWK } from \"jose\";\nimport { randomString } from \"./encoding.js\";\n\nconst { JOSEError } = errors;\n\n/**\n * Verified DPoP proof data\n */\nexport interface DpopProof {\n\t/** HTTP method from the proof */\n\thtm: string;\n\t/** HTTP URI from the proof (without query/fragment) */\n\thtu: string;\n\t/** Unique proof identifier (for replay prevention) */\n\tjti: string;\n\t/** Access token hash (if present) */\n\tath?: string;\n\t/** Key thumbprint (JWK thumbprint of the proof key) */\n\tjkt: string;\n\t/** The public JWK from the proof */\n\tjwk: JWK;\n}\n\n/**\n * DPoP verification options\n */\nexport interface DpopVerifyOptions {\n\t/** Access token to verify ath claim against (optional) */\n\taccessToken?: string;\n\t/** Allowed signature algorithms (default: ['ES256']) */\n\tallowedAlgorithms?: string[];\n\t/** Expected nonce value (optional, for nonce binding) */\n\texpectedNonce?: string;\n\t/** Max token age in seconds (default: 60) */\n\tmaxTokenAge?: number;\n}\n\n/**\n * DPoP verification error\n */\nexport class DpopError extends Error {\n\treadonly code: string;\n\tconstructor(message: string, code: string, options?: ErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.name = \"DpopError\";\n\t\tthis.code = code;\n\t}\n}\n\n/**\n * Normalize URI for HTU comparison\n * Removes query string and fragment per RFC 9449\n */\nfunction normalizeHtuUrl(url: URL): string {\n\treturn url.origin + url.pathname;\n}\n\n/**\n * Parse and validate HTU claim\n */\nfunction parseHtu(htu: string): string {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(htu);\n\t} catch {\n\t\tthrow new DpopError('DPoP \"htu\" is not a valid URL', \"invalid_dpop\");\n\t}\n\n\tif (url.password || url.username) {\n\t\tthrow new DpopError('DPoP \"htu\" must not contain credentials', \"invalid_dpop\");\n\t}\n\n\tif (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n\t\tthrow new DpopError('DPoP \"htu\" must be http or https', \"invalid_dpop\");\n\t}\n\n\treturn normalizeHtuUrl(url);\n}\n\n/**\n * Verify a DPoP proof from a request\n * Uses jose library for JWT verification\n * @param request The HTTP request containing the DPoP header\n * @param options Verification options\n * @returns The verified proof data\n * @throws DpopError if verification fails\n */\nexport async function verifyDpopProof(\n\trequest: Request,\n\toptions: DpopVerifyOptions = {}\n): Promise<DpopProof> {\n\tconst { allowedAlgorithms = [\"ES256\"], accessToken, expectedNonce, maxTokenAge = 60 } = options;\n\n\tconst dpopHeader = request.headers.get(\"DPoP\");\n\tif (!dpopHeader) {\n\t\tthrow new DpopError(\"Missing DPoP header\", \"missing_dpop\");\n\t}\n\n\tlet protectedHeader: { alg: string; jwk?: JWK };\n\tlet payload: {\n\t\tjti?: string;\n\t\thtm?: string;\n\t\thtu?: string;\n\t\tiat?: number;\n\t\tath?: string;\n\t\tnonce?: string;\n\t};\n\n\ttry {\n\t\tconst result = await jwtVerify(dpopHeader, EmbeddedJWK, {\n\t\t\ttyp: \"dpop+jwt\",\n\t\t\talgorithms: allowedAlgorithms,\n\t\t\tmaxTokenAge,\n\t\t\tclockTolerance: 10,\n\t\t});\n\t\tprotectedHeader = result.protectedHeader as typeof protectedHeader;\n\t\tpayload = result.payload as typeof payload;\n\t} catch (err) {\n\t\tif (err instanceof JOSEError) {\n\t\t\tthrow new DpopError(`DPoP verification failed: ${err.message}`, \"invalid_dpop\", { cause: err });\n\t\t}\n\t\tthrow new DpopError(\"DPoP verification failed\", \"invalid_dpop\", { cause: err });\n\t}\n\n\tif (!payload.jti || typeof payload.jti !== \"string\") {\n\t\tthrow new DpopError('DPoP \"jti\" missing', \"invalid_dpop\");\n\t}\n\n\tif (!payload.htm || typeof payload.htm !== \"string\") {\n\t\tthrow new DpopError('DPoP \"htm\" missing', \"invalid_dpop\");\n\t}\n\n\tif (!payload.htu || typeof payload.htu !== \"string\") {\n\t\tthrow new DpopError('DPoP \"htu\" missing', \"invalid_dpop\");\n\t}\n\n\tif (payload.htm !== request.method) {\n\t\tthrow new DpopError('DPoP \"htm\" mismatch', \"invalid_dpop\");\n\t}\n\n\tconst requestUrl = new URL(request.url);\n\tconst expectedHtu = normalizeHtuUrl(requestUrl);\n\tconst proofHtu = parseHtu(payload.htu);\n\tif (proofHtu !== expectedHtu) {\n\t\tthrow new DpopError('DPoP \"htu\" mismatch', \"invalid_dpop\");\n\t}\n\n\tif (expectedNonce !== undefined && payload.nonce !== expectedNonce) {\n\t\tthrow new DpopError('DPoP \"nonce\" mismatch', \"use_dpop_nonce\");\n\t}\n\n\t// Verify ath (access token hash) binding per RFC 9449 Section 4.3\n\tif (accessToken) {\n\t\tif (!payload.ath) {\n\t\t\tthrow new DpopError('DPoP \"ath\" missing when access token provided', \"invalid_dpop\");\n\t\t}\n\n\t\tconst tokenHash = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(accessToken));\n\t\tconst expectedAth = base64url.encode(new Uint8Array(tokenHash));\n\n\t\tif (payload.ath !== expectedAth) {\n\t\t\tthrow new DpopError('DPoP \"ath\" mismatch', \"invalid_dpop\");\n\t\t}\n\t} else if (payload.ath !== undefined) {\n\t\tthrow new DpopError('DPoP \"ath\" claim not allowed without access token', \"invalid_dpop\");\n\t}\n\n\tconst jwk = protectedHeader.jwk!;\n\tconst jkt = await calculateJwkThumbprint(jwk, \"sha256\");\n\n\treturn Object.freeze({\n\t\thtm: payload.htm,\n\t\thtu: payload.htu,\n\t\tjti: payload.jti,\n\t\tath: payload.ath,\n\t\tjkt,\n\t\tjwk,\n\t});\n}\n\n/**\n * Generate a random DPoP nonce\n * @returns A base64url-encoded random nonce (16 bytes)\n */\nexport function generateDpopNonce(): string {\n\treturn randomString(16);\n}\n","/**\n * PAR (Pushed Authorization Requests) handler\n * Implements RFC 9126\n */\n\nimport type { OAuthParResponse } from \"@atproto/oauth-types\";\nimport type { OAuthStorage, PARData } from \"./storage.js\";\nimport { randomString } from \"./encoding.js\";\nimport { parseRequestBody } from \"./provider.js\";\n\nexport type { OAuthParResponse };\n\n/** PAR request URI prefix per RFC 9126 */\nconst REQUEST_URI_PREFIX = \"urn:ietf:params:oauth:request_uri:\";\n\n/** Default PAR expiration in seconds (90 seconds per RFC recommendation) */\nconst DEFAULT_EXPIRES_IN = 90;\n\n/**\n * OAuth error response\n */\nexport interface OAuthErrorResponse {\n\terror: string;\n\terror_description?: string;\n}\n\n/**\n * Generate a unique request URI\n */\nfunction generateRequestUri(): string {\n\treturn REQUEST_URI_PREFIX + randomString(32);\n}\n\n/**\n * Required OAuth parameters for authorization request\n */\nconst REQUIRED_PARAMS = [\"client_id\", \"redirect_uri\", \"response_type\", \"code_challenge\", \"code_challenge_method\", \"state\"];\n\n/**\n * Handler for Pushed Authorization Requests (PAR)\n */\nexport class PARHandler {\n\tprivate storage: OAuthStorage;\n\tprivate issuer: string;\n\tprivate expiresIn: number;\n\n\t/**\n\t * Create a PAR handler\n\t * @param storage OAuth storage implementation\n\t * @param issuer The OAuth issuer URL\n\t * @param expiresIn PAR expiration time in seconds (default: 90)\n\t */\n\tconstructor(storage: OAuthStorage, issuer: string, expiresIn: number = DEFAULT_EXPIRES_IN) {\n\t\tthis.storage = storage;\n\t\tthis.issuer = issuer;\n\t\tthis.expiresIn = expiresIn;\n\t}\n\n\t/**\n\t * Handle a PAR push request\n\t * POST /oauth/par\n\t * @param request The HTTP request\n\t * @returns Response with request_uri or error\n\t */\n\tasync handlePushRequest(request: Request): Promise<Response> {\n\t\tlet params: Record<string, string>;\n\t\ttry {\n\t\t\tparams = await parseRequestBody(request);\n\t\t} catch (e) {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\te instanceof Error ? e.message : \"Invalid request\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tconst clientId = params.client_id;\n\t\tif (!clientId) {\n\t\t\treturn this.errorResponse(\"invalid_request\", \"Missing client_id parameter\", 400);\n\t\t}\n\n\t\tfor (const param of REQUIRED_PARAMS) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn this.errorResponse(\"invalid_request\", `Missing required parameter: ${param}`, 400);\n\t\t\t}\n\t\t}\n\n\t\tif (params.response_type !== \"code\") {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"unsupported_response_type\",\n\t\t\t\t\"Only response_type=code is supported\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tif (params.code_challenge_method !== \"S256\") {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t\"Only code_challenge_method=S256 is supported\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tconst codeChallenge = params.code_challenge!;\n\t\tif (!/^[A-Za-z0-9_-]{43}$/.test(codeChallenge)) {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t\"Invalid code_challenge format\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\tnew URL(params.redirect_uri!);\n\t\t} catch {\n\t\t\treturn this.errorResponse(\"invalid_request\", \"Invalid redirect_uri\", 400);\n\t\t}\n\n\t\tconst requestUri = generateRequestUri();\n\t\tconst expiresAt = Date.now() + this.expiresIn * 1000;\n\n\t\tconst parData: PARData = {\n\t\t\tclientId,\n\t\t\tparams,\n\t\t\texpiresAt,\n\t\t};\n\n\t\tawait this.storage.savePAR(requestUri, parData);\n\n\t\tconst response: OAuthParResponse = {\n\t\t\trequest_uri: requestUri,\n\t\t\texpires_in: this.expiresIn,\n\t\t};\n\n\t\treturn new Response(JSON.stringify(response), {\n\t\t\tstatus: 201,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Retrieve and consume PAR parameters\n\t * Called during authorization request handling\n\t * @param requestUri The request URI from the authorization request\n\t * @param clientId The client_id from the authorization request (for verification)\n\t * @returns The stored parameters or null if not found/expired\n\t */\n\tasync retrieveParams(\n\t\trequestUri: string,\n\t\tclientId: string\n\t): Promise<Record<string, string> | null> {\n\t\tif (!requestUri.startsWith(REQUEST_URI_PREFIX)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst parData = await this.storage.getPAR(requestUri);\n\t\tif (!parData) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (parData.clientId !== clientId) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// One-time use: delete after retrieval\n\t\tawait this.storage.deletePAR(requestUri);\n\n\t\treturn parData.params;\n\t}\n\n\t/**\n\t * Check if a request_uri is valid format\n\t */\n\tstatic isRequestUri(value: string): boolean {\n\t\treturn value.startsWith(REQUEST_URI_PREFIX);\n\t}\n\n\t/**\n\t * Create an OAuth error response\n\t */\n\tprivate errorResponse(\n\t\terror: string,\n\t\tdescription: string,\n\t\tstatus: number = 400\n\t): Response {\n\t\tconst body: OAuthErrorResponse = {\n\t\t\terror,\n\t\t\terror_description: description,\n\t\t};\n\t\treturn new Response(JSON.stringify(body), {\n\t\t\tstatus,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n}\n","/**\n * Client resolver for DID-based client discovery\n * Resolves OAuth client metadata from DIDs for AT Protocol\n */\n\nimport { ensureValidDid } from \"@atproto/syntax\";\nimport {\n\toauthClientMetadataSchema,\n\ttype OAuthClientMetadata,\n} from \"@atproto/oauth-types\";\nimport type { ClientMetadata, OAuthStorage, JWK } from \"./storage.js\";\n\nexport type { OAuthClientMetadata };\n\n/**\n * Client resolution error\n */\nexport class ClientResolutionError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly code: string\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"ClientResolutionError\";\n\t}\n}\n\n/**\n * Options for client resolution\n */\nexport interface ClientResolverOptions {\n\t/** Storage for caching client metadata */\n\tstorage?: OAuthStorage;\n\t/** Cache TTL in milliseconds (default: 1 hour) */\n\tcacheTtl?: number;\n\t/** Fetch function for making HTTP requests (for testing) */\n\tfetch?: typeof globalThis.fetch;\n}\n\n/**\n * Check if a string is a valid HTTPS URL\n */\nfunction isHttpsUrl(value: string): boolean {\n\ttry {\n\t\tconst url = new URL(value);\n\t\treturn url.protocol === \"https:\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Validate that a string is a valid DID using @atproto/syntax\n */\nfunction isValidDid(value: string): boolean {\n\ttry {\n\t\tensureValidDid(value);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Get the client metadata URL from a client ID\n * Supports both URL-based and DID-based client IDs\n */\nfunction getClientMetadataUrl(clientId: string): string | null {\n\t// URL-based client ID: the URL itself is the metadata endpoint\n\tif (isHttpsUrl(clientId)) {\n\t\treturn clientId;\n\t}\n\n\t// DID-based client ID: derive the metadata URL\n\tif (clientId.startsWith(\"did:web:\")) {\n\t\t// did:web:example.com -> https://example.com/.well-known/oauth-client-metadata\n\t\t// did:web:example.com:path -> https://example.com/path/.well-known/oauth-client-metadata\n\t\tconst parts = clientId.slice(8).split(\":\");\n\t\tconst host = parts[0]!.replace(/%3A/g, \":\");\n\t\tconst path = parts.slice(1).join(\"/\");\n\t\tconst baseUrl = `https://${host}${path ? \"/\" + path : \"\"}`;\n\t\treturn `${baseUrl}/.well-known/oauth-client-metadata`;\n\t}\n\n\t// Unsupported client ID format\n\treturn null;\n}\n\n/**\n * Resolve client metadata from a DID\n */\nexport class ClientResolver {\n\tprivate storage?: OAuthStorage;\n\tprivate cacheTtl: number;\n\tprivate fetchFn: typeof globalThis.fetch;\n\n\tconstructor(options: ClientResolverOptions = {}) {\n\t\tthis.storage = options.storage;\n\t\tthis.cacheTtl = options.cacheTtl ?? 60 * 60 * 1000; // 1 hour default\n\t\tthis.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);\n\t}\n\n\t/**\n\t * Resolve client metadata from a client ID (URL or DID)\n\t * @param clientId The client ID (HTTPS URL or DID)\n\t * @returns The client metadata\n\t * @throws ClientResolutionError if resolution fails\n\t */\n\tasync resolveClient(clientId: string): Promise<ClientMetadata> {\n\t\tif (!isHttpsUrl(clientId) && !isValidDid(clientId)) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Invalid client ID format: ${clientId}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tif (this.storage) {\n\t\t\tconst cached = await this.storage.getClient(clientId);\n\t\t\tif (cached && cached.cachedAt && Date.now() - cached.cachedAt < this.cacheTtl) {\n\t\t\t\treturn cached;\n\t\t\t}\n\t\t}\n\n\t\tconst metadataUrl = getClientMetadataUrl(clientId);\n\t\tif (!metadataUrl) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Unsupported client ID format: ${clientId}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await this.fetchFn(metadataUrl, {\n\t\t\t\theaders: {\n\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (e) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Failed to fetch client metadata: ${e}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Client metadata fetch failed with status ${response.status}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tlet doc: OAuthClientMetadata;\n\t\ttry {\n\t\t\tconst json = await response.json();\n\t\t\tdoc = oauthClientMetadataSchema.parse(json);\n\t\t} catch (e) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Invalid client metadata: ${e instanceof Error ? e.message : \"validation failed\"}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tif (doc.client_id !== clientId) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Client ID mismatch: expected ${clientId}, got ${doc.client_id}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tconst metadata: ClientMetadata = {\n\t\t\tclientId: doc.client_id,\n\t\t\tclientName: doc.client_name ?? clientId,\n\t\t\tredirectUris: doc.redirect_uris,\n\t\t\tlogoUri: doc.logo_uri,\n\t\t\tclientUri: doc.client_uri,\n\t\t\ttokenEndpointAuthMethod: (doc.token_endpoint_auth_method as \"none\" | \"private_key_jwt\") ?? \"none\",\n\t\t\tjwks: doc.jwks as { keys: JWK[] } | undefined,\n\t\t\tjwksUri: doc.jwks_uri,\n\t\t\tcachedAt: Date.now(),\n\t\t};\n\n\t\tif (this.storage) {\n\t\t\tawait this.storage.saveClient(clientId, metadata);\n\t\t}\n\n\t\treturn metadata;\n\t}\n\n\t/**\n\t * Validate that a redirect URI is allowed for a client\n\t * @param clientId The client DID\n\t * @param redirectUri The redirect URI to validate\n\t * @returns true if the redirect URI is allowed\n\t */\n\tasync validateRedirectUri(clientId: string, redirectUri: string): Promise<boolean> {\n\t\ttry {\n\t\t\tconst metadata = await this.resolveClient(clientId);\n\t\t\treturn metadata.redirectUris.includes(redirectUri);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/**\n * Create a client resolver with optional caching\n */\nexport function createClientResolver(options: ClientResolverOptions = {}): ClientResolver {\n\treturn new ClientResolver(options);\n}\n","/**\n * Token generation and validation\n * Generates opaque tokens (not JWTs) that are stored in the database\n */\n\nimport type { OAuthTokenResponse } from \"@atproto/oauth-types\";\nimport type { TokenData } from \"./storage.js\";\nimport { randomString } from \"./encoding.js\";\n\n/** Default access token TTL: 1 hour */\nexport const ACCESS_TOKEN_TTL = 60 * 60 * 1000;\n\n/** Default refresh token TTL: 90 days */\nexport const REFRESH_TOKEN_TTL = 90 * 24 * 60 * 60 * 1000;\n\n/** Authorization code TTL: 5 minutes */\nexport const AUTH_CODE_TTL = 5 * 60 * 1000;\n\n/**\n * Generate a cryptographically random token\n * @param bytes Number of random bytes (default: 32)\n * @returns Base64URL-encoded token\n */\nexport function generateRandomToken(bytes: number = 32): string {\n\treturn randomString(bytes);\n}\n\n/**\n * Generate an authorization code\n * @returns A random authorization code\n */\nexport function generateAuthCode(): string {\n\treturn generateRandomToken(32);\n}\n\n/**\n * Token generation result\n */\nexport interface GeneratedTokens {\n\t/** Opaque access token */\n\taccessToken: string;\n\t/** Opaque refresh token */\n\trefreshToken: string;\n\t/** Access token type (Bearer or DPoP) */\n\ttokenType: \"Bearer\" | \"DPoP\";\n\t/** Access token expiration in seconds */\n\texpiresIn: number;\n\t/** Scope granted */\n\tscope: string;\n\t/** Subject (user DID) */\n\tsub: string;\n}\n\n/**\n * Options for token generation\n */\nexport interface GenerateTokensOptions {\n\t/** User DID */\n\tsub: string;\n\t/** Client DID */\n\tclientId: string;\n\t/** Scope granted */\n\tscope: string;\n\t/** DPoP key thumbprint (if using DPoP) */\n\tdpopJkt?: string;\n\t/** Custom access token TTL in ms (default: 1 hour) */\n\taccessTokenTtl?: number;\n\t/** Custom refresh token TTL in ms (default: 90 days) */\n\trefreshTokenTtl?: number;\n}\n\n/**\n * Generate access and refresh tokens\n * Tokens are opaque - their meaning comes from the database entry\n * @param options Token generation options\n * @returns Generated tokens and metadata\n */\nexport function generateTokens(options: GenerateTokensOptions): {\n\ttokens: GeneratedTokens;\n\ttokenData: TokenData;\n} {\n\tconst {\n\t\tsub,\n\t\tclientId,\n\t\tscope,\n\t\tdpopJkt,\n\t\taccessTokenTtl = ACCESS_TOKEN_TTL,\n\t} = options;\n\n\tconst accessToken = generateRandomToken(32);\n\tconst refreshToken = generateRandomToken(32);\n\tconst now = Date.now();\n\n\tconst tokenData: TokenData = {\n\t\taccessToken,\n\t\trefreshToken,\n\t\tclientId,\n\t\tsub,\n\t\tscope,\n\t\tdpopJkt,\n\t\tissuedAt: now,\n\t\texpiresAt: now + accessTokenTtl,\n\t\trevoked: false,\n\t};\n\n\tconst tokens: GeneratedTokens = {\n\t\taccessToken,\n\t\trefreshToken,\n\t\ttokenType: dpopJkt ? \"DPoP\" : \"Bearer\",\n\t\texpiresIn: Math.floor(accessTokenTtl / 1000),\n\t\tscope,\n\t\tsub,\n\t};\n\n\treturn { tokens, tokenData };\n}\n\n/**\n * Refresh tokens - generates new access token, optionally rotates refresh token\n * @param existingData The existing token data\n * @param rotateRefreshToken Whether to generate a new refresh token\n * @param accessTokenTtl Custom access token TTL in ms\n * @returns Updated tokens and token data\n */\nexport function refreshTokens(\n\texistingData: TokenData,\n\trotateRefreshToken: boolean = false,\n\taccessTokenTtl: number = ACCESS_TOKEN_TTL\n): {\n\ttokens: GeneratedTokens;\n\ttokenData: TokenData;\n} {\n\tconst accessToken = generateRandomToken(32);\n\tconst refreshToken = rotateRefreshToken ? generateRandomToken(32) : existingData.refreshToken;\n\tconst now = Date.now();\n\n\tconst tokenData: TokenData = {\n\t\t...existingData,\n\t\taccessToken,\n\t\trefreshToken,\n\t\tissuedAt: now,\n\t\texpiresAt: now + accessTokenTtl,\n\t};\n\n\tconst tokens: GeneratedTokens = {\n\t\taccessToken,\n\t\trefreshToken,\n\t\ttokenType: existingData.dpopJkt ? \"DPoP\" : \"Bearer\",\n\t\texpiresIn: Math.floor(accessTokenTtl / 1000),\n\t\tscope: existingData.scope,\n\t\tsub: existingData.sub,\n\t};\n\n\treturn { tokens, tokenData };\n}\n\n/**\n * Build token response for OAuth token endpoint\n * @param tokens The generated tokens\n * @returns JSON-serializable token response\n */\nexport function buildTokenResponse(tokens: GeneratedTokens): OAuthTokenResponse {\n\treturn {\n\t\taccess_token: tokens.accessToken,\n\t\ttoken_type: tokens.tokenType,\n\t\texpires_in: tokens.expiresIn,\n\t\trefresh_token: tokens.refreshToken,\n\t\tscope: tokens.scope,\n\t\tsub: tokens.sub,\n\t};\n}\n\n/**\n * Extract access token from Authorization header\n * Supports both Bearer and DPoP token types\n * @param request The HTTP request\n * @returns The access token and type, or null if not found\n */\nexport function extractAccessToken(\n\trequest: Request\n): { token: string; type: \"Bearer\" | \"DPoP\" } | null {\n\tconst authHeader = request.headers.get(\"Authorization\");\n\tif (!authHeader) {\n\t\treturn null;\n\t}\n\n\tif (authHeader.startsWith(\"Bearer \")) {\n\t\treturn {\n\t\t\ttoken: authHeader.slice(7),\n\t\t\ttype: \"Bearer\",\n\t\t};\n\t}\n\n\tif (authHeader.startsWith(\"DPoP \")) {\n\t\treturn {\n\t\t\ttoken: authHeader.slice(5),\n\t\t\ttype: \"DPoP\",\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Validate that a token is not expired or revoked\n * @param tokenData The token data from storage\n * @returns true if the token is valid\n */\nexport function isTokenValid(tokenData: TokenData): boolean {\n\tif (tokenData.revoked) {\n\t\treturn false;\n\t}\n\tif (Date.now() > tokenData.expiresAt) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n","/**\n * Authorization consent UI\n * Renders the HTML page for user consent during OAuth authorization\n */\n\nimport type { ClientMetadata } from \"./storage.js\";\n\n/**\n * Content Security Policy for the consent UI\n *\n * - default-src 'none': Deny all by default\n * - style-src 'unsafe-inline': Allow inline styles (our CSS is inline)\n * - img-src https: data:: Allow images from HTTPS URLs (client logos) and data URIs\n * - frame-ancestors 'none': Prevent clickjacking by disallowing framing\n * - base-uri 'none': Prevent base tag injection\n *\n * Note: form-action is intentionally omitted. Browser behavior for blocking\n * redirects after form submission is inconsistent - Chrome blocks redirects\n * to URLs not in form-action, while Firefox does not. Since OAuth requires\n * redirecting to the client's callback URL after form submission, we cannot\n * use form-action without breaking the flow in Chrome.\n * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/form-action\n */\nexport const CONSENT_UI_CSP =\n\t\"default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; frame-ancestors 'none'; base-uri 'none'\";\n\n/**\n * Escape HTML to prevent XSS\n */\nfunction escapeHtml(text: string): string {\n\treturn text\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n\n/**\n * Parse scope string into human-readable descriptions\n */\nfunction getScopeDescriptions(scope: string): string[] {\n\tconst scopes = scope.split(\" \").filter(Boolean);\n\tconst descriptions: string[] = [];\n\n\tfor (const s of scopes) {\n\t\tswitch (s) {\n\t\t\tcase \"atproto\":\n\t\t\t\tdescriptions.push(\"Access your AT Protocol account\");\n\t\t\t\tbreak;\n\t\t\tcase \"transition:generic\":\n\t\t\t\tdescriptions.push(\"Perform account operations\");\n\t\t\t\tbreak;\n\t\t\tcase \"transition:chat.bsky\":\n\t\t\t\tdescriptions.push(\"Access chat functionality\");\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t// Don't show unknown scopes to avoid confusion\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t// If no recognized scopes, show a generic message\n\tif (descriptions.length === 0) {\n\t\tdescriptions.push(\"Access your account on your behalf\");\n\t}\n\n\treturn descriptions;\n}\n\n/**\n * Options for rendering the consent UI\n */\nexport interface ConsentUIOptions {\n\t/** The OAuth client metadata */\n\tclient: ClientMetadata;\n\t/** The requested scope */\n\tscope: string;\n\t/** URL to POST the consent form to */\n\tauthorizeUrl: string;\n\t/** State parameter to include in the form */\n\tstate: string;\n\t/** OAuth parameters to include as hidden fields */\n\toauthParams: Record<string, string>;\n\t/** User's handle (for display) */\n\tuserHandle?: string;\n\t/** Whether to show a login form instead of consent */\n\tshowLogin?: boolean;\n\t/** Error message to display */\n\terror?: string;\n}\n\n/**\n * Render the consent UI HTML\n * @param options Consent UI options\n * @returns HTML string\n */\nexport function renderConsentUI(options: ConsentUIOptions): string {\n\tconst { client, scope, authorizeUrl, oauthParams, userHandle, showLogin, error } = options;\n\n\tconst clientName = escapeHtml(client.clientName);\n\tconst scopeDescriptions = getScopeDescriptions(scope);\n\tconst logoHtml = client.logoUri\n\t\t? `<img src=\"${escapeHtml(client.logoUri)}\" alt=\"${clientName} logo\" class=\"app-logo\" />`\n\t\t: `<div class=\"app-logo-placeholder\">${clientName.charAt(0).toUpperCase()}</div>`;\n\n\tconst errorHtml = error\n\t\t? `<div class=\"error-message\">${escapeHtml(error)}</div>`\n\t\t: \"\";\n\n\tconst loginFormHtml = showLogin\n\t\t? `\n\t\t\t<div class=\"login-form\">\n\t\t\t\t<p>Sign in to continue</p>\n\t\t\t\t<input type=\"password\" name=\"password\" placeholder=\"Password\" required autocomplete=\"current-password\" />\n\t\t\t</div>\n\t\t`\n\t\t: \"\";\n\n\t// Render OAuth params as hidden form fields\n\tconst hiddenFieldsHtml = Object.entries(oauthParams)\n\t\t.map(([key, value]) => `<input type=\"hidden\" name=\"${escapeHtml(key)}\" value=\"${escapeHtml(value)}\" />`)\n\t\t.join(\"\\n\\t\\t\\t\");\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>Authorize ${clientName}</title>\n\t<style>\n\t\t* {\n\t\t\tbox-sizing: border-box;\n\t\t\tmargin: 0;\n\t\t\tpadding: 0;\n\t\t}\n\n\t\tbody {\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n\t\t\tbackground: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n\t\t\tmin-height: 100vh;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tpadding: 20px;\n\t\t\tcolor: #e0e0e0;\n\t\t}\n\n\t\t.container {\n\t\t\tbackground: #1e1e30;\n\t\t\tborder-radius: 16px;\n\t\t\tpadding: 32px;\n\t\t\tmax-width: 400px;\n\t\t\twidth: 100%;\n\t\t\tbox-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t}\n\n\t\t.header {\n\t\t\ttext-align: center;\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.app-logo {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tborder-radius: 12px;\n\t\t\tmargin-bottom: 16px;\n\t\t\tobject-fit: cover;\n\t\t}\n\n\t\t.app-logo-placeholder {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tborder-radius: 12px;\n\t\t\tmargin: 0 auto 16px;\n\t\t\tbackground: linear-gradient(135deg, #3b82f6, #8b5cf6);\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tfont-size: 28px;\n\t\t\tfont-weight: 600;\n\t\t\tcolor: white;\n\t\t}\n\n\t\th1 {\n\t\t\tfont-size: 20px;\n\t\t\tfont-weight: 600;\n\t\t\tmargin-bottom: 8px;\n\t\t}\n\n\t\t.client-name {\n\t\t\tcolor: #60a5fa;\n\t\t}\n\n\t\t.user-info {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t}\n\n\t\t.permissions {\n\t\t\tbackground: rgba(255, 255, 255, 0.05);\n\t\t\tborder-radius: 12px;\n\t\t\tpadding: 16px;\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.permissions-title {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t\tmargin-bottom: 12px;\n\t\t}\n\n\t\t.permissions-list {\n\t\t\tlist-style: none;\n\t\t}\n\n\t\t.permissions-list li {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: 10px;\n\t\t\tpadding: 8px 0;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\t.permissions-list li::before {\n\t\t\tcontent: \"\";\n\t\t\twidth: 8px;\n\t\t\theight: 8px;\n\t\t\tbackground: #22c55e;\n\t\t\tborder-radius: 50%;\n\t\t\tflex-shrink: 0;\n\t\t}\n\n\t\t.buttons {\n\t\t\tdisplay: flex;\n\t\t\tgap: 12px;\n\t\t}\n\n\t\tbutton {\n\t\t\tflex: 1;\n\t\t\tpadding: 12px 20px;\n\t\t\tborder-radius: 8px;\n\t\t\tfont-size: 14px;\n\t\t\tfont-weight: 500;\n\t\t\tcursor: pointer;\n\t\t\ttransition: all 0.2s;\n\t\t\tborder: none;\n\t\t}\n\n\t\t.btn-deny {\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t\tcolor: #e0e0e0;\n\t\t}\n\n\t\t.btn-deny:hover {\n\t\t\tbackground: rgba(255, 255, 255, 0.15);\n\t\t}\n\n\t\t.btn-allow {\n\t\t\tbackground: linear-gradient(135deg, #3b82f6, #2563eb);\n\t\t\tcolor: white;\n\t\t}\n\n\t\t.btn-allow:hover {\n\t\t\tbackground: linear-gradient(135deg, #2563eb, #1d4ed8);\n\t\t}\n\n\t\t.info {\n\t\t\tmargin-top: 16px;\n\t\t\tfont-size: 12px;\n\t\t\tcolor: #6b7280;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.error-message {\n\t\t\tbackground: rgba(239, 68, 68, 0.1);\n\t\t\tborder: 1px solid rgba(239, 68, 68, 0.3);\n\t\t\tcolor: #f87171;\n\t\t\tpadding: 12px;\n\t\t\tborder-radius: 8px;\n\t\t\tmargin-bottom: 16px;\n\t\t\tfont-size: 14px;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.login-form {\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.login-form p {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t\tmargin-bottom: 12px;\n\t\t}\n\n\t\t.login-form input {\n\t\t\twidth: 100%;\n\t\t\tpadding: 12px;\n\t\t\tborder-radius: 8px;\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t\tbackground: rgba(255, 255, 255, 0.05);\n\t\t\tcolor: #e0e0e0;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\t.login-form input:focus {\n\t\t\toutline: none;\n\t\t\tborder-color: #3b82f6;\n\t\t}\n\n\t\t.login-form input::placeholder {\n\t\t\tcolor: #6b7280;\n\t\t}\n\n\t\t.client-uri {\n\t\t\tfont-size: 12px;\n\t\t\tcolor: #6b7280;\n\t\t\tmargin-top: 4px;\n\t\t}\n\n\t\t.client-uri a {\n\t\t\tcolor: #60a5fa;\n\t\t\ttext-decoration: none;\n\t\t}\n\n\t\t.client-uri a:hover {\n\t\t\ttext-decoration: underline;\n\t\t}\n\t</style>\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<div class=\"header\">\n\t\t\t${logoHtml}\n\t\t\t<h1>Authorize <span class=\"client-name\">${clientName}</span></h1>\n\t\t\t${userHandle ? `<p class=\"user-info\">as @${escapeHtml(userHandle)}</p>` : \"\"}\n\t\t\t${client.clientUri ? `<p class=\"client-uri\"><a href=\"${escapeHtml(client.clientUri)}\" target=\"_blank\" rel=\"noopener\">${escapeHtml(new URL(client.clientUri).hostname)}</a></p>` : \"\"}\n\t\t</div>\n\n\t\t${errorHtml}\n\n\t\t<form method=\"POST\" action=\"${escapeHtml(authorizeUrl)}\">\n\t\t\t${hiddenFieldsHtml}\n\n\t\t\t${loginFormHtml}\n\n\t\t\t<div class=\"permissions\">\n\t\t\t\t<p class=\"permissions-title\">This app wants to:</p>\n\t\t\t\t<ul class=\"permissions-list\">\n\t\t\t\t\t${scopeDescriptions.map((desc) => `<li>${escapeHtml(desc)}</li>`).join(\"\")}\n\t\t\t\t</ul>\n\t\t\t</div>\n\n\t\t\t<div class=\"buttons\">\n\t\t\t\t<button type=\"submit\" name=\"action\" value=\"deny\" class=\"btn-deny\">Deny</button>\n\t\t\t\t<button type=\"submit\" name=\"action\" value=\"allow\" class=\"btn-allow\">Allow</button>\n\t\t\t</div>\n\t\t</form>\n\n\t\t<p class=\"info\">You can revoke access anytime in your account settings.</p>\n\t</div>\n</body>\n</html>`;\n}\n\n/**\n * Render an error page\n * @param error Error code\n * @param description Error description\n * @param redirectUri Optional redirect URI for the error\n * @returns HTML string\n */\nexport function renderErrorPage(\n\terror: string,\n\tdescription: string,\n\tredirectUri?: string\n): string {\n\tconst escapedError = escapeHtml(error);\n\tconst escapedDescription = escapeHtml(description);\n\n\tconst redirectHtml = redirectUri\n\t\t? `<p style=\"margin-top: 16px;\"><a href=\"${escapeHtml(redirectUri)}\" style=\"color: #60a5fa;\">Return to application</a></p>`\n\t\t: \"\";\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>Authorization Error</title>\n\t<style>\n\t\tbody {\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n\t\t\tbackground: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n\t\t\tmin-height: 100vh;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tpadding: 20px;\n\t\t\tcolor: #e0e0e0;\n\t\t\tmargin: 0;\n\t\t}\n\n\t\t.container {\n\t\t\tbackground: #1e1e30;\n\t\t\tborder-radius: 16px;\n\t\t\tpadding: 32px;\n\t\t\tmax-width: 400px;\n\t\t\twidth: 100%;\n\t\t\tbox-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.error-icon {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tbackground: rgba(239, 68, 68, 0.1);\n\t\t\tborder-radius: 50%;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tmargin: 0 auto 16px;\n\t\t\tfont-size: 32px;\n\t\t}\n\n\t\th1 {\n\t\t\tfont-size: 20px;\n\t\t\tmargin-bottom: 8px;\n\t\t\tcolor: #f87171;\n\t\t}\n\n\t\tp {\n\t\t\tcolor: #9ca3af;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\tcode {\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t\tpadding: 2px 6px;\n\t\t\tborder-radius: 4px;\n\t\t\tfont-size: 12px;\n\t\t}\n\t</style>\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<div class=\"error-icon\">!</div>\n\t\t<h1>Authorization Error</h1>\n\t\t<p>${escapedDescription}</p>\n\t\t<p style=\"margin-top: 8px;\"><code>${escapedError}</code></p>\n\t\t${redirectHtml}\n\t</div>\n</body>\n</html>`;\n}\n","/**\n * Client authentication for confidential clients using private_key_jwt\n * Implements RFC 7523 (JWT Bearer Client Authentication)\n */\n\nimport { jwtVerify, createRemoteJWKSet, importJWK, errors } from \"jose\";\nimport type { JWTPayload } from \"jose\";\nimport type { ClientMetadata, JWK } from \"./storage.js\";\n\nconst { JOSEError } = errors;\n\n/** Expected assertion type for private_key_jwt */\nexport const JWT_BEARER_ASSERTION_TYPE = \"urn:ietf:params:oauth:client-assertion-type:jwt-bearer\";\n\n/**\n * Client authentication error\n */\nexport class ClientAuthError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly code: string\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"ClientAuthError\";\n\t}\n}\n\n/**\n * Result of client authentication\n */\nexport interface ClientAuthResult {\n\t/** Whether client authentication was performed */\n\tauthenticated: boolean;\n\t/** The client ID from the assertion (if authenticated) */\n\tclientId?: string;\n}\n\n/**\n * Options for client authentication\n */\nexport interface ClientAuthOptions {\n\t/** Token endpoint URL (for audience validation) */\n\ttokenEndpoint: string;\n\t/** Fetch function for fetching remote JWKS (for testing) */\n\tfetch?: typeof globalThis.fetch;\n\t/** Check if a JTI has been used (for replay prevention) */\n\tcheckJti?: (jti: string) => Promise<boolean>;\n}\n\n/**\n * Parse client assertion from request parameters\n */\nexport function parseClientAssertion(params: Record<string, string>): {\n\tassertionType?: string;\n\tassertion?: string;\n} {\n\treturn {\n\t\tassertionType: params.client_assertion_type,\n\t\tassertion: params.client_assertion,\n\t};\n}\n\n/**\n * Verify a client assertion JWT\n * @param assertion The JWT assertion\n * @param client The client metadata (with JWKS)\n * @param options Verification options\n * @returns The verified JWT payload\n * @throws ClientAuthError if verification fails\n */\nexport async function verifyClientAssertion(\n\tassertion: string,\n\tclient: ClientMetadata,\n\toptions: ClientAuthOptions\n): Promise<JWTPayload> {\n\tconst { tokenEndpoint, fetch: fetchFn = globalThis.fetch.bind(globalThis), checkJti } = options;\n\n\t// Get the key resolver\n\tlet keyResolver: Parameters<typeof jwtVerify>[1];\n\n\tif (client.jwks && client.jwks.keys.length > 0) {\n\t\t// For inline JWKS, we need to find the right key based on the JWT header\n\t\tkeyResolver = async (header) => {\n\t\t\tconst keys = client.jwks!.keys;\n\t\t\t// Find key by kid if present, otherwise use first key with matching alg\n\t\t\tlet key: JWK | undefined;\n\t\t\tif (header.kid) {\n\t\t\t\tkey = keys.find((k) => k.kid === header.kid);\n\t\t\t}\n\t\t\tif (!key) {\n\t\t\t\tkey = keys.find((k) => !k.alg || k.alg === header.alg);\n\t\t\t}\n\t\t\tif (!key) {\n\t\t\t\tkey = keys[0];\n\t\t\t}\n\t\t\tif (!key) {\n\t\t\t\tthrow new ClientAuthError(\"No suitable key found in client JWKS\", \"invalid_client\");\n\t\t\t}\n\t\t\t// Pass the algorithm from the header when the JWK doesn't have one\n\t\t\tconst alg = key.alg ?? header.alg;\n\t\t\treturn importJWK(key as Parameters<typeof importJWK>[0], alg);\n\t\t};\n\t} else if (client.jwksUri) {\n\t\t// Use remote JWKS\n\t\tkeyResolver = createRemoteJWKSet(new URL(client.jwksUri), {\n\t\t\t[Symbol.for(\"fetch\")]: fetchFn,\n\t\t});\n\t} else {\n\t\tthrow new ClientAuthError(\"Client has no JWKS configured\", \"invalid_client\");\n\t}\n\n\tlet payload: JWTPayload;\n\ttry {\n\t\tconst result = await jwtVerify(assertion, keyResolver, {\n\t\t\talgorithms: [\"ES256\"], // ATProto requires ES256\n\t\t\tclockTolerance: 30, // 30 seconds clock skew tolerance\n\t\t\tmaxTokenAge: \"5m\", // JWTs should be short-lived\n\t\t});\n\t\tpayload = result.payload;\n\t} catch (err) {\n\t\tif (err instanceof JOSEError) {\n\t\t\tthrow new ClientAuthError(`JWT verification failed: ${err.message}`, \"invalid_client\");\n\t\t}\n\t\tthrow new ClientAuthError(\n\t\t\t`JWT verification failed: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t\"invalid_client\"\n\t\t);\n\t}\n\n\t// Validate required claims per RFC 7523\n\n\t// iss (issuer) must equal client_id\n\tif (payload.iss !== client.clientId) {\n\t\tthrow new ClientAuthError(\n\t\t\t`JWT issuer mismatch: expected ${client.clientId}, got ${payload.iss}`,\n\t\t\t\"invalid_client\"\n\t\t);\n\t}\n\n\t// sub (subject) must equal client_id\n\tif (payload.sub !== client.clientId) {\n\t\tthrow new ClientAuthError(\n\t\t\t`JWT subject mismatch: expected ${client.clientId}, got ${payload.sub}`,\n\t\t\t\"invalid_client\"\n\t\t);\n\t}\n\n\t// aud (audience) must include the token endpoint\n\tconst aud = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];\n\tif (!aud.includes(tokenEndpoint)) {\n\t\tthrow new ClientAuthError(\n\t\t\t`JWT audience must include token endpoint: ${tokenEndpoint}`,\n\t\t\t\"invalid_client\"\n\t\t);\n\t}\n\n\t// jti (JWT ID) must be present and unique\n\tif (!payload.jti) {\n\t\tthrow new ClientAuthError(\"JWT must include jti claim\", \"invalid_client\");\n\t}\n\n\t// Check jti for replay prevention if callback provided\n\tif (checkJti) {\n\t\tconst isUnique = await checkJti(payload.jti);\n\t\tif (!isUnique) {\n\t\t\tthrow new ClientAuthError(\"JWT has already been used (replay detected)\", \"invalid_client\");\n\t\t}\n\t}\n\n\t// iat (issued at) must be present (verified by jose maxTokenAge)\n\tif (!payload.iat) {\n\t\tthrow new ClientAuthError(\"JWT must include iat claim\", \"invalid_client\");\n\t}\n\n\treturn payload;\n}\n\n/**\n * Authenticate a client from request parameters\n * @param params Request parameters containing client_id, client_assertion_type, client_assertion\n * @param getClient Function to resolve client metadata\n * @param options Authentication options\n * @returns Authentication result\n * @throws ClientAuthError if authentication fails\n */\nexport async function authenticateClient(\n\tparams: Record<string, string>,\n\tgetClient: (clientId: string) => Promise<ClientMetadata | null>,\n\toptions: ClientAuthOptions\n): Promise<ClientAuthResult> {\n\tconst clientId = params.client_id;\n\tif (!clientId) {\n\t\tthrow new ClientAuthError(\"Missing client_id\", \"invalid_request\");\n\t}\n\n\tconst { assertionType, assertion } = parseClientAssertion(params);\n\n\t// Resolve client metadata\n\tconst client = await getClient(clientId);\n\tif (!client) {\n\t\tthrow new ClientAuthError(`Unknown client: ${clientId}`, \"invalid_client\");\n\t}\n\n\tconst authMethod = client.tokenEndpointAuthMethod ?? \"none\";\n\n\t// Public client (no authentication required)\n\tif (authMethod === \"none\") {\n\t\t// If assertion is provided for public client, that's an error\n\t\tif (assertion || assertionType) {\n\t\t\tthrow new ClientAuthError(\n\t\t\t\t\"Client assertion not expected for public client\",\n\t\t\t\t\"invalid_request\"\n\t\t\t);\n\t\t}\n\t\treturn { authenticated: false, clientId };\n\t}\n\n\t// Confidential client (private_key_jwt required)\n\tif (authMethod === \"private_key_jwt\") {\n\t\tif (!assertionType || !assertion) {\n\t\t\tthrow new ClientAuthError(\n\t\t\t\t\"Client assertion required for confidential client\",\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tif (assertionType !== JWT_BEARER_ASSERTION_TYPE) {\n\t\t\tthrow new ClientAuthError(\n\t\t\t\t`Unsupported assertion type: ${assertionType}. Expected: ${JWT_BEARER_ASSERTION_TYPE}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\t// Verify the JWT assertion\n\t\tawait verifyClientAssertion(assertion, client, options);\n\n\t\treturn { authenticated: true, clientId };\n\t}\n\n\tthrow new ClientAuthError(`Unsupported auth method: ${authMethod}`, \"invalid_client\");\n}\n","/**\n * Core OAuth 2.1 Provider with AT Protocol extensions\n * Orchestrates authorization code flow with PKCE, DPoP, and PAR\n */\n\nimport type { OAuthAuthorizationServerMetadata } from \"@atproto/oauth-types\";\nimport type { OAuthStorage, AuthCodeData, TokenData, ClientMetadata } from \"./storage.js\";\nimport { verifyPkceChallenge } from \"./pkce.js\";\nimport { verifyDpopProof, DpopError, generateDpopNonce } from \"./dpop.js\";\nimport { PARHandler } from \"./par.js\";\nimport { ClientResolver } from \"./client-resolver.js\";\nimport {\n\tgenerateAuthCode,\n\tgenerateTokens,\n\trefreshTokens,\n\tbuildTokenResponse,\n\textractAccessToken,\n\tisTokenValid,\n\tAUTH_CODE_TTL,\n} from \"./tokens.js\";\nimport { renderConsentUI, renderErrorPage, CONSENT_UI_CSP } from \"./ui.js\";\nimport { authenticateClient, ClientAuthError } from \"./client-auth.js\";\n\n/**\n * OAuth provider configuration\n */\nexport interface OAuthProviderConfig {\n\t/** OAuth storage implementation */\n\tstorage: OAuthStorage;\n\t/** The OAuth issuer URL (e.g., https://your-pds.com) */\n\tissuer: string;\n\t/** Whether DPoP is required for all tokens (default: true for AT Protocol) */\n\tdpopRequired?: boolean;\n\t/** Whether PAR is enabled (default: true) */\n\tenablePAR?: boolean;\n\t/** Client resolver for DID-based discovery */\n\tclientResolver?: ClientResolver;\n\t/** Callback to verify user credentials */\n\tverifyUser?: (password: string) => Promise<{ sub: string; handle: string } | null>;\n\t/** Get the current user (if already authenticated) */\n\tgetCurrentUser?: () => Promise<{ sub: string; handle: string } | null>;\n}\n\n/**\n * OAuth error response builder\n */\nfunction oauthError(error: string, description: string, status: number = 400): Response {\n\treturn new Response(\n\t\tJSON.stringify({\n\t\t\terror,\n\t\t\terror_description: description,\n\t\t}),\n\t\t{\n\t\t\tstatus,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t}\n\t);\n}\n\n/**\n * Error thrown when request body parsing fails\n */\nexport class RequestBodyError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"RequestBodyError\";\n\t}\n}\n\n/**\n * Parse request body from JSON or form-urlencoded\n * @throws RequestBodyError if content type is unsupported or parsing fails\n */\nexport async function parseRequestBody(request: Request): Promise<Record<string, string>> {\n\tconst contentType = request.headers.get(\"Content-Type\") ?? \"\";\n\n\ttry {\n\t\tif (contentType.includes(\"application/json\")) {\n\t\t\tconst json = await request.json();\n\t\t\treturn Object.fromEntries(\n\t\t\t\tObject.entries(json as Record<string, unknown>).map(([k, v]) => [k, String(v)])\n\t\t\t);\n\t\t} else if (contentType.includes(\"application/x-www-form-urlencoded\")) {\n\t\t\tconst body = await request.text();\n\t\t\treturn Object.fromEntries(new URLSearchParams(body).entries());\n\t\t} else {\n\t\t\tthrow new RequestBodyError(\n\t\t\t\t\"Content-Type must be application/json or application/x-www-form-urlencoded\"\n\t\t\t);\n\t\t}\n\t} catch (e) {\n\t\tif (e instanceof RequestBodyError) {\n\t\t\tthrow e;\n\t\t}\n\t\tthrow new RequestBodyError(\"Failed to parse request body\");\n\t}\n}\n\n/**\n * AT Protocol OAuth 2.1 Provider\n */\nexport class ATProtoOAuthProvider {\n\tprivate storage: OAuthStorage;\n\tprivate issuer: string;\n\tprivate dpopRequired: boolean;\n\tprivate enablePAR: boolean;\n\tprivate parHandler: PARHandler;\n\tprivate clientResolver: ClientResolver;\n\tprivate verifyUser?: (password: string) => Promise<{ sub: string; handle: string } | null>;\n\tprivate getCurrentUser?: () => Promise<{ sub: string; handle: string } | null>;\n\n\tconstructor(config: OAuthProviderConfig) {\n\t\tthis.storage = config.storage;\n\t\tthis.issuer = config.issuer;\n\t\tthis.dpopRequired = config.dpopRequired ?? true;\n\t\tthis.enablePAR = config.enablePAR ?? true;\n\t\tthis.parHandler = new PARHandler(config.storage, config.issuer);\n\t\tthis.clientResolver = config.clientResolver ?? new ClientResolver({ storage: config.storage });\n\t\tthis.verifyUser = config.verifyUser;\n\t\tthis.getCurrentUser = config.getCurrentUser;\n\t}\n\n\t/**\n\t * Handle authorization request (GET/POST /oauth/authorize)\n\t */\n\tasync handleAuthorize(request: Request): Promise<Response> {\n\t\tconst url = new URL(request.url);\n\n\t\t// Parse OAuth params from query string (GET) or form data (POST)\n\t\tlet params: Record<string, string>;\n\n\t\tif (request.method === \"POST\") {\n\t\t\t// POST: parse from form data (includes hidden fields with OAuth params)\n\t\t\tconst formData = await request.formData();\n\t\t\tparams = {};\n\t\t\tfor (const [key, value] of formData.entries()) {\n\t\t\t\tif (typeof value === \"string\") {\n\t\t\t\t\tparams[key] = value;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// GET: check for PAR or query params\n\t\t\tconst requestUri = url.searchParams.get(\"request_uri\");\n\t\t\tconst clientId = url.searchParams.get(\"client_id\");\n\n\t\t\tif (requestUri && this.enablePAR) {\n\t\t\t\tif (!clientId) {\n\t\t\t\t\treturn this.renderError(\"invalid_request\", \"client_id required with request_uri\");\n\t\t\t\t}\n\t\t\t\tconst parParams = await this.parHandler.retrieveParams(requestUri, clientId);\n\t\t\t\tif (!parParams) {\n\t\t\t\t\treturn this.renderError(\"invalid_request\", \"Invalid or expired request_uri\");\n\t\t\t\t}\n\t\t\t\tparams = parParams;\n\t\t\t} else {\n\t\t\t\t// Parse query parameters\n\t\t\t\tparams = Object.fromEntries(url.searchParams.entries());\n\t\t\t}\n\t\t}\n\n\t\t// Validate required parameters\n\t\tconst required = [\"client_id\", \"redirect_uri\", \"response_type\", \"code_challenge\", \"state\"];\n\t\tfor (const param of required) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn this.renderError(\"invalid_request\", `Missing required parameter: ${param}`);\n\t\t\t}\n\t\t}\n\n\t\t// Validate response_type\n\t\tif (params.response_type !== \"code\") {\n\t\t\treturn this.renderError(\"unsupported_response_type\", \"Only response_type=code is supported\");\n\t\t}\n\n\t\t// Validate code_challenge_method\n\t\tif (params.code_challenge_method && params.code_challenge_method !== \"S256\") {\n\t\t\treturn this.renderError(\"invalid_request\", \"Only code_challenge_method=S256 is supported\");\n\t\t}\n\n\t\t// Resolve client metadata\n\t\tlet client: ClientMetadata;\n\t\ttry {\n\t\t\tclient = await this.clientResolver.resolveClient(params.client_id!);\n\t\t} catch (e) {\n\t\t\treturn this.renderError(\"invalid_client\", `Failed to resolve client: ${e}`);\n\t\t}\n\n\t\t// Validate redirect_uri\n\t\tif (!client.redirectUris.includes(params.redirect_uri!)) {\n\t\t\treturn this.renderError(\"invalid_request\", \"Invalid redirect_uri for this client\");\n\t\t}\n\n\t\t// Handle POST (form submission)\n\t\tif (request.method === \"POST\") {\n\t\t\treturn this.handleAuthorizePost(request, params, client);\n\t\t}\n\n\t\t// Check if user is authenticated\n\t\tlet user: { sub: string; handle: string } | null = null;\n\t\tif (this.getCurrentUser) {\n\t\t\tuser = await this.getCurrentUser();\n\t\t}\n\n\t\t// Show consent UI\n\t\tconst scope = params.scope ?? \"atproto\";\n\t\tconst html = renderConsentUI({\n\t\t\tclient,\n\t\t\tscope,\n\t\t\tauthorizeUrl: url.pathname,\n\t\t\tstate: params.state!,\n\t\t\toauthParams: params,\n\t\t\tuserHandle: user?.handle,\n\t\t\tshowLogin: !user && !!this.verifyUser,\n\t\t});\n\n\t\treturn new Response(html, {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Handle authorization form POST\n\t */\n\tprivate async handleAuthorizePost(\n\t\trequest: Request,\n\t\tparams: Record<string, string>,\n\t\tclient: ClientMetadata\n\t): Promise<Response> {\n\t\t// Form data was already parsed in handleAuthorize - extract action and password\n\t\tconst action = params.action;\n\t\tconst password = params.password ?? null;\n\n\t\tconst redirectUri = params.redirect_uri!;\n\t\tconst state = params.state!;\n\t\t// Default response_mode is \"query\" for authorization code flow per RFC 6749\n\t\tconst responseMode = params.response_mode ?? \"query\";\n\n\t\t// Handle deny\n\t\tif (action === \"deny\") {\n\t\t\tconst errorUrl = new URL(redirectUri);\n\n\t\t\tif (responseMode === \"fragment\") {\n\t\t\t\tconst hashParams = new URLSearchParams();\n\t\t\t\thashParams.set(\"error\", \"access_denied\");\n\t\t\t\thashParams.set(\"error_description\", \"User denied authorization\");\n\t\t\t\thashParams.set(\"state\", state);\n\t\t\t\thashParams.set(\"iss\", this.issuer);\n\t\t\t\terrorUrl.hash = hashParams.toString();\n\t\t\t} else {\n\t\t\t\terrorUrl.searchParams.set(\"error\", \"access_denied\");\n\t\t\t\terrorUrl.searchParams.set(\"error_description\", \"User denied authorization\");\n\t\t\t\terrorUrl.searchParams.set(\"state\", state);\n\t\t\t\terrorUrl.searchParams.set(\"iss\", this.issuer);\n\t\t\t}\n\n\t\t\treturn Response.redirect(errorUrl.toString(), 302);\n\t\t}\n\n\t\t// Get or verify user\n\t\tlet user: { sub: string; handle: string } | null = null;\n\n\t\tif (this.getCurrentUser) {\n\t\t\tuser = await this.getCurrentUser();\n\t\t}\n\n\t\tif (!user && password && this.verifyUser) {\n\t\t\tuser = await this.verifyUser(password);\n\t\t}\n\n\t\tif (!user) {\n\t\t\t// Show login form with error\n\t\t\tconst url = new URL(request.url);\n\t\t\tconst scope = params.scope ?? \"atproto\";\n\t\t\tconst html = renderConsentUI({\n\t\t\t\tclient,\n\t\t\t\tscope,\n\t\t\t\tauthorizeUrl: url.pathname,\n\t\t\t\tstate,\n\t\t\t\toauthParams: params,\n\t\t\t\tshowLogin: true,\n\t\t\t\terror: \"Invalid password\",\n\t\t\t});\n\t\t\treturn new Response(html, {\n\t\t\t\tstatus: 401,\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Generate authorization code\n\t\tconst code = generateAuthCode();\n\t\tconst scope = params.scope ?? \"atproto\";\n\n\t\tconst authCodeData: AuthCodeData = {\n\t\t\tclientId: params.client_id!,\n\t\t\tredirectUri,\n\t\t\tcodeChallenge: params.code_challenge!,\n\t\t\tcodeChallengeMethod: \"S256\",\n\t\t\tscope,\n\t\t\tsub: user.sub,\n\t\t\texpiresAt: Date.now() + AUTH_CODE_TTL,\n\t\t};\n\n\t\tawait this.storage.saveAuthCode(code, authCodeData);\n\n\t\t// Redirect with code (using fragment mode if requested)\n\t\tconst successUrl = new URL(redirectUri);\n\n\t\tif (responseMode === \"fragment\") {\n\t\t\t// Put params in hash fragment\n\t\t\tconst hashParams = new URLSearchParams();\n\t\t\thashParams.set(\"code\", code);\n\t\t\thashParams.set(\"state\", state);\n\t\t\thashParams.set(\"iss\", this.issuer);\n\t\t\tsuccessUrl.hash = hashParams.toString();\n\t\t} else {\n\t\t\t// Put params in query string\n\t\t\tsuccessUrl.searchParams.set(\"code\", code);\n\t\t\tsuccessUrl.searchParams.set(\"state\", state);\n\t\t\tsuccessUrl.searchParams.set(\"iss\", this.issuer);\n\t\t}\n\n\t\treturn Response.redirect(successUrl.toString(), 302);\n\t}\n\n\t/**\n\t * Handle token request (POST /oauth/token)\n\t */\n\tasync handleToken(request: Request): Promise<Response> {\n\t\tlet params: Record<string, string>;\n\t\ttry {\n\t\t\tparams = await parseRequestBody(request);\n\t\t} catch (e) {\n\t\t\treturn oauthError(\"invalid_request\", e instanceof Error ? e.message : \"Invalid request\");\n\t\t}\n\n\t\tconst grantType = params.grant_type;\n\n\t\tif (grantType === \"authorization_code\") {\n\t\t\treturn this.handleAuthorizationCodeGrant(request, params);\n\t\t} else if (grantType === \"refresh_token\") {\n\t\t\treturn this.handleRefreshTokenGrant(request, params);\n\t\t} else {\n\t\t\treturn oauthError(\"unsupported_grant_type\", `Unsupported grant_type: ${grantType}`);\n\t\t}\n\t}\n\n\t/**\n\t * Handle authorization code grant\n\t */\n\tprivate async handleAuthorizationCodeGrant(\n\t\trequest: Request,\n\t\tparams: Record<string, string>\n\t): Promise<Response> {\n\t\t// Validate required parameters\n\t\tconst required = [\"code\", \"client_id\", \"redirect_uri\", \"code_verifier\"];\n\t\tfor (const param of required) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn oauthError(\"invalid_request\", `Missing required parameter: ${param}`);\n\t\t\t}\n\t\t}\n\n\t\t// Authenticate client (validates private_key_jwt for confidential clients)\n\t\ttry {\n\t\t\tawait authenticateClient(\n\t\t\t\tparams,\n\t\t\t\tasync (clientId) => {\n\t\t\t\t\tif (this.clientResolver) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn await this.clientResolver.resolveClient(clientId);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn this.storage.getClient(clientId);\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttokenEndpoint: `${this.issuer}/oauth/token`,\n\t\t\t\t\tcheckJti: async (jti) => this.storage.checkAndSaveNonce(jti),\n\t\t\t\t}\n\t\t\t);\n\t\t} catch (e) {\n\t\t\tif (e instanceof ClientAuthError) {\n\t\t\t\treturn oauthError(e.code, e.message);\n\t\t\t}\n\t\t\treturn oauthError(\"invalid_client\", \"Client authentication failed\");\n\t\t}\n\n\t\t// Get authorization code data\n\t\tconst codeData = await this.storage.getAuthCode(params.code!);\n\t\tif (!codeData) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Invalid or expired authorization code\");\n\t\t}\n\n\t\t// Delete code (one-time use)\n\t\tawait this.storage.deleteAuthCode(params.code!);\n\n\t\t// Verify client_id matches\n\t\tif (codeData.clientId !== params.client_id) {\n\t\t\treturn oauthError(\"invalid_grant\", \"client_id mismatch\");\n\t\t}\n\n\t\t// Verify redirect_uri matches\n\t\tif (codeData.redirectUri !== params.redirect_uri) {\n\t\t\treturn oauthError(\"invalid_grant\", \"redirect_uri mismatch\");\n\t\t}\n\n\t\t// Verify PKCE\n\t\tconst pkceValid = await verifyPkceChallenge(\n\t\t\tparams.code_verifier!,\n\t\t\tcodeData.codeChallenge,\n\t\t\tcodeData.codeChallengeMethod\n\t\t);\n\t\tif (!pkceValid) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Invalid code_verifier\");\n\t\t}\n\n\t\t// Verify DPoP if required\n\t\tlet dpopJkt: string | undefined;\n\t\tif (this.dpopRequired) {\n\t\t\ttry {\n\t\t\t\tconst dpopProof = await verifyDpopProof(request);\n\n\t\t\t\t// Verify jti is unique (replay prevention)\n\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP proof replay detected\");\n\t\t\t\t}\n\n\t\t\t\tdpopJkt = dpopProof.jkt;\n\t\t\t} catch (e) {\n\t\t\t\tif (e instanceof DpopError) {\n\t\t\t\t\t// Check if we need to send a nonce\n\t\t\t\t\tif (e.code === \"use_dpop_nonce\") {\n\t\t\t\t\t\tconst nonce = generateDpopNonce();\n\t\t\t\t\t\treturn new Response(\n\t\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\t\terror: \"use_dpop_nonce\",\n\t\t\t\t\t\t\t\terror_description: \"DPoP nonce required\",\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tstatus: 400,\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\"DPoP-Nonce\": nonce,\n\t\t\t\t\t\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", e.message);\n\t\t\t\t}\n\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP verification failed\");\n\t\t\t}\n\t\t} else {\n\t\t\t// Check if DPoP header is present (optional but binding)\n\t\t\tconst dpopHeader = request.headers.get(\"DPoP\");\n\t\t\tif (dpopHeader) {\n\t\t\t\ttry {\n\t\t\t\t\tconst dpopProof = await verifyDpopProof(request);\n\t\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP proof replay detected\");\n\t\t\t\t\t}\n\t\t\t\t\tdpopJkt = dpopProof.jkt;\n\t\t\t\t} catch (e) {\n\t\t\t\t\tif (e instanceof DpopError) {\n\t\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", e.message);\n\t\t\t\t\t}\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP verification failed\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Generate tokens\n\t\tconst { tokens, tokenData } = generateTokens({\n\t\t\tsub: codeData.sub,\n\t\t\tclientId: codeData.clientId,\n\t\t\tscope: codeData.scope,\n\t\t\tdpopJkt,\n\t\t});\n\n\t\t// Save tokens\n\t\tawait this.storage.saveTokens(tokenData);\n\n\t\t// Return token response\n\t\treturn new Response(JSON.stringify(buildTokenResponse(tokens)), {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Handle refresh token grant\n\t */\n\tprivate async handleRefreshTokenGrant(\n\t\trequest: Request,\n\t\tparams: Record<string, string>\n\t): Promise<Response> {\n\t\tconst refreshToken = params.refresh_token;\n\t\tif (!refreshToken) {\n\t\t\treturn oauthError(\"invalid_request\", \"Missing refresh_token parameter\");\n\t\t}\n\n\t\t// Authenticate client if client_id is provided\n\t\tif (params.client_id) {\n\t\t\ttry {\n\t\t\t\tawait authenticateClient(\n\t\t\t\t\tparams,\n\t\t\t\t\tasync (clientId) => {\n\t\t\t\t\t\tif (this.clientResolver) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\treturn await this.clientResolver.resolveClient(clientId);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn this.storage.getClient(clientId);\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\ttokenEndpoint: `${this.issuer}/oauth/token`,\n\t\t\t\t\t\tcheckJti: async (jti) => this.storage.checkAndSaveNonce(jti),\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t} catch (e) {\n\t\t\t\tif (e instanceof ClientAuthError) {\n\t\t\t\t\treturn oauthError(e.code, e.message);\n\t\t\t\t}\n\t\t\t\treturn oauthError(\"invalid_client\", \"Client authentication failed\");\n\t\t\t}\n\t\t}\n\n\t\t// Get token data\n\t\tconst existingData = await this.storage.getTokenByRefresh(refreshToken);\n\t\tif (!existingData) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Invalid refresh token\");\n\t\t}\n\n\t\t// Check if token was revoked\n\t\tif (existingData.revoked) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Token has been revoked\");\n\t\t}\n\n\t\t// Verify client_id if provided\n\t\tif (params.client_id && params.client_id !== existingData.clientId) {\n\t\t\treturn oauthError(\"invalid_grant\", \"client_id mismatch\");\n\t\t}\n\n\t\t// Verify DPoP if token was DPoP-bound\n\t\tif (existingData.dpopJkt) {\n\t\t\ttry {\n\t\t\t\tconst dpopProof = await verifyDpopProof(request);\n\n\t\t\t\t// Verify key thumbprint matches\n\t\t\t\tif (dpopProof.jkt !== existingData.dpopJkt) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP key mismatch\");\n\t\t\t\t}\n\n\t\t\t\t// Verify jti is unique\n\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP proof replay detected\");\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tif (e instanceof DpopError) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", e.message);\n\t\t\t\t}\n\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP verification failed\");\n\t\t\t}\n\t\t}\n\n\t\t// Revoke old tokens\n\t\tawait this.storage.revokeToken(existingData.accessToken);\n\n\t\t// Generate new tokens (with refresh token rotation)\n\t\tconst { tokens, tokenData } = refreshTokens(existingData, true);\n\n\t\t// Save new tokens\n\t\tawait this.storage.saveTokens(tokenData);\n\n\t\t// Return token response\n\t\treturn new Response(JSON.stringify(buildTokenResponse(tokens)), {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Handle PAR request (POST /oauth/par)\n\t */\n\tasync handlePAR(request: Request): Promise<Response> {\n\t\tif (!this.enablePAR) {\n\t\t\treturn oauthError(\"invalid_request\", \"PAR is not enabled\");\n\t\t}\n\t\treturn this.parHandler.handlePushRequest(request);\n\t}\n\n\t/**\n\t * Handle metadata request (GET /.well-known/oauth-authorization-server)\n\t */\n\thandleMetadata(): Response {\n\t\t// URLs are built dynamically so we cast to the schema type\n\t\tconst metadata: OAuthAuthorizationServerMetadata = {\n\t\t\tissuer: this.issuer,\n\t\t\tauthorization_endpoint: `${this.issuer}/oauth/authorize`,\n\t\t\ttoken_endpoint: `${this.issuer}/oauth/token`,\n\t\t\tuserinfo_endpoint: `${this.issuer}/oauth/userinfo`,\n\t\t\tresponse_types_supported: [\"code\"],\n\t\t\tresponse_modes_supported: [\"fragment\", \"query\"],\n\t\t\tgrant_types_supported: [\"authorization_code\", \"refresh_token\"],\n\t\t\tcode_challenge_methods_supported: [\"S256\"],\n\t\t\ttoken_endpoint_auth_methods_supported: [\"none\", \"private_key_jwt\"],\n\t\t\tscopes_supported: [\"atproto\", \"transition:generic\", \"transition:chat.bsky\"],\n\t\t\tsubject_types_supported: [\"public\"],\n\t\t\tauthorization_response_iss_parameter_supported: true,\n\t\t\tclient_id_metadata_document_supported: true,\n\t\t\ttoken_endpoint_auth_signing_alg_values_supported: [\"ES256\"],\n\t\t\t...(this.enablePAR && {\n\t\t\t\tpushed_authorization_request_endpoint: `${this.issuer}/oauth/par`,\n\t\t\t\trequire_pushed_authorization_requests: false,\n\t\t\t}),\n\t\t\t...(this.dpopRequired && {\n\t\t\t\tdpop_signing_alg_values_supported: [\"ES256\"],\n\t\t\t}),\n\t\t} as OAuthAuthorizationServerMetadata;\n\n\t\treturn new Response(JSON.stringify(metadata), {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"max-age=3600\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Verify an access token from a request\n\t * @param request The HTTP request\n\t * @param requiredScope Optional scope to require\n\t * @returns Token data if valid\n\t */\n\tasync verifyAccessToken(\n\t\trequest: Request,\n\t\trequiredScope?: string\n\t): Promise<TokenData | null> {\n\t\t// Extract token from Authorization header\n\t\tconst tokenInfo = extractAccessToken(request);\n\t\tif (!tokenInfo) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Lookup token\n\t\tconst tokenData = await this.storage.getTokenByAccess(tokenInfo.token);\n\t\tif (!tokenData) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Check validity\n\t\tif (!isTokenValid(tokenData)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Check token type matches\n\t\tif (tokenData.dpopJkt && tokenInfo.type !== \"DPoP\") {\n\t\t\treturn null; // DPoP-bound token must use DPoP header\n\t\t}\n\n\t\t// Verify DPoP if token is bound\n\t\tif (tokenData.dpopJkt) {\n\t\t\ttry {\n\t\t\t\tconst dpopProof = await verifyDpopProof(request, {\n\t\t\t\t\taccessToken: tokenInfo.token,\n\t\t\t\t});\n\n\t\t\t\t// Verify key thumbprint matches\n\t\t\t\tif (dpopProof.jkt !== tokenData.dpopJkt) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\t// Verify jti is unique\n\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\t// Check scope if required\n\t\tif (requiredScope) {\n\t\t\tconst scopes = tokenData.scope.split(\" \");\n\t\t\tif (!scopes.includes(requiredScope)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\treturn tokenData;\n\t}\n\n\t/**\n\t * Render an error page\n\t */\n\tprivate renderError(error: string, description: string): Response {\n\t\tconst html = renderErrorPage(error, description);\n\t\treturn new Response(html, {\n\t\t\tstatus: 400,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n}\n","/**\n * OAuth storage interface and types\n * Defines the storage abstraction for auth codes, tokens, clients, etc.\n */\n\n/**\n * Data stored with an authorization code\n */\nexport interface AuthCodeData {\n\t/** Client DID that requested the code */\n\tclientId: string;\n\t/** Redirect URI used in the authorization request */\n\tredirectUri: string;\n\t/** PKCE code challenge */\n\tcodeChallenge: string;\n\t/** PKCE challenge method (always S256 for AT Protocol) */\n\tcodeChallengeMethod: \"S256\";\n\t/** Authorized scope */\n\tscope: string;\n\t/** User DID that authorized the request */\n\tsub: string;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n}\n\n/**\n * Data stored with access and refresh tokens\n */\nexport interface TokenData {\n\t/** Opaque access token */\n\taccessToken: string;\n\t/** Opaque refresh token */\n\trefreshToken: string;\n\t/** Client DID that received the token */\n\tclientId: string;\n\t/** User DID the token is for */\n\tsub: string;\n\t/** Authorized scope */\n\tscope: string;\n\t/** DPoP key thumbprint (for token binding) */\n\tdpopJkt?: string;\n\t/** Issuance timestamp (Unix ms) */\n\tissuedAt: number;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n\t/** Whether the token has been revoked */\n\trevoked?: boolean;\n}\n\n/**\n * JSON Web Key for client authentication\n */\nexport interface JWK {\n\tkty: string;\n\tuse?: string;\n\tkey_ops?: string[];\n\talg?: string;\n\tkid?: string;\n\t// EC key parameters\n\tcrv?: string;\n\tx?: string;\n\ty?: string;\n\t// RSA key parameters (not used for ATProto but included for completeness)\n\tn?: string;\n\te?: string;\n}\n\n/**\n * OAuth client metadata (discovered from DID document)\n */\nexport interface ClientMetadata {\n\t/** Client DID */\n\tclientId: string;\n\t/** Human-readable client name */\n\tclientName: string;\n\t/** Allowed redirect URIs */\n\tredirectUris: string[];\n\t/** Client logo URI (optional) */\n\tlogoUri?: string;\n\t/** Client homepage URI (optional) */\n\tclientUri?: string;\n\t/** Token endpoint auth method (\"none\" for public, \"private_key_jwt\" for confidential) */\n\ttokenEndpointAuthMethod?: \"none\" | \"private_key_jwt\";\n\t/** JSON Web Key Set for confidential client authentication */\n\tjwks?: { keys: JWK[] };\n\t/** URI to fetch JWKS from (alternative to inline jwks) */\n\tjwksUri?: string;\n\t/** When the metadata was cached (Unix ms) */\n\tcachedAt?: number;\n}\n\n/**\n * Data stored for Pushed Authorization Requests (PAR)\n */\nexport interface PARData {\n\t/** Client DID that pushed the request */\n\tclientId: string;\n\t/** All OAuth parameters from the push request */\n\tparams: Record<string, string>;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n}\n\n/**\n * Storage interface for OAuth data\n * Implementations should handle TTL-based expiration\n */\nexport interface OAuthStorage {\n\t// ============================================\n\t// Authorization Codes (5 min TTL)\n\t// ============================================\n\n\t/**\n\t * Save an authorization code\n\t * @param code The authorization code\n\t * @param data Associated data\n\t */\n\tsaveAuthCode(code: string, data: AuthCodeData): Promise<void>;\n\n\t/**\n\t * Get authorization code data\n\t * @param code The authorization code\n\t * @returns The data or null if not found/expired\n\t */\n\tgetAuthCode(code: string): Promise<AuthCodeData | null>;\n\n\t/**\n\t * Delete an authorization code (after use)\n\t * @param code The authorization code\n\t */\n\tdeleteAuthCode(code: string): Promise<void>;\n\n\t// ============================================\n\t// Tokens\n\t// ============================================\n\n\t/**\n\t * Save token data\n\t * @param data The token data\n\t */\n\tsaveTokens(data: TokenData): Promise<void>;\n\n\t/**\n\t * Get token data by access token\n\t * @param accessToken The access token\n\t * @returns The data or null if not found/expired/revoked\n\t */\n\tgetTokenByAccess(accessToken: string): Promise<TokenData | null>;\n\n\t/**\n\t * Get token data by refresh token\n\t * @param refreshToken The refresh token\n\t * @returns The data or null if not found/expired/revoked\n\t */\n\tgetTokenByRefresh(refreshToken: string): Promise<TokenData | null>;\n\n\t/**\n\t * Revoke a token by access token\n\t * @param accessToken The access token to revoke\n\t */\n\trevokeToken(accessToken: string): Promise<void>;\n\n\t/**\n\t * Revoke all tokens for a user (for logout)\n\t * @param sub The user DID\n\t */\n\trevokeAllTokens?(sub: string): Promise<void>;\n\n\t// ============================================\n\t// Clients (DID-based, cached)\n\t// ============================================\n\n\t/**\n\t * Save client metadata (cached from DID document)\n\t * @param clientId The client DID\n\t * @param metadata The client metadata\n\t */\n\tsaveClient(clientId: string, metadata: ClientMetadata): Promise<void>;\n\n\t/**\n\t * Get cached client metadata\n\t * @param clientId The client DID\n\t * @returns The metadata or null if not cached\n\t */\n\tgetClient(clientId: string): Promise<ClientMetadata | null>;\n\n\t// ============================================\n\t// PAR Requests (90 sec TTL)\n\t// ============================================\n\n\t/**\n\t * Save PAR request data\n\t * @param requestUri The unique request URI\n\t * @param data The PAR data\n\t */\n\tsavePAR(requestUri: string, data: PARData): Promise<void>;\n\n\t/**\n\t * Get PAR request data\n\t * @param requestUri The request URI\n\t * @returns The data or null if not found/expired\n\t */\n\tgetPAR(requestUri: string): Promise<PARData | null>;\n\n\t/**\n\t * Delete PAR request (after use - one-time use)\n\t * @param requestUri The request URI\n\t */\n\tdeletePAR(requestUri: string): Promise<void>;\n\n\t// ============================================\n\t// DPoP Nonces (5 min TTL, replay prevention)\n\t// ============================================\n\n\t/**\n\t * Check if a nonce has been used and save it if not\n\t * Used for DPoP replay prevention\n\t * @param nonce The nonce to check\n\t * @returns true if the nonce is new (valid), false if already used\n\t */\n\tcheckAndSaveNonce(nonce: string): Promise<boolean>;\n}\n\n/**\n * In-memory storage implementation for testing\n */\nexport class InMemoryOAuthStorage implements OAuthStorage {\n\tprivate authCodes = new Map<string, AuthCodeData>();\n\tprivate tokens = new Map<string, TokenData>();\n\tprivate refreshTokenIndex = new Map<string, string>(); // refreshToken -> accessToken\n\tprivate clients = new Map<string, ClientMetadata>();\n\tprivate parRequests = new Map<string, PARData>();\n\tprivate nonces = new Set<string>();\n\n\tasync saveAuthCode(code: string, data: AuthCodeData): Promise<void> {\n\t\tthis.authCodes.set(code, data);\n\t}\n\n\tasync getAuthCode(code: string): Promise<AuthCodeData | null> {\n\t\tconst data = this.authCodes.get(code);\n\t\tif (!data) return null;\n\t\tif (Date.now() > data.expiresAt) {\n\t\t\tthis.authCodes.delete(code);\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync deleteAuthCode(code: string): Promise<void> {\n\t\tthis.authCodes.delete(code);\n\t}\n\n\tasync saveTokens(data: TokenData): Promise<void> {\n\t\tthis.tokens.set(data.accessToken, data);\n\t\tthis.refreshTokenIndex.set(data.refreshToken, data.accessToken);\n\t}\n\n\tasync getTokenByAccess(accessToken: string): Promise<TokenData | null> {\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (!data) return null;\n\t\tif (data.revoked || Date.now() > data.expiresAt) {\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync getTokenByRefresh(refreshToken: string): Promise<TokenData | null> {\n\t\tconst accessToken = this.refreshTokenIndex.get(refreshToken);\n\t\tif (!accessToken) return null;\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (!data) return null;\n\t\tif (data.revoked) return null;\n\t\t// Refresh tokens don't use accessToken expiresAt\n\t\treturn data;\n\t}\n\n\tasync revokeToken(accessToken: string): Promise<void> {\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (data) {\n\t\t\tdata.revoked = true;\n\t\t}\n\t}\n\n\tasync revokeAllTokens(sub: string): Promise<void> {\n\t\tfor (const [, data] of this.tokens) {\n\t\t\tif (data.sub === sub) {\n\t\t\t\tdata.revoked = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync saveClient(clientId: string, metadata: ClientMetadata): Promise<void> {\n\t\tthis.clients.set(clientId, metadata);\n\t}\n\n\tasync getClient(clientId: string): Promise<ClientMetadata | null> {\n\t\treturn this.clients.get(clientId) ?? null;\n\t}\n\n\tasync savePAR(requestUri: string, data: PARData): Promise<void> {\n\t\tthis.parRequests.set(requestUri, data);\n\t}\n\n\tasync getPAR(requestUri: string): Promise<PARData | null> {\n\t\tconst data = this.parRequests.get(requestUri);\n\t\tif (!data) return null;\n\t\tif (Date.now() > data.expiresAt) {\n\t\t\tthis.parRequests.delete(requestUri);\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync deletePAR(requestUri: string): Promise<void> {\n\t\tthis.parRequests.delete(requestUri);\n\t}\n\n\tasync checkAndSaveNonce(nonce: string): Promise<boolean> {\n\t\tif (this.nonces.has(nonce)) {\n\t\t\treturn false;\n\t\t}\n\t\tthis.nonces.add(nonce);\n\t\t// Note: No auto-cleanup in test implementation - use clear() between tests\n\t\t// Production SQLite storage handles TTL-based cleanup properly\n\t\treturn true;\n\t}\n\n\t/** Clear all stored data (for testing) */\n\tclear(): void {\n\t\tthis.authCodes.clear();\n\t\tthis.tokens.clear();\n\t\tthis.refreshTokenIndex.clear();\n\t\tthis.clients.clear();\n\t\tthis.parRequests.clear();\n\t\tthis.nonces.clear();\n\t}\n}\n"],"mappings":"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"}
|