UNPKG

virgil-sdk

Version:
2,047 lines 69.7 kB
'use strict';

var base64 = require('base-64');
var fetchPonyfill = require('fetch-ponyfill');
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');

/**
 * Decodes the base64 encoded string into a `string`.
 * @hidden
 * @param {string} input
 * @returns {string}
 */
function base64Decode(input) {
    return base64.decode(input);
}
/**
 * Encodes the `input` string into a base64 `string`.
 * @hidden
 * @param {string} input
 * @returns {string}
 */
function base64Encode(input) {
    return base64.encode(input);
}
/**
 * Converts regular base64 encoded string to URL-safe base64 encoded string.
 * @hidden
 * @param {string} input - Regular base64 encoded string.
 * @returns {string} - URL-safe base64 encoded string.
 */
function base64UrlFromBase64(input) {
    input = input.split('=')[0];
    input = input.replace(/\+/g, '-').replace(/\//g, '_');
    return input;
}
/**
 * Converts URL-safe base64 encoded string to regular base64 encoded string.
 * @hidden
 * @param {string} input - URL-safe base64 encoded string.
 * @returns {string} - Regular base64 encoded string.
 */
function base64UrlToBase64(input) {
    input = input.replace(/-/g, '+').replace(/_/g, '/');
    switch (input.length % 4) {
        case 0: break; // no padding needed
        case 2:
            input = input + '==';
            break;
        case 3:
            input = input + '=';
            break;
        default:
            throw new Error('Invalid base64 string');
    }
    return input;
}
/**
 * * Encodes the `input` string into a string using URL-safe base64 encoding.
 *
 * @hidden
 *
 * @param {string} input - The input.
 * @returns {string}
 */
function base64UrlEncode(input) {
    let output = base64Encode(input);
    return base64UrlFromBase64(output);
}
/**
 * Decodes the URL-safe base64-encoded `input` string into a `string`.
 *
 * @hidden
 *
 * @param {string} input
 * @returns {string}
 */
function base64UrlDecode(input) {
    const str = base64UrlToBase64(input);
    return base64Decode(str);
}

/**
 * Converts javascript date object or timestamp in milliseconds
 * to Unix timestamp.
 *
 * @hidden
 *
 * @param {Date | number} date - The date or timestamp to convert.
 * @returns {number}
 */
function getUnixTimestamp(date) {
    let time;
    if (typeof date === 'number') {
        time = date;
    }
    else {
        time = date.getTime();
    }
    return Math.floor(time / 1000);
}
/**
 * Adds the given number of seconds to the given date.
 *
 * @hidden
 *
 * @param {Date | number} date - The date to add seconds to.
 * If `date` is a `number` it is treated as a timestamp in milliseconds.
 * @param {number} seconds - The number of seconds to add.
 * @returns {Date} - The new date.
 */
function addSeconds(date, seconds) {
    if (typeof date === 'number') {
        return new Date(date + seconds * 1000);
    }
    return new Date(date.getTime() + seconds * 1000);
}

/**
 * JWT Subject.
 * @hidden
 */
const SubjectPrefix = "identity-";
/**
 * JWT Issuer.
 * @hidden
 */
const IssuerPrefix = "virgil-";
/**
 * Content type of the token. Used to convey structural information
 * about the JWT.
 * @hidden
 */
const VirgilContentType = "virgil-jwt;v=1";
/**
 * Media type of the JWT.
 * @hidden
 */
const JwtContentType = "JWT";

/**
 * Class representing the JWT providing access to the
 * Virgil Security APIs.
 * Implements {@link IAccessToken} interface.
 */
class Jwt {
    constructor(header, body, signature) {
        if (typeof header === 'string') {
            const stringRepresentation = header;
            const parts = stringRepresentation.split('.');
            if (parts.length !== 3)
                throw new Error('Wrong JWT format');
            try {
                this.header = JSON.parse(base64UrlDecode(parts[0]));
                this.body = JSON.parse(base64UrlDecode(parts[1]));
                this.signature = base64UrlToBase64(parts[2]);
            }
            catch (e) {
                throw new Error('Wrong JWT format');
            }
            this.unsignedData = parts[0] + '.' + parts[1];
            this.stringRepresentation = stringRepresentation;
        }
        else if (typeof header === 'object' && typeof body === 'object') {
            this.header = header;
            this.body = body;
            this.signature = signature;
            this.unsignedData = this.headerBase64() + '.' + this.bodyBase64();
            this.stringRepresentation = this.signature == null
                ? this.unsignedData
                : this.unsignedData + '.' + this.signatureBase64();
        }
        else {
            throw new TypeError('Invalid arguments for function Jwt. ' +
                'Expected a string representation of a token, or header and body as objects');
        }
    }
    /**
     * Parses the string representation of the JWT into
     * an object representation.
     *
     * @param {string} jwtStr - The JWT string. Must have the following format:
     *
     * `base64UrlEncode(Header) + "." + base64UrlEncode(Body) + "." + base64UrlEncode(Signature)`
     *
     * See the {@link https://jwt.io/introduction/ | Introduction to JWT} for more details.
     *
     * @returns {Jwt}
     */
    static fromString(jwtStr) {
        return new Jwt(jwtStr);
    }
    /**
     * Returns the string representation of this JWT.
     * @returns {string}
     */
    toString() {
        return this.stringRepresentation;
    }
    /**
     * Retrieves the identity that is the subject of this JWT.
     * @returns {string}
     */
    identity() {
        if (this.body.sub.indexOf(SubjectPrefix) !== 0) {
            throw new Error('wrong sub format');
        }
        return this.body.sub.substr(SubjectPrefix.length);
    }
    /**
     * Retrieves the application ID that is the issuer of this JWT.
     * @returns {string}
     */
    appId() {
        if (this.body.iss.indexOf(IssuerPrefix) !== 0) {
            throw new Error('wrong iss format');
        }
        return this.body.iss.substr(IssuerPrefix.length);
    }
    /**
     * Returns a boolean indicating whether this JWT is (or will be)
     * expired at the given date or not.
     *
     * @param {Date} at - The date to check. Defaults to `new Date()`.
     * @returns {boolean} - `true` if token is expired, otherwise `false`.
     */
    isExpired(at = new Date) {
        const now = getUnixTimestamp(at);
        return this.body.exp < now;
    }
    headerBase64() {
        return base64UrlEncode(JSON.stringify(this.header));
    }
    bodyBase64() {
        return base64UrlEncode(JSON.stringify(this.body));
    }
    signatureBase64() {
        return base64UrlFromBase64(this.signature);
    }
}

/**
 * Test if `condition` is truthy. If it is not, an `Error` is thrown with a
 * `message` property equal to `message` parameter.
 * @hidden
 * @param {boolean} condition
 * @param {string} message
 */
function assert(condition, message) {
    if (!condition) {
        throw new Error(message);
    }
}

const DEFAULT_TOKEN_TTL = 20 * 60 * 1000; // 20 minutes
/**
 * Class responsible for JWT generation.
 */
class JwtGenerator {
    constructor(options) {
        validateOptions$1(options);
        this.appId = options.appId;
        this.apiKey = options.apiKey;
        this.apiKeyId = options.apiKeyId;
        this.accessTokenSigner = options.accessTokenSigner;
        this.millisecondsToLive = options.millisecondsToLive !== undefined
            ? Number(options.millisecondsToLive)
            : DEFAULT_TOKEN_TTL;
    }
    /**
     * Generates a token with the given identity as the subject and optional
     * additional data.
     * @param {string} identity - Identity to be associated with JWT (i.e.
     * the Subject).
     * @param {IExtraData} ada - Additional data to be encoded in the JWT.
     * @returns {Jwt}
     */
    generateToken(identity, ada) {
        if (!identity) {
            throw new TypeError('Illegal arguments for function `generateToken`. Argument `identity` is required.');
        }
        const iat = getUnixTimestamp(new Date());
        const exp = getUnixTimestamp(new Date().getTime() + this.millisecondsToLive);
        const body = {
            iss: IssuerPrefix + this.appId,
            sub: SubjectPrefix + identity,
            iat,
            exp,
            ada
        };
        const header = {
            alg: this.accessTokenSigner.getAlgorithm(),
            kid: this.apiKeyId,
            typ: JwtContentType,
            cty: VirgilContentType
        };
        const unsignedJwt = new Jwt(header, body);
        const signature = this.accessTokenSigner.generateTokenSignature({ value: unsignedJwt.unsignedData, encoding: 'utf8' }, this.apiKey);
        return new Jwt(header, body, signature.toString('base64'));
    }
}
function validateOptions$1(opts) {
    const invalidOptionMessage = (name) => `Invalid JwtGenerator options. \`${name}\` is required`;
    assert(opts != null, 'JwtGenerator options must be provided');
    assert(opts.apiKey != null, invalidOptionMessage('apiKey'));
    assert(opts.apiKeyId != null, invalidOptionMessage('apiKeyId'));
    assert(opts.appId != null, invalidOptionMessage('appId'));
    assert(opts.accessTokenSigner != null, invalidOptionMessage('accessTokenSigner'));
}

/**
 * Class responsible for verification of JWTs.
 */
class JwtVerifier {
    constructor(options) {
        validateOptions(options);
        this.accessTokenSigner = options.accessTokenSigner;
        this.apiPublicKey = options.apiPublicKey;
        this.apiKeyId = options.apiKeyId;
    }
    /**
     * Verifies the validity of the given JWT.
     * @param {Jwt} token - The JWT to verify.
     * @returns {boolean}
     */
    verifyToken(token) {
        if (token == null) {
            throw new Error('Token is empty');
        }
        if (!this.allFieldsAreCorrect(token)) {
            return false;
        }
        return this.accessTokenSigner.verifyTokenSignature({ value: token.unsignedData, encoding: 'utf8' }, { value: token.signature, encoding: 'base64' }, this.apiPublicKey);
    }
    allFieldsAreCorrect(token) {
        return token.header.kid == this.apiKeyId
            && token.header.alg == this.accessTokenSigner.getAlgorithm()
            && token.header.cty == VirgilContentType
            && token.header.typ == JwtContentType;
    }
}
function validateOptions(opts) {
    const invalidOptionMessage = (name) => `Invalid JwtVerifier options. \`${name}\` is required`;
    assert(opts != null, 'JwtVerifier options must be provided');
    assert(opts.apiPublicKey != null, invalidOptionMessage('apiPublicKey'));
    assert(opts.apiKeyId != null, invalidOptionMessage('apiKeyId'));
    assert(opts.accessTokenSigner != null, invalidOptionMessage('accessTokenSigner'));
}

const TOKEN_EXPIRATION_MARGIN = 5;
/**
 * Implementation of {@link IAccessTokenProvider} that caches the JWT
 * in memory while it's fresh (i.e. not expired) and uses the user-provided
 * callback function to get the JWT when requested by the clients.
 */
class CachingJwtProvider {
    /**
     * Creates a new instance of `CachingJwtProvider`.
     * @param {GetJwtCallback} renewJwtFn - The function that will be called
     * whenever the fresh JWT is needed. If the `renewJwtFn` returns the JWT
     * as a string, it will be converted to {@link Jwt} instance automatically.
     * @param {Jwt|string} [initialToken] - Optional initial JWT.
     */
    constructor(renewJwtFn, initialToken) {
        if (typeof renewJwtFn !== 'function') {
            throw new TypeError('`renewJwtFn` must be a function');
        }
        if (initialToken) {
            let jwt;
            if (typeof initialToken === 'string') {
                jwt = Jwt.fromString(initialToken);
            }
            else if (initialToken instanceof Jwt) {
                jwt = initialToken;
            }
            else {
                throw new Error(`Expected "initialToken" to be a string or an instance of Jwt, got ${typeof initialToken}`);
            }
            this.cachedJwt = jwt;
        }
        this.getJwt = (context) => {
            if (this.cachedJwt && !this.cachedJwt.isExpired(addSeconds(new Date, TOKEN_EXPIRATION_MARGIN))) {
                return Promise.resolve(this.cachedJwt);
            }
            if (this.jwtPromise) {
                return this.jwtPromise;
            }
            this.jwtPromise = Promise.resolve(renewJwtFn(context))
                .then(token => {
                const jwt = typeof token === 'string' ? Jwt.fromString(token) : token;
                this.cachedJwt = jwt;
                this.jwtPromise = undefined;
                return jwt;
            }).catch(err => {
                this.jwtPromise = undefined;
                throw err;
            });
            return this.jwtPromise;
        };
    }
    /**
     * Returns a `Promise` resolved with the cached token if it's fresh, or the
     * token obtained by the call to the `renewJwtCallback` otherwise. The token
     * obtained from the `renewJwtCallback` is then cached. If the `renewJwtCallback`
     * returns the JWT as a string, it is converted to {@link Jwt} instance before returning.
     * @param {ITokenContext} context
     * @returns {Promise<IAccessToken>}
     */
    getToken(context) {
        return this.getJwt(context);
    }
}

/**
 * Implementation of {@link IAccessToken} that calls the user-provided
 * callback function to get the JWT when requested by the clients.
 */
class CallbackJwtProvider {
    /**
     * Creates a new instance of `CallbackJwtProvider`.
     *
     * @param {GetJwtCallback} getJwtFn - The function that will be called
     * whenever the JWT is needed. If the `getJwtFn` returns the JWT as a
     * string, it will be converted to {@link Jwt} instance automatically.
     */
    constructor(getJwtFn) {
        if (typeof getJwtFn !== 'function') {
            throw new TypeError('`getJwtFn` must be a function');
        }
        this.getJwt = (context) => Promise.resolve(getJwtFn(context))
            .then(token => typeof token === 'string' ? Jwt.fromString(token) : token);
    }
    /**
     * Returns a `Promise` resolved with the {@link Jwt} instance obtained
     * by the call to the {@link CallbackJwtProvider.getJwt}. If the
     * `getJwtFn` returns the JWT as a string, it is converted to
     * {@link Jwt} instance before returning.
     *
     * @param {ITokenContext} context
     * @returns {Promise<IAccessToken>}
     */
    getToken(context) {
        return this.getJwt(context);
    }
}

/**
 * Implementation of {@link IAccessTokenProvider} that returns a
 * user-provided constant access token whenever it is requested by the clients.
 */
class ConstAccessTokenProvider {
    /**
     * Creates a new instance of `ConstAccessTokenProvider`
     * @param {IAccessToken} accessToken - The access token to be returned
     * whenever it is requested.
     */
    constructor(accessToken) {
        this.accessToken = accessToken;
        if (accessToken == null) {
            throw new TypeError('`accessToken` is required');
        }
    }
    ;
    /**
     * Returns a `Promise` fulfilled with the
     * {@link ConstAccessTokenProvider.accessToken} provided to the constructor
     * of this instance.
     *
     * @param {ITokenContext} context
     * @returns {Promise<IAccessToken>}
     */
    getToken(context) {
        return Promise.resolve(this.accessToken);
    }
}

/**
 * Implementation of {@link IAccessTokenProvider} that generates a
 * new JWT whenever it is requested by the clients.
 *
 * This class is meant to be used on the server side only.
 */
class GeneratorJwtProvider {
    /**
     * Creates a new instance of `GeneratorJwtProvider` with the given
     * {@link JwtGenerator}, additional data and default identity.
     *
     * @param {JwtGenerator} jwtGenerator - Object to delegate the JWT generation to.
     * @param {IExtraData} additionalData - Additional data to include with the JWT.
     * @param {string} defaultIdentity - Identity of the user to include in the token
     * when none is provided explicitly by the client.
     */
    constructor(jwtGenerator, additionalData, defaultIdentity) {
        this.jwtGenerator = jwtGenerator;
        this.additionalData = additionalData;
        this.defaultIdentity = defaultIdentity;
        if (jwtGenerator == null) {
            throw new TypeError('`jwtGenerator` is required');
        }
    }
    /**
     * Returns a `Promise` fulfilled with the JWT obtained from the call
     * to {@link GeneratorJwtProvider.jwtGenerator} {@link JwtGenerator.generateToken}
     * method, passing it the {@link GeneratorJwtProvider.additionalData} and
     * {@link GeneratorJwtProvider.defaultIdentity}
     *
     * @param {ITokenContext} context
     * @returns {Promise<IAccessToken>}
     */
    getToken(context) {
        return Promise.resolve().then(() => {
            const jwt = this.jwtGenerator.generateToken(context.identity || this.defaultIdentity || '', this.additionalData);
            return jwt;
        });
    }
}

/**
 * Intermediate representation of the Virgil Card with `contentSnapshot`
 * and `snapshot`s of the signatures in UTF-8.
 */
class RawSignedModel {
    /**
     * Initializes a new instance of `RawSignedModel`.
     * @param {string} contentSnapshot - The content snapshot in UTF-8.
     * @param {IRawSignature[]} signatures - The signatures. If signatures
     * themselves have snapshots, those must also be in UTF-8.
     */
    constructor(contentSnapshot, signatures) {
        this.contentSnapshot = contentSnapshot;
        this.signatures = signatures;
    }
    /**
     * Converts the `str` in base64 encoding into a `RawSignedModel` object.
     *
     * @param {string} str - Base64 string representation of the card as
     * returned by {@RawSignedModel.toString} method.
     *
     * @returns {RawSignedModel}
     */
    static fromString(str) {
        const jsonStr = base64Decode(str);
        let obj;
        try {
            obj = JSON.parse(jsonStr);
        }
        catch (error) {
            throw new Error('The string to be parsed is in invalid format');
        }
        return RawSignedModel.fromJson(obj);
    }
    /**
     * Converts the `json` serializable object into a `RawSignedModel` object.
     * @param {IRawSignedModelJson} json - JSON-serializable object returned by
     * {@link RawSignedModel.toJson} method.
     * @returns {RawSignedModel}
     */
    static fromJson(json) {
        const contentSnapshotUtf8 = base64Decode(json.content_snapshot);
        const signaturesWithUtf8Snapshots = (json.signatures || []).map(({ signer, signature, snapshot }) => {
            if (snapshot) {
                return {
                    signer,
                    signature,
                    snapshot: base64Decode(snapshot)
                };
            }
            return { signer, signature };
        });
        return new RawSignedModel(contentSnapshotUtf8, signaturesWithUtf8Snapshots);
    }
    /**
     * This is to make it work with `JSON.stringify`, calls
     * {@link RawSignedModel.toJson} under the hood.
     * @returns {IRawSignedModelJson}
     */
    toJSON() {
        return this.toJson();
    }
    /**
     * Returns a JSON-serializable representation of this model in the
     * format it is stored in the Virgil Cards Service. (i.e. with
     * `contentSnapshot` and `snapshot`s of the signatures as base64 encoded
     * strings.
     * @returns {IRawSignedModelJson}
     */
    toJson() {
        return {
            content_snapshot: base64Encode(this.contentSnapshot),
            signatures: this.signatures.map(({ signer, signature, snapshot }) => {
                if (snapshot) {
                    return {
                        signer,
                        signature,
                        snapshot: base64Encode(snapshot)
                    };
                }
                return { signer, signature };
            })
        };
    }
    /**
     * Serializes this model to string in base64 encoding.
     * @returns {string}
     */
    toString() {
        return base64Encode(JSON.stringify(this));
    }
    /**
     * Same as {@link RawSignedModel.toJson}. Please use that instead.
     * @returns {IRawSignedModelJson}
     */
    exportAsJson() {
        return this.toJson();
    }
    /**
     * Same as {@link RawSignedModel.toString}. Please use that instead.
     * @returns {string}
     */
    exportAsString() {
        return this.toString();
    }
}

const { fetch, Request, Response, Headers } = fetchPonyfill();

const OS_LIST = [
    /* Windows Phone */
    {
        name: 'Windows Phone',
        test: [/windows phone/i],
    },
    /* Windows */
    {
        test: [/windows/i],
        name: 'Windows'
    },
    /* macOS */
    {
        test: [/macintosh/i],
        name: 'macOS'
    },
    /* iOS */
    {
        test: [/(ipod|iphone|ipad)/i],
        name: 'iOS'
    },
    /* Android */
    {
        test: [/android/i],
        name: 'Android',
    },
    /* Linux */
    {
        test: [/linux/i],
        name: 'Linux',
    },
    /* Chrome OS */
    {
        test: [/CrOS/],
        name: 'Chrome OS'
    },
    /* Playstation 4 */
    {
        test: [/PlayStation 4/],
        name: 'PlayStation 4',
    },
];

// Order of browsers matters! Edge, Opera and Chromium have Chrome in User Agent.
const BROWSER_LIST = [
    {
        test: [/googlebot/i],
        name: 'Googlebot'
    },
    {
        test: [/opera/i, /opr\/|opios/i],
        name: 'Opera',
    },
    {
        test: [/msie|trident/i],
        name: 'Internet Explorer',
    },
    {
        test: [/\sedg/i],
        name: 'Microsoft Edge'
    },
    {
        test: [/firefox|iceweasel|fxios/i],
        name: 'Firefox',
    },
    {
        test: [/chromium/i],
        name: 'Chromium'
    },
    {
        test: [/chrome|crios|crmo/i],
        name: 'Chrome',
    },
    {
        test: [/android/i],
        name: 'Android Browser'
    },
    {
        test: [/playstation 4/i],
        name: 'PlayStation 4',
    },
    {
        test: [/safari|applewebkit/i],
        name: 'Safari',
    }
];

/**
 * Class responsible for tracking which Virgil SDK is being used to make requests, its version,
 * browser and platform.
 */
class VirgilAgent {
    /**
     * Initializes a new instance of `VirgilAgent`.
     * @param {string} product - name of product eg (sdk, brainkey, bpp, keyknox, ratchet, e3kit, purekit)
     * argument of request methods.
     * @param {string} version - version of the product.
     * @param {string} [userAgent] - string with device user agent. Optional
     */
    constructor(product, version, userAgent) {
        /**
         * Detect Cordova / PhoneGap / Ionic frameworks on a mobile device.
         * @return {boolean} true if detects Ionic.
         */
        this.isIonic = () => typeof window !== "undefined" &&
            !!("cordova" in window || "phonegap" in window || "PhoneGap" in window) &&
            /android|ios|iphone|ipod|ipad|iemobile/i.test(this.userAgent);
        this.userAgent = userAgent || this.getUserAgent();
        this.value = `${product};js;${this.getHeaderValue()};${version}`;
    }
    /**
     * Returns navigator.userAgent string or '' if it's not defined.
     * @return {string} user agent string
     */
    getUserAgent() {
        if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string") {
            return navigator.userAgent;
        }
        else {
            return "";
        }
    }
    ;
    /**
     * Detects device OS
     * @returns {string} returns OS if detected or 'other'.
     */
    getOsName() {
        const os = OS_LIST.find((os) => os.test.some(condition => condition.test(this.userAgent)));
        return os ? os.name : 'other';
    }
    /**
     * Detects device browser
     * @returns {string} returns browser if detected of 'other'.
     */
    getBrowser() {
        const browser = BROWSER_LIST.find((browser) => browser.test.some(condition => condition.test(this.userAgent)));
        return browser ? browser.name : 'other';
    }
    /**
     * Detect React Native.
     * @return {boolean} true if detects ReactNative .
     */
    isReactNative() {
        return typeof navigator === "object" && navigator.product === "ReactNative";
    }
    /**
     * Return information for `virgil-agent` header.
     * @return {string} string in format: PRODUCT;FAMILY;PLATFORM;VERSION
     */
    getHeaderValue() {
        try {
            if (this.isReactNative())
                return "ReactNative";
            if (this.isIonic())
                return `Ionic/${this.getOsName()}`;
            if (!undefined && typeof global !== 'undefined') {
                const majorVersion = process.version.replace(/\.\d+\.\d+$/, '').replace('v', '');
                return `Node${majorVersion}/${process.platform}`;
            }
            return `${this.getBrowser()}/${this.getOsName()}`;
        }
        catch (e) {
            return `Unknown`;
        }
    }
}

