@getcirrus/oauth-provider 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +284 -0
- package/dist/index.d.ts +591 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1395 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["url: URL","protectedHeader: { alg: string; jwk?: JWK }","payload: {\n\t\tjti?: string;\n\t\thtm?: string;\n\t\thtu?: string;\n\t\tiat?: number;\n\t\tath?: string;\n\t\tnonce?: string;\n\t}","params: Record<string, string>","parData: PARData","response: OAuthParResponse","body: OAuthErrorResponse","code: string","response: Response","doc: OAuthClientMetadata","metadata: ClientMetadata","tokenData: TokenData","descriptions: string[]","params: Record<string, string>","client: ClientMetadata","user: { sub: string; handle: string } | null","authCodeData: AuthCodeData","dpopJkt: string | undefined","metadata: OAuthAuthorizationServerMetadata"],"sources":["../src/pkce.ts","../src/encoding.ts","../src/dpop.ts","../src/par.ts","../src/client-resolver.ts","../src/tokens.ts","../src/ui.ts","../src/provider.ts","../src/storage.ts"],"sourcesContent":["/**\n * PKCE (Proof Key for Code Exchange) verification\n * Implements RFC 7636 with S256 challenge method\n */\n\nimport { base64url } from \"jose\";\n\n/**\n * Generate the S256 code challenge from a verifier\n * challenge = BASE64URL(SHA256(verifier))\n */\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst data = encoder.encode(verifier);\n\tconst hash = await crypto.subtle.digest(\"SHA-256\", data);\n\treturn base64url.encode(new Uint8Array(hash));\n}\n\n/**\n * Verify a PKCE code challenge against a verifier\n * @param verifier The code verifier from the token request\n * @param challenge The code challenge from the authorization request\n * @param method The challenge method (only S256 supported for AT Protocol)\n * @returns true if the verifier matches the challenge\n */\nexport async function verifyPkceChallenge(\n\tverifier: string,\n\tchallenge: string,\n\tmethod: \"S256\"\n): Promise<boolean> {\n\tif (method !== \"S256\") {\n\t\tthrow new Error(\"Only S256 challenge method is supported\");\n\t}\n\n\t// Validate verifier format (RFC 7636 Section 4.1)\n\t// Must be 43-128 characters, unreserved characters only\n\tif (verifier.length < 43 || verifier.length > 128) {\n\t\treturn false;\n\t}\n\tif (!/^[A-Za-z0-9._~-]+$/.test(verifier)) {\n\t\treturn false;\n\t}\n\n\tconst expectedChallenge = await generateCodeChallenge(verifier);\n\treturn expectedChallenge === challenge;\n}\n","/**\n * Shared encoding utilities for OAuth provider\n */\n\nimport { base64url } from \"jose\";\n\n/**\n * Generate a cryptographically random string\n *\n * @param byteLength Number of random bytes (default: 32 = 256 bits)\n * @returns Base64URL-encoded random string\n */\nexport function randomString(byteLength: number = 32): string {\n\tconst buffer = new Uint8Array(byteLength);\n\tcrypto.getRandomValues(buffer);\n\treturn base64url.encode(buffer);\n}\n","/**\n * DPoP (Demonstrating Proof of Possession) verification\n * Implements RFC 9449 using jose library for JWT operations\n */\n\nimport { jwtVerify, EmbeddedJWK, calculateJwkThumbprint, errors, base64url } from \"jose\";\nimport type { JWK } from \"jose\";\nimport { randomString } from \"./encoding.js\";\n\nconst { JOSEError } = errors;\n\n/**\n * Verified DPoP proof data\n */\nexport interface DpopProof {\n\t/** HTTP method from the proof */\n\thtm: string;\n\t/** HTTP URI from the proof (without query/fragment) */\n\thtu: string;\n\t/** Unique proof identifier (for replay prevention) */\n\tjti: string;\n\t/** Access token hash (if present) */\n\tath?: string;\n\t/** Key thumbprint (JWK thumbprint of the proof key) */\n\tjkt: string;\n\t/** The public JWK from the proof */\n\tjwk: JWK;\n}\n\n/**\n * DPoP verification options\n */\nexport interface DpopVerifyOptions {\n\t/** Access token to verify ath claim against (optional) */\n\taccessToken?: string;\n\t/** Allowed signature algorithms (default: ['ES256']) */\n\tallowedAlgorithms?: string[];\n\t/** Expected nonce value (optional, for nonce binding) */\n\texpectedNonce?: string;\n\t/** Max token age in seconds (default: 60) */\n\tmaxTokenAge?: number;\n}\n\n/**\n * DPoP verification error\n */\nexport class DpopError extends Error {\n\treadonly code: string;\n\tconstructor(message: string, code: string, options?: ErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.name = \"DpopError\";\n\t\tthis.code = code;\n\t}\n}\n\n/**\n * Normalize URI for HTU comparison\n * Removes query string and fragment per RFC 9449\n */\nfunction normalizeHtuUrl(url: URL): string {\n\treturn url.origin + url.pathname;\n}\n\n/**\n * Parse and validate HTU claim\n */\nfunction parseHtu(htu: string): string {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(htu);\n\t} catch {\n\t\tthrow new DpopError('DPoP \"htu\" is not a valid URL', \"invalid_dpop\");\n\t}\n\n\tif (url.password || url.username) {\n\t\tthrow new DpopError('DPoP \"htu\" must not contain credentials', \"invalid_dpop\");\n\t}\n\n\tif (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n\t\tthrow new DpopError('DPoP \"htu\" must be http or https', \"invalid_dpop\");\n\t}\n\n\treturn normalizeHtuUrl(url);\n}\n\n/**\n * Verify a DPoP proof from a request\n * Uses jose library for JWT verification\n * @param request The HTTP request containing the DPoP header\n * @param options Verification options\n * @returns The verified proof data\n * @throws DpopError if verification fails\n */\nexport async function verifyDpopProof(\n\trequest: Request,\n\toptions: DpopVerifyOptions = {}\n): Promise<DpopProof> {\n\tconst { allowedAlgorithms = [\"ES256\"], accessToken, expectedNonce, maxTokenAge = 60 } = options;\n\n\tconst dpopHeader = request.headers.get(\"DPoP\");\n\tif (!dpopHeader) {\n\t\tthrow new DpopError(\"Missing DPoP header\", \"missing_dpop\");\n\t}\n\n\tlet protectedHeader: { alg: string; jwk?: JWK };\n\tlet payload: {\n\t\tjti?: string;\n\t\thtm?: string;\n\t\thtu?: string;\n\t\tiat?: number;\n\t\tath?: string;\n\t\tnonce?: string;\n\t};\n\n\ttry {\n\t\tconst result = await jwtVerify(dpopHeader, EmbeddedJWK, {\n\t\t\ttyp: \"dpop+jwt\",\n\t\t\talgorithms: allowedAlgorithms,\n\t\t\tmaxTokenAge,\n\t\t\tclockTolerance: 10,\n\t\t});\n\t\tprotectedHeader = result.protectedHeader as typeof protectedHeader;\n\t\tpayload = result.payload as typeof payload;\n\t} catch (err) {\n\t\tif (err instanceof JOSEError) {\n\t\t\tthrow new DpopError(`DPoP verification failed: ${err.message}`, \"invalid_dpop\", { cause: err });\n\t\t}\n\t\tthrow new DpopError(\"DPoP verification failed\", \"invalid_dpop\", { cause: err });\n\t}\n\n\tif (!payload.jti || typeof payload.jti !== \"string\") {\n\t\tthrow new DpopError('DPoP \"jti\" missing', \"invalid_dpop\");\n\t}\n\n\tif (!payload.htm || typeof payload.htm !== \"string\") {\n\t\tthrow new DpopError('DPoP \"htm\" missing', \"invalid_dpop\");\n\t}\n\n\tif (!payload.htu || typeof payload.htu !== \"string\") {\n\t\tthrow new DpopError('DPoP \"htu\" missing', \"invalid_dpop\");\n\t}\n\n\tif (payload.htm !== request.method) {\n\t\tthrow new DpopError('DPoP \"htm\" mismatch', \"invalid_dpop\");\n\t}\n\n\tconst requestUrl = new URL(request.url);\n\tconst expectedHtu = normalizeHtuUrl(requestUrl);\n\tconst proofHtu = parseHtu(payload.htu);\n\tif (proofHtu !== expectedHtu) {\n\t\tthrow new DpopError('DPoP \"htu\" mismatch', \"invalid_dpop\");\n\t}\n\n\tif (expectedNonce !== undefined && payload.nonce !== expectedNonce) {\n\t\tthrow new DpopError('DPoP \"nonce\" mismatch', \"use_dpop_nonce\");\n\t}\n\n\t// Verify ath (access token hash) binding per RFC 9449 Section 4.3\n\tif (accessToken) {\n\t\tif (!payload.ath) {\n\t\t\tthrow new DpopError('DPoP \"ath\" missing when access token provided', \"invalid_dpop\");\n\t\t}\n\n\t\tconst tokenHash = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(accessToken));\n\t\tconst expectedAth = base64url.encode(new Uint8Array(tokenHash));\n\n\t\tif (payload.ath !== expectedAth) {\n\t\t\tthrow new DpopError('DPoP \"ath\" mismatch', \"invalid_dpop\");\n\t\t}\n\t} else if (payload.ath !== undefined) {\n\t\tthrow new DpopError('DPoP \"ath\" claim not allowed without access token', \"invalid_dpop\");\n\t}\n\n\tconst jwk = protectedHeader.jwk!;\n\tconst jkt = await calculateJwkThumbprint(jwk, \"sha256\");\n\n\treturn Object.freeze({\n\t\thtm: payload.htm,\n\t\thtu: payload.htu,\n\t\tjti: payload.jti,\n\t\tath: payload.ath,\n\t\tjkt,\n\t\tjwk,\n\t});\n}\n\n/**\n * Generate a random DPoP nonce\n * @returns A base64url-encoded random nonce (16 bytes)\n */\nexport function generateDpopNonce(): string {\n\treturn randomString(16);\n}\n","/**\n * PAR (Pushed Authorization Requests) handler\n * Implements RFC 9126\n */\n\nimport type { OAuthParResponse } from \"@atproto/oauth-types\";\nimport type { OAuthStorage, PARData } from \"./storage.js\";\nimport { randomString } from \"./encoding.js\";\nimport { parseRequestBody } from \"./provider.js\";\n\nexport type { OAuthParResponse };\n\n/** PAR request URI prefix per RFC 9126 */\nconst REQUEST_URI_PREFIX = \"urn:ietf:params:oauth:request_uri:\";\n\n/** Default PAR expiration in seconds (90 seconds per RFC recommendation) */\nconst DEFAULT_EXPIRES_IN = 90;\n\n/**\n * OAuth error response\n */\nexport interface OAuthErrorResponse {\n\terror: string;\n\terror_description?: string;\n}\n\n/**\n * Generate a unique request URI\n */\nfunction generateRequestUri(): string {\n\treturn REQUEST_URI_PREFIX + randomString(32);\n}\n\n/**\n * Required OAuth parameters for authorization request\n */\nconst REQUIRED_PARAMS = [\"client_id\", \"redirect_uri\", \"response_type\", \"code_challenge\", \"code_challenge_method\", \"state\"];\n\n/**\n * Handler for Pushed Authorization Requests (PAR)\n */\nexport class PARHandler {\n\tprivate storage: OAuthStorage;\n\tprivate issuer: string;\n\tprivate expiresIn: number;\n\n\t/**\n\t * Create a PAR handler\n\t * @param storage OAuth storage implementation\n\t * @param issuer The OAuth issuer URL\n\t * @param expiresIn PAR expiration time in seconds (default: 90)\n\t */\n\tconstructor(storage: OAuthStorage, issuer: string, expiresIn: number = DEFAULT_EXPIRES_IN) {\n\t\tthis.storage = storage;\n\t\tthis.issuer = issuer;\n\t\tthis.expiresIn = expiresIn;\n\t}\n\n\t/**\n\t * Handle a PAR push request\n\t * POST /oauth/par\n\t * @param request The HTTP request\n\t * @returns Response with request_uri or error\n\t */\n\tasync handlePushRequest(request: Request): Promise<Response> {\n\t\tlet params: Record<string, string>;\n\t\ttry {\n\t\t\tparams = await parseRequestBody(request);\n\t\t} catch (e) {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\te instanceof Error ? e.message : \"Invalid request\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tconst clientId = params.client_id;\n\t\tif (!clientId) {\n\t\t\treturn this.errorResponse(\"invalid_request\", \"Missing client_id parameter\", 400);\n\t\t}\n\n\t\tfor (const param of REQUIRED_PARAMS) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn this.errorResponse(\"invalid_request\", `Missing required parameter: ${param}`, 400);\n\t\t\t}\n\t\t}\n\n\t\tif (params.response_type !== \"code\") {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"unsupported_response_type\",\n\t\t\t\t\"Only response_type=code is supported\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tif (params.code_challenge_method !== \"S256\") {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t\"Only code_challenge_method=S256 is supported\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\tconst codeChallenge = params.code_challenge!;\n\t\tif (!/^[A-Za-z0-9_-]{43}$/.test(codeChallenge)) {\n\t\t\treturn this.errorResponse(\n\t\t\t\t\"invalid_request\",\n\t\t\t\t\"Invalid code_challenge format\",\n\t\t\t\t400\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\tnew URL(params.redirect_uri!);\n\t\t} catch {\n\t\t\treturn this.errorResponse(\"invalid_request\", \"Invalid redirect_uri\", 400);\n\t\t}\n\n\t\tconst requestUri = generateRequestUri();\n\t\tconst expiresAt = Date.now() + this.expiresIn * 1000;\n\n\t\tconst parData: PARData = {\n\t\t\tclientId,\n\t\t\tparams,\n\t\t\texpiresAt,\n\t\t};\n\n\t\tawait this.storage.savePAR(requestUri, parData);\n\n\t\tconst response: OAuthParResponse = {\n\t\t\trequest_uri: requestUri,\n\t\t\texpires_in: this.expiresIn,\n\t\t};\n\n\t\treturn new Response(JSON.stringify(response), {\n\t\t\tstatus: 201,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Retrieve and consume PAR parameters\n\t * Called during authorization request handling\n\t * @param requestUri The request URI from the authorization request\n\t * @param clientId The client_id from the authorization request (for verification)\n\t * @returns The stored parameters or null if not found/expired\n\t */\n\tasync retrieveParams(\n\t\trequestUri: string,\n\t\tclientId: string\n\t): Promise<Record<string, string> | null> {\n\t\tif (!requestUri.startsWith(REQUEST_URI_PREFIX)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst parData = await this.storage.getPAR(requestUri);\n\t\tif (!parData) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (parData.clientId !== clientId) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// One-time use: delete after retrieval\n\t\tawait this.storage.deletePAR(requestUri);\n\n\t\treturn parData.params;\n\t}\n\n\t/**\n\t * Check if a request_uri is valid format\n\t */\n\tstatic isRequestUri(value: string): boolean {\n\t\treturn value.startsWith(REQUEST_URI_PREFIX);\n\t}\n\n\t/**\n\t * Create an OAuth error response\n\t */\n\tprivate errorResponse(\n\t\terror: string,\n\t\tdescription: string,\n\t\tstatus: number = 400\n\t): Response {\n\t\tconst body: OAuthErrorResponse = {\n\t\t\terror,\n\t\t\terror_description: description,\n\t\t};\n\t\treturn new Response(JSON.stringify(body), {\n\t\t\tstatus,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n}\n","/**\n * Client resolver for DID-based client discovery\n * Resolves OAuth client metadata from DIDs for AT Protocol\n */\n\nimport { ensureValidDid } from \"@atproto/syntax\";\nimport {\n\toauthClientMetadataSchema,\n\ttype OAuthClientMetadata,\n} from \"@atproto/oauth-types\";\nimport type { ClientMetadata, OAuthStorage } from \"./storage.js\";\n\nexport type { OAuthClientMetadata };\n\n/**\n * Client resolution error\n */\nexport class ClientResolutionError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly code: string\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"ClientResolutionError\";\n\t}\n}\n\n/**\n * Options for client resolution\n */\nexport interface ClientResolverOptions {\n\t/** Storage for caching client metadata */\n\tstorage?: OAuthStorage;\n\t/** Cache TTL in milliseconds (default: 1 hour) */\n\tcacheTtl?: number;\n\t/** Fetch function for making HTTP requests (for testing) */\n\tfetch?: typeof globalThis.fetch;\n}\n\n/**\n * Check if a string is a valid HTTPS URL\n */\nfunction isHttpsUrl(value: string): boolean {\n\ttry {\n\t\tconst url = new URL(value);\n\t\treturn url.protocol === \"https:\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Validate that a string is a valid DID using @atproto/syntax\n */\nfunction isValidDid(value: string): boolean {\n\ttry {\n\t\tensureValidDid(value);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Get the client metadata URL from a client ID\n * Supports both URL-based and DID-based client IDs\n */\nfunction getClientMetadataUrl(clientId: string): string | null {\n\t// URL-based client ID: the URL itself is the metadata endpoint\n\tif (isHttpsUrl(clientId)) {\n\t\treturn clientId;\n\t}\n\n\t// DID-based client ID: derive the metadata URL\n\tif (clientId.startsWith(\"did:web:\")) {\n\t\t// did:web:example.com -> https://example.com/.well-known/oauth-client-metadata\n\t\t// did:web:example.com:path -> https://example.com/path/.well-known/oauth-client-metadata\n\t\tconst parts = clientId.slice(8).split(\":\");\n\t\tconst host = parts[0]!.replace(/%3A/g, \":\");\n\t\tconst path = parts.slice(1).join(\"/\");\n\t\tconst baseUrl = `https://${host}${path ? \"/\" + path : \"\"}`;\n\t\treturn `${baseUrl}/.well-known/oauth-client-metadata`;\n\t}\n\n\t// Unsupported client ID format\n\treturn null;\n}\n\n/**\n * Resolve client metadata from a DID\n */\nexport class ClientResolver {\n\tprivate storage?: OAuthStorage;\n\tprivate cacheTtl: number;\n\tprivate fetchFn: typeof globalThis.fetch;\n\n\tconstructor(options: ClientResolverOptions = {}) {\n\t\tthis.storage = options.storage;\n\t\tthis.cacheTtl = options.cacheTtl ?? 60 * 60 * 1000; // 1 hour default\n\t\tthis.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);\n\t}\n\n\t/**\n\t * Resolve client metadata from a client ID (URL or DID)\n\t * @param clientId The client ID (HTTPS URL or DID)\n\t * @returns The client metadata\n\t * @throws ClientResolutionError if resolution fails\n\t */\n\tasync resolveClient(clientId: string): Promise<ClientMetadata> {\n\t\tif (!isHttpsUrl(clientId) && !isValidDid(clientId)) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Invalid client ID format: ${clientId}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tif (this.storage) {\n\t\t\tconst cached = await this.storage.getClient(clientId);\n\t\t\tif (cached && cached.cachedAt && Date.now() - cached.cachedAt < this.cacheTtl) {\n\t\t\t\treturn cached;\n\t\t\t}\n\t\t}\n\n\t\tconst metadataUrl = getClientMetadataUrl(clientId);\n\t\tif (!metadataUrl) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Unsupported client ID format: ${clientId}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await this.fetchFn(metadataUrl, {\n\t\t\t\theaders: {\n\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (e) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Failed to fetch client metadata: ${e}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Client metadata fetch failed with status ${response.status}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tlet doc: OAuthClientMetadata;\n\t\ttry {\n\t\t\tconst json = await response.json();\n\t\t\tdoc = oauthClientMetadataSchema.parse(json);\n\t\t} catch (e) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Invalid client metadata: ${e instanceof Error ? e.message : \"validation failed\"}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tif (doc.client_id !== clientId) {\n\t\t\tthrow new ClientResolutionError(\n\t\t\t\t`Client ID mismatch: expected ${clientId}, got ${doc.client_id}`,\n\t\t\t\t\"invalid_client\"\n\t\t\t);\n\t\t}\n\n\t\tconst metadata: ClientMetadata = {\n\t\t\tclientId: doc.client_id,\n\t\t\tclientName: doc.client_name ?? clientId,\n\t\t\tredirectUris: doc.redirect_uris,\n\t\t\tlogoUri: doc.logo_uri,\n\t\t\tclientUri: doc.client_uri,\n\t\t\tcachedAt: Date.now(),\n\t\t};\n\n\t\tif (this.storage) {\n\t\t\tawait this.storage.saveClient(clientId, metadata);\n\t\t}\n\n\t\treturn metadata;\n\t}\n\n\t/**\n\t * Validate that a redirect URI is allowed for a client\n\t * @param clientId The client DID\n\t * @param redirectUri The redirect URI to validate\n\t * @returns true if the redirect URI is allowed\n\t */\n\tasync validateRedirectUri(clientId: string, redirectUri: string): Promise<boolean> {\n\t\ttry {\n\t\t\tconst metadata = await this.resolveClient(clientId);\n\t\t\treturn metadata.redirectUris.includes(redirectUri);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n/**\n * Create a client resolver with optional caching\n */\nexport function createClientResolver(options: ClientResolverOptions = {}): ClientResolver {\n\treturn new ClientResolver(options);\n}\n","/**\n * Token generation and validation\n * Generates opaque tokens (not JWTs) that are stored in the database\n */\n\nimport type { OAuthTokenResponse } from \"@atproto/oauth-types\";\nimport type { TokenData } from \"./storage.js\";\nimport { randomString } from \"./encoding.js\";\n\n/** Default access token TTL: 1 hour */\nexport const ACCESS_TOKEN_TTL = 60 * 60 * 1000;\n\n/** Default refresh token TTL: 90 days */\nexport const REFRESH_TOKEN_TTL = 90 * 24 * 60 * 60 * 1000;\n\n/** Authorization code TTL: 5 minutes */\nexport const AUTH_CODE_TTL = 5 * 60 * 1000;\n\n/**\n * Generate a cryptographically random token\n * @param bytes Number of random bytes (default: 32)\n * @returns Base64URL-encoded token\n */\nexport function generateRandomToken(bytes: number = 32): string {\n\treturn randomString(bytes);\n}\n\n/**\n * Generate an authorization code\n * @returns A random authorization code\n */\nexport function generateAuthCode(): string {\n\treturn generateRandomToken(32);\n}\n\n/**\n * Token generation result\n */\nexport interface GeneratedTokens {\n\t/** Opaque access token */\n\taccessToken: string;\n\t/** Opaque refresh token */\n\trefreshToken: string;\n\t/** Access token type (Bearer or DPoP) */\n\ttokenType: \"Bearer\" | \"DPoP\";\n\t/** Access token expiration in seconds */\n\texpiresIn: number;\n\t/** Scope granted */\n\tscope: string;\n\t/** Subject (user DID) */\n\tsub: string;\n}\n\n/**\n * Options for token generation\n */\nexport interface GenerateTokensOptions {\n\t/** User DID */\n\tsub: string;\n\t/** Client DID */\n\tclientId: string;\n\t/** Scope granted */\n\tscope: string;\n\t/** DPoP key thumbprint (if using DPoP) */\n\tdpopJkt?: string;\n\t/** Custom access token TTL in ms (default: 1 hour) */\n\taccessTokenTtl?: number;\n\t/** Custom refresh token TTL in ms (default: 90 days) */\n\trefreshTokenTtl?: number;\n}\n\n/**\n * Generate access and refresh tokens\n * Tokens are opaque - their meaning comes from the database entry\n * @param options Token generation options\n * @returns Generated tokens and metadata\n */\nexport function generateTokens(options: GenerateTokensOptions): {\n\ttokens: GeneratedTokens;\n\ttokenData: TokenData;\n} {\n\tconst {\n\t\tsub,\n\t\tclientId,\n\t\tscope,\n\t\tdpopJkt,\n\t\taccessTokenTtl = ACCESS_TOKEN_TTL,\n\t} = options;\n\n\tconst accessToken = generateRandomToken(32);\n\tconst refreshToken = generateRandomToken(32);\n\tconst now = Date.now();\n\n\tconst tokenData: TokenData = {\n\t\taccessToken,\n\t\trefreshToken,\n\t\tclientId,\n\t\tsub,\n\t\tscope,\n\t\tdpopJkt,\n\t\tissuedAt: now,\n\t\texpiresAt: now + accessTokenTtl,\n\t\trevoked: false,\n\t};\n\n\tconst tokens: GeneratedTokens = {\n\t\taccessToken,\n\t\trefreshToken,\n\t\ttokenType: dpopJkt ? \"DPoP\" : \"Bearer\",\n\t\texpiresIn: Math.floor(accessTokenTtl / 1000),\n\t\tscope,\n\t\tsub,\n\t};\n\n\treturn { tokens, tokenData };\n}\n\n/**\n * Refresh tokens - generates new access token, optionally rotates refresh token\n * @param existingData The existing token data\n * @param rotateRefreshToken Whether to generate a new refresh token\n * @param accessTokenTtl Custom access token TTL in ms\n * @returns Updated tokens and token data\n */\nexport function refreshTokens(\n\texistingData: TokenData,\n\trotateRefreshToken: boolean = false,\n\taccessTokenTtl: number = ACCESS_TOKEN_TTL\n): {\n\ttokens: GeneratedTokens;\n\ttokenData: TokenData;\n} {\n\tconst accessToken = generateRandomToken(32);\n\tconst refreshToken = rotateRefreshToken ? generateRandomToken(32) : existingData.refreshToken;\n\tconst now = Date.now();\n\n\tconst tokenData: TokenData = {\n\t\t...existingData,\n\t\taccessToken,\n\t\trefreshToken,\n\t\tissuedAt: now,\n\t\texpiresAt: now + accessTokenTtl,\n\t};\n\n\tconst tokens: GeneratedTokens = {\n\t\taccessToken,\n\t\trefreshToken,\n\t\ttokenType: existingData.dpopJkt ? \"DPoP\" : \"Bearer\",\n\t\texpiresIn: Math.floor(accessTokenTtl / 1000),\n\t\tscope: existingData.scope,\n\t\tsub: existingData.sub,\n\t};\n\n\treturn { tokens, tokenData };\n}\n\n/**\n * Build token response for OAuth token endpoint\n * @param tokens The generated tokens\n * @returns JSON-serializable token response\n */\nexport function buildTokenResponse(tokens: GeneratedTokens): OAuthTokenResponse {\n\treturn {\n\t\taccess_token: tokens.accessToken,\n\t\ttoken_type: tokens.tokenType,\n\t\texpires_in: tokens.expiresIn,\n\t\trefresh_token: tokens.refreshToken,\n\t\tscope: tokens.scope,\n\t\tsub: tokens.sub,\n\t};\n}\n\n/**\n * Extract access token from Authorization header\n * Supports both Bearer and DPoP token types\n * @param request The HTTP request\n * @returns The access token and type, or null if not found\n */\nexport function extractAccessToken(\n\trequest: Request\n): { token: string; type: \"Bearer\" | \"DPoP\" } | null {\n\tconst authHeader = request.headers.get(\"Authorization\");\n\tif (!authHeader) {\n\t\treturn null;\n\t}\n\n\tif (authHeader.startsWith(\"Bearer \")) {\n\t\treturn {\n\t\t\ttoken: authHeader.slice(7),\n\t\t\ttype: \"Bearer\",\n\t\t};\n\t}\n\n\tif (authHeader.startsWith(\"DPoP \")) {\n\t\treturn {\n\t\t\ttoken: authHeader.slice(5),\n\t\t\ttype: \"DPoP\",\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Validate that a token is not expired or revoked\n * @param tokenData The token data from storage\n * @returns true if the token is valid\n */\nexport function isTokenValid(tokenData: TokenData): boolean {\n\tif (tokenData.revoked) {\n\t\treturn false;\n\t}\n\tif (Date.now() > tokenData.expiresAt) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n","/**\n * Authorization consent UI\n * Renders the HTML page for user consent during OAuth authorization\n */\n\nimport type { ClientMetadata } from \"./storage.js\";\n\n/**\n * Content Security Policy for the consent UI\n *\n * - default-src 'none': Deny all by default\n * - style-src 'unsafe-inline': Allow inline styles (our CSS is inline)\n * - img-src https: data:: Allow images from HTTPS URLs (client logos) and data URIs\n * - form-action 'self': Form can only POST to same origin\n * - frame-ancestors 'none': Prevent clickjacking by disallowing framing\n * - base-uri 'none': Prevent base tag injection\n */\nexport const CONSENT_UI_CSP =\n\t\"default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; form-action 'self'; frame-ancestors 'none'; base-uri 'none'\";\n\n/**\n * Escape HTML to prevent XSS\n */\nfunction escapeHtml(text: string): string {\n\treturn text\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n\n/**\n * Parse scope string into human-readable descriptions\n */\nfunction getScopeDescriptions(scope: string): string[] {\n\tconst scopes = scope.split(\" \").filter(Boolean);\n\tconst descriptions: string[] = [];\n\n\tfor (const s of scopes) {\n\t\tswitch (s) {\n\t\t\tcase \"atproto\":\n\t\t\t\tdescriptions.push(\"Access your AT Protocol account\");\n\t\t\t\tbreak;\n\t\t\tcase \"transition:generic\":\n\t\t\t\tdescriptions.push(\"Perform account operations\");\n\t\t\t\tbreak;\n\t\t\tcase \"transition:chat.bsky\":\n\t\t\t\tdescriptions.push(\"Access chat functionality\");\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t// Don't show unknown scopes to avoid confusion\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t// If no recognized scopes, show a generic message\n\tif (descriptions.length === 0) {\n\t\tdescriptions.push(\"Access your account on your behalf\");\n\t}\n\n\treturn descriptions;\n}\n\n/**\n * Options for rendering the consent UI\n */\nexport interface ConsentUIOptions {\n\t/** The OAuth client metadata */\n\tclient: ClientMetadata;\n\t/** The requested scope */\n\tscope: string;\n\t/** URL to POST the consent form to */\n\tauthorizeUrl: string;\n\t/** State parameter to include in the form */\n\tstate: string;\n\t/** OAuth parameters to include as hidden fields */\n\toauthParams: Record<string, string>;\n\t/** User's handle (for display) */\n\tuserHandle?: string;\n\t/** Whether to show a login form instead of consent */\n\tshowLogin?: boolean;\n\t/** Error message to display */\n\terror?: string;\n}\n\n/**\n * Render the consent UI HTML\n * @param options Consent UI options\n * @returns HTML string\n */\nexport function renderConsentUI(options: ConsentUIOptions): string {\n\tconst { client, scope, authorizeUrl, oauthParams, userHandle, showLogin, error } = options;\n\n\tconst clientName = escapeHtml(client.clientName);\n\tconst scopeDescriptions = getScopeDescriptions(scope);\n\tconst logoHtml = client.logoUri\n\t\t? `<img src=\"${escapeHtml(client.logoUri)}\" alt=\"${clientName} logo\" class=\"app-logo\" />`\n\t\t: `<div class=\"app-logo-placeholder\">${clientName.charAt(0).toUpperCase()}</div>`;\n\n\tconst errorHtml = error\n\t\t? `<div class=\"error-message\">${escapeHtml(error)}</div>`\n\t\t: \"\";\n\n\tconst loginFormHtml = showLogin\n\t\t? `\n\t\t\t<div class=\"login-form\">\n\t\t\t\t<p>Sign in to continue</p>\n\t\t\t\t<input type=\"password\" name=\"password\" placeholder=\"Password\" required autocomplete=\"current-password\" />\n\t\t\t</div>\n\t\t`\n\t\t: \"\";\n\n\t// Render OAuth params as hidden form fields\n\tconst hiddenFieldsHtml = Object.entries(oauthParams)\n\t\t.map(([key, value]) => `<input type=\"hidden\" name=\"${escapeHtml(key)}\" value=\"${escapeHtml(value)}\" />`)\n\t\t.join(\"\\n\\t\\t\\t\");\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>Authorize ${clientName}</title>\n\t<style>\n\t\t* {\n\t\t\tbox-sizing: border-box;\n\t\t\tmargin: 0;\n\t\t\tpadding: 0;\n\t\t}\n\n\t\tbody {\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n\t\t\tbackground: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n\t\t\tmin-height: 100vh;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tpadding: 20px;\n\t\t\tcolor: #e0e0e0;\n\t\t}\n\n\t\t.container {\n\t\t\tbackground: #1e1e30;\n\t\t\tborder-radius: 16px;\n\t\t\tpadding: 32px;\n\t\t\tmax-width: 400px;\n\t\t\twidth: 100%;\n\t\t\tbox-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t}\n\n\t\t.header {\n\t\t\ttext-align: center;\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.app-logo {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tborder-radius: 12px;\n\t\t\tmargin-bottom: 16px;\n\t\t\tobject-fit: cover;\n\t\t}\n\n\t\t.app-logo-placeholder {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tborder-radius: 12px;\n\t\t\tmargin: 0 auto 16px;\n\t\t\tbackground: linear-gradient(135deg, #3b82f6, #8b5cf6);\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tfont-size: 28px;\n\t\t\tfont-weight: 600;\n\t\t\tcolor: white;\n\t\t}\n\n\t\th1 {\n\t\t\tfont-size: 20px;\n\t\t\tfont-weight: 600;\n\t\t\tmargin-bottom: 8px;\n\t\t}\n\n\t\t.client-name {\n\t\t\tcolor: #60a5fa;\n\t\t}\n\n\t\t.user-info {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t}\n\n\t\t.permissions {\n\t\t\tbackground: rgba(255, 255, 255, 0.05);\n\t\t\tborder-radius: 12px;\n\t\t\tpadding: 16px;\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.permissions-title {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t\tmargin-bottom: 12px;\n\t\t}\n\n\t\t.permissions-list {\n\t\t\tlist-style: none;\n\t\t}\n\n\t\t.permissions-list li {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: 10px;\n\t\t\tpadding: 8px 0;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\t.permissions-list li::before {\n\t\t\tcontent: \"\";\n\t\t\twidth: 8px;\n\t\t\theight: 8px;\n\t\t\tbackground: #22c55e;\n\t\t\tborder-radius: 50%;\n\t\t\tflex-shrink: 0;\n\t\t}\n\n\t\t.buttons {\n\t\t\tdisplay: flex;\n\t\t\tgap: 12px;\n\t\t}\n\n\t\tbutton {\n\t\t\tflex: 1;\n\t\t\tpadding: 12px 20px;\n\t\t\tborder-radius: 8px;\n\t\t\tfont-size: 14px;\n\t\t\tfont-weight: 500;\n\t\t\tcursor: pointer;\n\t\t\ttransition: all 0.2s;\n\t\t\tborder: none;\n\t\t}\n\n\t\t.btn-deny {\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t\tcolor: #e0e0e0;\n\t\t}\n\n\t\t.btn-deny:hover {\n\t\t\tbackground: rgba(255, 255, 255, 0.15);\n\t\t}\n\n\t\t.btn-allow {\n\t\t\tbackground: linear-gradient(135deg, #3b82f6, #2563eb);\n\t\t\tcolor: white;\n\t\t}\n\n\t\t.btn-allow:hover {\n\t\t\tbackground: linear-gradient(135deg, #2563eb, #1d4ed8);\n\t\t}\n\n\t\t.info {\n\t\t\tmargin-top: 16px;\n\t\t\tfont-size: 12px;\n\t\t\tcolor: #6b7280;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.error-message {\n\t\t\tbackground: rgba(239, 68, 68, 0.1);\n\t\t\tborder: 1px solid rgba(239, 68, 68, 0.3);\n\t\t\tcolor: #f87171;\n\t\t\tpadding: 12px;\n\t\t\tborder-radius: 8px;\n\t\t\tmargin-bottom: 16px;\n\t\t\tfont-size: 14px;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.login-form {\n\t\t\tmargin-bottom: 24px;\n\t\t}\n\n\t\t.login-form p {\n\t\t\tfont-size: 14px;\n\t\t\tcolor: #9ca3af;\n\t\t\tmargin-bottom: 12px;\n\t\t}\n\n\t\t.login-form input {\n\t\t\twidth: 100%;\n\t\t\tpadding: 12px;\n\t\t\tborder-radius: 8px;\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t\tbackground: rgba(255, 255, 255, 0.05);\n\t\t\tcolor: #e0e0e0;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\t.login-form input:focus {\n\t\t\toutline: none;\n\t\t\tborder-color: #3b82f6;\n\t\t}\n\n\t\t.login-form input::placeholder {\n\t\t\tcolor: #6b7280;\n\t\t}\n\n\t\t.client-uri {\n\t\t\tfont-size: 12px;\n\t\t\tcolor: #6b7280;\n\t\t\tmargin-top: 4px;\n\t\t}\n\n\t\t.client-uri a {\n\t\t\tcolor: #60a5fa;\n\t\t\ttext-decoration: none;\n\t\t}\n\n\t\t.client-uri a:hover {\n\t\t\ttext-decoration: underline;\n\t\t}\n\t</style>\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<div class=\"header\">\n\t\t\t${logoHtml}\n\t\t\t<h1>Authorize <span class=\"client-name\">${clientName}</span></h1>\n\t\t\t${userHandle ? `<p class=\"user-info\">as @${escapeHtml(userHandle)}</p>` : \"\"}\n\t\t\t${client.clientUri ? `<p class=\"client-uri\"><a href=\"${escapeHtml(client.clientUri)}\" target=\"_blank\" rel=\"noopener\">${escapeHtml(new URL(client.clientUri).hostname)}</a></p>` : \"\"}\n\t\t</div>\n\n\t\t${errorHtml}\n\n\t\t<form method=\"POST\" action=\"${escapeHtml(authorizeUrl)}\">\n\t\t\t${hiddenFieldsHtml}\n\n\t\t\t${loginFormHtml}\n\n\t\t\t<div class=\"permissions\">\n\t\t\t\t<p class=\"permissions-title\">This app wants to:</p>\n\t\t\t\t<ul class=\"permissions-list\">\n\t\t\t\t\t${scopeDescriptions.map((desc) => `<li>${escapeHtml(desc)}</li>`).join(\"\")}\n\t\t\t\t</ul>\n\t\t\t</div>\n\n\t\t\t<div class=\"buttons\">\n\t\t\t\t<button type=\"submit\" name=\"action\" value=\"deny\" class=\"btn-deny\">Deny</button>\n\t\t\t\t<button type=\"submit\" name=\"action\" value=\"allow\" class=\"btn-allow\">Allow</button>\n\t\t\t</div>\n\t\t</form>\n\n\t\t<p class=\"info\">You can revoke access anytime in your account settings.</p>\n\t</div>\n</body>\n</html>`;\n}\n\n/**\n * Render an error page\n * @param error Error code\n * @param description Error description\n * @param redirectUri Optional redirect URI for the error\n * @returns HTML string\n */\nexport function renderErrorPage(\n\terror: string,\n\tdescription: string,\n\tredirectUri?: string\n): string {\n\tconst escapedError = escapeHtml(error);\n\tconst escapedDescription = escapeHtml(description);\n\n\tconst redirectHtml = redirectUri\n\t\t? `<p style=\"margin-top: 16px;\"><a href=\"${escapeHtml(redirectUri)}\" style=\"color: #60a5fa;\">Return to application</a></p>`\n\t\t: \"\";\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>Authorization Error</title>\n\t<style>\n\t\tbody {\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n\t\t\tbackground: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n\t\t\tmin-height: 100vh;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tpadding: 20px;\n\t\t\tcolor: #e0e0e0;\n\t\t\tmargin: 0;\n\t\t}\n\n\t\t.container {\n\t\t\tbackground: #1e1e30;\n\t\t\tborder-radius: 16px;\n\t\t\tpadding: 32px;\n\t\t\tmax-width: 400px;\n\t\t\twidth: 100%;\n\t\t\tbox-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.error-icon {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t\tbackground: rgba(239, 68, 68, 0.1);\n\t\t\tborder-radius: 50%;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tmargin: 0 auto 16px;\n\t\t\tfont-size: 32px;\n\t\t}\n\n\t\th1 {\n\t\t\tfont-size: 20px;\n\t\t\tmargin-bottom: 8px;\n\t\t\tcolor: #f87171;\n\t\t}\n\n\t\tp {\n\t\t\tcolor: #9ca3af;\n\t\t\tfont-size: 14px;\n\t\t}\n\n\t\tcode {\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t\tpadding: 2px 6px;\n\t\t\tborder-radius: 4px;\n\t\t\tfont-size: 12px;\n\t\t}\n\t</style>\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<div class=\"error-icon\">!</div>\n\t\t<h1>Authorization Error</h1>\n\t\t<p>${escapedDescription}</p>\n\t\t<p style=\"margin-top: 8px;\"><code>${escapedError}</code></p>\n\t\t${redirectHtml}\n\t</div>\n</body>\n</html>`;\n}\n","/**\n * Core OAuth 2.1 Provider with AT Protocol extensions\n * Orchestrates authorization code flow with PKCE, DPoP, and PAR\n */\n\nimport type { OAuthAuthorizationServerMetadata } from \"@atproto/oauth-types\";\nimport type { OAuthStorage, AuthCodeData, TokenData, ClientMetadata } from \"./storage.js\";\nimport { verifyPkceChallenge } from \"./pkce.js\";\nimport { verifyDpopProof, DpopError, generateDpopNonce } from \"./dpop.js\";\nimport { PARHandler } from \"./par.js\";\nimport { ClientResolver } from \"./client-resolver.js\";\nimport {\n\tgenerateAuthCode,\n\tgenerateTokens,\n\trefreshTokens,\n\tbuildTokenResponse,\n\textractAccessToken,\n\tisTokenValid,\n\tAUTH_CODE_TTL,\n} from \"./tokens.js\";\nimport { renderConsentUI, renderErrorPage, CONSENT_UI_CSP } from \"./ui.js\";\n\n/**\n * OAuth provider configuration\n */\nexport interface OAuthProviderConfig {\n\t/** OAuth storage implementation */\n\tstorage: OAuthStorage;\n\t/** The OAuth issuer URL (e.g., https://your-pds.com) */\n\tissuer: string;\n\t/** Whether DPoP is required for all tokens (default: true for AT Protocol) */\n\tdpopRequired?: boolean;\n\t/** Whether PAR is enabled (default: true) */\n\tenablePAR?: boolean;\n\t/** Client resolver for DID-based discovery */\n\tclientResolver?: ClientResolver;\n\t/** Callback to verify user credentials */\n\tverifyUser?: (password: string) => Promise<{ sub: string; handle: string } | null>;\n\t/** Get the current user (if already authenticated) */\n\tgetCurrentUser?: () => Promise<{ sub: string; handle: string } | null>;\n}\n\n/**\n * OAuth error response builder\n */\nfunction oauthError(error: string, description: string, status: number = 400): Response {\n\treturn new Response(\n\t\tJSON.stringify({\n\t\t\terror,\n\t\t\terror_description: description,\n\t\t}),\n\t\t{\n\t\t\tstatus,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t}\n\t);\n}\n\n/**\n * Error thrown when request body parsing fails\n */\nexport class RequestBodyError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"RequestBodyError\";\n\t}\n}\n\n/**\n * Parse request body from JSON or form-urlencoded\n * @throws RequestBodyError if content type is unsupported or parsing fails\n */\nexport async function parseRequestBody(request: Request): Promise<Record<string, string>> {\n\tconst contentType = request.headers.get(\"Content-Type\") ?? \"\";\n\n\ttry {\n\t\tif (contentType.includes(\"application/json\")) {\n\t\t\tconst json = await request.json();\n\t\t\treturn Object.fromEntries(\n\t\t\t\tObject.entries(json as Record<string, unknown>).map(([k, v]) => [k, String(v)])\n\t\t\t);\n\t\t} else if (contentType.includes(\"application/x-www-form-urlencoded\")) {\n\t\t\tconst body = await request.text();\n\t\t\treturn Object.fromEntries(new URLSearchParams(body).entries());\n\t\t} else {\n\t\t\tthrow new RequestBodyError(\n\t\t\t\t\"Content-Type must be application/json or application/x-www-form-urlencoded\"\n\t\t\t);\n\t\t}\n\t} catch (e) {\n\t\tif (e instanceof RequestBodyError) {\n\t\t\tthrow e;\n\t\t}\n\t\tthrow new RequestBodyError(\"Failed to parse request body\");\n\t}\n}\n\n/**\n * AT Protocol OAuth 2.1 Provider\n */\nexport class ATProtoOAuthProvider {\n\tprivate storage: OAuthStorage;\n\tprivate issuer: string;\n\tprivate dpopRequired: boolean;\n\tprivate enablePAR: boolean;\n\tprivate parHandler: PARHandler;\n\tprivate clientResolver: ClientResolver;\n\tprivate verifyUser?: (password: string) => Promise<{ sub: string; handle: string } | null>;\n\tprivate getCurrentUser?: () => Promise<{ sub: string; handle: string } | null>;\n\n\tconstructor(config: OAuthProviderConfig) {\n\t\tthis.storage = config.storage;\n\t\tthis.issuer = config.issuer;\n\t\tthis.dpopRequired = config.dpopRequired ?? true;\n\t\tthis.enablePAR = config.enablePAR ?? true;\n\t\tthis.parHandler = new PARHandler(config.storage, config.issuer);\n\t\tthis.clientResolver = config.clientResolver ?? new ClientResolver({ storage: config.storage });\n\t\tthis.verifyUser = config.verifyUser;\n\t\tthis.getCurrentUser = config.getCurrentUser;\n\t}\n\n\t/**\n\t * Handle authorization request (GET/POST /oauth/authorize)\n\t */\n\tasync handleAuthorize(request: Request): Promise<Response> {\n\t\tconst url = new URL(request.url);\n\n\t\t// Parse OAuth params from query string (GET) or form data (POST)\n\t\tlet params: Record<string, string>;\n\n\t\tif (request.method === \"POST\") {\n\t\t\t// POST: parse from form data (includes hidden fields with OAuth params)\n\t\t\tconst formData = await request.formData();\n\t\t\tparams = {};\n\t\t\tfor (const [key, value] of formData.entries()) {\n\t\t\t\tif (typeof value === \"string\") {\n\t\t\t\t\tparams[key] = value;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// GET: check for PAR or query params\n\t\t\tconst requestUri = url.searchParams.get(\"request_uri\");\n\t\t\tconst clientId = url.searchParams.get(\"client_id\");\n\n\t\t\tif (requestUri && this.enablePAR) {\n\t\t\t\tif (!clientId) {\n\t\t\t\t\treturn this.renderError(\"invalid_request\", \"client_id required with request_uri\");\n\t\t\t\t}\n\t\t\t\tconst parParams = await this.parHandler.retrieveParams(requestUri, clientId);\n\t\t\t\tif (!parParams) {\n\t\t\t\t\treturn this.renderError(\"invalid_request\", \"Invalid or expired request_uri\");\n\t\t\t\t}\n\t\t\t\tparams = parParams;\n\t\t\t} else {\n\t\t\t\t// Parse query parameters\n\t\t\t\tparams = Object.fromEntries(url.searchParams.entries());\n\t\t\t}\n\t\t}\n\n\t\t// Validate required parameters\n\t\tconst required = [\"client_id\", \"redirect_uri\", \"response_type\", \"code_challenge\", \"state\"];\n\t\tfor (const param of required) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn this.renderError(\"invalid_request\", `Missing required parameter: ${param}`);\n\t\t\t}\n\t\t}\n\n\t\t// Validate response_type\n\t\tif (params.response_type !== \"code\") {\n\t\t\treturn this.renderError(\"unsupported_response_type\", \"Only response_type=code is supported\");\n\t\t}\n\n\t\t// Validate code_challenge_method\n\t\tif (params.code_challenge_method && params.code_challenge_method !== \"S256\") {\n\t\t\treturn this.renderError(\"invalid_request\", \"Only code_challenge_method=S256 is supported\");\n\t\t}\n\n\t\t// Resolve client metadata\n\t\tlet client: ClientMetadata;\n\t\ttry {\n\t\t\tclient = await this.clientResolver.resolveClient(params.client_id!);\n\t\t} catch (e) {\n\t\t\treturn this.renderError(\"invalid_client\", `Failed to resolve client: ${e}`);\n\t\t}\n\n\t\t// Validate redirect_uri\n\t\tif (!client.redirectUris.includes(params.redirect_uri!)) {\n\t\t\treturn this.renderError(\"invalid_request\", \"Invalid redirect_uri for this client\");\n\t\t}\n\n\t\t// Handle POST (form submission)\n\t\tif (request.method === \"POST\") {\n\t\t\treturn this.handleAuthorizePost(request, params, client);\n\t\t}\n\n\t\t// Check if user is authenticated\n\t\tlet user: { sub: string; handle: string } | null = null;\n\t\tif (this.getCurrentUser) {\n\t\t\tuser = await this.getCurrentUser();\n\t\t}\n\n\t\t// Show consent UI\n\t\tconst scope = params.scope ?? \"atproto\";\n\t\tconst html = renderConsentUI({\n\t\t\tclient,\n\t\t\tscope,\n\t\t\tauthorizeUrl: url.pathname,\n\t\t\tstate: params.state!,\n\t\t\toauthParams: params,\n\t\t\tuserHandle: user?.handle,\n\t\t\tshowLogin: !user && !!this.verifyUser,\n\t\t});\n\n\t\treturn new Response(html, {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Handle authorization form POST\n\t */\n\tprivate async handleAuthorizePost(\n\t\trequest: Request,\n\t\tparams: Record<string, string>,\n\t\tclient: ClientMetadata\n\t): Promise<Response> {\n\t\t// Form data was already parsed in handleAuthorize - extract action and password\n\t\tconst action = params.action;\n\t\tconst password = params.password ?? null;\n\n\t\tconst redirectUri = params.redirect_uri!;\n\t\tconst state = params.state!;\n\t\tconst responseMode = params.response_mode ?? \"fragment\";\n\n\t\t// Handle deny\n\t\tif (action === \"deny\") {\n\t\t\tconst errorUrl = new URL(redirectUri);\n\n\t\t\tif (responseMode === \"fragment\") {\n\t\t\t\tconst hashParams = new URLSearchParams();\n\t\t\t\thashParams.set(\"error\", \"access_denied\");\n\t\t\t\thashParams.set(\"error_description\", \"User denied authorization\");\n\t\t\t\thashParams.set(\"state\", state);\n\t\t\t\thashParams.set(\"iss\", this.issuer);\n\t\t\t\terrorUrl.hash = hashParams.toString();\n\t\t\t} else {\n\t\t\t\terrorUrl.searchParams.set(\"error\", \"access_denied\");\n\t\t\t\terrorUrl.searchParams.set(\"error_description\", \"User denied authorization\");\n\t\t\t\terrorUrl.searchParams.set(\"state\", state);\n\t\t\t\terrorUrl.searchParams.set(\"iss\", this.issuer);\n\t\t\t}\n\n\t\t\treturn Response.redirect(errorUrl.toString(), 302);\n\t\t}\n\n\t\t// Get or verify user\n\t\tlet user: { sub: string; handle: string } | null = null;\n\n\t\tif (this.getCurrentUser) {\n\t\t\tuser = await this.getCurrentUser();\n\t\t}\n\n\t\tif (!user && password && this.verifyUser) {\n\t\t\tuser = await this.verifyUser(password);\n\t\t}\n\n\t\tif (!user) {\n\t\t\t// Show login form with error\n\t\t\tconst url = new URL(request.url);\n\t\t\tconst scope = params.scope ?? \"atproto\";\n\t\t\tconst html = renderConsentUI({\n\t\t\t\tclient,\n\t\t\t\tscope,\n\t\t\t\tauthorizeUrl: url.pathname,\n\t\t\t\tstate,\n\t\t\t\toauthParams: params,\n\t\t\t\tshowLogin: true,\n\t\t\t\terror: \"Invalid password\",\n\t\t\t});\n\t\t\treturn new Response(html, {\n\t\t\t\tstatus: 401,\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Generate authorization code\n\t\tconst code = generateAuthCode();\n\t\tconst scope = params.scope ?? \"atproto\";\n\n\t\tconst authCodeData: AuthCodeData = {\n\t\t\tclientId: params.client_id!,\n\t\t\tredirectUri,\n\t\t\tcodeChallenge: params.code_challenge!,\n\t\t\tcodeChallengeMethod: \"S256\",\n\t\t\tscope,\n\t\t\tsub: user.sub,\n\t\t\texpiresAt: Date.now() + AUTH_CODE_TTL,\n\t\t};\n\n\t\tawait this.storage.saveAuthCode(code, authCodeData);\n\n\t\t// Redirect with code (using fragment mode if requested)\n\t\tconst successUrl = new URL(redirectUri);\n\n\t\tif (responseMode === \"fragment\") {\n\t\t\t// Put params in hash fragment\n\t\t\tconst hashParams = new URLSearchParams();\n\t\t\thashParams.set(\"code\", code);\n\t\t\thashParams.set(\"state\", state);\n\t\t\thashParams.set(\"iss\", this.issuer);\n\t\t\tsuccessUrl.hash = hashParams.toString();\n\t\t} else {\n\t\t\t// Put params in query string\n\t\t\tsuccessUrl.searchParams.set(\"code\", code);\n\t\t\tsuccessUrl.searchParams.set(\"state\", state);\n\t\t\tsuccessUrl.searchParams.set(\"iss\", this.issuer);\n\t\t}\n\n\t\treturn Response.redirect(successUrl.toString(), 302);\n\t}\n\n\t/**\n\t * Handle token request (POST /oauth/token)\n\t */\n\tasync handleToken(request: Request): Promise<Response> {\n\t\tlet params: Record<string, string>;\n\t\ttry {\n\t\t\tparams = await parseRequestBody(request);\n\t\t} catch (e) {\n\t\t\treturn oauthError(\"invalid_request\", e instanceof Error ? e.message : \"Invalid request\");\n\t\t}\n\n\t\tconst grantType = params.grant_type;\n\n\t\tif (grantType === \"authorization_code\") {\n\t\t\treturn this.handleAuthorizationCodeGrant(request, params);\n\t\t} else if (grantType === \"refresh_token\") {\n\t\t\treturn this.handleRefreshTokenGrant(request, params);\n\t\t} else {\n\t\t\treturn oauthError(\"unsupported_grant_type\", `Unsupported grant_type: ${grantType}`);\n\t\t}\n\t}\n\n\t/**\n\t * Handle authorization code grant\n\t */\n\tprivate async handleAuthorizationCodeGrant(\n\t\trequest: Request,\n\t\tparams: Record<string, string>\n\t): Promise<Response> {\n\t\t// Validate required parameters\n\t\tconst required = [\"code\", \"client_id\", \"redirect_uri\", \"code_verifier\"];\n\t\tfor (const param of required) {\n\t\t\tif (!params[param]) {\n\t\t\t\treturn oauthError(\"invalid_request\", `Missing required parameter: ${param}`);\n\t\t\t}\n\t\t}\n\n\t\t// Get authorization code data\n\t\tconst codeData = await this.storage.getAuthCode(params.code!);\n\t\tif (!codeData) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Invalid or expired authorization code\");\n\t\t}\n\n\t\t// Delete code (one-time use)\n\t\tawait this.storage.deleteAuthCode(params.code!);\n\n\t\t// Verify client_id matches\n\t\tif (codeData.clientId !== params.client_id) {\n\t\t\treturn oauthError(\"invalid_grant\", \"client_id mismatch\");\n\t\t}\n\n\t\t// Verify redirect_uri matches\n\t\tif (codeData.redirectUri !== params.redirect_uri) {\n\t\t\treturn oauthError(\"invalid_grant\", \"redirect_uri mismatch\");\n\t\t}\n\n\t\t// Verify PKCE\n\t\tconst pkceValid = await verifyPkceChallenge(\n\t\t\tparams.code_verifier!,\n\t\t\tcodeData.codeChallenge,\n\t\t\tcodeData.codeChallengeMethod\n\t\t);\n\t\tif (!pkceValid) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Invalid code_verifier\");\n\t\t}\n\n\t\t// Verify DPoP if required\n\t\tlet dpopJkt: string | undefined;\n\t\tif (this.dpopRequired) {\n\t\t\ttry {\n\t\t\t\tconst dpopProof = await verifyDpopProof(request);\n\n\t\t\t\t// Verify jti is unique (replay prevention)\n\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP proof replay detected\");\n\t\t\t\t}\n\n\t\t\t\tdpopJkt = dpopProof.jkt;\n\t\t\t} catch (e) {\n\t\t\t\tif (e instanceof DpopError) {\n\t\t\t\t\t// Check if we need to send a nonce\n\t\t\t\t\tif (e.code === \"use_dpop_nonce\") {\n\t\t\t\t\t\tconst nonce = generateDpopNonce();\n\t\t\t\t\t\treturn new Response(\n\t\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\t\terror: \"use_dpop_nonce\",\n\t\t\t\t\t\t\t\terror_description: \"DPoP nonce required\",\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tstatus: 400,\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\t\t\t\t\"DPoP-Nonce\": nonce,\n\t\t\t\t\t\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", e.message);\n\t\t\t\t}\n\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP verification failed\");\n\t\t\t}\n\t\t} else {\n\t\t\t// Check if DPoP header is present (optional but binding)\n\t\t\tconst dpopHeader = request.headers.get(\"DPoP\");\n\t\t\tif (dpopHeader) {\n\t\t\t\ttry {\n\t\t\t\t\tconst dpopProof = await verifyDpopProof(request);\n\t\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP proof replay detected\");\n\t\t\t\t\t}\n\t\t\t\t\tdpopJkt = dpopProof.jkt;\n\t\t\t\t} catch (e) {\n\t\t\t\t\tif (e instanceof DpopError) {\n\t\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", e.message);\n\t\t\t\t\t}\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP verification failed\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Generate tokens\n\t\tconst { tokens, tokenData } = generateTokens({\n\t\t\tsub: codeData.sub,\n\t\t\tclientId: codeData.clientId,\n\t\t\tscope: codeData.scope,\n\t\t\tdpopJkt,\n\t\t});\n\n\t\t// Save tokens\n\t\tawait this.storage.saveTokens(tokenData);\n\n\t\t// Return token response\n\t\treturn new Response(JSON.stringify(buildTokenResponse(tokens)), {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Handle refresh token grant\n\t */\n\tprivate async handleRefreshTokenGrant(\n\t\trequest: Request,\n\t\tparams: Record<string, string>\n\t): Promise<Response> {\n\t\tconst refreshToken = params.refresh_token;\n\t\tif (!refreshToken) {\n\t\t\treturn oauthError(\"invalid_request\", \"Missing refresh_token parameter\");\n\t\t}\n\n\t\t// Get token data\n\t\tconst existingData = await this.storage.getTokenByRefresh(refreshToken);\n\t\tif (!existingData) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Invalid refresh token\");\n\t\t}\n\n\t\t// Check if token was revoked\n\t\tif (existingData.revoked) {\n\t\t\treturn oauthError(\"invalid_grant\", \"Token has been revoked\");\n\t\t}\n\n\t\t// Verify client_id if provided\n\t\tif (params.client_id && params.client_id !== existingData.clientId) {\n\t\t\treturn oauthError(\"invalid_grant\", \"client_id mismatch\");\n\t\t}\n\n\t\t// Verify DPoP if token was DPoP-bound\n\t\tif (existingData.dpopJkt) {\n\t\t\ttry {\n\t\t\t\tconst dpopProof = await verifyDpopProof(request);\n\n\t\t\t\t// Verify key thumbprint matches\n\t\t\t\tif (dpopProof.jkt !== existingData.dpopJkt) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP key mismatch\");\n\t\t\t\t}\n\n\t\t\t\t// Verify jti is unique\n\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP proof replay detected\");\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tif (e instanceof DpopError) {\n\t\t\t\t\treturn oauthError(\"invalid_dpop_proof\", e.message);\n\t\t\t\t}\n\t\t\t\treturn oauthError(\"invalid_dpop_proof\", \"DPoP verification failed\");\n\t\t\t}\n\t\t}\n\n\t\t// Revoke old tokens\n\t\tawait this.storage.revokeToken(existingData.accessToken);\n\n\t\t// Generate new tokens (with refresh token rotation)\n\t\tconst { tokens, tokenData } = refreshTokens(existingData, true);\n\n\t\t// Save new tokens\n\t\tawait this.storage.saveTokens(tokenData);\n\n\t\t// Return token response\n\t\treturn new Response(JSON.stringify(buildTokenResponse(tokens)), {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Handle PAR request (POST /oauth/par)\n\t */\n\tasync handlePAR(request: Request): Promise<Response> {\n\t\tif (!this.enablePAR) {\n\t\t\treturn oauthError(\"invalid_request\", \"PAR is not enabled\");\n\t\t}\n\t\treturn this.parHandler.handlePushRequest(request);\n\t}\n\n\t/**\n\t * Handle metadata request (GET /.well-known/oauth-authorization-server)\n\t */\n\thandleMetadata(): Response {\n\t\t// URLs are built dynamically so we cast to the schema type\n\t\tconst metadata: OAuthAuthorizationServerMetadata = {\n\t\t\tissuer: this.issuer,\n\t\t\tauthorization_endpoint: `${this.issuer}/oauth/authorize`,\n\t\t\ttoken_endpoint: `${this.issuer}/oauth/token`,\n\t\t\tresponse_types_supported: [\"code\"],\n\t\t\tresponse_modes_supported: [\"fragment\", \"query\"],\n\t\t\tgrant_types_supported: [\"authorization_code\", \"refresh_token\"],\n\t\t\tcode_challenge_methods_supported: [\"S256\"],\n\t\t\ttoken_endpoint_auth_methods_supported: [\"none\"],\n\t\t\tscopes_supported: [\"atproto\", \"transition:generic\", \"transition:chat.bsky\"],\n\t\t\tsubject_types_supported: [\"public\"],\n\t\t\tauthorization_response_iss_parameter_supported: true,\n\t\t\tclient_id_metadata_document_supported: true,\n\t\t\t...(this.enablePAR && {\n\t\t\t\tpushed_authorization_request_endpoint: `${this.issuer}/oauth/par`,\n\t\t\t\trequire_pushed_authorization_requests: false,\n\t\t\t}),\n\t\t\t...(this.dpopRequired && {\n\t\t\t\tdpop_signing_alg_values_supported: [\"ES256\"],\n\t\t\t\ttoken_endpoint_auth_signing_alg_values_supported: [\"ES256\"],\n\t\t\t}),\n\t\t} as OAuthAuthorizationServerMetadata;\n\n\t\treturn new Response(JSON.stringify(metadata), {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"Cache-Control\": \"max-age=3600\",\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Verify an access token from a request\n\t * @param request The HTTP request\n\t * @param requiredScope Optional scope to require\n\t * @returns Token data if valid\n\t */\n\tasync verifyAccessToken(\n\t\trequest: Request,\n\t\trequiredScope?: string\n\t): Promise<TokenData | null> {\n\t\t// Extract token from Authorization header\n\t\tconst tokenInfo = extractAccessToken(request);\n\t\tif (!tokenInfo) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Lookup token\n\t\tconst tokenData = await this.storage.getTokenByAccess(tokenInfo.token);\n\t\tif (!tokenData) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Check validity\n\t\tif (!isTokenValid(tokenData)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Check token type matches\n\t\tif (tokenData.dpopJkt && tokenInfo.type !== \"DPoP\") {\n\t\t\treturn null; // DPoP-bound token must use DPoP header\n\t\t}\n\n\t\t// Verify DPoP if token is bound\n\t\tif (tokenData.dpopJkt) {\n\t\t\ttry {\n\t\t\t\tconst dpopProof = await verifyDpopProof(request, {\n\t\t\t\t\taccessToken: tokenInfo.token,\n\t\t\t\t});\n\n\t\t\t\t// Verify key thumbprint matches\n\t\t\t\tif (dpopProof.jkt !== tokenData.dpopJkt) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\t// Verify jti is unique\n\t\t\t\tconst nonceUnique = await this.storage.checkAndSaveNonce(dpopProof.jti);\n\t\t\t\tif (!nonceUnique) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\t// Check scope if required\n\t\tif (requiredScope) {\n\t\t\tconst scopes = tokenData.scope.split(\" \");\n\t\t\tif (!scopes.includes(requiredScope)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\treturn tokenData;\n\t}\n\n\t/**\n\t * Render an error page\n\t */\n\tprivate renderError(error: string, description: string): Response {\n\t\tconst html = renderErrorPage(error, description);\n\t\treturn new Response(html, {\n\t\t\tstatus: 400,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\t\"Content-Security-Policy\": CONSENT_UI_CSP,\n\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t},\n\t\t});\n\t}\n}\n","/**\n * OAuth storage interface and types\n * Defines the storage abstraction for auth codes, tokens, clients, etc.\n */\n\n/**\n * Data stored with an authorization code\n */\nexport interface AuthCodeData {\n\t/** Client DID that requested the code */\n\tclientId: string;\n\t/** Redirect URI used in the authorization request */\n\tredirectUri: string;\n\t/** PKCE code challenge */\n\tcodeChallenge: string;\n\t/** PKCE challenge method (always S256 for AT Protocol) */\n\tcodeChallengeMethod: \"S256\";\n\t/** Authorized scope */\n\tscope: string;\n\t/** User DID that authorized the request */\n\tsub: string;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n}\n\n/**\n * Data stored with access and refresh tokens\n */\nexport interface TokenData {\n\t/** Opaque access token */\n\taccessToken: string;\n\t/** Opaque refresh token */\n\trefreshToken: string;\n\t/** Client DID that received the token */\n\tclientId: string;\n\t/** User DID the token is for */\n\tsub: string;\n\t/** Authorized scope */\n\tscope: string;\n\t/** DPoP key thumbprint (for token binding) */\n\tdpopJkt?: string;\n\t/** Issuance timestamp (Unix ms) */\n\tissuedAt: number;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n\t/** Whether the token has been revoked */\n\trevoked?: boolean;\n}\n\n/**\n * OAuth client metadata (discovered from DID document)\n */\nexport interface ClientMetadata {\n\t/** Client DID */\n\tclientId: string;\n\t/** Human-readable client name */\n\tclientName: string;\n\t/** Allowed redirect URIs */\n\tredirectUris: string[];\n\t/** Client logo URI (optional) */\n\tlogoUri?: string;\n\t/** Client homepage URI (optional) */\n\tclientUri?: string;\n\t/** When the metadata was cached (Unix ms) */\n\tcachedAt?: number;\n}\n\n/**\n * Data stored for Pushed Authorization Requests (PAR)\n */\nexport interface PARData {\n\t/** Client DID that pushed the request */\n\tclientId: string;\n\t/** All OAuth parameters from the push request */\n\tparams: Record<string, string>;\n\t/** Expiration timestamp (Unix ms) */\n\texpiresAt: number;\n}\n\n/**\n * Storage interface for OAuth data\n * Implementations should handle TTL-based expiration\n */\nexport interface OAuthStorage {\n\t// ============================================\n\t// Authorization Codes (5 min TTL)\n\t// ============================================\n\n\t/**\n\t * Save an authorization code\n\t * @param code The authorization code\n\t * @param data Associated data\n\t */\n\tsaveAuthCode(code: string, data: AuthCodeData): Promise<void>;\n\n\t/**\n\t * Get authorization code data\n\t * @param code The authorization code\n\t * @returns The data or null if not found/expired\n\t */\n\tgetAuthCode(code: string): Promise<AuthCodeData | null>;\n\n\t/**\n\t * Delete an authorization code (after use)\n\t * @param code The authorization code\n\t */\n\tdeleteAuthCode(code: string): Promise<void>;\n\n\t// ============================================\n\t// Tokens\n\t// ============================================\n\n\t/**\n\t * Save token data\n\t * @param data The token data\n\t */\n\tsaveTokens(data: TokenData): Promise<void>;\n\n\t/**\n\t * Get token data by access token\n\t * @param accessToken The access token\n\t * @returns The data or null if not found/expired/revoked\n\t */\n\tgetTokenByAccess(accessToken: string): Promise<TokenData | null>;\n\n\t/**\n\t * Get token data by refresh token\n\t * @param refreshToken The refresh token\n\t * @returns The data or null if not found/expired/revoked\n\t */\n\tgetTokenByRefresh(refreshToken: string): Promise<TokenData | null>;\n\n\t/**\n\t * Revoke a token by access token\n\t * @param accessToken The access token to revoke\n\t */\n\trevokeToken(accessToken: string): Promise<void>;\n\n\t/**\n\t * Revoke all tokens for a user (for logout)\n\t * @param sub The user DID\n\t */\n\trevokeAllTokens?(sub: string): Promise<void>;\n\n\t// ============================================\n\t// Clients (DID-based, cached)\n\t// ============================================\n\n\t/**\n\t * Save client metadata (cached from DID document)\n\t * @param clientId The client DID\n\t * @param metadata The client metadata\n\t */\n\tsaveClient(clientId: string, metadata: ClientMetadata): Promise<void>;\n\n\t/**\n\t * Get cached client metadata\n\t * @param clientId The client DID\n\t * @returns The metadata or null if not cached\n\t */\n\tgetClient(clientId: string): Promise<ClientMetadata | null>;\n\n\t// ============================================\n\t// PAR Requests (90 sec TTL)\n\t// ============================================\n\n\t/**\n\t * Save PAR request data\n\t * @param requestUri The unique request URI\n\t * @param data The PAR data\n\t */\n\tsavePAR(requestUri: string, data: PARData): Promise<void>;\n\n\t/**\n\t * Get PAR request data\n\t * @param requestUri The request URI\n\t * @returns The data or null if not found/expired\n\t */\n\tgetPAR(requestUri: string): Promise<PARData | null>;\n\n\t/**\n\t * Delete PAR request (after use - one-time use)\n\t * @param requestUri The request URI\n\t */\n\tdeletePAR(requestUri: string): Promise<void>;\n\n\t// ============================================\n\t// DPoP Nonces (5 min TTL, replay prevention)\n\t// ============================================\n\n\t/**\n\t * Check if a nonce has been used and save it if not\n\t * Used for DPoP replay prevention\n\t * @param nonce The nonce to check\n\t * @returns true if the nonce is new (valid), false if already used\n\t */\n\tcheckAndSaveNonce(nonce: string): Promise<boolean>;\n}\n\n/**\n * In-memory storage implementation for testing\n */\nexport class InMemoryOAuthStorage implements OAuthStorage {\n\tprivate authCodes = new Map<string, AuthCodeData>();\n\tprivate tokens = new Map<string, TokenData>();\n\tprivate refreshTokenIndex = new Map<string, string>(); // refreshToken -> accessToken\n\tprivate clients = new Map<string, ClientMetadata>();\n\tprivate parRequests = new Map<string, PARData>();\n\tprivate nonces = new Set<string>();\n\n\tasync saveAuthCode(code: string, data: AuthCodeData): Promise<void> {\n\t\tthis.authCodes.set(code, data);\n\t}\n\n\tasync getAuthCode(code: string): Promise<AuthCodeData | null> {\n\t\tconst data = this.authCodes.get(code);\n\t\tif (!data) return null;\n\t\tif (Date.now() > data.expiresAt) {\n\t\t\tthis.authCodes.delete(code);\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync deleteAuthCode(code: string): Promise<void> {\n\t\tthis.authCodes.delete(code);\n\t}\n\n\tasync saveTokens(data: TokenData): Promise<void> {\n\t\tthis.tokens.set(data.accessToken, data);\n\t\tthis.refreshTokenIndex.set(data.refreshToken, data.accessToken);\n\t}\n\n\tasync getTokenByAccess(accessToken: string): Promise<TokenData | null> {\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (!data) return null;\n\t\tif (data.revoked || Date.now() > data.expiresAt) {\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync getTokenByRefresh(refreshToken: string): Promise<TokenData | null> {\n\t\tconst accessToken = this.refreshTokenIndex.get(refreshToken);\n\t\tif (!accessToken) return null;\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (!data) return null;\n\t\tif (data.revoked) return null;\n\t\t// Refresh tokens don't use accessToken expiresAt\n\t\treturn data;\n\t}\n\n\tasync revokeToken(accessToken: string): Promise<void> {\n\t\tconst data = this.tokens.get(accessToken);\n\t\tif (data) {\n\t\t\tdata.revoked = true;\n\t\t}\n\t}\n\n\tasync revokeAllTokens(sub: string): Promise<void> {\n\t\tfor (const [, data] of this.tokens) {\n\t\t\tif (data.sub === sub) {\n\t\t\t\tdata.revoked = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync saveClient(clientId: string, metadata: ClientMetadata): Promise<void> {\n\t\tthis.clients.set(clientId, metadata);\n\t}\n\n\tasync getClient(clientId: string): Promise<ClientMetadata | null> {\n\t\treturn this.clients.get(clientId) ?? null;\n\t}\n\n\tasync savePAR(requestUri: string, data: PARData): Promise<void> {\n\t\tthis.parRequests.set(requestUri, data);\n\t}\n\n\tasync getPAR(requestUri: string): Promise<PARData | null> {\n\t\tconst data = this.parRequests.get(requestUri);\n\t\tif (!data) return null;\n\t\tif (Date.now() > data.expiresAt) {\n\t\t\tthis.parRequests.delete(requestUri);\n\t\t\treturn null;\n\t\t}\n\t\treturn data;\n\t}\n\n\tasync deletePAR(requestUri: string): Promise<void> {\n\t\tthis.parRequests.delete(requestUri);\n\t}\n\n\tasync checkAndSaveNonce(nonce: string): Promise<boolean> {\n\t\tif (this.nonces.has(nonce)) {\n\t\t\treturn false;\n\t\t}\n\t\tthis.nonces.add(nonce);\n\t\t// Note: No auto-cleanup in test implementation - use clear() between tests\n\t\t// Production SQLite storage handles TTL-based cleanup properly\n\t\treturn true;\n\t}\n\n\t/** Clear all stored data (for testing) */\n\tclear(): void {\n\t\tthis.authCodes.clear();\n\t\tthis.tokens.clear();\n\t\tthis.refreshTokenIndex.clear();\n\t\tthis.clients.clear();\n\t\tthis.parRequests.clear();\n\t\tthis.nonces.clear();\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;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,cAAc;;;;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,eAAe,UAClB,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;AACrD,OAAI,UAAU,OAAO,YAAY,KAAK,KAAK,GAAG,OAAO,WAAW,KAAK,SACpE,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,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;;;;;;ACpMnC,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;;;;;;;;;;;;;;;ACtMR,MAAa,iBACZ;;;;AAKD,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;;;;;AAM1B,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;;;;;;;AA8BR,SAAgB,gBAAgB,SAAmC;CAClE,MAAM,EAAE,QAAQ,OAAO,cAAc,aAAa,YAAY,WAAW,UAAU;CAEnF,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;;;;;MAMA;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA6M1B,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;;;;;;;;;;;;;;;;;;;;;;AAuBhF,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;;;;;;;;;;;ACjZjB,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;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;;;;;CAM9B,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,KAAK,YAAY,mBAAmB,sCAAsC;IAElF,MAAM,YAAY,MAAM,KAAK,WAAW,eAAe,YAAY,SAAS;AAC5E,QAAI,CAAC,UACJ,QAAO,KAAK,YAAY,mBAAmB,iCAAiC;AAE7E,aAAS;SAGT,UAAS,OAAO,YAAY,IAAI,aAAa,SAAS,CAAC;;AAMzD,OAAK,MAAM,SADM;GAAC;GAAa;GAAgB;GAAiB;GAAkB;GAAQ,CAEzF,KAAI,CAAC,OAAO,OACX,QAAO,KAAK,YAAY,mBAAmB,+BAA+B,QAAQ;AAKpF,MAAI,OAAO,kBAAkB,OAC5B,QAAO,KAAK,YAAY,6BAA6B,uCAAuC;AAI7F,MAAI,OAAO,yBAAyB,OAAO,0BAA0B,OACpE,QAAO,KAAK,YAAY,mBAAmB,+CAA+C;EAI3F,IAAIC;AACJ,MAAI;AACH,YAAS,MAAM,KAAK,eAAe,cAAc,OAAO,UAAW;WAC3D,GAAG;AACX,UAAO,KAAK,YAAY,kBAAkB,6BAA6B,IAAI;;AAI5E,MAAI,CAAC,OAAO,aAAa,SAAS,OAAO,aAAc,CACtD,QAAO,KAAK,YAAY,mBAAmB,uCAAuC;AAInF,MAAI,QAAQ,WAAW,OACtB,QAAO,KAAK,oBAAoB,SAAS,QAAQ,OAAO;EAIzD,IAAIC,OAA+C;AACnD,MAAI,KAAK,eACR,QAAO,MAAM,KAAK,gBAAgB;EAInC,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,CAAC;AAEF,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;EACrB,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,IAAIA,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;AACF,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,MAAMC,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,IAAIH;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;EAK9E,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,IAAII;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;EAIxE,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,0BAA0B,CAAC,OAAO;GAClC,0BAA0B,CAAC,YAAY,QAAQ;GAC/C,uBAAuB,CAAC,sBAAsB,gBAAgB;GAC9D,kCAAkC,CAAC,OAAO;GAC1C,uCAAuC,CAAC,OAAO;GAC/C,kBAAkB;IAAC;IAAW;IAAsB;IAAuB;GAC3E,yBAAyB,CAAC,SAAS;GACnC,gDAAgD;GAChD,uCAAuC;GACvC,GAAI,KAAK,aAAa;IACrB,uCAAuC,GAAG,KAAK,OAAO;IACtD,uCAAuC;IACvC;GACD,GAAI,KAAK,gBAAgB;IACxB,mCAAmC,CAAC,QAAQ;IAC5C,kDAAkD,CAAC,QAAQ;IAC3D;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;;;;;CAMR,AAAQ,YAAY,OAAe,aAA+B;EACjE,MAAM,OAAO,gBAAgB,OAAO,YAAY;AAChD,SAAO,IAAI,SAAS,MAAM;GACzB,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,2BAA2B;IAC3B,iBAAiB;IACjB;GACD,CAAC;;;;;;;;;ACrdJ,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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getcirrus/oauth-provider",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OAuth 2.1 Provider with AT Protocol extensions for Cloudflare Workers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@atproto/oauth-types": "^0.5.2",
|
|
18
|
+
"@atproto/syntax": "^0.4.2",
|
|
19
|
+
"jose": "^6.1.3"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
23
|
+
"@cloudflare/workers-types": "^4.20251225.0",
|
|
24
|
+
"publint": "^0.3.16",
|
|
25
|
+
"tsdown": "^0.18.3",
|
|
26
|
+
"typescript": "^5.9.3",
|
|
27
|
+
"vitest": "^4.0.0"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/ascorbic/cirrus.git",
|
|
32
|
+
"directory": "packages/oauth-provider"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/ascorbic/cirrus",
|
|
35
|
+
"keywords": [
|
|
36
|
+
"atproto",
|
|
37
|
+
"bluesky",
|
|
38
|
+
"oauth",
|
|
39
|
+
"oauth2.1",
|
|
40
|
+
"dpop",
|
|
41
|
+
"pkce",
|
|
42
|
+
"cloudflare-workers"
|
|
43
|
+
],
|
|
44
|
+
"author": "Matt Kane",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsdown",
|
|
48
|
+
"dev": "tsdown --watch",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"check": "publint && attw --pack --ignore-rules=cjs-resolves-to-esm"
|
|
51
|
+
}
|
|
52
|
+
}
|