"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;