UNPKG

agents

Version:

A home for your AI agents

293 lines (292 loc) 10.4 kB
import { AsyncLocalStorage } from "node:async_hooks"; import { nanoid } from "nanoid"; //#region src/mcp/do-oauth-client-provider.ts const STATE_EXPIRATION_MS = 600 * 1e3; const codeVerifierStateStorage = new AsyncLocalStorage(); function parseOAuthState(state) { const parts = state.split("."); if (parts.length !== 2) return; const [nonce, serverId] = parts; if (!nonce || !serverId) return; return { nonce, serverId }; } function base64UrlEncode(bytes) { let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } async function createCodeChallenge(verifier) { const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)); return base64UrlEncode(new Uint8Array(digest)); } function isExpired(createdAt) { return Date.now() - createdAt > STATE_EXPIRATION_MS; } var DurableObjectOAuthClientProvider = class { constructor(storage, clientName, baseRedirectUrl) { this.storage = storage; this.clientName = clientName; this.baseRedirectUrl = baseRedirectUrl; if (!storage) throw new Error("DurableObjectOAuthClientProvider requires a valid DurableObjectStorage instance"); } get clientMetadata() { return { client_name: this.clientName, client_uri: this.clientUri, grant_types: ["authorization_code", "refresh_token"], redirect_uris: [this.redirectUrl], response_types: ["code"], token_endpoint_auth_method: "none" }; } get clientUri() { return new URL(this.redirectUrl).origin; } get redirectUrl() { return this.baseRedirectUrl; } get clientId() { if (!this._clientId_) throw new Error("Trying to access clientId before it was set"); return this._clientId_; } set clientId(clientId_) { this._clientId_ = clientId_; } get serverId() { if (!this._serverId_) throw new Error("Trying to access serverId before it was set"); return this._serverId_; } set serverId(serverId_) { this._serverId_ = serverId_; } keyPrefix(clientId) { return `/${this.clientName}/${this.serverId}/${clientId}`; } clientInfoKey(clientId) { return `${this.keyPrefix(clientId)}/client_info/`; } discoveryStateKey() { return `/${this.clientName}/${this.serverId}/oauth_discovery`; } async saveDiscoveryState(state) { await this.storage.put(this.discoveryStateKey(), state); } async discoveryState() { return await this.storage.get(this.discoveryStateKey()) ?? void 0; } async clientInformation(_context) { if (!this._clientId_) return void 0; return await this.storage.get(this.clientInfoKey(this.clientId)) ?? void 0; } async saveClientInformation(clientInformation, _context) { this.clientId = clientInformation.client_id; await this.storage.put(this.clientInfoKey(clientInformation.client_id), clientInformation); } tokenKey(clientId) { return `${this.keyPrefix(clientId)}/token`; } async tokens(_context) { if (!this._clientId_) return void 0; return await this.storage.get(this.tokenKey(this.clientId)) ?? void 0; } async saveTokens(tokens, _context) { await this.storage.put(this.tokenKey(this.clientId), tokens); await this.storage.delete(this.discoveryStateKey()); } get authUrl() { return this._authUrl_; } stateKey(nonce) { return `/${this.clientName}/${this.serverId}/state/${nonce}`; } async state() { const nonce = nanoid(); const state = `${nonce}.${this.serverId}`; const storedState = { nonce, serverId: this.serverId, createdAt: Date.now() }; await this.storage.put(this.stateKey(nonce), storedState); return state; } async checkState(state) { const parsed = parseOAuthState(state); if (!parsed) return { valid: false, error: "Invalid state format" }; const { nonce, serverId } = parsed; const key = this.stateKey(nonce); const storedState = await this.storage.get(key); if (!storedState) return { valid: false, error: "State not found or already used" }; if (storedState.serverId !== serverId) { await this.storage.delete(key); return { valid: false, error: "State serverId mismatch" }; } if (isExpired(storedState.createdAt)) { const deleteKeys = [key]; if (this._clientId_) deleteKeys.push(this.stateCodeVerifierKey(this.clientId, nonce)); await this.storage.delete(deleteKeys); return { valid: false, error: "State expired" }; } return { valid: true, serverId }; } async consumeState(state) { const parsed = parseOAuthState(state); if (!parsed) { console.warn(`[OAuth] consumeState called with invalid state format`); return; } await this.storage.delete(this.stateKey(parsed.nonce)); } async redirectToAuthorization(authUrl) { this._authUrl_ = authUrl.toString(); const clientId = this._clientId_; const serverId = this._serverId_; if (!clientId || !serverId) return; const state = authUrl.searchParams.get("state"); const codeChallenge = authUrl.searchParams.get("code_challenge"); if (!state || !codeChallenge) return; const parsed = parseOAuthState(state); if (!parsed || parsed.serverId !== serverId) return; const challengeKey = this.challengeCodeVerifierKey(clientId, codeChallenge); const pendingVerifier = await this.storage.get(challengeKey); if (!pendingVerifier) return; if (isExpired(pendingVerifier.createdAt)) { await this.storage.delete(challengeKey); return; } await this.storage.put(this.stateCodeVerifierKey(clientId, parsed.nonce), pendingVerifier); await this.storage.delete(challengeKey); } async invalidateCredentials(scope) { const deleteKeys = []; if (scope === "all" || scope === "discovery") deleteKeys.push(this.discoveryStateKey()); if (this._clientId_) { const clientId = this.clientId; if (scope === "all" || scope === "client") deleteKeys.push(this.clientInfoKey(clientId)); if (scope === "all" || scope === "tokens") deleteKeys.push(this.tokenKey(clientId)); if (scope === "all" || scope === "verifier") deleteKeys.push(...await this.codeVerifierKeys(clientId, { includeChallengeKeys: true })); } if (deleteKeys.length > 0) await this.storage.delete([...new Set(deleteKeys)]); } codeVerifierKey(clientId) { return `${this.keyPrefix(clientId)}/code_verifier`; } stateCodeVerifierPrefix(clientId) { return `${this.keyPrefix(clientId)}/code_verifier/`; } stateCodeVerifierKey(clientId, nonce) { return `${this.stateCodeVerifierPrefix(clientId)}${nonce}`; } challengeCodeVerifierPrefix(clientId) { return `${this.keyPrefix(clientId)}/code_verifier_challenge/`; } challengeCodeVerifierKey(clientId, codeChallenge) { return `${this.challengeCodeVerifierPrefix(clientId)}${codeChallenge}`; } async codeVerifierKeys(clientId, options = {}) { const legacyKey = this.codeVerifierKey(clientId); const keys = []; if (await this.storage.get(legacyKey)) keys.push(legacyKey); const stateKeys = await this.storage.list({ prefix: this.stateCodeVerifierPrefix(clientId) }); keys.push(...stateKeys.keys()); if (options.includeChallengeKeys) { const challengeKeys = await this.storage.list({ prefix: this.challengeCodeVerifierPrefix(clientId) }); keys.push(...challengeKeys.keys()); } return keys; } async saveCodeVerifier(verifier) { await this.deleteExpiredChallengeCodeVerifiers(this.clientId); const codeChallenge = await createCodeChallenge(verifier); const storedVerifier = { verifier, createdAt: Date.now() }; await this.storage.put(this.challengeCodeVerifierKey(this.clientId, codeChallenge), storedVerifier); } async deleteExpiredChallengeCodeVerifiers(clientId) { const expiredKeys = [...(await this.storage.list({ prefix: this.challengeCodeVerifierPrefix(clientId) })).entries()].filter(([, storedVerifier]) => isExpired(storedVerifier.createdAt)).map(([key]) => key); if (expiredKeys.length > 0) await this.storage.delete(expiredKeys); } async codeVerifier() { const context = codeVerifierStateStorage.getStore(); if (context) { const stateVerifier = await this.codeVerifierForState(context.state); if (stateVerifier) { context.servedKey = stateVerifier.key; return stateVerifier.verifier; } } const legacyVerifier = await this.storage.get(this.codeVerifierKey(this.clientId)); if (legacyVerifier) { if (context) context.servedKey = this.codeVerifierKey(this.clientId); return legacyVerifier; } if (context) throw new Error("No code verifier found for OAuth state"); const pendingVerifiers = await this.storage.list({ prefix: this.stateCodeVerifierPrefix(this.clientId) }); const unexpiredPendingVerifiers = [...pendingVerifiers.entries()].filter(([, storedVerifier]) => !isExpired(storedVerifier.createdAt)); const expiredKeys = [...pendingVerifiers.entries()].filter(([, storedVerifier]) => isExpired(storedVerifier.createdAt)).map(([key]) => key); if (expiredKeys.length > 0) await this.storage.delete(expiredKeys); if (unexpiredPendingVerifiers.length === 1) { const [[, storedVerifier]] = unexpiredPendingVerifiers; return storedVerifier.verifier; } if (unexpiredPendingVerifiers.length > 1) throw new Error("Multiple OAuth code verifiers are pending; complete authorization with the callback state"); throw new Error("No code verifier found"); } async codeVerifierForState(state) { const parsed = parseOAuthState(state); if (!parsed) throw new Error("Invalid state format"); const key = this.stateCodeVerifierKey(this.clientId, parsed.nonce); const storedVerifier = await this.storage.get(key); if (!storedVerifier) return; if (isExpired(storedVerifier.createdAt)) { await this.storage.delete(key); throw new Error("Code verifier expired"); } return { key, verifier: storedVerifier.verifier }; } async runWithCodeVerifierState(state, callback) { return codeVerifierStateStorage.run({ state }, callback); } async deleteCodeVerifier() { const context = codeVerifierStateStorage.getStore(); if (context?.servedKey) { await this.storage.delete(context.servedKey); return; } if (context) { const parsed = parseOAuthState(context.state); if (parsed) { await this.storage.delete(this.stateCodeVerifierKey(this.clientId, parsed.nonce)); return; } } const keys = await this.codeVerifierKeys(this.clientId); if (keys.length > 0) await this.storage.delete(keys); } }; //#endregion export { DurableObjectOAuthClientProvider }; //# sourceMappingURL=do-oauth-client-provider.js.map