agents
Version:
A home for your AI agents
293 lines (292 loc) • 10.4 kB
JavaScript
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