@metamask/snaps-utils
Version:
A collection of utilities for MetaMask Snaps
225 lines • 10.8 kB
JavaScript
;
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