index.js

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
exports.__esModule = true;
var client_js_1 = __importDefault(require("../rpc-websocket/client.js"));
var wasm_key_manager_1 = require("@holo-host/wasm-key-manager");
var async_with_timeout = require('../rpc-websocket/async_with_timeout.js');
var TimeoutError = async_with_timeout.TimeoutError;
var DEFAULT_TIMEOUT = 5000;
var IS_BROWSER = (typeof window === 'object'
    && window.constructor.name === 'Window');
// if ( IS_BROWSER ) {
//     window.global			= (global as any);
// }
// These lines stop Typescript from complaining that these globals do not exist.  In the browser,
// they will exist, and in Node.js the importer must define these globals.  For example:
//
// ```js
// global.COMB = {
//     "connect": () => null,
//     "listen": () => null,
// }
// global.crypto = require('crypto');
// ```
//
// COMB is mocked because it can only work for real in a browser context.
var COMB = global.COMB || window.COMB;
var crypto = global.crypto || window.crypto;
var fetch = global.fetch || window.fetch;
function randomBytes(length) {
    if (length === void 0) { length = 32; }
    if (IS_BROWSER) {
        var array = new Uint8Array(length);
        window.crypto.getRandomValues(array);
        return array;
    }
    else {
        return crypto.randomBytes(length);
    }
}
function resolveHostsForHashAgent(hash, agent_id) {
    return __awaiter(this, void 0, void 0, function () {
        var params, resp, data;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    params = {
                        "hash": hash
                    };
                    if (agent_id === false)
                        params.anonymous = true;
                    else
                        params.agent_id = agent_id;
                    return [4 /*yield*/, fetch("https://resolver.holohost.net", {
                            method: "POST",
                            headers: {
                                "Content-Type": "application/json"
                            },
                            body: JSON.stringify(params)
                        })];
                case 1:
                    resp = _a.sent();
                    return [4 /*yield*/, resp.json()];
                case 2:
                    data = _a.sent();
                    return [2 /*return*/, arrayPickRandom(data.hosts)];
            }
        });
    });
}
function resolveHostname2Hash(hostname) {
    return __awaiter(this, void 0, void 0, function () {
        var resp, data;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    if (typeof hostname !== "string")
                        throw new Error("Bad input: hostname paramater '" + hostname + "' (typeof " + (typeof hostname) + ") must be a string");
                    return [4 /*yield*/, fetch("https://resolver.holohost.net/resolve/hostname", {
                            method: "POST",
                            headers: {
                                "Content-Type": "application/json"
                            },
                            body: JSON.stringify({
                                "url": hostname
                            })
                        })];
                case 1:
                    resp = _a.sent();
                    return [4 /*yield*/, resp.json()];
                case 2:
                    data = _a.sent();
                    return [2 /*return*/, data.hash];
            }
        });
    });
}
function arrayPickRandom(list) {
    return list[Math.floor(Math.random() * list.length)];
}
/**
 * @module @holo-host/chaperone
 *
 * @description
 * Chaperone has one instance that is automatically initiated when the script is loaded.
 * ```html
 * <script type="text/javascript" src="./holo_hosting_chaperone.js"></script>
 * <script type="text/javascript">
 * (async () => {
 *     await chaperone.ready();
 *
 *     chaperone.instance_prefix = "some_instance_prefix";
 *
 *     let response = await chaperone.callZomeFunction( "dna_alias", "zome_name", "func_name", {} );
 * })();
 * </script>
 * ```
 *
 */
