@getcirrus/oauth-provider 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +27 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +318 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -286,6 +286,13 @@ interface OAuthProviderConfig {
|
|
|
286
286
|
sub: string;
|
|
287
287
|
handle: string;
|
|
288
288
|
} | null>;
|
|
289
|
+
/** Get passkey authentication options (returns null if no passkeys are registered) */
|
|
290
|
+
getPasskeyOptions?: () => Promise<Record<string, unknown> | null>;
|
|
291
|
+
/** Verify passkey authentication */
|
|
292
|
+
verifyPasskey?: (response: unknown, challenge: string) => Promise<{
|
|
293
|
+
sub: string;
|
|
294
|
+
handle: string;
|
|
295
|
+
} | null>;
|
|
289
296
|
}
|
|
290
297
|
/**
|
|
291
298
|
* Error thrown when request body parsing fails
|
|
@@ -310,6 +317,8 @@ declare class ATProtoOAuthProvider {
|
|
|
310
317
|
private clientResolver;
|
|
311
318
|
private verifyUser?;
|
|
312
319
|
private getCurrentUser?;
|
|
320
|
+
private getPasskeyOptions?;
|
|
321
|
+
private verifyPasskey?;
|
|
313
322
|
constructor(config: OAuthProviderConfig);
|
|
314
323
|
/**
|
|
315
324
|
* Handle authorization request (GET/POST /oauth/authorize)
|
|
@@ -346,6 +355,14 @@ declare class ATProtoOAuthProvider {
|
|
|
346
355
|
* @returns Token data if valid
|
|
347
356
|
*/
|
|
348
357
|
verifyAccessToken(request: Request, requiredScope?: string): Promise<TokenData | null>;
|
|
358
|
+
/**
|
|
359
|
+
* Handle passkey authentication (POST /oauth/passkey-auth)
|
|
360
|
+
*
|
|
361
|
+
* This endpoint is called by the client-side JavaScript after a successful
|
|
362
|
+
* WebAuthn authentication. It verifies the passkey and returns a redirect URL
|
|
363
|
+
* to complete the OAuth authorization flow.
|
|
364
|
+
*/
|
|
365
|
+
handlePasskeyAuth(request: Request): Promise<Response>;
|
|
349
366
|
/**
|
|
350
367
|
* Render an error page
|
|
351
368
|
*/
|
|
@@ -563,6 +580,10 @@ declare function extractAccessToken(request: Request): {
|
|
|
563
580
|
declare function isTokenValid(tokenData: TokenData): boolean;
|
|
564
581
|
//#endregion
|
|
565
582
|
//#region src/ui.d.ts
|
|
583
|
+
/**
|
|
584
|
+
* Get the script hash for the passkey auth script
|
|
585
|
+
*/
|
|
586
|
+
declare function getPasskeyAuthScriptHash(): Promise<string>;
|
|
566
587
|
/**
|
|
567
588
|
* Content Security Policy for the consent UI
|
|
568
589
|
*
|
|
@@ -579,7 +600,7 @@ declare function isTokenValid(tokenData: TokenData): boolean;
|
|
|
579
600
|
* use form-action without breaking the flow in Chrome.
|
|
580
601
|
* See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/form-action
|
|
581
602
|
*/
|
|
582
|
-
declare
|
|
603
|
+
declare function getConsentUiCsp(includePasskeyScript: boolean): Promise<string>;
|
|
583
604
|
/**
|
|
584
605
|
* Options for rendering the consent UI
|
|
585
606
|
*/
|
|
@@ -600,6 +621,10 @@ interface ConsentUIOptions {
|
|
|
600
621
|
showLogin?: boolean;
|
|
601
622
|
/** Error message to display */
|
|
602
623
|
error?: string;
|
|
624
|
+
/** Whether passkey login is available */
|
|
625
|
+
passkeyAvailable?: boolean;
|
|
626
|
+
/** WebAuthn authentication options for passkey login */
|
|
627
|
+
passkeyOptions?: Record<string, unknown>;
|
|
603
628
|
}
|
|
604
629
|
/**
|
|
605
630
|
* Render the consent UI HTML
|
|
@@ -674,5 +699,5 @@ declare function verifyClientAssertion(assertion: string, client: ClientMetadata
|
|
|
674
699
|
*/
|
|
675
700
|
declare function authenticateClient(params: Record<string, string>, getClient: (clientId: string) => Promise<ClientMetadata | null>, options: ClientAuthOptions): Promise<ClientAuthResult>;
|
|
676
701
|
//#endregion
|
|
677
|
-
export { ACCESS_TOKEN_TTL, ATProtoOAuthProvider, AUTH_CODE_TTL, type AuthCodeData,
|
|
702
|
+
export { ACCESS_TOKEN_TTL, ATProtoOAuthProvider, AUTH_CODE_TTL, type AuthCodeData, ClientAuthError, type ClientAuthOptions, type ClientAuthResult, type ClientMetadata, ClientResolutionError, ClientResolver, type ClientResolverOptions, type ConsentUIOptions, DpopError, type DpopProof, type DpopVerifyOptions, type GenerateTokensOptions, type GeneratedTokens, InMemoryOAuthStorage, type JWK, JWT_BEARER_ASSERTION_TYPE, type OAuthClientMetadata, type OAuthErrorResponse, type OAuthParResponse, type OAuthProviderConfig, type OAuthStorage, type PARData, PARHandler, REFRESH_TOKEN_TTL, RequestBodyError, type TokenData, authenticateClient, buildTokenResponse, createClientResolver, extractAccessToken, generateAuthCode, generateDpopNonce, generateRandomToken, generateTokens, getConsentUiCsp, getPasskeyAuthScriptHash, isTokenValid, parseClientAssertion, parseRequestBody, refreshTokens, renderConsentUI, renderErrorPage, verifyClientAssertion, verifyDpopProof, verifyPkceChallenge };
|
|
678
703
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/storage.ts","../src/client-resolver.ts","../src/provider.ts","../src/pkce.ts","../src/dpop.ts","../src/par.ts","../src/tokens.ts","../src/ui.ts","../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;EA6DzB;EAKS,QAAA,EAAA,MAAA;EAY0B;EAAR,UAAA,EAAA,MAAA;EA2F2B;EAAO,YAAA,EAAA,MAAA,EAAA;EAa1D;;;;EC1LC;EAEP,uBAAA,CAAA,EAAA,MAAA,GAAA,iBAAA;EAQQ;EAEkB,IAAA,CAAA,EAAA;IAEZ,IAAA,EF4CR,GE5CQ,EAAA;
|
|
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
|
@@ -532,6 +532,147 @@ function isTokenValid(tokenData) {
|
|
|
532
532
|
//#endregion
|
|
533
533
|
//#region src/ui.ts
|
|
534
534
|
/**
|
|
535
|
+
* The passkey authentication script (static, can be hashed).
|
|
536
|
+
* Dynamic data is passed via data attributes on the script element.
|
|
537
|
+
*/
|
|
538
|
+
const PASSKEY_AUTH_SCRIPT = `
|
|
539
|
+
// Get dynamic data from script element
|
|
540
|
+
const scriptEl = document.currentScript;
|
|
541
|
+
const passkeyOptions = JSON.parse(scriptEl.dataset.passkeyOptions);
|
|
542
|
+
const oauthParams = JSON.parse(scriptEl.dataset.oauthParams);
|
|
543
|
+
|
|
544
|
+
// Convert base64url to ArrayBuffer
|
|
545
|
+
function base64urlToBuffer(base64url) {
|
|
546
|
+
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
547
|
+
const padding = '='.repeat((4 - base64.length % 4) % 4);
|
|
548
|
+
const binary = atob(base64 + padding);
|
|
549
|
+
const bytes = new Uint8Array(binary.length);
|
|
550
|
+
for (let i = 0; i < binary.length; i++) {
|
|
551
|
+
bytes[i] = binary.charCodeAt(i);
|
|
552
|
+
}
|
|
553
|
+
return bytes.buffer;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Convert ArrayBuffer to base64url
|
|
557
|
+
function bufferToBase64url(buffer) {
|
|
558
|
+
const bytes = new Uint8Array(buffer);
|
|
559
|
+
let binary = '';
|
|
560
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
561
|
+
binary += String.fromCharCode(bytes[i]);
|
|
562
|
+
}
|
|
563
|
+
return btoa(binary)
|
|
564
|
+
.replace(/\\+/g, '-')
|
|
565
|
+
.replace(/\\//g, '_')
|
|
566
|
+
.replace(/=/g, '');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function authenticateWithPasskey() {
|
|
570
|
+
const btn = document.getElementById('passkey-btn');
|
|
571
|
+
const statusEl = document.querySelector('.passkey-status') || (() => {
|
|
572
|
+
const el = document.createElement('div');
|
|
573
|
+
el.className = 'passkey-status';
|
|
574
|
+
btn.parentNode.insertBefore(el, btn.nextSibling);
|
|
575
|
+
return el;
|
|
576
|
+
})();
|
|
577
|
+
|
|
578
|
+
btn.disabled = true;
|
|
579
|
+
btn.innerHTML = '<span class="passkey-icon">🔐</span> Authenticating...';
|
|
580
|
+
statusEl.textContent = '';
|
|
581
|
+
statusEl.className = 'passkey-status';
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
// Convert options for WebAuthn API
|
|
585
|
+
const publicKeyOptions = {
|
|
586
|
+
challenge: base64urlToBuffer(passkeyOptions.challenge),
|
|
587
|
+
timeout: passkeyOptions.timeout,
|
|
588
|
+
rpId: passkeyOptions.rpId,
|
|
589
|
+
userVerification: passkeyOptions.userVerification,
|
|
590
|
+
allowCredentials: (passkeyOptions.allowCredentials || []).map(cred => ({
|
|
591
|
+
id: base64urlToBuffer(cred.id),
|
|
592
|
+
type: cred.type,
|
|
593
|
+
transports: cred.transports,
|
|
594
|
+
})),
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// Perform WebAuthn ceremony
|
|
598
|
+
// mediation: "optional" ensures modal UI appears for cross-device auth
|
|
599
|
+
const credential = await navigator.credentials.get({
|
|
600
|
+
publicKey: publicKeyOptions,
|
|
601
|
+
mediation: "optional"
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
if (!credential) {
|
|
605
|
+
throw new Error('No credential returned');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Prepare response for server
|
|
609
|
+
const response = {
|
|
610
|
+
id: credential.id,
|
|
611
|
+
rawId: bufferToBase64url(credential.rawId),
|
|
612
|
+
response: {
|
|
613
|
+
clientDataJSON: bufferToBase64url(credential.response.clientDataJSON),
|
|
614
|
+
authenticatorData: bufferToBase64url(credential.response.authenticatorData),
|
|
615
|
+
signature: bufferToBase64url(credential.response.signature),
|
|
616
|
+
userHandle: credential.response.userHandle ? bufferToBase64url(credential.response.userHandle) : undefined,
|
|
617
|
+
},
|
|
618
|
+
type: credential.type,
|
|
619
|
+
clientExtensionResults: credential.getClientExtensionResults(),
|
|
620
|
+
authenticatorAttachment: credential.authenticatorAttachment,
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// Submit to server
|
|
624
|
+
const result = await fetch('/oauth/passkey-auth', {
|
|
625
|
+
method: 'POST',
|
|
626
|
+
headers: {
|
|
627
|
+
'Content-Type': 'application/json',
|
|
628
|
+
},
|
|
629
|
+
body: JSON.stringify({
|
|
630
|
+
response,
|
|
631
|
+
challenge: passkeyOptions.challenge,
|
|
632
|
+
oauthParams,
|
|
633
|
+
}),
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const data = await result.json();
|
|
637
|
+
|
|
638
|
+
if (data.redirectUrl) {
|
|
639
|
+
// Success - redirect to complete authorization
|
|
640
|
+
window.location.href = data.redirectUrl;
|
|
641
|
+
} else {
|
|
642
|
+
throw new Error(data.error || 'Authentication failed');
|
|
643
|
+
}
|
|
644
|
+
} catch (err) {
|
|
645
|
+
console.error('Passkey auth error:', err);
|
|
646
|
+
statusEl.textContent = err.name === 'NotAllowedError' ? 'Authentication cancelled' : (err.message || 'Authentication failed');
|
|
647
|
+
statusEl.className = 'passkey-status error';
|
|
648
|
+
btn.disabled = false;
|
|
649
|
+
btn.innerHTML = '<span class="passkey-icon">🔐</span> Sign in with Passkey';
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const passkeyBtn = document.getElementById('passkey-btn');
|
|
654
|
+
if (passkeyBtn) {
|
|
655
|
+
passkeyBtn.addEventListener('click', authenticateWithPasskey);
|
|
656
|
+
}
|
|
657
|
+
`;
|
|
658
|
+
/**
|
|
659
|
+
* Compute SHA-256 hash for CSP script-src
|
|
660
|
+
*/
|
|
661
|
+
async function computeScriptHash(script) {
|
|
662
|
+
const data = new TextEncoder().encode(script);
|
|
663
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
664
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
665
|
+
return `'sha256-${btoa(String.fromCharCode(...hashArray))}'`;
|
|
666
|
+
}
|
|
667
|
+
let passkeyAuthScriptHashPromise = null;
|
|
668
|
+
/**
|
|
669
|
+
* Get the script hash for the passkey auth script
|
|
670
|
+
*/
|
|
671
|
+
async function getPasskeyAuthScriptHash() {
|
|
672
|
+
if (!passkeyAuthScriptHashPromise) passkeyAuthScriptHashPromise = computeScriptHash(PASSKEY_AUTH_SCRIPT);
|
|
673
|
+
return passkeyAuthScriptHashPromise;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
535
676
|
* Content Security Policy for the consent UI
|
|
536
677
|
*
|
|
537
678
|
* - default-src 'none': Deny all by default
|
|
@@ -547,7 +688,9 @@ function isTokenValid(tokenData) {
|
|
|
547
688
|
* use form-action without breaking the flow in Chrome.
|
|
548
689
|
* See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/form-action
|
|
549
690
|
*/
|
|
550
|
-
|
|
691
|
+
async function getConsentUiCsp(includePasskeyScript) {
|
|
692
|
+
return `default-src 'none'; script-src ${includePasskeyScript ? await getPasskeyAuthScriptHash() : "'none'"}; style-src 'unsafe-inline'; img-src https: data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'`;
|
|
693
|
+
}
|
|
551
694
|
/**
|
|
552
695
|
* Escape HTML to prevent XSS
|
|
553
696
|
*/
|
|
@@ -581,7 +724,7 @@ function getScopeDescriptions(scope) {
|
|
|
581
724
|
* @returns HTML string
|
|
582
725
|
*/
|
|
583
726
|
function renderConsentUI(options) {
|
|
584
|
-
const { client, scope, authorizeUrl, oauthParams, userHandle, showLogin, error } = options;
|
|
727
|
+
const { client, scope, authorizeUrl, oauthParams, userHandle, showLogin, error, passkeyAvailable, passkeyOptions } = options;
|
|
585
728
|
const clientName = escapeHtml(client.clientName);
|
|
586
729
|
const scopeDescriptions = getScopeDescriptions(scope);
|
|
587
730
|
const logoHtml = client.logoUri ? `<img src="${escapeHtml(client.logoUri)}" alt="${clientName} logo" class="app-logo" />` : `<div class="app-logo-placeholder">${clientName.charAt(0).toUpperCase()}</div>`;
|
|
@@ -589,7 +732,14 @@ function renderConsentUI(options) {
|
|
|
589
732
|
const loginFormHtml = showLogin ? `
|
|
590
733
|
<div class="login-form">
|
|
591
734
|
<p>Sign in to continue</p>
|
|
592
|
-
|
|
735
|
+
${passkeyAvailable ? `
|
|
736
|
+
<button type="button" class="btn-passkey" id="passkey-btn">
|
|
737
|
+
<span class="passkey-icon">🔐</span>
|
|
738
|
+
Sign in with Passkey
|
|
739
|
+
</button>
|
|
740
|
+
<div class="or-divider"><span>or</span></div>
|
|
741
|
+
` : ""}
|
|
742
|
+
<input type="password" name="password" placeholder="Password" autocomplete="current-password" required />
|
|
593
743
|
</div>
|
|
594
744
|
` : "";
|
|
595
745
|
const hiddenFieldsHtml = Object.entries(oauthParams).map(([key, value]) => `<input type="hidden" name="${escapeHtml(key)}" value="${escapeHtml(value)}" />`).join("\n ");
|
|
@@ -798,6 +948,68 @@ function renderConsentUI(options) {
|
|
|
798
948
|
.client-uri a:hover {
|
|
799
949
|
text-decoration: underline;
|
|
800
950
|
}
|
|
951
|
+
|
|
952
|
+
.btn-passkey {
|
|
953
|
+
width: 100%;
|
|
954
|
+
padding: 12px 20px;
|
|
955
|
+
border-radius: 8px;
|
|
956
|
+
font-size: 14px;
|
|
957
|
+
font-weight: 500;
|
|
958
|
+
cursor: pointer;
|
|
959
|
+
transition: all 0.2s;
|
|
960
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
961
|
+
background: rgba(255, 255, 255, 0.05);
|
|
962
|
+
color: #e0e0e0;
|
|
963
|
+
display: flex;
|
|
964
|
+
align-items: center;
|
|
965
|
+
justify-content: center;
|
|
966
|
+
gap: 8px;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.btn-passkey:hover:not(:disabled) {
|
|
970
|
+
background: rgba(255, 255, 255, 0.1);
|
|
971
|
+
border-color: #3b82f6;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
.btn-passkey:disabled {
|
|
975
|
+
opacity: 0.5;
|
|
976
|
+
cursor: not-allowed;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.passkey-icon {
|
|
980
|
+
font-size: 16px;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.or-divider {
|
|
984
|
+
display: flex;
|
|
985
|
+
align-items: center;
|
|
986
|
+
margin: 16px 0;
|
|
987
|
+
color: #6b7280;
|
|
988
|
+
font-size: 12px;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.or-divider::before,
|
|
992
|
+
.or-divider::after {
|
|
993
|
+
content: "";
|
|
994
|
+
flex: 1;
|
|
995
|
+
height: 1px;
|
|
996
|
+
background: rgba(255, 255, 255, 0.1);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
.or-divider span {
|
|
1000
|
+
padding: 0 12px;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.passkey-status {
|
|
1004
|
+
margin-top: 8px;
|
|
1005
|
+
font-size: 12px;
|
|
1006
|
+
text-align: center;
|
|
1007
|
+
min-height: 16px;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.passkey-status.error {
|
|
1011
|
+
color: #f87171;
|
|
1012
|
+
}
|
|
801
1013
|
</style>
|
|
802
1014
|
</head>
|
|
803
1015
|
<body>
|
|
@@ -831,6 +1043,9 @@ function renderConsentUI(options) {
|
|
|
831
1043
|
|
|
832
1044
|
<p class="info">You can revoke access anytime in your account settings.</p>
|
|
833
1045
|
</div>
|
|
1046
|
+
${passkeyAvailable && passkeyOptions ? `
|
|
1047
|
+
<script data-passkey-options="${escapeHtml(JSON.stringify(passkeyOptions))}" data-oauth-params="${escapeHtml(JSON.stringify(oauthParams))}">${PASSKEY_AUTH_SCRIPT}<\/script>
|
|
1048
|
+
` : ""}
|
|
834
1049
|
</body>
|
|
835
1050
|
</html>`;
|
|
836
1051
|
}
|
|
@@ -1080,6 +1295,8 @@ var ATProtoOAuthProvider = class {
|
|
|
1080
1295
|
clientResolver;
|
|
1081
1296
|
verifyUser;
|
|
1082
1297
|
getCurrentUser;
|
|
1298
|
+
getPasskeyOptions;
|
|
1299
|
+
verifyPasskey;
|
|
1083
1300
|
constructor(config) {
|
|
1084
1301
|
this.storage = config.storage;
|
|
1085
1302
|
this.issuer = config.issuer;
|
|
@@ -1089,6 +1306,8 @@ var ATProtoOAuthProvider = class {
|
|
|
1089
1306
|
this.clientResolver = config.clientResolver ?? new ClientResolver({ storage: config.storage });
|
|
1090
1307
|
this.verifyUser = config.verifyUser;
|
|
1091
1308
|
this.getCurrentUser = config.getCurrentUser;
|
|
1309
|
+
this.getPasskeyOptions = config.getPasskeyOptions;
|
|
1310
|
+
this.verifyPasskey = config.verifyPasskey;
|
|
1092
1311
|
}
|
|
1093
1312
|
/**
|
|
1094
1313
|
* Handle authorization request (GET/POST /oauth/authorize)
|
|
@@ -1104,11 +1323,11 @@ var ATProtoOAuthProvider = class {
|
|
|
1104
1323
|
const requestUri = url.searchParams.get("request_uri");
|
|
1105
1324
|
const clientId = url.searchParams.get("client_id");
|
|
1106
1325
|
if (requestUri && this.enablePAR) {
|
|
1107
|
-
if (!clientId) return this.renderError("invalid_request", "client_id required with request_uri");
|
|
1326
|
+
if (!clientId) return await this.renderError("invalid_request", "client_id required with request_uri");
|
|
1108
1327
|
const parParams = await this.parHandler.retrieveParams(requestUri, clientId);
|
|
1109
|
-
if (!parParams) return this.renderError("invalid_request", "Invalid or expired request_uri");
|
|
1328
|
+
if (!parParams) return await this.renderError("invalid_request", "Invalid or expired request_uri");
|
|
1110
1329
|
params = parParams;
|
|
1111
|
-
} else if (this.enablePAR) return this.renderError("invalid_request", "Pushed Authorization Request required. Use the PAR endpoint first.");
|
|
1330
|
+
} else if (this.enablePAR) return await this.renderError("invalid_request", "Pushed Authorization Request required. Use the PAR endpoint first.");
|
|
1112
1331
|
else params = Object.fromEntries(url.searchParams.entries());
|
|
1113
1332
|
}
|
|
1114
1333
|
for (const param of [
|
|
@@ -1117,19 +1336,22 @@ var ATProtoOAuthProvider = class {
|
|
|
1117
1336
|
"response_type",
|
|
1118
1337
|
"code_challenge",
|
|
1119
1338
|
"state"
|
|
1120
|
-
]) if (!params[param]) return this.renderError("invalid_request", `Missing required parameter: ${param}`);
|
|
1121
|
-
if (params.response_type !== "code") return this.renderError("unsupported_response_type", "Only response_type=code is supported");
|
|
1122
|
-
if (params.code_challenge_method && params.code_challenge_method !== "S256") return this.renderError("invalid_request", "Only code_challenge_method=S256 is supported");
|
|
1339
|
+
]) if (!params[param]) return await this.renderError("invalid_request", `Missing required parameter: ${param}`);
|
|
1340
|
+
if (params.response_type !== "code") return await this.renderError("unsupported_response_type", "Only response_type=code is supported");
|
|
1341
|
+
if (params.code_challenge_method && params.code_challenge_method !== "S256") return await this.renderError("invalid_request", "Only code_challenge_method=S256 is supported");
|
|
1123
1342
|
let client;
|
|
1124
1343
|
try {
|
|
1125
1344
|
client = await this.clientResolver.resolveClient(params.client_id);
|
|
1126
1345
|
} catch (e) {
|
|
1127
|
-
return this.renderError("invalid_client", `Failed to resolve client: ${e}`);
|
|
1346
|
+
return await this.renderError("invalid_client", `Failed to resolve client: ${e}`);
|
|
1128
1347
|
}
|
|
1129
|
-
if (!client.redirectUris.includes(params.redirect_uri)) return this.renderError("invalid_request", "Invalid redirect_uri for this client");
|
|
1348
|
+
if (!client.redirectUris.includes(params.redirect_uri)) return await this.renderError("invalid_request", "Invalid redirect_uri for this client");
|
|
1130
1349
|
if (request.method === "POST") return this.handleAuthorizePost(request, params, client);
|
|
1131
1350
|
let user = null;
|
|
1132
1351
|
if (this.getCurrentUser) user = await this.getCurrentUser();
|
|
1352
|
+
let passkeyOptions = null;
|
|
1353
|
+
if (!user && this.getPasskeyOptions) passkeyOptions = await this.getPasskeyOptions();
|
|
1354
|
+
const passkeyAvailable = !user && !!passkeyOptions;
|
|
1133
1355
|
const scope = params.scope ?? "atproto";
|
|
1134
1356
|
const html = renderConsentUI({
|
|
1135
1357
|
client,
|
|
@@ -1138,13 +1360,16 @@ var ATProtoOAuthProvider = class {
|
|
|
1138
1360
|
state: params.state,
|
|
1139
1361
|
oauthParams: params,
|
|
1140
1362
|
userHandle: user?.handle,
|
|
1141
|
-
showLogin: !user && !!this.verifyUser
|
|
1363
|
+
showLogin: !user && !!this.verifyUser,
|
|
1364
|
+
passkeyAvailable,
|
|
1365
|
+
passkeyOptions: passkeyOptions ?? void 0
|
|
1142
1366
|
});
|
|
1367
|
+
const csp = await getConsentUiCsp(passkeyAvailable);
|
|
1143
1368
|
return new Response(html, {
|
|
1144
1369
|
status: 200,
|
|
1145
1370
|
headers: {
|
|
1146
1371
|
"Content-Type": "text/html; charset=utf-8",
|
|
1147
|
-
"Content-Security-Policy":
|
|
1372
|
+
"Content-Security-Policy": csp,
|
|
1148
1373
|
"Cache-Control": "no-store"
|
|
1149
1374
|
}
|
|
1150
1375
|
});
|
|
@@ -1189,11 +1414,12 @@ var ATProtoOAuthProvider = class {
|
|
|
1189
1414
|
showLogin: true,
|
|
1190
1415
|
error: "Invalid password"
|
|
1191
1416
|
});
|
|
1417
|
+
const csp = await getConsentUiCsp(false);
|
|
1192
1418
|
return new Response(html, {
|
|
1193
1419
|
status: 401,
|
|
1194
1420
|
headers: {
|
|
1195
1421
|
"Content-Type": "text/html; charset=utf-8",
|
|
1196
|
-
"Content-Security-Policy":
|
|
1422
|
+
"Content-Security-Policy": csp,
|
|
1197
1423
|
"Cache-Control": "no-store"
|
|
1198
1424
|
}
|
|
1199
1425
|
});
|
|
@@ -1436,15 +1662,90 @@ var ATProtoOAuthProvider = class {
|
|
|
1436
1662
|
return tokenData;
|
|
1437
1663
|
}
|
|
1438
1664
|
/**
|
|
1665
|
+
* Handle passkey authentication (POST /oauth/passkey-auth)
|
|
1666
|
+
*
|
|
1667
|
+
* This endpoint is called by the client-side JavaScript after a successful
|
|
1668
|
+
* WebAuthn authentication. It verifies the passkey and returns a redirect URL
|
|
1669
|
+
* to complete the OAuth authorization flow.
|
|
1670
|
+
*/
|
|
1671
|
+
async handlePasskeyAuth(request) {
|
|
1672
|
+
if (!this.verifyPasskey) return oauthError("unsupported_auth_method", "Passkey authentication is not configured", 400);
|
|
1673
|
+
let body;
|
|
1674
|
+
try {
|
|
1675
|
+
body = await request.json();
|
|
1676
|
+
} catch {
|
|
1677
|
+
return oauthError("invalid_request", "Invalid JSON body", 400);
|
|
1678
|
+
}
|
|
1679
|
+
const { response, challenge, oauthParams } = body;
|
|
1680
|
+
if (!response || !challenge || !oauthParams) return oauthError("invalid_request", "Missing required parameters", 400);
|
|
1681
|
+
const user = await this.verifyPasskey(response, challenge);
|
|
1682
|
+
if (!user) return new Response(JSON.stringify({ error: "Authentication failed" }), {
|
|
1683
|
+
status: 401,
|
|
1684
|
+
headers: { "Content-Type": "application/json" }
|
|
1685
|
+
});
|
|
1686
|
+
for (const param of [
|
|
1687
|
+
"client_id",
|
|
1688
|
+
"redirect_uri",
|
|
1689
|
+
"state",
|
|
1690
|
+
"code_challenge"
|
|
1691
|
+
]) if (!oauthParams[param]) return new Response(JSON.stringify({ error: `Missing OAuth parameter: ${param}` }), {
|
|
1692
|
+
status: 400,
|
|
1693
|
+
headers: { "Content-Type": "application/json" }
|
|
1694
|
+
});
|
|
1695
|
+
let client;
|
|
1696
|
+
try {
|
|
1697
|
+
client = await this.clientResolver.resolveClient(oauthParams.client_id);
|
|
1698
|
+
} catch (e) {
|
|
1699
|
+
return new Response(JSON.stringify({ error: `Invalid client: ${e}` }), {
|
|
1700
|
+
status: 400,
|
|
1701
|
+
headers: { "Content-Type": "application/json" }
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
if (!client.redirectUris.includes(oauthParams.redirect_uri)) return new Response(JSON.stringify({ error: "Invalid redirect_uri for this client" }), {
|
|
1705
|
+
status: 400,
|
|
1706
|
+
headers: { "Content-Type": "application/json" }
|
|
1707
|
+
});
|
|
1708
|
+
const code = generateAuthCode();
|
|
1709
|
+
const scope = oauthParams.scope ?? "atproto";
|
|
1710
|
+
const authCodeData = {
|
|
1711
|
+
clientId: oauthParams.client_id,
|
|
1712
|
+
redirectUri: oauthParams.redirect_uri,
|
|
1713
|
+
codeChallenge: oauthParams.code_challenge,
|
|
1714
|
+
codeChallengeMethod: "S256",
|
|
1715
|
+
scope,
|
|
1716
|
+
sub: user.sub,
|
|
1717
|
+
expiresAt: Date.now() + AUTH_CODE_TTL
|
|
1718
|
+
};
|
|
1719
|
+
await this.storage.saveAuthCode(code, authCodeData);
|
|
1720
|
+
const responseMode = oauthParams.response_mode ?? "query";
|
|
1721
|
+
const redirectUrl = new URL(oauthParams.redirect_uri);
|
|
1722
|
+
if (responseMode === "fragment") {
|
|
1723
|
+
const hashParams = new URLSearchParams();
|
|
1724
|
+
hashParams.set("code", code);
|
|
1725
|
+
hashParams.set("state", oauthParams.state);
|
|
1726
|
+
hashParams.set("iss", this.issuer);
|
|
1727
|
+
redirectUrl.hash = hashParams.toString();
|
|
1728
|
+
} else {
|
|
1729
|
+
redirectUrl.searchParams.set("code", code);
|
|
1730
|
+
redirectUrl.searchParams.set("state", oauthParams.state);
|
|
1731
|
+
redirectUrl.searchParams.set("iss", this.issuer);
|
|
1732
|
+
}
|
|
1733
|
+
return new Response(JSON.stringify({ redirectUrl: redirectUrl.toString() }), {
|
|
1734
|
+
status: 200,
|
|
1735
|
+
headers: { "Content-Type": "application/json" }
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1439
1739
|
* Render an error page
|
|
1440
1740
|
*/
|
|
1441
|
-
renderError(error, description) {
|
|
1741
|
+
async renderError(error, description) {
|
|
1442
1742
|
const html = renderErrorPage(error, description);
|
|
1743
|
+
const csp = await getConsentUiCsp(false);
|
|
1443
1744
|
return new Response(html, {
|
|
1444
1745
|
status: 400,
|
|
1445
1746
|
headers: {
|
|
1446
1747
|
"Content-Type": "text/html; charset=utf-8",
|
|
1447
|
-
"Content-Security-Policy":
|
|
1748
|
+
"Content-Security-Policy": csp,
|
|
1448
1749
|
"Cache-Control": "no-store"
|
|
1449
1750
|
}
|
|
1450
1751
|
});
|
|
@@ -1541,5 +1842,5 @@ var InMemoryOAuthStorage = class {
|
|
|
1541
1842
|
};
|
|
1542
1843
|
|
|
1543
1844
|
//#endregion
|
|
1544
|
-
export { ACCESS_TOKEN_TTL, ATProtoOAuthProvider, AUTH_CODE_TTL,
|
|
1845
|
+
export { ACCESS_TOKEN_TTL, ATProtoOAuthProvider, AUTH_CODE_TTL, ClientAuthError, ClientResolutionError, ClientResolver, DpopError, InMemoryOAuthStorage, JWT_BEARER_ASSERTION_TYPE, PARHandler, REFRESH_TOKEN_TTL, RequestBodyError, authenticateClient, buildTokenResponse, createClientResolver, extractAccessToken, generateAuthCode, generateDpopNonce, generateRandomToken, generateTokens, getConsentUiCsp, getPasskeyAuthScriptHash, isTokenValid, parseClientAssertion, parseRequestBody, refreshTokens, renderConsentUI, renderErrorPage, verifyClientAssertion, verifyDpopProof, verifyPkceChallenge };
|
|
1545
1846
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["url: URL","protectedHeader: { alg: string; jwk?: JWK }","payload: {\n\t\tjti?: string;\n\t\thtm?: string;\n\t\thtu?: string;\n\t\tiat?: number;\n\t\tath?: string;\n\t\tnonce?: string;\n\t}","JOSEError","params: Record<string, string>","parData: PARData","response: OAuthParResponse","body: OAuthErrorResponse","code: string","response: Response","doc: OAuthClientMetadata","metadata: ClientMetadata","tokenData: TokenData","descriptions: string[]","code: string","keyResolver: Parameters<typeof jwtVerify>[1]","key: JWK | undefined","payload: JWTPayload","params: Record<string, string>","client: ClientMetadata","user: { sub: string; handle: string } | null","authCodeData: AuthCodeData","dpopJkt: string | undefined","metadata: OAuthAuthorizationServerMetadata"],"sources":["../src/pkce.ts","../src/encoding.ts","../src/dpop.ts","../src/par.ts","../src/client-resolver.ts","../src/tokens.ts","../src/ui.ts","../src/client-auth.ts","../src/provider.ts","../src/storage.ts"],"sourcesContent":["/**\n * PKCE (Proof Key for Code Exchange) verification\n * Implements RFC 7636 with S256 challenge method\n */\n\nimport { base64url } from \"jose\";\n\n/**\n * Generate the S256 code challenge from a verifier\n * challenge = BASE64URL(SHA256(verifier))\n */\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst data = encoder.encode(verifier);\n\tconst hash = await crypto.subtle.digest(\"SHA-256\", data);\n\treturn base64url.encode(new Uint8Array(hash));\n}\n\n/**\n * Verify a PKCE code challenge against a verifier\n * @param verifier The code verifier from the token request\n * @param challenge The code challenge from the authorization request\n * @param method The challenge method (only S256 supported for AT Protocol)\n * @returns true if the verifier matches the challenge\n */\nexport async function verifyPkceChallenge(\n\tverifier: string,\n\tchallenge: string,\n\tmethod: \"S256\"\n): Promise<boolean> {\n\tif (method !== \"S256\") {\n\t\tthrow new Error(\"Only S256 challenge method is supported\");\n\t}\n\n\t// Validate verifier format (RFC 7636 Section 4.1)\n\t// Must be 43-128 characters, unreserved characters only\n\tif (verifier.length < 43 || verifier.length > 128) {\n\t\treturn false;\n\t}\n\tif (!/^[A-Za-z0-9._~-]+$/.test(verifier)) {\n\t\treturn false;\n\t}\n\n\tconst expectedChallenge = await generateCodeChallenge(verifier);\n\treturn expectedChallenge === challenge;\n}\n","/**\n * Shared encoding utilities for OAuth provider\n */\n\nimport { base64url } from \"jose\";\n\n/**\n * Generate a cryptographically random string\n *\n * @param byteLength Number of random bytes (default: 32 = 256 bits)\n * @returns Base64URL-encoded random string\n */\nexport function randomString(byteLength: number = 32): string {\n\tconst buffer = new Uint8Array(byteLength);\n\tcrypto.getRandomValues(buffer);\n\treturn base64url.encode(buffer);\n}\n","/**\n * DPoP (Demonstrating Proof of Possession) verification\n * Implements RFC 9449 using jose library for JWT operations\n */\n\nimport { jwtVerify, EmbeddedJWK, calculateJwkThumbprint, errors, base64url } from \"jose\";\nimport type { JWK } from \"jose\";\nimport { randomString } from \"./encoding.js\";\n\nconst { JOSEError } = errors;\n\n/**\n * Verified DPoP proof data\n */\nexport interface DpopProof {\n\t/** HTTP method from the proof */\n\thtm: string;\n\t/** HTTP URI from the proof (without query/fragment) */\n\thtu: string;\n\t/** Unique proof identifier (for replay prevention) */\n\tjti: string;\n\t/** Access token hash (if present) */\n\tath?: string;\n\t/** Key thumbprint (JWK thumbprint of the proof key) */\n\tjkt: string;\n\t/** The public JWK from the proof */\n\tjwk: JWK;\n}\n\n/**\n * DPoP verification options\n */\nexport interface DpopVerifyOptions {\n\t/** Access token to verify ath claim against (optional) */\n\taccessToken?: string;\n\t/** Allowed signature algorithms (default: ['ES256']) */\n\tallowedAlgorithms?: string[];\n\t/** Expected nonce value (optional, for nonce binding) */\n\texpectedNonce?: string;\n\t/** Max token age in seconds (default: 60) */\n\tmaxTokenAge?: number;\n}\n\n/**\n * DPoP verification error\n */\nexport class DpopError extends Error {\n\treadonly code: string;\n\tconstructor(message: string, code: string, options?: ErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.name = \"DpopError\";\n\t\tthis.code = code;\n\t}\n}\n\n/**\n * Normalize URI for HTU comparison\n * Removes query string and fragment per RFC 9449\n */\nfunction normalizeHtuUrl(url: URL): string {\n\treturn url.origin + url.pathname;\n}\n\n/**\n * Parse and validate HTU claim\n */\nfunction parseHtu(htu: string): string {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(htu);\n\t} catch {\n\t\tthrow new DpopError('DPoP \"htu\" is not a valid URL', \"invalid_dpop\");\n\t}\n\n\tif (url.password || url.username) {\n\t\tthrow new DpopError('DPoP \"htu\" must not contain credentials', \"invalid_dpop\");\n\t}\n\n\tif (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n\t\tthrow new DpopError('DPoP \"htu\" must be http or https', \"invalid_dpop\");\n\t}\n\n\treturn normalizeHtuUrl(url);\n}\n\n/**\n * Verify a DPoP proof from a request\n * Uses jose library for JWT verification\n * @param request The HTTP request containing the DPoP header\n * @param options Verification options\n * @returns The verified proof data\n * @throws DpopError if verification fails\n */\nexport async function verifyDpopProof(\n\trequest: Request,\n\toptions: DpopVerifyOptions = {}\n): Promise<DpopProof> {\n\tconst { allowedAlgorithms = [\"ES256\"], accessToken, expectedNonce, maxTokenAge = 60 } = options;\n\n\tconst dpopHeader = request.headers.get(\"DPoP\");\n\tif (!dpopHeader) {\n\t\tthrow new DpopError(\"Missing DPoP header\", \"missing_dpop\");\n\t}\n\n\tlet protectedHeader: { alg: string; jwk?: JWK };\n\tlet payload: {\n\t\tjti?: string;\n\t\thtm?: string;\n\t\thtu?: string;\n\t\tiat?: number;\n\t\tath?: string;\n\t\tnonce?: string;\n\t};\n\n\ttry {\n\t\tconst result = await jwtVerify(dpopHeader, EmbeddedJWK, {\n\t\t\ttyp: \"dpop+jwt\",\n\t\t\talgorithms: allowedAlgorithms,\n\t\t\tmaxTokenAge,\n\t\t\tclockTolerance: 10,\n\t\t});\n\t\tprotectedHeader = result.protectedHeader as typeof protectedHeader;\n\t\tpayload = result.payload as typeof payload;\n\t} catch (err) {\n\t\tif (err instanceof JOSEError) {\n\t\t\tthrow new DpopError(`DPoP verification failed: ${err.message}`, \"invalid_dpop\", { cause: err });\n\t\t}\n\t\tthrow new DpopError(\"DPoP verification failed\", \"invalid_dpop\", { cause: err });\n\t}\n\n\tif (!payload.jti || typeof payload.jti !== \"string\") {\n\t\tthrow new DpopError('DPoP \"jti\" missing', \"invalid_dpop\");\n\t}\n\n\tif (!payload.htm || typeof payload.htm !== \"string\") {\n\t\tthrow new DpopError('DPoP \"htm\" missing', \"invalid_dpop\");\n\t}\n\n\tif (!payload.htu || typeof payload.htu !== \"string\") {\n\t\tthrow new DpopError('DPoP \"htu\" missing', \"invalid_dpop\");\n\t}\n\n\tif (payload.htm !== request.method) {\n\t\tthrow new DpopError('DPoP \"htm\" mismatch', \"invalid_dpop\");\n\t}\n\n\tconst requestUrl = new URL(request.url);\n\tconst expectedHtu = normalizeHtuUrl(requestUrl);\n\tconst proofHtu = parseHtu(payload.htu);\n\tif (proofHtu !== expectedHtu) {\n\t\tthrow new DpopError('DPoP \"htu\" mismatch', \"invalid_dpop\");\n\t}\n\n\tif (expectedNonce !== undefined && payload.nonce !== expectedNonce) {\n\t\tthrow new DpopError('DPoP \"nonce\" mismatch', \"use_dpop_nonce\");\n\t}\n\n\t// Verify ath (access token hash) binding per RFC 9449 Section 4.3\n\tif (accessToken) {\n\t\tif (!payload.ath) {\n\t\t\tthrow new DpopError('DPoP \"ath\" missing when access token provided', \"invalid_dpop\");\n\t\t}\n\n\t\tconst tokenHash = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(accessToken));\n\t\tconst expectedAth = base64url.encode(new Uint8Array(tokenHash));\n\n\t\tif (payload.ath !== expectedAth) {\n\t\t\tthrow new DpopError('DPoP \"ath\" mismatch', \"invalid_dpop\");\n\t\t}\n\t} else if (payload.ath !== undefined) {\n\t\tthrow new DpopError('DPoP \"ath\" claim not allowed without access token', \"invalid_dpop\");\n\t}\n\n\tconst jwk = protectedHeader.jwk!;\n\tconst jkt = await calculateJwkThumbprint(jwk, \"sha256\");\n\n\treturn Object.freeze({\n\t\thtm: payload.htm,\n\t\thtu: payload.htu,\n\t\tjti: payload.jti,\n\t\tath: payload.ath,\n\t\tjkt,\n\t\tjwk,\n\t});\n}\n\n/**\n * Generate a random DPoP nonce\n * @returns A base64url-encoded random nonce (16 bytes)\n */\nexport function generateDpopNonce(): string {\n\treturn randomString(16);\n}\n","/**\n * PAR (Pushed Authorization Requests) handler\n * Implements RFC 9126\n */\n\nimport type { OAuthParResponse } from \"@atproto/oauth-types\";\nimport type { OAuthStorage, PARData } from \"./storage.js\";\nimport { randomString } from \"./encoding.js\";\nimport { parseRequestBody } from \"./provider.js\";\n\nexport type { OAuthParResponse };\n\n/** PAR request URI prefix per RFC 9126 */\nconst REQUEST_URI_PREFIX = \"urn:ietf:params:oauth:request_uri:\";\n\n/** Default PAR expiration in seconds (90 seconds per RFC recommendation) */\nconst DEFAULT_EXPIRES_IN = 90;\n\n/**\n * OAuth error response\n */\nexport interface OAuthErrorResponse {\n\terror: string;\n\terror_description?: string;\n}\n\n/**\n * Generate a unique request URI\n */\nfunction generateRequestUri(): string {\n\treturn REQUEST_URI_PREFIX + randomString(32);\n}\n\n/**\n * Required OAuth parameters for authorization request\n */\nconst REQUIRED_PARAMS = [\"client_id\", \"redirect_uri\", \"response_type\", \"code_challenge\", \"code_challenge_method\", \"state\"];\n\n/**\n * Handler for Pushed Authorization Requests (PAR)\n */\nexport class PARHandler {\n\tprivate storage: OAuthStorage;\n\tprivate issuer: string;\n\tprivate expiresIn: number;\n\n\t/**\n\t * Create a PAR handler\n\t * @param storage OAuth storage implementation\n\t * @param issuer The OAuth issuer URL\n\t * @param expiresIn PAR expiration time in seconds (default: 90)\n\t */\n\tconstructor(storage: OAuthStorage, issuer: string, expiresIn: number = DEFAULT_EXPIRES_IN) {\n\t\tthis.storage = storage;\n\t\tthis.issuer = issuer;\n\t\tthis.expiresIn = expiresIn;\n\t}\n\n\t/**\n\t * Handle a PAR push request\n\t * POST /oauth/par\n\t * @param request The HTTP request\n\t * @returns Response with request_uri or error\n\t */\n\tasync handlePushRequest(request: Request): Promise<Response> {\n\t\tlet params: Record<string, string>;\n\t\ttry {\n\t\t\tparams = await parseRequestBody(request);\n\t\t} catch (e) {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\te instanceof Error ? e.message : \"Invalid request\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tconst clientId = params.client_id;\n\t\tif (!clientId) {\n\t\t\treturn this.errorResponse(\"invalid_request\", \"Missing client_id parameter\", 400);\n\t\t}\n\n\t\tfor (const param of REQUIRED_PARAMS) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn this.errorResponse(\"invalid_request\", `Missing required parameter: ${param}`, 400);\n\t\t\t}\n\t\t}\n\n\t\tif (params.response_type !== \"code\") {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"unsupported_response_type\",\n\t\t\t\t\"Only response_type=code is supported\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tif (params.code_challenge_method !== \"S256\") {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t\"Only code_challenge_method=S256 is supported\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tconst codeChallenge = params.code_challenge!;\n\t\tif (!/^[A-Za-z0-9_-]{43}$/.test(codeChallenge)) {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t\"Invalid code_challenge format\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\tnew URL(params.redirect_uri!);\n\t\t} catch {\n\t\t\treturn this.errorResponse(\"invalid_request\", \"Invalid redirect_uri\", 400);\n\t\t}\n\n\t\tconst requestUri = generateRequestUri();\n\t\tconst expiresAt = Date.now() + this.expiresIn * 1000;\n\n\t\tconst parData: PARData = {\n\t\t\tclientId,\n\t\t\tparams,\n\t\t\texpiresAt,\n\t\t};\n\n\t\tawait this.storage.savePAR(requestUri, parData);\n\n\t\tconst response: OAuthParResponse = {\n\t\t\trequest_uri: requestUri,\n\t\t\texpires_in: this.expiresIn,\n\t\t};\n\n\t\treturn new Response(JSON.stringify(response), {\n\t\t\tstatus: 201,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Retrieve and consume PAR parameters\n\t * Called during authorization request handling\n\t * @param requestUri The request URI from the authorization request\n\t * @param clientId The client_id from the authorization request (for verification)\n\t * @returns The stored parameters or null if not found/expired\n\t */\n\tasync retrieveParams(\n\t\trequestUri: string,\n\t\tclientId: string\n\t): Promise<Record<string, string> | null> {\n\t\tif (!requestUri.startsWith(REQUEST_URI_PREFIX)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst parData = await this.storage.getPAR(requestUri);\n\t\tif (!parData) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (parData.clientId !== clientId) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// One-time use: delete after retrieval\n\t\tawait this.storage.deletePAR(requestUri);\n\n\t\treturn parData.params;\n\t}\n\n\t/**\n\t * Check if a request_uri is valid format\n\t */\n\tstatic isRequestUri(value: string): boolean {\n\t\treturn value.startsWith(REQUEST_URI_PREFIX);\n\t}\n\n\t/**\n\t * Create an OAuth error response\n\t */\n\tprivate errorResponse(\n\t\terror: string,\n\t\tdescription: string,\n\t\tstatus: number = 400\n\t): Response {\n\t\tconst body: OAuthErrorResponse = {\n\t\t\terror,\n\t\t\terror_description: description,\n\t\t};\n\t\treturn new Response(JSON.stringify(body), {\n\t\t\tstatus,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n}\n","/**\n * Client resolver for DID-based client discovery\n * Resolves OAuth client metadata from DIDs for AT Protocol\n */\n\nimport { ensureValidDid } from \"@atproto/syntax\";\nimport {\n\toauthClientMetadataSchema,\n\ttype OAuthClientMetadata,\n} from \"@atproto/oauth-types\";\nimport type { ClientMetadata, OAuthStorage, JWK } from \"./storage.js\";\n\nexport type { OAuthClientMetadata };\n\n/**\n * Client resolution error\n */\nexport class ClientResolutionError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly code: string\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"ClientResolutionError\";\n\t}\n}\n\n/**\n * Options for client resolution\n */\nexport interface ClientResolverOptions {\n\t/** Storage for caching client metadata */\n\tstorage?: OAuthStorage;\n\t/** Cache TTL in milliseconds (default: 1 hour) */\n\tcacheTtl?: number;\n\t/** Fetch function for making HTTP requests (for testing) */\n\tfetch?: typeof globalThis.fetch;\n}\n\n/**\n * Check if a string is a valid HTTPS URL\n */\nfunction isHttpsUrl(value: string): boolean {\n\ttry {\n\t\tconst url = new URL(value);\n\t\treturn url.protocol === \"https:\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Validate that a string is a valid DID using @atproto/syntax\n */\nfunction isValidDid(value: string): boolean {\n\ttry {\n\t\tensureValidDid(value);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Get the client metadata URL from a client ID\n * Supports both URL-based and DID-based client IDs\n */\nfunction getClientMetadataUrl(clientId: string): string | null {\n\t// URL-based client ID: the URL itself is the metadata endpoint\n\tif (isHttpsUrl(clientId)) {\n\t\treturn clientId;\n\t}\n\n\t// DID-based client ID: derive the metadata URL\n\tif (clientId.startsWith(\"did:web:\")) {\n\t\t// did:web:example.com -> https://example.com/.well-known/oauth-client-metadata\n\t\t// did:web:example.com:path -> https://example.com/path/.well-known/oauth-client-metadata\n\t\tconst parts = clientId.slice(8).split(\":\");\n\t\tconst host = parts[0]!.replace(/%3A/g, \":\");\n\t\tconst path = parts.slice(1).join(\"/\");\n\t\tconst baseUrl = `https://${host}${path ? \"/\" + path : \"\"}`;\n\t\treturn `${baseUrl}/.well-known/oauth-client-metadata`;\n\t}\n\n\t// Unsupported client ID format\n\treturn null;\n}\n\n/**\n * Resolve client metadata from a DID\n */\nexport class ClientResolver {\n\tprivate storage?: OAuthStorage;\n\tprivate cacheTtl: number;\n\tprivate fetchFn: typeof globalThis.fetch;\n\n\tconstructor(options: ClientResolverOptions = {}) {\n\t\tthis.storage = options.storage;\n\t\tthis.cacheTtl = options.cacheTtl ?? 60 * 60 * 1000; // 1 hour default\n\t\tthis.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);\n\t}\n\n\t/**\n\t * Resolve client metadata from a client ID (URL or DID)\n\t * @param clientId The client ID (HTTPS URL or DID)\n\t * @returns The client metadata\n\t * @throws ClientResolutionError if resolution fails\n\t */\n\tasync resolveClient(clientId: string): Promise<ClientMetadata> {\n\t\tif (!isHttpsUrl(clientId) && !isValidDid(clientId)) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Invalid client ID format: ${clientId}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tif (this.storage) {\n\t\t\tconst cached = await this.storage.getClient(clientId);\n\t\t\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 * Content Security Policy for the consent UI\n *\n * - default-src 'none': Deny all by default\n * - style-src 'unsafe-inline': Allow inline styles (our CSS is inline)\n * - img-src https: data:: Allow images from HTTPS URLs (client logos) and data URIs\n * - frame-ancestors 'none': Prevent clickjacking by disallowing framing\n * - base-uri 'none': Prevent base tag injection\n *\n * Note: form-action is intentionally omitted. Browser behavior for blocking\n * redirects after form submission is inconsistent - Chrome blocks redirects\n * to URLs not in form-action, while Firefox does not. Since OAuth requires\n * redirecting to the client's callback URL after form submission, we cannot\n * use form-action without breaking the flow in Chrome.\n * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/form-action\n */\nexport const CONSENT_UI_CSP =\n\t\"default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; frame-ancestors 'none'; base-uri 'none'\";\n\n/**\n * Escape HTML to prevent XSS\n */\nfunction escapeHtml(text: string): string {\n\treturn text\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n\n/**\n * Parse scope string into human-readable descriptions\n */\nfunction getScopeDescriptions(scope: string): string[] {\n\tconst scopes = scope.split(\" \").filter(Boolean);\n\tconst descriptions: string[] = [];\n\n\tfor (const s of scopes) {\n\t\tswitch (s) {\n\t\t\tcase \"atproto\":\n\t\t\t\tdescriptions.push(\"Access your AT Protocol account\");\n\t\t\t\tbreak;\n\t\t\tcase \"transition:generic\":\n\t\t\t\tdescriptions.push(\"Perform account operations\");\n\t\t\t\tbreak;\n\t\t\tcase \"transition:chat.bsky\":\n\t\t\t\tdescriptions.push(\"Access chat functionality\");\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t// Don't show unknown scopes to avoid confusion\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t// If no recognized scopes, show a generic message\n\tif (descriptions.length === 0) {\n\t\tdescriptions.push(\"Access your account on your behalf\");\n\t}\n\n\treturn descriptions;\n}\n\n/**\n * Options for rendering the consent UI\n */\nexport interface ConsentUIOptions {\n\t/** The OAuth client metadata */\n\tclient: ClientMetadata;\n\t/** The requested scope */\n\tscope: string;\n\t/** URL to POST the consent form to */\n\tauthorizeUrl: string;\n\t/** State parameter to include in the form */\n\tstate: string;\n\t/** OAuth parameters to include as hidden fields */\n\toauthParams: Record<string, string>;\n\t/** User's handle (for display) */\n\tuserHandle?: string;\n\t/** Whether to show a login form instead of consent */\n\tshowLogin?: boolean;\n\t/** Error message to display */\n\terror?: string;\n}\n\n/**\n * Render the consent UI HTML\n * @param options Consent UI options\n * @returns HTML string\n */\nexport function renderConsentUI(options: ConsentUIOptions): string {\n\tconst { client, scope, authorizeUrl, oauthParams, userHandle, showLogin, error } = options;\n\n\tconst clientName = escapeHtml(client.clientName);\n\tconst scopeDescriptions = getScopeDescriptions(scope);\n\tconst logoHtml = client.logoUri\n\t\t? `<img src=\"${escapeHtml(client.logoUri)}\" alt=\"${clientName} logo\" class=\"app-logo\" />`\n\t\t: `<div class=\"app-logo-placeholder\">${clientName.charAt(0).toUpperCase()}</div>`;\n\n\tconst errorHtml = error\n\t\t? `<div class=\"error-message\">${escapeHtml(error)}</div>`\n\t\t: \"\";\n\n\tconst loginFormHtml = showLogin\n\t\t? `\n\t\t\t<div class=\"login-form\">\n\t\t\t\t<p>Sign in to continue</p>\n\t\t\t\t<input type=\"password\" name=\"password\" placeholder=\"Password\" required autocomplete=\"current-password\" />\n\t\t\t</div>\n\t\t`\n\t\t: \"\";\n\n\t// Render OAuth params as hidden form fields\n\tconst hiddenFieldsHtml = Object.entries(oauthParams)\n\t\t.map(([key, value]) => `<input type=\"hidden\" name=\"${escapeHtml(key)}\" value=\"${escapeHtml(value)}\" />`)\n\t\t.join(\"\\n\\t\\t\\t\");\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>Authorize ${clientName}</title>\n\t<style>\n\t\t* {\n\t\t\tbox-sizing: border-box;\n\t\t\tmargin: 0;\n\t\t\tpadding: 0;\n\t\t}\n\n\t\tbody {\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n\t\t\tbackground: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n\t\t\tmin-height: 100vh;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tpadding: 20px;\n\t\t\tcolor: #e0e0e0;\n\t\t}\n\n\t\t.container {\n\t\t\tbackground: #1e1e30;\n\t\t\tborder-radius: 16px;\n\t\t\tpadding: 32px;\n\t\t\tmax-width: 400px;\n\t\t\twidth: 100%;\n\t\t\tbox-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t}\n\n\t\t.header {\n\t\t\ttext-align: center;\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.app-logo {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tborder-radius: 12px;\n\t\t\tmargin-bottom: 16px;\n\t\t\tobject-fit: cover;\n\t\t}\n\n\t\t.app-logo-placeholder {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tborder-radius: 12px;\n\t\t\tmargin: 0 auto 16px;\n\t\t\tbackground: linear-gradient(135deg, #3b82f6, #8b5cf6);\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tfont-size: 28px;\n\t\t\tfont-weight: 600;\n\t\t\tcolor: white;\n\t\t}\n\n\t\th1 {\n\t\t\tfont-size: 20px;\n\t\t\tfont-weight: 600;\n\t\t\tmargin-bottom: 8px;\n\t\t}\n\n\t\t.client-name {\n\t\t\tcolor: #60a5fa;\n\t\t}\n\n\t\t.user-info {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t}\n\n\t\t.permissions {\n\t\t\tbackground: rgba(255, 255, 255, 0.05);\n\t\t\tborder-radius: 12px;\n\t\t\tpadding: 16px;\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.permissions-title {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t\tmargin-bottom: 12px;\n\t\t}\n\n\t\t.permissions-list {\n\t\t\tlist-style: none;\n\t\t}\n\n\t\t.permissions-list li {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: 10px;\n\t\t\tpadding: 8px 0;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\t.permissions-list li::before {\n\t\t\tcontent: \"\";\n\t\t\twidth: 8px;\n\t\t\theight: 8px;\n\t\t\tbackground: #22c55e;\n\t\t\tborder-radius: 50%;\n\t\t\tflex-shrink: 0;\n\t\t}\n\n\t\t.buttons {\n\t\t\tdisplay: flex;\n\t\t\tgap: 12px;\n\t\t}\n\n\t\tbutton {\n\t\t\tflex: 1;\n\t\t\tpadding: 12px 20px;\n\t\t\tborder-radius: 8px;\n\t\t\tfont-size: 14px;\n\t\t\tfont-weight: 500;\n\t\t\tcursor: pointer;\n\t\t\ttransition: all 0.2s;\n\t\t\tborder: none;\n\t\t}\n\n\t\t.btn-deny {\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t\tcolor: #e0e0e0;\n\t\t}\n\n\t\t.btn-deny:hover {\n\t\t\tbackground: rgba(255, 255, 255, 0.15);\n\t\t}\n\n\t\t.btn-allow {\n\t\t\tbackground: linear-gradient(135deg, #3b82f6, #2563eb);\n\t\t\tcolor: white;\n\t\t}\n\n\t\t.btn-allow:hover {\n\t\t\tbackground: linear-gradient(135deg, #2563eb, #1d4ed8);\n\t\t}\n\n\t\t.info {\n\t\t\tmargin-top: 16px;\n\t\t\tfont-size: 12px;\n\t\t\tcolor: #6b7280;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.error-message {\n\t\t\tbackground: rgba(239, 68, 68, 0.1);\n\t\t\tborder: 1px solid rgba(239, 68, 68, 0.3);\n\t\t\tcolor: #f87171;\n\t\t\tpadding: 12px;\n\t\t\tborder-radius: 8px;\n\t\t\tmargin-bottom: 16px;\n\t\t\tfont-size: 14px;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.login-form {\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.login-form p {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t\tmargin-bottom: 12px;\n\t\t}\n\n\t\t.login-form input {\n\t\t\twidth: 100%;\n\t\t\tpadding: 12px;\n\t\t\tborder-radius: 8px;\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t\tbackground: rgba(255, 255, 255, 0.05);\n\t\t\tcolor: #e0e0e0;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\t.login-form input:focus {\n\t\t\toutline: none;\n\t\t\tborder-color: #3b82f6;\n\t\t}\n\n\t\t.login-form input::placeholder {\n\t\t\tcolor: #6b7280;\n\t\t}\n\n\t\t.client-uri {\n\t\t\tfont-size: 12px;\n\t\t\tcolor: #6b7280;\n\t\t\tmargin-top: 4px;\n\t\t}\n\n\t\t.client-uri a {\n\t\t\tcolor: #60a5fa;\n\t\t\ttext-decoration: none;\n\t\t}\n\n\t\t.client-uri a:hover {\n\t\t\ttext-decoration: underline;\n\t\t}\n\t</style>\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<div class=\"header\">\n\t\t\t${logoHtml}\n\t\t\t<h1>Authorize <span class=\"client-name\">${clientName}</span></h1>\n\t\t\t${userHandle ? `<p class=\"user-info\">as @${escapeHtml(userHandle)}</p>` : \"\"}\n\t\t\t${client.clientUri ? `<p class=\"client-uri\"><a href=\"${escapeHtml(client.clientUri)}\" target=\"_blank\" rel=\"noopener\">${escapeHtml(new URL(client.clientUri).hostname)}</a></p>` : \"\"}\n\t\t</div>\n\n\t\t${errorHtml}\n\n\t\t<form method=\"POST\" action=\"${escapeHtml(authorizeUrl)}\">\n\t\t\t${hiddenFieldsHtml}\n\n\t\t\t${loginFormHtml}\n\n\t\t\t<div class=\"permissions\">\n\t\t\t\t<p class=\"permissions-title\">This app wants to:</p>\n\t\t\t\t<ul class=\"permissions-list\">\n\t\t\t\t\t${scopeDescriptions.map((desc) => `<li>${escapeHtml(desc)}</li>`).join(\"\")}\n\t\t\t\t</ul>\n\t\t\t</div>\n\n\t\t\t<div class=\"buttons\">\n\t\t\t\t<button type=\"submit\" name=\"action\" value=\"deny\" class=\"btn-deny\">Deny</button>\n\t\t\t\t<button type=\"submit\" name=\"action\" value=\"allow\" class=\"btn-allow\">Allow</button>\n\t\t\t</div>\n\t\t</form>\n\n\t\t<p class=\"info\">You can revoke access anytime in your account settings.</p>\n\t</div>\n</body>\n</html>`;\n}\n\n/**\n * Render an error page\n * @param error Error code\n * @param description Error description\n * @param redirectUri Optional redirect URI for the error\n * @returns HTML string\n */\nexport function renderErrorPage(\n\terror: string,\n\tdescription: string,\n\tredirectUri?: string\n): string {\n\tconst escapedError = escapeHtml(error);\n\tconst escapedDescription = escapeHtml(description);\n\n\tconst redirectHtml = redirectUri\n\t\t? `<p style=\"margin-top: 16px;\"><a href=\"${escapeHtml(redirectUri)}\" style=\"color: #60a5fa;\">Return to application</a></p>`\n\t\t: \"\";\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>Authorization Error</title>\n\t<style>\n\t\tbody {\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n\t\t\tbackground: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n\t\t\tmin-height: 100vh;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tpadding: 20px;\n\t\t\tcolor: #e0e0e0;\n\t\t\tmargin: 0;\n\t\t}\n\n\t\t.container {\n\t\t\tbackground: #1e1e30;\n\t\t\tborder-radius: 16px;\n\t\t\tpadding: 32px;\n\t\t\tmax-width: 400px;\n\t\t\twidth: 100%;\n\t\t\tbox-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.error-icon {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tbackground: rgba(239, 68, 68, 0.1);\n\t\t\tborder-radius: 50%;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tmargin: 0 auto 16px;\n\t\t\tfont-size: 32px;\n\t\t}\n\n\t\th1 {\n\t\t\tfont-size: 20px;\n\t\t\tmargin-bottom: 8px;\n\t\t\tcolor: #f87171;\n\t\t}\n\n\t\tp {\n\t\t\tcolor: #9ca3af;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\tcode {\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t\tpadding: 2px 6px;\n\t\t\tborder-radius: 4px;\n\t\t\tfont-size: 12px;\n\t\t}\n\t</style>\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<div class=\"error-icon\">!</div>\n\t\t<h1>Authorization Error</h1>\n\t\t<p>${escapedDescription}</p>\n\t\t<p style=\"margin-top: 8px;\"><code>${escapedError}</code></p>\n\t\t${redirectHtml}\n\t</div>\n</body>\n</html>`;\n}\n","/**\n * Client authentication for confidential clients using private_key_jwt\n * Implements RFC 7523 (JWT Bearer Client Authentication)\n */\n\nimport { jwtVerify, createRemoteJWKSet, importJWK, errors, 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, CONSENT_UI_CSP } from \"./ui.js\";\nimport { authenticateClient, ClientAuthError } from \"./client-auth.js\";\n\n/**\n * OAuth provider configuration\n */\nexport interface OAuthProviderConfig {\n\t/** OAuth storage implementation */\n\tstorage: OAuthStorage;\n\t/** The OAuth issuer URL (e.g., https://your-pds.com) */\n\tissuer: string;\n\t/** Whether DPoP is required for all tokens (default: true for AT Protocol) */\n\tdpopRequired?: boolean;\n\t/** Whether PAR is enabled (default: true) */\n\tenablePAR?: boolean;\n\t/** Client resolver for DID-based discovery */\n\tclientResolver?: ClientResolver;\n\t/** Callback to verify user credentials */\n\tverifyUser?: (password: string) => Promise<{ sub: string; handle: string } | null>;\n\t/** Get the current user (if already authenticated) */\n\tgetCurrentUser?: () => Promise<{ sub: string; handle: string } | null>;\n}\n\n/**\n * OAuth error response builder\n */\nfunction oauthError(error: string, description: string, status: number = 400): Response {\n\treturn new Response(\n\t\tJSON.stringify({\n\t\t\terror,\n\t\t\terror_description: description,\n\t\t}),\n\t\t{\n\t\t\tstatus,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t}\n\t);\n}\n\n/**\n * Error thrown when request body parsing fails\n */\nexport class RequestBodyError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"RequestBodyError\";\n\t}\n}\n\n/**\n * Parse request body from JSON or form-urlencoded\n * @throws RequestBodyError if content type is unsupported or parsing fails\n */\nexport async function parseRequestBody(request: Request): Promise<Record<string, string>> {\n\tconst contentType = request.headers.get(\"Content-Type\") ?? \"\";\n\n\ttry {\n\t\tif (contentType.includes(\"application/json\")) {\n\t\t\tconst json = await request.json();\n\t\t\treturn Object.fromEntries(\n\t\t\t\tObject.entries(json as Record<string, unknown>).map(([k, v]) => [k, String(v)])\n\t\t\t);\n\t\t} else if (contentType.includes(\"application/x-www-form-urlencoded\")) {\n\t\t\tconst body = await request.text();\n\t\t\treturn Object.fromEntries(new URLSearchParams(body).entries());\n\t\t} else {\n\t\t\tthrow new RequestBodyError(\n\t\t\t\t\"Content-Type must be application/json or application/x-www-form-urlencoded\"\n\t\t\t);\n\t\t}\n\t} catch (e) {\n\t\tif (e instanceof RequestBodyError) {\n\t\t\tthrow e;\n\t\t}\n\t\tthrow new RequestBodyError(\"Failed to parse request body\");\n\t}\n}\n\n/**\n * AT Protocol OAuth 2.1 Provider\n */\nexport class ATProtoOAuthProvider {\n\tprivate storage: OAuthStorage;\n\tprivate issuer: string;\n\tprivate dpopRequired: boolean;\n\tprivate enablePAR: boolean;\n\tprivate parHandler: PARHandler;\n\tprivate clientResolver: ClientResolver;\n\tprivate verifyUser?: (password: string) => Promise<{ sub: string; handle: string } | null>;\n\tprivate getCurrentUser?: () => Promise<{ sub: string; handle: string } | null>;\n\n\tconstructor(config: OAuthProviderConfig) {\n\t\tthis.storage = config.storage;\n\t\tthis.issuer = config.issuer;\n\t\tthis.dpopRequired = config.dpopRequired ?? true;\n\t\tthis.enablePAR = config.enablePAR ?? true;\n\t\tthis.parHandler = new PARHandler(config.storage, config.issuer);\n\t\tthis.clientResolver = config.clientResolver ?? new ClientResolver({ storage: config.storage });\n\t\tthis.verifyUser = config.verifyUser;\n\t\tthis.getCurrentUser = config.getCurrentUser;\n\t}\n\n\t/**\n\t * Handle authorization request (GET/POST /oauth/authorize)\n\t */\n\tasync handleAuthorize(request: Request): Promise<Response> {\n\t\tconst url = new URL(request.url);\n\n\t\t// Parse OAuth params from query string (GET) or form data (POST)\n\t\tlet params: Record<string, string>;\n\n\t\tif (request.method === \"POST\") {\n\t\t\t// POST: parse from form data (includes hidden fields with OAuth params)\n\t\t\tconst formData = await request.formData();\n\t\t\tparams = {};\n\t\t\tfor (const [key, value] of formData.entries()) {\n\t\t\t\tif (typeof value === \"string\") {\n\t\t\t\t\tparams[key] = value;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// GET: check for PAR or query params\n\t\t\tconst requestUri = url.searchParams.get(\"request_uri\");\n\t\t\tconst clientId = url.searchParams.get(\"client_id\");\n\n\t\t\tif (requestUri && this.enablePAR) {\n\t\t\t\tif (!clientId) {\n\t\t\t\t\treturn this.renderError(\"invalid_request\", \"client_id required with request_uri\");\n\t\t\t\t}\n\t\t\t\tconst parParams = await this.parHandler.retrieveParams(requestUri, clientId);\n\t\t\t\tif (!parParams) {\n\t\t\t\t\treturn this.renderError(\"invalid_request\", \"Invalid or expired request_uri\");\n\t\t\t\t}\n\t\t\t\tparams = parParams;\n\t\t\t} else if (this.enablePAR) {\n\t\t\t\t// PAR is required when enabled - reject direct authorization requests\n\t\t\t\treturn 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 this.renderError(\"invalid_request\", `Missing required parameter: ${param}`);\n\t\t\t}\n\t\t}\n\n\t\t// Validate response_type\n\t\tif (params.response_type !== \"code\") {\n\t\t\treturn this.renderError(\"unsupported_response_type\", \"Only response_type=code is supported\");\n\t\t}\n\n\t\t// Validate code_challenge_method\n\t\tif (params.code_challenge_method && params.code_challenge_method !== \"S256\") {\n\t\t\treturn this.renderError(\"invalid_request\", \"Only code_challenge_method=S256 is supported\");\n\t\t}\n\n\t\t// Resolve client metadata\n\t\tlet client: ClientMetadata;\n\t\ttry {\n\t\t\tclient = await this.clientResolver.resolveClient(params.client_id!);\n\t\t} catch (e) {\n\t\t\treturn this.renderError(\"invalid_client\", `Failed to resolve client: ${e}`);\n\t\t}\n\n\t\t// Validate redirect_uri\n\t\tif (!client.redirectUris.includes(params.redirect_uri!)) {\n\t\t\treturn this.renderError(\"invalid_request\", \"Invalid redirect_uri for this client\");\n\t\t}\n\n\t\t// Handle POST (form submission)\n\t\tif (request.method === \"POST\") {\n\t\t\treturn this.handleAuthorizePost(request, params, client);\n\t\t}\n\n\t\t// Check if user is authenticated\n\t\tlet user: { sub: string; handle: string } | null = null;\n\t\tif (this.getCurrentUser) {\n\t\t\tuser = await this.getCurrentUser();\n\t\t}\n\n\t\t// Show consent UI\n\t\tconst scope = params.scope ?? \"atproto\";\n\t\tconst html = renderConsentUI({\n\t\t\tclient,\n\t\t\tscope,\n\t\t\tauthorizeUrl: url.pathname,\n\t\t\tstate: params.state!,\n\t\t\toauthParams: params,\n\t\t\tuserHandle: user?.handle,\n\t\t\tshowLogin: !user && !!this.verifyUser,\n\t\t});\n\n\t\treturn new Response(html, {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Handle authorization form POST\n\t */\n\tprivate async handleAuthorizePost(\n\t\trequest: Request,\n\t\tparams: Record<string, string>,\n\t\tclient: ClientMetadata\n\t): Promise<Response> {\n\t\t// Form data was already parsed in handleAuthorize - extract action and password\n\t\tconst action = params.action;\n\t\tconst password = params.password ?? null;\n\n\t\tconst redirectUri = params.redirect_uri!;\n\t\tconst state = params.state!;\n\t\t// Default response_mode is \"query\" for authorization code flow per RFC 6749\n\t\tconst responseMode = params.response_mode ?? \"query\";\n\n\t\t// Handle deny\n\t\tif (action === \"deny\") {\n\t\t\tconst errorUrl = new URL(redirectUri);\n\n\t\t\tif (responseMode === \"fragment\") {\n\t\t\t\tconst hashParams = new URLSearchParams();\n\t\t\t\thashParams.set(\"error\", \"access_denied\");\n\t\t\t\thashParams.set(\"error_description\", \"User denied authorization\");\n\t\t\t\thashParams.set(\"state\", state);\n\t\t\t\thashParams.set(\"iss\", this.issuer);\n\t\t\t\terrorUrl.hash = hashParams.toString();\n\t\t\t} else {\n\t\t\t\terrorUrl.searchParams.set(\"error\", \"access_denied\");\n\t\t\t\terrorUrl.searchParams.set(\"error_description\", \"User denied authorization\");\n\t\t\t\terrorUrl.searchParams.set(\"state\", state);\n\t\t\t\terrorUrl.searchParams.set(\"iss\", this.issuer);\n\t\t\t}\n\n\t\t\treturn Response.redirect(errorUrl.toString(), 302);\n\t\t}\n\n\t\t// Get or verify user\n\t\tlet user: { sub: string; handle: string } | null = null;\n\n\t\tif (this.getCurrentUser) {\n\t\t\tuser = await this.getCurrentUser();\n\t\t}\n\n\t\tif (!user && password && this.verifyUser) {\n\t\t\tuser = await this.verifyUser(password);\n\t\t}\n\n\t\tif (!user) {\n\t\t\t// Show login form with error\n\t\t\tconst url = new URL(request.url);\n\t\t\tconst scope = params.scope ?? \"atproto\";\n\t\t\tconst html = renderConsentUI({\n\t\t\t\tclient,\n\t\t\t\tscope,\n\t\t\t\tauthorizeUrl: url.pathname,\n\t\t\t\tstate,\n\t\t\t\toauthParams: params,\n\t\t\t\tshowLogin: true,\n\t\t\t\terror: \"Invalid password\",\n\t\t\t});\n\t\t\treturn new Response(html, {\n\t\t\t\tstatus: 401,\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Generate authorization code\n\t\tconst code = generateAuthCode();\n\t\tconst scope = params.scope ?? \"atproto\";\n\n\t\tconst authCodeData: AuthCodeData = {\n\t\t\tclientId: params.client_id!,\n\t\t\tredirectUri,\n\t\t\tcodeChallenge: params.code_challenge!,\n\t\t\tcodeChallengeMethod: \"S256\",\n\t\t\tscope,\n\t\t\tsub: user.sub,\n\t\t\texpiresAt: Date.now() + AUTH_CODE_TTL,\n\t\t};\n\n\t\tawait this.storage.saveAuthCode(code, authCodeData);\n\n\t\t// Redirect with code (using fragment mode if requested)\n\t\tconst successUrl = new URL(redirectUri);\n\n\t\tif (responseMode === \"fragment\") {\n\t\t\t// Put params in hash fragment\n\t\t\tconst hashParams = new URLSearchParams();\n\t\t\thashParams.set(\"code\", code);\n\t\t\thashParams.set(\"state\", state);\n\t\t\thashParams.set(\"iss\", this.issuer);\n\t\t\tsuccessUrl.hash = hashParams.toString();\n\t\t} else {\n\t\t\t// Put params in query string\n\t\t\tsuccessUrl.searchParams.set(\"code\", code);\n\t\t\tsuccessUrl.searchParams.set(\"state\", state);\n\t\t\tsuccessUrl.searchParams.set(\"iss\", this.issuer);\n\t\t}\n\n\t\treturn Response.redirect(successUrl.toString(), 302);\n\t}\n\n\t/**\n\t * Handle token request (POST /oauth/token)\n\t */\n\tasync handleToken(request: Request): Promise<Response> {\n\t\tlet params: Record<string, string>;\n\t\ttry {\n\t\t\tparams = await parseRequestBody(request);\n\t\t} catch (e) {\n\t\t\treturn oauthError(\"invalid_request\", e instanceof Error ? e.message : \"Invalid request\");\n\t\t}\n\n\t\tconst grantType = params.grant_type;\n\n\t\tif (grantType === \"authorization_code\") {\n\t\t\treturn this.handleAuthorizationCodeGrant(request, params);\n\t\t} else if (grantType === \"refresh_token\") {\n\t\t\treturn this.handleRefreshTokenGrant(request, params);\n\t\t} else {\n\t\t\treturn oauthError(\"unsupported_grant_type\", `Unsupported grant_type: ${grantType}`);\n\t\t}\n\t}\n\n\t/**\n\t * Handle authorization code grant\n\t */\n\tprivate async handleAuthorizationCodeGrant(\n\t\trequest: Request,\n\t\tparams: Record<string, string>\n\t): Promise<Response> {\n\t\t// Validate required parameters\n\t\tconst required = [\"code\", \"client_id\", \"redirect_uri\", \"code_verifier\"];\n\t\tfor (const param of required) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn oauthError(\"invalid_request\", `Missing required parameter: ${param}`);\n\t\t\t}\n\t\t}\n\n\t\t// Authenticate client (validates private_key_jwt for confidential clients)\n\t\ttry {\n\t\t\tawait authenticateClient(\n\t\t\t\tparams,\n\t\t\t\tasync (clientId) => {\n\t\t\t\t\tif (this.clientResolver) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn await this.clientResolver.resolveClient(clientId);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn this.storage.getClient(clientId);\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttokenEndpoint: `${this.issuer}/oauth/token`,\n\t\t\t\t\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 * Render an error page\n\t */\n\tprivate renderError(error: string, description: string): Response {\n\t\tconst html = renderErrorPage(error, description);\n\t\treturn new Response(html, {\n\t\t\tstatus: 400,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n}\n","/**\n * OAuth storage interface and types\n * Defines the storage abstraction for auth codes, tokens, clients, etc.\n */\n\n/**\n * Data stored with an authorization code\n */\nexport interface AuthCodeData {\n\t/** Client DID that requested the code */\n\tclientId: string;\n\t/** Redirect URI used in the authorization request */\n\tredirectUri: string;\n\t/** PKCE code challenge */\n\tcodeChallenge: string;\n\t/** PKCE challenge method (always S256 for AT Protocol) */\n\tcodeChallengeMethod: \"S256\";\n\t/** Authorized scope */\n\tscope: string;\n\t/** User DID that authorized the request */\n\tsub: string;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n}\n\n/**\n * Data stored with access and refresh tokens\n */\nexport interface TokenData {\n\t/** Opaque access token */\n\taccessToken: string;\n\t/** Opaque refresh token */\n\trefreshToken: string;\n\t/** Client DID that received the token */\n\tclientId: string;\n\t/** User DID the token is for */\n\tsub: string;\n\t/** Authorized scope */\n\tscope: string;\n\t/** DPoP key thumbprint (for token binding) */\n\tdpopJkt?: string;\n\t/** Issuance timestamp (Unix ms) */\n\tissuedAt: number;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n\t/** Whether the token has been revoked */\n\trevoked?: boolean;\n}\n\n/**\n * JSON Web Key for client authentication\n */\nexport interface JWK {\n\tkty: string;\n\tuse?: string;\n\tkey_ops?: string[];\n\talg?: string;\n\tkid?: string;\n\t// EC key parameters\n\tcrv?: string;\n\tx?: string;\n\ty?: string;\n\t// RSA key parameters (not used for ATProto but included for completeness)\n\tn?: string;\n\te?: string;\n}\n\n/**\n * OAuth client metadata (discovered from DID document)\n */\nexport interface ClientMetadata {\n\t/** Client DID */\n\tclientId: string;\n\t/** Human-readable client name */\n\tclientName: string;\n\t/** Allowed redirect URIs */\n\tredirectUris: string[];\n\t/** Client logo URI (optional) */\n\tlogoUri?: string;\n\t/** Client homepage URI (optional) */\n\tclientUri?: string;\n\t/** Token endpoint auth method (\"none\" for public, \"private_key_jwt\" for confidential) */\n\ttokenEndpointAuthMethod?: \"none\" | \"private_key_jwt\";\n\t/** JSON Web Key Set for confidential client authentication */\n\tjwks?: { keys: JWK[] };\n\t/** URI to fetch JWKS from (alternative to inline jwks) */\n\tjwksUri?: string;\n\t/** When the metadata was cached (Unix ms) */\n\tcachedAt?: number;\n}\n\n/**\n * Data stored for Pushed Authorization Requests (PAR)\n */\nexport interface PARData {\n\t/** Client DID that pushed the request */\n\tclientId: string;\n\t/** All OAuth parameters from the push request */\n\tparams: Record<string, string>;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n}\n\n/**\n * Storage interface for OAuth data\n * Implementations should handle TTL-based expiration\n */\nexport interface OAuthStorage {\n\t// ============================================\n\t// Authorization Codes (5 min TTL)\n\t// ============================================\n\n\t/**\n\t * Save an authorization code\n\t * @param code The authorization code\n\t * @param data Associated data\n\t */\n\tsaveAuthCode(code: string, data: AuthCodeData): Promise<void>;\n\n\t/**\n\t * Get authorization code data\n\t * @param code The authorization code\n\t * @returns The data or null if not found/expired\n\t */\n\tgetAuthCode(code: string): Promise<AuthCodeData | null>;\n\n\t/**\n\t * Delete an authorization code (after use)\n\t * @param code The authorization code\n\t */\n\tdeleteAuthCode(code: string): Promise<void>;\n\n\t// ============================================\n\t// Tokens\n\t// ============================================\n\n\t/**\n\t * Save token data\n\t * @param data The token data\n\t */\n\tsaveTokens(data: TokenData): Promise<void>;\n\n\t/**\n\t * Get token data by access token\n\t * @param accessToken The access token\n\t * @returns The data or null if not found/expired/revoked\n\t */\n\tgetTokenByAccess(accessToken: string): Promise<TokenData | null>;\n\n\t/**\n\t * Get token data by refresh token\n\t * @param refreshToken The refresh token\n\t * @returns The data or null if not found/expired/revoked\n\t */\n\tgetTokenByRefresh(refreshToken: string): Promise<TokenData | null>;\n\n\t/**\n\t * Revoke a token by access token\n\t * @param accessToken The access token to revoke\n\t */\n\trevokeToken(accessToken: string): Promise<void>;\n\n\t/**\n\t * Revoke all tokens for a user (for logout)\n\t * @param sub The user DID\n\t */\n\trevokeAllTokens?(sub: string): Promise<void>;\n\n\t// ============================================\n\t// Clients (DID-based, cached)\n\t// ============================================\n\n\t/**\n\t * Save client metadata (cached from DID document)\n\t * @param clientId The client DID\n\t * @param metadata The client metadata\n\t */\n\tsaveClient(clientId: string, metadata: ClientMetadata): Promise<void>;\n\n\t/**\n\t * Get cached client metadata\n\t * @param clientId The client DID\n\t * @returns The metadata or null if not cached\n\t */\n\tgetClient(clientId: string): Promise<ClientMetadata | null>;\n\n\t// ============================================\n\t// PAR Requests (90 sec TTL)\n\t// ============================================\n\n\t/**\n\t * Save PAR request data\n\t * @param requestUri The unique request URI\n\t * @param data The PAR data\n\t */\n\tsavePAR(requestUri: string, data: PARData): Promise<void>;\n\n\t/**\n\t * Get PAR request data\n\t * @param requestUri The request URI\n\t * @returns The data or null if not found/expired\n\t */\n\tgetPAR(requestUri: string): Promise<PARData | null>;\n\n\t/**\n\t * Delete PAR request (after use - one-time use)\n\t * @param requestUri The request URI\n\t */\n\tdeletePAR(requestUri: string): Promise<void>;\n\n\t// ============================================\n\t// DPoP Nonces (5 min TTL, replay prevention)\n\t// ============================================\n\n\t/**\n\t * Check if a nonce has been used and save it if not\n\t * Used for DPoP replay prevention\n\t * @param nonce The nonce to check\n\t * @returns true if the nonce is new (valid), false if already used\n\t */\n\tcheckAndSaveNonce(nonce: string): Promise<boolean>;\n}\n\n/**\n * In-memory storage implementation for testing\n */\nexport class InMemoryOAuthStorage implements OAuthStorage {\n\tprivate authCodes = new Map<string, AuthCodeData>();\n\tprivate tokens = new Map<string, TokenData>();\n\tprivate refreshTokenIndex = new Map<string, string>(); // refreshToken -> accessToken\n\tprivate clients = new Map<string, ClientMetadata>();\n\tprivate parRequests = new Map<string, PARData>();\n\tprivate nonces = new Set<string>();\n\n\tasync saveAuthCode(code: string, data: AuthCodeData): Promise<void> {\n\t\tthis.authCodes.set(code, data);\n\t}\n\n\tasync getAuthCode(code: string): Promise<AuthCodeData | null> {\n\t\tconst data = this.authCodes.get(code);\n\t\tif (!data) return null;\n\t\tif (Date.now() > data.expiresAt) {\n\t\t\tthis.authCodes.delete(code);\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync deleteAuthCode(code: string): Promise<void> {\n\t\tthis.authCodes.delete(code);\n\t}\n\n\tasync saveTokens(data: TokenData): Promise<void> {\n\t\tthis.tokens.set(data.accessToken, data);\n\t\tthis.refreshTokenIndex.set(data.refreshToken, data.accessToken);\n\t}\n\n\tasync getTokenByAccess(accessToken: string): Promise<TokenData | null> {\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (!data) return null;\n\t\tif (data.revoked || Date.now() > data.expiresAt) {\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync getTokenByRefresh(refreshToken: string): Promise<TokenData | null> {\n\t\tconst accessToken = this.refreshTokenIndex.get(refreshToken);\n\t\tif (!accessToken) return null;\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (!data) return null;\n\t\tif (data.revoked) return null;\n\t\t// Refresh tokens don't use accessToken expiresAt\n\t\treturn data;\n\t}\n\n\tasync revokeToken(accessToken: string): Promise<void> {\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (data) {\n\t\t\tdata.revoked = true;\n\t\t}\n\t}\n\n\tasync revokeAllTokens(sub: string): Promise<void> {\n\t\tfor (const [, data] of this.tokens) {\n\t\t\tif (data.sub === sub) {\n\t\t\t\tdata.revoked = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync saveClient(clientId: string, metadata: ClientMetadata): Promise<void> {\n\t\tthis.clients.set(clientId, metadata);\n\t}\n\n\tasync getClient(clientId: string): Promise<ClientMetadata | null> {\n\t\treturn this.clients.get(clientId) ?? null;\n\t}\n\n\tasync savePAR(requestUri: string, data: PARData): Promise<void> {\n\t\tthis.parRequests.set(requestUri, data);\n\t}\n\n\tasync getPAR(requestUri: string): Promise<PARData | null> {\n\t\tconst data = this.parRequests.get(requestUri);\n\t\tif (!data) return null;\n\t\tif (Date.now() > data.expiresAt) {\n\t\t\tthis.parRequests.delete(requestUri);\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync deletePAR(requestUri: string): Promise<void> {\n\t\tthis.parRequests.delete(requestUri);\n\t}\n\n\tasync checkAndSaveNonce(nonce: string): Promise<boolean> {\n\t\tif (this.nonces.has(nonce)) {\n\t\t\treturn false;\n\t\t}\n\t\tthis.nonces.add(nonce);\n\t\t// Note: No auto-cleanup in test implementation - use clear() between tests\n\t\t// Production SQLite storage handles TTL-based cleanup properly\n\t\treturn true;\n\t}\n\n\t/** Clear all stored data (for testing) */\n\tclear(): void {\n\t\tthis.authCodes.clear();\n\t\tthis.tokens.clear();\n\t\tthis.refreshTokenIndex.clear();\n\t\tthis.clients.clear();\n\t\tthis.parRequests.clear();\n\t\tthis.nonces.clear();\n\t}\n}\n"],"mappings":"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"}
|
|
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":";;;;;;;;;;;;;AAWA,eAAe,sBAAsB,UAAmC;CAEvE,MAAM,OADU,IAAI,aAAa,CACZ,OAAO,SAAS;CACrC,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,KAAK;AACxD,QAAO,UAAU,OAAO,IAAI,WAAW,KAAK,CAAC;;;;;;;;;AAU9C,eAAsB,oBACrB,UACA,WACA,QACmB;AACnB,KAAI,WAAW,OACd,OAAM,IAAI,MAAM,0CAA0C;AAK3D,KAAI,SAAS,SAAS,MAAM,SAAS,SAAS,IAC7C,QAAO;AAER,KAAI,CAAC,qBAAqB,KAAK,SAAS,CACvC,QAAO;AAIR,QAD0B,MAAM,sBAAsB,SAAS,KAClC;;;;;;;;;;;;;;AChC9B,SAAgB,aAAa,aAAqB,IAAY;CAC7D,MAAM,SAAS,IAAI,WAAW,WAAW;AACzC,QAAO,gBAAgB,OAAO;AAC9B,QAAO,UAAU,OAAO,OAAO;;;;;;;;;ACNhC,MAAM,EAAE,2BAAc;;;;AAqCtB,IAAa,YAAb,cAA+B,MAAM;CACpC,AAAS;CACT,YAAY,SAAiB,MAAc,SAAwB;AAClE,QAAM,SAAS,QAAQ;AACvB,OAAK,OAAO;AACZ,OAAK,OAAO;;;;;;;AAQd,SAAS,gBAAgB,KAAkB;AAC1C,QAAO,IAAI,SAAS,IAAI;;;;;AAMzB,SAAS,SAAS,KAAqB;CACtC,IAAIA;AACJ,KAAI;AACH,QAAM,IAAI,IAAI,IAAI;SACX;AACP,QAAM,IAAI,UAAU,mCAAiC,eAAe;;AAGrE,KAAI,IAAI,YAAY,IAAI,SACvB,OAAM,IAAI,UAAU,6CAA2C,eAAe;AAG/E,KAAI,IAAI,aAAa,WAAW,IAAI,aAAa,SAChD,OAAM,IAAI,UAAU,sCAAoC,eAAe;AAGxE,QAAO,gBAAgB,IAAI;;;;;;;;;;AAW5B,eAAsB,gBACrB,SACA,UAA6B,EAAE,EACV;CACrB,MAAM,EAAE,oBAAoB,CAAC,QAAQ,EAAE,aAAa,eAAe,cAAc,OAAO;CAExF,MAAM,aAAa,QAAQ,QAAQ,IAAI,OAAO;AAC9C,KAAI,CAAC,WACJ,OAAM,IAAI,UAAU,uBAAuB,eAAe;CAG3D,IAAIC;CACJ,IAAIC;AASJ,KAAI;EACH,MAAM,SAAS,MAAM,UAAU,YAAY,aAAa;GACvD,KAAK;GACL,YAAY;GACZ;GACA,gBAAgB;GAChB,CAAC;AACF,oBAAkB,OAAO;AACzB,YAAU,OAAO;UACT,KAAK;AACb,MAAI,eAAeC,YAClB,OAAM,IAAI,UAAU,6BAA6B,IAAI,WAAW,gBAAgB,EAAE,OAAO,KAAK,CAAC;AAEhG,QAAM,IAAI,UAAU,4BAA4B,gBAAgB,EAAE,OAAO,KAAK,CAAC;;AAGhF,KAAI,CAAC,QAAQ,OAAO,OAAO,QAAQ,QAAQ,SAC1C,OAAM,IAAI,UAAU,wBAAsB,eAAe;AAG1D,KAAI,CAAC,QAAQ,OAAO,OAAO,QAAQ,QAAQ,SAC1C,OAAM,IAAI,UAAU,wBAAsB,eAAe;AAG1D,KAAI,CAAC,QAAQ,OAAO,OAAO,QAAQ,QAAQ,SAC1C,OAAM,IAAI,UAAU,wBAAsB,eAAe;AAG1D,KAAI,QAAQ,QAAQ,QAAQ,OAC3B,OAAM,IAAI,UAAU,yBAAuB,eAAe;CAI3D,MAAM,cAAc,gBADD,IAAI,IAAI,QAAQ,IAAI,CACQ;AAE/C,KADiB,SAAS,QAAQ,IAAI,KACrB,YAChB,OAAM,IAAI,UAAU,yBAAuB,eAAe;AAG3D,KAAI,kBAAkB,UAAa,QAAQ,UAAU,cACpD,OAAM,IAAI,UAAU,2BAAyB,iBAAiB;AAI/D,KAAI,aAAa;AAChB,MAAI,CAAC,QAAQ,IACZ,OAAM,IAAI,UAAU,mDAAiD,eAAe;EAGrF,MAAM,YAAY,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI,aAAa,CAAC,OAAO,YAAY,CAAC;EAC9F,MAAM,cAAc,UAAU,OAAO,IAAI,WAAW,UAAU,CAAC;AAE/D,MAAI,QAAQ,QAAQ,YACnB,OAAM,IAAI,UAAU,yBAAuB,eAAe;YAEjD,QAAQ,QAAQ,OAC1B,OAAM,IAAI,UAAU,uDAAqD,eAAe;CAGzF,MAAM,MAAM,gBAAgB;CAC5B,MAAM,MAAM,MAAM,uBAAuB,KAAK,SAAS;AAEvD,QAAO,OAAO,OAAO;EACpB,KAAK,QAAQ;EACb,KAAK,QAAQ;EACb,KAAK,QAAQ;EACb,KAAK,QAAQ;EACb;EACA;EACA,CAAC;;;;;;AAOH,SAAgB,oBAA4B;AAC3C,QAAO,aAAa,GAAG;;;;;;AClLxB,MAAM,qBAAqB;;AAG3B,MAAM,qBAAqB;;;;AAa3B,SAAS,qBAA6B;AACrC,QAAO,qBAAqB,aAAa,GAAG;;;;;AAM7C,MAAM,kBAAkB;CAAC;CAAa;CAAgB;CAAiB;CAAkB;CAAyB;CAAQ;;;;AAK1H,IAAa,aAAb,MAAwB;CACvB,AAAQ;CACR,AAAQ;CACR,AAAQ;;;;;;;CAQR,YAAY,SAAuB,QAAgB,YAAoB,oBAAoB;AAC1F,OAAK,UAAU;AACf,OAAK,SAAS;AACd,OAAK,YAAY;;;;;;;;CASlB,MAAM,kBAAkB,SAAqC;EAC5D,IAAIC;AACJ,MAAI;AACH,YAAS,MAAM,iBAAiB,QAAQ;WAChC,GAAG;AACX,UAAO,KAAK,cACX,mBACA,aAAa,QAAQ,EAAE,UAAU,mBACjC,IACA;;EAGF,MAAM,WAAW,OAAO;AACxB,MAAI,CAAC,SACJ,QAAO,KAAK,cAAc,mBAAmB,+BAA+B,IAAI;AAGjF,OAAK,MAAM,SAAS,gBACnB,KAAI,CAAC,OAAO,OACX,QAAO,KAAK,cAAc,mBAAmB,+BAA+B,SAAS,IAAI;AAI3F,MAAI,OAAO,kBAAkB,OAC5B,QAAO,KAAK,cACX,6BACA,wCACA,IACA;AAGF,MAAI,OAAO,0BAA0B,OACpC,QAAO,KAAK,cACX,mBACA,gDACA,IACA;EAGF,MAAM,gBAAgB,OAAO;AAC7B,MAAI,CAAC,sBAAsB,KAAK,cAAc,CAC7C,QAAO,KAAK,cACX,mBACA,iCACA,IACA;AAGF,MAAI;AACH,OAAI,IAAI,OAAO,aAAc;UACtB;AACP,UAAO,KAAK,cAAc,mBAAmB,wBAAwB,IAAI;;EAG1E,MAAM,aAAa,oBAAoB;EACvC,MAAM,YAAY,KAAK,KAAK,GAAG,KAAK,YAAY;EAEhD,MAAMC,UAAmB;GACxB;GACA;GACA;GACA;AAED,QAAM,KAAK,QAAQ,QAAQ,YAAY,QAAQ;EAE/C,MAAMC,WAA6B;GAClC,aAAa;GACb,YAAY,KAAK;GACjB;AAED,SAAO,IAAI,SAAS,KAAK,UAAU,SAAS,EAAE;GAC7C,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,iBAAiB;IACjB;GACD,CAAC;;;;;;;;;CAUH,MAAM,eACL,YACA,UACyC;AACzC,MAAI,CAAC,WAAW,WAAW,mBAAmB,CAC7C,QAAO;EAGR,MAAM,UAAU,MAAM,KAAK,QAAQ,OAAO,WAAW;AACrD,MAAI,CAAC,QACJ,QAAO;AAGR,MAAI,QAAQ,aAAa,SACxB,QAAO;AAIR,QAAM,KAAK,QAAQ,UAAU,WAAW;AAExC,SAAO,QAAQ;;;;;CAMhB,OAAO,aAAa,OAAwB;AAC3C,SAAO,MAAM,WAAW,mBAAmB;;;;;CAM5C,AAAQ,cACP,OACA,aACA,SAAiB,KACN;EACX,MAAMC,OAA2B;GAChC;GACA,mBAAmB;GACnB;AACD,SAAO,IAAI,SAAS,KAAK,UAAU,KAAK,EAAE;GACzC;GACA,SAAS;IACR,gBAAgB;IAChB,iBAAiB;IACjB;GACD,CAAC;;;;;;;;;;;;;ACrLJ,IAAa,wBAAb,cAA2C,MAAM;CAChD,YACC,SACA,AAAgBC,MACf;AACD,QAAM,QAAQ;EAFE;AAGhB,OAAK,OAAO;;;;;;AAmBd,SAAS,WAAW,OAAwB;AAC3C,KAAI;AAEH,SADY,IAAI,IAAI,MAAM,CACf,aAAa;SACjB;AACP,SAAO;;;;;;AAOT,SAAS,WAAW,OAAwB;AAC3C,KAAI;AACH,iBAAe,MAAM;AACrB,SAAO;SACA;AACP,SAAO;;;;;;;AAQT,SAAS,qBAAqB,UAAiC;AAE9D,KAAI,WAAW,SAAS,CACvB,QAAO;AAIR,KAAI,SAAS,WAAW,WAAW,EAAE;EAGpC,MAAM,QAAQ,SAAS,MAAM,EAAE,CAAC,MAAM,IAAI;EAC1C,MAAM,OAAO,MAAM,GAAI,QAAQ,QAAQ,IAAI;EAC3C,MAAM,OAAO,MAAM,MAAM,EAAE,CAAC,KAAK,IAAI;AAErC,SAAO,GADS,WAAW,OAAO,OAAO,MAAM,OAAO,KACpC;;AAInB,QAAO;;;;;AAMR,IAAa,iBAAb,MAA4B;CAC3B,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,YAAY,UAAiC,EAAE,EAAE;AAChD,OAAK,UAAU,QAAQ;AACvB,OAAK,WAAW,QAAQ,YAAY,OAAU;AAC9C,OAAK,UAAU,QAAQ,SAAS,WAAW,MAAM,KAAK,WAAW;;;;;;;;CASlE,MAAM,cAAc,UAA2C;AAC9D,MAAI,CAAC,WAAW,SAAS,IAAI,CAAC,WAAW,SAAS,CACjD,OAAM,IAAI,sBACT,6BAA6B,YAC7B,iBACA;AAGF,MAAI,KAAK,SAAS;GACjB,MAAM,SAAS,MAAM,KAAK,QAAQ,UAAU,SAAS;AAGrD,OAAI,UAAU,OAAO,YACpB,KAAK,KAAK,GAAG,OAAO,WAAW,KAAK,YACpC,OAAO,4BAA4B,OACnC,QAAO;;EAIT,MAAM,cAAc,qBAAqB,SAAS;AAClD,MAAI,CAAC,YACJ,OAAM,IAAI,sBACT,iCAAiC,YACjC,iBACA;EAGF,IAAIC;AACJ,MAAI;AACH,cAAW,MAAM,KAAK,QAAQ,aAAa,EAC1C,SAAS,EACR,QAAQ,oBACR,EACD,CAAC;WACM,GAAG;AACX,SAAM,IAAI,sBACT,oCAAoC,KACpC,iBACA;;AAGF,MAAI,CAAC,SAAS,GACb,OAAM,IAAI,sBACT,4CAA4C,SAAS,UACrD,iBACA;EAGF,IAAIC;AACJ,MAAI;GACH,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,SAAM,0BAA0B,MAAM,KAAK;WACnC,GAAG;AACX,SAAM,IAAI,sBACT,4BAA4B,aAAa,QAAQ,EAAE,UAAU,uBAC7D,iBACA;;AAGF,MAAI,IAAI,cAAc,SACrB,OAAM,IAAI,sBACT,gCAAgC,SAAS,QAAQ,IAAI,aACrD,iBACA;EAGF,MAAMC,WAA2B;GAChC,UAAU,IAAI;GACd,YAAY,IAAI,eAAe;GAC/B,cAAc,IAAI;GAClB,SAAS,IAAI;GACb,WAAW,IAAI;GACf,yBAA0B,IAAI,8BAA6D;GAC3F,MAAM,IAAI;GACV,SAAS,IAAI;GACb,UAAU,KAAK,KAAK;GACpB;AAED,MAAI,KAAK,QACR,OAAM,KAAK,QAAQ,WAAW,UAAU,SAAS;AAGlD,SAAO;;;;;;;;CASR,MAAM,oBAAoB,UAAkB,aAAuC;AAClF,MAAI;AAEH,WADiB,MAAM,KAAK,cAAc,SAAS,EACnC,aAAa,SAAS,YAAY;UAC3C;AACP,UAAO;;;;;;;AAQV,SAAgB,qBAAqB,UAAiC,EAAE,EAAkB;AACzF,QAAO,IAAI,eAAe,QAAQ;;;;;;AC3MnC,MAAa,mBAAmB,OAAU;;AAG1C,MAAa,oBAAoB,OAAU,KAAK,KAAK;;AAGrD,MAAa,gBAAgB,MAAS;;;;;;AAOtC,SAAgB,oBAAoB,QAAgB,IAAY;AAC/D,QAAO,aAAa,MAAM;;;;;;AAO3B,SAAgB,mBAA2B;AAC1C,QAAO,oBAAoB,GAAG;;;;;;;;AA6C/B,SAAgB,eAAe,SAG7B;CACD,MAAM,EACL,KACA,UACA,OACA,SACA,iBAAiB,qBACd;CAEJ,MAAM,cAAc,oBAAoB,GAAG;CAC3C,MAAM,eAAe,oBAAoB,GAAG;CAC5C,MAAM,MAAM,KAAK,KAAK;CAEtB,MAAMC,YAAuB;EAC5B;EACA;EACA;EACA;EACA;EACA;EACA,UAAU;EACV,WAAW,MAAM;EACjB,SAAS;EACT;AAWD,QAAO;EAAE,QATuB;GAC/B;GACA;GACA,WAAW,UAAU,SAAS;GAC9B,WAAW,KAAK,MAAM,iBAAiB,IAAK;GAC5C;GACA;GACA;EAEgB;EAAW;;;;;;;;;AAU7B,SAAgB,cACf,cACA,qBAA8B,OAC9B,iBAAyB,kBAIxB;CACD,MAAM,cAAc,oBAAoB,GAAG;CAC3C,MAAM,eAAe,qBAAqB,oBAAoB,GAAG,GAAG,aAAa;CACjF,MAAM,MAAM,KAAK,KAAK;CAEtB,MAAMA,YAAuB;EAC5B,GAAG;EACH;EACA;EACA,UAAU;EACV,WAAW,MAAM;EACjB;AAWD,QAAO;EAAE,QATuB;GAC/B;GACA;GACA,WAAW,aAAa,UAAU,SAAS;GAC3C,WAAW,KAAK,MAAM,iBAAiB,IAAK;GAC5C,OAAO,aAAa;GACpB,KAAK,aAAa;GAClB;EAEgB;EAAW;;;;;;;AAQ7B,SAAgB,mBAAmB,QAA6C;AAC/E,QAAO;EACN,cAAc,OAAO;EACrB,YAAY,OAAO;EACnB,YAAY,OAAO;EACnB,eAAe,OAAO;EACtB,OAAO,OAAO;EACd,KAAK,OAAO;EACZ;;;;;;;;AASF,SAAgB,mBACf,SACoD;CACpD,MAAM,aAAa,QAAQ,QAAQ,IAAI,gBAAgB;AACvD,KAAI,CAAC,WACJ,QAAO;AAGR,KAAI,WAAW,WAAW,UAAU,CACnC,QAAO;EACN,OAAO,WAAW,MAAM,EAAE;EAC1B,MAAM;EACN;AAGF,KAAI,WAAW,WAAW,QAAQ,CACjC,QAAO;EACN,OAAO,WAAW,MAAM,EAAE;EAC1B,MAAM;EACN;AAGF,QAAO;;;;;;;AAQR,SAAgB,aAAa,WAA+B;AAC3D,KAAI,UAAU,QACb,QAAO;AAER,KAAI,KAAK,KAAK,GAAG,UAAU,UAC1B,QAAO;AAER,QAAO;;;;;;;;;AC5MR,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4H5B,eAAe,kBAAkB,QAAiC;CAEjE,MAAM,OADU,IAAI,aAAa,CACZ,OAAO,OAAO;CACnC,MAAM,aAAa,MAAM,OAAO,OAAO,OAAO,WAAW,KAAK;CAC9D,MAAM,YAAY,MAAM,KAAK,IAAI,WAAW,WAAW,CAAC;AAExD,QAAO,WADY,KAAK,OAAO,aAAa,GAAG,UAAU,CAAC,CAC7B;;AAI9B,IAAIC,+BAAuD;;;;AAK3D,eAAsB,2BAA4C;AACjE,KAAI,CAAC,6BACJ,gCAA+B,kBAAkB,oBAAoB;AAEtE,QAAO;;;;;;;;;;;;;;;;;;AAmBR,eAAsB,gBAAgB,sBAAgD;AAIrF,QAAO,kCAHW,uBACf,MAAM,0BAA0B,GAChC,SACgD;;;;;AAMpD,SAAS,WAAW,MAAsB;AACzC,QAAO,KACL,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,SAAS,CACvB,QAAQ,MAAM,SAAS;;;;;AAO1B,SAAS,qBAAqB,OAAyB;CACtD,MAAM,SAAS,MAAM,MAAM,IAAI,CAAC,OAAO,QAAQ;CAC/C,MAAMC,eAAyB,EAAE;AAEjC,MAAK,MAAM,KAAK,OACf,SAAQ,GAAR;EACC,KAAK;AACJ,gBAAa,KAAK,kCAAkC;AACpD;EACD,KAAK;AACJ,gBAAa,KAAK,6BAA6B;AAC/C;EACD,KAAK;AACJ,gBAAa,KAAK,4BAA4B;AAC9C;EACD,QAEC;;AAKH,KAAI,aAAa,WAAW,EAC3B,cAAa,KAAK,qCAAqC;AAGxD,QAAO;;;;;;;AAkCR,SAAgB,gBAAgB,SAAmC;CAClE,MAAM,EAAE,QAAQ,OAAO,cAAc,aAAa,YAAY,WAAW,OAAO,kBAAkB,mBAAmB;CAErH,MAAM,aAAa,WAAW,OAAO,WAAW;CAChD,MAAM,oBAAoB,qBAAqB,MAAM;CACrD,MAAM,WAAW,OAAO,UACrB,aAAa,WAAW,OAAO,QAAQ,CAAC,SAAS,WAAW,8BAC5D,qCAAqC,WAAW,OAAO,EAAE,CAAC,aAAa,CAAC;CAE3E,MAAM,YAAY,QACf,8BAA8B,WAAW,MAAM,CAAC,UAChD;CAEH,MAAM,gBAAgB,YACnB;;;MAGE,mBAAmB;;;;;;QAMjB,GAAG;;;MAIP;CAGH,MAAM,mBAAmB,OAAO,QAAQ,YAAY,CAClD,KAAK,CAAC,KAAK,WAAW,8BAA8B,WAAW,IAAI,CAAC,WAAW,WAAW,MAAM,CAAC,MAAM,CACvG,KAAK,QAAW;AAElB,QAAO;;;;;oBAKY,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA2Q1B,SAAS;6CAC+B,WAAW;KACnD,aAAa,4BAA4B,WAAW,WAAW,CAAC,QAAQ,GAAG;KAC3E,OAAO,YAAY,kCAAkC,WAAW,OAAO,UAAU,CAAC,mCAAmC,WAAW,IAAI,IAAI,OAAO,UAAU,CAAC,SAAS,CAAC,YAAY,GAAG;;;IAGpL,UAAU;;gCAEkB,WAAW,aAAa,CAAC;KACpD,iBAAiB;;KAEjB,cAAc;;;;;OAKZ,kBAAkB,KAAK,SAAS,OAAO,WAAW,KAAK,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC;;;;;;;;;;;;GAY7E,oBAAoB,iBAAiB;iCACP,WAAW,KAAK,UAAU,eAAe,CAAC,CAAC,uBAAuB,WAAW,KAAK,UAAU,YAAY,CAAC,CAAC,IAAI,oBAAoB;KAC9J,GAAG;;;;;;;;;;;AAYR,SAAgB,gBACf,OACA,aACA,aACS;CACT,MAAM,eAAe,WAAW,MAAM;AAOtC,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OANoB,WAAW,YAAY,CAuEzB;sCACY,aAAa;IAtE7B,cAClB,yCAAyC,WAAW,YAAY,CAAC,2DACjE,GAqEa;;;;;;;;;;;;AClqBjB,MAAM,EAAE,cAAc;;AAGtB,MAAa,4BAA4B;;;;AAKzC,IAAa,kBAAb,cAAqC,MAAM;CAC1C,YACC,SACA,AAAgBC,MACf;AACD,QAAM,QAAQ;EAFE;AAGhB,OAAK,OAAO;;;;;;AA+Bd,SAAgB,qBAAqB,QAGnC;AACD,QAAO;EACN,eAAe,OAAO;EACtB,WAAW,OAAO;EAClB;;;;;;;;;;AAWF,eAAsB,sBACrB,WACA,QACA,SACsB;CACtB,MAAM,EAAE,eAAe,QAAQ,OAAO,UAAU,WAAW,MAAM,KAAK,WAAW,EAAE,aAAa;CAGhG,IAAIC;AAEJ,KAAI,OAAO,QAAQ,OAAO,KAAK,KAAK,SAAS,EAE5C,eAAc,OAAO,WAAW;EAC/B,MAAM,OAAO,OAAO,KAAM;EAE1B,IAAIC;AACJ,MAAI,OAAO,IACV,OAAM,KAAK,MAAM,MAAM,EAAE,QAAQ,OAAO,IAAI;AAE7C,MAAI,CAAC,IACJ,OAAM,KAAK,MAAM,MAAM,CAAC,EAAE,OAAO,EAAE,QAAQ,OAAO,IAAI;AAEvD,MAAI,CAAC,IACJ,OAAM,KAAK;AAEZ,MAAI,CAAC,IACJ,OAAM,IAAI,gBAAgB,wCAAwC,iBAAiB;EAGpF,MAAM,MAAM,IAAI,OAAO,OAAO;AAC9B,SAAO,UAAU,KAAwC,IAAI;;UAEpD,OAAO,QAEjB,eAAc,mBAAmB,IAAI,IAAI,OAAO,QAAQ,EAAE,GACxD,cAAc,SACf,CAAC;KAEF,OAAM,IAAI,gBAAgB,iCAAiC,iBAAiB;CAG7E,IAAIC;AACJ,KAAI;AAMH,aALe,MAAM,UAAU,WAAW,aAAa;GACtD,YAAY,CAAC,QAAQ;GACrB,gBAAgB;GAChB,aAAa;GACb,CAAC,EACe;UACT,KAAK;AACb,MAAI,eAAe,UAClB,OAAM,IAAI,gBAAgB,4BAA4B,IAAI,WAAW,iBAAiB;AAEvF,QAAM,IAAI,gBACT,4BAA4B,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,IAC5E,iBACA;;AAMF,KAAI,QAAQ,QAAQ,OAAO,SAC1B,OAAM,IAAI,gBACT,iCAAiC,OAAO,SAAS,QAAQ,QAAQ,OACjE,iBACA;AAIF,KAAI,QAAQ,QAAQ,OAAO,SAC1B,OAAM,IAAI,gBACT,kCAAkC,OAAO,SAAS,QAAQ,QAAQ,OAClE,iBACA;CAKF,MAAM,MAAM,MAAM,QAAQ,QAAQ,IAAI,GAAG,QAAQ,MAAM,QAAQ,MAAM,CAAC,QAAQ,IAAI,GAAG,EAAE;AACvF,KAAI,CAAC,IAAI,SAAS,cAAc,IAAI,CAAC,IAAI,SAAS,OAAO,CACxD,OAAM,IAAI,gBACT,6CAA6C,cAAc,eAAe,OAAO,IACjF,iBACA;AAIF,KAAI,CAAC,QAAQ,IACZ,OAAM,IAAI,gBAAgB,8BAA8B,iBAAiB;AAI1E,KAAI,UAEH;MAAI,CADa,MAAM,SAAS,QAAQ,IAAI,CAE3C,OAAM,IAAI,gBAAgB,+CAA+C,iBAAiB;;AAK5F,KAAI,CAAC,QAAQ,IACZ,OAAM,IAAI,gBAAgB,8BAA8B,iBAAiB;AAG1E,QAAO;;;;;;;;;;AAWR,eAAsB,mBACrB,QACA,WACA,SAC4B;CAC5B,MAAM,WAAW,OAAO;AACxB,KAAI,CAAC,SACJ,OAAM,IAAI,gBAAgB,qBAAqB,kBAAkB;CAGlE,MAAM,EAAE,eAAe,cAAc,qBAAqB,OAAO;CAGjE,MAAM,SAAS,MAAM,UAAU,SAAS;AACxC,KAAI,CAAC,OACJ,OAAM,IAAI,gBAAgB,mBAAmB,YAAY,iBAAiB;CAG3E,MAAM,aAAa,OAAO,2BAA2B;AAGrD,KAAI,eAAe,QAAQ;AAE1B,MAAI,aAAa,cAChB,OAAM,IAAI,gBACT,mDACA,kBACA;AAEF,SAAO;GAAE,eAAe;GAAO;GAAU;;AAI1C,KAAI,eAAe,mBAAmB;AACrC,MAAI,CAAC,iBAAiB,CAAC,UACtB,OAAM,IAAI,gBACT,qDACA,iBACA;AAGF,MAAI,kBAAkB,0BACrB,OAAM,IAAI,gBACT,+BAA+B,cAAc,cAAc,6BAC3D,iBACA;AAIF,QAAM,sBAAsB,WAAW,QAAQ,QAAQ;AAEvD,SAAO;GAAE,eAAe;GAAM;GAAU;;AAGzC,OAAM,IAAI,gBAAgB,4BAA4B,cAAc,iBAAiB;;;;;;;;AChMtF,SAAS,WAAW,OAAe,aAAqB,SAAiB,KAAe;AACvF,QAAO,IAAI,SACV,KAAK,UAAU;EACd;EACA,mBAAmB;EACnB,CAAC,EACF;EACC;EACA,SAAS;GACR,gBAAgB;GAChB,iBAAiB;GACjB;EACD,CACD;;;;;AAMF,IAAa,mBAAb,cAAsC,MAAM;CAC3C,YAAY,SAAiB;AAC5B,QAAM,QAAQ;AACd,OAAK,OAAO;;;;;;;AAQd,eAAsB,iBAAiB,SAAmD;CACzF,MAAM,cAAc,QAAQ,QAAQ,IAAI,eAAe,IAAI;AAE3D,KAAI;AACH,MAAI,YAAY,SAAS,mBAAmB,EAAE;GAC7C,MAAM,OAAO,MAAM,QAAQ,MAAM;AACjC,UAAO,OAAO,YACb,OAAO,QAAQ,KAAgC,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,GAAG,OAAO,EAAE,CAAC,CAAC,CAC/E;aACS,YAAY,SAAS,oCAAoC,EAAE;GACrE,MAAM,OAAO,MAAM,QAAQ,MAAM;AACjC,UAAO,OAAO,YAAY,IAAI,gBAAgB,KAAK,CAAC,SAAS,CAAC;QAE9D,OAAM,IAAI,iBACT,6EACA;UAEM,GAAG;AACX,MAAI,aAAa,iBAChB,OAAM;AAEP,QAAM,IAAI,iBAAiB,+BAA+B;;;;;;AAO5D,IAAa,uBAAb,MAAkC;CACjC,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,YAAY,QAA6B;AACxC,OAAK,UAAU,OAAO;AACtB,OAAK,SAAS,OAAO;AACrB,OAAK,eAAe,OAAO,gBAAgB;AAC3C,OAAK,YAAY,OAAO,aAAa;AACrC,OAAK,aAAa,IAAI,WAAW,OAAO,SAAS,OAAO,OAAO;AAC/D,OAAK,iBAAiB,OAAO,kBAAkB,IAAI,eAAe,EAAE,SAAS,OAAO,SAAS,CAAC;AAC9F,OAAK,aAAa,OAAO;AACzB,OAAK,iBAAiB,OAAO;AAC7B,OAAK,oBAAoB,OAAO;AAChC,OAAK,gBAAgB,OAAO;;;;;CAM7B,MAAM,gBAAgB,SAAqC;EAC1D,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI;EAGhC,IAAIC;AAEJ,MAAI,QAAQ,WAAW,QAAQ;GAE9B,MAAM,WAAW,MAAM,QAAQ,UAAU;AACzC,YAAS,EAAE;AACX,QAAK,MAAM,CAAC,KAAK,UAAU,SAAS,SAAS,CAC5C,KAAI,OAAO,UAAU,SACpB,QAAO,OAAO;SAGV;GAEN,MAAM,aAAa,IAAI,aAAa,IAAI,cAAc;GACtD,MAAM,WAAW,IAAI,aAAa,IAAI,YAAY;AAElD,OAAI,cAAc,KAAK,WAAW;AACjC,QAAI,CAAC,SACJ,QAAO,MAAM,KAAK,YAAY,mBAAmB,sCAAsC;IAExF,MAAM,YAAY,MAAM,KAAK,WAAW,eAAe,YAAY,SAAS;AAC5E,QAAI,CAAC,UACJ,QAAO,MAAM,KAAK,YAAY,mBAAmB,iCAAiC;AAEnF,aAAS;cACC,KAAK,UAEf,QAAO,MAAM,KAAK,YACjB,mBACA,qEACA;OAGD,UAAS,OAAO,YAAY,IAAI,aAAa,SAAS,CAAC;;AAMzD,OAAK,MAAM,SADM;GAAC;GAAa;GAAgB;GAAiB;GAAkB;GAAQ,CAEzF,KAAI,CAAC,OAAO,OACX,QAAO,MAAM,KAAK,YAAY,mBAAmB,+BAA+B,QAAQ;AAK1F,MAAI,OAAO,kBAAkB,OAC5B,QAAO,MAAM,KAAK,YAAY,6BAA6B,uCAAuC;AAInG,MAAI,OAAO,yBAAyB,OAAO,0BAA0B,OACpE,QAAO,MAAM,KAAK,YAAY,mBAAmB,+CAA+C;EAIjG,IAAIC;AACJ,MAAI;AACH,YAAS,MAAM,KAAK,eAAe,cAAc,OAAO,UAAW;WAC3D,GAAG;AACX,UAAO,MAAM,KAAK,YAAY,kBAAkB,6BAA6B,IAAI;;AAIlF,MAAI,CAAC,OAAO,aAAa,SAAS,OAAO,aAAc,CACtD,QAAO,MAAM,KAAK,YAAY,mBAAmB,uCAAuC;AAIzF,MAAI,QAAQ,WAAW,OACtB,QAAO,KAAK,oBAAoB,SAAS,QAAQ,OAAO;EAIzD,IAAIC,OAA+C;AACnD,MAAI,KAAK,eACR,QAAO,MAAM,KAAK,gBAAgB;EAInC,IAAIC,iBAAiD;AACrD,MAAI,CAAC,QAAQ,KAAK,kBACjB,kBAAiB,MAAM,KAAK,mBAAmB;EAGhD,MAAM,mBAAmB,CAAC,QAAQ,CAAC,CAAC;EAGpC,MAAM,QAAQ,OAAO,SAAS;EAC9B,MAAM,OAAO,gBAAgB;GAC5B;GACA;GACA,cAAc,IAAI;GAClB,OAAO,OAAO;GACd,aAAa;GACb,YAAY,MAAM;GAClB,WAAW,CAAC,QAAQ,CAAC,CAAC,KAAK;GAC3B;GACA,gBAAgB,kBAAkB;GAClC,CAAC;EAEF,MAAM,MAAM,MAAM,gBAAgB,iBAAiB;AAEnD,SAAO,IAAI,SAAS,MAAM;GACzB,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,2BAA2B;IAC3B,iBAAiB;IACjB;GACD,CAAC;;;;;CAMH,MAAc,oBACb,SACA,QACA,QACoB;EAEpB,MAAM,SAAS,OAAO;EACtB,MAAM,WAAW,OAAO,YAAY;EAEpC,MAAM,cAAc,OAAO;EAC3B,MAAM,QAAQ,OAAO;EAErB,MAAM,eAAe,OAAO,iBAAiB;AAG7C,MAAI,WAAW,QAAQ;GACtB,MAAM,WAAW,IAAI,IAAI,YAAY;AAErC,OAAI,iBAAiB,YAAY;IAChC,MAAM,aAAa,IAAI,iBAAiB;AACxC,eAAW,IAAI,SAAS,gBAAgB;AACxC,eAAW,IAAI,qBAAqB,4BAA4B;AAChE,eAAW,IAAI,SAAS,MAAM;AAC9B,eAAW,IAAI,OAAO,KAAK,OAAO;AAClC,aAAS,OAAO,WAAW,UAAU;UAC/B;AACN,aAAS,aAAa,IAAI,SAAS,gBAAgB;AACnD,aAAS,aAAa,IAAI,qBAAqB,4BAA4B;AAC3E,aAAS,aAAa,IAAI,SAAS,MAAM;AACzC,aAAS,aAAa,IAAI,OAAO,KAAK,OAAO;;AAG9C,UAAO,SAAS,SAAS,SAAS,UAAU,EAAE,IAAI;;EAInD,IAAID,OAA+C;AAEnD,MAAI,KAAK,eACR,QAAO,MAAM,KAAK,gBAAgB;AAGnC,MAAI,CAAC,QAAQ,YAAY,KAAK,WAC7B,QAAO,MAAM,KAAK,WAAW,SAAS;AAGvC,MAAI,CAAC,MAAM;GAEV,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI;GAEhC,MAAM,OAAO,gBAAgB;IAC5B;IACA,OAHa,OAAO,SAAS;IAI7B,cAAc,IAAI;IAClB;IACA,aAAa;IACb,WAAW;IACX,OAAO;IACP,CAAC;GACF,MAAM,MAAM,MAAM,gBAAgB,MAAM;AACxC,UAAO,IAAI,SAAS,MAAM;IACzB,QAAQ;IACR,SAAS;KACR,gBAAgB;KAChB,2BAA2B;KAC3B,iBAAiB;KACjB;IACD,CAAC;;EAIH,MAAM,OAAO,kBAAkB;EAC/B,MAAM,QAAQ,OAAO,SAAS;EAE9B,MAAME,eAA6B;GAClC,UAAU,OAAO;GACjB;GACA,eAAe,OAAO;GACtB,qBAAqB;GACrB;GACA,KAAK,KAAK;GACV,WAAW,KAAK,KAAK,GAAG;GACxB;AAED,QAAM,KAAK,QAAQ,aAAa,MAAM,aAAa;EAGnD,MAAM,aAAa,IAAI,IAAI,YAAY;AAEvC,MAAI,iBAAiB,YAAY;GAEhC,MAAM,aAAa,IAAI,iBAAiB;AACxC,cAAW,IAAI,QAAQ,KAAK;AAC5B,cAAW,IAAI,SAAS,MAAM;AAC9B,cAAW,IAAI,OAAO,KAAK,OAAO;AAClC,cAAW,OAAO,WAAW,UAAU;SACjC;AAEN,cAAW,aAAa,IAAI,QAAQ,KAAK;AACzC,cAAW,aAAa,IAAI,SAAS,MAAM;AAC3C,cAAW,aAAa,IAAI,OAAO,KAAK,OAAO;;AAGhD,SAAO,SAAS,SAAS,WAAW,UAAU,EAAE,IAAI;;;;;CAMrD,MAAM,YAAY,SAAqC;EACtD,IAAIJ;AACJ,MAAI;AACH,YAAS,MAAM,iBAAiB,QAAQ;WAChC,GAAG;AACX,UAAO,WAAW,mBAAmB,aAAa,QAAQ,EAAE,UAAU,kBAAkB;;EAGzF,MAAM,YAAY,OAAO;AAEzB,MAAI,cAAc,qBACjB,QAAO,KAAK,6BAA6B,SAAS,OAAO;WAC/C,cAAc,gBACxB,QAAO,KAAK,wBAAwB,SAAS,OAAO;MAEpD,QAAO,WAAW,0BAA0B,2BAA2B,YAAY;;;;;CAOrF,MAAc,6BACb,SACA,QACoB;AAGpB,OAAK,MAAM,SADM;GAAC;GAAQ;GAAa;GAAgB;GAAgB,CAEtE,KAAI,CAAC,OAAO,OACX,QAAO,WAAW,mBAAmB,+BAA+B,QAAQ;AAK9E,MAAI;AACH,SAAM,mBACL,QACA,OAAO,aAAa;AACnB,QAAI,KAAK,eACR,KAAI;AACH,YAAO,MAAM,KAAK,eAAe,cAAc,SAAS;YACjD;AACP,YAAO;;AAGT,WAAO,KAAK,QAAQ,UAAU,SAAS;MAExC;IACC,eAAe,GAAG,KAAK,OAAO;IAC9B,QAAQ,KAAK;IACb,UAAU,OAAO,QAAQ,KAAK,QAAQ,kBAAkB,IAAI;IAC5D,CACD;WACO,GAAG;AACX,OAAI,aAAa,gBAChB,QAAO,WAAW,EAAE,MAAM,EAAE,QAAQ;AAErC,UAAO,WAAW,kBAAkB,+BAA+B;;EAIpE,MAAM,WAAW,MAAM,KAAK,QAAQ,YAAY,OAAO,KAAM;AAC7D,MAAI,CAAC,SACJ,QAAO,WAAW,iBAAiB,wCAAwC;AAI5E,QAAM,KAAK,QAAQ,eAAe,OAAO,KAAM;AAG/C,MAAI,SAAS,aAAa,OAAO,UAChC,QAAO,WAAW,iBAAiB,qBAAqB;AAIzD,MAAI,SAAS,gBAAgB,OAAO,aACnC,QAAO,WAAW,iBAAiB,wBAAwB;AAS5D,MAAI,CALc,MAAM,oBACvB,OAAO,eACP,SAAS,eACT,SAAS,oBACT,CAEA,QAAO,WAAW,iBAAiB,wBAAwB;EAI5D,IAAIK;AACJ,MAAI,KAAK,aACR,KAAI;GACH,MAAM,YAAY,MAAM,gBAAgB,QAAQ;AAIhD,OAAI,CADgB,MAAM,KAAK,QAAQ,kBAAkB,UAAU,IAAI,CAEtE,QAAO,WAAW,sBAAsB,6BAA6B;AAGtE,aAAU,UAAU;WACZ,GAAG;AACX,OAAI,aAAa,WAAW;AAE3B,QAAI,EAAE,SAAS,kBAAkB;KAChC,MAAM,QAAQ,mBAAmB;AACjC,YAAO,IAAI,SACV,KAAK,UAAU;MACd,OAAO;MACP,mBAAmB;MACnB,CAAC,EACF;MACC,QAAQ;MACR,SAAS;OACR,gBAAgB;OAChB,cAAc;OACd,iBAAiB;OACjB;MACD,CACD;;AAEF,WAAO,WAAW,sBAAsB,EAAE,QAAQ;;AAEnD,UAAO,WAAW,sBAAsB,2BAA2B;;WAIjD,QAAQ,QAAQ,IAAI,OAAO,CAE7C,KAAI;GACH,MAAM,YAAY,MAAM,gBAAgB,QAAQ;AAEhD,OAAI,CADgB,MAAM,KAAK,QAAQ,kBAAkB,UAAU,IAAI,CAEtE,QAAO,WAAW,sBAAsB,6BAA6B;AAEtE,aAAU,UAAU;WACZ,GAAG;AACX,OAAI,aAAa,UAChB,QAAO,WAAW,sBAAsB,EAAE,QAAQ;AAEnD,UAAO,WAAW,sBAAsB,2BAA2B;;EAMtE,MAAM,EAAE,QAAQ,cAAc,eAAe;GAC5C,KAAK,SAAS;GACd,UAAU,SAAS;GACnB,OAAO,SAAS;GAChB;GACA,CAAC;AAGF,QAAM,KAAK,QAAQ,WAAW,UAAU;AAGxC,SAAO,IAAI,SAAS,KAAK,UAAU,mBAAmB,OAAO,CAAC,EAAE;GAC/D,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,iBAAiB;IACjB;GACD,CAAC;;;;;CAMH,MAAc,wBACb,SACA,QACoB;EACpB,MAAM,eAAe,OAAO;AAC5B,MAAI,CAAC,aACJ,QAAO,WAAW,mBAAmB,kCAAkC;AAIxE,MAAI,OAAO,UACV,KAAI;AACH,SAAM,mBACL,QACA,OAAO,aAAa;AACnB,QAAI,KAAK,eACR,KAAI;AACH,YAAO,MAAM,KAAK,eAAe,cAAc,SAAS;YACjD;AACP,YAAO;;AAGT,WAAO,KAAK,QAAQ,UAAU,SAAS;MAExC;IACC,eAAe,GAAG,KAAK,OAAO;IAC9B,QAAQ,KAAK;IACb,UAAU,OAAO,QAAQ,KAAK,QAAQ,kBAAkB,IAAI;IAC5D,CACD;WACO,GAAG;AACX,OAAI,aAAa,gBAChB,QAAO,WAAW,EAAE,MAAM,EAAE,QAAQ;AAErC,UAAO,WAAW,kBAAkB,+BAA+B;;EAKrE,MAAM,eAAe,MAAM,KAAK,QAAQ,kBAAkB,aAAa;AACvE,MAAI,CAAC,aACJ,QAAO,WAAW,iBAAiB,wBAAwB;AAI5D,MAAI,aAAa,QAChB,QAAO,WAAW,iBAAiB,yBAAyB;AAI7D,MAAI,OAAO,aAAa,OAAO,cAAc,aAAa,SACzD,QAAO,WAAW,iBAAiB,qBAAqB;AAIzD,MAAI,aAAa,QAChB,KAAI;GACH,MAAM,YAAY,MAAM,gBAAgB,QAAQ;AAGhD,OAAI,UAAU,QAAQ,aAAa,QAClC,QAAO,WAAW,sBAAsB,oBAAoB;AAK7D,OAAI,CADgB,MAAM,KAAK,QAAQ,kBAAkB,UAAU,IAAI,CAEtE,QAAO,WAAW,sBAAsB,6BAA6B;WAE9D,GAAG;AACX,OAAI,aAAa,UAChB,QAAO,WAAW,sBAAsB,EAAE,QAAQ;AAEnD,UAAO,WAAW,sBAAsB,2BAA2B;;AAKrE,QAAM,KAAK,QAAQ,YAAY,aAAa,YAAY;EAGxD,MAAM,EAAE,QAAQ,cAAc,cAAc,cAAc,KAAK;AAG/D,QAAM,KAAK,QAAQ,WAAW,UAAU;AAGxC,SAAO,IAAI,SAAS,KAAK,UAAU,mBAAmB,OAAO,CAAC,EAAE;GAC/D,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,iBAAiB;IACjB;GACD,CAAC;;;;;CAMH,MAAM,UAAU,SAAqC;AACpD,MAAI,CAAC,KAAK,UACT,QAAO,WAAW,mBAAmB,qBAAqB;AAE3D,SAAO,KAAK,WAAW,kBAAkB,QAAQ;;;;;CAMlD,iBAA2B;EAE1B,MAAMC,WAA6C;GAClD,QAAQ,KAAK;GACb,wBAAwB,GAAG,KAAK,OAAO;GACvC,gBAAgB,GAAG,KAAK,OAAO;GAC/B,mBAAmB,GAAG,KAAK,OAAO;GAClC,0BAA0B,CAAC,OAAO;GAClC,0BAA0B,CAAC,YAAY,QAAQ;GAC/C,uBAAuB,CAAC,sBAAsB,gBAAgB;GAC9D,kCAAkC,CAAC,OAAO;GAC1C,uCAAuC,CAAC,QAAQ,kBAAkB;GAClE,kBAAkB;IAAC;IAAW;IAAsB;IAAuB;GAC3E,yBAAyB,CAAC,SAAS;GACnC,gDAAgD;GAChD,uCAAuC;GACvC,kDAAkD,CAAC,QAAQ;GAC3D,GAAI,KAAK,aAAa;IACrB,uCAAuC,GAAG,KAAK,OAAO;IACtD,uCAAuC;IACvC;GACD,GAAI,KAAK,gBAAgB,EACxB,mCAAmC,CAAC,QAAQ,EAC5C;GACD;AAED,SAAO,IAAI,SAAS,KAAK,UAAU,SAAS,EAAE;GAC7C,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,iBAAiB;IACjB;GACD,CAAC;;;;;;;;CASH,MAAM,kBACL,SACA,eAC4B;EAE5B,MAAM,YAAY,mBAAmB,QAAQ;AAC7C,MAAI,CAAC,UACJ,QAAO;EAIR,MAAM,YAAY,MAAM,KAAK,QAAQ,iBAAiB,UAAU,MAAM;AACtE,MAAI,CAAC,UACJ,QAAO;AAIR,MAAI,CAAC,aAAa,UAAU,CAC3B,QAAO;AAIR,MAAI,UAAU,WAAW,UAAU,SAAS,OAC3C,QAAO;AAIR,MAAI,UAAU,QACb,KAAI;GACH,MAAM,YAAY,MAAM,gBAAgB,SAAS,EAChD,aAAa,UAAU,OACvB,CAAC;AAGF,OAAI,UAAU,QAAQ,UAAU,QAC/B,QAAO;AAKR,OAAI,CADgB,MAAM,KAAK,QAAQ,kBAAkB,UAAU,IAAI,CAEtE,QAAO;UAED;AACP,UAAO;;AAKT,MAAI,eAEH;OAAI,CADW,UAAU,MAAM,MAAM,IAAI,CAC7B,SAAS,cAAc,CAClC,QAAO;;AAIT,SAAO;;;;;;;;;CAUR,MAAM,kBAAkB,SAAqC;AAC5D,MAAI,CAAC,KAAK,cACT,QAAO,WAAW,2BAA2B,4CAA4C,IAAI;EAG9F,IAAIC;AAMJ,MAAI;AACH,UAAO,MAAM,QAAQ,MAAM;UACpB;AACP,UAAO,WAAW,mBAAmB,qBAAqB,IAAI;;EAG/D,MAAM,EAAE,UAAU,WAAW,gBAAgB;AAE7C,MAAI,CAAC,YAAY,CAAC,aAAa,CAAC,YAC/B,QAAO,WAAW,mBAAmB,+BAA+B,IAAI;EAIzE,MAAM,OAAO,MAAM,KAAK,cAAc,UAAU,UAAU;AAC1D,MAAI,CAAC,KACJ,QAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,yBAAyB,CAAC,EAAE;GACvE,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAC/C,CAAC;AAKH,OAAK,MAAM,SADM;GAAC;GAAa;GAAgB;GAAS;GAAiB,CAExE,KAAI,CAAC,YAAY,OAChB,QAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,4BAA4B,SAAS,CAAC,EAAE;GACnF,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAC/C,CAAC;EAKJ,IAAIN;AACJ,MAAI;AACH,YAAS,MAAM,KAAK,eAAe,cAAc,YAAY,UAAW;WAChE,GAAG;AACX,UAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,mBAAmB,KAAK,CAAC,EAAE;IACtE,QAAQ;IACR,SAAS,EAAE,gBAAgB,oBAAoB;IAC/C,CAAC;;AAGH,MAAI,CAAC,OAAO,aAAa,SAAS,YAAY,aAAc,CAC3D,QAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,wCAAwC,CAAC,EAAE;GACtF,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAC/C,CAAC;EAIH,MAAM,OAAO,kBAAkB;EAC/B,MAAM,QAAQ,YAAY,SAAS;EAEnC,MAAMG,eAA6B;GAClC,UAAU,YAAY;GACtB,aAAa,YAAY;GACzB,eAAe,YAAY;GAC3B,qBAAqB;GACrB;GACA,KAAK,KAAK;GACV,WAAW,KAAK,KAAK,GAAG;GACxB;AAED,QAAM,KAAK,QAAQ,aAAa,MAAM,aAAa;EAGnD,MAAM,eAAe,YAAY,iBAAiB;EAClD,MAAM,cAAc,IAAI,IAAI,YAAY,aAAc;AAEtD,MAAI,iBAAiB,YAAY;GAChC,MAAM,aAAa,IAAI,iBAAiB;AACxC,cAAW,IAAI,QAAQ,KAAK;AAC5B,cAAW,IAAI,SAAS,YAAY,MAAO;AAC3C,cAAW,IAAI,OAAO,KAAK,OAAO;AAClC,eAAY,OAAO,WAAW,UAAU;SAClC;AACN,eAAY,aAAa,IAAI,QAAQ,KAAK;AAC1C,eAAY,aAAa,IAAI,SAAS,YAAY,MAAO;AACzD,eAAY,aAAa,IAAI,OAAO,KAAK,OAAO;;AAGjD,SAAO,IAAI,SAAS,KAAK,UAAU,EAAE,aAAa,YAAY,UAAU,EAAE,CAAC,EAAE;GAC5E,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAC/C,CAAC;;;;;CAMH,MAAc,YAAY,OAAe,aAAwC;EAChF,MAAM,OAAO,gBAAgB,OAAO,YAAY;EAChD,MAAM,MAAM,MAAM,gBAAgB,MAAM;AACxC,SAAO,IAAI,SAAS,MAAM;GACzB,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,2BAA2B;IAC3B,iBAAiB;IACjB;GACD,CAAC;;;;;;;;;AC9nBJ,IAAa,uBAAb,MAA0D;CACzD,AAAQ,4BAAY,IAAI,KAA2B;CACnD,AAAQ,yBAAS,IAAI,KAAwB;CAC7C,AAAQ,oCAAoB,IAAI,KAAqB;CACrD,AAAQ,0BAAU,IAAI,KAA6B;CACnD,AAAQ,8BAAc,IAAI,KAAsB;CAChD,AAAQ,yBAAS,IAAI,KAAa;CAElC,MAAM,aAAa,MAAc,MAAmC;AACnE,OAAK,UAAU,IAAI,MAAM,KAAK;;CAG/B,MAAM,YAAY,MAA4C;EAC7D,MAAM,OAAO,KAAK,UAAU,IAAI,KAAK;AACrC,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,KAAK,GAAG,KAAK,WAAW;AAChC,QAAK,UAAU,OAAO,KAAK;AAC3B,UAAO;;AAER,SAAO;;CAGR,MAAM,eAAe,MAA6B;AACjD,OAAK,UAAU,OAAO,KAAK;;CAG5B,MAAM,WAAW,MAAgC;AAChD,OAAK,OAAO,IAAI,KAAK,aAAa,KAAK;AACvC,OAAK,kBAAkB,IAAI,KAAK,cAAc,KAAK,YAAY;;CAGhE,MAAM,iBAAiB,aAAgD;EACtE,MAAM,OAAO,KAAK,OAAO,IAAI,YAAY;AACzC,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,WAAW,KAAK,KAAK,GAAG,KAAK,UACrC,QAAO;AAER,SAAO;;CAGR,MAAM,kBAAkB,cAAiD;EACxE,MAAM,cAAc,KAAK,kBAAkB,IAAI,aAAa;AAC5D,MAAI,CAAC,YAAa,QAAO;EACzB,MAAM,OAAO,KAAK,OAAO,IAAI,YAAY;AACzC,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,QAAS,QAAO;AAEzB,SAAO;;CAGR,MAAM,YAAY,aAAoC;EACrD,MAAM,OAAO,KAAK,OAAO,IAAI,YAAY;AACzC,MAAI,KACH,MAAK,UAAU;;CAIjB,MAAM,gBAAgB,KAA4B;AACjD,OAAK,MAAM,GAAG,SAAS,KAAK,OAC3B,KAAI,KAAK,QAAQ,IAChB,MAAK,UAAU;;CAKlB,MAAM,WAAW,UAAkB,UAAyC;AAC3E,OAAK,QAAQ,IAAI,UAAU,SAAS;;CAGrC,MAAM,UAAU,UAAkD;AACjE,SAAO,KAAK,QAAQ,IAAI,SAAS,IAAI;;CAGtC,MAAM,QAAQ,YAAoB,MAA8B;AAC/D,OAAK,YAAY,IAAI,YAAY,KAAK;;CAGvC,MAAM,OAAO,YAA6C;EACzD,MAAM,OAAO,KAAK,YAAY,IAAI,WAAW;AAC7C,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,KAAK,GAAG,KAAK,WAAW;AAChC,QAAK,YAAY,OAAO,WAAW;AACnC,UAAO;;AAER,SAAO;;CAGR,MAAM,UAAU,YAAmC;AAClD,OAAK,YAAY,OAAO,WAAW;;CAGpC,MAAM,kBAAkB,OAAiC;AACxD,MAAI,KAAK,OAAO,IAAI,MAAM,CACzB,QAAO;AAER,OAAK,OAAO,IAAI,MAAM;AAGtB,SAAO;;;CAIR,QAAc;AACb,OAAK,UAAU,OAAO;AACtB,OAAK,OAAO,OAAO;AACnB,OAAK,kBAAkB,OAAO;AAC9B,OAAK,QAAQ,OAAO;AACpB,OAAK,YAAY,OAAO;AACxB,OAAK,OAAO,OAAO"}
|