@reclaimprotocol/js-sdk
Version:
Designed to request proofs from the Reclaim protocol and manage the flow of claims and witness interactions.
1,249 lines (1,227 loc) • 177 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __objRest = (source, exclude) => {
var target = {};
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop];
if (source != null && __getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(source)) {
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop];
}
return target;
};
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// package.json
var require_package = __commonJS({
"package.json"(exports2, module2) {
module2.exports = {
name: "@reclaimprotocol/js-sdk",
version: "5.8.2",
description: "Designed to request proofs from the Reclaim protocol and manage the flow of claims and witness interactions.",
main: "dist/index.js",
types: "dist/index.d.ts",
exports: {
".": {
types: "./dist/index.d.ts",
default: "./dist/index.js"
},
"./internal": {
types: "./dist/internal.d.ts",
default: "./dist/internal.js"
}
},
keywords: [
"reclaim",
"protocol",
"blockchain",
"proof",
"verification",
"identity",
"claims",
"witness",
"sdk",
"javascript",
"typescript",
"decentralized",
"web3"
],
files: [
"dist"
],
tsup: {
entry: [
"src/index.ts",
"src/internal.ts"
],
splitting: false,
sourcemap: true,
clean: true
},
scripts: {
build: "sh scripts/build.sh",
prepare: "npm run build",
release: "release-it",
test: "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
commitlint: "commitlint --edit"
},
repository: {
type: "git",
url: "https://github.com/reclaimprotocol/reclaim-js-sdk"
},
author: "ali <ali@creatoros.co>",
license: "See License in <https://github.com/reclaimprotocol/.github/blob/main/LICENSE>",
bugs: {
url: "https://github.com/reclaimprotocol/reclaim-js-sdk/issues"
},
homepage: "https://github.com/reclaimprotocol/reclaim-js-sdk/",
publishConfig: {
registry: "https://registry.npmjs.org/",
access: "public"
},
"release-it": {
git: {
commitMessage: "chore: release ${version}",
tagName: "v${version}"
},
npm: {
publish: true,
tag: "latest"
},
github: {
release: true
},
plugins: {
"@release-it/conventional-changelog": {
preset: "angular"
}
}
},
devDependencies: {
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@release-it/conventional-changelog": "10.0.6",
"@types/jest": "^30.0.0",
"@types/node-forge": "^1.3.14",
"@types/qrcode": "^1.5.5",
"@types/qs": "^6.9.11",
"@types/url-parse": "^1.4.11",
"@types/uuid": "^9.0.7",
jest: "^30.1.3",
"jest-environment-jsdom": "^30.1.2",
qs: "^6.11.2",
"release-it": "^19.2.4",
"ts-jest": "^29.4.1",
tsup: "^8.0.1",
typescript: "^5.3.3"
},
dependencies: {
canonicalize: "^2.0.0",
ethers: "^6.9.1",
"fetch-retry": "^6.0.0",
"node-forge": "^1.3.3",
qrcode: "^1.5.4",
"url-parse": "^1.5.10",
uuid: "^9.0.1"
},
overrides: {
"@conventional-changelog/git-client": "^2.0.0"
}
};
}
});
// src/index.ts
var index_exports = {};
__export(index_exports, {
ApplicationError: () => ApplicationError,
AttestorTeeVerificationError: () => AttestorTeeVerificationError,
BackendServerError: () => BackendServerError,
CallbackUrlRequiredError: () => CallbackUrlRequiredError,
ErrorDuringVerificationError: () => ErrorDuringVerificationError,
GetAppCallbackUrlError: () => GetAppCallbackUrlError,
GetStatusUrlError: () => GetStatusUrlError,
HASH_MATCH_MULTIPLE_DEFAULT: () => HASH_MATCH_MULTIPLE_DEFAULT,
HASH_REQUIRED_DEFAULT: () => HASH_REQUIRED_DEFAULT,
InavlidParametersError: () => InavlidParametersError,
InitError: () => InitError,
InitSessionError: () => InitSessionError,
InvalidParamError: () => InvalidParamError,
InvalidRequestSpecError: () => InvalidRequestSpecError,
InvalidSignatureError: () => InvalidSignatureError,
NoProviderParamsError: () => NoProviderParamsError,
ProofNotValidatedError: () => ProofNotValidatedError,
ProofNotVerifiedError: () => ProofNotVerifiedError,
ProofSubmissionFailedError: () => ProofSubmissionFailedError,
ProviderConfigFetchError: () => ProviderConfigFetchError,
ProviderFailedError: () => ProviderFailedError,
ProviderNotFoundError: () => ProviderNotFoundError,
ReclaimProofRequest: () => ReclaimProofRequest,
SessionNotStartedError: () => SessionNotStartedError,
SetContextError: () => SetContextError,
SetParamsError: () => SetParamsError,
SetSignatureError: () => SetSignatureError,
SignatureGeneratingError: () => SignatureGeneratingError,
SignatureNotFoundError: () => SignatureNotFoundError,
StatusUrlError: () => StatusUrlError,
TeeVerificationError: () => TeeVerificationError,
TimeoutError: () => TimeoutError,
UnknownProofsNotValidatedError: () => UnknownProofsNotValidatedError,
UpdateSessionError: () => UpdateSessionError,
assertValidProofsByHash: () => assertValidProofsByHash,
assertValidateProof: () => assertValidateProof,
assertVerifiedProof: () => assertVerifiedProof,
clearDeviceCache: () => clearDeviceCache,
createLinkWithTemplateData: () => createLinkWithTemplateData,
createSignDataForClaim: () => createSignDataForClaim,
fetchProviderConfigs: () => fetchProviderConfigs,
fetchProviderHashRequirementsBy: () => fetchProviderHashRequirementsBy,
fetchStatusUrl: () => fetchStatusUrl,
generateAttestationNonce: () => generateAttestationNonce,
generateInitSignature: () => generateInitSignature,
generateSpecsFromRequestSpecTemplate: () => generateSpecsFromRequestSpecTemplate,
getAttestors: () => getAttestors,
getDeviceType: () => getDeviceType,
getHashFromProof: () => getHashFromProof,
getHttpProviderClaimParamsFromProof: () => getHttpProviderClaimParamsFromProof,
getIdentifierFromClaimInfo: () => getIdentifierFromClaimInfo,
getMobileDeviceType: () => getMobileDeviceType,
getProviderHashRequirementSpecFromProviderConfig: () => getProviderHashRequirementSpecFromProviderConfig,
getProviderHashRequirementsFromSpec: () => getProviderHashRequirementsFromSpec,
getProviderParamsAsCanonicalizedString: () => getProviderParamsAsCanonicalizedString,
getShortenedUrl: () => getShortenedUrl,
hashProofClaimParams: () => hashProofClaimParams,
hashRequestSpec: () => hashRequestSpec,
initSession: () => initSession,
isDesktopDevice: () => isDesktopDevice,
isHttpProviderClaimParams: () => isHttpProviderClaimParams,
isMobileDevice: () => isMobileDevice,
recoverSignersOfSignedClaim: () => recoverSignersOfSignedClaim,
runTeeVerification: () => runTeeVerification,
takePairsWhereValueIsArray: () => takePairsWhereValueIsArray,
takeTemplateParametersFromProofs: () => takeTemplateParametersFromProofs,
transformForOnchain: () => transformForOnchain,
updateSession: () => updateSession,
verifyAttestorTeeAttestation: () => verifyAttestorTeeAttestation,
verifyProof: () => verifyProof,
verifyTeeAttestation: () => verifyTeeAttestation
});
module.exports = __toCommonJS(index_exports);
// src/utils/interfaces.ts
var RECLAIM_EXTENSION_ACTIONS = {
CHECK_EXTENSION: "RECLAIM_EXTENSION_CHECK",
EXTENSION_RESPONSE: "RECLAIM_EXTENSION_RESPONSE",
START_VERIFICATION: "RECLAIM_START_VERIFICATION",
STATUS_UPDATE: "RECLAIM_STATUS_UPDATE"
};
// src/Reclaim.ts
var import_ethers7 = require("ethers");
var import_canonicalize3 = __toESM(require("canonicalize"));
// src/utils/errors.ts
function createErrorClass(name) {
return class extends Error {
constructor(message, innerError) {
const fullMessage = innerError ? `${message || ""} caused by ${innerError && typeof innerError === "object" && "name" in innerError ? innerError.name : "Error"}: ${innerError && typeof innerError === "object" && "message" in innerError ? innerError.message : String(innerError)}` : message;
super(fullMessage);
this.innerError = innerError;
this.name = name;
if (innerError) {
this.stack += `
Caused by: ${innerError && typeof innerError === "object" && "stack" in innerError ? innerError.stack : String(innerError)}`;
}
}
};
}
var TimeoutError = createErrorClass("TimeoutError");
var ProofNotVerifiedError = createErrorClass("ProofNotVerifiedError");
var ProofNotValidatedError = createErrorClass("ProofNotValidatedError");
var InvalidRequestSpecError = createErrorClass("InvalidRequestSpecError");
var UnknownProofsNotValidatedError = createErrorClass("UnknownProofsNotValidatedError");
var SessionNotStartedError = createErrorClass("SessionNotStartedError");
var ProviderNotFoundError = createErrorClass("ProviderNotFoundError");
var SignatureGeneratingError = createErrorClass("SignatureGeneratingError");
var SignatureNotFoundError = createErrorClass("SignatureNotFoundError");
var InvalidSignatureError = createErrorClass("InvalidSignatureError");
var UpdateSessionError = createErrorClass("UpdateSessionError");
var InitSessionError = createErrorClass("InitSessionError");
var ProviderFailedError = createErrorClass("ProviderFailedError");
var InvalidParamError = createErrorClass("InvalidParamError");
var ApplicationError = createErrorClass("ApplicationError");
var InitError = createErrorClass("InitError");
var BackendServerError = createErrorClass("BackendServerError");
var GetStatusUrlError = createErrorClass("GetStatusUrlError");
var NoProviderParamsError = createErrorClass("NoProviderParamsError");
var SetParamsError = createErrorClass("SetParamsError");
var SetContextError = createErrorClass("SetContextError");
var SetSignatureError = createErrorClass("SetSignatureError");
var GetAppCallbackUrlError = createErrorClass("GetAppCallbackUrlError");
var StatusUrlError = createErrorClass("StatusUrlError");
var ProviderConfigFetchError = createErrorClass("ProviderConfigFetchError");
var InavlidParametersError = createErrorClass("InavlidParametersError");
var ProofSubmissionFailedError = createErrorClass("ProofSubmissionFailedError");
var ErrorDuringVerificationError = createErrorClass("ErrorDuringVerificationError");
var CallbackUrlRequiredError = createErrorClass("CallbackUrlRequiredError");
var TeeVerificationError = createErrorClass("TeeVerificationError");
var AttestorTeeVerificationError = createErrorClass("AttestorTeeVerificationError");
// src/utils/logger.ts
var SimpleLogger = class {
constructor() {
this.level = "info";
}
setLevel(level) {
this.level = level;
}
shouldLog(messageLevel) {
const levels = ["error", "warn", "info", "silent"];
return levels.indexOf(this.level) >= levels.indexOf(messageLevel);
}
log(level, message, ...args) {
if (this.shouldLog(level) && this.level !== "silent") {
const logFunction = this.getLogFunction(level);
console.log("current level", this.level);
logFunction(`[${level.toUpperCase()}]`, message, ...args);
}
}
getLogFunction(level) {
switch (level) {
case "error":
return console.error;
case "warn":
return console.warn;
case "info":
return console.info;
default:
return () => {
};
}
}
info(message, ...args) {
this.log("info", message, ...args);
}
warn(message, ...args) {
this.log("warn", message, ...args);
}
error(message, ...args) {
this.log("error", message, ...args);
}
};
var logger = new SimpleLogger();
function setLogLevel(level) {
logger.setLevel(level);
}
var logger_default = {
logger,
setLogLevel
};
// src/utils/validationUtils.ts
var import_ethers = require("ethers");
var import_canonicalize2 = __toESM(require("canonicalize"));
// src/utils/strings.ts
var import_canonicalize = __toESM(require("canonicalize"));
function canonicalStringify(params) {
if (!params) {
return "";
}
return (0, import_canonicalize.default)(params) || "";
}
// src/utils/validationUtils.ts
var logger2 = logger_default.logger;
function validateFunctionParams(params, functionName) {
params.forEach(({ input, paramName, isString }) => {
if (input == null) {
logger2.info(`Validation failed: ${paramName} in ${functionName} is null or undefined`);
throw new InvalidParamError(`${paramName} passed to ${functionName} must not be null or undefined.`);
}
if (isString && typeof input !== "string") {
logger2.info(`Validation failed: ${paramName} in ${functionName} is not a string`);
throw new InvalidParamError(`${paramName} passed to ${functionName} must be a string.`);
}
if (isString && input.trim() === "") {
logger2.info(`Validation failed: ${paramName} in ${functionName} is an empty string`);
throw new InvalidParamError(`${paramName} passed to ${functionName} must not be an empty string.`);
}
});
}
function validateFunctionParamsWithFn(param, functionName) {
if (!param.isValid()) {
logger2.info(`Validation failed: ${param.paramName} in ${functionName} is not valid`);
throw new InvalidParamError(`${param.paramName} passed to ${functionName} must be valid.`);
}
}
function validateParameters(parameters) {
try {
if (typeof parameters !== "object" || parameters === null) {
logger2.info(`Parameters validation failed: Provided parameters is not an object`);
throw new InavlidParametersError(`The provided parameters is not an object`);
}
for (const [key, value] of Object.entries(parameters)) {
if (typeof key !== "string" || typeof value !== "string") {
logger2.info(`Parameters validation failed: Provided parameters is not an object of key value pairs of string and string`);
throw new InavlidParametersError(`The provided parameters is not an object of key value pairs of string and string`);
}
}
} catch (e) {
logger2.info(`Parameters validation failed: ${e.message}`);
throw new InavlidParametersError(`Invalid parameters passed to validateParameters.`, e);
}
}
function validateURL(url, functionName) {
try {
new URL(url);
} catch (e) {
logger2.info(`URL validation failed for ${url} in ${functionName}: ${e.message}`);
throw new InvalidParamError(`Invalid URL format ${url} passed to ${functionName}.`, e);
}
}
function validateRedirectionMethod(method, functionName) {
try {
if (method === null || method === void 0) {
return;
}
if (method !== "GET" && method !== "POST") {
throw new Error(`Invalid redirection method ${method} passed to ${functionName}.`);
}
} catch (e) {
logger2.info(`Redirection method validation failed for ${method} in ${functionName}: ${e.message}`);
throw new InvalidParamError(`Invalid redirection method ${method} passed to ${functionName}.`, e);
}
}
function validateRedirectionBody(records, functionName) {
try {
if (records === null || records === void 0) {
return;
}
if (Array.isArray(records)) {
for (const record of records) {
if ("name" in record && record.name && typeof record.name === "string") {
if ("value" in record && typeof record.value === "string") {
continue;
}
}
throw new Error("Record in form entries do not have a valid name and/or value");
}
} else {
throw new Error("Redirection body must be an array of objects with name, and value");
}
} catch (e) {
logger2.info(`Redirection body validation failed for ${records} in ${functionName}: ${e.message}`);
throw new InvalidParamError(`Invalid redirection body ${records} passed to ${functionName}.`, e);
}
}
function validateSignature(providerId, signature, applicationId, timestamp) {
try {
logger2.info(`Starting signature validation for providerId: ${providerId}, applicationId: ${applicationId}, timestamp: ${timestamp}`);
const message = (0, import_canonicalize2.default)({ providerId, timestamp });
if (!message) {
logger2.info("Failed to canonicalize message for signature validation");
throw new Error("Failed to canonicalize message");
}
const messageHash = import_ethers.ethers.keccak256(new TextEncoder().encode(message));
let appId = import_ethers.ethers.verifyMessage(
import_ethers.ethers.getBytes(messageHash),
import_ethers.ethers.hexlify(signature)
).toLowerCase();
if (import_ethers.ethers.getAddress(appId) !== import_ethers.ethers.getAddress(applicationId)) {
logger2.info(`Signature validation failed: Mismatch between derived appId (${appId}) and provided applicationId (${applicationId})`);
throw new InvalidSignatureError(`Signature does not match the application id: ${appId}`);
}
logger2.info(`Signature validated successfully for applicationId: ${applicationId}`);
} catch (err) {
logger2.info(`Signature validation failed: ${err.message}`);
if (err instanceof InvalidSignatureError) {
throw err;
}
throw new InvalidSignatureError(`Failed to validate signature: ${err.message}`);
}
}
function validateContext(context) {
if (context && !("contextAddress" in context)) {
try {
validateFunctionParams([
{ input: context, paramName: "context", isString: false }
], "validateContext");
JSON.parse(canonicalStringify(context));
return;
} catch (e) {
logger2.info(`Context validation failed: Provided JSON serializable context is not valid`);
throw new InvalidParamError(`The provided context is not valid`);
}
}
if (!context.contextAddress) {
logger2.info(`Context validation failed: Provided context address in context is not valid`);
throw new InvalidParamError(`The provided context address in context is not valid`);
}
if (!context.contextMessage) {
logger2.info(`Context validation failed: Provided context message in context is not valid`);
throw new InvalidParamError(`The provided context message in context is not valid`);
}
validateFunctionParams([
{ input: context.contextAddress, paramName: "contextAddress", isString: true },
{ input: context.contextMessage, paramName: "contextMessage", isString: true }
], "validateContext");
}
function validateModalOptions(modalOptions, functionName, paramPrefix = "") {
if (modalOptions.title !== void 0) {
validateFunctionParams([
{ input: modalOptions.title, paramName: `${paramPrefix}title`, isString: true }
], functionName);
}
if (modalOptions.description !== void 0) {
validateFunctionParams([
{ input: modalOptions.description, paramName: `${paramPrefix}description`, isString: true }
], functionName);
}
if (modalOptions.extensionUrl !== void 0) {
validateURL(modalOptions.extensionUrl, functionName);
validateFunctionParams([
{ input: modalOptions.extensionUrl, paramName: `${paramPrefix}extensionUrl`, isString: true }
], functionName);
}
if (modalOptions.darkTheme !== void 0) {
if (typeof modalOptions.darkTheme !== "boolean") {
throw new InvalidParamError(`${paramPrefix}darkTheme prop must be a boolean`);
}
validateFunctionParams([
{ input: modalOptions.darkTheme, paramName: `${paramPrefix}darkTheme` }
], functionName);
}
if (modalOptions.modalPopupTimer !== void 0) {
if (typeof modalOptions.modalPopupTimer !== "number" || modalOptions.modalPopupTimer <= 0 || !Number.isInteger(modalOptions.modalPopupTimer)) {
throw new InvalidParamError(`${paramPrefix}modalPopupTimer prop must be a valid time in minutes`);
}
validateFunctionParams([
{ input: modalOptions.modalPopupTimer, paramName: `${paramPrefix}modalPopupTimer` }
], functionName);
}
if (modalOptions.showExtensionInstallButton !== void 0) {
if (typeof modalOptions.showExtensionInstallButton !== "boolean") {
throw new InvalidParamError(`${paramPrefix}showExtensionInstallButton prop must be a boolean`);
}
validateFunctionParams([
{ input: modalOptions.showExtensionInstallButton, paramName: `${paramPrefix}showExtensionInstallButton` }
], functionName);
}
}
function hashObject(o) {
try {
const canonicalData = canonicalStringify(o);
const messageHash = import_ethers.ethers.keccak256(new TextEncoder().encode(canonicalData));
return messageHash;
} catch (e) {
logger2.info(`Failed to hash object: ${e.message}`);
throw new Error(`Failed to hash object: ${e.message}`);
}
}
// src/utils/helper.ts
var logger3 = logger_default.logger;
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function replaceAll(str, find, replace) {
if (find === "") return str;
return str.replace(new RegExp(escapeRegExp(find), "g"), replace);
}
function scheduleIntervalEndingTask(sessionId, intervals, onFailureCallback, timeout = 1e3 * 60 * 10) {
setTimeout(() => {
if (intervals.has(sessionId)) {
const message = "Interval ended without receiving proofs";
onFailureCallback(new TimeoutError(message));
logger3.info(message);
clearInterval(intervals.get(sessionId));
intervals.delete(sessionId);
}
}, timeout);
}
var createVerifyProofResultSuccess = (proofs, isTeeAttestationVerified, isAttestorTeeAttestationVerified) => {
return {
isVerified: true,
isTeeAttestationVerified,
isAttestorTeeAttestationVerified,
error: void 0,
data: proofs.map(createTrustedDataFromProofData),
publicData: getPublicDataFromProofs(proofs)
};
};
var createVerifyProofResultFailure = (error, isTeeAttestationVerified, isAttestorTeeAttestationVerified) => {
return {
isVerified: false,
isTeeAttestationVerified,
isAttestorTeeAttestationVerified,
error,
data: [],
publicData: []
};
};
function createTrustedDataFromProofData(proof) {
try {
const context = JSON.parse(proof.claimData.context);
const _a = context, { extractedParameters } = _a, rest = __objRest(_a, ["extractedParameters"]);
return {
context: rest,
extractedParameters: extractedParameters != null ? extractedParameters : {}
};
} catch (e) {
return {
context: {},
extractedParameters: {}
};
}
}
function getPublicDataFromProofs(proofs) {
const data = [];
const seenData = /* @__PURE__ */ new Set();
for (const proof of proofs) {
const publicData = proof.publicData;
if (publicData === null || publicData === void 0) {
continue;
}
try {
const hash = hashObject(publicData);
if (seenData.has(hash)) {
continue;
}
seenData.add(hash);
} catch (_) {
}
data.push(publicData);
}
return data;
}
// src/utils/constants.ts
var BACKEND_BASE_URL = "https://api.reclaimprotocol.org";
function setBackendBaseUrl(url) {
BACKEND_BASE_URL = url;
}
var constants = {
// Default callback URL for Reclaim protocol
get DEFAULT_RECLAIM_CALLBACK_URL() {
return `${BACKEND_BASE_URL}/api/sdk/callback?callbackId=`;
},
// Default error callback URL for Reclaim protocol
get DEFAULT_RECLAIM_CANCEL_CALLBACK_URL() {
return `${BACKEND_BASE_URL}/api/sdk/error-callback?callbackId=`;
},
// Default status URL for Reclaim sessions
get DEFAULT_RECLAIM_STATUS_URL() {
return `${BACKEND_BASE_URL}/api/sdk/session/`;
},
// Default attestors URL for Reclaim sessions
get DEFAULT_ATTESTORS_URL() {
return `${BACKEND_BASE_URL}/api/attestors`;
},
DEFAULT_PROVIDER_CONFIGS_URL(providerId, exactProviderVersionString, allowedTags) {
return `${BACKEND_BASE_URL}/api/providers/${providerId}/configs?versionNumber=${exactProviderVersionString || ""}&allowedTags=${(allowedTags == null ? void 0 : allowedTags.join(",")) || ""}`;
},
// Default portal URL
DEFAULT_PORTAL_URL: "https://portal.reclaimprotocol.org",
// Default sharepage URL
DEFAULT_APP_SHARE_PAGE_URL: "https://share.reclaimprotocol.org/verifier",
// URL for sharing Reclaim templates
RECLAIM_SHARE_URL: "https://share.reclaimprotocol.org/verifier/?template=",
// Chrome extension URL for Reclaim Protocol
CHROME_EXTENSION_URL: "https://chromewebstore.google.com/detail/reclaim-extension/oafieibbbcepkmenknelhmgaoahamdeh"
};
var GCP_CONFIDENTIAL_SPACE_ROOT_CA = `-----BEGIN CERTIFICATE-----
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
uyT5CnJulxSC5CT1
-----END CERTIFICATE-----`;
var ATTESTOR_NONCE_PATTERN = /^attestor_public_key:0x([0-9a-fA-F]{40})$/;
var GCP_CONFIDENTIAL_SPACE_ISSUER = "https://confidentialcomputing.googleapis.com";
// src/utils/fetch.ts
var import_fetch_retry = __toESM(require("fetch-retry"));
var MAX_RETRIES = 3;
var MAX_RETRY_DELAY_MS = 60 * 1e3;
var isHttpResponseStatusRetryable = (statusCode) => {
return statusCode === 408 || statusCode === 429 || statusCode >= 500;
};
var getRetryDelay = (response) => {
const retryAfter = response == null ? void 0 : response.headers.get("Retry-After");
if (!retryAfter) {
return void 0;
}
const trimmed = retryAfter.trim();
if (/^\d+$/.test(trimmed)) {
return Math.min(parseInt(trimmed, 10) * 1e3, MAX_RETRY_DELAY_MS);
}
const date = new Date(trimmed);
if (!isNaN(date.getTime())) {
return Math.min(Math.max(0, date.getTime() - Date.now()), MAX_RETRY_DELAY_MS);
}
return void 0;
};
var http = {
get client() {
return (0, import_fetch_retry.default)(globalThis.fetch, {
retries: MAX_RETRIES,
retryDelay: function(attempt, _, response) {
const delay = getRetryDelay(response);
if (delay !== void 0) {
return delay;
}
return Math.pow(2, attempt) * 1e3;
},
retryOn: (attempt, error, response) => {
if (attempt >= MAX_RETRIES) {
return false;
}
if (response && Number.isInteger(response.status)) {
return isHttpResponseStatusRetryable(response.status);
}
return !!error && error.name !== "AbortError";
}
});
}
};
// src/utils/sessionUtils.ts
var logger4 = logger_default.logger;
function initSession(providerId, appId, timestamp, signature, versionNumber, orgId) {
return __async(this, null, function* () {
logger4.info(`Initializing session for providerId: ${providerId}, appId: ${appId}`);
try {
const response = yield http.client(`${BACKEND_BASE_URL}/api/sdk/init/session/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(__spreadValues({ providerId, appId, timestamp, signature, versionNumber }, orgId ? { orgId } : {}))
});
const res = yield response.json();
if (!response.ok) {
logger4.info(`Session initialization failed: ${res.message || "Unknown error"}`);
throw new InitSessionError(res.message || `Error initializing session with providerId: ${providerId}`);
}
return res;
} catch (err) {
logger4.info(`Failed to initialize session for providerId: ${providerId}, appId: ${appId}`, err);
throw err;
}
});
}
function updateSession(sessionId, status, orgId) {
return __async(this, null, function* () {
logger4.info(`Updating session status for sessionId: ${sessionId}, new status: ${status}`);
validateFunctionParams(
[{ input: sessionId, paramName: "sessionId", isString: true }],
"updateSession"
);
try {
const response = yield http.client(`${BACKEND_BASE_URL}/api/sdk/update/session/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(__spreadValues({ sessionId, status }, orgId ? { orgId } : {}))
});
const res = yield response.json();
if (!response.ok) {
const errorMessage = `Error updating session with sessionId: ${sessionId}. Status Code: ${response.status}`;
logger4.info(errorMessage, res);
throw new UpdateSessionError(errorMessage);
}
logger4.info(`Session status updated successfully for sessionId: ${sessionId}`);
return res;
} catch (err) {
const errorMessage = `Failed to update session with sessionId: ${sessionId}`;
logger4.info(errorMessage, err);
throw new UpdateSessionError(`Error updating session with sessionId: ${sessionId}`);
}
});
}
function fetchStatusUrl(sessionId) {
return __async(this, null, function* () {
validateFunctionParams(
[{ input: sessionId, paramName: "sessionId", isString: true }],
"fetchStatusUrl"
);
try {
const response = yield http.client(`${constants.DEFAULT_RECLAIM_STATUS_URL}${sessionId}`, {
method: "GET",
headers: { "Content-Type": "application/json" }
});
const res = yield response.json();
if (!response.ok) {
const errorMessage = `Error fetching status URL for sessionId: ${sessionId}. Status Code: ${response.status}`;
logger4.info(errorMessage, res);
throw new StatusUrlError(errorMessage);
}
return res;
} catch (err) {
const errorMessage = `Failed to fetch status URL for sessionId: ${sessionId}`;
logger4.info(errorMessage, err);
throw new StatusUrlError(`Error fetching status URL for sessionId: ${sessionId}`);
}
});
}
function fetchProviderConfigs(providerId, exactProviderVersionString, allowedTags) {
return __async(this, null, function* () {
validateFunctionParams(
[
{ input: providerId, paramName: "providerId", isString: true }
],
"fetchProviderConfigs"
);
if (exactProviderVersionString != null && exactProviderVersionString != void 0) {
validateFunctionParams(
[
{ input: exactProviderVersionString, paramName: "exactProviderVersionString", isString: true }
],
"fetchProviderConfigs"
);
}
try {
const response = yield http.client(constants.DEFAULT_PROVIDER_CONFIGS_URL(providerId, exactProviderVersionString, allowedTags), {
method: "GET",
headers: { "Content-Type": "application/json" }
});
const res = yield response.json();
if (!response.ok) {
const errorMessage = `Error fetching provider config for providerId: ${providerId}, exactProviderVersionString: ${exactProviderVersionString}. Status Code: ${response.status}`;
logger4.info(errorMessage, res);
throw new ProviderConfigFetchError(errorMessage);
}
return res;
} catch (err) {
const errorMessage = `Failed to fetch provider config for providerId: ${providerId}, exactProviderVersionString: ${exactProviderVersionString}`;
logger4.info(errorMessage, err);
throw new ProviderConfigFetchError(`Error fetching provider config for providerId: ${providerId}, exactProviderVersionString: ${exactProviderVersionString}`);
}
});
}
// src/utils/proofUtils.ts
var import_ethers3 = require("ethers");
// src/witness.ts
var import_ethers2 = require("ethers");
function createSignDataForClaim(data) {
const identifier = getIdentifierFromClaimInfo(data);
const lines = [
identifier,
data.owner.toLowerCase(),
data.timestampS.toString(),
data.epoch.toString()
];
return lines.join("\n");
}
function getIdentifierFromClaimInfo(info) {
let canonicalContext = info.context || "";
if (canonicalContext.length > 0) {
try {
const ctx = JSON.parse(canonicalContext);
canonicalContext = canonicalStringify(ctx);
} catch (e) {
throw new Error("unable to parse non-empty context. Must be JSON");
}
}
const str = `${info.provider}
${info.parameters}
${canonicalContext}`;
return import_ethers2.ethers.keccak256(strToUint8Array(str)).toLowerCase();
}
function hashProofClaimParams(params) {
const serializedParams = getProviderParamsAsCanonicalizedString(params);
if (Array.isArray(serializedParams)) {
return serializedParams.map(
(serialized) => import_ethers2.ethers.keccak256(strToUint8Array(serialized)).toLowerCase()
);
}
return import_ethers2.ethers.keccak256(
strToUint8Array(serializedParams)
).toLowerCase();
}
function strToUint8Array(str) {
return new TextEncoder().encode(str);
}
function getProviderParamsAsCanonicalizedString(params) {
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
const pairsCount = (_b = (_a = params == null ? void 0 : params.responseMatches) == null ? void 0 : _a.length) != null ? _b : 0;
const validCanonicalizedStrings = [];
const totalCombinations = 1 << pairsCount;
for (let i = 0; i < totalCombinations; i++) {
let isValidCombination = true;
let includedCount = 0;
const currentMatches = [];
const currentRedactions = [];
for (let j = 0; j < pairsCount; j++) {
const isIncluded = (i & 1 << j) !== 0;
const match = (_c = params == null ? void 0 : params.responseMatches) == null ? void 0 : _c[j];
const redaction = (_d = params == null ? void 0 : params.responseRedactions) == null ? void 0 : _d[j];
if (isIncluded) {
if (match) {
currentMatches.push({
value: (_e = match.value) != null ? _e : "",
// This needs to be explicitly specified and absence should cause error, but we're choosing to ignore it in this case
type: (_f = match.type) != null ? _f : "contains",
invert: match.invert || void 0
});
}
if (redaction) {
currentRedactions.push({
xPath: (_g = redaction.xPath) != null ? _g : "",
jsonPath: (_h = redaction.jsonPath) != null ? _h : "",
regex: (_i = redaction.regex) != null ? _i : "",
hash: redaction.hash || void 0
});
}
includedCount++;
} else {
if (match && !match.isOptional) {
isValidCombination = false;
break;
}
}
}
if (isValidCombination && includedCount > 0) {
const filteredParams = {
url: (_j = params == null ? void 0 : params.url) != null ? _j : "",
// METHOD needs to be explicitly specified and absence or unknown method should cause error, but we're choosing to ignore it in this case
method: (_k = params == null ? void 0 : params.method) != null ? _k : "GET",
body: (_l = params == null ? void 0 : params.body) != null ? _l : "",
responseMatches: currentMatches,
responseRedactions: currentRedactions
};
validCanonicalizedStrings.push(canonicalStringify(filteredParams));
}
}
if (validCanonicalizedStrings.length === 0) {
const filteredParams = {
url: (_m = params == null ? void 0 : params.url) != null ? _m : "",
method: (_n = params == null ? void 0 : params.method) != null ? _n : "GET",
body: (_o = params == null ? void 0 : params.body) != null ? _o : "",
responseMatches: [],
responseRedactions: []
};
return [canonicalStringify(filteredParams)];
}
return validCanonicalizedStrings;
}
// src/utils/proofUtils.ts
var logger5 = logger_default.logger;
function getShortenedUrl(url) {
return __async(this, null, function* () {
logger5.info(`Attempting to shorten URL: ${url}`);
try {
validateURL(url, "getShortenedUrl");
const response = yield http.client(`${BACKEND_BASE_URL}/api/sdk/shortener`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fullUrl: url })
});
const res = yield response.json();
if (!response.ok) {
logger5.info(`Failed to shorten URL: status=${response.status}`);
return url;
}
const shortenedVerificationUrl = res.result.shortUrl;
return shortenedVerificationUrl;
} catch (err) {
logger5.info(`Error shortening URL: ${url}, Error: ${err}`);
return url;
}
});
}
function createLinkWithTemplateData(templateData, sharePagePath) {
return __async(this, null, function* () {
let template = encodeURIComponent(JSON.stringify(templateData));
template = replaceAll(template, "(", "%28");
template = replaceAll(template, ")", "%29");
const fullLink = sharePagePath ? `${sharePagePath}/?template=${template}` : `${constants.RECLAIM_SHARE_URL}${template}`;
try {
const shortenedLink = yield getShortenedUrl(fullLink);
return shortenedLink;
} catch (err) {
logger5.info(`Error creating link for sessionId: ${templateData.sessionId}, Error: ${err}`);
return fullLink;
}
});
}
function getAttestors() {
return __async(this, null, function* () {
var _a;
const response = yield http.client(constants.DEFAULT_ATTESTORS_URL);
if (!response.ok) {
(_a = response.body) == null ? void 0 : _a.cancel();
throw new BackendServerError(
`Failed to fetch witness addresses: ${response.status}`
);
}
const { data } = yield response.json();
return data.map((wt) => ({ id: wt.address, url: "" }));
});
}
function recoverSignersOfSignedClaim({
claim,
signatures
}) {
const dataStr = createSignDataForClaim(__spreadValues({}, claim));
const signers = signatures.map(
(signature) => import_ethers3.ethers.verifyMessage(dataStr, import_ethers3.ethers.hexlify(signature)).toLowerCase()
);
return signers;
}
function assertVerifiedProof(proof, attestors) {
return __async(this, null, function* () {
const signers = recoverSignersOfSignedClaim({
claim: proof.claimData,
signatures: proof.signatures.map((signature) => import_ethers3.ethers.getBytes(signature))
});
if (!attestors.some((attestor) => signers.includes(attestor.id.toLowerCase()))) {
throw new ProofNotVerifiedError("Identifier mismatch");
}
});
}
// src/utils/modalUtils.ts
var import_qrcode = __toESM(require("qrcode"));
var logger6 = logger_default.logger;
var QRCodeModal = class {
constructor(options = {}) {
this.countdownSeconds = 60;
this.modalId = "reclaim-qr-modal";
this.options = __spreadValues({
title: "Verify with Reclaim",
description: "Scan the QR code with your mobile device to complete verification",
extensionUrl: constants.CHROME_EXTENSION_URL,
darkTheme: false,
modalPopupTimer: 1,
// default to 1 minute
showExtensionInstallButton: false
}, options);
}
show(requestUrl) {
return __async(this, null, function* () {
try {
this.close();
const modalHTML = this.createModalHTML();
document.body.insertAdjacentHTML("beforeend", modalHTML);
yield this.generateQRCode(requestUrl, "reclaim-qr-code");
this.addEventListeners();
this.startAutoCloseTimer();
} catch (error) {
logger6.info("Error showing QR code modal:", error);
throw error;
}
});
}
close() {
if (this.autoCloseTimer) {
clearTimeout(this.autoCloseTimer);
this.autoCloseTimer = void 0;
}
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = void 0;
}
const modal = document.getElementById(this.modalId);
if (modal) {
modal.remove();
}
if (this.options.onClose) {
this.options.onClose();
}
}
getThemeStyles() {
const isDark = this.options.darkTheme;
return {
modalBackground: isDark ? "rgba(0, 0, 0, 0.8)" : "rgba(0, 0, 0, 0.5)",
cardBackground: isDark ? "#1f2937" : "white",
titleColor: isDark ? "#f9fafb" : "#1f2937",
textColor: isDark ? "#d1d5db" : "#6b7280",
qrBackground: isDark ? "#374151" : "#f9fafb",
tipBackground: isDark ? "#1e40af" : "#f0f9ff",
tipBorder: isDark ? "#1e40af" : "#e0f2fe",
tipTextColor: isDark ? "#dbeafe" : "#0369a1",
buttonBackground: isDark ? "#374151" : "#f3f4f6",
buttonColor: isDark ? "#f9fafb" : "#374151",
buttonHoverBackground: isDark ? "#4b5563" : "#e5e7eb",
countdownColor: isDark ? "#6b7280" : "#9ca3af",
progressBackground: isDark ? "#4b5563" : "#e5e7eb",
progressGradient: isDark ? "linear-gradient(90deg, #3b82f6 0%, #2563eb 50%, #1d4ed8 100%)" : "linear-gradient(90deg, #2563eb 0%, #1d4ed8 50%, #1e40af 100%)",
linkColor: isDark ? "#60a5fa" : "#2563eb",
extensionButtonBackground: isDark ? "#1e40af" : "#2563eb",
extensionButtonHover: isDark ? "#1d4ed8" : "#1d4ed8"
};
}
createModalHTML() {
const styles = this.getThemeStyles();
return `
<div id="${this.modalId}" style="
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: ${styles.modalBackground};
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
">
<div style="
background: ${styles.cardBackground};
border-radius: 12px;
padding: 32px;
max-width: 400px;
width: 90%;
text-align: center;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
position: relative;
">
<button id="reclaim-close-modal" style="
position: absolute;
top: 16px;
right: 16px;
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
width: 32px;
height: 32px;
"
onmouseover="this.style.backgroundColor='${styles.buttonHoverBackground}'"
onmouseout="this.style.backgroundColor='transparent'"
title="Close modal">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4L4 12M4 4L12 12" stroke="${styles.buttonColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<h2 style="
margin: 0 0 16px 0;
font-size: 24px;
font-weight: 600;
color: ${styles.titleColor};
">${this.options.title}</h2>
<p style="
margin: 0 0 24px 0;
color: ${styles.textColor};
font-size: 14px;
line-height: 1.5;
">${this.options.description}</p>
<div id="reclaim-qr-code" style="
margin: 0 auto 24px auto;
background: ${styles.qrBackground};
border-radius: 8px;
display: inline-block;
"></div>
${this.options.showExtensionInstallButton ? `
<div style="
margin-bottom: 24px;
padding: 16px;
background: ${styles.tipBackground};
border: 1px solid ${styles.tipBorder};
border-radius: 8px;
">
<p style="
margin: 0 0 12px 0;
font-size: 14px;
color: ${styles.tipTextColor};
font-weight: 500;
">\u{1F4A1} For a better experience</p>
<p style="
margin: 0 0 12px 0;
font-size: 13px;
color: ${styles.tipTextColor};
line-height: 1.4;
">Install our browser extension for seamless verification without QR codes</p>
<a href="${this.options.extensionUrl}"
target="_blank"
style="
display: inline-block;
background: ${styles.extensionButtonBackground};