UNPKG

@opendatalabs/vana-sdk

Version:

A TypeScript library for interacting with Vana Network smart contracts.

250 lines 8.96 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var oauth_client_exports = {}; __export(oauth_client_exports, { OAuthClient: () => OAuthClient }); module.exports = __toCommonJS(oauth_client_exports); var import_pkce = require("./pkce"); var import_token_store = require("./token-store"); const VERIFIER_TTL_SECONDS = 600; const RESERVED_AUTHORIZE_PARAMS = /* @__PURE__ */ new Set([ "response_type", "client_id", "redirect_uri", "scope", "state", "code_challenge", "code_challenge_method" ]); class OAuthClient { #config; constructor(config) { const fetchImpl = config.fetchImpl ?? globalThis.fetch; if (typeof fetchImpl !== "function") { throw new TypeError( "OAuthClient requires a global `fetch` or an explicit `fetchImpl`" ); } this.#config = { authorizationEndpoint: config.authorizationEndpoint, tokenEndpoint: config.tokenEndpoint, clientId: config.clientId, redirectUri: config.redirectUri, scope: config.scope, tokenStore: config.tokenStore ?? new import_token_store.InMemoryTokenStore(), fetchImpl, generateState: config.generateState ?? defaultGenerateState }; } /** Build the authorize URL and persist the PKCE verifier keyed by `state`. */ async buildAuthorizationUrl(opts = {}) { const state = opts.state ?? this.#config.generateState(); const scope = opts.scope ?? this.#config.scope; const verifier = (0, import_pkce.generatePkceVerifier)(); const challenge = await (0, import_pkce.computePkceChallenge)(verifier); await this.#config.tokenStore.set(this.#verifierKey(state), { token: verifier, expiresAt: Math.floor(Date.now() / 1e3) + VERIFIER_TTL_SECONDS }); const params = new URLSearchParams(); params.set("response_type", "code"); params.set("client_id", this.#config.clientId); params.set("redirect_uri", this.#config.redirectUri); if (scope !== void 0 && scope.length > 0) { params.set("scope", scope); } params.set("state", state); params.set("code_challenge", challenge); params.set("code_challenge_method", "S256"); if (opts.extraParams !== void 0) { for (const k of Object.keys(opts.extraParams)) { if (RESERVED_AUTHORIZE_PARAMS.has(k)) { throw new Error( `extraParams may not override the reserved OAuth/PKCE parameter "${k}"` ); } } for (const [k, v] of Object.entries(opts.extraParams)) { params.set(k, v); } } const sep = this.#config.authorizationEndpoint.includes("?") ? "&" : "?"; const url = `${this.#config.authorizationEndpoint}${sep}${params.toString()}`; return { url, state }; } /** * Handle the redirect-callback URL. Validates `state`, retrieves the saved * verifier, exchanges the authorization code + verifier for tokens, and * persists them. Returns the access {@link TokenRecord}. */ async handleCallback(callbackUrl) { const parsed = new URL(callbackUrl); const params = parsed.searchParams; const errorCode = params.get("error"); if (errorCode !== null) { throw new Error( formatOAuthError({ error: errorCode, error_description: params.get("error_description") ?? void 0 }) ); } const code = params.get("code"); const state = params.get("state"); if (code === null || state === null) { throw new Error("OAuth callback is missing `code` or `state`"); } const verifierRecord = await this.#config.tokenStore.get( this.#verifierKey(state) ); if (verifierRecord === null) { throw new Error( "OAuth callback state does not match any in-flight verifier (possible CSRF or expired flow)" ); } const body = new URLSearchParams(); body.set("grant_type", "authorization_code"); body.set("code", code); body.set("redirect_uri", this.#config.redirectUri); body.set("client_id", this.#config.clientId); body.set("code_verifier", verifierRecord.token); let tokens; try { tokens = await this.#tokenRequest(body); } finally { await this.#config.tokenStore.delete(this.#verifierKey(state)); } return this.#persistTokens(tokens); } /** * Exchange a stored refresh token for a fresh access token. Throws if no * refresh token is available. */ async refresh() { const refreshRecord = await this.#config.tokenStore.get(this.#refreshKey()); if (refreshRecord === null) { throw new Error("OAuth refresh failed: no refresh token stored"); } const body = new URLSearchParams(); body.set("grant_type", "refresh_token"); body.set("refresh_token", refreshRecord.token); body.set("client_id", this.#config.clientId); const tokens = await this.#tokenRequest(body); return this.#persistTokens(tokens, refreshRecord.token); } /** * Get the current access token if valid (refreshing first if expired and a * refresh token is available). Returns `null` when no usable token exists. */ async getAccessToken() { const stored = await this.#config.tokenStore.get(this.#accessKey()); if (stored !== null) return stored.token; const refresh = await this.#config.tokenStore.get(this.#refreshKey()); if (refresh === null) return null; try { const refreshed = await this.refresh(); return refreshed.token; } catch { return null; } } /** Forget tokens (logout). Does NOT call any remote revocation endpoint. */ async signOut() { await this.#config.tokenStore.delete(this.#accessKey()); await this.#config.tokenStore.delete(this.#refreshKey()); } #accessKey() { return `oauth:tokens:${this.#config.clientId}`; } #refreshKey() { return `oauth:refresh:${this.#config.clientId}`; } #verifierKey(state) { return `oauth:verifier:${state}`; } async #tokenRequest(body) { const response = await this.#config.fetchImpl(this.#config.tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" }, body: body.toString() }); const text = await response.text(); const parsed = parseJsonBody(text); if (!response.ok) { throw new Error(formatOAuthError(parsed ?? {}, response.status)); } if (parsed === null || typeof parsed !== "object" || typeof parsed.access_token !== "string") { throw new Error( "OAuth token endpoint returned a response without an `access_token` string" ); } return parsed; } async #persistTokens(tokens, previousRefreshToken) { const record = { token: tokens.access_token }; if (typeof tokens.expires_in === "number" && tokens.expires_in > 0) { record.expiresAt = Math.floor(Date.now() / 1e3) + tokens.expires_in; } await this.#config.tokenStore.set(this.#accessKey(), record); const newRefresh = tokens.refresh_token ?? previousRefreshToken; if (newRefresh !== void 0) { await this.#config.tokenStore.set(this.#refreshKey(), { token: newRefresh }); } return record; } } function defaultGenerateState() { const bytes = new Uint8Array(24); crypto.getRandomValues(bytes); let binary = ""; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function parseJsonBody(text) { if (text.length === 0) return null; try { return JSON.parse(text); } catch { return null; } } function formatOAuthError(body, status) { const parts = ["OAuth token request failed"]; if (status !== void 0) parts.push(`(HTTP ${String(status)})`); if (body.error !== void 0 && body.error.length > 0) { parts.push(`: ${body.error}`); if (body.error_description !== void 0 && body.error_description.length > 0) { parts.push(`- ${body.error_description}`); } } return parts.join(" ").replace(" : ", ": ").replace(" - ", " - "); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { OAuthClient }); //# sourceMappingURL=oauth-client.cjs.map