/**
 * Class responsible for making HTTP requests.
 * @hidden
 */
class Connection {
    /**
     * Initializes a new instance of `Connection`.
     * @param {string} prefix - `prefix` will be prepended to the `endpoint`
     * argument of request methods.
     * @param {VirgilAgentValue} [virgilAgentValue] - optional instance of VirgilAgent for products that wraps
     * Virgil SDK
     */
    constructor(prefix, info) {
        this.prefix = prefix;
        if (!info)
            info = { product: 'sdk', version: "6.2.0" };
        this.virgilAgentValue = new VirgilAgent(info.product, info.version).value;
    }
    /**
     * Issues a GET request against the `endpoint`.
     * @param {string} endpoint - Endpoint URL relative to the `prefix`.
     * @param {string} accessToken - Token to authenticate the request.
     * @returns {Promise<Response>}
     */
    get(endpoint, accessToken) {
        const headers = this.createHeaders(accessToken);
        return this.send(endpoint, 'GET', { headers });
    }
    /**
     * Issues a POST request against the `endpoint` sending the `data` as JSON.
     * @param {string} endpoint - Endpoint URL relative to the `prefix`.
     * @param {string} accessToken - Token to authenticate the request.
     * @param {object} data - Response body.
     * @returns {Promise<Response>}
     */
    post(endpoint, accessToken, data = {}) {
        const headers = this.createHeaders(accessToken);
        headers.set('Content-Type', 'application/json');
        return this.send(endpoint, 'POST', {
            headers: headers,
            body: JSON.stringify(data)
        });
    }
    send(endpoint, method, params) {
        return fetch(this.prefix + endpoint, { method, ...params });
    }
    createHeaders(accessToken) {
        const headers = new Headers();
        headers.set('Authorization', `Virgil ${accessToken}`);
        headers.set('Virgil-Agent', this.virgilAgentValue);
        return headers;
    }
}

