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