UNPKG

@opendatalabs/vana-sdk

Version:

A TypeScript library for interacting with Vana Network smart contracts.

129 lines 4.32 kB
import { recoverMessageAddress } from "viem"; import { fromBase64 } from "../utils/encoding.js"; import { MissingAuthError, InvalidSignatureError, ExpiredTokenError } from "./errors.js"; import { computeBodyHash } from "./web3-signed-builder.js"; const WEB3_SIGNED_PREFIX = "Web3Signed "; const CLOCK_SKEW_SECONDS = 60; function base64urlDecode(input) { let base64 = input.replace(/-/g, "+").replace(/_/g, "/"); const padLength = (4 - base64.length % 4) % 4; base64 += "=".repeat(padLength); return new TextDecoder().decode(fromBase64(base64)); } function isFiniteNumber(value) { return typeof value === "number" && Number.isFinite(value); } function parsePayload(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) { throw new InvalidSignatureError({ reason: "Invalid payload shape" }); } const payload = value; if (typeof payload["aud"] !== "string" || typeof payload["method"] !== "string" || typeof payload["uri"] !== "string" || typeof payload["bodyHash"] !== "string" || !isFiniteNumber(payload["iat"]) || !isFiniteNumber(payload["exp"])) { throw new InvalidSignatureError({ reason: "Invalid payload claims" }); } if (payload["grantId"] !== void 0 && typeof payload["grantId"] !== "string") { throw new InvalidSignatureError({ reason: "Invalid grantId claim" }); } return { aud: payload["aud"], method: payload["method"], uri: payload["uri"], bodyHash: payload["bodyHash"], iat: payload["iat"], exp: payload["exp"], grantId: payload["grantId"] }; } function parseWeb3SignedHeader(headerValue) { if (!headerValue) { throw new MissingAuthError(); } if (!headerValue.startsWith(WEB3_SIGNED_PREFIX)) { throw new InvalidSignatureError({ reason: "Missing Web3Signed prefix" }); } const value = headerValue.slice(WEB3_SIGNED_PREFIX.length); const dotIndex = value.indexOf("."); if (dotIndex === -1 || dotIndex === 0 || dotIndex === value.length - 1) { throw new InvalidSignatureError({ reason: "Invalid header format" }); } const payloadBase64 = value.slice(0, dotIndex); const signatureStr = value.slice(dotIndex + 1); if (!signatureStr.startsWith("0x")) { throw new InvalidSignatureError({ reason: "Invalid signature format" }); } let payload; try { const decoded = base64urlDecode(payloadBase64); payload = parsePayload(JSON.parse(decoded)); } catch (err) { if (err instanceof InvalidSignatureError) throw err; throw new InvalidSignatureError({ reason: "Invalid payload encoding" }); } return { payloadBase64, payload, signature: signatureStr }; } async function verifyWeb3Signed(params) { const { payloadBase64, payload, signature } = parseWeb3SignedHeader( params.headerValue ); let signer; try { signer = await recoverMessageAddress({ message: payloadBase64, signature }); } catch { throw new InvalidSignatureError({ reason: "Signature recovery failed" }); } if (payload.aud !== params.expectedOrigin) { throw new InvalidSignatureError({ reason: "Audience mismatch", expected: params.expectedOrigin, actual: payload.aud }); } if (payload.method !== params.expectedMethod) { throw new InvalidSignatureError({ reason: "Method mismatch", expected: params.expectedMethod, actual: payload.method }); } if (payload.uri !== params.expectedPath) { throw new InvalidSignatureError({ reason: "URI mismatch", expected: params.expectedPath, actual: payload.uri }); } if (params.bodyBytes !== void 0) { const expectedBodyHash = computeBodyHash(params.bodyBytes); if (payload.bodyHash !== expectedBodyHash) { throw new InvalidSignatureError({ reason: "Body hash mismatch", expected: expectedBodyHash, actual: payload.bodyHash }); } } const now = params.now ?? Math.floor(Date.now() / 1e3); if (payload.exp < now - CLOCK_SKEW_SECONDS) { throw new ExpiredTokenError({ reason: "Token expired" }); } if (payload.iat > now + CLOCK_SKEW_SECONDS) { throw new ExpiredTokenError({ reason: "Token issued in the future" }); } return { signer, payload }; } export { parseWeb3SignedHeader, verifyWeb3Signed }; //# sourceMappingURL=web3-signed.js.map