/**
 * Custom error class for errors specific to Virgil SDK.
 */
class VirgilError extends Error {
    constructor(m, name = 'VirgilError', DerivedClass = VirgilError) {
        super(m);
        Object.setPrototypeOf(this, DerivedClass.prototype);
        this.name = name;
    }
}

var ErrorCode;
(function (ErrorCode) {
    ErrorCode[ErrorCode["AccessTokenExpired"] = 20304] = "AccessTokenExpired";
    ErrorCode[ErrorCode["Unknown"] = 0] = "Unknown";
})(ErrorCode || (ErrorCode = {}));
/**
 * Error thrown by {@link CardManager} when request to the Virgil Cards Service
 * fails.
 */
class VirgilHttpError extends VirgilError {
    constructor(message, status, errorCode) {
        super(message, 'VirgilHttpError', VirgilHttpError);
        this.httpStatus = status;
        this.errorCode = errorCode;
    }
}
/**
 * Generates error object from response object with HTTP status >= 400
 *
 * @hidden
 *
 * @param {Response} response
 * @returns {Promise<VirgilHttpError>}
 */
async function generateErrorFromResponse(response) {
    if (response.status >= 400 && response.status < 500) {
        const reason = await response.json();
        return new VirgilHttpError(reason.message, response.status, reason.code);
    }
    else {
        return new VirgilHttpError(response.statusText, response.status, 0);
    }
}

