@drakkar.software/octospaces-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +972 -0
- package/dist/index.js +1656 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/index.d.ts +9 -0
- package/dist/platform/index.js +111 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/index.native.d.ts +9 -0
- package/dist/platform/index.native.js +106 -0
- package/dist/platform/index.native.js.map +1 -0
- package/package.json +50 -0
- package/src/core/adapters.ts +34 -0
- package/src/core/config.ts +87 -0
- package/src/core/ids.test.ts +45 -0
- package/src/core/ids.ts +29 -0
- package/src/core/space-access-error.ts +13 -0
- package/src/core/storage-types.ts +71 -0
- package/src/core/types.ts +162 -0
- package/src/index.ts +221 -0
- package/src/objects/objects.test.ts +288 -0
- package/src/objects/objects.ts +296 -0
- package/src/platform/index.native.ts +3 -0
- package/src/platform/index.ts +3 -0
- package/src/platform/kv.native.ts +23 -0
- package/src/platform/kv.ts +29 -0
- package/src/platform/platform.native.ts +16 -0
- package/src/platform/platform.ts +10 -0
- package/src/spaces/members.test.ts +87 -0
- package/src/spaces/members.ts +271 -0
- package/src/spaces/object-index.test.ts +105 -0
- package/src/spaces/object-index.ts +160 -0
- package/src/spaces/registry.test.ts +111 -0
- package/src/spaces/registry.ts +466 -0
- package/src/sync/account-seal.test.ts +70 -0
- package/src/sync/account-seal.ts +80 -0
- package/src/sync/base64.ts +89 -0
- package/src/sync/base64url.ts +22 -0
- package/src/sync/client.ts +301 -0
- package/src/sync/fetch-timeout.test.ts +26 -0
- package/src/sync/fetch-timeout.ts +23 -0
- package/src/sync/identity.ts +158 -0
- package/src/sync/pairing.ts +103 -0
- package/src/sync/paths.test.ts +135 -0
- package/src/sync/paths.ts +177 -0
- package/src/sync/profile-cache.ts +34 -0
- package/src/sync/pull-cache.test.ts +55 -0
- package/src/sync/pull-cache.ts +33 -0
- package/src/sync/space-access-store.test.ts +129 -0
- package/src/sync/space-access-store.ts +117 -0
- package/src/sync/space-access.ts +136 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +40 -0
- package/vitest.config.ts +12 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1656 @@
|
|
|
1
|
+
// src/core/config.ts
|
|
2
|
+
var cfg = null;
|
|
3
|
+
function configureOctoSpaces(config) {
|
|
4
|
+
if ("namespace" in config && !config.syncNamespace) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
`octospaces-sdk: configureOctoSpaces received "namespace" \u2014 did you mean "syncNamespace"?`
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
const ns = (config.syncNamespace ?? "").trim();
|
|
10
|
+
if (ns !== "" && !/^[A-Za-z0-9_-]+$/.test(ns)) {
|
|
11
|
+
throw new Error(`octospaces-sdk: syncNamespace must be a bare name ([A-Za-z0-9_-]+), got "${ns}"`);
|
|
12
|
+
}
|
|
13
|
+
const sharedNs = (config.sharedSpacesNamespace ?? "").trim();
|
|
14
|
+
if (sharedNs !== "" && !/^[A-Za-z0-9_-]+$/.test(sharedNs)) {
|
|
15
|
+
throw new Error(`octospaces-sdk: sharedSpacesNamespace must be a bare name ([A-Za-z0-9_-]+), got "${sharedNs}"`);
|
|
16
|
+
}
|
|
17
|
+
cfg = {
|
|
18
|
+
...config,
|
|
19
|
+
syncNamespace: ns || void 0,
|
|
20
|
+
sharedSpacesNamespace: sharedNs || void 0
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function req() {
|
|
24
|
+
if (!cfg) throw new Error("octospaces-sdk: configureOctoSpaces() not called \u2014 wire it at app boot.");
|
|
25
|
+
return cfg;
|
|
26
|
+
}
|
|
27
|
+
var getSyncBase = () => req().syncBase;
|
|
28
|
+
var getSyncNamespace = () => req().syncNamespace;
|
|
29
|
+
var getSyncPrefix = () => {
|
|
30
|
+
const ns = req().syncNamespace;
|
|
31
|
+
return ns ? `/v1/${ns}` : "";
|
|
32
|
+
};
|
|
33
|
+
var getSharedSpacesNamespace = () => cfg?.sharedSpacesNamespace;
|
|
34
|
+
var getOnServerReachable = () => cfg?.onServerReachable;
|
|
35
|
+
|
|
36
|
+
// src/core/adapters.ts
|
|
37
|
+
var kv = null;
|
|
38
|
+
function configureKv(adapter) {
|
|
39
|
+
kv = adapter;
|
|
40
|
+
}
|
|
41
|
+
function getKv() {
|
|
42
|
+
if (!kv) throw new Error("octospaces-sdk: configureKv() not called \u2014 wire it at app boot.");
|
|
43
|
+
return kv;
|
|
44
|
+
}
|
|
45
|
+
var kvGet = (key2) => getKv().get(key2);
|
|
46
|
+
var kvSet = (key2, value) => getKv().set(key2, value);
|
|
47
|
+
var kvRemove = (key2) => getKv().remove(key2);
|
|
48
|
+
|
|
49
|
+
// src/core/ids.ts
|
|
50
|
+
function randomId() {
|
|
51
|
+
const bytes = new Uint8Array(16);
|
|
52
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
53
|
+
let s = "";
|
|
54
|
+
for (const b of bytes) s += b.toString(16).padStart(2, "0");
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
function roomSlug(name) {
|
|
58
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "room";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/sync/paths.ts
|
|
62
|
+
var pull = (rest) => `/pull/${rest}`;
|
|
63
|
+
var push = (rest) => `/push/${rest}`;
|
|
64
|
+
var spaceIdFromRoomId = (roomId) => roomId.split("-").slice(0, 2).join("-");
|
|
65
|
+
var keyringName = (spaceId) => `spaces/${spaceId}`;
|
|
66
|
+
var keyringPull = (spaceId) => pull(`${keyringName(spaceId)}/_keyring`);
|
|
67
|
+
var keyringPush = (spaceId) => push(`${keyringName(spaceId)}/_keyring`);
|
|
68
|
+
var attachmentName = (roomId, blobId) => `spaces/${spaceIdFromRoomId(roomId)}/attachments/${roomId}/${blobId}`;
|
|
69
|
+
var attachmentPull = (roomId, blobId) => pull(attachmentName(roomId, blobId));
|
|
70
|
+
var attachmentPush = (roomId, blobId) => push(attachmentName(roomId, blobId));
|
|
71
|
+
var profilePull = (userId) => pull(`user/${userId}/profile`);
|
|
72
|
+
var profilePush = (userId) => push(`user/${userId}/profile`);
|
|
73
|
+
var spacesPull = (userId) => pull(`user/${userId}/_spaces`);
|
|
74
|
+
var spacesPush = (userId) => push(`user/${userId}/_spaces`);
|
|
75
|
+
var roomsRegistryPull = (spaceId) => pull(`spaces/${spaceId}/_rooms`);
|
|
76
|
+
var roomsRegistryPush = (spaceId) => push(`spaces/${spaceId}/_rooms`);
|
|
77
|
+
var objIndexName = (spaceId) => `spaces/${spaceId}/objects/_index`;
|
|
78
|
+
var objIndexPull = (spaceId) => pull(objIndexName(spaceId));
|
|
79
|
+
var objIndexPush = (spaceId) => push(objIndexName(spaceId));
|
|
80
|
+
var objLogName = (spaceId, objectId) => `spaces/${spaceId}/objects/logs/${objectId}`;
|
|
81
|
+
var objLogPull = (spaceId, objectId) => pull(objLogName(spaceId, objectId));
|
|
82
|
+
var objLogPush = (spaceId, objectId) => push(objLogName(spaceId, objectId));
|
|
83
|
+
var objDocName = (spaceId, objectId) => `spaces/${spaceId}/objects/docs/${objectId}`;
|
|
84
|
+
var objDocPull = (spaceId, objectId) => pull(objDocName(spaceId, objectId));
|
|
85
|
+
var objDocPush = (spaceId, objectId) => push(objDocName(spaceId, objectId));
|
|
86
|
+
var objectBlobName = (spaceId, blobId) => `spaces/${spaceId}/objects/blobs/${blobId}`;
|
|
87
|
+
var objectBlobPull = (spaceId, blobId) => pull(objectBlobName(spaceId, blobId));
|
|
88
|
+
var objectBlobPush = (spaceId, blobId) => push(objectBlobName(spaceId, blobId));
|
|
89
|
+
var typesIndexName = (spaceId) => `spaces/${spaceId}/types/_index`;
|
|
90
|
+
var typesIndexPull = (spaceId) => pull(typesIndexName(spaceId));
|
|
91
|
+
var typesIndexPush = (spaceId) => push(typesIndexName(spaceId));
|
|
92
|
+
var spaceIndexName = (shard) => `_index/spaces/${shard}`;
|
|
93
|
+
var spaceIndexPull = (shard) => pull(spaceIndexName(shard));
|
|
94
|
+
var OBJECT_COLLECTIONS = [
|
|
95
|
+
"keyring",
|
|
96
|
+
"objindex",
|
|
97
|
+
"objlog",
|
|
98
|
+
"objsnap",
|
|
99
|
+
"objdoc",
|
|
100
|
+
"objblob",
|
|
101
|
+
"typeindex"
|
|
102
|
+
];
|
|
103
|
+
function ownerScope() {
|
|
104
|
+
return {
|
|
105
|
+
ops: ["read", "list", "write"],
|
|
106
|
+
collections: OBJECT_COLLECTIONS,
|
|
107
|
+
paths: ["spaces/**"]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function spaceMemberScope(spaceId, canWrite) {
|
|
111
|
+
const ops = canWrite ? ["read", "list", "write"] : ["read", "list"];
|
|
112
|
+
return {
|
|
113
|
+
ops,
|
|
114
|
+
collections: OBJECT_COLLECTIONS,
|
|
115
|
+
paths: [`spaces/${spaceId}/**`]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function accountScope(userId) {
|
|
119
|
+
return {
|
|
120
|
+
ops: ["read", "list", "write"],
|
|
121
|
+
collections: ["profile", "devices", "spaces", "rooms"],
|
|
122
|
+
paths: [
|
|
123
|
+
`user/${userId}/profile`,
|
|
124
|
+
`users/${userId}/_devices`,
|
|
125
|
+
`user/${userId}/_spaces`,
|
|
126
|
+
"spaces/**"
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function linkedDeviceScope(userId) {
|
|
131
|
+
return {
|
|
132
|
+
ops: ["read", "list", "write"],
|
|
133
|
+
collections: [...OBJECT_COLLECTIONS, "profile", "devices", "spaces", "rooms"],
|
|
134
|
+
paths: [
|
|
135
|
+
"spaces/**",
|
|
136
|
+
`user/${userId}/profile`,
|
|
137
|
+
`users/${userId}/_devices`,
|
|
138
|
+
`user/${userId}/_spaces`
|
|
139
|
+
]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function bytesToHex(b) {
|
|
143
|
+
let s = "";
|
|
144
|
+
for (const x of b) s += x.toString(16).padStart(2, "0");
|
|
145
|
+
return s;
|
|
146
|
+
}
|
|
147
|
+
async function userIdFromEdPub(edPubHex) {
|
|
148
|
+
const bytes = new Uint8Array(edPubHex.length / 2);
|
|
149
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(edPubHex.slice(i * 2, i * 2 + 2), 16);
|
|
150
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
151
|
+
return bytesToHex(new Uint8Array(digest)).slice(0, 32);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/sync/client.ts
|
|
155
|
+
import { StarfishClient } from "@drakkar.software/starfish-client";
|
|
156
|
+
import { createKeyring, createKeyringEncryptor } from "@drakkar.software/starfish-keyring";
|
|
157
|
+
import { signRequest, stableStringify } from "@drakkar.software/starfish-protocol";
|
|
158
|
+
|
|
159
|
+
// src/sync/fetch-timeout.ts
|
|
160
|
+
var CONNECT_TIMEOUT_MS = 12e3;
|
|
161
|
+
function fetchWithTimeout(timeoutMs = CONNECT_TIMEOUT_MS) {
|
|
162
|
+
return (input, init) => {
|
|
163
|
+
const ctrl = new AbortController();
|
|
164
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
165
|
+
const caller = init?.signal;
|
|
166
|
+
if (caller) {
|
|
167
|
+
if (caller.aborted) ctrl.abort();
|
|
168
|
+
else caller.addEventListener("abort", () => ctrl.abort(), { once: true });
|
|
169
|
+
}
|
|
170
|
+
return fetch(input, { ...init, signal: ctrl.signal }).finally(() => clearTimeout(timer));
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/sync/pull-cache.ts
|
|
175
|
+
var PREFIX = "octospaces.pullcache.";
|
|
176
|
+
var PULL_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
177
|
+
var shared;
|
|
178
|
+
function pullCache() {
|
|
179
|
+
return shared ?? (shared = {
|
|
180
|
+
get: (key2) => kvGet(PREFIX + key2),
|
|
181
|
+
set: (key2, value) => kvSet(PREFIX + key2, value)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/sync/profile-cache.ts
|
|
186
|
+
var key = (userId) => `octospaces.profile.v1.${userId}`;
|
|
187
|
+
function cacheProfile(userId, profile) {
|
|
188
|
+
void kvSet(key(userId), JSON.stringify(profile)).catch(() => {
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async function loadCachedProfile(userId) {
|
|
192
|
+
try {
|
|
193
|
+
const raw = await kvGet(key(userId));
|
|
194
|
+
if (!raw) return null;
|
|
195
|
+
const d = JSON.parse(raw);
|
|
196
|
+
return {
|
|
197
|
+
pseudo: typeof d.pseudo === "string" ? d.pseudo : null,
|
|
198
|
+
avatar: typeof d.avatar === "string" ? d.avatar : null,
|
|
199
|
+
edPub: typeof d.edPub === "string" ? d.edPub : null,
|
|
200
|
+
kemPub: typeof d.kemPub === "string" ? d.kemPub : null
|
|
201
|
+
};
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/core/space-access-error.ts
|
|
208
|
+
var SpaceAccessError = class extends Error {
|
|
209
|
+
constructor(message) {
|
|
210
|
+
super(message);
|
|
211
|
+
this.name = "SpaceAccessError";
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/sync/client.ts
|
|
216
|
+
function capProviderFor(cap, devEdPrivHex) {
|
|
217
|
+
return {
|
|
218
|
+
async getCap() {
|
|
219
|
+
return { cap, devEdPrivHex };
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function makeClient(cap, devEdPrivHex, namespaceOverride) {
|
|
224
|
+
return new StarfishClient({
|
|
225
|
+
baseUrl: getSyncBase(),
|
|
226
|
+
namespace: namespaceOverride ?? getSyncNamespace(),
|
|
227
|
+
capProvider: capProviderFor(cap, devEdPrivHex),
|
|
228
|
+
fetch: fetchWithTimeout(),
|
|
229
|
+
cache: pullCache(),
|
|
230
|
+
cacheMaxAgeMs: PULL_CACHE_MAX_AGE_MS,
|
|
231
|
+
cacheFallbackStatuses: [429, 500, 502, 503, 504],
|
|
232
|
+
onRevalidated: () => getOnServerReachable()?.()
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async function openEncryptor(client, keys, spaceId, trustedAdders) {
|
|
236
|
+
const res = await client.pull(keyringPull(spaceId)).catch(() => {
|
|
237
|
+
throw new Error("Could not reach the server to fetch space keys.");
|
|
238
|
+
});
|
|
239
|
+
const keyring = res?.data;
|
|
240
|
+
if (!keyring || !keyring.epochs) {
|
|
241
|
+
throw new SpaceAccessError("This space has no keyring yet \u2014 ask the owner to open it first.");
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const enc = await createKeyringEncryptor(
|
|
245
|
+
keyring,
|
|
246
|
+
{ kemPubHex: keys.kemPub, kemPrivHex: keys.kemPriv },
|
|
247
|
+
{ trustedAdders }
|
|
248
|
+
);
|
|
249
|
+
return enc;
|
|
250
|
+
} catch {
|
|
251
|
+
throw new SpaceAccessError("You're not a recipient of this space's keyring yet \u2014 ask the owner to re-invite.");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function buildEncryptor(client, keys, spaceId, trustedAdders) {
|
|
255
|
+
try {
|
|
256
|
+
return await openEncryptor(client, keys, spaceId, trustedAdders);
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function ownerEnsureKeyring(client, keys, spaceId, trustedAdders = [keys.edPub]) {
|
|
262
|
+
const krRes = await client.pull(keyringPull(spaceId)).catch(() => null);
|
|
263
|
+
let keyring = krRes?.data;
|
|
264
|
+
if (!keyring || !keyring.epochs) {
|
|
265
|
+
const created = await createKeyring({ edPrivHex: keys.edPriv, edPubHex: keys.edPub }, [
|
|
266
|
+
{ subKemHex: keys.kemPub }
|
|
267
|
+
]);
|
|
268
|
+
keyring = created.keyring;
|
|
269
|
+
await client.push(keyringPush(spaceId), keyring, krRes?.hash ?? null);
|
|
270
|
+
}
|
|
271
|
+
const enc = await createKeyringEncryptor(
|
|
272
|
+
keyring,
|
|
273
|
+
{ kemPubHex: keys.kemPub, kemPrivHex: keys.kemPriv },
|
|
274
|
+
{ trustedAdders }
|
|
275
|
+
);
|
|
276
|
+
return enc;
|
|
277
|
+
}
|
|
278
|
+
async function readProfile(userId) {
|
|
279
|
+
try {
|
|
280
|
+
const r = await fetchWithTimeout()(`${getSyncBase()}${getSyncPrefix()}${profilePull(userId)}`);
|
|
281
|
+
if (!r.ok) return { pseudo: null, avatar: null, edPub: null, kemPub: null };
|
|
282
|
+
const body = await r.json();
|
|
283
|
+
const data = body?.data;
|
|
284
|
+
const profile = {
|
|
285
|
+
pseudo: typeof data?.pseudo === "string" ? data.pseudo : null,
|
|
286
|
+
avatar: typeof data?.avatar === "string" ? data.avatar : null,
|
|
287
|
+
edPub: typeof data?.edPub === "string" ? data.edPub : null,
|
|
288
|
+
kemPub: typeof data?.kemPub === "string" ? data.kemPub : null
|
|
289
|
+
};
|
|
290
|
+
cacheProfile(userId, profile);
|
|
291
|
+
return profile;
|
|
292
|
+
} catch {
|
|
293
|
+
return await loadCachedProfile(userId) ?? { pseudo: null, avatar: null, edPub: null, kemPub: null };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function readPseudo(userId) {
|
|
297
|
+
return (await readProfile(userId)).pseudo;
|
|
298
|
+
}
|
|
299
|
+
var profileBatchClient;
|
|
300
|
+
function getProfileBatchClient() {
|
|
301
|
+
if (!profileBatchClient) {
|
|
302
|
+
profileBatchClient = new StarfishClient({ baseUrl: getSyncBase(), namespace: getSyncNamespace(), fetch: fetchWithTimeout() });
|
|
303
|
+
}
|
|
304
|
+
return profileBatchClient;
|
|
305
|
+
}
|
|
306
|
+
var PROFILE_BATCH_CHUNK = 24;
|
|
307
|
+
async function readProfiles(ids) {
|
|
308
|
+
const out = /* @__PURE__ */ new Map();
|
|
309
|
+
const client = getProfileBatchClient();
|
|
310
|
+
for (let i = 0; i < ids.length; i += PROFILE_BATCH_CHUNK) {
|
|
311
|
+
const chunk = ids.slice(i, i + PROFILE_BATCH_CHUNK);
|
|
312
|
+
let entries;
|
|
313
|
+
try {
|
|
314
|
+
entries = await client.batchPullMany("profile", chunk.map((id) => ({ identity: id })));
|
|
315
|
+
} catch {
|
|
316
|
+
for (const id of chunk) {
|
|
317
|
+
const cached = await loadCachedProfile(id);
|
|
318
|
+
if (cached) out.set(id, cached);
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
chunk.forEach((id, j) => {
|
|
323
|
+
const entry = entries[j];
|
|
324
|
+
if (!entry || entry.error) return;
|
|
325
|
+
const data = entry.data ?? null;
|
|
326
|
+
const profile = {
|
|
327
|
+
pseudo: typeof data?.pseudo === "string" ? data.pseudo : null,
|
|
328
|
+
avatar: typeof data?.avatar === "string" ? data.avatar : null,
|
|
329
|
+
edPub: typeof data?.edPub === "string" ? data.edPub : null,
|
|
330
|
+
kemPub: typeof data?.kemPub === "string" ? data.kemPub : null
|
|
331
|
+
};
|
|
332
|
+
cacheProfile(id, profile);
|
|
333
|
+
out.set(id, profile);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
async function writeProfile(client, userId, patch) {
|
|
339
|
+
const current = await client.pull(profilePull(userId)).catch(() => null);
|
|
340
|
+
const base = current?.data ?? {};
|
|
341
|
+
const next = { ...base, ...patch, v: 1 };
|
|
342
|
+
if (next.avatar == null) delete next.avatar;
|
|
343
|
+
await client.push(profilePush(userId), next, current?.hash ?? null);
|
|
344
|
+
}
|
|
345
|
+
async function writePseudo(client, userId, pseudo) {
|
|
346
|
+
await writeProfile(client, userId, { pseudo });
|
|
347
|
+
}
|
|
348
|
+
async function ensureProfileKeys(client, userId, keys) {
|
|
349
|
+
let confirmedAbsent = false;
|
|
350
|
+
try {
|
|
351
|
+
const r = await fetchWithTimeout()(`${getSyncBase()}${getSyncPrefix()}${profilePull(userId)}`);
|
|
352
|
+
if (r.status === 404) confirmedAbsent = true;
|
|
353
|
+
else if (r.ok) {
|
|
354
|
+
const body = await r.json();
|
|
355
|
+
const data = body?.data;
|
|
356
|
+
confirmedAbsent = !(typeof data?.edPub === "string" && typeof data?.kemPub === "string");
|
|
357
|
+
} else return;
|
|
358
|
+
} catch {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (!confirmedAbsent) return;
|
|
362
|
+
await writeProfile(client, userId, { edPub: keys.edPub, kemPub: keys.kemPub });
|
|
363
|
+
}
|
|
364
|
+
async function buildAuthHeaders(cap, devEdPrivHex, method, pathAndQuery) {
|
|
365
|
+
let host = "";
|
|
366
|
+
try {
|
|
367
|
+
host = new URL(getSyncBase()).host;
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
const { sig, ts, nonce } = await signRequest(
|
|
371
|
+
{ method, pathAndQuery, host },
|
|
372
|
+
devEdPrivHex
|
|
373
|
+
);
|
|
374
|
+
const capJson = stableStringify(cap);
|
|
375
|
+
const capB64 = typeof btoa === "function" ? btoa(capJson) : Buffer.from(capJson, "utf-8").toString("base64");
|
|
376
|
+
return {
|
|
377
|
+
Authorization: `Cap ${capB64}`,
|
|
378
|
+
"X-Starfish-Sig": sig,
|
|
379
|
+
"X-Starfish-Ts": String(ts),
|
|
380
|
+
"X-Starfish-Nonce": nonce
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async function readOwnPseudo(userId) {
|
|
384
|
+
try {
|
|
385
|
+
const r = await fetchWithTimeout()(`${getSyncBase()}${getSyncPrefix()}${profilePull(userId)}`);
|
|
386
|
+
if (r.status === 404) return { read: true, pseudo: null };
|
|
387
|
+
if (!r.ok) return { read: false, pseudo: null };
|
|
388
|
+
const body = await r.json();
|
|
389
|
+
const data = body?.data;
|
|
390
|
+
return { read: true, pseudo: typeof data?.pseudo === "string" ? data.pseudo : null };
|
|
391
|
+
} catch {
|
|
392
|
+
return { read: false, pseudo: null };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function ensurePseudo(client, userId, fallback) {
|
|
396
|
+
const { read, pseudo } = await readOwnPseudo(userId);
|
|
397
|
+
if (pseudo && pseudo.trim()) return pseudo;
|
|
398
|
+
if (!read) return fallback;
|
|
399
|
+
await writeProfile(client, userId, { pseudo: fallback });
|
|
400
|
+
return fallback;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/sync/identity.ts
|
|
404
|
+
import { generateMnemonic, validateMnemonic } from "@scure/bip39";
|
|
405
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
406
|
+
import { bootstrapRootIdentity, mintDeviceCap } from "@drakkar.software/starfish-identities";
|
|
407
|
+
function ownerTrustedAdders(session) {
|
|
408
|
+
return session.ownerEdPub === session.keys.edPub ? [session.keys.edPub] : [session.ownerEdPub, session.keys.edPub];
|
|
409
|
+
}
|
|
410
|
+
function generateSeedWords() {
|
|
411
|
+
return generateMnemonic(wordlist, 128).split(" ");
|
|
412
|
+
}
|
|
413
|
+
function isValidSeed(words) {
|
|
414
|
+
return validateMnemonic(words.join(" ").trim(), wordlist);
|
|
415
|
+
}
|
|
416
|
+
function fingerprintFromUserId(userId) {
|
|
417
|
+
const h = userId.replace(/[^0-9a-f]/gi, "").toUpperCase();
|
|
418
|
+
return [h.slice(0, 4), h.slice(4, 8), h.slice(8, 12)].filter(Boolean).join(" \xB7 ");
|
|
419
|
+
}
|
|
420
|
+
async function buildSession({ userId, keys }, name) {
|
|
421
|
+
const fallback = name && name.trim() ? name.trim() : `user-${userId.slice(0, 6)}`;
|
|
422
|
+
const sub = { edPubHex: keys.edPub, kemPubHex: keys.kemPub };
|
|
423
|
+
const chatCap = await mintDeviceCap(keys.edPriv, keys.edPub, sub, ownerScope());
|
|
424
|
+
const accountCap = await mintDeviceCap(keys.edPriv, keys.edPub, sub, accountScope(userId));
|
|
425
|
+
const chatClient = makeClient(chatCap, keys.edPriv);
|
|
426
|
+
const accountClient = makeClient(accountCap, keys.edPriv);
|
|
427
|
+
const sharedNs = getSharedSpacesNamespace();
|
|
428
|
+
const spacesRegistryClient = sharedNs ? makeClient(accountCap, keys.edPriv, sharedNs) : accountClient;
|
|
429
|
+
const spacesKeyringClient = sharedNs ? makeClient(chatCap, keys.edPriv, sharedNs) : chatClient;
|
|
430
|
+
const displayName = await ensurePseudo(accountClient, userId, fallback).catch(() => fallback);
|
|
431
|
+
void ensureProfileKeys(accountClient, userId, keys).catch(() => {
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
userId,
|
|
435
|
+
name: displayName,
|
|
436
|
+
keys,
|
|
437
|
+
chatCap,
|
|
438
|
+
accountCap,
|
|
439
|
+
chatClient,
|
|
440
|
+
accountClient,
|
|
441
|
+
spacesRegistryClient,
|
|
442
|
+
spacesKeyringClient,
|
|
443
|
+
fingerprint: fingerprintFromUserId(userId),
|
|
444
|
+
ownerEdPub: keys.edPub
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async function buildLinkedSession({ userId, keys, capCert }, name) {
|
|
448
|
+
const fallback = name && name.trim() ? name.trim() : `user-${userId.slice(0, 6)}`;
|
|
449
|
+
const chatClient = makeClient(capCert, keys.edPriv);
|
|
450
|
+
const accountClient = makeClient(capCert, keys.edPriv);
|
|
451
|
+
const sharedNs = getSharedSpacesNamespace();
|
|
452
|
+
const spacesRegistryClient = sharedNs ? makeClient(capCert, keys.edPriv, sharedNs) : accountClient;
|
|
453
|
+
const spacesKeyringClient = sharedNs ? makeClient(capCert, keys.edPriv, sharedNs) : chatClient;
|
|
454
|
+
const displayName = await ensurePseudo(accountClient, userId, fallback).catch(() => fallback);
|
|
455
|
+
return {
|
|
456
|
+
userId,
|
|
457
|
+
name: displayName,
|
|
458
|
+
keys,
|
|
459
|
+
chatCap: capCert,
|
|
460
|
+
accountCap: capCert,
|
|
461
|
+
chatClient,
|
|
462
|
+
accountClient,
|
|
463
|
+
spacesRegistryClient,
|
|
464
|
+
spacesKeyringClient,
|
|
465
|
+
fingerprint: fingerprintFromUserId(userId),
|
|
466
|
+
ownerEdPub: capCert.iss
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
async function deriveSession(seedWords, name) {
|
|
470
|
+
const passphrase = seedWords.join(" ").trim();
|
|
471
|
+
const creds = await bootstrapRootIdentity(passphrase);
|
|
472
|
+
return buildSession({ userId: creds.userId, keys: creds.device }, name);
|
|
473
|
+
}
|
|
474
|
+
function rootIdentityOf(s) {
|
|
475
|
+
return { userId: s.userId, keys: s.keys };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/sync/account-seal.ts
|
|
479
|
+
import {
|
|
480
|
+
bytesToHex as bytesToHex2,
|
|
481
|
+
hexToBytes,
|
|
482
|
+
unwrapFromEntry,
|
|
483
|
+
verifyEntrySignature,
|
|
484
|
+
wrapForRecipient
|
|
485
|
+
} from "@drakkar.software/starfish-keyring";
|
|
486
|
+
var SELF_EPOCH = 0;
|
|
487
|
+
var subtle = () => globalThis.crypto.subtle;
|
|
488
|
+
async function seal(session, recipientKemPub, plaintext) {
|
|
489
|
+
const cek = globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
490
|
+
const entry = await wrapForRecipient(cek, recipientKemPub, {
|
|
491
|
+
adderEdPrivHex: session.keys.edPriv,
|
|
492
|
+
adderEdPubHex: session.keys.edPub,
|
|
493
|
+
addedAt: Math.floor(Date.now() / 1e3),
|
|
494
|
+
epoch: SELF_EPOCH
|
|
495
|
+
});
|
|
496
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
497
|
+
const key2 = await subtle().importKey("raw", cek, { name: "AES-GCM" }, false, ["encrypt"]);
|
|
498
|
+
const ctBuf = await subtle().encrypt({ name: "AES-GCM", iv }, key2, new TextEncoder().encode(plaintext));
|
|
499
|
+
const packed = new Uint8Array(iv.length + ctBuf.byteLength);
|
|
500
|
+
packed.set(iv, 0);
|
|
501
|
+
packed.set(new Uint8Array(ctBuf), iv.length);
|
|
502
|
+
return { entry, ct: bytesToHex2(packed) };
|
|
503
|
+
}
|
|
504
|
+
async function open(session, blob) {
|
|
505
|
+
const cek = await unwrapFromEntry(blob.entry, session.keys.kemPriv);
|
|
506
|
+
const packed = hexToBytes(blob.ct);
|
|
507
|
+
const iv = new Uint8Array(packed.subarray(0, 12));
|
|
508
|
+
const ctBytes = new Uint8Array(packed.subarray(12));
|
|
509
|
+
const key2 = await subtle().importKey("raw", new Uint8Array(cek), { name: "AES-GCM" }, false, ["decrypt"]);
|
|
510
|
+
const out = await subtle().decrypt({ name: "AES-GCM", iv }, key2, ctBytes);
|
|
511
|
+
return new TextDecoder().decode(out);
|
|
512
|
+
}
|
|
513
|
+
function sealToSelf(session, plaintext) {
|
|
514
|
+
return seal(session, session.keys.kemPub, plaintext);
|
|
515
|
+
}
|
|
516
|
+
async function unsealFromSelf(session, blob) {
|
|
517
|
+
if (blob.entry.addedBy !== session.keys.edPub) throw new Error("sealed blob not self-signed");
|
|
518
|
+
if (!await verifyEntrySignature(blob.entry, SELF_EPOCH)) throw new Error("sealed blob signature invalid");
|
|
519
|
+
return open(session, blob);
|
|
520
|
+
}
|
|
521
|
+
function sealToRecipient(session, recipientKemPub, plaintext) {
|
|
522
|
+
return seal(session, recipientKemPub, plaintext);
|
|
523
|
+
}
|
|
524
|
+
async function unsealFromRecipient(session, blob) {
|
|
525
|
+
if (!await verifyEntrySignature(blob.entry, SELF_EPOCH)) throw new Error("sealed blob signature invalid");
|
|
526
|
+
return open(session, blob);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/sync/space-access-store.ts
|
|
530
|
+
var keyFor = (userId) => `octospaces.spaceaccess.${userId}`;
|
|
531
|
+
var cache = {};
|
|
532
|
+
var activeKey = null;
|
|
533
|
+
async function hydrateSpaceAccessStore(userId, serverCaps, serverLinkAccess) {
|
|
534
|
+
const key2 = keyFor(userId);
|
|
535
|
+
if (activeKey === key2) return;
|
|
536
|
+
activeKey = key2;
|
|
537
|
+
cache = {};
|
|
538
|
+
const raw = await kvGet(key2);
|
|
539
|
+
if (raw) {
|
|
540
|
+
try {
|
|
541
|
+
cache = JSON.parse(raw);
|
|
542
|
+
} catch (e) {
|
|
543
|
+
console.error("[octospaces] space-access-store: corrupt cache, resetting:", e);
|
|
544
|
+
cache = {};
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
let changed = false;
|
|
548
|
+
for (const [spaceId, capJson] of Object.entries(serverCaps)) {
|
|
549
|
+
cache[spaceId] = { kind: "member", cap: capJson };
|
|
550
|
+
changed = true;
|
|
551
|
+
}
|
|
552
|
+
for (const [spaceId, access] of Object.entries(serverLinkAccess)) {
|
|
553
|
+
cache[spaceId] = { kind: "link", cap: access.cap, key: access.key, write: access.write };
|
|
554
|
+
changed = true;
|
|
555
|
+
}
|
|
556
|
+
if (changed) await kvSet(key2, JSON.stringify(cache));
|
|
557
|
+
}
|
|
558
|
+
function persist() {
|
|
559
|
+
if (activeKey) void kvSet(activeKey, JSON.stringify(cache));
|
|
560
|
+
}
|
|
561
|
+
function getSpaceAccessEntry(spaceId) {
|
|
562
|
+
return cache[spaceId] ?? null;
|
|
563
|
+
}
|
|
564
|
+
function saveSpaceAccessEntry(spaceId, entry) {
|
|
565
|
+
cache = { ...cache, [spaceId]: entry };
|
|
566
|
+
persist();
|
|
567
|
+
}
|
|
568
|
+
function removeSpaceAccessEntry(spaceId) {
|
|
569
|
+
if (!(spaceId in cache)) return;
|
|
570
|
+
const next = { ...cache };
|
|
571
|
+
delete next[spaceId];
|
|
572
|
+
cache = next;
|
|
573
|
+
persist();
|
|
574
|
+
}
|
|
575
|
+
function localSpaceAccessEntries() {
|
|
576
|
+
return cache;
|
|
577
|
+
}
|
|
578
|
+
function memberCapsFromStore() {
|
|
579
|
+
const out = {};
|
|
580
|
+
for (const [id, e] of Object.entries(cache)) if (e.kind === "member") out[id] = e.cap;
|
|
581
|
+
return out;
|
|
582
|
+
}
|
|
583
|
+
function linkAccessFromStore() {
|
|
584
|
+
const out = {};
|
|
585
|
+
for (const [id, e] of Object.entries(cache)) {
|
|
586
|
+
if (e.kind === "link") out[id] = { cap: e.cap, key: e.key, write: e.write };
|
|
587
|
+
}
|
|
588
|
+
return out;
|
|
589
|
+
}
|
|
590
|
+
function clearSpaceAccessStore() {
|
|
591
|
+
cache = {};
|
|
592
|
+
activeKey = null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/sync/space-access.ts
|
|
596
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
597
|
+
function clearSpaceAccessCache() {
|
|
598
|
+
cache2.clear();
|
|
599
|
+
}
|
|
600
|
+
function getSpaceAccess(spaceId, session, reg) {
|
|
601
|
+
const hit = cache2.get(spaceId);
|
|
602
|
+
if (hit) return hit;
|
|
603
|
+
const p = (async () => {
|
|
604
|
+
const entry = getSpaceAccessEntry(spaceId);
|
|
605
|
+
if (entry?.kind === "link") {
|
|
606
|
+
const cap = entry.cap;
|
|
607
|
+
const client = makeClient(cap, entry.key);
|
|
608
|
+
return { encryptor: null, client, isOwnerOpen: false };
|
|
609
|
+
}
|
|
610
|
+
if (entry?.kind === "member") {
|
|
611
|
+
const cap = JSON.parse(entry.cap);
|
|
612
|
+
const client = makeClient(cap, session.keys.edPriv);
|
|
613
|
+
const encryptor2 = await openEncryptor(client, session.keys, spaceId, cap.iss ? [cap.iss] : []);
|
|
614
|
+
return { encryptor: encryptor2, client, isOwnerOpen: false };
|
|
615
|
+
}
|
|
616
|
+
const visibility = reg?.visibility;
|
|
617
|
+
if (visibility === "public") {
|
|
618
|
+
return { encryptor: null, client: session.chatClient, isOwnerOpen: reg.owner === session.userId };
|
|
619
|
+
}
|
|
620
|
+
const owner = reg?.owner ?? null;
|
|
621
|
+
const members = reg?.members ?? [];
|
|
622
|
+
if (owner !== null && owner !== session.userId) {
|
|
623
|
+
throw new SpaceAccessError(
|
|
624
|
+
members.includes(session.userId) ? "You're a member of this space, but its key isn't on this device yet \u2014 reconnect, or ask the owner to re-invite." : "You don't have access to this space."
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
const encryptor = await ownerEnsureKeyring(
|
|
628
|
+
session.chatClient,
|
|
629
|
+
session.keys,
|
|
630
|
+
spaceId,
|
|
631
|
+
ownerTrustedAdders(session)
|
|
632
|
+
);
|
|
633
|
+
return { encryptor, client: session.chatClient, isOwnerOpen: true };
|
|
634
|
+
})();
|
|
635
|
+
cache2.set(spaceId, p);
|
|
636
|
+
p.catch(() => cache2.delete(spaceId));
|
|
637
|
+
return p;
|
|
638
|
+
}
|
|
639
|
+
async function buildSpaceAccess(session, spaceId, hint) {
|
|
640
|
+
const entry = getSpaceAccessEntry(spaceId);
|
|
641
|
+
if (entry?.kind === "link") {
|
|
642
|
+
const client2 = makeClient(entry.cap, entry.key);
|
|
643
|
+
return { client: client2, encryptor: null };
|
|
644
|
+
}
|
|
645
|
+
let client = session.chatClient;
|
|
646
|
+
let trustedAdders = ownerTrustedAdders(session);
|
|
647
|
+
if (entry?.kind === "member") {
|
|
648
|
+
const cap = JSON.parse(entry.cap);
|
|
649
|
+
client = makeClient(cap, session.keys.edPriv);
|
|
650
|
+
if (cap.iss) trustedAdders = [cap.iss];
|
|
651
|
+
const encryptor2 = await buildEncryptor(client, session.keys, spaceId, trustedAdders);
|
|
652
|
+
return encryptor2 ? { client, encryptor: encryptor2 } : null;
|
|
653
|
+
}
|
|
654
|
+
if (hint?.visibility === "public") {
|
|
655
|
+
return { client, encryptor: null };
|
|
656
|
+
}
|
|
657
|
+
const encryptor = await buildEncryptor(client, session.keys, spaceId, trustedAdders);
|
|
658
|
+
return encryptor ? { client, encryptor } : null;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/spaces/registry.ts
|
|
662
|
+
import { ConflictError as ConflictError2, StarfishHttpError } from "@drakkar.software/starfish-client";
|
|
663
|
+
|
|
664
|
+
// src/objects/objects.ts
|
|
665
|
+
var DEFAULT_CATEGORY = "CHANNELS";
|
|
666
|
+
var categoryId = (name) => `cat-${roomSlug(name) || randomId()}`;
|
|
667
|
+
function roomKindToSubtype(kind) {
|
|
668
|
+
switch (kind) {
|
|
669
|
+
case "dm":
|
|
670
|
+
return "dm";
|
|
671
|
+
case "automated":
|
|
672
|
+
return "automation";
|
|
673
|
+
default:
|
|
674
|
+
return "channel";
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function subtypeToRoomKind(subtype) {
|
|
678
|
+
switch (subtype) {
|
|
679
|
+
case "dm":
|
|
680
|
+
return "dm";
|
|
681
|
+
case "automation":
|
|
682
|
+
return "automated";
|
|
683
|
+
default:
|
|
684
|
+
return "channel";
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function compareSiblings(a, b) {
|
|
688
|
+
if (a.order !== b.order) return a.order - b.order;
|
|
689
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
690
|
+
}
|
|
691
|
+
function nextOrder(siblings) {
|
|
692
|
+
let max = 0;
|
|
693
|
+
for (const s of siblings) if (s.order > max) max = s.order;
|
|
694
|
+
return max + 1;
|
|
695
|
+
}
|
|
696
|
+
function buildTree(nodes, includeArchived = false) {
|
|
697
|
+
const live = includeArchived ? nodes : nodes.filter((n) => !n.archived);
|
|
698
|
+
const byId = new Map(live.map((n) => [n.id, n]));
|
|
699
|
+
const effectiveParent = (n) => {
|
|
700
|
+
if (n.parentId == null) return null;
|
|
701
|
+
if (!byId.has(n.parentId)) return null;
|
|
702
|
+
const seen = /* @__PURE__ */ new Set([n.id]);
|
|
703
|
+
let cur = n.parentId;
|
|
704
|
+
while (cur != null) {
|
|
705
|
+
if (seen.has(cur)) return null;
|
|
706
|
+
seen.add(cur);
|
|
707
|
+
const parent = byId.get(cur);
|
|
708
|
+
if (!parent) return null;
|
|
709
|
+
cur = parent.parentId;
|
|
710
|
+
}
|
|
711
|
+
return n.parentId;
|
|
712
|
+
};
|
|
713
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
714
|
+
for (const n of live) {
|
|
715
|
+
const p = effectiveParent(n);
|
|
716
|
+
const bucket = childrenOf.get(p) ?? [];
|
|
717
|
+
bucket.push(n);
|
|
718
|
+
childrenOf.set(p, bucket);
|
|
719
|
+
}
|
|
720
|
+
function attach(parent, depth) {
|
|
721
|
+
return (childrenOf.get(parent) ?? []).slice().sort(compareSiblings).map((n) => ({ ...n, depth, children: attach(n.id, depth + 1) }));
|
|
722
|
+
}
|
|
723
|
+
return attach(null, 0);
|
|
724
|
+
}
|
|
725
|
+
function breadcrumbs(nodes, id) {
|
|
726
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
727
|
+
const trail = [];
|
|
728
|
+
const seen = /* @__PURE__ */ new Set();
|
|
729
|
+
let cur = id;
|
|
730
|
+
while (cur != null && byId.has(cur) && !seen.has(cur)) {
|
|
731
|
+
seen.add(cur);
|
|
732
|
+
const node = byId.get(cur);
|
|
733
|
+
trail.unshift(node);
|
|
734
|
+
cur = node.parentId;
|
|
735
|
+
}
|
|
736
|
+
return trail;
|
|
737
|
+
}
|
|
738
|
+
function ancestors(nodes, id) {
|
|
739
|
+
return breadcrumbs(nodes, id).slice(0, -1);
|
|
740
|
+
}
|
|
741
|
+
function subtreeIds(nodes, rootId) {
|
|
742
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
743
|
+
for (const n of nodes) {
|
|
744
|
+
const bucket = childrenOf.get(n.parentId) ?? [];
|
|
745
|
+
bucket.push(n.id);
|
|
746
|
+
childrenOf.set(n.parentId, bucket);
|
|
747
|
+
}
|
|
748
|
+
const out = /* @__PURE__ */ new Set();
|
|
749
|
+
const walk = (id) => {
|
|
750
|
+
if (out.has(id)) return;
|
|
751
|
+
out.add(id);
|
|
752
|
+
for (const child of childrenOf.get(id) ?? []) walk(child);
|
|
753
|
+
};
|
|
754
|
+
walk(rootId);
|
|
755
|
+
return out;
|
|
756
|
+
}
|
|
757
|
+
function addObject(nodes, input, now) {
|
|
758
|
+
const parentId = input.parentId ?? null;
|
|
759
|
+
const siblings = nodes.filter((n) => n.parentId === parentId);
|
|
760
|
+
const node = {
|
|
761
|
+
id: input.id ?? `obj-${randomId()}`,
|
|
762
|
+
type: input.type,
|
|
763
|
+
...input.subtype ? { subtype: input.subtype } : {},
|
|
764
|
+
parentId,
|
|
765
|
+
order: nextOrder(siblings),
|
|
766
|
+
title: input.title,
|
|
767
|
+
...input.emoji ? { emoji: input.emoji } : {},
|
|
768
|
+
updatedAt: now,
|
|
769
|
+
...input.automation ? { automation: input.automation } : {}
|
|
770
|
+
};
|
|
771
|
+
return { nodes: [...nodes, node], node };
|
|
772
|
+
}
|
|
773
|
+
function patchObject(nodes, id, patch, now) {
|
|
774
|
+
return nodes.map((n) => n.id === id ? { ...n, ...patch, updatedAt: now } : n);
|
|
775
|
+
}
|
|
776
|
+
function reparentObject(nodes, id, parentId, now) {
|
|
777
|
+
if (id === parentId) return nodes;
|
|
778
|
+
if (parentId != null && subtreeIds(nodes, id).has(parentId)) return nodes;
|
|
779
|
+
const siblings = nodes.filter((n) => n.parentId === parentId && n.id !== id);
|
|
780
|
+
return nodes.map((n) => n.id === id ? { ...n, parentId, order: nextOrder(siblings), updatedAt: now } : n);
|
|
781
|
+
}
|
|
782
|
+
function reorderObjects(nodes, orderById, now) {
|
|
783
|
+
return nodes.map((n) => n.id in orderById ? { ...n, order: orderById[n.id], updatedAt: now } : n);
|
|
784
|
+
}
|
|
785
|
+
function archiveObject(nodes, id, now) {
|
|
786
|
+
const ids = subtreeIds(nodes, id);
|
|
787
|
+
return nodes.map((n) => ids.has(n.id) ? { ...n, archived: true, updatedAt: now } : n);
|
|
788
|
+
}
|
|
789
|
+
function objectsToRoomCategories(nodes, spaceId, fallbackCategory) {
|
|
790
|
+
const live = nodes.filter((n) => !n.archived);
|
|
791
|
+
const cats = live.filter((n) => n.type === "category").slice().sort(compareSiblings);
|
|
792
|
+
const rooms = live.filter((n) => n.type === "room");
|
|
793
|
+
if (cats.length === 0 && rooms.length === 0) return null;
|
|
794
|
+
const titleById = new Map(cats.map((c) => [c.id, c.title]));
|
|
795
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
796
|
+
for (const c of cats) buckets.set(c.title, []);
|
|
797
|
+
const toRoom = (n, category) => ({
|
|
798
|
+
id: n.id,
|
|
799
|
+
spaceId,
|
|
800
|
+
category,
|
|
801
|
+
name: n.title,
|
|
802
|
+
kind: subtypeToRoomKind(n.subtype),
|
|
803
|
+
...n.automation ? { automation: n.automation } : {}
|
|
804
|
+
});
|
|
805
|
+
for (const n of rooms.slice().sort(compareSiblings)) {
|
|
806
|
+
const category = n.parentId != null && titleById.get(n.parentId) || fallbackCategory;
|
|
807
|
+
if (!buckets.has(category)) buckets.set(category, []);
|
|
808
|
+
buckets.get(category).push(toRoom(n, category));
|
|
809
|
+
}
|
|
810
|
+
return [...buckets.entries()].map(([name, rs]) => ({ name, rooms: rs }));
|
|
811
|
+
}
|
|
812
|
+
function excludeAutomatedRooms(categories) {
|
|
813
|
+
return categories.map((c) => ({ ...c, rooms: c.rooms.filter((r) => r.kind !== "automated") })).filter((c, i) => c.rooms.length > 0 || categories[i].rooms.length === 0);
|
|
814
|
+
}
|
|
815
|
+
function seedIndexNodes(rooms, now) {
|
|
816
|
+
const out = [];
|
|
817
|
+
const catId = /* @__PURE__ */ new Map();
|
|
818
|
+
let catOrder = 0;
|
|
819
|
+
for (const r of rooms) {
|
|
820
|
+
if (catId.has(r.category)) continue;
|
|
821
|
+
const id = categoryId(r.category);
|
|
822
|
+
catId.set(r.category, id);
|
|
823
|
+
out.push({ id, type: "category", parentId: null, order: catOrder++, title: r.category, updatedAt: now });
|
|
824
|
+
}
|
|
825
|
+
const orderInCat = /* @__PURE__ */ new Map();
|
|
826
|
+
for (const r of rooms) {
|
|
827
|
+
const parentId = catId.get(r.category);
|
|
828
|
+
const order = (orderInCat.get(parentId) ?? 0) + 1;
|
|
829
|
+
orderInCat.set(parentId, order);
|
|
830
|
+
out.push({ id: r.id, type: "room", subtype: roomKindToSubtype(r.kind), parentId, order, title: r.name, updatedAt: now });
|
|
831
|
+
}
|
|
832
|
+
return out;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/spaces/object-index.ts
|
|
836
|
+
import { ConflictError } from "@drakkar.software/starfish-client";
|
|
837
|
+
function indexNodes(plain) {
|
|
838
|
+
return Array.isArray(plain.objects) ? plain.objects : [];
|
|
839
|
+
}
|
|
840
|
+
async function readIndexRooms(client, encryptor, indexPath, spaceId) {
|
|
841
|
+
try {
|
|
842
|
+
const res = await client.pull(indexPath).catch(() => null);
|
|
843
|
+
if (!res?.data) return null;
|
|
844
|
+
const plain = encryptor ? await encryptor.decrypt(res.data) : res.data;
|
|
845
|
+
const cats = objectsToRoomCategories(indexNodes(plain), spaceId, DEFAULT_CATEGORY);
|
|
846
|
+
if (!cats) return null;
|
|
847
|
+
return { rooms: cats.flatMap((c) => c.rooms), categories: cats.map((c) => c.name) };
|
|
848
|
+
} catch {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function readSpaceIndexRooms(session, spaceId, reg) {
|
|
853
|
+
if (reg.owner === null && reg.visibility !== "public") return null;
|
|
854
|
+
try {
|
|
855
|
+
const { encryptor, client } = await getSpaceAccess(spaceId, session, reg);
|
|
856
|
+
return await readIndexRooms(client, encryptor, objIndexPull(spaceId), spaceId);
|
|
857
|
+
} catch {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async function readSpaceRooms(session, spaceId, hint) {
|
|
862
|
+
const access = await buildSpaceAccess(session, spaceId, hint).catch(() => null);
|
|
863
|
+
if (!access) return [];
|
|
864
|
+
const idx = await readIndexRooms(access.client, access.encryptor, objIndexPull(spaceId), spaceId);
|
|
865
|
+
return idx?.rooms ?? [];
|
|
866
|
+
}
|
|
867
|
+
async function pushIndexSeed(client, encryptor, spaceId, rooms) {
|
|
868
|
+
const res = await client.pull(objIndexPull(spaceId)).catch(() => null);
|
|
869
|
+
const existing = res?.data;
|
|
870
|
+
if (existing?._encrypted || Array.isArray(existing?.objects)) return;
|
|
871
|
+
const nodes = seedIndexNodes(rooms, Date.now());
|
|
872
|
+
const payload = encryptor ? await encryptor.encrypt({ objects: nodes }) : { objects: nodes };
|
|
873
|
+
await client.push(objIndexPush(spaceId), payload, res?.hash ?? null);
|
|
874
|
+
}
|
|
875
|
+
async function seedSpaceObjectIndex(session, spaceId, rooms, opts) {
|
|
876
|
+
const { encryptor, client } = await getSpaceAccess(spaceId, session, {
|
|
877
|
+
owner: session.userId,
|
|
878
|
+
members: [],
|
|
879
|
+
visibility: opts?.visibility
|
|
880
|
+
});
|
|
881
|
+
await pushIndexSeed(client, encryptor, spaceId, rooms);
|
|
882
|
+
}
|
|
883
|
+
async function updateObjectIndex(session, spaceId, mutator, reg) {
|
|
884
|
+
const { client, encryptor } = await getSpaceAccess(spaceId, session, reg ?? null);
|
|
885
|
+
const pullPath = objIndexPull(spaceId);
|
|
886
|
+
const pushPath = objIndexPush(spaceId);
|
|
887
|
+
const MAX_ATTEMPTS = 3;
|
|
888
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
889
|
+
const res = await client.pull(pullPath).catch(() => null);
|
|
890
|
+
const raw = res?.data;
|
|
891
|
+
const plain = raw ? encryptor ? await encryptor.decrypt(raw) : raw : {};
|
|
892
|
+
const cur = Array.isArray(plain.objects) ? plain.objects : [];
|
|
893
|
+
const next = mutator(cur, Date.now());
|
|
894
|
+
if (!next) return;
|
|
895
|
+
const payload = encryptor ? await encryptor.encrypt({ objects: next }) : { objects: next };
|
|
896
|
+
try {
|
|
897
|
+
await client.push(pushPath, payload, res?.hash ?? null);
|
|
898
|
+
return;
|
|
899
|
+
} catch (err) {
|
|
900
|
+
if (err instanceof ConflictError && attempt < MAX_ATTEMPTS - 1) continue;
|
|
901
|
+
throw err;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/spaces/registry.ts
|
|
907
|
+
var spaceMetaListeners = /* @__PURE__ */ new Set();
|
|
908
|
+
function onSpaceMeta(fn) {
|
|
909
|
+
spaceMetaListeners.add(fn);
|
|
910
|
+
return () => {
|
|
911
|
+
spaceMetaListeners.delete(fn);
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function broadcastSpaceMeta(spaceId, meta) {
|
|
915
|
+
for (const fn of spaceMetaListeners) fn(spaceId, meta);
|
|
916
|
+
}
|
|
917
|
+
function coerceDms(raw) {
|
|
918
|
+
const src = raw && typeof raw === "object" ? raw : {};
|
|
919
|
+
const out = {};
|
|
920
|
+
for (const [k, v] of Object.entries(src)) if (typeof v === "string") out[k] = v;
|
|
921
|
+
return out;
|
|
922
|
+
}
|
|
923
|
+
function coerceMutes(raw) {
|
|
924
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
925
|
+
const pick = (v) => v && typeof v === "object" ? v : {};
|
|
926
|
+
return { rooms: pick(r.rooms), spaces: pick(r.spaces) };
|
|
927
|
+
}
|
|
928
|
+
function coerceReads(raw) {
|
|
929
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
930
|
+
const src = r.rooms && typeof r.rooms === "object" ? r.rooms : {};
|
|
931
|
+
const rooms = {};
|
|
932
|
+
for (const [id, v] of Object.entries(src)) if (typeof v === "number" && Number.isFinite(v)) rooms[id] = v;
|
|
933
|
+
return { rooms };
|
|
934
|
+
}
|
|
935
|
+
function coerceQuickReactions(raw) {
|
|
936
|
+
return Array.isArray(raw) ? raw.filter((v) => typeof v === "string") : [];
|
|
937
|
+
}
|
|
938
|
+
function coerceArchivedDms(raw) {
|
|
939
|
+
const src = raw && typeof raw === "object" ? raw : {};
|
|
940
|
+
const out = {};
|
|
941
|
+
for (const [k, v] of Object.entries(src)) if (v === true) out[k] = true;
|
|
942
|
+
return out;
|
|
943
|
+
}
|
|
944
|
+
async function pullSpacesDoc(client, userId) {
|
|
945
|
+
const res = await client.pull(spacesPull(userId)).catch((err) => {
|
|
946
|
+
if (err instanceof StarfishHttpError && err.status === 404) return null;
|
|
947
|
+
throw err;
|
|
948
|
+
});
|
|
949
|
+
const data = res?.data;
|
|
950
|
+
return {
|
|
951
|
+
spaces: Array.isArray(data?.spaces) ? data.spaces : [],
|
|
952
|
+
caps: data?.caps && typeof data.caps === "object" ? data.caps : {},
|
|
953
|
+
mutes: coerceMutes(data?.mutes),
|
|
954
|
+
reads: coerceReads(data?.reads),
|
|
955
|
+
pubAccess: data?.pubAccess && typeof data.pubAccess === "object" ? data.pubAccess : {},
|
|
956
|
+
dms: coerceDms(data?.dms),
|
|
957
|
+
quickReactions: coerceQuickReactions(data?.quickReactions),
|
|
958
|
+
archivedDms: coerceArchivedDms(data?.archivedDms),
|
|
959
|
+
hash: res?.hash ?? null
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
async function readSpaces(client, userId) {
|
|
963
|
+
try {
|
|
964
|
+
return await pullSpacesDoc(client, userId);
|
|
965
|
+
} catch (err) {
|
|
966
|
+
console.error("[readSpaces] failed to pull spaces registry", err);
|
|
967
|
+
return {
|
|
968
|
+
spaces: [],
|
|
969
|
+
caps: {},
|
|
970
|
+
mutes: coerceMutes(void 0),
|
|
971
|
+
reads: coerceReads(void 0),
|
|
972
|
+
pubAccess: {},
|
|
973
|
+
dms: {},
|
|
974
|
+
quickReactions: [],
|
|
975
|
+
archivedDms: {},
|
|
976
|
+
hash: null
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
async function updateSpacesDoc(client, userId, mutator) {
|
|
981
|
+
const MAX_ATTEMPTS = 3;
|
|
982
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
983
|
+
const { spaces, caps, mutes, reads, pubAccess, dms, quickReactions, archivedDms, hash } = await pullSpacesDoc(client, userId);
|
|
984
|
+
const cur = { spaces, caps, pubAccess };
|
|
985
|
+
const next = mutator(cur);
|
|
986
|
+
if (next === cur) return;
|
|
987
|
+
try {
|
|
988
|
+
await client.push(
|
|
989
|
+
spacesPush(userId),
|
|
990
|
+
{ v: 1, spaces: next.spaces, caps: next.caps, mutes, reads, pubAccess: next.pubAccess, dms, quickReactions, archivedDms },
|
|
991
|
+
hash
|
|
992
|
+
);
|
|
993
|
+
return;
|
|
994
|
+
} catch (err) {
|
|
995
|
+
if (err instanceof ConflictError2 && attempt < MAX_ATTEMPTS - 1) continue;
|
|
996
|
+
throw err;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async function updateMutesDoc(client, userId, mutator) {
|
|
1001
|
+
const MAX_ATTEMPTS = 3;
|
|
1002
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1003
|
+
const { spaces, caps, mutes, reads, pubAccess, dms, quickReactions, archivedDms, hash } = await pullSpacesDoc(client, userId);
|
|
1004
|
+
const next = mutator(mutes);
|
|
1005
|
+
if (!next) return;
|
|
1006
|
+
try {
|
|
1007
|
+
await client.push(spacesPush(userId), { v: 1, spaces, caps, mutes: next, reads, pubAccess, dms, quickReactions, archivedDms }, hash);
|
|
1008
|
+
return;
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
if (err instanceof ConflictError2 && attempt < MAX_ATTEMPTS - 1) continue;
|
|
1011
|
+
throw err;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
async function updateReadsDoc(client, userId, mutator) {
|
|
1016
|
+
const MAX_ATTEMPTS = 3;
|
|
1017
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1018
|
+
const { spaces, caps, mutes, reads, pubAccess, dms, quickReactions, archivedDms, hash } = await pullSpacesDoc(client, userId);
|
|
1019
|
+
const next = mutator(reads);
|
|
1020
|
+
if (!next) return;
|
|
1021
|
+
try {
|
|
1022
|
+
await client.push(spacesPush(userId), { v: 1, spaces, caps, mutes, reads: next, pubAccess, dms, quickReactions, archivedDms }, hash);
|
|
1023
|
+
return;
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
if (err instanceof ConflictError2 && attempt < MAX_ATTEMPTS - 1) continue;
|
|
1026
|
+
throw err;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
async function updateDmsDoc(client, userId, mutator) {
|
|
1031
|
+
const MAX_ATTEMPTS = 3;
|
|
1032
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1033
|
+
const { spaces, caps, mutes, reads, pubAccess, dms, quickReactions, archivedDms, hash } = await pullSpacesDoc(client, userId);
|
|
1034
|
+
const next = mutator(dms);
|
|
1035
|
+
if (!next) return;
|
|
1036
|
+
try {
|
|
1037
|
+
await client.push(spacesPush(userId), { v: 1, spaces, caps, mutes, reads, pubAccess, dms: next, quickReactions, archivedDms }, hash);
|
|
1038
|
+
return;
|
|
1039
|
+
} catch (err) {
|
|
1040
|
+
if (err instanceof ConflictError2 && attempt < MAX_ATTEMPTS - 1) continue;
|
|
1041
|
+
throw err;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
async function updateQuickReactionsDoc(client, userId, mutator) {
|
|
1046
|
+
const MAX_ATTEMPTS = 3;
|
|
1047
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1048
|
+
const { spaces, caps, mutes, reads, pubAccess, dms, quickReactions, archivedDms, hash } = await pullSpacesDoc(client, userId);
|
|
1049
|
+
const next = mutator(quickReactions);
|
|
1050
|
+
if (!next) return;
|
|
1051
|
+
try {
|
|
1052
|
+
await client.push(spacesPush(userId), { v: 1, spaces, caps, mutes, reads, pubAccess, dms, quickReactions: next, archivedDms }, hash);
|
|
1053
|
+
return;
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
if (err instanceof ConflictError2 && attempt < MAX_ATTEMPTS - 1) continue;
|
|
1056
|
+
throw err;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
async function updateArchivedDmsDoc(client, userId, mutator) {
|
|
1061
|
+
const MAX_ATTEMPTS = 3;
|
|
1062
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1063
|
+
const { spaces, caps, mutes, reads, pubAccess, dms, quickReactions, archivedDms, hash } = await pullSpacesDoc(client, userId);
|
|
1064
|
+
const next = mutator(archivedDms);
|
|
1065
|
+
if (!next) return;
|
|
1066
|
+
try {
|
|
1067
|
+
await client.push(spacesPush(userId), { v: 1, spaces, caps, mutes, reads, pubAccess, dms, quickReactions, archivedDms: next }, hash);
|
|
1068
|
+
return;
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
if (err instanceof ConflictError2 && attempt < MAX_ATTEMPTS - 1) continue;
|
|
1071
|
+
throw err;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
async function setDmMapping(client, userId, peerUserId, spaceId) {
|
|
1076
|
+
await updateDmsDoc(client, userId, (cur) => cur[peerUserId] === spaceId ? null : { ...cur, [peerUserId]: spaceId });
|
|
1077
|
+
}
|
|
1078
|
+
async function writeSpaces(client, userId, spaces, _hash) {
|
|
1079
|
+
await updateSpacesDoc(client, userId, (cur) => ({ spaces, caps: cur.caps, pubAccess: cur.pubAccess }));
|
|
1080
|
+
}
|
|
1081
|
+
async function reorderSpaces(client, userId, order) {
|
|
1082
|
+
await updateSpacesDoc(client, userId, (cur) => {
|
|
1083
|
+
const byId = new Map(cur.spaces.map((s) => [s.id, s]));
|
|
1084
|
+
const next = [];
|
|
1085
|
+
for (const id of order) {
|
|
1086
|
+
const s = byId.get(id);
|
|
1087
|
+
if (s) {
|
|
1088
|
+
next.push(s);
|
|
1089
|
+
byId.delete(id);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
for (const s of cur.spaces) if (byId.has(s.id)) next.push(s);
|
|
1093
|
+
const unchanged = next.length === cur.spaces.length && next.every((s, i) => s === cur.spaces[i]);
|
|
1094
|
+
if (unchanged) return cur;
|
|
1095
|
+
return { spaces: next, caps: cur.caps, pubAccess: cur.pubAccess };
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
function newSpaceId() {
|
|
1099
|
+
return `sp-${randomId()}`;
|
|
1100
|
+
}
|
|
1101
|
+
function normalizeCategories(rooms, stored) {
|
|
1102
|
+
const distinct = [];
|
|
1103
|
+
for (const r of rooms) if (r.category && !distinct.includes(r.category)) distinct.push(r.category);
|
|
1104
|
+
const list = Array.isArray(stored) ? stored.filter((c) => typeof c === "string") : [];
|
|
1105
|
+
if (!list.length) return distinct;
|
|
1106
|
+
const result = [...list];
|
|
1107
|
+
for (const c of distinct) if (!result.includes(c)) result.push(c);
|
|
1108
|
+
return result;
|
|
1109
|
+
}
|
|
1110
|
+
async function readRooms(client, spaceId) {
|
|
1111
|
+
const res = await client.pull(roomsRegistryPull(spaceId)).catch((err) => {
|
|
1112
|
+
if (err instanceof StarfishHttpError && err.status === 404) return null;
|
|
1113
|
+
throw err;
|
|
1114
|
+
});
|
|
1115
|
+
const data = res?.data;
|
|
1116
|
+
return {
|
|
1117
|
+
owner: typeof data?.owner === "string" ? data.owner : null,
|
|
1118
|
+
members: Array.isArray(data?.members) ? data.members.filter((m) => typeof m === "string") : [],
|
|
1119
|
+
visibility: data?.visibility === "public" ? "public" : null,
|
|
1120
|
+
name: typeof data?.name === "string" ? data.name : null,
|
|
1121
|
+
image: typeof data?.image === "string" ? data.image : null,
|
|
1122
|
+
hash: res?.hash ?? null
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
async function writeRooms(client, spaceId, owner, members, hash, meta) {
|
|
1126
|
+
const name = meta?.name?.trim() || void 0;
|
|
1127
|
+
const image = meta?.image || void 0;
|
|
1128
|
+
const visibility = meta?.visibility === "public" ? "public" : void 0;
|
|
1129
|
+
await client.push(
|
|
1130
|
+
roomsRegistryPush(spaceId),
|
|
1131
|
+
{ v: 1, owner, members, ...visibility ? { visibility } : {}, ...name ? { name } : {}, ...image ? { image } : {} },
|
|
1132
|
+
hash
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
async function addSpaceMember(client, spaceId, ownerUserId, memberUserId) {
|
|
1136
|
+
const { owner, members, visibility, name, image, hash } = await readRooms(client, spaceId);
|
|
1137
|
+
if (memberUserId === (owner ?? ownerUserId) || members.includes(memberUserId)) return;
|
|
1138
|
+
await writeRooms(client, spaceId, owner ?? ownerUserId, [...members, memberUserId], hash, { name, image, visibility: visibility ?? void 0 });
|
|
1139
|
+
}
|
|
1140
|
+
async function removeSpaceMember(client, spaceId, memberUserId) {
|
|
1141
|
+
const { owner, members, visibility, name, image, hash } = await readRooms(client, spaceId);
|
|
1142
|
+
if (!members.includes(memberUserId)) return;
|
|
1143
|
+
await writeRooms(client, spaceId, owner ?? memberUserId, members.filter((m) => m !== memberUserId), hash, { name, image, visibility: visibility ?? void 0 });
|
|
1144
|
+
}
|
|
1145
|
+
async function addJoinedSpace(client, userId, space) {
|
|
1146
|
+
await updateSpacesDoc(
|
|
1147
|
+
client,
|
|
1148
|
+
userId,
|
|
1149
|
+
(cur) => cur.spaces.some((s) => s.id === space.id) ? cur : { spaces: [...cur.spaces, space], caps: cur.caps, pubAccess: cur.pubAccess }
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
async function addJoinedSpaceWithCap(client, userId, space, capJson) {
|
|
1153
|
+
await updateSpacesDoc(client, userId, (cur) => ({
|
|
1154
|
+
spaces: cur.spaces.some((s) => s.id === space.id) ? cur.spaces : [...cur.spaces, space],
|
|
1155
|
+
caps: { ...cur.caps, [space.id]: capJson },
|
|
1156
|
+
pubAccess: cur.pubAccess
|
|
1157
|
+
}));
|
|
1158
|
+
}
|
|
1159
|
+
async function addJoinedSpaceWithLinkAccess(client, userId, space, sealed) {
|
|
1160
|
+
await updateSpacesDoc(client, userId, (cur) => ({
|
|
1161
|
+
spaces: cur.spaces.some((s) => s.id === space.id) ? cur.spaces : [...cur.spaces, space],
|
|
1162
|
+
caps: cur.caps,
|
|
1163
|
+
pubAccess: { ...cur.pubAccess, [space.id]: sealed }
|
|
1164
|
+
}));
|
|
1165
|
+
}
|
|
1166
|
+
async function createSpace(session, name, opts) {
|
|
1167
|
+
const { accountClient, userId } = session;
|
|
1168
|
+
const { spaces, hash } = await readSpaces(accountClient, userId);
|
|
1169
|
+
const trimmed = name.trim() || "New Space";
|
|
1170
|
+
const visibility = opts?.visibility ?? "private";
|
|
1171
|
+
const id = newSpaceId();
|
|
1172
|
+
const space = {
|
|
1173
|
+
id,
|
|
1174
|
+
name: trimmed,
|
|
1175
|
+
short: trimmed.slice(0, 2).toUpperCase(),
|
|
1176
|
+
members: 1,
|
|
1177
|
+
...visibility === "public" ? { visibility: "public", ownerId: userId, write: true } : {}
|
|
1178
|
+
};
|
|
1179
|
+
await writeRooms(accountClient, id, userId, [], null, { name: trimmed, visibility: visibility === "public" ? "public" : void 0 });
|
|
1180
|
+
await seedSpaceObjectIndex(session, id, [{ id: `${id}-general`, name: "general", kind: "channel", category: DEFAULT_CATEGORY }], { visibility });
|
|
1181
|
+
await writeSpaces(accountClient, userId, [...spaces, space], hash);
|
|
1182
|
+
return space;
|
|
1183
|
+
}
|
|
1184
|
+
var CategoryError = class extends Error {
|
|
1185
|
+
};
|
|
1186
|
+
async function reconcileSpaceMeta(client, userId, spaceId, shared2, knownSpaces) {
|
|
1187
|
+
const sharedName = typeof shared2.name === "string" && shared2.name.trim() ? shared2.name : null;
|
|
1188
|
+
const sharedImage = typeof shared2.image === "string" && shared2.image ? shared2.image : null;
|
|
1189
|
+
if (sharedName === null && sharedImage === null) return;
|
|
1190
|
+
const known = knownSpaces?.find((s) => s.id === spaceId);
|
|
1191
|
+
if (known) {
|
|
1192
|
+
const name2 = sharedName ?? known.name;
|
|
1193
|
+
const short2 = name2.slice(0, 2).toUpperCase();
|
|
1194
|
+
const image2 = sharedImage ?? known.image;
|
|
1195
|
+
if (name2 === known.name && short2 === known.short && (image2 ?? null) === (known.image ?? null)) return;
|
|
1196
|
+
}
|
|
1197
|
+
const { spaces, hash } = await readSpaces(client, userId);
|
|
1198
|
+
const cur = spaces.find((s) => s.id === spaceId);
|
|
1199
|
+
if (!cur) return;
|
|
1200
|
+
const name = sharedName ?? cur.name;
|
|
1201
|
+
const image = sharedImage ?? cur.image;
|
|
1202
|
+
const short = name.slice(0, 2).toUpperCase();
|
|
1203
|
+
if (name === cur.name && short === cur.short && (image ?? null) === (cur.image ?? null)) return;
|
|
1204
|
+
const next = spaces.map((s) => s.id === spaceId ? { ...s, name, short, image } : s);
|
|
1205
|
+
await writeSpaces(client, userId, next, hash);
|
|
1206
|
+
broadcastSpaceMeta(spaceId, { name, short, image });
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/spaces/members.ts
|
|
1210
|
+
import { generateDeviceKeys } from "@drakkar.software/starfish-identities";
|
|
1211
|
+
import { addCollectionRecipient } from "@drakkar.software/starfish-keyring";
|
|
1212
|
+
import { mintMemberCap } from "@drakkar.software/starfish-sharing";
|
|
1213
|
+
|
|
1214
|
+
// src/sync/base64url.ts
|
|
1215
|
+
function toBase64Url(json) {
|
|
1216
|
+
const bytes = new TextEncoder().encode(json);
|
|
1217
|
+
let bin = "";
|
|
1218
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
1219
|
+
const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(json, "utf-8").toString("base64");
|
|
1220
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1221
|
+
}
|
|
1222
|
+
function fromBase64Url(b64url) {
|
|
1223
|
+
const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
1224
|
+
if (typeof atob === "function") {
|
|
1225
|
+
const bin = atob(b64);
|
|
1226
|
+
const bytes = new Uint8Array(bin.length);
|
|
1227
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1228
|
+
return new TextDecoder().decode(bytes);
|
|
1229
|
+
}
|
|
1230
|
+
return Buffer.from(b64, "base64").toString("utf-8");
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/spaces/members.ts
|
|
1234
|
+
function makeJoinRequest(session) {
|
|
1235
|
+
const req2 = { edPub: session.keys.edPub, kemPub: session.keys.kemPub, userId: session.userId };
|
|
1236
|
+
return JSON.stringify(req2);
|
|
1237
|
+
}
|
|
1238
|
+
function isAlreadyPresentRecipient(err) {
|
|
1239
|
+
return err instanceof Error && /already present in epoch/.test(err.message);
|
|
1240
|
+
}
|
|
1241
|
+
async function addDeviceToSpaceKeyring(session, spaceId, recipient) {
|
|
1242
|
+
try {
|
|
1243
|
+
await addCollectionRecipient(
|
|
1244
|
+
session.chatClient,
|
|
1245
|
+
keyringName(spaceId),
|
|
1246
|
+
{ subKem: recipient.kemPub, userId: recipient.userId, label: recipient.userId.slice(0, 8) },
|
|
1247
|
+
{ edPriv: session.keys.edPriv, edPub: session.keys.edPub, kemPriv: session.keys.kemPriv },
|
|
1248
|
+
{ trustedAdders: [session.keys.edPub] }
|
|
1249
|
+
);
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
if (!isAlreadyPresentRecipient(err)) throw err;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async function inviteToSpace(session, spaceId, requestJson, canWrite = true, spaceName) {
|
|
1255
|
+
const req2 = JSON.parse(requestJson);
|
|
1256
|
+
if (!req2.edPub || !req2.kemPub || !req2.userId) throw new Error("That is not a valid join request.");
|
|
1257
|
+
await addDeviceToSpaceKeyring(session, spaceId, { kemPub: req2.kemPub, userId: req2.userId });
|
|
1258
|
+
await addSpaceMember(session.accountClient, spaceId, session.userId, req2.userId);
|
|
1259
|
+
const cap = await mintMemberCap(
|
|
1260
|
+
session.keys.edPriv,
|
|
1261
|
+
session.keys.edPub,
|
|
1262
|
+
{ edPubHex: req2.edPub, kemPubHex: req2.kemPub, userIdHex: req2.userId },
|
|
1263
|
+
"chat",
|
|
1264
|
+
spaceMemberScope(spaceId, canWrite)
|
|
1265
|
+
);
|
|
1266
|
+
let name = spaceName?.trim();
|
|
1267
|
+
if (!name) {
|
|
1268
|
+
const { spaces } = await readSpaces(session.accountClient, session.userId);
|
|
1269
|
+
name = spaces.find((s) => s.id === spaceId)?.name ?? "Space";
|
|
1270
|
+
}
|
|
1271
|
+
const invite = { spaceId, spaceName: name, cap };
|
|
1272
|
+
return JSON.stringify(invite);
|
|
1273
|
+
}
|
|
1274
|
+
async function acceptSpaceInvite(session, inviteJson) {
|
|
1275
|
+
const inv = JSON.parse(inviteJson);
|
|
1276
|
+
const cap = inv.cap;
|
|
1277
|
+
if (!cap || !inv.spaceId) throw new Error("That is not a valid space invite.");
|
|
1278
|
+
if (cap.kind !== "member") throw new Error("That is not a valid space invite.");
|
|
1279
|
+
if (!cap.sub || cap.sub !== session.keys.edPub) {
|
|
1280
|
+
throw new Error("This invite was issued for a different identity.");
|
|
1281
|
+
}
|
|
1282
|
+
if (!cap.iss) throw new Error("This invite is missing its issuer.");
|
|
1283
|
+
const spaceId = inv.spaceId;
|
|
1284
|
+
const client = makeClient(cap, session.keys.edPriv);
|
|
1285
|
+
const enc = await buildEncryptor(client, session.keys, spaceId, [cap.iss]);
|
|
1286
|
+
if (!enc) throw new Error("Accepted, but you're not in the space keyring yet \u2014 ask the owner to re-invite.");
|
|
1287
|
+
const capJson = JSON.stringify(cap);
|
|
1288
|
+
const name = inv.spaceName?.trim() || `space-${spaceId.slice(-6)}`;
|
|
1289
|
+
const space = { id: spaceId, name, short: name.slice(0, 2).toUpperCase(), members: 1 };
|
|
1290
|
+
await addJoinedSpaceWithCap(session.accountClient, session.userId, space, capJson);
|
|
1291
|
+
saveSpaceAccessEntry(spaceId, { kind: "member", cap: capJson });
|
|
1292
|
+
return space;
|
|
1293
|
+
}
|
|
1294
|
+
function encodeSpaceInviteLink(origin, token) {
|
|
1295
|
+
const base = origin.replace(/\/+$/, "");
|
|
1296
|
+
return `${base}/join#${toBase64Url(JSON.stringify(token))}`;
|
|
1297
|
+
}
|
|
1298
|
+
function decodeSpaceInviteLink(fragment) {
|
|
1299
|
+
const frag = fragment.startsWith("#") ? fragment.slice(1) : fragment;
|
|
1300
|
+
const tok = JSON.parse(fromBase64Url(frag));
|
|
1301
|
+
if (!tok || !tok.spaceId || !tok.cap || !tok.key) {
|
|
1302
|
+
throw new Error("That space invite link is malformed or incomplete.");
|
|
1303
|
+
}
|
|
1304
|
+
return {
|
|
1305
|
+
v: 1,
|
|
1306
|
+
spaceId: tok.spaceId,
|
|
1307
|
+
spaceName: tok.spaceName ?? "Space",
|
|
1308
|
+
cap: tok.cap,
|
|
1309
|
+
key: tok.key,
|
|
1310
|
+
write: !!tok.write
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
async function createSpaceInviteLink(session, spaceId, spaceName, write, origin) {
|
|
1314
|
+
const ek = generateDeviceKeys();
|
|
1315
|
+
const ephemeralUserId = await userIdFromEdPub(ek.edPub);
|
|
1316
|
+
const cap = await mintMemberCap(
|
|
1317
|
+
session.keys.edPriv,
|
|
1318
|
+
session.keys.edPub,
|
|
1319
|
+
{ edPubHex: ek.edPub, kemPubHex: ek.kemPub, userIdHex: ephemeralUserId },
|
|
1320
|
+
"chat",
|
|
1321
|
+
spaceMemberScope(spaceId, write)
|
|
1322
|
+
);
|
|
1323
|
+
await addSpaceMember(session.accountClient, spaceId, session.userId, ephemeralUserId);
|
|
1324
|
+
const token = { v: 1, spaceId, spaceName, cap, key: ek.edPriv, write };
|
|
1325
|
+
return { token, link: encodeSpaceInviteLink(origin, token) };
|
|
1326
|
+
}
|
|
1327
|
+
async function joinSpaceByLink(session, token) {
|
|
1328
|
+
const cap = token.cap;
|
|
1329
|
+
const ownerId = cap.iss ? await userIdFromEdPub(cap.iss) : void 0;
|
|
1330
|
+
const name = token.spaceName.trim() || `space-${token.spaceId.slice(-6)}`;
|
|
1331
|
+
const space = {
|
|
1332
|
+
id: token.spaceId,
|
|
1333
|
+
name,
|
|
1334
|
+
short: name.slice(0, 2).toUpperCase(),
|
|
1335
|
+
members: 1,
|
|
1336
|
+
visibility: "public",
|
|
1337
|
+
...ownerId ? { ownerId } : {},
|
|
1338
|
+
write: token.write
|
|
1339
|
+
};
|
|
1340
|
+
const accessPayload = { cap: token.cap, key: token.key, write: token.write };
|
|
1341
|
+
const sealed = await sealToSelf(session, JSON.stringify(accessPayload));
|
|
1342
|
+
await addJoinedSpaceWithLinkAccess(session.accountClient, session.userId, space, sealed);
|
|
1343
|
+
saveSpaceAccessEntry(token.spaceId, { kind: "link", cap: token.cap, key: token.key, write: token.write });
|
|
1344
|
+
return space;
|
|
1345
|
+
}
|
|
1346
|
+
async function recoverSpaceAccess(session, server) {
|
|
1347
|
+
const linkAccess = {};
|
|
1348
|
+
for (const [spaceId, sealed] of Object.entries(server.pubAccess)) {
|
|
1349
|
+
try {
|
|
1350
|
+
const raw = await unsealFromSelf(session, sealed);
|
|
1351
|
+
const parsed = JSON.parse(raw);
|
|
1352
|
+
if (parsed.cap && parsed.key) linkAccess[spaceId] = parsed;
|
|
1353
|
+
} catch (e) {
|
|
1354
|
+
console.error("[octospaces] recoverSpaceAccess: failed to unseal", spaceId, e);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
await hydrateSpaceAccessStore(session.userId, server.caps, linkAccess);
|
|
1358
|
+
const local = localSpaceAccessEntries();
|
|
1359
|
+
const missingMemberCaps = Object.entries(local).filter(([id, e]) => e.kind === "member" && !(id in server.caps));
|
|
1360
|
+
const missingLinks = Object.entries(local).filter(([id, e]) => e.kind === "link" && !(id in server.pubAccess));
|
|
1361
|
+
if (missingMemberCaps.length === 0 && missingLinks.length === 0) return;
|
|
1362
|
+
try {
|
|
1363
|
+
const newCaps = {};
|
|
1364
|
+
for (const [id, e] of missingMemberCaps) if (e.kind === "member") newCaps[id] = e.cap;
|
|
1365
|
+
const newPubAccess = {};
|
|
1366
|
+
for (const [id, e] of missingLinks) {
|
|
1367
|
+
if (e.kind === "link") {
|
|
1368
|
+
newPubAccess[id] = await sealToSelf(session, JSON.stringify({ cap: e.cap, key: e.key, write: e.write }));
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
await updateSpacesDoc(session.accountClient, session.userId, (cur) => ({
|
|
1372
|
+
spaces: cur.spaces,
|
|
1373
|
+
caps: { ...cur.caps, ...newCaps },
|
|
1374
|
+
pubAccess: { ...cur.pubAccess, ...newPubAccess }
|
|
1375
|
+
}));
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
console.error("[octospaces] recoverSpaceAccess: backfill failed", e);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// src/sync/pairing.ts
|
|
1382
|
+
import { StarfishClient as StarfishClient2 } from "@drakkar.software/starfish-client";
|
|
1383
|
+
import {
|
|
1384
|
+
installPairingBundle,
|
|
1385
|
+
openWithPassphrase,
|
|
1386
|
+
provisionDevice,
|
|
1387
|
+
sealWithPassphrase
|
|
1388
|
+
} from "@drakkar.software/starfish-identities";
|
|
1389
|
+
var PAIR_PREFIX = "octospaces-pair:";
|
|
1390
|
+
var LINKED_DEVICE_TTL_SEC = 365 * 24 * 60 * 60;
|
|
1391
|
+
function anonClient() {
|
|
1392
|
+
return new StarfishClient2({ baseUrl: getSyncBase(), namespace: getSyncNamespace(), fetch: fetchWithTimeout() });
|
|
1393
|
+
}
|
|
1394
|
+
function randomNonce() {
|
|
1395
|
+
const b = new Uint8Array(16);
|
|
1396
|
+
globalThis.crypto.getRandomValues(b);
|
|
1397
|
+
return bytesToHex(b);
|
|
1398
|
+
}
|
|
1399
|
+
async function startDevicePairing(session, pin) {
|
|
1400
|
+
const { deviceKeys, bundle } = await provisionDevice(
|
|
1401
|
+
{ edPriv: session.keys.edPriv, edPub: session.keys.edPub },
|
|
1402
|
+
{ scope: linkedDeviceScope(session.userId), ttlSec: LINKED_DEVICE_TTL_SEC }
|
|
1403
|
+
);
|
|
1404
|
+
const { spaces, caps } = await readSpaces(session.accountClient, session.userId);
|
|
1405
|
+
for (const space of spaces) {
|
|
1406
|
+
if (caps[space.id]) continue;
|
|
1407
|
+
try {
|
|
1408
|
+
await addDeviceToSpaceKeyring(session, space.id, { kemPub: deviceKeys.kemPub, userId: session.userId });
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
console.log("[pairing] keyring grant failed", { spaceId: space.id, error: String(err?.message ?? err) });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
const blob = JSON.stringify({ v: 1, keys: deviceKeys, bundle });
|
|
1414
|
+
const sealed = await sealWithPassphrase(pin, new TextEncoder().encode(blob));
|
|
1415
|
+
const nonce = randomNonce();
|
|
1416
|
+
await anonClient().push(`/push/_pairing/${nonce}`, sealed, null);
|
|
1417
|
+
return `${PAIR_PREFIX}${nonce}.${session.keys.edPub}`;
|
|
1418
|
+
}
|
|
1419
|
+
async function completeDevicePairing(payload, pin) {
|
|
1420
|
+
const body = (payload.startsWith(PAIR_PREFIX) || payload.includes("-pair:") ? payload.slice(payload.indexOf(":") + 1) : payload).trim();
|
|
1421
|
+
const [nonce, expectedRootEdPub] = body.split(".");
|
|
1422
|
+
const res = await anonClient().pull(`/pull/_pairing/${nonce}`).catch(() => null);
|
|
1423
|
+
const sealed = res?.data;
|
|
1424
|
+
if (!sealed || !sealed.v) throw new Error("Pairing code not found or expired.");
|
|
1425
|
+
let inner;
|
|
1426
|
+
try {
|
|
1427
|
+
inner = await openWithPassphrase(pin, sealed);
|
|
1428
|
+
} catch {
|
|
1429
|
+
throw new Error("Wrong PIN or corrupted pairing code.");
|
|
1430
|
+
}
|
|
1431
|
+
const blob = JSON.parse(new TextDecoder().decode(inner));
|
|
1432
|
+
const opts = expectedRootEdPub ? { expectedRootEdPub } : {};
|
|
1433
|
+
const installed = await installPairingBundle(
|
|
1434
|
+
blob.bundle,
|
|
1435
|
+
blob.keys,
|
|
1436
|
+
opts
|
|
1437
|
+
);
|
|
1438
|
+
const userId = installed.credentials.userId;
|
|
1439
|
+
return {
|
|
1440
|
+
userId,
|
|
1441
|
+
fingerprint: fingerprintFromUserId(userId),
|
|
1442
|
+
deviceKeys: installed.credentials.device,
|
|
1443
|
+
capCert: installed.credentials.capCert
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/sync/base64.ts
|
|
1448
|
+
var CHUNK = 24576;
|
|
1449
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1450
|
+
var REVERSE = (() => {
|
|
1451
|
+
const table = new Int16Array(128).fill(-1);
|
|
1452
|
+
for (let i = 0; i < ALPHABET.length; i++) table[ALPHABET.charCodeAt(i)] = i;
|
|
1453
|
+
return table;
|
|
1454
|
+
})();
|
|
1455
|
+
var nativeCodec = typeof globalThis !== "undefined" && typeof globalThis.btoa === "function" && typeof globalThis.atob === "function";
|
|
1456
|
+
function encodeViaBtoa(data) {
|
|
1457
|
+
let binary = "";
|
|
1458
|
+
for (let i = 0; i < data.length; i += CHUNK) {
|
|
1459
|
+
binary += String.fromCharCode.apply(null, data.subarray(i, i + CHUNK));
|
|
1460
|
+
}
|
|
1461
|
+
return globalThis.btoa(binary);
|
|
1462
|
+
}
|
|
1463
|
+
function decodeViaAtob(encoded) {
|
|
1464
|
+
const binary = globalThis.atob(encoded);
|
|
1465
|
+
const out = new Uint8Array(binary.length);
|
|
1466
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
1467
|
+
return out;
|
|
1468
|
+
}
|
|
1469
|
+
function encodePure(data) {
|
|
1470
|
+
const len = data.length;
|
|
1471
|
+
const full = len - len % 3;
|
|
1472
|
+
const parts = [];
|
|
1473
|
+
for (let start = 0; start < full; start += CHUNK) {
|
|
1474
|
+
const stop = Math.min(start + CHUNK, full);
|
|
1475
|
+
let s = "";
|
|
1476
|
+
for (let i = start; i < stop; i += 3) {
|
|
1477
|
+
const n = data[i] << 16 | data[i + 1] << 8 | data[i + 2];
|
|
1478
|
+
s += ALPHABET[n >> 18 & 63] + ALPHABET[n >> 12 & 63] + ALPHABET[n >> 6 & 63] + ALPHABET[n & 63];
|
|
1479
|
+
}
|
|
1480
|
+
parts.push(s);
|
|
1481
|
+
}
|
|
1482
|
+
if (len - full === 1) {
|
|
1483
|
+
const n = data[full] << 16;
|
|
1484
|
+
parts.push(ALPHABET[n >> 18 & 63] + ALPHABET[n >> 12 & 63] + "==");
|
|
1485
|
+
} else if (len - full === 2) {
|
|
1486
|
+
const n = data[full] << 16 | data[full + 1] << 8;
|
|
1487
|
+
parts.push(ALPHABET[n >> 18 & 63] + ALPHABET[n >> 12 & 63] + ALPHABET[n >> 6 & 63] + "=");
|
|
1488
|
+
}
|
|
1489
|
+
return parts.join("");
|
|
1490
|
+
}
|
|
1491
|
+
function decodePure(encoded) {
|
|
1492
|
+
let validLen = encoded.length;
|
|
1493
|
+
while (validLen > 0 && encoded.charCodeAt(validLen - 1) === 61) validLen--;
|
|
1494
|
+
const out = new Uint8Array(validLen * 3 >> 2);
|
|
1495
|
+
let o = 0, buf = 0, bits = 0;
|
|
1496
|
+
for (let i = 0; i < validLen; i++) {
|
|
1497
|
+
const code = encoded.charCodeAt(i);
|
|
1498
|
+
const v = code < 128 ? REVERSE[code] : -1;
|
|
1499
|
+
if (v < 0) continue;
|
|
1500
|
+
buf = buf << 6 | v;
|
|
1501
|
+
bits += 6;
|
|
1502
|
+
if (bits >= 8) {
|
|
1503
|
+
bits -= 8;
|
|
1504
|
+
out[o++] = buf >> bits & 255;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return o === out.length ? out : out.subarray(0, o);
|
|
1508
|
+
}
|
|
1509
|
+
var starfishBase64 = nativeCodec ? { encode: encodeViaBtoa, decode: decodeViaAtob } : { encode: encodePure, decode: decodePure };
|
|
1510
|
+
export {
|
|
1511
|
+
CONNECT_TIMEOUT_MS,
|
|
1512
|
+
CategoryError,
|
|
1513
|
+
DEFAULT_CATEGORY,
|
|
1514
|
+
OBJECT_COLLECTIONS,
|
|
1515
|
+
PAIR_PREFIX,
|
|
1516
|
+
PULL_CACHE_MAX_AGE_MS,
|
|
1517
|
+
SpaceAccessError,
|
|
1518
|
+
acceptSpaceInvite,
|
|
1519
|
+
accountScope,
|
|
1520
|
+
addDeviceToSpaceKeyring,
|
|
1521
|
+
addJoinedSpace,
|
|
1522
|
+
addJoinedSpaceWithCap,
|
|
1523
|
+
addJoinedSpaceWithLinkAccess,
|
|
1524
|
+
addObject,
|
|
1525
|
+
addSpaceMember,
|
|
1526
|
+
ancestors,
|
|
1527
|
+
archiveObject,
|
|
1528
|
+
attachmentPull,
|
|
1529
|
+
attachmentPush,
|
|
1530
|
+
breadcrumbs,
|
|
1531
|
+
broadcastSpaceMeta,
|
|
1532
|
+
buildAuthHeaders,
|
|
1533
|
+
buildEncryptor,
|
|
1534
|
+
buildLinkedSession,
|
|
1535
|
+
buildSession,
|
|
1536
|
+
buildSpaceAccess,
|
|
1537
|
+
buildTree,
|
|
1538
|
+
bytesToHex,
|
|
1539
|
+
cacheProfile,
|
|
1540
|
+
capProviderFor,
|
|
1541
|
+
categoryId,
|
|
1542
|
+
clearSpaceAccessCache,
|
|
1543
|
+
clearSpaceAccessStore,
|
|
1544
|
+
completeDevicePairing,
|
|
1545
|
+
configureKv,
|
|
1546
|
+
configureOctoSpaces,
|
|
1547
|
+
createSpace,
|
|
1548
|
+
createSpaceInviteLink,
|
|
1549
|
+
decodeSpaceInviteLink,
|
|
1550
|
+
deriveSession,
|
|
1551
|
+
encodeSpaceInviteLink,
|
|
1552
|
+
ensureProfileKeys,
|
|
1553
|
+
ensurePseudo,
|
|
1554
|
+
excludeAutomatedRooms,
|
|
1555
|
+
fetchWithTimeout,
|
|
1556
|
+
fingerprintFromUserId,
|
|
1557
|
+
fromBase64Url,
|
|
1558
|
+
generateSeedWords,
|
|
1559
|
+
getSharedSpacesNamespace,
|
|
1560
|
+
getSpaceAccess,
|
|
1561
|
+
getSpaceAccessEntry,
|
|
1562
|
+
getSyncBase,
|
|
1563
|
+
getSyncNamespace,
|
|
1564
|
+
getSyncPrefix,
|
|
1565
|
+
hydrateSpaceAccessStore,
|
|
1566
|
+
inviteToSpace,
|
|
1567
|
+
isValidSeed,
|
|
1568
|
+
joinSpaceByLink,
|
|
1569
|
+
keyringName,
|
|
1570
|
+
keyringPull,
|
|
1571
|
+
keyringPush,
|
|
1572
|
+
kvGet,
|
|
1573
|
+
kvRemove,
|
|
1574
|
+
kvSet,
|
|
1575
|
+
linkAccessFromStore,
|
|
1576
|
+
linkedDeviceScope,
|
|
1577
|
+
loadCachedProfile,
|
|
1578
|
+
localSpaceAccessEntries,
|
|
1579
|
+
makeClient,
|
|
1580
|
+
makeJoinRequest,
|
|
1581
|
+
memberCapsFromStore,
|
|
1582
|
+
nextOrder,
|
|
1583
|
+
normalizeCategories,
|
|
1584
|
+
objDocPull,
|
|
1585
|
+
objDocPush,
|
|
1586
|
+
objIndexPull,
|
|
1587
|
+
objIndexPush,
|
|
1588
|
+
objLogPull,
|
|
1589
|
+
objLogPush,
|
|
1590
|
+
objectBlobPull,
|
|
1591
|
+
objectBlobPush,
|
|
1592
|
+
objectsToRoomCategories,
|
|
1593
|
+
onSpaceMeta,
|
|
1594
|
+
openEncryptor,
|
|
1595
|
+
ownerEnsureKeyring,
|
|
1596
|
+
ownerScope,
|
|
1597
|
+
ownerTrustedAdders,
|
|
1598
|
+
patchObject,
|
|
1599
|
+
profilePull,
|
|
1600
|
+
profilePush,
|
|
1601
|
+
pullCache,
|
|
1602
|
+
pushIndexSeed,
|
|
1603
|
+
randomId,
|
|
1604
|
+
readIndexRooms,
|
|
1605
|
+
readProfile,
|
|
1606
|
+
readProfiles,
|
|
1607
|
+
readPseudo,
|
|
1608
|
+
readRooms,
|
|
1609
|
+
readSpaceIndexRooms,
|
|
1610
|
+
readSpaceRooms,
|
|
1611
|
+
readSpaces,
|
|
1612
|
+
reconcileSpaceMeta,
|
|
1613
|
+
recoverSpaceAccess,
|
|
1614
|
+
removeSpaceAccessEntry,
|
|
1615
|
+
removeSpaceMember,
|
|
1616
|
+
reorderObjects,
|
|
1617
|
+
reorderSpaces,
|
|
1618
|
+
reparentObject,
|
|
1619
|
+
roomKindToSubtype,
|
|
1620
|
+
roomSlug,
|
|
1621
|
+
roomsRegistryPull,
|
|
1622
|
+
roomsRegistryPush,
|
|
1623
|
+
rootIdentityOf,
|
|
1624
|
+
saveSpaceAccessEntry,
|
|
1625
|
+
sealToRecipient,
|
|
1626
|
+
sealToSelf,
|
|
1627
|
+
seedIndexNodes,
|
|
1628
|
+
seedSpaceObjectIndex,
|
|
1629
|
+
setDmMapping,
|
|
1630
|
+
spaceIndexPull,
|
|
1631
|
+
spaceMemberScope,
|
|
1632
|
+
spacesPull,
|
|
1633
|
+
spacesPush,
|
|
1634
|
+
starfishBase64,
|
|
1635
|
+
startDevicePairing,
|
|
1636
|
+
subtreeIds,
|
|
1637
|
+
subtypeToRoomKind,
|
|
1638
|
+
toBase64Url,
|
|
1639
|
+
typesIndexPull,
|
|
1640
|
+
typesIndexPush,
|
|
1641
|
+
unsealFromRecipient,
|
|
1642
|
+
unsealFromSelf,
|
|
1643
|
+
updateArchivedDmsDoc,
|
|
1644
|
+
updateDmsDoc,
|
|
1645
|
+
updateMutesDoc,
|
|
1646
|
+
updateObjectIndex,
|
|
1647
|
+
updateQuickReactionsDoc,
|
|
1648
|
+
updateReadsDoc,
|
|
1649
|
+
updateSpacesDoc,
|
|
1650
|
+
userIdFromEdPub,
|
|
1651
|
+
writeProfile,
|
|
1652
|
+
writePseudo,
|
|
1653
|
+
writeRooms,
|
|
1654
|
+
writeSpaces
|
|
1655
|
+
};
|
|
1656
|
+
//# sourceMappingURL=index.js.map
|