var Chaperone = /** @class */ (function () {
    /**
     * Chaperone manages the connection(s), keys, and iframe message bus for a hApp user.
     *
     * @class Chaperone
     *
     * @param {number} port			- Set port property (default: 4656)
     *
     * @prop {object} COMB			- Local access to COMB library
     *
     * @prop {buffer} seed			- 32 bytes used for KeyPair seed
     * @prop {object} keys			- KeyManager instance
     * @prop {string} agent_id			- `hcs0` encoding of public key
     * @prop {boolean} anonymous		- True when `this.seed` is random bytes
     *
     * @prop {string} instance_prefix		- Used to guarantee instance IDs are unique (HHA ID for Envoy / hApp ID for Conductor)
     * @prop {string} hha_hash			- Holo Hosting App registered hApp Hash
     *
     * @prop {object} conn			- Current RPC WebSocket instance
     * @prop {string} host			- WebSocket server host
     * @prop {number} port			- WebSocket server port
     * @prop {boolean} opened			- True when WebSocket is in `readyState` === `OPEN`
     * @prop {boolean} wormhole_ready		- True when the wormhole listener has been established
     * @prop {object} wormhole_listeners	- Internal map of promises waiting for wormhole requests
     *
     * @example
     * const chaperone = new Chaperone();
     * await chaperone.ready();
     * chaperone.instance_prefix = "some_instance_prefix";
     */
    function Chaperone(opts) {
        if (opts === void 0) { opts = Chaperone.DEFAULTS; }
        this.anonymous = true;
        this.opened = false;
        this.wormhole_ready = false;
        this.wormhole_listeners = {};
        var _a = Object.assign({}, Chaperone.DEFAULTS, opts), mode = _a.mode, port = _a.port, timeout = _a.timeout;
        this.port = port;
        this.mode = mode;
        this.COMB = COMB;
        this.opts = opts;
        if (!this.COMB
            || typeof this.COMB.connect !== 'function'
            || typeof this.COMB.listen !== 'function')
            throw new Error("COMB is not the library we expected");
        this.init(timeout);
    }
    /**
     * Create an anonymous key pair.  Select a host and make a WebSocket connection.  Once the
     * WebSocket is in `readyState = OPEN`, register and subscribe for wormhole requests which are
     * automatically signed and returned to host.  By the time `init` is returns, the connection and
     * wormhole are ready for use.
     *
     * @async
     * @private
     *
     * @param {number} timeout		- Timeout for WebSocket connection
     *
     * @return {void} Return when setup is complete
     *
     * @example
     * await this.init( 5000 );
     * chaperone.opened         === true
     * chaperone.wormhole_ready === true
     * chaperone.anonymous      === true
     */
    Chaperone.prototype.init = function (timeout) {
        if (timeout === void 0) { timeout = DEFAULT_TIMEOUT; }
        return __awaiter(this, void 0, void 0, function () {
            var happ_host, _a;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        if (!(this.mode === Chaperone.DEVELOP)) return [3 /*break*/, 1];
                        this.agent_id = this.opts.agent_id;
                        this.hha_hash = this.opts.instance_prefix;
                        this.instance_prefix = this.opts.instance_prefix;
                        return [3 /*break*/, 3];
                    case 1:
                        this.seed = randomBytes(32);
                        this.keys = new wasm_key_manager_1.KeyManager(this.seed);
                        this.agent_id = this.keys.agentId();
                        happ_host = this.happHost();
                        _a = this;
                        return [4 /*yield*/, resolveHostname2Hash(happ_host)];
                    case 2:
                        _a.hha_hash = _b.sent();
                        this.instance_prefix = this.hha_hash;
                        _b.label = 3;
                    case 3:
                        console.log("Initialize Chaperone", this.COMB);
                        return [4 /*yield*/, this.connect(timeout)];
                    case 4:
                        _b.sent();
                        return [4 /*yield*/, this.handleWormholeRequests()];
                    case 5:
                        _b.sent();
                        return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Get a valid Host for a given hostname or hApp ID (and Agent ID if signed in), and then create
     * a WebSocket connection to that host.
     *
     * @async
     * @private
     *
     * @param {number} timeout		- Timeout for WebSocket connection
     *
     * @return {void} Return when connection is open
     *
     * @example
     * await this.connect( 5000 );
     * chaperone.opened === true
     */
    Chaperone.prototype.connect = function (timeout) {
        if (timeout === void 0) { timeout = DEFAULT_TIMEOUT; }
        return __awaiter(this, void 0, void 0, function () {
            var _a;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        if (this.opened === true)
                            throw new Error("Connection already open, run `await this.disconnect()` before starting a new connection.");
                        if (!(this.mode === Chaperone.DEVELOP)) return [3 /*break*/, 1];
                        this.host = "localhost";
                        return [3 /*break*/, 3];
                    case 1:
                        _a = this;
                        return [4 /*yield*/, resolveHostsForHashAgent(this.hha_hash, this.anonymous === true
                                ? false
                                : this.agent_id)];
                    case 2:
                        _a.host = _b.sent();
                        _b.label = 3;
                    case 3:
                        this.conn = new client_js_1["default"]("ws://" + this.host + ":" + this.port);
                        return [4 /*yield*/, this.conn.opened(timeout)];
                    case 4:
                        _b.sent();
                        this.opened = true;
                        return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Close current connection.
     *
     * @async
     * @private
     *
     * @return {void} Return when connection is closed
     *
     * @example
     * await this.disconnect();
     * chaperone.opened === false
     */
    Chaperone.prototype.disconnect = function () {
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (this.opened === false)
                            return [2 /*return*/]; // Already closed
                        this.conn.close();
                        return [4 /*yield*/, this.conn.closed()];
                    case 1:
                        _a.sent();
                        this.opened = false;
                        return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Async method that returns when the WebSocket and wormhole signing is setup.
     *
     * @async
     *
     * @param {number} timeout		- *Unused timeout*
     *
     * @return {this} Return self
     *
     * @example
     * try {
     *     await chaperone.ready( 5000 );
     * } catch ( err ) {
     *     // Timeout
     * }
     */
    Chaperone.prototype.ready = function (timeout) {
        if (timeout === void 0) { timeout = DEFAULT_TIMEOUT; }
        return __awaiter(this, void 0, void 0, function () {
            var id;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        _a.trys.push([0, , 2, 3]);
                        return [4 /*yield*/, async_with_timeout(function () { return __awaiter(_this, void 0, void 0, function () {
                                var _this = this;
                                return __generator(this, function (_a) {
                                    return [2 /*return*/, new Promise(function (f, _) {
                                            id = setInterval(function () { return __awaiter(_this, void 0, void 0, function () {
                                                return __generator(this, function (_a) {
                                                    if (!this.conn)
                                                        return [2 /*return*/]; // we are not ready to check yet
                                                    if (this.opened !== true)
                                                        return [2 /*return*/, console.log("WebSocket is not open")];
                                                    else if (this.wormhole_ready !== true)
                                                        return [2 /*return*/, console.log("Wormhole is not ready")];
                                                    else
                                                        f(this);
                                                    return [2 /*return*/];
                                                });
                                            }); }, 100);
                                        })];
                                });
                            }); }, timeout)];
                    case 1: return [2 /*return*/, _a.sent()];
                    case 2:
                        clearInterval(id);
                        return [7 /*endfinally*/];
                    case 3: return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Get the signature of a specific wormhole request.  Returns after wormhole response.
     *
     * **NOTE:** The original use case for this method is unit testing.  It may not be useful
         outside of the testing context.
     *
     * @async
     *
     * @param {number} id		- Wormhole request ID
     *
     * @return {string} Signature of wormhole request
     *
     * @example
     * let [signature,data] = chaperone.wormholeRequest( 0 );
     * // Signature is equivalent to
     * signature === chaperone.getSignature( data );
     */
    Chaperone.prototype.wormholeRequest = function (id) {
        var _this = this;
        return new Promise(function (f, r) {
            _this.wormhole_listeners[id] = [f, r];
        });
    };
    /**
     * Set up wormhole handler for RPC WebSocket connection.  Once complete, `this.wormhole_ready`
     * will be set to `true`.
     *
     * @async
     * @private
     *
     * @return {void} Returns after subscription is confirmed by server
     */
    Chaperone.prototype.handleWormholeRequests = function () {
        return __awaiter(this, void 0, void 0, function () {
            var signing_request;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (this.mode === Chaperone.DEVELOP) {
                            this.wormhole_ready = true;
                            return [2 /*return*/];
                        }
                        signing_request = this.agent_id + "/wormhole/request";
                        console.log("Register Agent: %s", this.agent_id);
                        return [4 /*yield*/, this.conn.call("holo/register/agent", [this.agent_id])];
                    case 1:
                        _a.sent();
                        console.log("Subscribe to %s", signing_request);
                        return [4 /*yield*/, this.conn.subscribe(signing_request)];
                    case 2:
                        _a.sent();
                        console.log("Register listener on %s", signing_request);
                        this.conn.on(signing_request, function (id, data) { return __awaiter(_this, void 0, void 0, function () {
                            var signature, response, _a, f, r;
                            return __generator(this, function (_b) {
                                switch (_b.label) {
                                    case 0:
                                        console.log("Signing log: received -", id, data);
                                        signature = this.getSignature(data);
                                        console.log("Signing log: sending -", id, signature);
                                        return [4 /*yield*/, this.conn.call("holo/wormhole/response", [id, signature])];
                                    case 1:
                                        response = _b.sent();
                                        if (response !== true)
                                            console.error("holo/wormhole/response failed:", response);
                                        if (this.wormhole_listeners[id] !== undefined) {
                                            _a = this.wormhole_listeners[id], f = _a[0], r = _a[1];
                                            console.log("Trigger wormhole listener", id);
                                            f([signature, data]);
                                            delete this.wormhole_listeners[id];
                                        }
                                        return [2 /*return*/];
                                }
                            });
                        }); });
                        this.wormhole_ready = true;
                        return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Create a derived seed from the given credentials and re-initialize connection process.
     * Returns once the new WebSocket connection is in `readyState = OPEN`.
     *
     * @async
     *
     * @param {string} email		- Agent's email address
     * @param {string} password		- Agent's password
     *
     * @return {void} Returns when new connection is open
     *
     * @example
     * await chaperone.signIn( "someone@example.com" , "Passw0rd!" );
     * // chaperone is now connected to a host for signed in user
     * chaperone.anonymous === false
     */
    Chaperone.prototype.signIn = function (email, password, _a) {
        var _b = (_a === void 0 ? {} : _a).timeout, timeout = _b === void 0 ? DEFAULT_TIMEOUT : _b;
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        if (this.mode === Chaperone.DEVELOP) {
                            this.anonymous = false;
                            return [2 /*return*/];
                        }
                        this.seed = Buffer.from(wasm_key_manager_1.KeyManager.deriveSeed(this.hha_hash, email, password));
                        this.keys = new wasm_key_manager_1.KeyManager(this.seed);
                        this.agent_id = this.keys.agentId();
                        this.anonymous = false;
                        return [4 /*yield*/, this.disconnect()];
                    case 1:
                        _c.sent();
                        return [4 /*yield*/, this.connect(timeout)];
                    case 2:
                        _c.sent();
                        return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Call a zome function on the connected Host with given parameters.  `instance_id` is derived
     * by combining `this.agent_id`, `this.instance_prefix`, and the given `dna_alias`.  When signed
     * in, the Agent ID and payload signature will be added to every request.
     *
     * @async
     *
     * @param {string} dna_alias	- DNA alias (used for instance ID)
     * @param {string} zome_name	- Zome name
     * @param {string} function_name	- Zome function name
     * @param {object} args		- Function arguments
     *
     * @return {*} Response from Host
     *
     * @example
     * let data = await chaperone.callZomeFunction( "holofuel", "transactions", "list_pending" );
     */
    Chaperone.prototype.callZomeFunction = function (dna_alias, zome_name, function_name, args) {
        if (args === void 0) { args = {}; }
        return __awaiter(this, void 0, void 0, function () {
            var inst_prefix, payload, request;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        inst_prefix = this.instance_prefix;
                        if (typeof inst_prefix !== 'string')
                            throw new Error("Instance prefix should be a string.  Found '" + inst_prefix + "' (" + (typeof inst_prefix) + ")");
                        payload = {
                            "instance_id": inst_prefix + "::" + dna_alias,
                            "zome": zome_name,
                            "function": function_name,
                            "args": args
                        };
                        // When signed in, include Agent ID in the instance ID
                        if (this.anonymous === false)
                            payload.instance_id = inst_prefix + "::" + this.agent_id + "-" + dna_alias;
                        if (!(this.mode === Chaperone.DEVELOP)) return [3 /*break*/, 2];
                        return [4 /*yield*/, this.conn.call("call", payload)];
                    case 1: return [2 /*return*/, _a.sent()];
                    case 2:
                        request = {
                            "agent_id": this.agentId(),
                            "signature": this.anonymous === true
                                ? false
                                : this.getSignature(payload),
                            payload: payload
                        };
                        return [4 /*yield*/, this.conn.call("holo/call", request)];
                    case 3: return [2 /*return*/, _a.sent()];
                }
            });
        });
    };
    /**
     * Shutdown any connections and change `this.opened` to `false`.
     *
     * @async
     *
     * @return {void} Returns after connections are closed
     *
     * @example
     * await chaperone.close();
     * chaperone.opened === false
     */
    Chaperone.prototype.close = function () {
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, this.disconnect()];
                    case 1:
                        _a.sent();
                        return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Get the `WebSocket` instance for our current connection.  RPC WebSocket is a class that wraps
     * the real WebSocket connection.  Unfortunately, the `rpc-websockets` module stores the socket
     * differently on web vs node.js implementations.
     *
     * @return {WebSocket} The real WebSocket instance
     *
     * @example
     * let ws = chaperone.websocket();
     * ws.constructor.name === "WebSocket";
     */
    Chaperone.prototype.websocket = function () {
        if (!this.opened)
            console.log("WARN: Returned WebSocket is not open and may be replaced by rpc-websocket library once open");
        // 'rpc-websockets' is not consistent between their node and web implementations.  The
        // actually WebSocket object is burried in '.socket.socket' in a browser context.
        return IS_BROWSER ? this.conn.socket.socket : this.conn.socket;
    };
    /**
     * Get the Agent ID of the signed in user.  Returns "anonymous" if user is not signed in.
     *
     * @return {string} Agent ID
     *
     * @example
     * let agent_id = chaperone.agentId();
     */
    Chaperone.prototype.agentId = function () {
        if (typeof this.agent_id !== 'string')
            throw new Error("Agend ID should be a string starting with HcS.  Found '" + this.agent_id + "' (" + (typeof this.agent_id) + ")");
        return this.anonymous === true ? "anonymous" : this.agent_id;
    };
    /**
     * Get the Host address of the parent window (hApp UI).
     *
     * @private
     *
     * @return {string} Domain
     *
     * @example
     * let happ_host = this.happHost();
     */
    Chaperone.prototype.happHost = function () {
        try {
            var url = window.location != window.parent.location
                ? document.referrer
                : document.location.href;
            return (new URL(url)).host;
        }
        catch (err) {
            console.error(err);
        }
    };
    /**
     * Get the base64 encoded signature for a given data object.  Converts objects using
     * `JSON.stringify` when given data is not a `string`.
     *
     * @param {string|object} data	- Data to be signed
     *
     * @return {string} Signature
     *
     * @example
     * let signature = chaperone.getSignature( { "email": "someone@example.com" );
     */
    Chaperone.prototype.getSignature = function (data) {
        var text = typeof data === "string" ? data : JSON.stringify(data);
        console.log(this.keys);
        var signature = this.keys.sign(text);
        return Buffer.from(signature).toString("base64");
    };
    Chaperone.PRODUCT = 0;
    Chaperone.DEVELOP = 1;
    Chaperone.DEFAULTS = {
        mode: Chaperone.PRODUCT,
        port: 4656,
        timeout: DEFAULT_TIMEOUT
    };
    return Chaperone;
}());
exports.Chaperone = Chaperone;