const PublishEndpoint = '/card/v5';
const SearchEndpoint = '/card/v5/actions/search';
const GetCardEndpoint = (cardId) => `/card/v5/${cardId}`;
const RevokeCardEndpoint = (cardId) => `/card/v5/actions/revoke/${cardId}`;
/**
 * Class responsible for sending requests to the Virgil Cards Service.
 *
 * @hidden
 */
class CardClient {
    /**
     * Initializes new instance of `CardClient`.
     * @param {IConnection | string} connection - Object implementing the
     * {@link IConnection} interface.
     */
    constructor(connection, productInfo) {
        if (typeof connection === 'string') {
            this.connection = new Connection(connection, productInfo);
        }
        else if (connection) {
            this.connection = connection;
        }
        else {
            this.connection = new Connection('https://api.virgilsecurity.com', productInfo);
        }
    }
    /**
     * Issues a request to search cards by the `identity`.
     * @param {string[]} identities - Array of identities to search for.
     * @param {string} accessToken - A token to authenticate the request.
     * @returns {Promise<RawSignedModel[]>}
     */
    async searchCards(identities, accessToken) {
        const response = await this.connection.post(SearchEndpoint, accessToken, { identities });
        if (!response.ok)
            throw await generateErrorFromResponse(response);
        const cardsJson = await response.json();
        if (cardsJson === null)
            return [];
        return cardsJson.map(RawSignedModel.fromJson);
    }
    /**
     * Issues a request to get the card by id.
     * @param {string} cardId - Id of the card to fetch.
     * @param {string} accessToken - A token to authenticate the request.
     * @returns {Promise<ICardResult>}
     */
    async getCard(cardId, accessToken) {
        if (!cardId)
            throw new TypeError('`cardId` should not be empty');
        if (!accessToken)
            throw new TypeError('`accessToken` should not be empty');
        const response = await this.connection.get(GetCardEndpoint(cardId), accessToken);
        if (!response.ok) {
            throw await generateErrorFromResponse(response);
        }
        const isOutdated = response.headers.get('X-Virgil-Is-Superseeded') === 'true';
        const cardJson = await response.json();
        const cardRaw = RawSignedModel.fromJson(cardJson);
        return { cardRaw, isOutdated };
    }
    /**
     * Issues a request to publish the card.
     * @param {RawSignedModel} model - Card to publish.
     * @param {string} accessToken - A token to authenticate the request.
     * @returns {Promise<RawSignedModel>}
     */
    async publishCard(model, accessToken) {
        if (!model)
            throw new TypeError('`model` should not be empty');
        if (!accessToken)
            throw new TypeError('`accessToken` should not be empty');
        const response = await this.connection.post(PublishEndpoint, accessToken, model);
        if (!response.ok) {
            throw await generateErrorFromResponse(response);
        }
        const cardJson = await response.json();
        return RawSignedModel.fromJson(cardJson);
    }
    async revokeCard(cardId, accessToken) {
        if (!cardId)
            throw new TypeError('`cardId` should not be empty');
        if (!accessToken)
            throw new TypeError('`accessToken` should not be empty');
        const response = await this.connection.post(RevokeCardEndpoint(cardId), accessToken);
        if (!response.ok) {
            throw await generateErrorFromResponse(response);
        }
    }
}

/**
 * @hidden
 */
const SelfSigner = "self";
/**
 * @hidden
 */
const VirgilSigner = "virgil";
/**
 * @hidden
 */
const CardVersion = '5.0';
/**
 * @hidden
 */
const CardIdByteLength = 32;

/**
 * Class responsible for generating signatures of the cards.
 */
class ModelSigner {
    /**
     * Initializes a new instance of `ModelSigner`.
     * @param {ICardCrypto} crypto - Object implementing the
     * {@link ICardCrypto} interface.
     */
    constructor(crypto) {
        this.crypto = crypto;
    }
    /**
     * Generates a new signature based on `rawParams`.
     * @param {IRawSignParams} rawParams
     */
    sign(rawParams) {
        const { model, signerPrivateKey, signer, extraSnapshot } = this.prepareParams(rawParams);
        const signedSnapshot = extraSnapshot != null
            ? model.contentSnapshot + extraSnapshot
            : model.contentSnapshot;
        const signature = this.crypto.generateSignature({ value: signedSnapshot, encoding: 'utf8' }, signerPrivateKey);
        model.signatures.push({
            signer,
            signature: signature.toString('base64'),
            snapshot: extraSnapshot
        });
    }
    prepareParams({ model, signerPrivateKey, extraFields, signer }) {
        signer = signer || SelfSigner;
        let extraSnapshot;
        if (extraFields != null) {
            extraSnapshot = JSON.stringify(extraFields);
        }
        const final = { model, signerPrivateKey, signer, extraSnapshot };
        this.validate(final);
        return final;
    }
    validate({ model, signerPrivateKey, signer }) {
        if (model == null) {
            throw new Error("Model is empty");
        }
        if (signerPrivateKey == null) {
            throw new Error("`signerPrivateKey` property is mandatory");
        }
        if (model.signatures != null && model.signatures.some(s => s.signer == signer)) {
            throw new Error("The model already has this signature.");
        }
    }
}

