UNPKG

@metamask/snaps-utils

Version:
225 lines 10.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyRequestedSnapPermissions = exports.isSnapPermitted = exports.assertIsValidSnapId = exports.isSnapId = exports.stripSnapPrefix = exports.getSnapPrefix = exports.SnapIdStruct = exports.SnapIdPrefixStruct = exports.HttpSnapIdStruct = exports.NpmSnapIdStruct = exports.LocalSnapIdStruct = exports.BaseSnapIdStruct = exports.LOCALHOST_HOSTNAMES = exports.validateSnapShasum = exports.getSnapChecksum = exports.SnapStatusEvents = exports.SnapStatus = exports.PROPOSED_NAME_REGEX = void 0; const snaps_sdk_1 = require("@metamask/snaps-sdk"); const superstruct_1 = require("@metamask/superstruct"); const utils_1 = require("@metamask/utils"); const base_1 = require("@scure/base"); const fast_json_stable_stringify_1 = __importDefault(require("fast-json-stable-stringify")); const validate_npm_package_name_1 = __importDefault(require("validate-npm-package-name")); const caveats_1 = require("./caveats.cjs"); const checksum_1 = require("./checksum.cjs"); const types_1 = require("./types.cjs"); // This RegEx matches valid npm package names (with some exceptions) and space- // separated alphanumerical words, optionally with dashes and underscores. // The RegEx consists of two parts. The first part matches space-separated // words. It is based on the following Stackoverflow answer: // https://stackoverflow.com/a/34974982 // The second part, after the pipe operator, is the same RegEx used for the // `name` field of the official package.json JSON Schema, except that we allow // mixed-case letters. It was originally copied from: // https://github.com/SchemaStore/schemastore/blob/81a16897c1dabfd98c72242a5fd62eb080ff76d8/src/schemas/json/package.json#L132-L138 exports.PROPOSED_NAME_REGEX = /^(?:[A-Za-z0-9-_]+( [A-Za-z0-9-_]+)*)|(?:(?:@[A-Za-z0-9-*~][A-Za-z0-9-*._~]*\/)?[A-Za-z0-9-~][A-Za-z0-9-._~]*)$/u; var SnapStatus; (function (SnapStatus) { SnapStatus["Installing"] = "installing"; SnapStatus["Updating"] = "updating"; SnapStatus["Running"] = "running"; SnapStatus["Stopped"] = "stopped"; SnapStatus["Crashed"] = "crashed"; })(SnapStatus || (exports.SnapStatus = SnapStatus = {})); var SnapStatusEvents; (function (SnapStatusEvents) { SnapStatusEvents["Start"] = "START"; SnapStatusEvents["Stop"] = "STOP"; SnapStatusEvents["Crash"] = "CRASH"; SnapStatusEvents["Update"] = "UPDATE"; })(SnapStatusEvents || (exports.SnapStatusEvents = SnapStatusEvents = {})); /** * Gets a checksummable manifest by removing the shasum property and reserializing the JSON using a deterministic algorithm. * * @param manifest - The manifest itself. * @returns A virtual file containing the checksummable manifest. */ function getChecksummableManifest(manifest) { const manifestCopy = manifest.clone(); delete manifestCopy.result.source.shasum; // We use fast-json-stable-stringify to deterministically serialize the JSON // This is required before checksumming so we get reproducible checksums across platforms etc manifestCopy.value = (0, fast_json_stable_stringify_1.default)(manifestCopy.result); return manifestCopy; } /** * Calculates the Base64-encoded SHA-256 digest of all required Snap files. * * @param files - All required Snap files to be included in the checksum. * @returns The Base64-encoded SHA-256 digest of the source code. */ async function getSnapChecksum(files) { const { manifest, sourceCode, svgIcon, auxiliaryFiles, localizationFiles } = files; const all = [ getChecksummableManifest(manifest), sourceCode, svgIcon, ...auxiliaryFiles, ...localizationFiles, ].filter((file) => file !== undefined); return base_1.base64.encode(await (0, checksum_1.checksumFiles)(all)); } exports.getSnapChecksum = getSnapChecksum; /** * Checks whether the `source.shasum` property of a Snap manifest matches the * shasum of the snap. * * @param files - All required Snap files to be included in the checksum. * @param errorMessage - The error message to throw if validation fails. */ async function validateSnapShasum(files, errorMessage = 'Invalid Snap manifest: manifest shasum does not match computed shasum.') { if (files.manifest.result.source.shasum !== (await getSnapChecksum(files))) { throw new Error(errorMessage); } } exports.validateSnapShasum = validateSnapShasum; exports.LOCALHOST_HOSTNAMES = ['localhost', '127.0.0.1', '[::1]']; // Require snap ids to only consist of printable ASCII characters exports.BaseSnapIdStruct = (0, utils_1.definePattern)('Base Snap Id', /^[\x21-\x7E]*$/u); const LocalSnapIdSubUrlStruct = (0, types_1.uri)({ protocol: (0, superstruct_1.enums)(['http:', 'https:']), hostname: (0, superstruct_1.enums)(exports.LOCALHOST_HOSTNAMES), hash: (0, superstruct_1.empty)((0, superstruct_1.string)()), search: (0, superstruct_1.empty)((0, superstruct_1.string)()), }); exports.LocalSnapIdStruct = (0, superstruct_1.refine)(exports.BaseSnapIdStruct, 'local Snap Id', (value) => { if (!value.startsWith(types_1.SnapIdPrefixes.local)) { return `Expected local snap ID, got "${value}".`; } const [error] = (0, superstruct_1.validate)(value.slice(types_1.SnapIdPrefixes.local.length), LocalSnapIdSubUrlStruct); return error ?? true; }); exports.NpmSnapIdStruct = (0, superstruct_1.intersection)([ exports.BaseSnapIdStruct, (0, types_1.uri)({ protocol: (0, superstruct_1.literal)(types_1.SnapIdPrefixes.npm), pathname: (0, superstruct_1.refine)((0, superstruct_1.string)(), 'package name', function* (value) { const normalized = value.startsWith('/') ? value.slice(1) : value; const { errors, validForNewPackages, warnings } = (0, validate_npm_package_name_1.default)(normalized); if (!validForNewPackages) { if (errors === undefined) { (0, utils_1.assert)(warnings !== undefined); yield* warnings; } else { yield* errors; } } return true; }), search: (0, superstruct_1.empty)((0, superstruct_1.string)()), hash: (0, superstruct_1.empty)((0, superstruct_1.string)()), }), ]); exports.HttpSnapIdStruct = (0, superstruct_1.intersection)([ exports.BaseSnapIdStruct, (0, types_1.uri)({ protocol: (0, superstruct_1.enums)(['http:', 'https:']), search: (0, superstruct_1.empty)((0, superstruct_1.string)()), hash: (0, superstruct_1.empty)((0, superstruct_1.string)()), }), ]); exports.SnapIdPrefixStruct = (0, superstruct_1.refine)((0, superstruct_1.string)(), 'Snap ID prefix', (value) => { if (Object.values(types_1.SnapIdPrefixes).some((prefix) => value.startsWith(prefix))) { return true; } const allowedPrefixes = Object.values(types_1.SnapIdPrefixes) .map((prefix) => `"${prefix}"`) .join(', '); return `Invalid or no prefix found. Expected Snap ID to start with one of: ${allowedPrefixes}, but received: "${value}"`; }); exports.SnapIdStruct = (0, snaps_sdk_1.selectiveUnion)((value) => { if (typeof value === 'string' && value.startsWith(types_1.SnapIdPrefixes.npm)) { return exports.NpmSnapIdStruct; } if (typeof value === 'string' && value.startsWith(types_1.SnapIdPrefixes.local)) { return exports.LocalSnapIdStruct; } return exports.SnapIdPrefixStruct; }); /** * Extracts the snap prefix from a snap ID. * * @param snapId - The snap ID to extract the prefix from. * @returns The snap prefix from a snap id, e.g. `npm:`. */ function getSnapPrefix(snapId) { const prefix = Object.values(types_1.SnapIdPrefixes).find((possiblePrefix) => snapId.startsWith(possiblePrefix)); if (prefix !== undefined) { return prefix; } throw new Error(`Invalid or no prefix found for "${snapId}"`); } exports.getSnapPrefix = getSnapPrefix; /** * Strips snap prefix from a full snap ID. * * @param snapId - The snap ID to strip. * @returns The stripped snap ID. */ function stripSnapPrefix(snapId) { return snapId.replace(getSnapPrefix(snapId), ''); } exports.stripSnapPrefix = stripSnapPrefix; /** * Check if the given value is a valid snap ID. This function is a type guard, * and will narrow the type of the value to `SnapId` if it returns `true`. * * @param value - The value to check. * @returns `true` if the value is a valid snap ID, and `false` otherwise. */ function isSnapId(value) { return (0, superstruct_1.is)(value, exports.SnapIdStruct); } exports.isSnapId = isSnapId; /** * Assert that the given value is a valid snap ID. * * @param value - The value to check. * @throws If the value is not a valid snap ID. */ function assertIsValidSnapId(value) { (0, utils_1.assertStruct)(value, exports.SnapIdStruct, 'Invalid snap ID'); } exports.assertIsValidSnapId = assertIsValidSnapId; /** * Utility function to check if an origin has permission (and caveat) for a particular snap. * * @param permissions - An origin's permissions object. * @param snapId - The id of the snap. * @returns A boolean based on if an origin has the specified snap. */ function isSnapPermitted(permissions, snapId) { return Boolean((permissions?.wallet_snap?.caveats?.find((caveat) => caveat.type === caveats_1.SnapCaveatType.SnapIds) ?? {}).value?.[snapId]); } exports.isSnapPermitted = isSnapPermitted; /** * Checks whether the passed in requestedPermissions is a valid * permission request for a `wallet_snap` permission. * * @param requestedPermissions - The requested permissions. * @throws If the criteria is not met. */ function verifyRequestedSnapPermissions(requestedPermissions) { (0, utils_1.assert)((0, utils_1.isObject)(requestedPermissions), 'Requested permissions must be an object.'); const { wallet_snap: walletSnapPermission } = requestedPermissions; (0, utils_1.assert)((0, utils_1.isObject)(walletSnapPermission), 'wallet_snap is missing from the requested permissions.'); const { caveats } = walletSnapPermission; (0, utils_1.assert)(Array.isArray(caveats) && caveats.length === 1, 'wallet_snap must have a caveat property with a single-item array value.'); const [caveat] = caveats; (0, utils_1.assert)((0, utils_1.isObject)(caveat) && caveat.type === caveats_1.SnapCaveatType.SnapIds && (0, utils_1.isObject)(caveat.value), `The requested permissions do not have a valid ${caveats_1.SnapCaveatType.SnapIds} caveat.`); } exports.verifyRequestedSnapPermissions = verifyRequestedSnapPermissions; //# sourceMappingURL=snaps.cjs.map