UNPKG

@fireproof/cloud

Version:

Fireproof Cloud gateway for Fireproof

444 lines (440 loc) 16.8 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/cloud/index.ts var cloud_exports = {}; __export(cloud_exports, { connect: () => connect, rawConnect: () => rawConnect }); module.exports = __toCommonJS(cloud_exports); var import_cement3 = require("@adviser/cement"); var import_core3 = require("@fireproof/core"); // src/connection-from-store.ts var import_cement = require("@adviser/cement"); var import_core = require("@fireproof/core"); var ConnectionFromStore = class extends import_core.bs.ConnectionBase { constructor(sthis, url) { const logger = (0, import_core.ensureLogger)(sthis, "ConnectionFromStore", { url: () => url.toString(), this: 1, log: 1 }); super(url, logger); this.stores = void 0; this.sthis = sthis; } async onConnect() { this.logger.Debug().Msg("onConnect-start"); const stores = { base: this.url // data: this.urlData, // meta: this.urlMeta, }; const rName = this.url.getParamResult("name"); if (rName.isErr()) { throw this.logger.Error().Err(rName).Msg("missing Parameter").AsError(); } const storeRuntime = import_core.bs.toStoreRuntime({ stores }, this.sthis); const loader = { name: rName.Ok(), ebOpts: { logger: this.logger, store: { stores }, storeRuntime }, sthis: this.sthis }; this.stores = { data: await storeRuntime.makeDataStore(loader), meta: await storeRuntime.makeMetaStore(loader) }; this.logger.Debug().Msg("onConnect-done"); return; } }; function connectionFactory(sthis, iurl) { return new ConnectionFromStore(sthis, import_cement.URI.from(iurl)); } function makeKeyBagUrlExtractable(sthis) { let base = sthis.env.get("FP_KEYBAG_URL"); if (!base) { if ((0, import_cement.runtimeFn)().isBrowser) { base = "indexdb://fp-keybag"; } else { base = "file://./dist/kb-dir-partykit"; } } const kbUrl = import_cement.BuildURI.from(base); kbUrl.defParam("extractKey", "_deprecated_internal_api"); sthis.env.set("FP_KEYBAG_URL", kbUrl.toString()); sthis.logger.Debug().Url(kbUrl, "keyBagUrl").Msg("Make keybag url extractable"); } // src/cloud/gateway.ts var import_partysocket = require("partysocket"); var import_cement2 = require("@adviser/cement"); var import_core2 = require("@fireproof/core"); var FireproofCloudGateway = class { constructor(sthis) { this.subscriberCallbacks = /* @__PURE__ */ new Set(); this.sthis = sthis; this.id = sthis.nextId().str; this.logger = (0, import_core2.ensureLogger)(sthis, "FireproofCloudGateway", { url: () => this.url?.toString(), this: this.id }); this.logger.Debug().Msg("constructor"); } async buildUrl(baseUrl, key) { return import_cement2.Result.Ok(baseUrl.build().setParam("key", key).URI()); } async start(uri) { this.logger.Debug().Msg("Starting FireproofCloudGateway with URI: " + uri.toString()); await this.sthis.start(); this.url = uri; const ret = uri.build().defParam("version", "v0.1-fireproof-cloud"); const rName = uri.getParamResult("name"); if (rName.isErr()) { return this.logger.Error().Err(rName).Msg("name not found").ResultError(); } let dbName = rName.Ok(); if (this.url.hasParam("index")) { dbName = dbName + "-idx"; } ret.defParam("party", "fireproof"); ret.defParam("protocol", "wss"); let possibleUndef = { protocol: ret.getParam("protocol") }; const protocolsStr = uri.getParam("protocols"); if (protocolsStr) { const ps = protocolsStr.split(",").map((x) => x.trim()).filter((x) => x); if (ps.length > 0) { possibleUndef = { ...possibleUndef, protocols: ps }; } } const prefixStr = uri.getParam("prefix"); if (prefixStr) { possibleUndef = { ...possibleUndef, prefix: prefixStr }; } const query = {}; const partySockOpts = { id: this.id, host: this.url.host, room: dbName, party: ret.getParam("party"), ...possibleUndef, query, path: this.url.pathname.replace(/^\//, "") }; if ((0, import_cement2.runtimeFn)().isNodeIsh) { const { WebSocket } = await import("ws"); partySockOpts.WebSocket = WebSocket; } this.pso = partySockOpts; return import_cement2.Result.Ok(ret.URI()); } async ready() { this.logger.Debug().Msg("ready"); } async connectFireproofCloud() { const pkKeyThis = pkKey(this.pso); return pkSockets.get(pkKeyThis).once(async () => { if (!this.pso) { throw new Error("Party socket options not found"); } this.party = new import_partysocket.PartySocket(this.pso); let exposedResolve; const openFn = () => { this.logger.Debug().Msg("party open"); this.party?.addEventListener("message", async (event) => { this.logger.Debug().Msg(`got message: ${event.data}`); const mbin = this.sthis.txt.encode(event.data); this.notifySubscribers(mbin); }); exposedResolve(true); }; return await new Promise((resolve) => { exposedResolve = resolve; this.party?.addEventListener("open", openFn); }); }); } async close() { await this.ready(); this.logger.Debug().Msg("close"); this.party?.close(); return import_cement2.Result.Ok(void 0); } async put(uri, body) { await this.ready(); const { store } = (0, import_core2.getStore)(uri, this.sthis, (...args) => args.join("/")); if (store === "meta") { const bodyRes = await import_core2.bs.addCryptoKeyToGatewayMetaPayload(uri, this.sthis, body); if (bodyRes.isErr()) { this.logger.Error().Err(bodyRes.Err()).Msg("Error in addCryptoKeyToGatewayMetaPayload"); throw bodyRes.Err(); } body = bodyRes.Ok(); } const rkey = uri.getParamResult("key"); if (rkey.isErr()) return import_cement2.Result.Err(rkey.Err()); const key = rkey.Ok(); if (store === "meta") { const uploadUrl = pkMetaURL(uri, key); return (0, import_cement2.exception2Result)(async () => { const response = await fetch(uploadUrl.asURL(), { method: "PUT", body }); if (response.status === 404) { throw this.logger.Error().Url(uploadUrl).Msg(`Failure in uploading ${store}!`).AsError(); } }); } else { const uploadUrl = pkURL(uri, key, "car"); return (0, import_cement2.exception2Result)(async () => { const response = await fetch(uploadUrl.asURL(), { method: "PUT" }); this.logger.Debug().Url(uploadUrl).Uint64("status", response.status).Str("status-text", response.statusText).Msg("put"); if (response.status === 404) { throw this.logger.Error().Url(uploadUrl).Msg(`Failure in uploading ${store}!`).AsError(); } const url = (await response.json()).url; this.logger.Debug().Url(url).Msg("put"); const uploadResponse = await fetch(url, { method: "PUT", body }); if (uploadResponse.status === 404) { throw this.logger.Error().Url(uploadUrl).Msg(`Failure in uploading ${store}!`).AsError(); } }); } } notifySubscribers(data) { for (const callback of this.subscriberCallbacks) { try { callback(data); } catch (error) { this.logger.Error().Err(error).Msg("Error in subscriber callback execution"); } } } async subscribe(uri, callback) { await this.ready(); await this.connectFireproofCloud(); const store = uri.getParam("store"); if (store !== "meta") { return import_cement2.Result.Err(new Error("store must be meta")); } this.subscriberCallbacks.add(callback); return import_cement2.Result.Ok(() => { this.subscriberCallbacks.delete(callback); }); } async get(uri) { await this.ready(); return (0, import_cement2.exception2Result)(async () => { const { store } = (0, import_core2.getStore)(uri, this.sthis, (...args) => args.join("/")); const key = uri.getParam("key"); if (!key) throw new Error("key not found"); let downloadUrl; this.logger.Debug().Str("store", store).Str("key", key).Msg("get"); switch (store) { case "meta": downloadUrl = pkMetaURL(uri, key); break; case "data": downloadUrl = pkCarGetURL(uri, key); break; default: throw new Error(`Unsupported store: ${store}`); } const response = await fetch(downloadUrl.toString(), { method: "GET" }); if (response.status === 404) { throw new Error(`Failure in downloading ${store}!`); } const body = new Uint8Array(await response.arrayBuffer()); if (store === "meta") { const resKeyInfo = await import_core2.bs.setCryptoKeyFromGatewayMetaPayload(uri, this.sthis, body); if (resKeyInfo.isErr()) { this.logger.Error().Url(uri).Err(resKeyInfo).Any("body", body).Msg("Error in setCryptoKeyFromGatewayMetaPayload"); throw resKeyInfo.Err(); } } return body; }); } async delete(_uri) { await this.ready(); throw new Error("no delete for fireproof cloud"); } async destroy(uri) { await this.ready(); return (0, import_cement2.exception2Result)(async () => { const deleteUrl = pkBaseURL(uri); const response = await fetch(deleteUrl.asURL(), { method: "DELETE" }); if (response.status === 404) { throw new Error("Failure in deleting data!"); } return import_cement2.Result.Ok(void 0); }); } }; var pkSockets = new import_cement2.KeyedResolvOnce(); function pkKey(set) { const ret = JSON.stringify( Object.entries(set || {}).sort(([a], [b]) => a.localeCompare(b)).filter(([k]) => k !== "id").map(([k, v]) => ({ [k]: v })) ); return ret; } function pkURL(uri, key, type) { const host = uri.host; const name = uri.getParam("name"); const idx = uri.getParam("index") || ""; const protocol = uri.getParam("protocol") === "ws" ? "http" : "https"; const path = `/parties/fireproof/${name}${idx}`; return import_cement2.BuildURI.from(`${protocol}://${host}${path}`).setParam(type, key).URI(); } function pkBaseURL(uri) { const host = uri.host; const name = uri.getParam("name"); const idx = uri.getParam("index") || ""; const protocol = uri.getParam("protocol") === "ws" ? "http" : "https"; const path = `/parties/fireproof/${name}${idx}`; return import_cement2.BuildURI.from(`${protocol}://${host}${path}`).URI(); } function pkCarGetURL(uri, key) { const baseUrl = uri.getParam("getBaseUrl"); if (!baseUrl) { return pkURL(uri, key, "car"); } const name = uri.getParam("name"); const idx = uri.getParam("index") || ""; const baseUri = import_cement2.URI.from(baseUrl).asURL(); baseUri.pathname = `/${name}${idx}/${key}`; return import_cement2.BuildURI.from(baseUri).URI(); } function pkMetaURL(uri, key) { return pkURL(uri, key, "meta"); } var FireproofCloudTestStore = class { constructor(gw, sthis) { this.sthis = sthis; this.logger = (0, import_core2.ensureLogger)(sthis, "FireproofCloudTestStore"); this.gateway = gw; } async get(uri, key) { const url = uri.build().setParam("key", key).URI(); const dbFile = this.sthis.pathOps.join(import_core2.rt.getPath(url, this.sthis), import_core2.rt.getFileName(url, this.sthis)); this.logger.Debug().Url(url).Str("dbFile", dbFile).Msg("get"); const buffer = await this.gateway.get(url); this.logger.Debug().Url(url).Str("dbFile", dbFile).Len(buffer).Msg("got"); return buffer.Ok(); } }; var onceRegisterFireproofCloudStoreProtocol = new import_cement2.KeyedResolvOnce(); function registerFireproofCloudStoreProtocol(protocol = "fireproof:", overrideBaseURL) { return onceRegisterFireproofCloudStoreProtocol.get(protocol).once(() => { import_cement2.URI.protocolHasHostpart(protocol); return import_core2.bs.registerStoreProtocol({ protocol, overrideBaseURL, gateway: async (sthis) => { return new FireproofCloudGateway(sthis); }, test: async (sthis) => { const gateway = new FireproofCloudGateway(sthis); return new FireproofCloudTestStore(gateway, sthis); } }); }); } // src/cloud/index.ts var SYNC_DB_NAME = "fp_sync"; if (!(0, import_cement3.runtimeFn)().isBrowser) { const url = import_cement3.BuildURI.from(process.env.FP_KEYBAG_URL || "file://./dist/kb-dir-FireproofCloud"); url.setParam("extractKey", "_deprecated_internal_api"); process.env.FP_KEYBAG_URL = url.toString(); } registerFireproofCloudStoreProtocol(); var connectionCache = new import_cement3.KeyedResolvOnce(); var rawConnect = (db, remoteDbName = "", url = "fireproof://cloud.fireproof.direct") => { const { sthis, blockstore, name: dbName } = db; if (!dbName) { throw new Error("dbName is required"); } const urlObj = import_cement3.BuildURI.from(url); const existingName = urlObj.getParam("name"); urlObj.defParam("name", remoteDbName || existingName || dbName); urlObj.defParam("localName", dbName); urlObj.defParam("storekey", `@${dbName}:data@`); urlObj.defParam("getBaseUrl", "https://storage.fireproof.direct/"); const fpUrl = urlObj.toString().replace(/^http:\/\//, "fireproof://").replace(/^https:\/\//, "fireproof://"); return connectionCache.get(fpUrl).once(() => { makeKeyBagUrlExtractable(sthis); const connection = connectionFactory(sthis, fpUrl); connection.connect_X(blockstore); return connection; }); }; async function getOrCreateRemoteName(dbName, remoteName) { const syncDb = (0, import_core3.fireproof)(SYNC_DB_NAME); const result = await syncDb.query("localName", { key: dbName, includeDocs: true }); if (result.rows.length === 0) { const doc2 = { remoteName: remoteName || syncDb.sthis.timeOrderedNextId().str, localName: dbName, firstConnect: !remoteName }; const { id } = await syncDb.put(doc2); return { ...doc2, _id: id }; } const doc = result.rows[0].doc; return doc; } function connect(db, remoteName, dashboardURI = "https://dashboard.fireproof.storage/", remoteURI = "fireproof://cloud.fireproof.direct") { const dbName = db.name; if (!dbName) { throw new Error("Database name is required for cloud connection"); } return getOrCreateRemoteName(dbName, remoteName).then(async (doc) => { if (!doc) { throw new Error("Failed to get or create remote name"); } doc.endpoint = import_cement3.URI.from(remoteURI).toString(); const connection = rawConnect(db, doc.remoteName, import_cement3.URI.from(doc.endpoint).toString()); const connectURI = import_cement3.URI.from(dashboardURI).build().pathname("/fp/databases/connect"); connectURI.defParam("localName", dbName); connectURI.defParam("remoteName", doc.remoteName); if (doc.endpoint) { connectURI.defParam("endpoint", doc.endpoint); } console.log("Fireproof Cloud: " + connectURI.toString()); if (doc.firstConnect && (0, import_cement3.runtimeFn)().isBrowser && window.location.href.indexOf(import_cement3.URI.from(dashboardURI).toString()) === -1) { const syncDb = (0, import_core3.fireproof)(SYNC_DB_NAME); doc.firstConnect = false; await syncDb.put(doc); } connection.dashboardUrl = import_cement3.URI.from(connectURI); return connection; }); } //# sourceMappingURL=index.cjs.map