/**
 * Converts an {@link ICard} to a {@link RawSignedModel}.
 *
 * @hidden
 *
 * @param {ICard} card - The {@link ICard} to convert.
 * @returns {RawSignedModel}
 */
function cardToRawSignedModel(card) {
    return new RawSignedModel(card.contentSnapshot, card.signatures.slice());
}
/**
 * Generates a {@link RawSignedModel} from the given `params`.
 *
 * @hidden
 *
 * @param {ICardCrypto} crypto - Object implementing the {@link ICardCrypto}
 * interface.
 * @param {INewCardParams} params - New card parameters.
 * @returns {RawSignedModel}
 */
function generateRawSigned(crypto, params) {
    const { identity, publicKey, previousCardId } = params;
    const now = getUnixTimestamp(new Date);
    const details = {
        identity: identity,
        previous_card_id: previousCardId,
        created_at: now,
        version: CardVersion,
        public_key: crypto.exportPublicKey(publicKey).toString('base64'),
    };
    return new RawSignedModel(JSON.stringify(details), []);
}
/**
 * Converts the {@link RawSignedModel} into the {@link ICard}.
 *
 * @hidden
 *
 * @param {ICardCrypto} crypto - Object implementing the {@link ICardCrypto}
 * interface.
 * @param {RawSignedModel} model - The model to convert.
 * @param {boolean} isOutdated - Boolean indicating whether there is a newer
 * Virgil Card replacing the one that `model` represents.
 *
 * @returns {ICard}
 */
function parseRawSignedModel(crypto, model, isOutdated = false) {
    const content = JSON.parse(model.contentSnapshot);
    const signatures = model.signatures.map(rawSignToCardSign);
    return {
        id: generateCardId(crypto, model.contentSnapshot),
        publicKey: crypto.importPublicKey({ value: content.public_key, encoding: 'base64' }),
        contentSnapshot: model.contentSnapshot,
        identity: content.identity,
        version: content.version,
        createdAt: new Date(content.created_at * 1000),
        previousCardId: content.previous_card_id,
        signatures,
        isOutdated
    };
}
/**
 * Given the array of `cards`, returns another array with outdated cards
 * filtered out and the `previousCard` properties of the cards that replace
 * the outdated ones being populated with appropriate outdated cards.
 * i.e. turns this (A is for Actual, O is for Outdated):
 * ```
 * A -> O -> A -> A -> O
 * ```
 * into this
 * ```
 * A -> A -> A
 * |         |
 * O         O
 * ```
 *
 * @hidden
 *
 * @param {ICard[]} cards - The cards array to transform.
 * @returns {ICard[]} - Transformed array.
 */
function linkedCardList(cards) {
    const unsorted = Object.create(null);
    for (const card of cards) {
        unsorted[card.id] = card;
    }
    for (const card of cards) {
        if (card.previousCardId == null)
            continue;
        if (unsorted[card.previousCardId] == null)
            continue;
        unsorted[card.previousCardId].isOutdated = true;
        card.previousCard = unsorted[card.previousCardId];
        delete unsorted[card.previousCardId];
    }
    return Object.keys(unsorted).map(key => unsorted[key]);
}
/**
 * Calculates ID for the VirgilCard from the `snapshot` of its contents.
 *
 * @hidden
 *
 * @param {ICardCrypto} crypto - Object implementing the {@link ICardCrypto}
 * interface.
 * @param {string} snapshot - The VirgilCard's contents snapshot.
 * @returns {string} - VirgilCard's ID encoded in HEX.
 */
function generateCardId(crypto, snapshot) {
    const fingerprint = crypto
        .generateSha512({ value: snapshot, encoding: 'utf8' })
        .slice(0, CardIdByteLength);
    return fingerprint.toString('hex');
}
function rawSignToCardSign({ snapshot, signature, signer }) {
    return {
        signer,
        signature,
        snapshot,
        extraFields: tryParseExtraFields(snapshot)
    };
}
function tryParseExtraFields(snapshot) {
    if (snapshot) {
        try {
            return JSON.parse(snapshot);
        }
        catch (ignored) { }
    }
    return {};
}

/**
 * Error thrown by {@link CardManager} instances when the card received from
 * the network (or imported from string\json) fails verification.
 */
class VirgilCardVerificationError extends VirgilError {
    constructor(m) {
        super(m, 'CardVerificationError', VirgilCardVerificationError);
    }
}

const CARDS_SERVICE__NAME = 'cards';
/**
 * @hidden
 */
const throwingAccessTokenProvider = {
    getToken: () => {
        throw new Error('Please set `CardManager.accessTokenProvider` to be able to make requests.');
    }
};
const getCardServiceTokenContext = (context) => ({
    ...context,
    service: CARDS_SERVICE__NAME,
});
/**
 * Class responsible for creating, publishing and retrieving Virgil Cards.
 */
