@getcirrus/oauth-provider 0.3.0 → 0.3.2
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 +58 -58
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +65 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,28 +29,28 @@ import { OAuthStorage } from "./your-storage-implementation";
|
|
|
29
29
|
|
|
30
30
|
// Initialize the provider
|
|
31
31
|
const provider = new OAuthProvider({
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
issuer: "https://your-pds.example.com",
|
|
33
|
+
storage: new OAuthStorage(),
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
// Handle OAuth endpoints in your Worker
|
|
37
37
|
app.post("/oauth/par", async (c) => {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const result = await provider.handlePAR(await c.req.formData());
|
|
39
|
+
return c.json(result);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
app.get("/oauth/authorize", async (c) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const result = await provider.handleAuthorize(c.req.url);
|
|
44
|
+
// Show authorization UI to user
|
|
45
|
+
return c.html(renderAuthUI(result));
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
app.post("/oauth/token", async (c) => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const result = await provider.handleToken(
|
|
50
|
+
await c.req.formData(),
|
|
51
|
+
c.req.header("DPoP"),
|
|
52
|
+
);
|
|
53
|
+
return c.json(result);
|
|
54
54
|
});
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -72,29 +72,29 @@ The provider uses a storage interface that you implement for your backend:
|
|
|
72
72
|
|
|
73
73
|
```typescript
|
|
74
74
|
export interface OAuthProviderStorage {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
75
|
+
// Authorization codes
|
|
76
|
+
saveAuthCode(code: string, data: AuthCodeData): Promise<void>;
|
|
77
|
+
getAuthCode(code: string): Promise<AuthCodeData | null>;
|
|
78
|
+
deleteAuthCode(code: string): Promise<void>;
|
|
79
|
+
|
|
80
|
+
// Access/refresh tokens
|
|
81
|
+
saveTokens(data: TokenData): Promise<void>;
|
|
82
|
+
getTokenByAccess(accessToken: string): Promise<TokenData | null>;
|
|
83
|
+
getTokenByRefresh(refreshToken: string): Promise<TokenData | null>;
|
|
84
|
+
revokeToken(accessToken: string): Promise<void>;
|
|
85
|
+
revokeAllTokens(sub: string): Promise<void>;
|
|
86
|
+
|
|
87
|
+
// Client metadata cache
|
|
88
|
+
saveClient(clientId: string, metadata: ClientMetadata): Promise<void>;
|
|
89
|
+
getClient(clientId: string): Promise<ClientMetadata | null>;
|
|
90
|
+
|
|
91
|
+
// PAR (Pushed Authorization Requests)
|
|
92
|
+
savePAR(requestUri: string, data: PARData): Promise<void>;
|
|
93
|
+
getPAR(requestUri: string): Promise<PARData | null>;
|
|
94
|
+
deletePAR(requestUri: string): Promise<void>;
|
|
95
|
+
|
|
96
|
+
// DPoP nonce tracking
|
|
97
|
+
checkAndSaveNonce(nonce: string): Promise<boolean>;
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
@@ -122,8 +122,8 @@ Response:
|
|
|
122
122
|
|
|
123
123
|
```json
|
|
124
124
|
{
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
"request_uri": "urn:ietf:params:oauth:request_uri:XXXXXX",
|
|
126
|
+
"expires_in": 90
|
|
127
127
|
}
|
|
128
128
|
```
|
|
129
129
|
|
|
@@ -162,12 +162,12 @@ Response:
|
|
|
162
162
|
|
|
163
163
|
```json
|
|
164
164
|
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
"access_token": "XXXXXX",
|
|
166
|
+
"token_type": "DPoP",
|
|
167
|
+
"expires_in": 3600,
|
|
168
|
+
"refresh_token": "YYYYYY",
|
|
169
|
+
"scope": "atproto",
|
|
170
|
+
"sub": "did:plc:abc123"
|
|
171
171
|
}
|
|
172
172
|
```
|
|
173
173
|
|
|
@@ -202,14 +202,14 @@ Clients are identified by a URL pointing to their metadata document:
|
|
|
202
202
|
|
|
203
203
|
```json
|
|
204
204
|
{
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
205
|
+
"client_id": "https://client.example.com/client-metadata.json",
|
|
206
|
+
"client_name": "Example App",
|
|
207
|
+
"redirect_uris": ["https://client.example.com/callback"],
|
|
208
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
|
209
|
+
"response_types": ["code"],
|
|
210
|
+
"scope": "atproto",
|
|
211
|
+
"token_endpoint_auth_method": "none",
|
|
212
|
+
"application_type": "web"
|
|
213
213
|
}
|
|
214
214
|
```
|
|
215
215
|
|
|
@@ -224,15 +224,15 @@ This provider is designed to work seamlessly with `@atproto/oauth-client`:
|
|
|
224
224
|
import { OAuthClient } from "@atproto/oauth-client";
|
|
225
225
|
|
|
226
226
|
const client = new OAuthClient({
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
clientMetadata: {
|
|
228
|
+
client_id: "https://my-app.example.com/client-metadata.json",
|
|
229
|
+
redirect_uris: ["https://my-app.example.com/callback"],
|
|
230
|
+
},
|
|
231
231
|
});
|
|
232
232
|
|
|
233
233
|
// Initiate login
|
|
234
234
|
const authUrl = await client.authorize("https://user-pds.example.com", {
|
|
235
|
-
|
|
235
|
+
scope: "atproto",
|
|
236
236
|
});
|
|
237
237
|
|
|
238
238
|
// Handle callback
|
|
@@ -245,8 +245,8 @@ The provider returns standard OAuth 2.1 error responses:
|
|
|
245
245
|
|
|
246
246
|
```json
|
|
247
247
|
{
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
"error": "invalid_request",
|
|
249
|
+
"error_description": "Missing required parameter: code_challenge"
|
|
250
250
|
}
|
|
251
251
|
```
|
|
252
252
|
|
package/dist/index.d.ts
CHANGED
|
@@ -243,18 +243,23 @@ declare class ClientResolver {
|
|
|
243
243
|
constructor(options?: ClientResolverOptions);
|
|
244
244
|
/**
|
|
245
245
|
* Resolve client metadata from a client ID (URL or DID)
|
|
246
|
-
* @param clientId The client ID (HTTPS URL or
|
|
246
|
+
* @param clientId The client ID (HTTPS URL, DID, or localhost URL)
|
|
247
247
|
* @returns The client metadata
|
|
248
248
|
* @throws ClientResolutionError if resolution fails
|
|
249
249
|
*/
|
|
250
250
|
resolveClient(clientId: string): Promise<ClientMetadata>;
|
|
251
251
|
/**
|
|
252
252
|
* Validate that a redirect URI is allowed for a client
|
|
253
|
-
* @param clientId The client DID
|
|
253
|
+
* @param clientId The client ID (URL or DID)
|
|
254
254
|
* @param redirectUri The redirect URI to validate
|
|
255
255
|
* @returns true if the redirect URI is allowed
|
|
256
256
|
*/
|
|
257
257
|
validateRedirectUri(clientId: string, redirectUri: string): Promise<boolean>;
|
|
258
|
+
/**
|
|
259
|
+
* Check if a redirect URI matches any allowed URI for localhost clients.
|
|
260
|
+
* Per AT Protocol spec, port numbers are not matched for localhost.
|
|
261
|
+
*/
|
|
262
|
+
private matchesLocalhostRedirectUri;
|
|
258
263
|
}
|
|
259
264
|
/**
|
|
260
265
|
* Create a client resolver with optional caching
|
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","../src/client-auth.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAQA;AAoBA;AAwBA;AAkBiB,UA9DA,YAAA,CA8Dc;EAwBd;EAaA,QAAA,EAAA,MAAY;EAUK;EAAe,WAAA,EAAA,MAAA;EAOb;EAAR,aAAA,EAAA,MAAA;EAMG;EAUb,mBAAA,EAAA,MAAA;EAAY;EAOkB,KAAA,EAAA,MAAA;EAAR;EAOU,GAAA,EAAA,MAAA;EAAR;EAMP,SAAA,EAAA,MAAA;;;;;AAwBL,UA5Jb,SAAA,CA4Ja;EAWK;EAAU,WAAA,EAAA,MAAA;EAOR;EAAR,YAAA,EAAA,MAAA;EAMG;EAYG,QAAA,EAAA,MAAA;EAAO;EAM7B,GAAA,EAAA,MAAA;EAQ2B;EAAe,KAAA,EAAA,MAAA;EAIb;EAAR,OAAA,CAAA,EAAA,MAAA;EAUG;EAIb,QAAA,EAAA,MAAA;EAAY;EAKkB,SAAA,EAAA,MAAA;EAAR;EASU,OAAA,CAAA,EAAA,OAAA;;;;;AAyBO,UA/O9C,GAAA,CA+O8C;EAInB,GAAA,EAAA,MAAA;EAAR,GAAA,CAAA,EAAA,MAAA;EAIK,OAAA,CAAA,EAAA,MAAA,EAAA;EAAU,GAAA,CAAA,EAAA,MAAA;EAIR,GAAA,CAAA,EAAA,MAAA;EAAR,GAAA,CAAA,EAAA,MAAA;EAUG,CAAA,CAAA,EAAA,MAAA;EAIG,CAAA,CAAA,EAAA,MAAA;EA3FI,CAAA,CAAA,EAAA,MAAA;EAAY,CAAA,CAAA,EAAA,MAAA;;;;ACjNzD;AAaiB,UDwCA,cAAA,CCxCqB;
|
|
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,
|
|
1
|
+
import { EmbeddedJWK, base64url, calculateJwkThumbprint, createLocalJWKSet, errors, jwtVerify } from "jose";
|
|
2
2
|
import { ensureValidDid } from "@atproto/syntax";
|
|
3
3
|
import { oauthClientMetadataSchema } from "@atproto/oauth-types";
|
|
4
4
|
|
|
@@ -297,6 +297,38 @@ function isHttpsUrl(value) {
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
/**
|
|
300
|
+
* Check if a client ID is a valid localhost client per AT Protocol spec.
|
|
301
|
+
* Localhost clients use http://localhost with no port, and encode
|
|
302
|
+
* redirect_uri and scope as query parameters.
|
|
303
|
+
* @see https://atproto.com/specs/oauth#clients
|
|
304
|
+
*/
|
|
305
|
+
function isLocalhostClient(value) {
|
|
306
|
+
try {
|
|
307
|
+
const url = new URL(value);
|
|
308
|
+
return url.protocol === "http:" && url.hostname === "localhost" && !url.port;
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Parse localhost client metadata from the client_id URL.
|
|
315
|
+
* Per AT Protocol spec, redirect_uri and scope are encoded as query params.
|
|
316
|
+
* Defaults to http://127.0.0.1/ and http://[::1]/ for redirect URIs.
|
|
317
|
+
*/
|
|
318
|
+
function parseLocalhostClientMetadata(clientId) {
|
|
319
|
+
const url = new URL(clientId);
|
|
320
|
+
const redirectUriParam = url.searchParams.get("redirect_uri");
|
|
321
|
+
const redirectUris = redirectUriParam ? [redirectUriParam] : ["http://127.0.0.1/", "http://[::1]/"];
|
|
322
|
+
url.searchParams.get("scope");
|
|
323
|
+
return {
|
|
324
|
+
clientId,
|
|
325
|
+
clientName: "Localhost Client",
|
|
326
|
+
redirectUris,
|
|
327
|
+
tokenEndpointAuthMethod: "none",
|
|
328
|
+
cachedAt: Date.now()
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
300
332
|
* Validate that a string is a valid DID using @atproto/syntax
|
|
301
333
|
*/
|
|
302
334
|
function isValidDid(value) {
|
|
@@ -335,11 +367,12 @@ var ClientResolver = class {
|
|
|
335
367
|
}
|
|
336
368
|
/**
|
|
337
369
|
* Resolve client metadata from a client ID (URL or DID)
|
|
338
|
-
* @param clientId The client ID (HTTPS URL or
|
|
370
|
+
* @param clientId The client ID (HTTPS URL, DID, or localhost URL)
|
|
339
371
|
* @returns The client metadata
|
|
340
372
|
* @throws ClientResolutionError if resolution fails
|
|
341
373
|
*/
|
|
342
374
|
async resolveClient(clientId) {
|
|
375
|
+
if (isLocalhostClient(clientId)) return parseLocalhostClientMetadata(clientId);
|
|
343
376
|
if (!isHttpsUrl(clientId) && !isValidDid(clientId)) throw new ClientResolutionError(`Invalid client ID format: ${clientId}`, "invalid_client");
|
|
344
377
|
if (this.storage) {
|
|
345
378
|
const cached = await this.storage.getClient(clientId);
|
|
@@ -368,7 +401,7 @@ var ClientResolver = class {
|
|
|
368
401
|
redirectUris: doc.redirect_uris,
|
|
369
402
|
logoUri: doc.logo_uri,
|
|
370
403
|
clientUri: doc.client_uri,
|
|
371
|
-
tokenEndpointAuthMethod: doc.token_endpoint_auth_method
|
|
404
|
+
tokenEndpointAuthMethod: doc.token_endpoint_auth_method === "private_key_jwt" ? "private_key_jwt" : "none",
|
|
372
405
|
jwks: doc.jwks,
|
|
373
406
|
jwksUri: doc.jwks_uri,
|
|
374
407
|
cachedAt: Date.now()
|
|
@@ -378,13 +411,31 @@ var ClientResolver = class {
|
|
|
378
411
|
}
|
|
379
412
|
/**
|
|
380
413
|
* Validate that a redirect URI is allowed for a client
|
|
381
|
-
* @param clientId The client DID
|
|
414
|
+
* @param clientId The client ID (URL or DID)
|
|
382
415
|
* @param redirectUri The redirect URI to validate
|
|
383
416
|
* @returns true if the redirect URI is allowed
|
|
384
417
|
*/
|
|
385
418
|
async validateRedirectUri(clientId, redirectUri) {
|
|
386
419
|
try {
|
|
387
|
-
|
|
420
|
+
const metadata = await this.resolveClient(clientId);
|
|
421
|
+
if (isLocalhostClient(clientId)) return this.matchesLocalhostRedirectUri(metadata.redirectUris, redirectUri);
|
|
422
|
+
return metadata.redirectUris.includes(redirectUri);
|
|
423
|
+
} catch {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Check if a redirect URI matches any allowed URI for localhost clients.
|
|
429
|
+
* Per AT Protocol spec, port numbers are not matched for localhost.
|
|
430
|
+
*/
|
|
431
|
+
matchesLocalhostRedirectUri(allowedUris, redirectUri) {
|
|
432
|
+
try {
|
|
433
|
+
const redirect = new URL(redirectUri);
|
|
434
|
+
for (const allowed of allowedUris) {
|
|
435
|
+
const allowedUrl = new URL(allowed);
|
|
436
|
+
if (redirect.protocol === allowedUrl.protocol && redirect.hostname === allowedUrl.hostname && redirect.pathname === allowedUrl.pathname) return true;
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
388
439
|
} catch {
|
|
389
440
|
return false;
|
|
390
441
|
}
|
|
@@ -1170,18 +1221,15 @@ function parseClientAssertion(params) {
|
|
|
1170
1221
|
async function verifyClientAssertion(assertion, client, options) {
|
|
1171
1222
|
const { tokenEndpoint, issuer, fetch: fetchFn = globalThis.fetch.bind(globalThis), checkJti } = options;
|
|
1172
1223
|
let keyResolver;
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if (!
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
};
|
|
1183
|
-
else if (client.jwksUri) keyResolver = createRemoteJWKSet(new URL(client.jwksUri), { [customFetch]: fetchFn });
|
|
1184
|
-
else throw new ClientAuthError("Client has no JWKS configured", "invalid_client");
|
|
1224
|
+
let jwks;
|
|
1225
|
+
if (client.jwks && client.jwks.keys.length > 0) jwks = client.jwks;
|
|
1226
|
+
else if (client.jwksUri) {
|
|
1227
|
+
const res = await fetchFn(client.jwksUri, { headers: { Accept: "application/json" } });
|
|
1228
|
+
if (!res.ok) throw new ClientAuthError(`Failed to fetch client JWKS: ${res.status}`, "invalid_client");
|
|
1229
|
+
jwks = await res.json();
|
|
1230
|
+
}
|
|
1231
|
+
if (!jwks?.keys?.length) throw new ClientAuthError("Client has no JWKS configured", "invalid_client");
|
|
1232
|
+
keyResolver = createLocalJWKSet({ keys: jwks.keys.map(({ key_ops, ...rest }) => rest) });
|
|
1185
1233
|
let payload;
|
|
1186
1234
|
try {
|
|
1187
1235
|
payload = (await jwtVerify(assertion, keyResolver, {
|
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}","JOSEError","params: Record<string, string>","parData: PARData","response: OAuthParResponse","body: OAuthErrorResponse","code: string","response: Response","doc: OAuthClientMetadata","metadata: ClientMetadata","tokenData: TokenData","passkeyAuthScriptHashPromise: Promise<string> | null","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","passkeyOptions: Record<string, unknown> | null","authCodeData: AuthCodeData","dpopJkt: string | undefined","metadata: OAuthAuthorizationServerMetadata","body: {\n\t\t\tresponse: unknown;\n\t\t\tchallenge: string;\n\t\t\toauthParams: Record<string, string>;\n\t\t}"],"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\t// Check cache validity: must have timestamp, not expired, and have auth method set\n\t\t\t// (entries without tokenEndpointAuthMethod are from before we added that field)\n\t\t\tif (cached && cached.cachedAt &&\n\t\t\t\tDate.now() - cached.cachedAt < this.cacheTtl &&\n\t\t\t\tcached.tokenEndpointAuthMethod !== undefined) {\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 * The passkey authentication script (static, can be hashed).\n * Dynamic data is passed via data attributes on the script element.\n */\nconst PASSKEY_AUTH_SCRIPT = `\n// Get dynamic data from script element\nconst scriptEl = document.currentScript;\nconst passkeyOptions = JSON.parse(scriptEl.dataset.passkeyOptions);\nconst oauthParams = JSON.parse(scriptEl.dataset.oauthParams);\n\n// Convert base64url to ArrayBuffer\nfunction base64urlToBuffer(base64url) {\n\tconst base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');\n\tconst padding = '='.repeat((4 - base64.length % 4) % 4);\n\tconst binary = atob(base64 + padding);\n\tconst bytes = new Uint8Array(binary.length);\n\tfor (let i = 0; i < binary.length; i++) {\n\t\tbytes[i] = binary.charCodeAt(i);\n\t}\n\treturn bytes.buffer;\n}\n\n// Convert ArrayBuffer to base64url\nfunction bufferToBase64url(buffer) {\n\tconst bytes = new Uint8Array(buffer);\n\tlet binary = '';\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\tbinary += String.fromCharCode(bytes[i]);\n\t}\n\treturn btoa(binary)\n\t\t.replace(/\\\\+/g, '-')\n\t\t.replace(/\\\\//g, '_')\n\t\t.replace(/=/g, '');\n}\n\nasync function authenticateWithPasskey() {\n\tconst btn = document.getElementById('passkey-btn');\n\tconst statusEl = document.querySelector('.passkey-status') || (() => {\n\t\tconst el = document.createElement('div');\n\t\tel.className = 'passkey-status';\n\t\tbtn.parentNode.insertBefore(el, btn.nextSibling);\n\t\treturn el;\n\t})();\n\n\tbtn.disabled = true;\n\tbtn.innerHTML = '<span class=\"passkey-icon\">🔐</span> Authenticating...';\n\tstatusEl.textContent = '';\n\tstatusEl.className = 'passkey-status';\n\n\ttry {\n\t\t// Convert options for WebAuthn API\n\t\tconst publicKeyOptions = {\n\t\t\tchallenge: base64urlToBuffer(passkeyOptions.challenge),\n\t\t\ttimeout: passkeyOptions.timeout,\n\t\t\trpId: passkeyOptions.rpId,\n\t\t\tuserVerification: passkeyOptions.userVerification,\n\t\t\tallowCredentials: (passkeyOptions.allowCredentials || []).map(cred => ({\n\t\t\t\tid: base64urlToBuffer(cred.id),\n\t\t\t\ttype: cred.type,\n\t\t\t\ttransports: cred.transports,\n\t\t\t})),\n\t\t};\n\n\t\t// Perform WebAuthn ceremony\n\t\t// mediation: \"optional\" ensures modal UI appears for cross-device auth\n\t\tconst credential = await navigator.credentials.get({\n\t\t\tpublicKey: publicKeyOptions,\n\t\t\tmediation: \"optional\"\n\t\t});\n\n\t\tif (!credential) {\n\t\t\tthrow new Error('No credential returned');\n\t\t}\n\n\t\t// Prepare response for server\n\t\tconst response = {\n\t\t\tid: credential.id,\n\t\t\trawId: bufferToBase64url(credential.rawId),\n\t\t\tresponse: {\n\t\t\t\tclientDataJSON: bufferToBase64url(credential.response.clientDataJSON),\n\t\t\t\tauthenticatorData: bufferToBase64url(credential.response.authenticatorData),\n\t\t\t\tsignature: bufferToBase64url(credential.response.signature),\n\t\t\t\tuserHandle: credential.response.userHandle ? bufferToBase64url(credential.response.userHandle) : undefined,\n\t\t\t},\n\t\t\ttype: credential.type,\n\t\t\tclientExtensionResults: credential.getClientExtensionResults(),\n\t\t\tauthenticatorAttachment: credential.authenticatorAttachment,\n\t\t};\n\n\t\t// Submit to server\n\t\tconst result = await fetch('/oauth/passkey-auth', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tresponse,\n\t\t\t\tchallenge: passkeyOptions.challenge,\n\t\t\t\toauthParams,\n\t\t\t}),\n\t\t});\n\n\t\tconst data = await result.json();\n\n\t\tif (data.redirectUrl) {\n\t\t\t// Success - redirect to complete authorization\n\t\t\twindow.location.href = data.redirectUrl;\n\t\t} else {\n\t\t\tthrow new Error(data.error || 'Authentication failed');\n\t\t}\n\t} catch (err) {\n\t\tconsole.error('Passkey auth error:', err);\n\t\tstatusEl.textContent = err.name === 'NotAllowedError' ? 'Authentication cancelled' : (err.message || 'Authentication failed');\n\t\tstatusEl.className = 'passkey-status error';\n\t\tbtn.disabled = false;\n\t\tbtn.innerHTML = '<span class=\"passkey-icon\">🔐</span> Sign in with Passkey';\n\t}\n}\n\nconst passkeyBtn = document.getElementById('passkey-btn');\nif (passkeyBtn) {\n\tpasskeyBtn.addEventListener('click', authenticateWithPasskey);\n}\n`;\n\n/**\n * Compute SHA-256 hash for CSP script-src\n */\nasync function computeScriptHash(script: string): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst data = encoder.encode(script);\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n\tconst hashArray = Array.from(new Uint8Array(hashBuffer));\n\tconst base64Hash = btoa(String.fromCharCode(...hashArray));\n\treturn `'sha256-${base64Hash}'`;\n}\n\n// Pre-computed hash (computed at module load, will be a Promise)\nlet passkeyAuthScriptHashPromise: Promise<string> | null = null;\n\n/**\n * Get the script hash for the passkey auth script\n */\nexport async function getPasskeyAuthScriptHash(): Promise<string> {\n\tif (!passkeyAuthScriptHashPromise) {\n\t\tpasskeyAuthScriptHashPromise = computeScriptHash(PASSKEY_AUTH_SCRIPT);\n\t}\n\treturn passkeyAuthScriptHashPromise;\n}\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 async function getConsentUiCsp(includePasskeyScript: boolean): Promise<string> {\n\tconst scriptSrc = includePasskeyScript\n\t\t? await getPasskeyAuthScriptHash()\n\t\t: \"'none'\";\n\treturn `default-src 'none'; script-src ${scriptSrc}; style-src 'unsafe-inline'; img-src https: data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'`;\n}\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/**\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\t/** Whether passkey login is available */\n\tpasskeyAvailable?: boolean;\n\t/** WebAuthn authentication options for passkey login */\n\tpasskeyOptions?: Record<string, unknown>;\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, passkeyAvailable, passkeyOptions } = 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${passkeyAvailable ? `\n\t\t\t\t<button type=\"button\" class=\"btn-passkey\" id=\"passkey-btn\">\n\t\t\t\t\t<span class=\"passkey-icon\">🔐</span>\n\t\t\t\t\tSign in with Passkey\n\t\t\t\t</button>\n\t\t\t\t<div class=\"or-divider\"><span>or</span></div>\n\t\t\t\t` : \"\"}\n\t\t\t\t<input type=\"password\" name=\"password\" placeholder=\"Password\" autocomplete=\"current-password\" required />\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\n\t\t.btn-passkey {\n\t\t\twidth: 100%;\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: 1px solid rgba(255, 255, 255, 0.2);\n\t\t\tbackground: rgba(255, 255, 255, 0.05);\n\t\t\tcolor: #e0e0e0;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tgap: 8px;\n\t\t}\n\n\t\t.btn-passkey:hover:not(:disabled) {\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t\tborder-color: #3b82f6;\n\t\t}\n\n\t\t.btn-passkey:disabled {\n\t\t\topacity: 0.5;\n\t\t\tcursor: not-allowed;\n\t\t}\n\n\t\t.passkey-icon {\n\t\t\tfont-size: 16px;\n\t\t}\n\n\t\t.or-divider {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tmargin: 16px 0;\n\t\t\tcolor: #6b7280;\n\t\t\tfont-size: 12px;\n\t\t}\n\n\t\t.or-divider::before,\n\t\t.or-divider::after {\n\t\t\tcontent: \"\";\n\t\t\tflex: 1;\n\t\t\theight: 1px;\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t}\n\n\t\t.or-divider span {\n\t\t\tpadding: 0 12px;\n\t\t}\n\n\t\t.passkey-status {\n\t\t\tmargin-top: 8px;\n\t\t\tfont-size: 12px;\n\t\t\ttext-align: center;\n\t\t\tmin-height: 16px;\n\t\t}\n\n\t\t.passkey-status.error {\n\t\t\tcolor: #f87171;\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\t${passkeyAvailable && passkeyOptions ? `\n\t<script data-passkey-options=\"${escapeHtml(JSON.stringify(passkeyOptions))}\" data-oauth-params=\"${escapeHtml(JSON.stringify(oauthParams))}\">${PASSKEY_AUTH_SCRIPT}</script>\n\t` : \"\"}\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, customFetch } 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/** Issuer URL (also accepted as audience per RFC 7523) */\n\tissuer: 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, issuer, 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[customFetch]: 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 or the issuer\n\t// Per RFC 7523, audience identifies the authorization server - both formats are valid\n\tconst aud = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];\n\tif (!aud.includes(tokenEndpoint) && !aud.includes(issuer)) {\n\t\tthrow new ClientAuthError(\n\t\t\t`JWT audience must include token endpoint (${tokenEndpoint}) or issuer (${issuer})`,\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, getConsentUiCsp } 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\t/** Get passkey authentication options (returns null if no passkeys are registered) */\n\tgetPasskeyOptions?: () => Promise<Record<string, unknown> | null>;\n\t/** Verify passkey authentication */\n\tverifyPasskey?: (response: unknown, challenge: string) => 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\tprivate getPasskeyOptions?: () => Promise<Record<string, unknown> | null>;\n\tprivate verifyPasskey?: (response: unknown, challenge: string) => 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\tthis.getPasskeyOptions = config.getPasskeyOptions;\n\t\tthis.verifyPasskey = config.verifyPasskey;\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 await 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 await 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 if (this.enablePAR) {\n\t\t\t\t// PAR is required when enabled - reject direct authorization requests\n\t\t\t\treturn await this.renderError(\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t\t\"Pushed Authorization Request required. Use the PAR endpoint first.\"\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\t// Parse query parameters (only when PAR is not enabled)\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 await 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 await 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 await 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 await 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 await 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// Get passkey options if user needs to log in\n\t\tlet passkeyOptions: Record<string, unknown> | null = null;\n\t\tif (!user && this.getPasskeyOptions) {\n\t\t\tpasskeyOptions = await this.getPasskeyOptions();\n\t\t}\n\n\t\tconst passkeyAvailable = !user && !!passkeyOptions;\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\tpasskeyAvailable,\n\t\t\tpasskeyOptions: passkeyOptions ?? undefined,\n\t\t});\n\n\t\tconst csp = await getConsentUiCsp(passkeyAvailable);\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\": 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\tconst csp = await getConsentUiCsp(false);\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\": 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\tissuer: this.issuer,\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\tissuer: this.issuer,\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: true,\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 * Handle passkey authentication (POST /oauth/passkey-auth)\n\t *\n\t * This endpoint is called by the client-side JavaScript after a successful\n\t * WebAuthn authentication. It verifies the passkey and returns a redirect URL\n\t * to complete the OAuth authorization flow.\n\t */\n\tasync handlePasskeyAuth(request: Request): Promise<Response> {\n\t\tif (!this.verifyPasskey) {\n\t\t\treturn oauthError(\"unsupported_auth_method\", \"Passkey authentication is not configured\", 400);\n\t\t}\n\n\t\tlet body: {\n\t\t\tresponse: unknown;\n\t\t\tchallenge: string;\n\t\t\toauthParams: Record<string, string>;\n\t\t};\n\n\t\ttry {\n\t\t\tbody = await request.json();\n\t\t} catch {\n\t\t\treturn oauthError(\"invalid_request\", \"Invalid JSON body\", 400);\n\t\t}\n\n\t\tconst { response, challenge, oauthParams } = body;\n\n\t\tif (!response || !challenge || !oauthParams) {\n\t\t\treturn oauthError(\"invalid_request\", \"Missing required parameters\", 400);\n\t\t}\n\n\t\t// Verify the passkey\n\t\tconst user = await this.verifyPasskey(response, challenge);\n\t\tif (!user) {\n\t\t\treturn new Response(JSON.stringify({ error: \"Authentication failed\" }), {\n\t\t\t\tstatus: 401,\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t});\n\t\t}\n\n\t\t// Validate OAuth params\n\t\tconst required = [\"client_id\", \"redirect_uri\", \"state\", \"code_challenge\"];\n\t\tfor (const param of required) {\n\t\t\tif (!oauthParams[param]) {\n\t\t\t\treturn new Response(JSON.stringify({ error: `Missing OAuth parameter: ${param}` }), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Resolve client and validate redirect_uri\n\t\tlet client: ClientMetadata;\n\t\ttry {\n\t\t\tclient = await this.clientResolver.resolveClient(oauthParams.client_id!);\n\t\t} catch (e) {\n\t\t\treturn new Response(JSON.stringify({ error: `Invalid client: ${e}` }), {\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t});\n\t\t}\n\n\t\tif (!client.redirectUris.includes(oauthParams.redirect_uri!)) {\n\t\t\treturn new Response(JSON.stringify({ error: \"Invalid redirect_uri for this client\" }), {\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t});\n\t\t}\n\n\t\t// Generate authorization code\n\t\tconst code = generateAuthCode();\n\t\tconst scope = oauthParams.scope ?? \"atproto\";\n\n\t\tconst authCodeData: AuthCodeData = {\n\t\t\tclientId: oauthParams.client_id!,\n\t\t\tredirectUri: oauthParams.redirect_uri!,\n\t\t\tcodeChallenge: oauthParams.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// Build redirect URL\n\t\tconst responseMode = oauthParams.response_mode ?? \"query\";\n\t\tconst redirectUrl = new URL(oauthParams.redirect_uri!);\n\n\t\tif (responseMode === \"fragment\") {\n\t\t\tconst hashParams = new URLSearchParams();\n\t\t\thashParams.set(\"code\", code);\n\t\t\thashParams.set(\"state\", oauthParams.state!);\n\t\t\thashParams.set(\"iss\", this.issuer);\n\t\t\tredirectUrl.hash = hashParams.toString();\n\t\t} else {\n\t\t\tredirectUrl.searchParams.set(\"code\", code);\n\t\t\tredirectUrl.searchParams.set(\"state\", oauthParams.state!);\n\t\t\tredirectUrl.searchParams.set(\"iss\", this.issuer);\n\t\t}\n\n\t\treturn new Response(JSON.stringify({ redirectUrl: redirectUrl.toString() }), {\n\t\t\tstatus: 200,\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t});\n\t}\n\n\t/**\n\t * Render an error page\n\t */\n\tprivate async renderError(error: string, description: string): Promise<Response> {\n\t\tconst html = renderErrorPage(error, description);\n\t\tconst csp = await getConsentUiCsp(false);\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\": 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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"}
|
|
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","passkeyAuthScriptHashPromise: Promise<string> | null","descriptions: string[]","code: string","keyResolver: Parameters<typeof jwtVerify>[1]","jwks: { keys: Record<string, unknown>[] } | undefined","payload: JWTPayload","params: Record<string, string>","client: ClientMetadata","user: { sub: string; handle: string } | null","passkeyOptions: Record<string, unknown> | null","authCodeData: AuthCodeData","dpopJkt: string | undefined","metadata: OAuthAuthorizationServerMetadata","body: {\n\t\t\tresponse: unknown;\n\t\t\tchallenge: string;\n\t\t\toauthParams: Record<string, string>;\n\t\t}"],"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 {\n\tjwtVerify,\n\tEmbeddedJWK,\n\tcalculateJwkThumbprint,\n\terrors,\n\tbase64url,\n} 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(\n\t\t\t'DPoP \"htu\" must not contain credentials',\n\t\t\t\"invalid_dpop\",\n\t\t);\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 {\n\t\tallowedAlgorithms = [\"ES256\"],\n\t\taccessToken,\n\t\texpectedNonce,\n\t\tmaxTokenAge = 60,\n\t} = 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(\n\t\t\t\t`DPoP verification failed: ${err.message}`,\n\t\t\t\t\"invalid_dpop\",\n\t\t\t\t{ cause: err },\n\t\t\t);\n\t\t}\n\t\tthrow new DpopError(\"DPoP verification failed\", \"invalid_dpop\", {\n\t\t\tcause: err,\n\t\t});\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(\n\t\t\t\t'DPoP \"ath\" missing when access token provided',\n\t\t\t\t\"invalid_dpop\",\n\t\t\t);\n\t\t}\n\n\t\tconst tokenHash = await crypto.subtle.digest(\n\t\t\t\"SHA-256\",\n\t\t\tnew TextEncoder().encode(accessToken),\n\t\t);\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(\n\t\t\t'DPoP \"ath\" claim not allowed without access token',\n\t\t\t\"invalid_dpop\",\n\t\t);\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 = [\n\t\"client_id\",\n\t\"redirect_uri\",\n\t\"response_type\",\n\t\"code_challenge\",\n\t\"code_challenge_method\",\n\t\"state\",\n];\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(\n\t\tstorage: OAuthStorage,\n\t\tissuer: string,\n\t\texpiresIn: number = DEFAULT_EXPIRES_IN,\n\t) {\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(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t\"Missing client_id parameter\",\n\t\t\t\t400,\n\t\t\t);\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(\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t\t`Missing required parameter: ${param}`,\n\t\t\t\t\t400,\n\t\t\t\t);\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 * Check if a client ID is a valid localhost client per AT Protocol spec.\n * Localhost clients use http://localhost with no port, and encode\n * redirect_uri and scope as query parameters.\n * @see https://atproto.com/specs/oauth#clients\n */\nfunction isLocalhostClient(value: string): boolean {\n\ttry {\n\t\tconst url = new URL(value);\n\t\t// Must be http://localhost with no port\n\t\treturn (\n\t\t\turl.protocol === \"http:\" && url.hostname === \"localhost\" && !url.port\n\t\t);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Parse localhost client metadata from the client_id URL.\n * Per AT Protocol spec, redirect_uri and scope are encoded as query params.\n * Defaults to http://127.0.0.1/ and http://[::1]/ for redirect URIs.\n */\nfunction parseLocalhostClientMetadata(clientId: string): ClientMetadata {\n\tconst url = new URL(clientId);\n\n\t// Get redirect_uri from query params, default to loopback addresses\n\tconst redirectUriParam = url.searchParams.get(\"redirect_uri\");\n\tconst redirectUris = redirectUriParam\n\t\t? [redirectUriParam]\n\t\t: [\"http://127.0.0.1/\", \"http://[::1]/\"];\n\n\t// Scope is informational for localhost clients\n\tconst scope = url.searchParams.get(\"scope\") ?? \"atproto\";\n\n\treturn {\n\t\tclientId,\n\t\tclientName: \"Localhost Client\",\n\t\tredirectUris,\n\t\ttokenEndpointAuthMethod: \"none\", // Localhost clients are always public\n\t\tcachedAt: Date.now(),\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, DID, or localhost URL)\n\t * @returns The client metadata\n\t * @throws ClientResolutionError if resolution fails\n\t */\n\tasync resolveClient(clientId: string): Promise<ClientMetadata> {\n\t\t// Handle localhost clients per AT Protocol spec\n\t\t// These don't need network resolution - metadata is in the URL\n\t\tif (isLocalhostClient(clientId)) {\n\t\t\treturn parseLocalhostClientMetadata(clientId);\n\t\t}\n\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\t// Check cache validity: must have timestamp, not expired, and have auth method set\n\t\t\t// (entries without tokenEndpointAuthMethod are from before we added that field)\n\t\t\tif (\n\t\t\t\tcached &&\n\t\t\t\tcached.cachedAt &&\n\t\t\t\tDate.now() - cached.cachedAt < this.cacheTtl &&\n\t\t\t\tcached.tokenEndpointAuthMethod !== undefined\n\t\t\t) {\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:\n\t\t\t\tdoc.token_endpoint_auth_method === \"private_key_jwt\"\n\t\t\t\t\t? \"private_key_jwt\"\n\t\t\t\t\t: \"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 ID (URL or 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(\n\t\tclientId: string,\n\t\tredirectUri: string,\n\t): Promise<boolean> {\n\t\ttry {\n\t\t\tconst metadata = await this.resolveClient(clientId);\n\n\t\t\t// For localhost clients, use relaxed matching per AT Protocol spec:\n\t\t\t// Port numbers are not matched, only scheme, host, and path\n\t\t\tif (isLocalhostClient(clientId)) {\n\t\t\t\treturn this.matchesLocalhostRedirectUri(\n\t\t\t\t\tmetadata.redirectUris,\n\t\t\t\t\tredirectUri,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn metadata.redirectUris.includes(redirectUri);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Check if a redirect URI matches any allowed URI for localhost clients.\n\t * Per AT Protocol spec, port numbers are not matched for localhost.\n\t */\n\tprivate matchesLocalhostRedirectUri(\n\t\tallowedUris: string[],\n\t\tredirectUri: string,\n\t): boolean {\n\t\ttry {\n\t\t\tconst redirect = new URL(redirectUri);\n\n\t\t\tfor (const allowed of allowedUris) {\n\t\t\t\tconst allowedUrl = new URL(allowed);\n\t\t\t\t// Match scheme, hostname, and path - ignore port\n\t\t\t\tif (\n\t\t\t\t\tredirect.protocol === allowedUrl.protocol &&\n\t\t\t\t\tredirect.hostname === allowedUrl.hostname &&\n\t\t\t\t\tredirect.pathname === allowedUrl.pathname\n\t\t\t\t) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\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(\n\toptions: ClientResolverOptions = {},\n): 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\n\t\t? generateRandomToken(32)\n\t\t: 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(\n\ttokens: GeneratedTokens,\n): 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 * The passkey authentication script (static, can be hashed).\n * Dynamic data is passed via data attributes on the script element.\n */\nconst PASSKEY_AUTH_SCRIPT = `\n// Get dynamic data from script element\nconst scriptEl = document.currentScript;\nconst passkeyOptions = JSON.parse(scriptEl.dataset.passkeyOptions);\nconst oauthParams = JSON.parse(scriptEl.dataset.oauthParams);\n\n// Convert base64url to ArrayBuffer\nfunction base64urlToBuffer(base64url) {\n\tconst base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');\n\tconst padding = '='.repeat((4 - base64.length % 4) % 4);\n\tconst binary = atob(base64 + padding);\n\tconst bytes = new Uint8Array(binary.length);\n\tfor (let i = 0; i < binary.length; i++) {\n\t\tbytes[i] = binary.charCodeAt(i);\n\t}\n\treturn bytes.buffer;\n}\n\n// Convert ArrayBuffer to base64url\nfunction bufferToBase64url(buffer) {\n\tconst bytes = new Uint8Array(buffer);\n\tlet binary = '';\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\tbinary += String.fromCharCode(bytes[i]);\n\t}\n\treturn btoa(binary)\n\t\t.replace(/\\\\+/g, '-')\n\t\t.replace(/\\\\//g, '_')\n\t\t.replace(/=/g, '');\n}\n\nasync function authenticateWithPasskey() {\n\tconst btn = document.getElementById('passkey-btn');\n\tconst statusEl = document.querySelector('.passkey-status') || (() => {\n\t\tconst el = document.createElement('div');\n\t\tel.className = 'passkey-status';\n\t\tbtn.parentNode.insertBefore(el, btn.nextSibling);\n\t\treturn el;\n\t})();\n\n\tbtn.disabled = true;\n\tbtn.innerHTML = '<span class=\"passkey-icon\">🔐</span> Authenticating...';\n\tstatusEl.textContent = '';\n\tstatusEl.className = 'passkey-status';\n\n\ttry {\n\t\t// Convert options for WebAuthn API\n\t\tconst publicKeyOptions = {\n\t\t\tchallenge: base64urlToBuffer(passkeyOptions.challenge),\n\t\t\ttimeout: passkeyOptions.timeout,\n\t\t\trpId: passkeyOptions.rpId,\n\t\t\tuserVerification: passkeyOptions.userVerification,\n\t\t\tallowCredentials: (passkeyOptions.allowCredentials || []).map(cred => ({\n\t\t\t\tid: base64urlToBuffer(cred.id),\n\t\t\t\ttype: cred.type,\n\t\t\t\ttransports: cred.transports,\n\t\t\t})),\n\t\t};\n\n\t\t// Perform WebAuthn ceremony\n\t\t// mediation: \"optional\" ensures modal UI appears for cross-device auth\n\t\tconst credential = await navigator.credentials.get({\n\t\t\tpublicKey: publicKeyOptions,\n\t\t\tmediation: \"optional\"\n\t\t});\n\n\t\tif (!credential) {\n\t\t\tthrow new Error('No credential returned');\n\t\t}\n\n\t\t// Prepare response for server\n\t\tconst response = {\n\t\t\tid: credential.id,\n\t\t\trawId: bufferToBase64url(credential.rawId),\n\t\t\tresponse: {\n\t\t\t\tclientDataJSON: bufferToBase64url(credential.response.clientDataJSON),\n\t\t\t\tauthenticatorData: bufferToBase64url(credential.response.authenticatorData),\n\t\t\t\tsignature: bufferToBase64url(credential.response.signature),\n\t\t\t\tuserHandle: credential.response.userHandle ? bufferToBase64url(credential.response.userHandle) : undefined,\n\t\t\t},\n\t\t\ttype: credential.type,\n\t\t\tclientExtensionResults: credential.getClientExtensionResults(),\n\t\t\tauthenticatorAttachment: credential.authenticatorAttachment,\n\t\t};\n\n\t\t// Submit to server\n\t\tconst result = await fetch('/oauth/passkey-auth', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tresponse,\n\t\t\t\tchallenge: passkeyOptions.challenge,\n\t\t\t\toauthParams,\n\t\t\t}),\n\t\t});\n\n\t\tconst data = await result.json();\n\n\t\tif (data.redirectUrl) {\n\t\t\t// Success - redirect to complete authorization\n\t\t\twindow.location.href = data.redirectUrl;\n\t\t} else {\n\t\t\tthrow new Error(data.error || 'Authentication failed');\n\t\t}\n\t} catch (err) {\n\t\tconsole.error('Passkey auth error:', err);\n\t\tstatusEl.textContent = err.name === 'NotAllowedError' ? 'Authentication cancelled' : (err.message || 'Authentication failed');\n\t\tstatusEl.className = 'passkey-status error';\n\t\tbtn.disabled = false;\n\t\tbtn.innerHTML = '<span class=\"passkey-icon\">🔐</span> Sign in with Passkey';\n\t}\n}\n\nconst passkeyBtn = document.getElementById('passkey-btn');\nif (passkeyBtn) {\n\tpasskeyBtn.addEventListener('click', authenticateWithPasskey);\n}\n`;\n\n/**\n * Compute SHA-256 hash for CSP script-src\n */\nasync function computeScriptHash(script: string): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst data = encoder.encode(script);\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n\tconst hashArray = Array.from(new Uint8Array(hashBuffer));\n\tconst base64Hash = btoa(String.fromCharCode(...hashArray));\n\treturn `'sha256-${base64Hash}'`;\n}\n\n// Pre-computed hash (computed at module load, will be a Promise)\nlet passkeyAuthScriptHashPromise: Promise<string> | null = null;\n\n/**\n * Get the script hash for the passkey auth script\n */\nexport async function getPasskeyAuthScriptHash(): Promise<string> {\n\tif (!passkeyAuthScriptHashPromise) {\n\t\tpasskeyAuthScriptHashPromise = computeScriptHash(PASSKEY_AUTH_SCRIPT);\n\t}\n\treturn passkeyAuthScriptHashPromise;\n}\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 async function getConsentUiCsp(\n\tincludePasskeyScript: boolean,\n): Promise<string> {\n\tconst scriptSrc = includePasskeyScript\n\t\t? await getPasskeyAuthScriptHash()\n\t\t: \"'none'\";\n\treturn `default-src 'none'; script-src ${scriptSrc}; style-src 'unsafe-inline'; img-src https: data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'`;\n}\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\t/** Whether passkey login is available */\n\tpasskeyAvailable?: boolean;\n\t/** WebAuthn authentication options for passkey login */\n\tpasskeyOptions?: Record<string, unknown>;\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 {\n\t\tclient,\n\t\tscope,\n\t\tauthorizeUrl,\n\t\toauthParams,\n\t\tuserHandle,\n\t\tshowLogin,\n\t\terror,\n\t\tpasskeyAvailable,\n\t\tpasskeyOptions,\n\t} = 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${\n\t\t\t\t\tpasskeyAvailable\n\t\t\t\t\t\t? `\n\t\t\t\t<button type=\"button\" class=\"btn-passkey\" id=\"passkey-btn\">\n\t\t\t\t\t<span class=\"passkey-icon\">🔐</span>\n\t\t\t\t\tSign in with Passkey\n\t\t\t\t</button>\n\t\t\t\t<div class=\"or-divider\"><span>or</span></div>\n\t\t\t\t`\n\t\t\t\t\t\t: \"\"\n\t\t\t\t}\n\t\t\t\t<input type=\"password\" name=\"password\" placeholder=\"Password\" autocomplete=\"current-password\" required />\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(\n\t\t\t([key, value]) =>\n\t\t\t\t`<input type=\"hidden\" name=\"${escapeHtml(key)}\" value=\"${escapeHtml(value)}\" />`,\n\t\t)\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\n\t\t.btn-passkey {\n\t\t\twidth: 100%;\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: 1px solid rgba(255, 255, 255, 0.2);\n\t\t\tbackground: rgba(255, 255, 255, 0.05);\n\t\t\tcolor: #e0e0e0;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tgap: 8px;\n\t\t}\n\n\t\t.btn-passkey:hover:not(:disabled) {\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t\tborder-color: #3b82f6;\n\t\t}\n\n\t\t.btn-passkey:disabled {\n\t\t\topacity: 0.5;\n\t\t\tcursor: not-allowed;\n\t\t}\n\n\t\t.passkey-icon {\n\t\t\tfont-size: 16px;\n\t\t}\n\n\t\t.or-divider {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tmargin: 16px 0;\n\t\t\tcolor: #6b7280;\n\t\t\tfont-size: 12px;\n\t\t}\n\n\t\t.or-divider::before,\n\t\t.or-divider::after {\n\t\t\tcontent: \"\";\n\t\t\tflex: 1;\n\t\t\theight: 1px;\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t}\n\n\t\t.or-divider span {\n\t\t\tpadding: 0 12px;\n\t\t}\n\n\t\t.passkey-status {\n\t\t\tmargin-top: 8px;\n\t\t\tfont-size: 12px;\n\t\t\ttext-align: center;\n\t\t\tmin-height: 16px;\n\t\t}\n\n\t\t.passkey-status.error {\n\t\t\tcolor: #f87171;\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\t${\n\t\tpasskeyAvailable && passkeyOptions\n\t\t\t? `\n\t<script data-passkey-options=\"${escapeHtml(JSON.stringify(passkeyOptions))}\" data-oauth-params=\"${escapeHtml(JSON.stringify(oauthParams))}\">${PASSKEY_AUTH_SCRIPT}</script>\n\t`\n\t\t\t: \"\"\n\t}\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 {\n\tjwtVerify,\n\tcreateLocalJWKSet,\n\tcreateRemoteJWKSet,\n\terrors,\n\tcustomFetch,\n} from \"jose\";\nimport type { JWTPayload } from \"jose\";\nimport type { ClientMetadata } from \"./storage.js\";\n\nconst { JOSEError } = errors;\n\n/** Expected assertion type for private_key_jwt */\nexport const JWT_BEARER_ASSERTION_TYPE =\n\t\"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/** Issuer URL (also accepted as audience per RFC 7523) */\n\tissuer: 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 {\n\t\ttokenEndpoint,\n\t\tissuer,\n\t\tfetch: fetchFn = globalThis.fetch.bind(globalThis),\n\t\tcheckJti,\n\t} = options;\n\n\t// Get the key resolver\n\tlet keyResolver: Parameters<typeof jwtVerify>[1];\n\n\t// Resolve JWKS from inline keys or remote URI\n\tlet jwks: { keys: Record<string, unknown>[] } | undefined;\n\tif (client.jwks && client.jwks.keys.length > 0) {\n\t\tjwks = client.jwks;\n\t} else if (client.jwksUri) {\n\t\tconst res = await fetchFn(client.jwksUri, {\n\t\t\theaders: { Accept: \"application/json\" },\n\t\t});\n\t\tif (!res.ok) {\n\t\t\tthrow new ClientAuthError(\n\t\t\t\t`Failed to fetch client JWKS: ${res.status}`,\n\t\t\t\t\"invalid_client\",\n\t\t\t);\n\t\t}\n\t\tjwks = await res.json();\n\t}\n\n\tif (!jwks?.keys?.length) {\n\t\tthrow new ClientAuthError(\n\t\t\t\"Client has no JWKS configured\",\n\t\t\t\"invalid_client\",\n\t\t);\n\t}\n\n\t// Strip key_ops before importing — clients in the wild include\n\t// invalid operations like \"encrypt\" on ECDSA signing keys, which\n\t// causes Web Crypto to reject the import. The algorithm is already\n\t// constrained to ES256 by the jwtVerify options below.\n\tkeyResolver = createLocalJWKSet({\n\t\tkeys: jwks.keys.map(({ key_ops, ...rest }) => rest),\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(\n\t\t\t\t`JWT verification failed: ${err.message}`,\n\t\t\t\t\"invalid_client\",\n\t\t\t);\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 or the issuer\n\t// Per RFC 7523, audience identifies the authorization server - both formats are valid\n\tconst aud = Array.isArray(payload.aud)\n\t\t? payload.aud\n\t\t: payload.aud\n\t\t\t? [payload.aud]\n\t\t\t: [];\n\tif (!aud.includes(tokenEndpoint) && !aud.includes(issuer)) {\n\t\tthrow new ClientAuthError(\n\t\t\t`JWT audience must include token endpoint (${tokenEndpoint}) or issuer (${issuer})`,\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(\n\t\t\t\t\"JWT has already been used (replay detected)\",\n\t\t\t\t\"invalid_client\",\n\t\t\t);\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(\n\t\t`Unsupported auth method: ${authMethod}`,\n\t\t\"invalid_client\",\n\t);\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 {\n\tOAuthStorage,\n\tAuthCodeData,\n\tTokenData,\n\tClientMetadata,\n} 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, getConsentUiCsp } 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?: (\n\t\tpassword: string,\n\t) => Promise<{ sub: string; handle: string } | null>;\n\t/** Get the current user (if already authenticated) */\n\tgetCurrentUser?: () => Promise<{ sub: string; handle: string } | null>;\n\t/** Get passkey authentication options (returns null if no passkeys are registered) */\n\tgetPasskeyOptions?: () => Promise<Record<string, unknown> | null>;\n\t/** Verify passkey authentication */\n\tverifyPasskey?: (\n\t\tresponse: unknown,\n\t\tchallenge: string,\n\t) => Promise<{ sub: string; handle: string } | null>;\n}\n\n/**\n * OAuth error response builder\n */\nfunction oauthError(\n\terror: string,\n\tdescription: string,\n\tstatus: number = 400,\n): 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(\n\trequest: Request,\n): 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]) => [\n\t\t\t\t\tk,\n\t\t\t\t\tString(v),\n\t\t\t\t]),\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?: (\n\t\tpassword: string,\n\t) => Promise<{ sub: string; handle: string } | null>;\n\tprivate getCurrentUser?: () => Promise<{\n\t\tsub: string;\n\t\thandle: string;\n\t} | null>;\n\tprivate getPasskeyOptions?: () => Promise<Record<string, unknown> | null>;\n\tprivate verifyPasskey?: (\n\t\tresponse: unknown,\n\t\tchallenge: string,\n\t) => 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 =\n\t\t\tconfig.clientResolver ?? new ClientResolver({ storage: config.storage });\n\t\tthis.verifyUser = config.verifyUser;\n\t\tthis.getCurrentUser = config.getCurrentUser;\n\t\tthis.getPasskeyOptions = config.getPasskeyOptions;\n\t\tthis.verifyPasskey = config.verifyPasskey;\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 await this.renderError(\n\t\t\t\t\t\t\"invalid_request\",\n\t\t\t\t\t\t\"client_id required with request_uri\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst parParams = await this.parHandler.retrieveParams(\n\t\t\t\t\trequestUri,\n\t\t\t\t\tclientId,\n\t\t\t\t);\n\t\t\t\tif (!parParams) {\n\t\t\t\t\treturn await this.renderError(\n\t\t\t\t\t\t\"invalid_request\",\n\t\t\t\t\t\t\"Invalid or expired request_uri\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tparams = parParams;\n\t\t\t} else if (this.enablePAR) {\n\t\t\t\t// PAR is required when enabled - reject direct authorization requests\n\t\t\t\treturn await this.renderError(\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t\t\"Pushed Authorization Request required. Use the PAR endpoint first.\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\t// Parse query parameters (only when PAR is not enabled)\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 = [\n\t\t\t\"client_id\",\n\t\t\t\"redirect_uri\",\n\t\t\t\"response_type\",\n\t\t\t\"code_challenge\",\n\t\t\t\"state\",\n\t\t];\n\t\tfor (const param of required) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn await this.renderError(\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t\t`Missing required parameter: ${param}`,\n\t\t\t\t);\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 await this.renderError(\n\t\t\t\t\"unsupported_response_type\",\n\t\t\t\t\"Only response_type=code is supported\",\n\t\t\t);\n\t\t}\n\n\t\t// Validate code_challenge_method\n\t\tif (\n\t\t\tparams.code_challenge_method &&\n\t\t\tparams.code_challenge_method !== \"S256\"\n\t\t) {\n\t\t\treturn await this.renderError(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t\"Only code_challenge_method=S256 is supported\",\n\t\t\t);\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 await this.renderError(\n\t\t\t\t\"invalid_client\",\n\t\t\t\t`Failed to resolve client: ${e}`,\n\t\t\t);\n\t\t}\n\n\t\t// Validate redirect_uri\n\t\tif (!client.redirectUris.includes(params.redirect_uri!)) {\n\t\t\treturn await this.renderError(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t\"Invalid redirect_uri for this client\",\n\t\t\t);\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// Get passkey options if user needs to log in\n\t\tlet passkeyOptions: Record<string, unknown> | null = null;\n\t\tif (!user && this.getPasskeyOptions) {\n\t\t\tpasskeyOptions = await this.getPasskeyOptions();\n\t\t}\n\n\t\tconst passkeyAvailable = !user && !!passkeyOptions;\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\tpasskeyAvailable,\n\t\t\tpasskeyOptions: passkeyOptions ?? undefined,\n\t\t});\n\n\t\tconst csp = await getConsentUiCsp(passkeyAvailable);\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\": 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(\n\t\t\t\t\t\"error_description\",\n\t\t\t\t\t\"User denied authorization\",\n\t\t\t\t);\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\tconst csp = await getConsentUiCsp(false);\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\": 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(\n\t\t\t\t\"invalid_request\",\n\t\t\t\te instanceof Error ? e.message : \"Invalid request\",\n\t\t\t);\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(\n\t\t\t\t\"unsupported_grant_type\",\n\t\t\t\t`Unsupported grant_type: ${grantType}`,\n\t\t\t);\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(\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t\t`Missing required parameter: ${param}`,\n\t\t\t\t);\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\tissuer: this.issuer,\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(\n\t\t\t\t\"invalid_grant\",\n\t\t\t\t\"Invalid or expired authorization code\",\n\t\t\t);\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(\n\t\t\t\t\t\tdpopProof.jti,\n\t\t\t\t\t);\n\t\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\t\treturn oauthError(\n\t\t\t\t\t\t\t\"invalid_dpop_proof\",\n\t\t\t\t\t\t\t\"DPoP proof replay detected\",\n\t\t\t\t\t\t);\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\tissuer: this.issuer,\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: [\n\t\t\t\t\"atproto\",\n\t\t\t\t\"transition:generic\",\n\t\t\t\t\"transition:chat.bsky\",\n\t\t\t],\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: true,\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 * Handle passkey authentication (POST /oauth/passkey-auth)\n\t *\n\t * This endpoint is called by the client-side JavaScript after a successful\n\t * WebAuthn authentication. It verifies the passkey and returns a redirect URL\n\t * to complete the OAuth authorization flow.\n\t */\n\tasync handlePasskeyAuth(request: Request): Promise<Response> {\n\t\tif (!this.verifyPasskey) {\n\t\t\treturn oauthError(\n\t\t\t\t\"unsupported_auth_method\",\n\t\t\t\t\"Passkey authentication is not configured\",\n\t\t\t\t400,\n\t\t\t);\n\t\t}\n\n\t\tlet body: {\n\t\t\tresponse: unknown;\n\t\t\tchallenge: string;\n\t\t\toauthParams: Record<string, string>;\n\t\t};\n\n\t\ttry {\n\t\t\tbody = await request.json();\n\t\t} catch {\n\t\t\treturn oauthError(\"invalid_request\", \"Invalid JSON body\", 400);\n\t\t}\n\n\t\tconst { response, challenge, oauthParams } = body;\n\n\t\tif (!response || !challenge || !oauthParams) {\n\t\t\treturn oauthError(\"invalid_request\", \"Missing required parameters\", 400);\n\t\t}\n\n\t\t// Verify the passkey\n\t\tconst user = await this.verifyPasskey(response, challenge);\n\t\tif (!user) {\n\t\t\treturn new Response(JSON.stringify({ error: \"Authentication failed\" }), {\n\t\t\t\tstatus: 401,\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t});\n\t\t}\n\n\t\t// Validate OAuth params\n\t\tconst required = [\"client_id\", \"redirect_uri\", \"state\", \"code_challenge\"];\n\t\tfor (const param of required) {\n\t\t\tif (!oauthParams[param]) {\n\t\t\t\treturn new Response(\n\t\t\t\t\tJSON.stringify({ error: `Missing OAuth parameter: ${param}` }),\n\t\t\t\t\t{\n\t\t\t\t\t\tstatus: 400,\n\t\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Resolve client and validate redirect_uri\n\t\tlet client: ClientMetadata;\n\t\ttry {\n\t\t\tclient = await this.clientResolver.resolveClient(oauthParams.client_id!);\n\t\t} catch (e) {\n\t\t\treturn new Response(JSON.stringify({ error: `Invalid client: ${e}` }), {\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t});\n\t\t}\n\n\t\tif (!client.redirectUris.includes(oauthParams.redirect_uri!)) {\n\t\t\treturn new Response(\n\t\t\t\tJSON.stringify({ error: \"Invalid redirect_uri for this client\" }),\n\t\t\t\t{\n\t\t\t\t\tstatus: 400,\n\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\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 = oauthParams.scope ?? \"atproto\";\n\n\t\tconst authCodeData: AuthCodeData = {\n\t\t\tclientId: oauthParams.client_id!,\n\t\t\tredirectUri: oauthParams.redirect_uri!,\n\t\t\tcodeChallenge: oauthParams.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// Build redirect URL\n\t\tconst responseMode = oauthParams.response_mode ?? \"query\";\n\t\tconst redirectUrl = new URL(oauthParams.redirect_uri!);\n\n\t\tif (responseMode === \"fragment\") {\n\t\t\tconst hashParams = new URLSearchParams();\n\t\t\thashParams.set(\"code\", code);\n\t\t\thashParams.set(\"state\", oauthParams.state!);\n\t\t\thashParams.set(\"iss\", this.issuer);\n\t\t\tredirectUrl.hash = hashParams.toString();\n\t\t} else {\n\t\t\tredirectUrl.searchParams.set(\"code\", code);\n\t\t\tredirectUrl.searchParams.set(\"state\", oauthParams.state!);\n\t\t\tredirectUrl.searchParams.set(\"iss\", this.issuer);\n\t\t}\n\n\t\treturn new Response(\n\t\t\tJSON.stringify({ redirectUrl: redirectUrl.toString() }),\n\t\t\t{\n\t\t\t\tstatus: 200,\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t},\n\t\t);\n\t}\n\n\t/**\n\t * Render an error page\n\t */\n\tprivate async renderError(\n\t\terror: string,\n\t\tdescription: string,\n\t): Promise<Response> {\n\t\tconst html = renderErrorPage(error, description);\n\t\tconst csp = await getConsentUiCsp(false);\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\": 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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getcirrus/oauth-provider",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "OAuth 2.1 Provider with AT Protocol extensions for Cloudflare Workers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"publint": "^0.3.16",
|
|
25
25
|
"tsdown": "^0.18.3",
|
|
26
26
|
"typescript": "^5.9.3",
|
|
27
|
-
"vitest": "
|
|
27
|
+
"vitest": "4.1.0-beta.1"
|
|
28
28
|
},
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|