UNPKG

catts-sdk

Version:

Facilitates the local development of C-ATTS recipes.

265 lines (264 loc) 9.43 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.recipeSchema = exports.queriesSchema = exports.querySchema = exports.queryVariablesSchema = void 0; exports.parseRecipe = parseRecipe; exports.fetchQuery = fetchQuery; exports.validateProcessorResult = validateProcessorResult; exports.validateSchemaItems = validateSchemaItems; exports.getSchemaUid = getSchemaUid; const eas_sdk_1 = require("@ethereum-attestation-service/eas-sdk"); const ethers_1 = require("ethers"); const zod_1 = require("zod"); // SDK requests are proxied through the cloudflare caching worker to ensure // consistent results with the smart contract canister const CATTS_GQL_PROXY_URL = "https://query.catts.run"; /** * Zod schema for query variables. Defines the shape of query variables to be used * in GraphQL queries. */ exports.queryVariablesSchema = zod_1.z.lazy(() => zod_1.z.record(zod_1.z.union([exports.queryVariablesSchema, zod_1.z.string(), zod_1.z.number()]))); // Custom validation function for the name const validateRecipeName = (val) => { if (val.startsWith("-") || val.endsWith("-")) { return false; } if (/^\d/.test(val)) { return false; } return /^[a-z0-9-]+$/.test(val); }; // Custom validation function for keywords const validateKeyword = (keyword) => { return /^[a-z0-9-]+$/.test(keyword); }; const toString = zod_1.z.preprocess((input) => { if (typeof input === "object") { return JSON.stringify(input); } throw new Error("Expected 'variables' to be an object"); }, zod_1.z.string()); /** * Zod schema for query variables. Defines the components of a GraphQL query, * including endpoint and variables. */ exports.querySchema = zod_1.z .object({ url: zod_1.z .string() .min(1, { message: "Endpoint must be at least 1 character long" }) .max(255, { message: "Endpoint must be at most 255 characters long" }), filter: zod_1.z.string().optional().nullable(), headers: zod_1.z.any().optional().nullable(), body: zod_1.z .object({ query: zod_1.z .string() .min(1, { message: "Query must be at least 1 character long" }) .max(1024, { message: "Query must be at most 1024 characters long" }), variables: zod_1.z.any(), }) .optional() .nullable(), }) .strict(); /** * Zod schema for an array of queries. */ exports.queriesSchema = zod_1.z.array(exports.querySchema); /** * Zod schema for a recipe. Defines the structure of a recipe, including queries * and output schema. */ exports.recipeSchema = zod_1.z .object({ // Name validation name: zod_1.z .string() .min(3, { message: "Name must be at least 3 characters long" }) .max(50, { message: "Name must be at most 50 characters long" }) .refine((val) => validateRecipeName(val), { message: "Name must be lowercase, alphanumeric, may contain hyphens, must not start or end with a hyphen, and must not start with a digit", }), // Description validation description: zod_1.z .string() .min(3, { message: "Description must be at least 3 characters long" }) .max(160, { message: "Description must be at most 160 characters long" }) .optional(), // Keywords validation keywords: zod_1.z .array(zod_1.z .string() .min(3, { message: "Each keyword must be at least 3 characters long", }) .max(50, { message: "Each keyword must be at most 50 characters long", }) .refine((keyword) => validateKeyword(keyword), { message: "Each keyword must be lowercase, alphanumeric, and may contain hyphens", })) .optional() .refine((keywords) => keywords && keywords.length > 0, { message: "Keywords must not be empty", path: ["keywords"], // Specify the path for the error message }), // Queries validation queries: zod_1.z.array(exports.querySchema), // Schema validation (length constraints) schema: zod_1.z .string() .min(1, { message: "Schema must be at least 1 character long" }) .max(512, { message: "Schema must be at most 512 characters long" }), // Resolver validation (length constraint, exactly 42 characters) resolver: zod_1.z .string() .length(42, { message: "Resolver must be exactly 42 characters long" }), // Revokable validation revokable: zod_1.z.boolean().refine((val) => val === false, { message: "'revokable' should be false, revokable attestations are not yet supported", }), }) .strict(); // Define the basic schema value types const schemaValueBase = zod_1.z.union([ zod_1.z.string(), zod_1.z.boolean(), zod_1.z.number(), zod_1.z.bigint(), ]); // Define the more complex schema value types const schemaValueComplex = zod_1.z.union([ zod_1.z.record(zod_1.z.unknown()), // Record<string, unknown> zod_1.z.array(zod_1.z.record(zod_1.z.unknown())), // Record<string, unknown>[] zod_1.z.array(zod_1.z.unknown()), // unknown[] ]); // Combine the base and complex schemas into a single SchemaValue schema const SchemaValue = zod_1.z.union([schemaValueBase, schemaValueComplex]); // Define the SchemaItem schema const SchemaItem = zod_1.z .object({ name: zod_1.z.string(), type: zod_1.z.string(), value: SchemaValue, }) .strict(); function parseRecipe(input) { return exports.recipeSchema.parse(input); } function substitutePlaceholders(args) { const placeholders = { "{user_eth_address}": args.placeHolderValues?.userEthAddress || "0x0000000000000000000000000000000000000000", "{user_eth_address_lowercase}": args.placeHolderValues?.userEthAddress?.toLowerCase() || "0x0000000000000000000000000000000000000000", }; let url = args.query.url; for (const [key, value] of Object.entries(placeholders)) { url = url.split(key).join(value); } let query = { ...args.query, url, }; if (args.query.filter) { let filter = args.query.filter; for (const [key, value] of Object.entries(placeholders)) { filter = filter.split(key).join(value); } query = { ...query, filter, }; } if (args.query.headers) { let headers = JSON.stringify(args.query.headers); for (const [key, value] of Object.entries(placeholders)) { headers = headers.split(key).join(value); } query = { ...query, headers: JSON.parse(headers), }; } if (args.query.body?.variables) { let variables = JSON.stringify(args.query.body.variables); for (const [key, value] of Object.entries(placeholders)) { variables = variables.split(key).join(value); } query = { ...query, body: { ...args.query.body, variables: JSON.parse(variables), }, }; } return query; } /** * Fetches the result of a query from the GraphQl endpoint specified in the query. * * @returns The result of the query in JSON format. */ async function fetchQuery(args) { const query = substitutePlaceholders(args); let proxyUrl = args?.proxyUrl || CATTS_GQL_PROXY_URL; const cacheKey = args?.cacheKey || Math.random().toString(36).substring(2, 15); proxyUrl = `${proxyUrl}/${cacheKey}`; const options = { method: "POST", body: JSON.stringify(query), }; if (args?.verbose) { console.log("Query:", query); } const response = await fetch(proxyUrl, options); if (args?.verbose) { console.log("Response status:", response.status); } if (!response.ok) { throw new Error(`Failed to fetch query from ${proxyUrl}`); } return response.json(); } /** * Validates that the processor result matches the output schema specified in the recipe. * * @param processorResult The raw result of the processor script as a string. * * @returns The result of the processor script in JSON format. On success, the output should match outputSchema specified in the recipe. */ async function validateProcessorResult({ processorResult, }) { if (typeof processorResult !== "string") { throw new Error("Processor result must be a string"); } const json = JSON.parse(processorResult); if (!Array.isArray(json)) { throw new Error("Processor result must be an array"); } if (json.length === 0) { throw new Error("Processor returned an empty array"); } let schemaItems; try { schemaItems = json.map((item) => SchemaItem.parse(item)); } catch (error) { throw new Error("Invalid processor result"); } return schemaItems; } /** * Validates that an array of schema items matches what is expected by the schema. */ async function validateSchemaItems({ schemaItems, schema, }) { const schemaEncoder = new eas_sdk_1.SchemaEncoder(schema); return schemaEncoder.encodeData(schemaItems); } /** * An EAS schema UID is a hash of the schema, resolver and revokable flag. */ function getSchemaUid({ schema, resolver, revokable, }) { return (0, ethers_1.solidityPackedKeccak256)(["string", "address", "bool"], [schema, resolver, revokable]); }