class CardManager {
    constructor(params) {
        this.crypto = params.cardCrypto;
        this.client = new CardClient(params.apiUrl, params.productInfo);
        this.modelSigner = new ModelSigner(params.cardCrypto);
        this.signCallback = params.signCallback;
        this.retryOnUnauthorized = params.retryOnUnauthorized;
        this.cardVerifier = params.cardVerifier;
        this.accessTokenProvider = params.accessTokenProvider || throwingAccessTokenProvider;
    }
    /**
     * Generates a {@link RawSignedModel} that represents a card from
     * `cardParams`.
     * Use this method if you don't need to publish the card right away, for
     * example if you need to first send it to your backend server to apply
     * additional signature.
     *
     * @param {INewCardParams} cardParams - New card parameters.
     * @returns {RawSignedModel}
     */
    generateRawCard(cardParams) {
        const model = generateRawSigned(this.crypto, cardParams);
        this.modelSigner.sign({
            model,
            signerPrivateKey: cardParams.privateKey,
            signer: SelfSigner,
            extraFields: cardParams.extraFields
        });
        return model;
    }
    /**
     * Generates a card from `cardParams` and publishes it in the Virgil Cards
     * Service.
     * @param {INewCardParams} cardParams - New card parameters.
     * @returns {Promise<ICard>}
     */
    async publishCard(cardParams) {
        validateCardParams(cardParams);
        const tokenContext = {
            service: CARDS_SERVICE__NAME,
            identity: cardParams.identity,
            operation: 'publish'
        };
        const token = await this.accessTokenProvider.getToken(tokenContext);
        const rawSignedModel = this.generateRawCard(Object.assign({}, cardParams, { identity: token.identity() }));
        return await this.publishRawSignedModel(rawSignedModel, tokenContext, token);
    }
    /**
     * Publishes a previously generated card in the form of
     * {@link RawSignedModel} object.
     *
     * @param {RawSignedModel} rawCard - The card to publish.
     * @returns {Promise<ICard>}
     */
    async publishRawCard(rawCard) {
        assert(rawCard != null && rawCard.contentSnapshot != null, '`rawCard` should not be empty');
        const cardDetails = JSON.parse(rawCard.contentSnapshot);
        const tokenContext = getCardServiceTokenContext({ identity: cardDetails.identity, operation: 'publish' });
        const token = await this.accessTokenProvider.getToken(tokenContext);
        return this.publishRawSignedModel(rawCard, tokenContext, token);
    }
    /**
     * Fetches the card by `cardId` from the Virgil Card Service.
     * @param {string} cardId - Id of the card to fetch.
     * @returns {Promise<ICard>}
     */
    async getCard(cardId) {
        const tokenContext = getCardServiceTokenContext({ operation: 'get' });
        const accessToken = await this.accessTokenProvider.getToken(tokenContext);
        const cardWithStatus = await this.tryDo(tokenContext, accessToken, async (token) => await this.client.getCard(cardId, token.toString()));
        const card = parseRawSignedModel(this.crypto, cardWithStatus.cardRaw, cardWithStatus.isOutdated);
        if (card.id !== cardId) {
            throw new VirgilCardVerificationError('Received invalid card');
        }
        this.validateCards([card]);
        return card;
    }
    /**
     * Fetches collection of cards with the given `identity` from the Virgil
     * Cards Service.
     * @param {string|string[]} identities - Identity or an array of identities of the cards to fetch.
     * @returns {Promise<ICard[]>}
     */
    async searchCards(identities) {
        if (!identities)
            throw new TypeError('Argument `identities` is required');
        const identitiesArr = Array.isArray(identities) ? identities : [identities];
        if (identitiesArr.length === 0)
            throw new TypeError('Identities array must not be empty');
        const tokenContext = getCardServiceTokenContext({ operation: 'search' });
        const accessToken = await this.accessTokenProvider.getToken(tokenContext);
        const rawCards = await this.tryDo(tokenContext, accessToken, async (token) => await this.client.searchCards(identitiesArr, token.toString()));
        const cards = rawCards.map(raw => parseRawSignedModel(this.crypto, raw, false));
        const identitiesSet = new Set(identitiesArr);
        if (cards.some(c => !identitiesSet.has(c.identity))) {
            throw new VirgilCardVerificationError('Received invalid cards');
        }
        this.validateCards(cards);
        return linkedCardList(cards);
    }
    /**
     * Marks the Virgil Card specified by `cardId` as revoked.  Revoked cards will have `isOutdated`
     * property set to `true` when retrieved via {@link CardManager.getCard} method.
     * Also revoked cards will be absent in the {@link CardManager.searchCards} result.
     * @param {string} cardId - Id of the card to revoke.
     * @returns {Promise}
     */
    async revokeCard(cardId) {
        if (!cardId)
            throw new TypeError('Argument `cardId` is required');
        const tokenContext = getCardServiceTokenContext({ operation: 'revoke' });
        const accessToken = await this.accessTokenProvider.getToken(tokenContext);
        await this.tryDo(tokenContext, accessToken, async (token) => await this.client.revokeCard(cardId, token.toString()));
    }
    /**
     * Converts the card in the form of {@link RawSignedModel} object to the
     * {@link ICard} object.
     *
     * @see {@link CardManager.exportCard}
     *
     * @param {RawSignedModel} rawCard - The card to convert.
     * @returns {ICard}
     */
    importCard(rawCard) {
        const card = parseRawSignedModel(this.crypto, rawCard);
        this.validateCards([card]);
        return card;
    }
    /**
     * Converts the card in the base64 string form to the {@link ICard} object.
     *
     * @see {@link CardManager.exportCardAsString}
     *
     * @param {string} str - The string in base64.
     * @returns {ICard}
     */
    importCardFromString(str) {
        assert(Boolean(str), '`str` should not be empty');
        return this.importCard(RawSignedModel.fromString(str));
    }
    /**
     * Converts the card in the JSON-serializable object form to the
     * {@link ICard} object.
     *
     * @see {@link CardManager.exportCardAsJson}
     *
     * @param {IRawSignedModelJson} json
     * @returns {ICard}
     */
    importCardFromJson(json) {
        assert(Boolean(json), '`json` should not be empty');
        return this.importCard(RawSignedModel.fromJson(json));
    }
    /**
     * Converts the card in the form of {@link ICard} object to the
     * {@link RawSignedModel} object.
     *
     * @see {@link CardManager.importCard}
     *
     * @param {ICard} card
     * @returns {RawSignedModel}
     */
    exportCard(card) {
        return cardToRawSignedModel(card);
    }
    /**
     * Converts the card in the form of {@link ICard} object to the string
     * in base64 encoding.
     *
     * @see {@link CardManager.importCardFromString}
     *
     * @param {ICard} card
     * @returns {string}
     */
    exportCardAsString(card) {
        return this.exportCard(card).toString();
    }
    /**
     * Converts the card in the form of {@link ICard} object to the
     * JSON-serializable object form.
     *
     * @see {@link CardManager.importCardFromJson}
     *
     * @param {ICard} card
     * @returns {IRawSignedModelJson}
     */
    exportCardAsJson(card) {
        return this.exportCard(card).toJson();
    }
    /**
     * @hidden
     */
    async publishRawSignedModel(rawCard, context, accessToken) {
        if (this.signCallback != null) {
            rawCard = await this.signCallback(rawCard);
        }
        const publishedModel = await this.tryDo(context, accessToken, async (token) => await this.client.publishCard(rawCard, token.toString()));
        if (rawCard.contentSnapshot !== publishedModel.contentSnapshot) {
            throw new VirgilCardVerificationError('Received invalid card');
        }
        const card = parseRawSignedModel(this.crypto, publishedModel);
        this.validateCards([card]);
        return card;
    }
    /**
     * @hidden
     */
    async tryDo(context, token, func) {
        try {
            return await func(token);
        }
        catch (e) {
            if (e instanceof VirgilHttpError &&
                e.httpStatus === 401 &&
                e.errorCode === ErrorCode.AccessTokenExpired &&
                this.retryOnUnauthorized) {
                token = await this.accessTokenProvider.getToken(getCardServiceTokenContext({
                    identity: context.identity,
                    operation: context.operation,
                    forceReload: true
                }));
                return await func(token);
            }
            throw e;
        }
    }
    /**
     * Delegates to the {@link CardManager.cardVerifier} to verify the validity
     * of the `cards`.
     *
     * @throws {@link VirgilCardVerificationError} if any of the cards is not
     * valid.
     *
     * @param {ICard[]} cards
     */
    validateCards(cards) {
        if (this.cardVerifier == null)
            return;
        for (const card of cards) {
            if (!this.cardVerifier.verifyCard(card)) {
                throw new VirgilCardVerificationError('Validation errors have been detected');
            }
        }
    }
}
/**
 * @hidden
 */
function validateCardParams(params, validateIdentity = false) {
    assert(params != null, 'Card parameters must be provided');
    assert(params.privateKey != null, 'Card\'s private key is required');
    assert(params.publicKey != null, 'Card\'s public key is required');
    if (validateIdentity) {
        assert(typeof params.identity === 'string' && params.identity !== '', 'Card\'s identity is required');
    }
}

const DEFAULTS$2 = {
    verifySelfSignature: true,
    verifyVirgilSignature: true,
    whitelists: []
};
const VIRGIL_CARDS_PUBKEY_BASE64 = 'MCowBQYDK2VwAyEAljOYGANYiVq1WbvVvoYIKtvZi2ji9bAhxyu6iV/LF8M=';
/**
 * Class responsible for validating cards by verifying their digital
 * signatures.
 */
