Ledger types (#1690)
* Do not truncate errors, pretty output * Introduce helpers for sagas * Update yarn lock * Initial types * Finish types * cleanup * Fix imports and filenames to cooperate with internal typings
This commit is contained in:
parent
7e605c4058
commit
c763b2ac98
|
@ -0,0 +1,56 @@
|
|||
declare module '@ledgerhq/hw-app-eth' {
|
||||
import LedgerTransport from '@ledgerhq/hw-transport';
|
||||
|
||||
export default class Eth<T extends LedgerTransport<any>> {
|
||||
constructor(transport: T);
|
||||
|
||||
/**
|
||||
*
|
||||
* @description get Ethereum address for a given BIP 32 path.
|
||||
* @param {string} path a path in BIP 32 format
|
||||
* @param {boolean} [boolDisplay] enable or not the display
|
||||
* @param {boolean} [boolChaincode] enable or not the chaincode request
|
||||
* @returns {Promise<{ publicKey: string; address: string; chainCode?: string }>}
|
||||
* @memberof Eth
|
||||
*/
|
||||
public getAddress(
|
||||
path: string,
|
||||
boolDisplay?: boolean,
|
||||
boolChaincode?: boolean
|
||||
): Promise<{ publicKey: string; address: string; chainCode?: string }>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description signs a raw transaction and returns v,r,s
|
||||
* @param {string} path
|
||||
* @param {string} rawTxHex
|
||||
* @returns {Promise<{s: string, v: string, r: string}>}
|
||||
* @memberof Eth
|
||||
*/
|
||||
public signTransaction(
|
||||
path: string,
|
||||
rawTxHex: string
|
||||
): Promise<{ s: string; v: string; r: string }>;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @returns {Promise<{ arbitraryDataEnabled: number; version: string }>}
|
||||
* @memberof Eth
|
||||
*/
|
||||
public getAppConfiguration(): Promise<{ arbitraryDataEnabled: number; version: string }>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description sign a message according to eth_sign RPC call
|
||||
* @param {string} path
|
||||
* @param {string} messageHex
|
||||
* @returns {Promise<{v: number, s: string, r: string}>}
|
||||
* @memberof Eth
|
||||
*/
|
||||
public signPersonalMessage(
|
||||
path: string,
|
||||
messageHex: string
|
||||
): Promise<{ v: number; s: string; r: string }>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
declare module '@ledgerhq/hw-transport-node-hid' {
|
||||
import LedgerTransport, { Observer, DescriptorEvent, Subscription } from '@ledgerhq/hw-transport';
|
||||
import { HID, Device } from 'node-hid';
|
||||
|
||||
export default class TransportNodeHid extends LedgerTransport<string> {
|
||||
/**
|
||||
* @description Creates an instance of TransportNodeHid.
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* import TransportNodeHid from "@ledgerhq/hw-transport-node-u2f";
|
||||
* TransportNodeHid.create().then(transport => ...);
|
||||
* ```
|
||||
*
|
||||
* @param {HID} device
|
||||
* @param {boolean} [ledgerTransport]
|
||||
* @param {number} [timeout]
|
||||
* @param {boolean} [debug]
|
||||
* @memberof TransportNodeHid
|
||||
*/
|
||||
constructor(device: HID, ledgerTransport?: boolean, timeout?: number, debug?: boolean);
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Check if an HID instance is active
|
||||
* @static
|
||||
* @returns {Promise<boolean>}
|
||||
* @memberof TransportNodeHid
|
||||
*/
|
||||
public static isSupported(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Lists all available HID device's paths
|
||||
* @static
|
||||
* @returns {Promise<string[]>}
|
||||
* @memberof TransportNodeHid
|
||||
*/
|
||||
public static list<Descriptor = string>(): Promise<Descriptor[]>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Listen all device events for a given Transport.
|
||||
* The method takes an Observer of DescriptorEvent and returns a Subscription
|
||||
* according to Observable paradigm https://github.com/tc39/proposal-observable
|
||||
* a DescriptorEvent is a { descriptor, type } object.
|
||||
* Type can be "add" or "remove" and descriptor is a value you can pass to open(descriptor).
|
||||
* Each listen() call will first emit all potential device already connected and then will emit events can come over times,
|
||||
* for instance if you plug a USB device after listen().
|
||||
* @static
|
||||
* @template Descriptor
|
||||
* @template Device
|
||||
* @template Err
|
||||
* @param {Observer<DescriptorEvent<Descriptor, Device>, Err>} observer
|
||||
* @returns {Subscription}
|
||||
* @memberof TransportNodeHid
|
||||
*/
|
||||
public static listen<Descriptor = string, Device = HID, Err = void>(
|
||||
observer: Observer<DescriptorEvent<Descriptor, Device>, Err>
|
||||
): Subscription;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Attempt to create a Transport instance with a descriptor.
|
||||
* @static
|
||||
* @template Descriptor
|
||||
* @param {Descriptor} devicePath
|
||||
* @returns {Promise<TransportNodeHid>}
|
||||
* @memberof TransportNodeHid
|
||||
*/
|
||||
public static open<Descriptor = string>(devicePath: Descriptor): Promise<TransportNodeHid>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Low level api to communicate with the device.
|
||||
* @param {Buffer} apdu
|
||||
* @returns {Promise<Buffer>}
|
||||
* @memberof TransportNodeHid
|
||||
*/
|
||||
public exchange(apdu: Buffer): Promise<Buffer>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Does nothing
|
||||
* @memberof TransportNodeHid
|
||||
*/
|
||||
public setScrambleKey(): void;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Close the exchange with the device.
|
||||
* @returns {Promise<void>}
|
||||
* @memberof TransportNodeHid
|
||||
*/
|
||||
public close(): Promise<void>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
declare module '@ledgerhq/hw-transport-u2f' {
|
||||
import LedgerTransport, {
|
||||
Observer,
|
||||
DescriptorEvent,
|
||||
Subscription,
|
||||
TransportError
|
||||
} from '@ledgerhq/hw-transport';
|
||||
import { isSupported, sign } from 'u2f-api';
|
||||
|
||||
export default class TransportU2F extends LedgerTransport<null> {
|
||||
public static isSupported: typeof isSupported;
|
||||
|
||||
/**
|
||||
* @description this transport is not discoverable but we are going to guess if it is here with isSupported()
|
||||
* @static
|
||||
* @template Descriptor An array with [null] if supported device
|
||||
* @returns {Descriptor}
|
||||
* @memberof TransportU2F
|
||||
*/
|
||||
public static list<Descriptor = [null] | never[]>(): Descriptor;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Listen all device events for a given Transport.
|
||||
* The method takes an Observer of DescriptorEvent and returns a Subscription
|
||||
* according to Observable paradigm https://github.com/tc39/proposal-observable
|
||||
* a DescriptorEvent is a { descriptor, type } object.
|
||||
* Type can be "add" or "remove" and descriptor is a value you can pass to open(descriptor).
|
||||
* Each listen() call will first emit all potential device already connected and then will emit events can come over times,
|
||||
* @static
|
||||
* @template Descriptor
|
||||
* @template Device
|
||||
* @template Err
|
||||
* @param {Observer<
|
||||
* DescriptorEvent<Device, Descriptor>,
|
||||
* ErrParam
|
||||
* >} observer
|
||||
* @returns {Subscription}
|
||||
* @memberof TransportU2F
|
||||
*/
|
||||
public static listen<Descriptor = undefined, Device = null, Err = TransportError>(
|
||||
observer: Observer<DescriptorEvent<Device, Descriptor>, Err>
|
||||
): Subscription;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description static function to create a new Transport from a connected
|
||||
* Ledger device discoverable via U2F (browser support)
|
||||
*
|
||||
* @static
|
||||
* @param {*} _
|
||||
* @param {number} [_openTimeout]
|
||||
* @returns {Promise<TransportU2F>}
|
||||
* @memberof TransportU2F
|
||||
*/
|
||||
public static open(_?: any, __?: number): Promise<TransportU2F>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Low level api to communicate with the device.
|
||||
* @param {Buffer} adpu
|
||||
* @returns {Promise<Buffer>}
|
||||
* @memberof TransportU2F
|
||||
*/
|
||||
public exchange(adpu: Buffer): Promise<Buffer>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Set the "scramble key" for the next exchange with the device.
|
||||
* Each App can have a different scramble key and they internally will set it at instantiation.
|
||||
* @param {string} scrambleKey
|
||||
* @memberof TransportU2F
|
||||
*/
|
||||
public setScrambleKey(scrambleKey: string): void;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Close the exchange with the device.
|
||||
* @returns {Promise<void>}
|
||||
* @memberof TransportU2F
|
||||
*/
|
||||
public close(): Promise<void>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
declare module '@ledgerhq/hw-transport' {
|
||||
/**
|
||||
* @description all possible status codes.
|
||||
* @see https://github.com/LedgerHQ/blue-app-btc/blob/d8a03d10f77ca5ef8b22a5d062678eef788b824a/include/btchip_apdu_constants.h#L85-L115
|
||||
* @example
|
||||
* import { StatusCodes } from "@ledgerhq/hw-transport";
|
||||
* @export
|
||||
* @enum {number}
|
||||
*/
|
||||
export enum StatusCodes {
|
||||
PIN_REMAINING_ATTEMPTS = 0x63c0,
|
||||
INCORRECT_LENGTH = 0x6700,
|
||||
COMMAND_INCOMPATIBLE_FILE_STRUCTURE = 0x6981,
|
||||
SECURITY_STATUS_NOT_SATISFIED = 0x6982,
|
||||
CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985,
|
||||
INCORRECT_DATA = 0x6a80,
|
||||
NOT_ENOUGH_MEMORY_SPACE = 0x6a84,
|
||||
REFERENCED_DATA_NOT_FOUND = 0x6a88,
|
||||
FILE_ALREADY_EXISTS = 0x6a89,
|
||||
INCORRECT_P1_P2 = 0x6b00,
|
||||
INS_NOT_SUPPORTED = 0x6d00,
|
||||
CLA_NOT_SUPPORTED = 0x6e00,
|
||||
TECHNICAL_PROBLEM = 0x6f00,
|
||||
OK = 0x9000,
|
||||
MEMORY_PROBLEM = 0x9240,
|
||||
NO_EF_SELECTED = 0x9400,
|
||||
INVALID_OFFSET = 0x9402,
|
||||
FILE_NOT_FOUND = 0x9404,
|
||||
INCONSISTENT_FILE = 0x9408,
|
||||
ALGORITHM_NOT_SUPPORTED = 0x9484,
|
||||
INVALID_KCV = 0x9485,
|
||||
CODE_NOT_INITIALIZED = 0x9802,
|
||||
ACCESS_CONDITION_NOT_FULFILLED = 0x9804,
|
||||
CONTRADICTION_SECRET_CODE_STATUS = 0x9808,
|
||||
CONTRADICTION_INVALIDATION = 0x9810,
|
||||
CODE_BLOCKED = 0x9840,
|
||||
MAX_VALUE_REACHED = 0x9850,
|
||||
GP_AUTH_FAILED = 0x6300,
|
||||
LICENSING = 0x6f42,
|
||||
HALTED = 0x6faa
|
||||
}
|
||||
|
||||
export enum AltStatusCodes {
|
||||
'Incorrect length' = 0x6700,
|
||||
'Security not satisfied (dongle locked or have invalid access rights)' = 0x6982,
|
||||
'Condition of use not satisfied (denied by the user?)' = 0x6985,
|
||||
'Invalid data received' = 0x6a80,
|
||||
'Invalid parameter received' = 0x6b00,
|
||||
INTERNAL_ERROR = 'Internal error, please report'
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
unsubscribe: () => void;
|
||||
}
|
||||
|
||||
export interface ITransportError extends Error {
|
||||
name: 'TransportError';
|
||||
message: string;
|
||||
stack?: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TransportError is used for any generic transport errors.
|
||||
* e.g. Error thrown when data received by exchanges are incorrect or if exchanged failed to communicate with the device for various reason.
|
||||
*/
|
||||
export class TransportError extends Error {
|
||||
new(message: string, id: string): ITransportError;
|
||||
}
|
||||
|
||||
export interface ITransportStatusError extends Error {
|
||||
name: 'TransportStatusError';
|
||||
message: string;
|
||||
stack?: string;
|
||||
statusCode: number;
|
||||
statusText: keyof typeof StatusCodes | 'UNKNOWN_ERROR';
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when a device returned a non success status.
|
||||
* the error.statusCode is one of the `StatusCodes` exported by this library.
|
||||
*/
|
||||
export class TransportStatusError extends Error {
|
||||
new(statusCode: number): ITransportStatusError;
|
||||
}
|
||||
|
||||
export interface Observer<Ev, Err> {
|
||||
next: (event: Ev) => void;
|
||||
error: (e: Err) => void;
|
||||
complete: () => void;
|
||||
}
|
||||
|
||||
export interface DescriptorEvent<Descriptor, Device> {
|
||||
type: 'add' | 'remove';
|
||||
descriptor: Descriptor;
|
||||
device?: Device;
|
||||
}
|
||||
|
||||
export type FunctionPropertyNames<T> = {
|
||||
[K in keyof T]: T[K] extends Function ? K : never
|
||||
}[keyof T];
|
||||
|
||||
export type ExtractPromise<T> = T extends Promise<infer U> ? U : T;
|
||||
|
||||
export default abstract class LedgerTransport<Descriptor> {
|
||||
/**
|
||||
*
|
||||
* @description Check if a transport is supported on the user's platform/browser.
|
||||
* @static
|
||||
* @returns {Promise<boolean>}
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public static isSupported(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description List once all available descriptors. For a better granularity, checkout listen().
|
||||
* @static
|
||||
* @template Descriptor
|
||||
* @returns {Promise<Descriptor[]>}
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public static list<Descriptor>(): Promise<Descriptor[]>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Listen all device events for a given Transport.
|
||||
* The method takes an Observer of DescriptorEvent and returns a Subscription
|
||||
* according to Observable paradigm https://github.com/tc39/proposal-observable
|
||||
* a DescriptorEvent is a { descriptor, type } object.
|
||||
* Type can be "add" or "remove" and descriptor is a value you can pass to open(descriptor).
|
||||
* Each listen() call will first emit all potential device already connected and then will emit events can come over times,
|
||||
* for instance if you plug a USB device after listen() or a bluetooth device become discoverable.
|
||||
* @static
|
||||
* @template Descriptor
|
||||
* @template Device
|
||||
* @template Err
|
||||
* @param {Observer<DescriptorEvent<Descriptor, Device>, Err>} observer
|
||||
* @returns {Subscription}
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public static listen<Descriptor, Device = any, Err = any>(
|
||||
observer: Observer<DescriptorEvent<Descriptor, Device>, Err>
|
||||
): Subscription;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Attempt to create a Transport instance with potentially a descriptor.
|
||||
* @static
|
||||
* @template Descriptor
|
||||
* @param {Descriptor} descriptor
|
||||
* @param {number} [timeout]
|
||||
* @returns {Promise<LedgerTransport<Descriptor>>}
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public static open<Descriptor>(
|
||||
descriptor: Descriptor,
|
||||
timeout?: number
|
||||
): Promise<LedgerTransport<Descriptor>>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description create() attempts open the first descriptor available or throw if:
|
||||
* - there is no descriptor
|
||||
* - if either timeout is reached
|
||||
*
|
||||
* This is a light alternative to using listen() and open() that you may need for any advanced usecases
|
||||
* @static
|
||||
* @template Descriptor
|
||||
* @param {number} [openTimeout]
|
||||
* @param {number} [listenTimeout]
|
||||
* @returns {Promise<LedgerTransport<Descriptor>>}
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public static create<Descriptor>(
|
||||
openTimeout?: number,
|
||||
listenTimeout?: number
|
||||
): Promise<LedgerTransport<Descriptor>>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Low level api to communicate with the device.
|
||||
* This method is for implementations to implement but should not be directly called.
|
||||
* Instead, the recommended way is to use send() method
|
||||
* @param {Buffer} apdu
|
||||
* @returns {Promise<Buffer>}
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public abstract exchange(apdu: Buffer): Promise<Buffer>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Set the "scramble key" for the next exchange with the device.
|
||||
* Each App can have a different scramble key and they internally will set it at instantiation.
|
||||
* @param {string} scrambleKey
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public setScrambleKey(scrambleKey: string): void;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Close the exchange with the device.
|
||||
* @returns {Promise<void>}
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public close(): Promise<void>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Listen to an event on an instance of transport.
|
||||
* Transport implementation can have specific events. Here are the common events:
|
||||
* - "disconnect" : triggered if Transport is disconnected
|
||||
* @param {string} eventName
|
||||
* @param {Listener} cb
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public on(eventName: string | 'listen', cb: (...args: any[]) => any): void;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Stop listening to an event on an instance of transport.
|
||||
* @param {string} eventName
|
||||
* @param {Listener} cb
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public off(eventName: string, cb: (...args: any[]) => any): void;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Toggle logs of binary exchange
|
||||
* @param {boolean} debug
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public setDebugMode(debug: boolean): void;
|
||||
|
||||
/**
|
||||
* @description Set a timeout (in milliseconds) for the exchange call.
|
||||
* Only some transport might implement it. (e.g. U2F)
|
||||
* @param {number} exchangeTimeout
|
||||
* @memberof LedgerTransport
|
||||
*/
|
||||
public setExchangeTimeout?(exchangeTimeout: number): void;
|
||||
|
||||
/**
|
||||
* @description Used to decorate all callable public methods of an app so that they
|
||||
* are mutually exclusive. Scramble key is application specific, e.g hw-app-eth will set
|
||||
* its own scramblekey
|
||||
* @param self
|
||||
* @param methods
|
||||
* @param scrambleKey
|
||||
*/
|
||||
public decorateAppAPIMethods<T>(
|
||||
self: T,
|
||||
methods: FunctionPropertyNames<T>[],
|
||||
scrambleKey: string
|
||||
): void;
|
||||
|
||||
/**
|
||||
* @description Decorates a function so that it uses a global mutex, if an
|
||||
* exchange is already in process, then calling the function will throw an
|
||||
* error about being locked
|
||||
* @param methodName
|
||||
* @param functionToDecorate
|
||||
* @param thisContext
|
||||
* @param scrambleKey
|
||||
*/
|
||||
public decorateAppAPIMethod<T, FArgs = any, FRet = any>(
|
||||
methodName: FunctionPropertyNames<T>,
|
||||
functionToDecorate: (...args: FArgs[]) => FRet,
|
||||
thisContext: T,
|
||||
scrambleKey: string
|
||||
): (...args: FArgs[]) => Promise<ExtractPromise<FRet>>; // make sure we dont wrap promises twice
|
||||
}
|
||||
}
|
|
@ -64,10 +64,12 @@
|
|||
"@types/classnames": "2.2.3",
|
||||
"@types/enzyme": "3.1.8",
|
||||
"@types/enzyme-adapter-react-16": "1.0.1",
|
||||
"@types/events": "1.2.0",
|
||||
"@types/history": "4.6.2",
|
||||
"@types/jest": "22.2.3",
|
||||
"@types/lodash": "4.14.107",
|
||||
"@types/moment-timezone": "0.5.4",
|
||||
"@types/node-hid": "0.7.0",
|
||||
"@types/qrcode": "0.8.0",
|
||||
"@types/qrcode.react": "0.6.3",
|
||||
"@types/query-string": "5.1.0",
|
||||
|
@ -131,6 +133,7 @@
|
|||
"tslint-react": "3.5.1",
|
||||
"types-rlp": "0.0.1",
|
||||
"typescript": "2.8.1",
|
||||
"u2f-api": "1.0.6",
|
||||
"uglifyjs-webpack-plugin": "1.2.4",
|
||||
"url-search-params-polyfill": "3.0.0",
|
||||
"webapp-webpack-plugin": "2.0.1",
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -88,6 +88,10 @@
|
|||
"@types/cheerio" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/events@1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
|
||||
|
||||
"@types/history@*", "@types/history@4.6.2":
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
|
||||
|
@ -106,6 +110,10 @@
|
|||
dependencies:
|
||||
moment ">=2.14.0"
|
||||
|
||||
"@types/node-hid@0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-hid/-/node-hid-0.7.0.tgz#f696f39c528059116236e41df90c8fcba077d711"
|
||||
|
||||
"@types/node@*", "@types/node@^9.6.2":
|
||||
version "9.6.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.5.tgz#ee700810fdf49ac1c399fc5980b7559b3e5a381d"
|
||||
|
@ -11159,6 +11167,10 @@ u2f-api@0.2.7:
|
|||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/u2f-api/-/u2f-api-0.2.7.tgz#17bf196b242f6bf72353d9858e6a7566cc192720"
|
||||
|
||||
u2f-api@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/u2f-api/-/u2f-api-1.0.6.tgz#fdde2a0788fcf7d8d273aa8688b217625961f866"
|
||||
|
||||
ua-parser-js@^0.7.9:
|
||||
version "0.7.17"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
|
||||
|
|
Loading…
Reference in New Issue