etebase
Version:
Etebase TypeScript API for the web and node
498 lines • 16.9 kB
JavaScript
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