UNPKG

@metamask/snaps-utils

Version:
219 lines 9.18 kB
function $importDefault(module) { if (module?.__esModule) { return module.default; } return module; } import { selectiveUnion } from "@metamask/snaps-sdk"; import { is, empty, enums, intersection, literal, refine, string, validate } from "@metamask/superstruct"; import { assert, isObject, assertStruct, definePattern } from "@metamask/utils"; import { base64 } from "@scure/base"; import $stableStringify from "fast-json-stable-stringify"; const stableStringify = $importDefault($stableStringify); import $validateNPMPackage from "validate-npm-package-name"; const validateNPMPackage = $importDefault($validateNPMPackage); import { SnapCaveatType } from "./caveats.mjs"; import { checksumFiles } from "./checksum.mjs"; import { SnapIdPrefixes, uri } from "./types.mjs"; // 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 export const 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; export var SnapStatus; (function (SnapStatus) { SnapStatus["Installing"] = "installing"; SnapStatus["Updating"] = "updating"; SnapStatus["Running"] = "running"; SnapStatus["Stopped"] = "stopped"; SnapStatus["Crashed"] = "crashed"; })(SnapStatus || (SnapStatus = {})); export var SnapStatusEvents; (function (SnapStatusEvents) { SnapStatusEvents["Start"] = "START"; SnapStatusEvents["Stop"] = "STOP"; SnapStatusEvents["Crash"] = "CRASH"; SnapStatusEvents["Update"] = "UPDATE"; })(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 = stableStringify(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. */ export async function getSnapChecksum(files) { const { manifest, sourceCode, svgIcon, auxiliaryFiles, localizationFiles } = files; const all = [ getChecksummableManifest(manifest), sourceCode, svgIcon, ...auxiliaryFiles, ...localizationFiles, ].filter((file) => file !== undefined); return base64.encode(await checksumFiles(all)); } /** * 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. */ export 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); } } export const LOCALHOST_HOSTNAMES = ['localhost', '127.0.0.1', '[::1]']; // Require snap ids to only consist of printable ASCII characters export const BaseSnapIdStruct = definePattern('Base Snap Id', /^[\x21-\x7E]*$/u); const LocalSnapIdSubUrlStruct = uri({ protocol: enums(['http:', 'https:']), hostname: enums(LOCALHOST_HOSTNAMES), hash: empty(string()), search: empty(string()), }); export const LocalSnapIdStruct = refine(BaseSnapIdStruct, 'local Snap Id', (value) => { if (!value.startsWith(SnapIdPrefixes.local)) { return `Expected local snap ID, got "${value}".`; } const [error] = validate(value.slice(SnapIdPrefixes.local.length), LocalSnapIdSubUrlStruct); return error ?? true; }); export const NpmSnapIdStruct = intersection([ BaseSnapIdStruct, uri({ protocol: literal(SnapIdPrefixes.npm), pathname: refine(string(), 'package name', function* (value) { const normalized = value.startsWith('/') ? value.slice(1) : value; const { errors, validForNewPackages, warnings } = validateNPMPackage(normalized); if (!validForNewPackages) { if (errors === undefined) { assert(warnings !== undefined); yield* warnings; } else { yield* errors; } } return true; }), search: empty(string()), hash: empty(string()), }), ]); export const HttpSnapIdStruct = intersection([ BaseSnapIdStruct, uri({ protocol: enums(['http:', 'https:']), search: empty(string()), hash: empty(string()), }), ]); export const SnapIdPrefixStruct = refine(string(), 'Snap ID prefix', (value) => { if (Object.values(SnapIdPrefixes).some((prefix) => value.startsWith(prefix))) { return true; } const allowedPrefixes = Object.values(SnapIdPrefixes) .map((prefix) => `"${prefix}"`) .join(', '); return `Invalid or no prefix found. Expected Snap ID to start with one of: ${allowedPrefixes}, but received: "${value}"`; }); export const SnapIdStruct = selectiveUnion((value) => { if (typeof value === 'string' && value.startsWith(SnapIdPrefixes.npm)) { return NpmSnapIdStruct; } if (typeof value === 'string' && value.startsWith(SnapIdPrefixes.local)) { return LocalSnapIdStruct; } return 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:`. */ export function getSnapPrefix(snapId) { const prefix = Object.values(SnapIdPrefixes).find((possiblePrefix) => snapId.startsWith(possiblePrefix)); if (prefix !== undefined) { return prefix; } throw new Error(`Invalid or no prefix found for "${snapId}"`); } /** * Strips snap prefix from a full snap ID. * * @param snapId - The snap ID to strip. * @returns The stripped snap ID. */ export function stripSnapPrefix(snapId) { return snapId.replace(getSnapPrefix(snapId), ''); } /** * 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. */ export function isSnapId(value) { return is(value, SnapIdStruct); } /** * 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. */ export function assertIsValidSnapId(value) { assertStruct(value, SnapIdStruct, 'Invalid snap ID'); } /** * 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. */ export function isSnapPermitted(permissions, snapId) { return Boolean((permissions?.wallet_snap?.caveats?.find((caveat) => caveat.type === SnapCaveatType.SnapIds) ?? {}).value?.[snapId]); } /** * 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. */ export function verifyRequestedSnapPermissions(requestedPermissions) { assert(isObject(requestedPermissions), 'Requested permissions must be an object.'); const { wallet_snap: walletSnapPermission } = requestedPermissions; assert(isObject(walletSnapPermission), 'wallet_snap is missing from the requested permissions.'); const { caveats } = walletSnapPermission; assert(Array.isArray(caveats) && caveats.length === 1, 'wallet_snap must have a caveat property with a single-item array value.'); const [caveat] = caveats; assert(isObject(caveat) && caveat.type === SnapCaveatType.SnapIds && isObject(caveat.value), `The requested permissions do not have a valid ${SnapCaveatType.SnapIds} caveat.`); } //# sourceMappingURL=snaps.mjs.map