UNPKG

etebase

Version:

Etebase TypeScript API for the web and node

498 lines 16.9 kB
import request from "./Request"; import WebSocket from "isomorphic-ws"; import ReconnectingWebSocket from "reconnecting-websocket"; import URI from "urijs"; export { deriveKey, ready } from "./Crypto"; import { HttpError, UnauthorizedError, PermissionDeniedError, ConflictError, NetworkError, ProgrammingError, NotFoundError, TemporaryServerError, ServerError } from "./Exceptions"; export * from "./Exceptions"; import { msgpackEncode, msgpackDecode, toString } from "./Helpers"; import { EncryptedCollection, EncryptedCollectionItem, } from "./EncryptedModels"; export var PrefetchOption; (function (PrefetchOption) { PrefetchOption["Auto"] = "auto"; PrefetchOption["Medium"] = "medium"; })(PrefetchOption || (PrefetchOption = {})); function urlExtend(baseUrlIn, segments) { const baseUrl = baseUrlIn.clone(); for (const segment of segments) { baseUrl.segment(segment); } baseUrl.segment(""); return baseUrl.normalize(); } class BaseNetwork { constructor(apiBase) { this.apiBase = URI(apiBase).normalize(); } async newCall(segments = [], extra = {}, apiBaseIn = this.apiBase) { const apiBase = urlExtend(apiBaseIn, segments); extra = { ...extra, headers: { Accept: "application/msgpack", ...extra.headers, }, }; let response; try { response = await request(apiBase.toString(), extra); } catch (e) { throw new NetworkError(e.message); } const body = response.body; let data; let strError = undefined; try { data = msgpackDecode(body); } catch (e) { data = new Uint8Array(body); try { strError = toString(data); } catch (_a) { // Ignore } } if (response.ok) { return data; } else { const content = data.detail || data.non_field_errors || strError; switch (response.status) { case 401: throw new UnauthorizedError(content, data); case 403: throw new PermissionDeniedError(content); case 404: throw new NotFoundError(content); case 409: throw new ConflictError(content); case 502: case 503: case 504: throw new TemporaryServerError(response.status, content, data); default: { if ((response.status >= 500) && (response.status <= 599)) { throw new ServerError(response.status, content, data); } else { throw new HttpError(response.status, content, data); } } } } } } export class Authenticator extends BaseNetwork { constructor(apiBase) { super(apiBase); this.apiBase = urlExtend(this.apiBase, ["api", "v1", "authentication"]); } async isEtebase() { try { await this.newCall(["is_etebase"]); return true; } catch (e) { if (e instanceof NotFoundError) { return false; } throw e; } } async signup(user, salt, loginPubkey, pubkey, encryptedContent) { user = { username: user.username, email: user.email, }; const extra = { method: "post", headers: { "Content-Type": "application/msgpack", }, body: msgpackEncode({ user, salt: salt, loginPubkey: loginPubkey, pubkey: pubkey, encryptedContent: encryptedContent, }), }; return this.newCall(["signup"], extra); } getLoginChallenge(username) { const extra = { method: "post", headers: { "Content-Type": "application/msgpack", }, body: msgpackEncode({ username }), }; return this.newCall(["login_challenge"], extra); } login(response, signature) { const extra = { method: "post", headers: { "Content-Type": "application/msgpack", }, body: msgpackEncode({ response: response, signature: signature, }), }; return this.newCall(["login"], extra); } logout(authToken) { const extra = { method: "post", headers: { "Content-Type": "application/msgpack", "Authorization": "Token " + authToken, }, }; return this.newCall(["logout"], extra); } async changePassword(authToken, response, signature) { const extra = { method: "post", headers: { "Content-Type": "application/msgpack", "Authorization": "Token " + authToken, }, body: msgpackEncode({ response: response, signature: signature, }), }; await this.newCall(["change_password"], extra); } async getDashboardUrl(authToken) { const extra = { method: "post", headers: { "Content-Type": "application/msgpack", "Authorization": "Token " + authToken, }, }; const ret = await this.newCall(["dashboard_url"], extra); return ret.url; } } class BaseManager extends BaseNetwork { constructor(etebase, segments) { super(etebase.serverUrl); this.etebase = etebase; this.apiBase = urlExtend(this.apiBase, ["api", "v1"].concat(segments)); } newCall(segments = [], extra = {}, apiBase = this.apiBase) { extra = { ...extra, headers: { "Content-Type": "application/msgpack", "Authorization": "Token " + this.etebase.authToken, ...extra.headers, }, }; return super.newCall(segments, extra, apiBase); } urlFromFetchOptions(options) { if (!options) { return this.apiBase; } const { stoken, prefetch, limit, withCollection, iterator } = options; return this.apiBase.clone().search({ stoken: (stoken !== null) ? stoken : undefined, iterator: (iterator !== null) ? iterator : undefined, limit: (limit && (limit > 0)) ? limit : undefined, withCollection: withCollection, prefetch, }); } } export class CollectionManagerOnline extends BaseManager { constructor(etebase) { super(etebase, ["collection"]); } async fetch(colUid, options) { const apiBase = this.urlFromFetchOptions(options); const json = await this.newCall([colUid], undefined, apiBase); return EncryptedCollection.deserialize(json); } async list(collectionTypes, options) { const apiBase = this.urlFromFetchOptions(options); const extra = { method: "post", body: msgpackEncode({ collectionTypes }), }; const json = await this.newCall(["list_multi"], extra, apiBase); return { ...json, data: json.data.map((val) => EncryptedCollection.deserialize(val)), }; } create(collection, options) { const apiBase = this.urlFromFetchOptions(options); const extra = { method: "post", body: msgpackEncode(collection.serialize()), }; return this.newCall(undefined, extra, apiBase); } } export class CollectionItemManagerOnline extends BaseManager { constructor(etebase, colUid) { super(etebase, ["collection", colUid, "item"]); } async fetch(itemUid, options) { const apiBase = this.urlFromFetchOptions(options); const json = await this.newCall([itemUid], undefined, apiBase); return EncryptedCollectionItem.deserialize(json); } async list(options) { const apiBase = this.urlFromFetchOptions(options); const json = await this.newCall(undefined, undefined, apiBase); return { ...json, data: json.data.map((val) => EncryptedCollectionItem.deserialize(val)), }; } async itemRevisions(item, options) { const apiBase = this.urlFromFetchOptions(options); const { uid, encryptionKey, version } = item.serialize(); const json = await this.newCall([item.uid, "revision"], undefined, apiBase); return { ...json, data: json.data.map((val) => EncryptedCollectionItem.deserialize({ uid, encryptionKey, version, etag: val.uid, content: val, })), }; } create(item) { const extra = { method: "post", body: msgpackEncode(item.serialize()), }; return this.newCall(undefined, extra); } async fetchUpdates(items, options) { const apiBase = this.urlFromFetchOptions(options); // We only use stoken if available const wantEtag = !(options === null || options === void 0 ? void 0 : options.stoken); const extra = { method: "post", body: msgpackEncode(items.map((x) => ({ uid: x.uid, etag: ((wantEtag) ? x.lastEtag : undefined) }))), }; const json = await this.newCall(["fetch_updates"], extra, apiBase); const data = json.data; return { ...json, data: data.map((val) => EncryptedCollectionItem.deserialize(val)), }; } async fetchMulti(items, options) { const apiBase = this.urlFromFetchOptions(options); const extra = { method: "post", body: msgpackEncode(items.map((x) => ({ uid: x }))), }; const json = await this.newCall(["fetch_updates"], extra, apiBase); const data = json.data; return { ...json, data: data.map((val) => EncryptedCollectionItem.deserialize(val)), }; } batch(items, deps, options) { const apiBase = this.urlFromFetchOptions(options); const extra = { method: "post", body: msgpackEncode({ items: items.map((x) => x.serialize()), deps: deps === null || deps === void 0 ? void 0 : deps.map((x) => ({ uid: x.uid, etag: x.lastEtag })), }), }; return this.newCall(["batch"], extra, apiBase); } transaction(items, deps, options) { const apiBase = this.urlFromFetchOptions(options); const extra = { method: "post", body: msgpackEncode({ items: items.map((x) => x.serialize()), deps: deps === null || deps === void 0 ? void 0 : deps.map((x) => ({ uid: x.uid, etag: x.lastEtag })), }), }; return this.newCall(["transaction"], extra, apiBase); } chunkUpload(item, chunk, options) { const apiBase = this.urlFromFetchOptions(options); const [chunkUid, chunkContent] = chunk; if (chunkContent === undefined) { throw new ProgrammingError("Tried uploading a missing chunk."); } const extra = { method: "put", headers: { "Content-Type": "application/octet-stream", }, body: chunkContent, }; return this.newCall([item.uid, "chunk", chunkUid], extra, apiBase); } chunkDownload(item, chunkUid, options) { const apiBase = this.urlFromFetchOptions(options); return this.newCall([item.uid, "chunk", chunkUid, "download"], undefined, apiBase); } async subscribeChanges(cb, options_) { const options = { ...options_ }; const getUrlOptions = async () => { const extra = { method: "post", }; const ret = await this.newCall(["subscription-ticket"], extra, undefined); return { ticket: ret.ticket, fetchOptions: options, }; }; const wsOnlineManager = new WebSocketManagerOnline(this.etebase, getUrlOptions); return wsOnlineManager.subscribe((raw) => { const response = msgpackDecode(raw); // Update the stoken we fetch by when reconnecting every time we get data options.stoken = response.stoken; cb({ ...response, data: response.data.map((val) => EncryptedCollectionItem.deserialize(val)), }); }); } } export class CollectionInvitationManagerOnline extends BaseManager { constructor(etebase) { super(etebase, ["invitation"]); } async listIncoming(options) { const apiBase = this.urlFromFetchOptions(options); const json = await this.newCall(["incoming"], undefined, apiBase); return { ...json, data: json.data.map((val) => val), }; } async listOutgoing(options) { const apiBase = this.urlFromFetchOptions(options); const json = await this.newCall(["outgoing"], undefined, apiBase); return { ...json, data: json.data.map((val) => val), }; } async accept(invitation, collectionType, encryptionKey) { const extra = { method: "post", body: msgpackEncode({ collectionType, encryptionKey, }), }; return this.newCall(["incoming", invitation.uid, "accept"], extra); } async reject(invitation) { const extra = { method: "delete", }; return this.newCall(["incoming", invitation.uid], extra); } async fetchUserProfile(username) { const apiBase = this.apiBase.clone().search({ username: username, }); return this.newCall(["outgoing", "fetch_user_profile"], undefined, apiBase); } async invite(invitation) { const extra = { method: "post", body: msgpackEncode(invitation), }; return this.newCall(["outgoing"], extra); } async disinvite(invitation) { const extra = { method: "delete", }; return this.newCall(["outgoing", invitation.uid], extra); } } export class CollectionMemberManagerOnline extends BaseManager { constructor(etebase, colUid) { super(etebase, ["collection", colUid, "member"]); } async list(options) { const apiBase = this.urlFromFetchOptions(options); return this.newCall(undefined, undefined, apiBase); } async remove(username) { const extra = { method: "delete", }; return this.newCall([username], extra); } async leave() { const extra = { method: "post", }; return this.newCall(["leave"], extra); } async modifyAccessLevel(username, accessLevel) { const extra = { method: "patch", body: msgpackEncode({ accessLevel, }), }; return this.newCall([username], extra); } } export class WebSocketHandle { constructor(urlProvider, cb) { this.connected = false; const options = { WebSocket, }; this.rws = new ReconnectingWebSocket(urlProvider, [], options); this.rws.addEventListener("open", () => { this.connected = true; }); this.rws.addEventListener("close", () => { this.connected = false; }); this.rws.addEventListener("error", () => { this.connected = false; }); this.rws.addEventListener("message", (ev) => { cb(ev.data); }); } async unsubscribe() { this.rws.close(); } } export class WebSocketManagerOnline extends BaseManager { constructor(etebase, getUrlOptions) { super(etebase, ["ws"]); this.getUrlOptions = getUrlOptions; } async subscribe(cb) { const protocol = (this.apiBase.protocol() === "https") ? "wss" : "ws"; const urlProvider = async () => { const options = await this.getUrlOptions(); const apiBase = this.urlFromFetchOptions(options.fetchOptions).protocol(protocol); return urlExtend(apiBase, [options.ticket]).toString(); }; return new WebSocketHandle(urlProvider, cb); } } //# sourceMappingURL=OnlineManagers.js.map