class VirgilCardVerifier {
    /**
     * Initializes a new instance of `VirgilCardVerifier`.
     * @param {ICardCrypto} crypto - Object implementing the
     * {@link ICardCrypto} interface.
     * @param {IVirgilCardVerifierParams} options - Initialization options.
     */
    constructor(crypto, options) {
        this.crypto = crypto;
        const params = { ...DEFAULTS$2, ...(options || {}) };
        this.verifySelfSignature = params.verifySelfSignature;
        this.verifyVirgilSignature = params.verifyVirgilSignature;
        this.whitelists = params.whitelists;
        this.virgilCardsPublicKey = crypto.importPublicKey({ value: VIRGIL_CARDS_PUBKEY_BASE64, encoding: 'base64' });
    }
    /**
     * Verifies the signatures of the `card`.
     * @param {ICard} card
     * @returns {boolean} `true` if the signatures to be verified are present
     * and valid, otherwise `false`.
     */
    verifyCard(card) {
        if (this.selfValidationFailed(card)) {
            return false;
        }
        if (this.virgilValidationFailed(card)) {
            return false;
        }
        if (!this.whitelists || this.whitelists.length === 0) {
            return true;
        }
        const signers = card.signatures.map(s => s.signer);
        for (const whitelist of this.whitelists) {
            if (whitelist == null || whitelist.length === 0) {
                return false;
            }
            const intersectedCreds = whitelist.filter(x => signers.indexOf(x.signer) !== -1);
            if (intersectedCreds.length === 0) {
                return false;
            }
            const isValidForSome = intersectedCreds.some(cred => this.validateSignerSignature(card, this.getPublicKey(cred.publicKeyBase64), cred.signer));
            if (!isValidForSome) {
                return false;
            }
        }
        return true;
    }
    selfValidationFailed(card) {
        return this.verifySelfSignature
            && !this.validateSignerSignature(card, card.publicKey, SelfSigner);
    }
    virgilValidationFailed(card) {
        return this.verifyVirgilSignature
            && !this.validateSignerSignature(card, this.virgilCardsPublicKey, VirgilSigner);
    }
    getPublicKey(signerPublicKeyBase64) {
        return this.crypto.importPublicKey({ value: signerPublicKeyBase64, encoding: 'base64' });
    }
    validateSignerSignature(card, signerPublicKey, signer) {
        const signature = card.signatures.find(s => s.signer === signer);
        if (signature == null)
            return false;
        const extendedSnapshot = signature.snapshot == null
            ? card.contentSnapshot
            : card.contentSnapshot + signature.snapshot;
        return this.crypto.verifySignature({ value: extendedSnapshot, encoding: 'utf8' }, { value: signature.signature, encoding: 'base64' }, signerPublicKey);
    }
}

/**
 * Error thrown by {@link IStorageAdapter.store} method when saving a value
 * with a key that already exists in store.
 */
class StorageEntryAlreadyExistsError extends VirgilError {
    constructor(key) {
        super(`Storage entry ${key ? 'with key ' + name : 'with the given key'}already exists`, 'StorageEntryAlreadyExistsError', StorageEntryAlreadyExistsError);
    }
}

const NO_SUCH_FILE = 'ENOENT';
const FILE_EXISTS = 'EEXIST';
/**
 * Implementation of {@link IStorageAdapter} that uses file system for
 * persistence. For use in Node.js.
 */
class FileSystemStorageAdapter {
    /**
     * Initializes a new instance of `FileSystemStorageAdapter`.
     * @param {IStorageAdapterConfig} config - Configuration options.
     * Currently only `dir` is supported and must be a file system path
     * to the folder where the data will be stored.
     */
    constructor(config) {
        this.config = config;
        mkdirp.sync(path.resolve(this.config.dir));
    }
    /**
     * @inheritDoc
     */
    store(key, data) {
        return new Promise((resolve, reject) => {
            const file = this.resolveFilePath(key);
            fs.writeFile(file, data, { flag: 'wx' }, err => {
                if (err && err.code === FILE_EXISTS) {
                    return reject(new StorageEntryAlreadyExistsError());
                }
                resolve();
            });
        });
    }
    /**
     * @inheritDoc
     */
    load(key) {
        return Promise.resolve().then(() => {
            const filename = this.resolveFilePath(key);
            return readFileAsync(filename);
        });
    }
    /**
     * @inheritDoc
     */
    exists(key) {
        return new Promise((resolve, reject) => {
            const file = this.resolveFilePath(key);
            fs.access(file, err => {
                if (err) {
                    if (err.code === NO_SUCH_FILE) {
                        return resolve(false);
                    }
                    return reject(err);
                }
                resolve(true);
            });
        });
    }
    /**
     * @inheritDoc
     */
    remove(key) {
        return new Promise((resolve, reject) => {
            const file = this.resolveFilePath(key);
            fs.unlink(file, err => {
                if (err) {
                    if (err.code === NO_SUCH_FILE) {
                        return resolve(false);
                    }
                    return reject(err);
                }
                resolve(true);
            });
        });
    }
    /**
     * @inheritDoc
     */
    update(key, data) {
        return new Promise((resolve, reject) => {
            const file = this.resolveFilePath(key);
            fs.writeFile(file, data, { flag: 'w' }, err => {
                if (err) {
                    return reject(err);
                }
                resolve();
            });
        });
    }
    /**
     * @inheritDoc
     */
    clear() {
        return new Promise((resolve, reject) => {
            rimraf(this.config.dir, err => {
                if (err) {
                    return reject(err);
                }
                resolve();
            });
        });
    }
    /**
     * @inheritDoc
     */
    list() {
        return new Promise((resolve, reject) => {
            fs.readdir(this.config.dir, (err, files) => {
                if (err) {
                    return reject(err);
                }
                Promise.all(files.map(filename => readFileAsync(path.resolve(this.config.dir, filename)))).then(contents => {
                    const entries = contents.filter(content => content !== null);
                    resolve(entries);
                }).catch(reject);
            });
        });
    }
    resolveFilePath(key) {
        return path.resolve(this.config.dir, this.hash(key));
    }
    hash(data) {
        return crypto
            .createHash('sha256')
            .update(data)
            .digest('hex');
    }
}
function readFileAsync(filename) {
    return new Promise((resolve, reject) => {
        fs.readFile(filename, (err, data) => {
            if (err) {
                if (err.code === NO_SUCH_FILE) {
                    return resolve(null);
                }
                return reject(err);
            }
            resolve(data.toString('utf8'));
        });
    });
}

/**
 * Error thrown from {@link PrivateKeyStorage.save} method when saving
 * a private key with a name that already exists in store.
 */
class PrivateKeyExistsError extends VirgilError {
    constructor(message = 'Private key with same name already exists') {
        super(message, 'PrivateKeyExistsError', PrivateKeyExistsError);
    }
}

const DEFAULTS$1 = {
    dir: '.virgil_keys',
    name: 'VirgilKeys'
};
/**
 * Class representing a storage container for private key data.
 * Use this class if you need to load the keys stored with
 * version 4.x of this library. For new code, use the
 * {@link PrivateKeyStorage} instead.
 *
 * @deprecated since version 5.0
 */
class KeyStorage {
    constructor(config = {}) {
        console.log('Warning! `KeyStorage` is deprecated. Use `PrivateKeyStorage` instead.');
        this.adapter = resolveAdapter$1(config);
    }
    /**
     * Checks whether a private key data with the given name exist in persistent storage.
     * @param {string} name - Name to check.
     * @returns {Promise<boolean>} - True if key data exist, otherwise false.
     */
    exists(name) {
        validateName$1(name);
        return this.adapter.exists(name);
    }
    /**
     * Loads the private key data by the given name.
     * @param {string} name - Name of key data to load.
     * @returns {Promise<string | null>} - Private key data as a string,
     * or null if there is no data for the given name.
     */
    load(name) {
        validateName$1(name);
        return this.adapter.load(name);
    }
    /**
     * Removes the private key data stored under the given name from persistent storage.
     * @param {string} name - Name of the key data to remove.
     * @returns {Promise<boolean>} - True if the key has been removed, otherwise false.
     */
    remove(name) {
        validateName$1(name);
        return this.adapter.remove(name);
    }
    /**
     * Persists the private key data under the given name.
     * @param {string} name - Name of the key data.
     * @param {string} data - The key data.
     * @returns {Promise<void>}
     */
    save(name, data) {
        validateName$1(name);
        return this.adapter.store(name, data)
            .catch(error => {
            if (error && error.code === 'EEXIST') {
                return Promise.reject(new PrivateKeyExistsError());
            }
            return Promise.reject(error);
        });
    }
}
function resolveAdapter$1(config) {
    if (typeof config === 'string') {
        return new FileSystemStorageAdapter({ dir: config, name: config });
    }
    const { adapter, ...rest } = config;
    if (adapter != null) {
        return adapter;
    }
    return new FileSystemStorageAdapter({ ...DEFAULTS$1, ...rest });
}
function validateName$1(name) {
    if (!name)
        throw new TypeError('Argument `name` is required.');
}

/**
 * Error thrown when the value loaded from persistent storage cannot be
 * parsed as a {@link IKeyEntry} object.
 */
class InvalidKeyEntryError extends VirgilError {
    constructor(message = 'Loaded key entry was in invalid format.') {
        super(message, 'InvalidKeyEntryError', InvalidKeyEntryError);
    }
}
/**
 * Error thrown from {@link KeyEntryStorage.save} method when saving a
 * a key entry with the name that already exists in store.
 */
