From 70ca4bf5f8924a3c8af9ffcd7dace4e6b3ed8f99 Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Mon, 14 Nov 2022 19:31:29 +0100 Subject: [PATCH 01/13] [TMP] Added SemVer opaque types --- .../snaps-cli/src/cmds/init/initHandler.ts | 6 +- .../src/snaps/SnapController.ts | 17 +-- .../snaps-controllers/src/snaps/utils/npm.ts | 21 ++- .../snaps-utils/src/manifest/validation.ts | 3 +- packages/snaps-utils/src/snaps.ts | 3 +- packages/snaps-utils/src/types.ts | 10 +- packages/snaps-utils/src/versions.test.ts | 118 +++++++++------- packages/snaps-utils/src/versions.ts | 131 ++++++++++++++---- 8 files changed, 208 insertions(+), 101 deletions(-) diff --git a/packages/snaps-cli/src/cmds/init/initHandler.ts b/packages/snaps-cli/src/cmds/init/initHandler.ts index 8a8a06c0f5..51a4b7bb65 100644 --- a/packages/snaps-cli/src/cmds/init/initHandler.ts +++ b/packages/snaps-cli/src/cmds/init/initHandler.ts @@ -4,6 +4,8 @@ import { readJsonFile, satisfiesVersionRange, NpmSnapPackageJson, + SemVerVersion, + SemVerRange, } from '@metamask/snaps-utils'; import { promises as fs } from 'fs'; import pathUtils from 'path'; @@ -19,7 +21,7 @@ import { yarnInstall, } from './initUtils'; -const SATISFIED_VERSION = '>=16'; +const SATISFIED_VERSION = '>=16' as SemVerRange; /** * Creates a new snap package, based on one of the provided templates. This @@ -35,7 +37,7 @@ export async function initHandler(argv: YargsArgs) { const { directory } = argv; const isVersionSupported = satisfiesVersionRange( - process.version, + process.version as SemVerVersion, SATISFIED_VERSION, ); diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 79587bb621..89b338c7dc 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -28,14 +28,15 @@ import { getSnapPrefix, gtVersion, InstallSnapsResult, - isValidSnapVersionRange, + isValidSemVerRange, LOCALHOST_HOSTNAMES, NpmSnapFileNames, PersistedSnap, ProcessSnapResult, RequestedSnapPermissions, - resolveVersion, + resolveVersionRange, satisfiesVersionRange, + SemVerRange, Snap, SnapId, SnapIdPrefixes, @@ -536,7 +537,7 @@ type SnapControllerArgs = { type AddSnapArgsBase = { id: SnapId; origin: string; - versionRange?: string; + versionRange?: SemVerRange; }; // A snap can either be added directly, with manifest and source code, or it @@ -1535,10 +1536,10 @@ export class SnapController extends BaseController< await Promise.all( Object.entries(requestedSnaps).map( async ([snapId, { version: rawVersion }]) => { - const version = resolveVersion(rawVersion); + const version = resolveVersionRange(rawVersion); const permissionName = getSnapPermissionName(snapId); - if (!isValidSnapVersionRange(version)) { + if (!isValidSemVerRange(version)) { result[snapId] = { error: ethErrors.rpc.invalidParams( `The "version" field must be a valid SemVer version range if specified. Received: "${version}".`, @@ -1585,7 +1586,7 @@ export class SnapController extends BaseController< private async processRequestedSnap( origin: string, snapId: SnapId, - versionRange: string, + versionRange: SemVerRange, ): Promise { try { validateSnapId(snapId); @@ -1688,7 +1689,7 @@ export class SnapController extends BaseController< ): Promise { const snap = this.getExpect(snapId); - if (!isValidSnapVersionRange(newVersionRange)) { + if (!isValidSemVerRange(newVersionRange)) { throw new Error( `Received invalid snap version range: "${newVersionRange}".`, ); @@ -2052,7 +2053,7 @@ export class SnapController extends BaseController< packageName: string, versionRange: string, ): Promise { - if (!isValidSnapVersionRange(versionRange)) { + if (!isValidSemVerRange(versionRange)) { throw new Error( `Received invalid Snap version range: "${versionRange}".`, ); diff --git a/packages/snaps-controllers/src/snaps/utils/npm.ts b/packages/snaps-controllers/src/snaps/utils/npm.ts index 2d3efee1fb..ac2433ec1a 100644 --- a/packages/snaps-controllers/src/snaps/utils/npm.ts +++ b/packages/snaps-controllers/src/snaps/utils/npm.ts @@ -4,6 +4,9 @@ import { isValidUrl, getTargetVersion, validateNpmSnap, + assertIsSemVerVersion, + SemVerRange, + SemVerVersion, } from '@metamask/snaps-utils'; import { isObject } from '@metamask/utils'; import createGunzipStream from 'gunzip-maybe'; @@ -26,7 +29,7 @@ export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org'; */ export async function fetchNpmSnap( packageName: string, - versionRange: string, + versionRange: SemVerRange, registryUrl = DEFAULT_NPM_REGISTRY, fetchFunction = fetch, ): Promise { @@ -56,7 +59,7 @@ export async function fetchNpmSnap( // object if they exist. return validateNpmSnap( snapFiles, - `npm Snap "${packageName}@${actualVersion}" validation error: `, + `npm Snap "${packageName}@${actualVersion}" validation error: ` as `${string}: `, ); } @@ -75,10 +78,10 @@ export async function fetchNpmSnap( */ async function fetchNpmTarball( packageName: string, - versionRange: string, + versionRange: SemVerRange, registryUrl = DEFAULT_NPM_REGISTRY, fetchFunction = fetch, -): Promise<[ReadableStream, string]> { +): Promise<[ReadableStream, SemVerVersion]> { const packageMetadata = await ( await fetchFunction(new URL(packageName, registryUrl).toString()) ).json(); @@ -89,11 +92,15 @@ async function fetchNpmTarball( ); } - const targetVersion = getTargetVersion( - Object.keys((packageMetadata as any)?.versions ?? {}), - versionRange, + const versions = Object.keys((packageMetadata as any)?.versions ?? {}).map( + (v) => { + assertIsSemVerVersion(v); + return v; + }, ); + const targetVersion = getTargetVersion(versions, versionRange); + if (targetVersion === null) { throw new Error( `Failed to find a matching version in npm metadata for package "${packageName}" and requested semver range "${versionRange}"`, diff --git a/packages/snaps-utils/src/manifest/validation.ts b/packages/snaps-utils/src/manifest/validation.ts index 09d423c7da..4c67ee91f9 100644 --- a/packages/snaps-utils/src/manifest/validation.ts +++ b/packages/snaps-utils/src/manifest/validation.ts @@ -20,7 +20,8 @@ import { import { CronjobSpecificationArrayStruct } from '../cronjob'; import { NamespacesStruct } from '../namespace'; -import { NameStruct, NpmSnapFileNames, VersionStruct } from '../types'; +import { VersionStruct } from '../versions'; +import { NameStruct, NpmSnapFileNames } from '../types'; // 0xd36e6170 - 0x80000000 export const SIP_6_MAGIC_VALUE = `1399742832'` as `${number}'`; diff --git a/packages/snaps-utils/src/snaps.ts b/packages/snaps-utils/src/snaps.ts index 3f0b021ee3..4e925c7bf7 100644 --- a/packages/snaps-utils/src/snaps.ts +++ b/packages/snaps-utils/src/snaps.ts @@ -5,6 +5,7 @@ import { SerializedEthereumRpcError } from 'eth-rpc-errors/dist/classes'; import { SnapManifest, SnapPermissions } from './manifest/validation'; import { SnapId, SnapIdPrefixes, SnapValidationFailureReason } from './types'; +import { SemVerVersion } from './versions'; export const LOCALHOST_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']); export const SNAP_PREFIX = 'wallet_snap_'; @@ -119,7 +120,7 @@ export type Snap = { /** * The version of the Snap. */ - version: string; + version: SemVerVersion; /** * The version history of the Snap. diff --git a/packages/snaps-utils/src/types.ts b/packages/snaps-utils/src/types.ts index b3aae7ff6f..04ce7203a4 100644 --- a/packages/snaps-utils/src/types.ts +++ b/packages/snaps-utils/src/types.ts @@ -3,33 +3,25 @@ import { SnapKeyring as Keyring, } from '@metamask/snaps-types'; import { assertStruct, Json } from '@metamask/utils'; -import { valid as validSemver } from 'semver'; import { Infer, is, object, optional, pattern, - refine, size, string, type, } from 'superstruct'; import { SnapManifest } from './manifest/validation'; +import { VersionStruct } from './versions'; export enum NpmSnapFileNames { PackageJson = 'package.json', Manifest = 'snap.manifest.json', } -/** - * A struct for validating a version string. - */ -export const VersionStruct = refine(string(), 'Version', (value) => { - return validSemver(value) !== null; -}); - export const NameStruct = size( pattern( string(), diff --git a/packages/snaps-utils/src/versions.test.ts b/packages/snaps-utils/src/versions.test.ts index 87b28c14c4..b97446f9cc 100644 --- a/packages/snaps-utils/src/versions.test.ts +++ b/packages/snaps-utils/src/versions.test.ts @@ -4,22 +4,38 @@ import { DEFAULT_REQUESTED_SNAP_VERSION, getTargetVersion, gtVersion, - isValidSnapVersionRange, - resolveVersion, + isValidSemVerRange, + resolveVersionRange, satisfiesVersionRange, + SemVerRange, + SemVerVersion, } from './versions'; +function v(value: string[]): SemVerVersion[]; +function v(value: string): SemVerVersion; +function v(value: string | string[]): SemVerVersion | SemVerVersion[] { + return value as any; +} + +function r(value: string): SemVerRange { + return value as SemVerRange; +} + describe('resolveVersion', () => { it('defaults "latest" to DEFAULT_REQUESTED_SNAP_VERSION', () => { - expect(resolveVersion('latest')).toBe(DEFAULT_REQUESTED_SNAP_VERSION); + expect(resolveVersionRange('latest')).toBe(DEFAULT_REQUESTED_SNAP_VERSION); }); it('defaults an undefined version to DEFAULT_REQUESTED_SNAP_VERSION', () => { - expect(resolveVersion(undefined)).toBe(DEFAULT_REQUESTED_SNAP_VERSION); + expect(resolveVersionRange(undefined)).toBe(DEFAULT_REQUESTED_SNAP_VERSION); }); it('returns the requested version for everything else', () => { - expect(resolveVersion('1.2.3')).toBe('1.2.3'); + expect(resolveVersionRange('1.2.3')).toBe('1.2.3'); + }); + + it.each([null, 1, {}, Error])('throws on invalid input', (value) => { + expect(() => resolveVersionRange(value)).toThrow('asd'); }); }); @@ -39,99 +55,105 @@ describe('getSnapPrefix', () => { }); }); -describe('isValidSnapVersionRange', () => { +describe('isValidSemVerRange', () => { it('supports *', () => { - expect(isValidSnapVersionRange('*')).toBe(true); + expect(isValidSemVerRange('*')).toBe(true); }); it('supports normal version ranges', () => { - expect(isValidSnapVersionRange('^1.2.3')).toBe(true); - expect(isValidSnapVersionRange('1.5.0')).toBe(true); + expect(isValidSemVerRange('^1.2.3')).toBe(true); + expect(isValidSemVerRange('1.5.0')).toBe(true); }); it('supports pre-release versions', () => { - expect(isValidSnapVersionRange('1.0.0-beta.1')).toBe(true); - expect(isValidSnapVersionRange('^1.0.0-beta.1')).toBe(true); + expect(isValidSemVerRange('1.0.0-beta.1')).toBe(true); + expect(isValidSemVerRange('^1.0.0-beta.1')).toBe(true); }); it('rejects non strings', () => { - expect(isValidSnapVersionRange(null)).toBe(false); - expect(isValidSnapVersionRange(undefined)).toBe(false); - expect(isValidSnapVersionRange(2)).toBe(false); - expect(isValidSnapVersionRange(true)).toBe(false); - expect(isValidSnapVersionRange({})).toBe(false); + expect(isValidSemVerRange(null)).toBe(false); + expect(isValidSemVerRange(undefined)).toBe(false); + expect(isValidSemVerRange(2)).toBe(false); + expect(isValidSemVerRange(true)).toBe(false); + expect(isValidSemVerRange({})).toBe(false); }); }); describe('gtVersion', () => { it('supports regular versions', () => { - expect(gtVersion('1.2.3', '1.0.0')).toBe(true); - expect(gtVersion('2.0.0', '1.0.0')).toBe(true); - expect(gtVersion('1.0.0', '1.2.3')).toBe(false); - expect(gtVersion('1.0.0', '2.0.0')).toBe(false); + expect(gtVersion(v('1.2.3'), v('1.0.0'))).toBe(true); + expect(gtVersion(v('2.0.0'), v('1.0.0'))).toBe(true); + expect(gtVersion(v('1.0.0'), v('1.2.3'))).toBe(false); + expect(gtVersion(v('1.0.0'), v('2.0.0'))).toBe(false); }); it('supports pre-release versions', () => { - expect(gtVersion('1.0.0-beta.2', '1.0.0-beta.1')).toBe(true); - expect(gtVersion('1.0.0-beta.2', '1.2.3')).toBe(false); - expect(gtVersion('1.0.0', '1.0.0-beta.2')).toBe(true); - expect(gtVersion('1.2.3-beta.1', '1.0.0')).toBe(true); - expect(gtVersion('1.2.3-beta.1', '1.2.3-alpha.2')).toBe(true); + expect(gtVersion(v('1.0.0-beta.2'), v('1.0.0-beta.1'))).toBe(true); + expect(gtVersion(v('1.0.0-beta.2'), v('1.2.3'))).toBe(false); + expect(gtVersion(v('1.0.0'), v('1.0.0-beta.2'))).toBe(true); + expect(gtVersion(v('1.2.3-beta.1'), v('1.0.0'))).toBe(true); + expect(gtVersion(v('1.2.3-beta.1'), v('1.2.3-alpha.2'))).toBe(true); }); }); describe('getTargetVersion', () => { it('supports *', () => { - expect(getTargetVersion(['1.2.3', '3.0.0'], '*')).toBe('3.0.0'); + expect(getTargetVersion(v(['1.2.3', '3.0.0']), r('*'))).toBe('3.0.0'); }); it('supports pre-release versions', () => { - expect(getTargetVersion(['1.0.0-beta.1'], '*')).toBe('1.0.0-beta.1'); - expect(getTargetVersion(['1.0.0-beta.1', '1.0.0-beta.2'], '*')).toBe( + expect(getTargetVersion(v(['1.0.0-beta.1']), r('*'))).toBe('1.0.0-beta.1'); + expect(getTargetVersion(v(['1.0.0-beta.1', '1.0.0-beta.2']), r('*'))).toBe( '1.0.0-beta.2', ); expect( - getTargetVersion(['1.0.0-beta.1', '1.0.0-beta.2'], '^1.0.0-beta.1'), + getTargetVersion(v(['1.0.0-beta.1', '1.0.0-beta.2']), r('^1.0.0-beta.1')), ).toBe('1.0.0-beta.2'); - expect(getTargetVersion(['1.0.0-alpha.2', '1.0.0-beta.1'], '*')).toBe( + expect(getTargetVersion(v(['1.0.0-alpha.2', '1.0.0-beta.1']), r('*'))).toBe( '1.0.0-beta.1', ); - expect(getTargetVersion(['0.9.0', '1.0.0-alpha.0'], '*')).toBe('0.9.0'); + expect(getTargetVersion(v(['0.9.0', '1.0.0-alpha.0']), r('*'))).toBe( + '0.9.0', + ); }); - it('doesnt return pre-release versions by default', () => { - expect(getTargetVersion(['1.0.0-beta.1', '1.0.0', '1.2.3'], '*')).toBe( - '1.2.3', - ); + it("doesn't return pre-release versions by default", () => { + expect( + getTargetVersion(v(['1.0.0-beta.1', '1.0.0', '1.2.3']), r('*')), + ).toBe('1.2.3'); - expect(getTargetVersion(['1.0.0-beta.1', '1.0.0', '1.2.3'], '^1.0.0')).toBe( - '1.2.3', + expect( + getTargetVersion(v(['1.0.0-beta.1', '1.0.0', '1.2.3']), r('^1.0.0')), + ).toBe('1.2.3'); + expect(getTargetVersion(v(['1.0.0-beta.1', '1.0.0']), r('*'))).toBe( + '1.0.0', ); - expect(getTargetVersion(['1.0.0-beta.1', '1.0.0'], '*')).toBe('1.0.0'); }); }); -describe('satifiesVersionRange', () => { +describe('satisfiesVersionRange', () => { it('supports *', () => { - expect(satisfiesVersionRange('3.0.0', '*')).toBe(true); + expect(satisfiesVersionRange(v('3.0.0'), r('*'))).toBe(true); }); it('supports exact versions', () => { - expect(satisfiesVersionRange('1.0.0-beta.1', '1.0.0-beta.1')).toBe(true); - expect(satisfiesVersionRange('1.0.0', '1.0.0')).toBe(true); - expect(satisfiesVersionRange('1.2.3', '1.0.0')).toBe(false); + expect(satisfiesVersionRange(v('1.0.0-beta.1'), r('1.0.0-beta.1'))).toBe( + true, + ); + expect(satisfiesVersionRange(v('1.0.0'), r('1.0.0'))).toBe(true); + expect(satisfiesVersionRange(v('1.2.3'), r('1.0.0'))).toBe(false); }); it('supports non-exact version ranges', () => { - expect(satisfiesVersionRange('1.2.3', '^1.0.0')).toBe(true); - expect(satisfiesVersionRange('2.0.0', '^1.0.0')).toBe(false); + expect(satisfiesVersionRange(v('1.2.3'), r('^1.0.0'))).toBe(true); + expect(satisfiesVersionRange(v('2.0.0'), r('^1.0.0'))).toBe(false); }); - it('prereleases can satisfy version range', () => { - expect(satisfiesVersionRange('1.0.0-beta.1', '*')).toBe(true); - expect(satisfiesVersionRange('1.0.0-beta.1', '^1.0.0')).toBe(false); + it('pre-releases can satisfy version range', () => { + expect(satisfiesVersionRange(v('1.0.0-beta.1'), r('*'))).toBe(true); + expect(satisfiesVersionRange(v('1.0.0-beta.1'), r('^1.0.0'))).toBe(false); }); }); diff --git a/packages/snaps-utils/src/versions.ts b/packages/snaps-utils/src/versions.ts index 574e1650f8..8f559dadd3 100644 --- a/packages/snaps-utils/src/versions.ts +++ b/packages/snaps-utils/src/versions.ts @@ -1,12 +1,104 @@ -import { Json } from '@metamask/utils'; +import { assertStruct, Json } from '@metamask/utils'; import { gt as gtSemver, maxSatisfying as maxSatisfyingSemver, satisfies as satisfiesSemver, - validRange as validRangeSemver, + valid as validSemVerVersion, + validRange as validSemVerRange, } from 'semver'; +import { is, refine, string, Struct } from 'superstruct'; -export const DEFAULT_REQUESTED_SNAP_VERSION = '*'; +export const DEFAULT_REQUESTED_SNAP_VERSION = '*' as SemVerRange; + +/** + * {@link https://codemix.com/opaque-types-in-javascript/ Opaque} type for SemVer ranges. + * Castable from string. + */ +declare type SemVerRange = { + __TYPE: 'semver_range'; +} & string; +/** + * {@link https://codemix.com/opaque-types-in-javascript/ Opaque} type for singular SemVer version. + * Castable from string. + */ +declare type SemVerVersion = { + __TYPE: 'semver_version'; +} & string; +export { SemVerRange, SemVerVersion }; + +/** + * A struct for validating a version string. + */ +export const VersionStruct = refine( + string() as unknown as Struct, + 'Version', + (value) => { + if (validSemVerVersion(value) === null) { + return `Expected SemVer version, got "${value}"`; + } + return true; + }, +); + +export const VersionRangeStruct = refine( + string() as unknown as Struct, + 'Version range', + (value) => { + if (validSemVerRange(value) === null) { + return `Expected SemVer range, got "${value}"`; + } + return true; + }, +); + +/** + * Checks whether a SemVer version is valid. + * + * @param version - A potential version. + * @returns `true` if the version is valid, and `false` otherwise. + */ +export function isValidSemVerVersion( + version: unknown, +): version is SemVerVersion { + return Boolean( + typeof version === 'string' && + validSemVerVersion(version, { includePrerelease: true }) !== null, + ); +} + +/** + * Checks whether a SemVer version range is valid. + * + * @param versionRange - A potential version range. + * @returns `true` if the version range is valid, and `false` otherwise. + */ +export function isValidSemVerRange( + versionRange: unknown, +): versionRange is SemVerRange { + return is(versionRange, VersionStruct); +} + +/** + * Asserts that a value is a valid concrete SemVer version. + * + * @param version - A potential SemVer concrete version. + */ +export function assertIsSemVerVersion( + version: unknown, +): asserts version is SemVerVersion { + assertStruct(version, VersionStruct); +} + +/** + * Asserts that a value is a valid SemVer range. + * + * @param range - A potential SemVer range. + */ +export function assertIsSemVerRange( + range: unknown, +): asserts range is SemVerRange { + assertStruct(range, VersionRangeStruct); +} /** * Checks whether a SemVer version is greater than another. @@ -15,7 +107,10 @@ export const DEFAULT_REQUESTED_SNAP_VERSION = '*'; * @param version2 - The right-hand version. * @returns `version1 > version2`. */ -export function gtVersion(version1: string, version2: string): boolean { +export function gtVersion( + version1: SemVerVersion, + version2: SemVerVersion, +): boolean { return gtSemver(version1, version2); } @@ -27,8 +122,8 @@ export function gtVersion(version1: string, version2: string): boolean { * @returns Whether the version satisfied the version range. */ export function satisfiesVersionRange( - version: string, - versionRange: string, + version: SemVerVersion, + versionRange: SemVerRange, ): boolean { return satisfiesSemver(version, versionRange, { includePrerelease: true, @@ -46,9 +141,9 @@ export function satisfiesVersionRange( * or `null` if none of them do. */ export function getTargetVersion( - versions: string[], - versionRange: string, -): string | null { + versions: SemVerVersion[], + versionRange: SemVerRange, +): SemVerVersion | null { const maxSatisfyingNonPreRelease = maxSatisfyingSemver( versions, versionRange, @@ -72,24 +167,10 @@ export function getTargetVersion( * @returns `*` if the version is `undefined` or `latest", otherwise returns * the specified version. */ -export function resolveVersion(version?: Json): Json { +export function resolveVersionRange(version?: Json): SemVerRange { if (version === undefined || version === 'latest') { return DEFAULT_REQUESTED_SNAP_VERSION; } + assertIsSemVerRange(version); return version; } - -/** - * Checks whether a SemVer version range is valid. - * - * @param versionRange - A potential version range. - * @returns `true` if the version range is valid, and `false` otherwise. - */ -export function isValidSnapVersionRange( - versionRange: unknown, -): versionRange is string { - return Boolean( - typeof versionRange === 'string' && - validRangeSemver(versionRange, { includePrerelease: true }) !== null, - ); -} From 1bf305bd476b4b878600227af256231fd73b90cc Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Mon, 14 Nov 2022 20:39:43 +0100 Subject: [PATCH 02/13] Added opaque types for SemVers --- .../src/cronjob/CronjobController.test.ts | 7 +- .../src/snaps/SnapController.test.ts | 28 +++++-- .../src/snaps/SnapController.ts | 7 +- packages/snaps-utils/jest.config.js | 2 +- .../snaps-utils/src/test-utils/manifest.ts | 20 +++-- packages/snaps-utils/src/test-utils/snap.ts | 23 +++-- packages/snaps-utils/src/versions.test.ts | 83 +++++++++++++++++-- packages/snaps-utils/src/versions.ts | 26 +++--- 8 files changed, 152 insertions(+), 44 deletions(-) diff --git a/packages/snaps-controllers/src/cronjob/CronjobController.test.ts b/packages/snaps-controllers/src/cronjob/CronjobController.test.ts index 875aafec1b..8b50c9ffb5 100644 --- a/packages/snaps-controllers/src/cronjob/CronjobController.test.ts +++ b/packages/snaps-controllers/src/cronjob/CronjobController.test.ts @@ -3,6 +3,7 @@ import { HandlerType, deepClone, TruncatedSnap, + SemVerVersion, } from '@metamask/snaps-utils'; import { MOCK_ORIGIN, @@ -315,7 +316,7 @@ describe('CronjobController', () => { id: MOCK_SNAP_ID, initialPermissions: {}, permissionName: '', - version: '', + version: '1.0' as SemVerVersion, }; // @ts-expect-error Accessing private property cronjobController._handleEventSnapInstalled(snapInfo); @@ -367,7 +368,7 @@ describe('CronjobController', () => { id: MOCK_SNAP_ID, initialPermissions: {}, permissionName: '', - version: '', + version: '1.0' as SemVerVersion, }; // @ts-expect-error Accessing private property @@ -432,7 +433,7 @@ describe('CronjobController', () => { id: MOCK_SNAP_ID, initialPermissions: {}, permissionName: '', - version: '', + version: '1.0' as SemVerVersion, }; // @ts-expect-error Accessing private property diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.ts b/packages/snaps-controllers/src/snaps/SnapController.test.ts index 0b5c6e2a9c..2ca937df96 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.test.ts @@ -9,6 +9,7 @@ import { DEFAULT_ENDOWMENTS, getSnapSourceShasum, HandlerType, + SemVerVersion, SnapCaveatType, SnapManifest, SnapStatus, @@ -30,8 +31,7 @@ import fetchMock from 'jest-fetch-mock'; import { createAsyncMiddleware, JsonRpcEngine } from 'json-rpc-engine'; import { createEngineStream } from 'json-rpc-middleware-stream'; import pump from 'pump'; -import { Duplex } from 'stream'; - +import { Duplex } from 'stream';; import { NodeThreadExecutionService, setupMultiplex } from '../services'; import { ExecutionEnvironmentStub, @@ -2545,7 +2545,7 @@ describe('SnapController', () => { fetchSnapSpy.mockImplementationOnce(async () => { const manifest: SnapManifest = { ...getSnapManifest(), - version: '1.1.0', + version: '1.1.0' as SemVerVersion, }; return Promise.resolve({ @@ -2580,7 +2580,7 @@ describe('SnapController', () => { fetchSnapSpy.mockImplementationOnce(async () => { const manifest: SnapManifest = { ...getSnapManifest(), - version: '0.9.0', + version: '0.9.0' as SemVerVersion, }; return Promise.resolve({ @@ -2625,7 +2625,7 @@ describe('SnapController', () => { .mockImplementationOnce(async () => { const manifest: SnapManifest = { ...getSnapManifest(), - version: '1.1.0', + version: '1.1.0' as SemVerVersion, }; return Promise.resolve({ @@ -2757,7 +2757,7 @@ describe('SnapController', () => { fetchSnapSpy.mockImplementationOnce(async () => { const manifest: SnapManifest = { ...getSnapManifest(), - version: '1.1.0', + version: '1.1.0' as SemVerVersion, }; return Promise.resolve({ @@ -2864,7 +2864,7 @@ describe('SnapController', () => { fetchSnapSpy.mockImplementationOnce(async () => { const manifest: SnapManifest = { ...getSnapManifest(), - version: '1.1.0', + version: '1.1.0' as SemVerVersion, }; return Promise.resolve({ @@ -3143,7 +3143,13 @@ describe('SnapController', () => { }); /* eslint-enable @typescript-eslint/naming-convention */ +<<<<<<< HEAD callActionSpy.mockImplementation((method, ...args: unknown[]): any => { +======= + callActionSpy.mockImplementation((method, ...params) => { + assert(params.length > 0); + const request: any = params[0]; +>>>>>>> c8c60447 (Added opaque types for SemVers) if (method === 'PermissionController:hasPermission') { return true; } else if (method === 'ApprovalController:addRequest') { @@ -3949,6 +3955,10 @@ describe('SnapController', () => { ) { return true; } +<<<<<<< HEAD +======= + +>>>>>>> c8c60447 (Added opaque types for SemVers) return (originalCall as any)(method, ...args); }); @@ -3979,6 +3989,10 @@ describe('SnapController', () => { }, }; } +<<<<<<< HEAD +======= + +>>>>>>> c8c60447 (Added opaque types for SemVers) return (originalCall as any)(method, ...args); }); diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 89b338c7dc..c7fce0de55 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -1536,10 +1536,8 @@ export class SnapController extends BaseController< await Promise.all( Object.entries(requestedSnaps).map( async ([snapId, { version: rawVersion }]) => { - const version = resolveVersionRange(rawVersion); - const permissionName = getSnapPermissionName(snapId); - - if (!isValidSemVerRange(version)) { + const [err, version] = resolveVersionRange(rawVersion); + if (err) { result[snapId] = { error: ethErrors.rpc.invalidParams( `The "version" field must be a valid SemVer version range if specified. Received: "${version}".`, @@ -1547,6 +1545,7 @@ export class SnapController extends BaseController< }; return; } + const permissionName = getSnapPermissionName(snapId); if ( this.messagingSystem.call( diff --git a/packages/snaps-utils/jest.config.js b/packages/snaps-utils/jest.config.js index 03abb3f1aa..1b4f207447 100644 --- a/packages/snaps-utils/jest.config.js +++ b/packages/snaps-utils/jest.config.js @@ -14,7 +14,7 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 93.15, + branches: 93.45, functions: 95.18, lines: 98.07, statements: 98.07, diff --git a/packages/snaps-utils/src/test-utils/manifest.ts b/packages/snaps-utils/src/test-utils/manifest.ts index 68f3192130..5ccce7ff76 100644 --- a/packages/snaps-utils/src/test-utils/manifest.ts +++ b/packages/snaps-utils/src/test-utils/manifest.ts @@ -1,3 +1,4 @@ +import { SemVerVersion } from '../versions'; import { SnapManifest } from '../manifest/validation'; import { Chain, @@ -8,7 +9,10 @@ import { import { NpmSnapPackageJson } from '../types'; import { DEFAULT_SNAP_SHASUM } from './snap'; -type GetSnapManifestOptions = Partial> & { +type GetSnapManifestOptions = Partial< + Omit +> & { + version?: SemVerVersion | string; shasum?: string; filePath?: string; packageName?: string; @@ -16,6 +20,10 @@ type GetSnapManifestOptions = Partial> & { iconPath?: string; }; +type GetPackageJsonOptions = Partial> & { + version?: SemVerVersion | string; +}; + /** * Get the default package repository, in a format compatible with * `package.json`. @@ -47,7 +55,7 @@ export const getDefaultRepository = () => { * @returns The snap manifest. */ export const getSnapManifest = ({ - version = '1.0.0', + version = '1.0.0' as SemVerVersion, description = 'The test example snap!', proposedName = '@metamask/example-snap', // eslint-disable-next-line @typescript-eslint/naming-convention @@ -59,7 +67,7 @@ export const getSnapManifest = ({ iconPath = 'images/icon.svg', }: GetSnapManifestOptions = {}): SnapManifest => { return { - version, + version: version as SemVerVersion, description, proposedName, repository, @@ -94,14 +102,14 @@ export const getSnapManifest = ({ */ export const getPackageJson = ({ name = '@metamask/example-snap', - version = '1.0.0', + version = '1.0.0' as SemVerVersion, description = 'The test example snap!', main = 'src/index.js', repository = getDefaultRepository(), -}: Partial = {}): NpmSnapPackageJson => { +}: GetPackageJsonOptions = {}): NpmSnapPackageJson => { return { name, - version, + version: version as SemVerVersion, description, main, repository, diff --git a/packages/snaps-utils/src/test-utils/snap.ts b/packages/snaps-utils/src/test-utils/snap.ts index 603efc078a..7b1cf00858 100644 --- a/packages/snaps-utils/src/test-utils/snap.ts +++ b/packages/snaps-utils/src/test-utils/snap.ts @@ -1,3 +1,4 @@ +import { SemVerVersion } from '../versions'; import { getSnapSourceShasum, PersistedSnap, @@ -27,6 +28,16 @@ export const MOCK_SNAP_ID = 'npm:@metamask/example-snap'; export const MOCK_LOCAL_SNAP_ID = 'local:@metamask/example-snap'; export const MOCK_ORIGIN = 'example.com'; +type GetPersistedSnapObjectOptions = Partial> & { + version?: string | SemVerVersion; +}; +type GetSnapObjectOptions = Partial> & { + version?: string | SemVerVersion; +}; +type GetTruncatedSnapOptions = Partial> & { + version?: string | SemVerVersion; +}; + export const getPersistedSnapObject = ({ blocked = false, enabled = true, @@ -40,13 +51,13 @@ export const getPersistedSnapObject = ({ versionHistory = [ { origin: MOCK_ORIGIN, version: '1.0.0', date: expect.any(Number) }, ], -}: Partial = {}): PersistedSnap => { +}: GetPersistedSnapObjectOptions = {}): PersistedSnap => { return { blocked, initialPermissions, id, permissionName, - version, + version: version as SemVerVersion, manifest, status, enabled, @@ -67,13 +78,13 @@ export const getSnapObject = ({ versionHistory = [ { origin: MOCK_ORIGIN, version: '1.0.0', date: expect.any(Number) }, ], -}: Partial = {}): Snap => { +}: GetSnapObjectOptions = {}): Snap => { return { blocked, initialPermissions, id, permissionName, - version, + version: version as SemVerVersion, manifest, status, enabled, @@ -88,12 +99,12 @@ export const getTruncatedSnap = ({ version = getSnapManifest().version, enabled = true, blocked = false, -}: Partial = {}): TruncatedSnap => { +}: GetTruncatedSnapOptions = {}): TruncatedSnap => { return { initialPermissions, id, permissionName, - version, + version: version as SemVerVersion, enabled, blocked, } as const; diff --git a/packages/snaps-utils/src/versions.test.ts b/packages/snaps-utils/src/versions.test.ts index b97446f9cc..2fa7837804 100644 --- a/packages/snaps-utils/src/versions.test.ts +++ b/packages/snaps-utils/src/versions.test.ts @@ -1,10 +1,14 @@ +import { assert } from '@metamask/utils'; import { getSnapPrefix } from './snaps'; import { SnapIdPrefixes } from './types'; import { + assertIsSemVerRange, + assertIsSemVerVersion, DEFAULT_REQUESTED_SNAP_VERSION, getTargetVersion, gtVersion, isValidSemVerRange, + isValidSemVerVersion, resolveVersionRange, satisfiesVersionRange, SemVerRange, @@ -13,29 +17,48 @@ import { function v(value: string[]): SemVerVersion[]; function v(value: string): SemVerVersion; +/** + * Utility to cast string to version. + * + * @param value - String to cast. + * @returns The same string with different type. + */ function v(value: string | string[]): SemVerVersion | SemVerVersion[] { return value as any; } +/** + * Utility to cast string to range. + * + * @param value - String to cast. + * @returns The same string with different type. + */ function r(value: string): SemVerRange { return value as SemVerRange; } describe('resolveVersion', () => { it('defaults "latest" to DEFAULT_REQUESTED_SNAP_VERSION', () => { - expect(resolveVersionRange('latest')).toBe(DEFAULT_REQUESTED_SNAP_VERSION); + expect(resolveVersionRange('latest')[1]).toBe( + DEFAULT_REQUESTED_SNAP_VERSION, + ); }); it('defaults an undefined version to DEFAULT_REQUESTED_SNAP_VERSION', () => { - expect(resolveVersionRange(undefined)).toBe(DEFAULT_REQUESTED_SNAP_VERSION); + expect(resolveVersionRange(undefined)[1]).toBe( + DEFAULT_REQUESTED_SNAP_VERSION, + ); }); it('returns the requested version for everything else', () => { - expect(resolveVersionRange('1.2.3')).toBe('1.2.3'); + expect(resolveVersionRange('1.2.3')[1]).toBe('1.2.3'); }); - it.each([null, 1, {}, Error])('throws on invalid input', (value) => { - expect(() => resolveVersionRange(value)).toThrow('asd'); + it.each([null, 1, {}, Error])('returns error on invalid input', (value) => { + const [err, result] = resolveVersionRange(value); + assert(err !== undefined); + expect(err.message).toMatch('Expected a string, but received: '); + expect(result).toBeUndefined(); }); }); @@ -55,6 +78,48 @@ describe('getSnapPrefix', () => { }); }); +describe('assertIsSemVerVersion', () => { + it('shows descriptive errors', () => { + expect(() => assertIsSemVerVersion('>1.2')).toThrow( + 'Expected SemVer version, got', + ); + }); +}); + +describe('assertIsSemVerRange', () => { + it('shows descriptive errors', () => { + expect(() => assertIsSemVerRange('.')).toThrow( + 'Expected SemVer range, got', + ); + }); +}); + +describe('isValidSemVerVersion', () => { + it.each([ + 'asd', + '()()', + '..', + '.', + '.1', + null, + undefined, + 2, + true, + {}, + Error, + ])('rejects invalid version', (version) => { + expect(isValidSemVerVersion(version)).toBe(false); + }); + + it('supports normal version ranges', () => { + expect(isValidSemVerVersion('1.5.0')).toBe(true); + }); + + it('supports pre-release versions', () => { + expect(isValidSemVerVersion('1.0.0-beta.1')).toBe(true); + }); +}); + describe('isValidSemVerRange', () => { it('supports *', () => { expect(isValidSemVerRange('*')).toBe(true); @@ -77,6 +142,13 @@ describe('isValidSemVerRange', () => { expect(isValidSemVerRange(true)).toBe(false); expect(isValidSemVerRange({})).toBe(false); }); + + it.each(['asd', '()()(', '..', '.', '1.'])( + 'rejects invalid ranges', + (range) => { + expect(isValidSemVerRange(range)).toBe(false); + }, + ); }); describe('gtVersion', () => { @@ -128,6 +200,7 @@ describe('getTargetVersion', () => { expect( getTargetVersion(v(['1.0.0-beta.1', '1.0.0', '1.2.3']), r('^1.0.0')), ).toBe('1.2.3'); + expect(getTargetVersion(v(['1.0.0-beta.1', '1.0.0']), r('*'))).toBe( '1.0.0', ); diff --git a/packages/snaps-utils/src/versions.ts b/packages/snaps-utils/src/versions.ts index 8f559dadd3..ce1a19cb4a 100644 --- a/packages/snaps-utils/src/versions.ts +++ b/packages/snaps-utils/src/versions.ts @@ -6,20 +6,24 @@ import { valid as validSemVerVersion, validRange as validSemVerRange, } from 'semver'; -import { is, refine, string, Struct } from 'superstruct'; +import { is, refine, string, Struct, validate } from 'superstruct'; export const DEFAULT_REQUESTED_SNAP_VERSION = '*' as SemVerRange; /** * {@link https://codemix.com/opaque-types-in-javascript/ Opaque} type for SemVer ranges. - * Castable from string. + * + * Use {@link assertIsSemVerRange} and {@link isValidSemVerRange} to cast to proper type. + * If you know what you're doing and want to side-step type safety, casting from a string works correctly. */ declare type SemVerRange = { __TYPE: 'semver_range'; } & string; /** * {@link https://codemix.com/opaque-types-in-javascript/ Opaque} type for singular SemVer version. - * Castable from string. + * + * Use {@link assertIsSemVerVersion} and {@link isValidSemVerVersion} to cast to proper type. + * If you know what you're doing and want to side-step type safety, casting from a string works correctly. */ declare type SemVerVersion = { __TYPE: 'semver_version'; @@ -60,10 +64,7 @@ export const VersionRangeStruct = refine( export function isValidSemVerVersion( version: unknown, ): version is SemVerVersion { - return Boolean( - typeof version === 'string' && - validSemVerVersion(version, { includePrerelease: true }) !== null, - ); + return is(version, VersionStruct); } /** @@ -75,7 +76,7 @@ export function isValidSemVerVersion( export function isValidSemVerRange( versionRange: unknown, ): versionRange is SemVerRange { - return is(versionRange, VersionStruct); + return is(versionRange, VersionRangeStruct); } /** @@ -167,10 +168,11 @@ export function getTargetVersion( * @returns `*` if the version is `undefined` or `latest", otherwise returns * the specified version. */ -export function resolveVersionRange(version?: Json): SemVerRange { +export function resolveVersionRange( + version?: Json, +): [undefined, SemVerRange] | [Error, undefined] { if (version === undefined || version === 'latest') { - return DEFAULT_REQUESTED_SNAP_VERSION; + return [undefined, DEFAULT_REQUESTED_SNAP_VERSION]; } - assertIsSemVerRange(version); - return version; + return validate(version, VersionRangeStruct); } From c97d2a049fffa3ff4c1dbd428a517306a13f0882 Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Mon, 14 Nov 2022 20:57:45 +0100 Subject: [PATCH 03/13] Better documentation --- packages/snaps-utils/src/versions.ts | 35 ++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/snaps-utils/src/versions.ts b/packages/snaps-utils/src/versions.ts index ce1a19cb4a..488c4e5420 100644 --- a/packages/snaps-utils/src/versions.ts +++ b/packages/snaps-utils/src/versions.ts @@ -13,17 +13,44 @@ export const DEFAULT_REQUESTED_SNAP_VERSION = '*' as SemVerRange; /** * {@link https://codemix.com/opaque-types-in-javascript/ Opaque} type for SemVer ranges. * - * Use {@link assertIsSemVerRange} and {@link isValidSemVerRange} to cast to proper type. - * If you know what you're doing and want to side-step type safety, casting from a string works correctly. + * @example Use {@link assertIsSemVerRange} and {@link isValidSemVerRange} to cast to proper type. + * ```typescript + * const unsafeRange: string = dataFromUser(); + * assertIsSemVerRange(unsafeRange); + * unsafeRange + * // ^? SemVerRange + * ``` + * @example If you know what you're doing and want to side-step type safety, casting from a string works correctly. + * ```typescript + * const unsafeRange: string = dataFromUser(); + * unsafeRange as SemVerRange; + * // ^? SemVerRange + * ``` + * @see {@link assertIsSemVerRange} + * @see {@link isValidSemVerRange} */ declare type SemVerRange = { __TYPE: 'semver_range'; } & string; + /** * {@link https://codemix.com/opaque-types-in-javascript/ Opaque} type for singular SemVer version. * - * Use {@link assertIsSemVerVersion} and {@link isValidSemVerVersion} to cast to proper type. - * If you know what you're doing and want to side-step type safety, casting from a string works correctly. + * @example Use {@link assertIsSemVerVersion} and {@link isValidSemVerVersion} to cast to proper type. + * ```typescript + * const unsafeVersion: string = dataFromUser(); + * assertIsSemVerVersion(unsafeRange); + * unsafeVersion + * // ^? SemVerVersion + * ``` + * @example If you know what you're doing and want to side-step type safety, casting from a string works correctly. + * ```typescript + * const unsafeVersion: string = dataFromUser(); + * unsafeRange as SemVerVersion; + * // ^? SemVerVersion + * ``` + * @see {@link assertIsSemVerVersion} + * @see {@link isValidSemVerVersion} */ declare type SemVerVersion = { __TYPE: 'semver_version'; From a7a11f0592c5d596e7bb335b8f36e0d5227b06de Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Mon, 14 Nov 2022 21:19:35 +0100 Subject: [PATCH 04/13] Removed versions.test.ts utillity functions --- packages/snaps-utils/src/versions.test.ts | 181 +++++++++++++++------- 1 file changed, 123 insertions(+), 58 deletions(-) diff --git a/packages/snaps-utils/src/versions.test.ts b/packages/snaps-utils/src/versions.test.ts index 2fa7837804..71554b38e5 100644 --- a/packages/snaps-utils/src/versions.test.ts +++ b/packages/snaps-utils/src/versions.test.ts @@ -15,28 +15,6 @@ import { SemVerVersion, } from './versions'; -function v(value: string[]): SemVerVersion[]; -function v(value: string): SemVerVersion; -/** - * Utility to cast string to version. - * - * @param value - String to cast. - * @returns The same string with different type. - */ -function v(value: string | string[]): SemVerVersion | SemVerVersion[] { - return value as any; -} - -/** - * Utility to cast string to range. - * - * @param value - String to cast. - * @returns The same string with different type. - */ -function r(value: string): SemVerRange { - return value as SemVerRange; -} - describe('resolveVersion', () => { it('defaults "latest" to DEFAULT_REQUESTED_SNAP_VERSION', () => { expect(resolveVersionRange('latest')[1]).toBe( @@ -153,80 +131,167 @@ describe('isValidSemVerRange', () => { describe('gtVersion', () => { it('supports regular versions', () => { - expect(gtVersion(v('1.2.3'), v('1.0.0'))).toBe(true); - expect(gtVersion(v('2.0.0'), v('1.0.0'))).toBe(true); - expect(gtVersion(v('1.0.0'), v('1.2.3'))).toBe(false); - expect(gtVersion(v('1.0.0'), v('2.0.0'))).toBe(false); + expect(gtVersion('1.2.3' as SemVerVersion, '1.0.0' as SemVerVersion)).toBe( + true, + ); + + expect(gtVersion('2.0.0' as SemVerVersion, '1.0.0' as SemVerVersion)).toBe( + true, + ); + + expect(gtVersion('1.0.0' as SemVerVersion, '1.2.3' as SemVerVersion)).toBe( + false, + ); + + expect(gtVersion('1.0.0' as SemVerVersion, '2.0.0' as SemVerVersion)).toBe( + false, + ); }); it('supports pre-release versions', () => { - expect(gtVersion(v('1.0.0-beta.2'), v('1.0.0-beta.1'))).toBe(true); - expect(gtVersion(v('1.0.0-beta.2'), v('1.2.3'))).toBe(false); - expect(gtVersion(v('1.0.0'), v('1.0.0-beta.2'))).toBe(true); - expect(gtVersion(v('1.2.3-beta.1'), v('1.0.0'))).toBe(true); - expect(gtVersion(v('1.2.3-beta.1'), v('1.2.3-alpha.2'))).toBe(true); + expect( + gtVersion( + '1.0.0-beta.2' as SemVerVersion, + '1.0.0-beta.1' as SemVerVersion, + ), + ).toBe(true); + + expect( + gtVersion('1.0.0-beta.2' as SemVerVersion, '1.2.3' as SemVerVersion), + ).toBe(false); + + expect( + gtVersion('1.0.0' as SemVerVersion, '1.0.0-beta.2' as SemVerVersion), + ).toBe(true); + + expect( + gtVersion('1.2.3-beta.1' as SemVerVersion, '1.0.0' as SemVerVersion), + ).toBe(true); + + expect( + gtVersion( + '1.2.3-beta.1' as SemVerVersion, + '1.2.3-alpha.2' as SemVerVersion, + ), + ).toBe(true); }); }); describe('getTargetVersion', () => { it('supports *', () => { - expect(getTargetVersion(v(['1.2.3', '3.0.0']), r('*'))).toBe('3.0.0'); + expect( + getTargetVersion( + ['1.2.3', '3.0.0'] as SemVerVersion[], + '*' as SemVerRange, + ), + ).toBe('3.0.0'); }); it('supports pre-release versions', () => { - expect(getTargetVersion(v(['1.0.0-beta.1']), r('*'))).toBe('1.0.0-beta.1'); - expect(getTargetVersion(v(['1.0.0-beta.1', '1.0.0-beta.2']), r('*'))).toBe( - '1.0.0-beta.2', - ); + expect( + getTargetVersion(['1.0.0-beta.1'] as SemVerVersion[], '*' as SemVerRange), + ).toBe('1.0.0-beta.1'); expect( - getTargetVersion(v(['1.0.0-beta.1', '1.0.0-beta.2']), r('^1.0.0-beta.1')), + getTargetVersion( + ['1.0.0-beta.1', '1.0.0-beta.2'] as SemVerVersion[], + '*' as SemVerRange, + ), ).toBe('1.0.0-beta.2'); - expect(getTargetVersion(v(['1.0.0-alpha.2', '1.0.0-beta.1']), r('*'))).toBe( - '1.0.0-beta.1', - ); + expect( + getTargetVersion( + ['1.0.0-beta.1', '1.0.0-beta.2'] as SemVerVersion[], + '^1.0.0-beta.1' as SemVerRange, + ), + ).toBe('1.0.0-beta.2'); - expect(getTargetVersion(v(['0.9.0', '1.0.0-alpha.0']), r('*'))).toBe( - '0.9.0', - ); + expect( + getTargetVersion( + ['1.0.0-alpha.2', '1.0.0-beta.1'] as SemVerVersion[], + '*' as SemVerRange, + ), + ).toBe('1.0.0-beta.1'); + + expect( + getTargetVersion( + ['0.9.0', '1.0.0-alpha.0'] as SemVerVersion[], + '*' as SemVerRange, + ), + ).toBe('0.9.0'); }); it("doesn't return pre-release versions by default", () => { expect( - getTargetVersion(v(['1.0.0-beta.1', '1.0.0', '1.2.3']), r('*')), + getTargetVersion( + ['1.0.0-beta.1', '1.0.0', '1.2.3'] as SemVerVersion[], + '*' as SemVerRange, + ), ).toBe('1.2.3'); expect( - getTargetVersion(v(['1.0.0-beta.1', '1.0.0', '1.2.3']), r('^1.0.0')), + getTargetVersion( + ['1.0.0-beta.1', '1.0.0', '1.2.3'] as SemVerVersion[], + '^1.0.0' as SemVerRange, + ), ).toBe('1.2.3'); - expect(getTargetVersion(v(['1.0.0-beta.1', '1.0.0']), r('*'))).toBe( - '1.0.0', - ); + expect( + getTargetVersion( + ['1.0.0-beta.1', '1.0.0'] as SemVerVersion[], + '*' as SemVerRange, + ), + ).toBe('1.0.0'); }); }); describe('satisfiesVersionRange', () => { it('supports *', () => { - expect(satisfiesVersionRange(v('3.0.0'), r('*'))).toBe(true); + expect( + satisfiesVersionRange('3.0.0' as SemVerVersion, '*' as SemVerRange), + ).toBe(true); }); it('supports exact versions', () => { - expect(satisfiesVersionRange(v('1.0.0-beta.1'), r('1.0.0-beta.1'))).toBe( - true, - ); - expect(satisfiesVersionRange(v('1.0.0'), r('1.0.0'))).toBe(true); - expect(satisfiesVersionRange(v('1.2.3'), r('1.0.0'))).toBe(false); + expect( + satisfiesVersionRange( + '1.0.0-beta.1' as SemVerVersion, + '1.0.0-beta.1' as SemVerRange, + ), + ).toBe(true); + + expect( + satisfiesVersionRange('1.0.0' as SemVerVersion, '1.0.0' as SemVerRange), + ).toBe(true); + + expect( + satisfiesVersionRange('1.2.3' as SemVerVersion, '1.0.0' as SemVerRange), + ).toBe(false); }); it('supports non-exact version ranges', () => { - expect(satisfiesVersionRange(v('1.2.3'), r('^1.0.0'))).toBe(true); - expect(satisfiesVersionRange(v('2.0.0'), r('^1.0.0'))).toBe(false); + expect( + satisfiesVersionRange('1.2.3' as SemVerVersion, '^1.0.0' as SemVerRange), + ).toBe(true); + + expect( + satisfiesVersionRange('2.0.0' as SemVerVersion, '^1.0.0' as SemVerRange), + ).toBe(false); }); it('pre-releases can satisfy version range', () => { - expect(satisfiesVersionRange(v('1.0.0-beta.1'), r('*'))).toBe(true); - expect(satisfiesVersionRange(v('1.0.0-beta.1'), r('^1.0.0'))).toBe(false); + expect( + satisfiesVersionRange( + '1.0.0-beta.1' as SemVerVersion, + '*' as SemVerRange, + ), + ).toBe(true); + + expect( + satisfiesVersionRange( + '1.0.0-beta.1' as SemVerVersion, + '^1.0.0' as SemVerRange, + ), + ).toBe(false); }); }); From f969ada6394461971c0035d8376c01be27e4a159 Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Tue, 15 Nov 2022 17:17:39 +0100 Subject: [PATCH 05/13] Used ts-opaque --- packages/snaps-utils/package.json | 3 ++- packages/snaps-utils/src/versions.ts | 12 +++++++++--- yarn.lock | 8 ++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/snaps-utils/package.json b/packages/snaps-utils/package.json index fefe857cf7..22f92bf0d6 100644 --- a/packages/snaps-utils/package.json +++ b/packages/snaps-utils/package.json @@ -62,7 +62,8 @@ "rfdc": "^1.3.0", "semver": "^7.3.7", "ses": "^0.17.0", - "superstruct": "^0.16.7" + "superstruct": "^0.16.7", + "ts-opaque": "^3.0.1" }, "devDependencies": { "@lavamoat/allow-scripts": "^2.0.3", diff --git a/packages/snaps-utils/src/versions.ts b/packages/snaps-utils/src/versions.ts index 488c4e5420..5ba122fcf3 100644 --- a/packages/snaps-utils/src/versions.ts +++ b/packages/snaps-utils/src/versions.ts @@ -7,6 +7,7 @@ import { validRange as validSemVerRange, } from 'semver'; import { is, refine, string, Struct, validate } from 'superstruct'; +import Opaque from 'ts-opaque'; export const DEFAULT_REQUESTED_SNAP_VERSION = '*' as SemVerRange; @@ -29,9 +30,12 @@ export const DEFAULT_REQUESTED_SNAP_VERSION = '*' as SemVerRange; * @see {@link assertIsSemVerRange} * @see {@link isValidSemVerRange} */ +/* declare type SemVerRange = { __TYPE: 'semver_range'; -} & string; +} & string;*/ +export type SemVerRange = Opaque; +const semVerRange = Symbol('Opaque branding for SemVerRange type'); /** * {@link https://codemix.com/opaque-types-in-javascript/ Opaque} type for singular SemVer version. @@ -52,10 +56,12 @@ declare type SemVerRange = { * @see {@link assertIsSemVerVersion} * @see {@link isValidSemVerVersion} */ +/* declare type SemVerVersion = { __TYPE: 'semver_version'; -} & string; -export { SemVerRange, SemVerVersion }; +} & string;*/ +export type SemVerVersion = Opaque; +const semVerVersion = Symbol('Opaque branding for SemVerVersion type'); /** * A struct for validating a version string. diff --git a/yarn.lock b/yarn.lock index cf2aa0857e..a74494a5f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3281,6 +3281,7 @@ __metadata: ses: ^0.17.0 superstruct: ^0.16.7 ts-jest: ^29.0.0 + ts-opaque: ^3.0.1 typescript: ~4.8.4 languageName: unknown linkType: soft @@ -16256,6 +16257,13 @@ __metadata: languageName: node linkType: hard +"ts-opaque@npm:^3.0.1": + version: 3.0.1 + resolution: "ts-opaque@npm:3.0.1" + checksum: 146f71d08d5915ead4551dce310719c53ead762a3ca6219373beacf273c84a7988b7716e5f8b9d0e9013d87ed84bc9f1e794ecdc4ff14083723ceacd9899040c + languageName: node + linkType: hard + "tsc-watch@npm:^4.5.0": version: 4.5.0 resolution: "tsc-watch@npm:4.5.0" From 1207f4d0fda6ffe7acba4d79f5609bfd860381f2 Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Tue, 15 Nov 2022 17:30:16 +0100 Subject: [PATCH 06/13] Removed debug comments --- packages/snaps-utils/src/versions.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/snaps-utils/src/versions.ts b/packages/snaps-utils/src/versions.ts index 5ba122fcf3..1de68d72c9 100644 --- a/packages/snaps-utils/src/versions.ts +++ b/packages/snaps-utils/src/versions.ts @@ -30,10 +30,6 @@ export const DEFAULT_REQUESTED_SNAP_VERSION = '*' as SemVerRange; * @see {@link assertIsSemVerRange} * @see {@link isValidSemVerRange} */ -/* -declare type SemVerRange = { - __TYPE: 'semver_range'; -} & string;*/ export type SemVerRange = Opaque; const semVerRange = Symbol('Opaque branding for SemVerRange type'); @@ -56,10 +52,6 @@ const semVerRange = Symbol('Opaque branding for SemVerRange type'); * @see {@link assertIsSemVerVersion} * @see {@link isValidSemVerVersion} */ -/* -declare type SemVerVersion = { - __TYPE: 'semver_version'; -} & string;*/ export type SemVerVersion = Opaque; const semVerVersion = Symbol('Opaque branding for SemVerVersion type'); From b8d3f94873716700b26600de507ad829995c320c Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Tue, 15 Nov 2022 18:35:11 +0100 Subject: [PATCH 07/13] Coverage --- packages/snaps-utils/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-utils/jest.config.js b/packages/snaps-utils/jest.config.js index 1b4f207447..3e571aa81e 100644 --- a/packages/snaps-utils/jest.config.js +++ b/packages/snaps-utils/jest.config.js @@ -15,7 +15,7 @@ module.exports = deepmerge(baseConfig, { coverageThreshold: { global: { branches: 93.45, - functions: 95.18, + functions: 95.45, lines: 98.07, statements: 98.07, }, From 08b53e567bf02b00bc1b383c5e5a7d512c0f9a76 Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Wed, 16 Nov 2022 17:17:32 +0100 Subject: [PATCH 08/13] PR review --- .../src/cronjob/CronjobController.test.ts | 8 +++++--- .../src/snaps/SnapController.test.ts | 19 ++++--------------- packages/snaps-utils/src/test-utils/common.ts | 12 ++++++++++++ .../snaps-utils/src/test-utils/manifest.ts | 10 +++------- packages/snaps-utils/src/test-utils/snap.ts | 13 ++++--------- 5 files changed, 28 insertions(+), 34 deletions(-) create mode 100644 packages/snaps-utils/src/test-utils/common.ts diff --git a/packages/snaps-controllers/src/cronjob/CronjobController.test.ts b/packages/snaps-controllers/src/cronjob/CronjobController.test.ts index 8b50c9ffb5..40410254df 100644 --- a/packages/snaps-controllers/src/cronjob/CronjobController.test.ts +++ b/packages/snaps-controllers/src/cronjob/CronjobController.test.ts @@ -78,6 +78,8 @@ const MOCK_CRONJOB_SINGLE_JOB_PERMISSION = { parentCapability: SnapEndowments.Cronjob, }; +const MOCK_VERSION = '1.0' as SemVerVersion; + describe('CronjobController', () => { beforeEach(() => { jest.useFakeTimers().setSystemTime(new Date('2022-01-01')); @@ -316,7 +318,7 @@ describe('CronjobController', () => { id: MOCK_SNAP_ID, initialPermissions: {}, permissionName: '', - version: '1.0' as SemVerVersion, + version: MOCK_VERSION, }; // @ts-expect-error Accessing private property cronjobController._handleEventSnapInstalled(snapInfo); @@ -368,7 +370,7 @@ describe('CronjobController', () => { id: MOCK_SNAP_ID, initialPermissions: {}, permissionName: '', - version: '1.0' as SemVerVersion, + version: MOCK_VERSION, }; // @ts-expect-error Accessing private property @@ -433,7 +435,7 @@ describe('CronjobController', () => { id: MOCK_SNAP_ID, initialPermissions: {}, permissionName: '', - version: '1.0' as SemVerVersion, + version: MOCK_VERSION, }; // @ts-expect-error Accessing private property diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.ts b/packages/snaps-controllers/src/snaps/SnapController.test.ts index 2ca937df96..6cdfc9f4b7 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.test.ts @@ -31,7 +31,7 @@ import fetchMock from 'jest-fetch-mock'; import { createAsyncMiddleware, JsonRpcEngine } from 'json-rpc-engine'; import { createEngineStream } from 'json-rpc-middleware-stream'; import pump from 'pump'; -import { Duplex } from 'stream';; +import { Duplex } from 'stream'; import { NodeThreadExecutionService, setupMultiplex } from '../services'; import { ExecutionEnvironmentStub, @@ -3143,13 +3143,7 @@ describe('SnapController', () => { }); /* eslint-enable @typescript-eslint/naming-convention */ -<<<<<<< HEAD callActionSpy.mockImplementation((method, ...args: unknown[]): any => { -======= - callActionSpy.mockImplementation((method, ...params) => { - assert(params.length > 0); - const request: any = params[0]; ->>>>>>> c8c60447 (Added opaque types for SemVers) if (method === 'PermissionController:hasPermission') { return true; } else if (method === 'ApprovalController:addRequest') { @@ -3955,10 +3949,9 @@ describe('SnapController', () => { ) { return true; } -<<<<<<< HEAD -======= - ->>>>>>> c8c60447 (Added opaque types for SemVers) + // TypeScript doesn't allow variadic list of arguments into non-variadic functions. + // messenger.call is a union of functions that take a constant amount of arguments, and + // so, TypeScript errors. return (originalCall as any)(method, ...args); }); @@ -3989,10 +3982,6 @@ describe('SnapController', () => { }, }; } -<<<<<<< HEAD -======= - ->>>>>>> c8c60447 (Added opaque types for SemVers) return (originalCall as any)(method, ...args); }); diff --git a/packages/snaps-utils/src/test-utils/common.ts b/packages/snaps-utils/src/test-utils/common.ts new file mode 100644 index 0000000000..a15d6c88f5 --- /dev/null +++ b/packages/snaps-utils/src/test-utils/common.ts @@ -0,0 +1,12 @@ +import { SemVerVersion } from '../versions'; + +/** + * Tens/hundreds legacy tests use creation utils. + * + * Updating them to use proper type casting is unfeasible at this time. + * We use this function to make creation utils backwards compatible, + * until we're ready to update tests. + */ +export type MakeSemVer = { + [K in keyof T]: K extends 'version' ? SemVerVersion | string : T[K]; +}; diff --git a/packages/snaps-utils/src/test-utils/manifest.ts b/packages/snaps-utils/src/test-utils/manifest.ts index 5ccce7ff76..02f50a05e2 100644 --- a/packages/snaps-utils/src/test-utils/manifest.ts +++ b/packages/snaps-utils/src/test-utils/manifest.ts @@ -8,11 +8,9 @@ import { } from '../namespace'; import { NpmSnapPackageJson } from '../types'; import { DEFAULT_SNAP_SHASUM } from './snap'; +import { MakeSemVer } from './common'; -type GetSnapManifestOptions = Partial< - Omit -> & { - version?: SemVerVersion | string; +type GetSnapManifestOptions = Partial> & { shasum?: string; filePath?: string; packageName?: string; @@ -20,9 +18,7 @@ type GetSnapManifestOptions = Partial< iconPath?: string; }; -type GetPackageJsonOptions = Partial> & { - version?: SemVerVersion | string; -}; +type GetPackageJsonOptions = Partial>; /** * Get the default package repository, in a format compatible with diff --git a/packages/snaps-utils/src/test-utils/snap.ts b/packages/snaps-utils/src/test-utils/snap.ts index 7b1cf00858..57e36081da 100644 --- a/packages/snaps-utils/src/test-utils/snap.ts +++ b/packages/snaps-utils/src/test-utils/snap.ts @@ -7,6 +7,7 @@ import { TruncatedSnap, } from '../snaps'; import { getSnapManifest } from './manifest'; +import { MakeSemVer } from './common'; /** * A mock snap source and its shasum. @@ -28,15 +29,9 @@ export const MOCK_SNAP_ID = 'npm:@metamask/example-snap'; export const MOCK_LOCAL_SNAP_ID = 'local:@metamask/example-snap'; export const MOCK_ORIGIN = 'example.com'; -type GetPersistedSnapObjectOptions = Partial> & { - version?: string | SemVerVersion; -}; -type GetSnapObjectOptions = Partial> & { - version?: string | SemVerVersion; -}; -type GetTruncatedSnapOptions = Partial> & { - version?: string | SemVerVersion; -}; +type GetPersistedSnapObjectOptions = Partial>; +type GetSnapObjectOptions = Partial>; +type GetTruncatedSnapOptions = Partial>; export const getPersistedSnapObject = ({ blocked = false, From 1ab518a2daec22656488486d37288a51955e8636 Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Fri, 18 Nov 2022 14:33:30 +0100 Subject: [PATCH 09/13] Coverage --- packages/snaps-utils/jest.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/snaps-utils/jest.config.js b/packages/snaps-utils/jest.config.js index 3e571aa81e..a84cb899d8 100644 --- a/packages/snaps-utils/jest.config.js +++ b/packages/snaps-utils/jest.config.js @@ -16,8 +16,8 @@ module.exports = deepmerge(baseConfig, { global: { branches: 93.45, functions: 95.45, - lines: 98.07, - statements: 98.07, + lines: 98.34, + statements: 98.34, }, }, testTimeout: 2500, From eb04a9b0a7a176ba69be3b0017d670299d420dc5 Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Fri, 18 Nov 2022 14:40:01 +0100 Subject: [PATCH 10/13] Lint --- .../snaps-controllers/src/snaps/SnapController.test.ts | 1 + packages/snaps-controllers/src/snaps/SnapController.ts | 4 ++-- packages/snaps-controllers/src/snaps/utils/npm.ts | 6 +++--- packages/snaps-utils/src/manifest/validation.ts | 2 +- packages/snaps-utils/src/test-utils/manifest.ts | 4 ++-- packages/snaps-utils/src/test-utils/snap.ts | 4 ++-- packages/snaps-utils/src/versions.test.ts | 7 ++++--- 7 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.ts b/packages/snaps-controllers/src/snaps/SnapController.test.ts index 6cdfc9f4b7..e9f872a490 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.test.ts @@ -32,6 +32,7 @@ import { createAsyncMiddleware, JsonRpcEngine } from 'json-rpc-engine'; import { createEngineStream } from 'json-rpc-middleware-stream'; import pump from 'pump'; import { Duplex } from 'stream'; + import { NodeThreadExecutionService, setupMultiplex } from '../services'; import { ExecutionEnvironmentStub, diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index c7fce0de55..b259c91440 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -1536,8 +1536,8 @@ export class SnapController extends BaseController< await Promise.all( Object.entries(requestedSnaps).map( async ([snapId, { version: rawVersion }]) => { - const [err, version] = resolveVersionRange(rawVersion); - if (err) { + const [error, version] = resolveVersionRange(rawVersion); + if (error) { result[snapId] = { error: ethErrors.rpc.invalidParams( `The "version" field must be a valid SemVer version range if specified. Received: "${version}".`, diff --git a/packages/snaps-controllers/src/snaps/utils/npm.ts b/packages/snaps-controllers/src/snaps/utils/npm.ts index ac2433ec1a..510e14a959 100644 --- a/packages/snaps-controllers/src/snaps/utils/npm.ts +++ b/packages/snaps-controllers/src/snaps/utils/npm.ts @@ -93,9 +93,9 @@ async function fetchNpmTarball( } const versions = Object.keys((packageMetadata as any)?.versions ?? {}).map( - (v) => { - assertIsSemVerVersion(v); - return v; + (version) => { + assertIsSemVerVersion(version); + return version; }, ); diff --git a/packages/snaps-utils/src/manifest/validation.ts b/packages/snaps-utils/src/manifest/validation.ts index 4c67ee91f9..ebfe4e8c92 100644 --- a/packages/snaps-utils/src/manifest/validation.ts +++ b/packages/snaps-utils/src/manifest/validation.ts @@ -20,8 +20,8 @@ import { import { CronjobSpecificationArrayStruct } from '../cronjob'; import { NamespacesStruct } from '../namespace'; -import { VersionStruct } from '../versions'; import { NameStruct, NpmSnapFileNames } from '../types'; +import { VersionStruct } from '../versions'; // 0xd36e6170 - 0x80000000 export const SIP_6_MAGIC_VALUE = `1399742832'` as `${number}'`; diff --git a/packages/snaps-utils/src/test-utils/manifest.ts b/packages/snaps-utils/src/test-utils/manifest.ts index 02f50a05e2..d045780d47 100644 --- a/packages/snaps-utils/src/test-utils/manifest.ts +++ b/packages/snaps-utils/src/test-utils/manifest.ts @@ -1,4 +1,3 @@ -import { SemVerVersion } from '../versions'; import { SnapManifest } from '../manifest/validation'; import { Chain, @@ -7,8 +6,9 @@ import { SessionNamespace, } from '../namespace'; import { NpmSnapPackageJson } from '../types'; -import { DEFAULT_SNAP_SHASUM } from './snap'; +import { SemVerVersion } from '../versions'; import { MakeSemVer } from './common'; +import { DEFAULT_SNAP_SHASUM } from './snap'; type GetSnapManifestOptions = Partial> & { shasum?: string; diff --git a/packages/snaps-utils/src/test-utils/snap.ts b/packages/snaps-utils/src/test-utils/snap.ts index 57e36081da..dcae80cca0 100644 --- a/packages/snaps-utils/src/test-utils/snap.ts +++ b/packages/snaps-utils/src/test-utils/snap.ts @@ -1,4 +1,3 @@ -import { SemVerVersion } from '../versions'; import { getSnapSourceShasum, PersistedSnap, @@ -6,8 +5,9 @@ import { SnapStatus, TruncatedSnap, } from '../snaps'; -import { getSnapManifest } from './manifest'; +import { SemVerVersion } from '../versions'; import { MakeSemVer } from './common'; +import { getSnapManifest } from './manifest'; /** * A mock snap source and its shasum. diff --git a/packages/snaps-utils/src/versions.test.ts b/packages/snaps-utils/src/versions.test.ts index 71554b38e5..7874a0e46b 100644 --- a/packages/snaps-utils/src/versions.test.ts +++ b/packages/snaps-utils/src/versions.test.ts @@ -1,4 +1,5 @@ import { assert } from '@metamask/utils'; + import { getSnapPrefix } from './snaps'; import { SnapIdPrefixes } from './types'; import { @@ -33,9 +34,9 @@ describe('resolveVersion', () => { }); it.each([null, 1, {}, Error])('returns error on invalid input', (value) => { - const [err, result] = resolveVersionRange(value); - assert(err !== undefined); - expect(err.message).toMatch('Expected a string, but received: '); + const [error, result] = resolveVersionRange(value); + assert(error !== undefined); + expect(error.message).toMatch('Expected a string, but received: '); expect(result).toBeUndefined(); }); }); From 338856d56f02fe389b24fc80f6ea99f71a3a848e Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Fri, 18 Nov 2022 14:55:58 +0100 Subject: [PATCH 11/13] Removed ts-opaque dependency --- packages/snaps-utils/src/types.ts | 5 +++++ packages/snaps-utils/src/versions.ts | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/snaps-utils/src/types.ts b/packages/snaps-utils/src/types.ts index 04ce7203a4..29020359b7 100644 --- a/packages/snaps-utils/src/types.ts +++ b/packages/snaps-utils/src/types.ts @@ -158,3 +158,8 @@ type KeyringParameters = KeyringParameter; export type SnapExportsParameters = | ObjectParameters | KeyringParameters; + +// We use a symbol property name instead of { _type: Brand }, because that would show up in IDE suggestions, +// while internal symbols do not. +declare const brand: unique symbol; +export type Opaque = Base & { [brand]: Brand }; diff --git a/packages/snaps-utils/src/versions.ts b/packages/snaps-utils/src/versions.ts index 1de68d72c9..c34bf64dc5 100644 --- a/packages/snaps-utils/src/versions.ts +++ b/packages/snaps-utils/src/versions.ts @@ -7,7 +7,8 @@ import { validRange as validSemVerRange, } from 'semver'; import { is, refine, string, Struct, validate } from 'superstruct'; -import Opaque from 'ts-opaque'; + +import { Opaque } from './types'; export const DEFAULT_REQUESTED_SNAP_VERSION = '*' as SemVerRange; @@ -31,7 +32,7 @@ export const DEFAULT_REQUESTED_SNAP_VERSION = '*' as SemVerRange; * @see {@link isValidSemVerRange} */ export type SemVerRange = Opaque; -const semVerRange = Symbol('Opaque branding for SemVerRange type'); +declare const semVerRange: unique symbol; /** * {@link https://codemix.com/opaque-types-in-javascript/ Opaque} type for singular SemVer version. @@ -53,7 +54,7 @@ const semVerRange = Symbol('Opaque branding for SemVerRange type'); * @see {@link isValidSemVerVersion} */ export type SemVerVersion = Opaque; -const semVerVersion = Symbol('Opaque branding for SemVerVersion type'); +declare const semVerVersion: unique symbol; /** * A struct for validating a version string. From b0c909bec87e4e9f4625478f2ec2d6e59f1716ad Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Fri, 18 Nov 2022 15:08:36 +0100 Subject: [PATCH 12/13] PR review --- packages/snaps-utils/package.json | 3 +-- packages/snaps-utils/src/versions.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/snaps-utils/package.json b/packages/snaps-utils/package.json index 22f92bf0d6..fefe857cf7 100644 --- a/packages/snaps-utils/package.json +++ b/packages/snaps-utils/package.json @@ -62,8 +62,7 @@ "rfdc": "^1.3.0", "semver": "^7.3.7", "ses": "^0.17.0", - "superstruct": "^0.16.7", - "ts-opaque": "^3.0.1" + "superstruct": "^0.16.7" }, "devDependencies": { "@lavamoat/allow-scripts": "^2.0.3", diff --git a/packages/snaps-utils/src/versions.ts b/packages/snaps-utils/src/versions.ts index c34bf64dc5..8fcd36553a 100644 --- a/packages/snaps-utils/src/versions.ts +++ b/packages/snaps-utils/src/versions.ts @@ -196,7 +196,7 @@ export function getTargetVersion( */ export function resolveVersionRange( version?: Json, -): [undefined, SemVerRange] | [Error, undefined] { +): [error: undefined, range: SemVerRange] | [error: Error, range: undefined] { if (version === undefined || version === 'latest') { return [undefined, DEFAULT_REQUESTED_SNAP_VERSION]; } From 07d06501367593dbaac0157733dd79c523d3498c Mon Sep 17 00:00:00 2001 From: Olaf Tomalka Date: Fri, 18 Nov 2022 15:12:03 +0100 Subject: [PATCH 13/13] Yarn.lock --- yarn.lock | 8 -------- 1 file changed, 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index a74494a5f5..cf2aa0857e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3281,7 +3281,6 @@ __metadata: ses: ^0.17.0 superstruct: ^0.16.7 ts-jest: ^29.0.0 - ts-opaque: ^3.0.1 typescript: ~4.8.4 languageName: unknown linkType: soft @@ -16257,13 +16256,6 @@ __metadata: languageName: node linkType: hard -"ts-opaque@npm:^3.0.1": - version: 3.0.1 - resolution: "ts-opaque@npm:3.0.1" - checksum: 146f71d08d5915ead4551dce310719c53ead762a3ca6219373beacf273c84a7988b7716e5f8b9d0e9013d87ed84bc9f1e794ecdc4ff14083723ceacd9899040c - languageName: node - linkType: hard - "tsc-watch@npm:^4.5.0": version: 4.5.0 resolution: "tsc-watch@npm:4.5.0"