@opendatalabs/vana-sdk
Version:
A TypeScript library for interacting with Vana Network smart contracts.
250 lines • 8.96 kB
JavaScript
;
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