class KeyEntryAlreadyExistsError extends VirgilError {
    constructor(name) {
        super(`Key entry ${name ? 'named ' + name : 'with same name'}already exists`, 'KeyEntryAlreadyExistsError', KeyEntryAlreadyExistsError);
    }
}
/**
 * Error thrown from {@link KeyEntryStorage.update} method when updating
 * a key entry that doesn't exist in store.
 */
class KeyEntryDoesNotExistError extends VirgilError {
    constructor(name) {
        super(`Key entry ${name ? 'named ' + name : 'with the given name'} does not exist.`, 'KeyEntryDoesNotExistError', KeyEntryDoesNotExistError);
    }
}

const DEFAULTS = {
    dir: '.virgil_key_entries',
    name: 'VirgilKeyEntries'
};
const CREATION_DATE_KEY = 'creationDate';
const MODIFICATION_DATE_KEY = 'modificationDate';
/**
 * Class responsible for persisting private key bytes with optional
 * user-defined metadata.
 */
class KeyEntryStorage {
    /**
     * Initializes a new instance of `KeyEntryStorage`.
     *
     * @param {IKeyEntryStorageConfig} config - Instance configuration.
     */
    constructor(config = {}) {
        this.adapter = resolveAdapter(config);
    }
    /**
     * @inheritDoc
     */
    exists(name) {
        validateName(name);
        return this.adapter.exists(name);
    }
    /**
     * @inheritDoc
     */
    load(name) {
        validateName(name);
        return this.adapter.load(name).then(data => {
            if (data == null) {
                return null;
            }
            return deserializeKeyEntry(data);
        });
    }
    /**
     * @inheritDoc
     */
    remove(name) {
        validateName(name);
        return this.adapter.remove(name);
    }
    /**
     * @inheritDoc
     */
    save({ name, value, meta }) {
        validateNameProperty(name);
        validateValueProperty(value);
        const keyEntry = {
            name: name,
            value: value,
            meta: meta,
            creationDate: new Date(),
            modificationDate: new Date()
        };
        return this.adapter.store(name, serializeKeyEntry(keyEntry))
            .then(() => keyEntry)
            .catch(error => {
            if (error && error.name === 'StorageEntryAlreadyExistsError') {
                throw new KeyEntryAlreadyExistsError(name);
            }
            throw error;
        });
    }
    /**
     * @inheritDoc
     */
    list() {
        return this.adapter.list()
            .then(entries => entries.map(entry => deserializeKeyEntry(entry)));
    }
    /**
     * @inheritDoc
     */
    update({ name, value, meta }) {
        validateNameProperty(name);
        if (!(value || meta)) {
            throw new TypeError('Invalid argument. Either `value` or `meta` property is required.');
        }
        return this.adapter.load(name)
            .then(data => {
            if (data === null) {
                throw new KeyEntryDoesNotExistError(name);
            }
            const entry = deserializeKeyEntry(data);
            const updatedEntry = Object.assign(entry, {
                value: value || entry.value,
                meta: meta || entry.meta,
                modificationDate: new Date()
            });
            return this.adapter.update(name, serializeKeyEntry(updatedEntry))
                .then(() => updatedEntry);
        });
    }
    /**
     * @inheritDoc
     */
    clear() {
        return this.adapter.clear();
    }
}
function serializeKeyEntry(keyEntry) {
    return JSON.stringify(keyEntry);
}
function deserializeKeyEntry(data) {
    try {
        return JSON.parse(data, (key, value) => {
            if (key === CREATION_DATE_KEY || key === MODIFICATION_DATE_KEY) {
                return new Date(value);
            }
            return value;
        });
    }
    catch (error) {
        throw new InvalidKeyEntryError();
    }
}
function resolveAdapter(config) {
    if (typeof config === 'string') {
        return new FileSystemStorageAdapter({ dir: config, name: config });
    }
    const { adapter, ...rest } = config;
    if (adapter != null) {
        return adapter;
    }
    return new FileSystemStorageAdapter({ ...DEFAULTS, ...rest });
}
const requiredArg = (name) => (value) => {
    if (!value)
        throw new TypeError(`Argument '${name}' is required.`);
};
const requiredProp = (name) => (value) => {
    if (!value)
        throw new TypeError(`Invalid argument. Property ${name} is required`);
};
const validateName = requiredArg('name');
const validateNameProperty = requiredProp('name');
const validateValueProperty = requiredProp('value');

/**
 * Class responsible for storage of private keys.
 */
class PrivateKeyStorage {
    /**
     * Initializes a new instance of `PrivateKeyStorage`.
     * @param {IPrivateKeyExporter} privateKeyExporter - Object responsible for
     * exporting private key bytes from `IPrivateKey` objects and importing
     * private key bytes into `IPrivateKey` objects.
     * @param {IKeyEntryStorage} keyEntryStorage - Object responsible for
     * persistence of private keys data.
     */
    constructor(privateKeyExporter, keyEntryStorage = new KeyEntryStorage()) {
        this.privateKeyExporter = privateKeyExporter;
        this.keyEntryStorage = keyEntryStorage;
    }
    /**
     * Persists the given `privateKey` and `meta` under the given `name`.
     * If an entry with the same name already exists rejects the returned
     * Promise with {@link PrivateKeyExistsError} error.
     *
     * @param {string} name - Name of the private key.
     * @param {IPrivateKey} privateKey - The private key object.
     * @param {Object<string, string>} [meta] - Optional metadata to store with the key.
     *
     * @returns {Promise<void>}
     */
    async store(name, privateKey, meta) {
        const privateKeyData = this.privateKeyExporter.exportPrivateKey(privateKey);
        try {
            await this.keyEntryStorage.save({ name, value: privateKeyData.toString('base64'), meta });
        }
        catch (error) {
            if (error && error.name === 'KeyEntryAlreadyExistsError') {
                throw new PrivateKeyExistsError(`Private key with the name ${name} already exists.`);
            }
            throw error;
        }
    }
    /**
     * Retrieves the private key with the given `name` from persistent storage.
     * If private with the given name does not exist, resolves the returned
     * Promise with `null`.
     *
     * @param {string} name - Name of the private key to load.
     * @returns {Promise<IPrivateKeyEntry|null>}
     */
    async load(name) {
        const keyEntry = await this.keyEntryStorage.load(name);
        if (keyEntry === null) {
            return null;
        }
        const privateKey = this.privateKeyExporter.importPrivateKey(keyEntry.value);
        return {
            privateKey,
            meta: keyEntry.meta
        };
    }
    /**
     * Removes the private key entry with the given `name` from persistent
     * storage.
     *
     * @param {string} name - Name of the private key to remove.
     * @returns {Promise<void>}
     */
    async delete(name) {
        await this.keyEntryStorage.remove(name);
    }
}

exports.CachingJwtProvider = CachingJwtProvider;
exports.CallbackJwtProvider = CallbackJwtProvider;
exports.CardManager = CardManager;
exports.ConstAccessTokenProvider = ConstAccessTokenProvider;
exports.DefaultStorageAdapter = FileSystemStorageAdapter;
exports.GeneratorJwtProvider = GeneratorJwtProvider;
exports.InvalidKeyEntryError = InvalidKeyEntryError;
exports.Jwt = Jwt;
exports.JwtGenerator = JwtGenerator;
exports.JwtVerifier = JwtVerifier;
exports.KeyEntryAlreadyExistsError = KeyEntryAlreadyExistsError;
exports.KeyEntryDoesNotExistError = KeyEntryDoesNotExistError;
exports.KeyEntryStorage = KeyEntryStorage;
exports.KeyStorage = KeyStorage;
exports.ModelSigner = ModelSigner;
exports.PrivateKeyStorage = PrivateKeyStorage;
exports.RawSignedModel = RawSignedModel;
exports.StorageEntryAlreadyExistsError = StorageEntryAlreadyExistsError;
exports.VirgilAgent = VirgilAgent;
exports.VirgilCardVerifier = VirgilCardVerifier;