@drakkar.software/octospaces-sdk 0.1.0 → 0.4.3
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/CHANGELOG.md +200 -0
- package/dist/index.d.ts +481 -274
- package/dist/index.js +1000 -493
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/types.ts +50 -83
- package/src/index.ts +62 -28
- package/src/objects/objects.test.ts +55 -95
- package/src/objects/objects.ts +23 -136
- package/src/spaces/members.test.ts +10 -3
- package/src/spaces/members.ts +86 -49
- package/src/spaces/nodes.test.ts +225 -0
- package/src/spaces/nodes.ts +427 -0
- package/src/spaces/object-index.test.ts +127 -71
- package/src/spaces/object-index.ts +61 -107
- package/src/spaces/registry.test.ts +59 -46
- package/src/spaces/registry.ts +28 -47
- package/src/sync/client.ts +20 -15
- package/src/sync/pairing.ts +10 -12
- package/src/sync/paths.test.ts +124 -16
- package/src/sync/paths.ts +73 -32
- package/src/sync/space-access-store.ts +17 -0
- package/src/sync/space-access.ts +112 -67
- package/src/utils/invite-preview.test.ts +169 -0
- package/src/utils/invite-preview.ts +101 -0
- package/src/utils/live-sync-bus.test.ts +116 -0
- package/src/utils/live-sync-bus.ts +71 -0
- package/src/utils/search-match.test.ts +149 -0
- package/src/utils/search-match.ts +145 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
1
11
|
// src/core/config.ts
|
|
2
|
-
var cfg = null;
|
|
3
12
|
function configureOctoSpaces(config) {
|
|
4
13
|
if ("namespace" in config && !config.syncNamespace) {
|
|
5
14
|
throw new Error(
|
|
@@ -24,17 +33,23 @@ function req() {
|
|
|
24
33
|
if (!cfg) throw new Error("octospaces-sdk: configureOctoSpaces() not called \u2014 wire it at app boot.");
|
|
25
34
|
return cfg;
|
|
26
35
|
}
|
|
27
|
-
var getSyncBase
|
|
28
|
-
var
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
var cfg, getSyncBase, getSyncNamespace, getSyncPrefix, getSharedSpacesNamespace, getOnServerReachable;
|
|
37
|
+
var init_config = __esm({
|
|
38
|
+
"src/core/config.ts"() {
|
|
39
|
+
"use strict";
|
|
40
|
+
cfg = null;
|
|
41
|
+
getSyncBase = () => req().syncBase;
|
|
42
|
+
getSyncNamespace = () => req().syncNamespace;
|
|
43
|
+
getSyncPrefix = () => {
|
|
44
|
+
const ns = req().syncNamespace;
|
|
45
|
+
return ns ? `/v1/${ns}` : "";
|
|
46
|
+
};
|
|
47
|
+
getSharedSpacesNamespace = () => cfg?.sharedSpacesNamespace;
|
|
48
|
+
getOnServerReachable = () => cfg?.onServerReachable;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
35
51
|
|
|
36
52
|
// src/core/adapters.ts
|
|
37
|
-
var kv = null;
|
|
38
53
|
function configureKv(adapter) {
|
|
39
54
|
kv = adapter;
|
|
40
55
|
}
|
|
@@ -42,9 +57,16 @@ function getKv() {
|
|
|
42
57
|
if (!kv) throw new Error("octospaces-sdk: configureKv() not called \u2014 wire it at app boot.");
|
|
43
58
|
return kv;
|
|
44
59
|
}
|
|
45
|
-
var kvGet
|
|
46
|
-
var
|
|
47
|
-
|
|
60
|
+
var kv, kvGet, kvSet, kvRemove;
|
|
61
|
+
var init_adapters = __esm({
|
|
62
|
+
"src/core/adapters.ts"() {
|
|
63
|
+
"use strict";
|
|
64
|
+
kv = null;
|
|
65
|
+
kvGet = (key2) => getKv().get(key2);
|
|
66
|
+
kvSet = (key2, value) => getKv().set(key2, value);
|
|
67
|
+
kvRemove = (key2) => getKv().remove(key2);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
48
70
|
|
|
49
71
|
// src/core/ids.ts
|
|
50
72
|
function randomId() {
|
|
@@ -57,49 +79,13 @@ function randomId() {
|
|
|
57
79
|
function roomSlug(name) {
|
|
58
80
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "room";
|
|
59
81
|
}
|
|
82
|
+
var init_ids = __esm({
|
|
83
|
+
"src/core/ids.ts"() {
|
|
84
|
+
"use strict";
|
|
85
|
+
}
|
|
86
|
+
});
|
|
60
87
|
|
|
61
88
|
// 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
89
|
function ownerScope() {
|
|
104
90
|
return {
|
|
105
91
|
ops: ["read", "list", "write"],
|
|
@@ -115,10 +101,18 @@ function spaceMemberScope(spaceId, canWrite) {
|
|
|
115
101
|
paths: [`spaces/${spaceId}/**`]
|
|
116
102
|
};
|
|
117
103
|
}
|
|
104
|
+
function nodeMemberScope(spaceId, nodeId, canWrite) {
|
|
105
|
+
const ops = canWrite ? ["read", "list", "write"] : ["read", "list"];
|
|
106
|
+
return {
|
|
107
|
+
ops,
|
|
108
|
+
collections: ["objinv"],
|
|
109
|
+
paths: [`spaces/${spaceId}/objects/n/${nodeId}/**`]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
118
112
|
function accountScope(userId) {
|
|
119
113
|
return {
|
|
120
114
|
ops: ["read", "list", "write"],
|
|
121
|
-
collections: ["profile", "devices", "spaces", "
|
|
115
|
+
collections: ["profile", "devices", "spaces", "spaceregistry"],
|
|
122
116
|
paths: [
|
|
123
117
|
`user/${userId}/profile`,
|
|
124
118
|
`users/${userId}/_devices`,
|
|
@@ -130,7 +124,7 @@ function accountScope(userId) {
|
|
|
130
124
|
function linkedDeviceScope(userId) {
|
|
131
125
|
return {
|
|
132
126
|
ops: ["read", "list", "write"],
|
|
133
|
-
collections: [...OBJECT_COLLECTIONS, "profile", "devices", "spaces", "
|
|
127
|
+
collections: [...OBJECT_COLLECTIONS, "profile", "devices", "spaces", "spaceregistry"],
|
|
134
128
|
paths: [
|
|
135
129
|
"spaces/**",
|
|
136
130
|
`user/${userId}/profile`,
|
|
@@ -150,14 +144,62 @@ async function userIdFromEdPub(edPubHex) {
|
|
|
150
144
|
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
151
145
|
return bytesToHex(new Uint8Array(digest)).slice(0, 32);
|
|
152
146
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
var pull, push, spaceIdFromRoomId, keyringName, keyringPull, keyringPush, attachmentName, attachmentPull, attachmentPush, profilePull, profilePush, spacesPull, spacesPush, spaceAccessPull, spaceAccessPush, objIndexName, objIndexPull, objIndexPush, objLogName, objLogPull, objLogPush, objDocName, objDocPull, objDocPush, objectBlobName, objectBlobPull, objectBlobPush, objPubName, objPubPull, objPubPush, objInvName, objInvPull, objInvPush, typesIndexName, typesIndexPull, typesIndexPush, objectDirName, objectDirPull, OBJECT_COLLECTIONS;
|
|
148
|
+
var init_paths = __esm({
|
|
149
|
+
"src/sync/paths.ts"() {
|
|
150
|
+
"use strict";
|
|
151
|
+
pull = (rest) => `/pull/${rest}`;
|
|
152
|
+
push = (rest) => `/push/${rest}`;
|
|
153
|
+
spaceIdFromRoomId = (roomId) => roomId.split("-").slice(0, 2).join("-");
|
|
154
|
+
keyringName = (spaceId) => `spaces/${spaceId}`;
|
|
155
|
+
keyringPull = (spaceId) => pull(`${keyringName(spaceId)}/_keyring`);
|
|
156
|
+
keyringPush = (spaceId) => push(`${keyringName(spaceId)}/_keyring`);
|
|
157
|
+
attachmentName = (roomId, blobId) => `spaces/${spaceIdFromRoomId(roomId)}/attachments/${roomId}/${blobId}`;
|
|
158
|
+
attachmentPull = (roomId, blobId) => pull(attachmentName(roomId, blobId));
|
|
159
|
+
attachmentPush = (roomId, blobId) => push(attachmentName(roomId, blobId));
|
|
160
|
+
profilePull = (userId) => pull(`user/${userId}/profile`);
|
|
161
|
+
profilePush = (userId) => push(`user/${userId}/profile`);
|
|
162
|
+
spacesPull = (userId) => pull(`user/${userId}/_spaces`);
|
|
163
|
+
spacesPush = (userId) => push(`user/${userId}/_spaces`);
|
|
164
|
+
spaceAccessPull = (spaceId) => pull(`spaces/${spaceId}/_access`);
|
|
165
|
+
spaceAccessPush = (spaceId) => push(`spaces/${spaceId}/_access`);
|
|
166
|
+
objIndexName = (spaceId) => `spaces/${spaceId}/objects/_index`;
|
|
167
|
+
objIndexPull = (spaceId) => pull(objIndexName(spaceId));
|
|
168
|
+
objIndexPush = (spaceId) => push(objIndexName(spaceId));
|
|
169
|
+
objLogName = (spaceId, objectId) => `spaces/${spaceId}/objects/logs/${objectId}`;
|
|
170
|
+
objLogPull = (spaceId, objectId) => pull(objLogName(spaceId, objectId));
|
|
171
|
+
objLogPush = (spaceId, objectId) => push(objLogName(spaceId, objectId));
|
|
172
|
+
objDocName = (spaceId, objectId) => `spaces/${spaceId}/objects/docs/${objectId}`;
|
|
173
|
+
objDocPull = (spaceId, objectId) => pull(objDocName(spaceId, objectId));
|
|
174
|
+
objDocPush = (spaceId, objectId) => push(objDocName(spaceId, objectId));
|
|
175
|
+
objectBlobName = (spaceId, blobId) => `spaces/${spaceId}/objects/blobs/${blobId}`;
|
|
176
|
+
objectBlobPull = (spaceId, blobId) => pull(objectBlobName(spaceId, blobId));
|
|
177
|
+
objectBlobPush = (spaceId, blobId) => push(objectBlobName(spaceId, blobId));
|
|
178
|
+
objPubName = (spaceId, nodeId) => `spaces/${spaceId}/objects/pub/${nodeId}`;
|
|
179
|
+
objPubPull = (spaceId, nodeId) => pull(objPubName(spaceId, nodeId));
|
|
180
|
+
objPubPush = (spaceId, nodeId) => push(objPubName(spaceId, nodeId));
|
|
181
|
+
objInvName = (spaceId, nodeId) => `spaces/${spaceId}/objects/n/${nodeId}/content`;
|
|
182
|
+
objInvPull = (spaceId, nodeId) => pull(objInvName(spaceId, nodeId));
|
|
183
|
+
objInvPush = (spaceId, nodeId) => push(objInvName(spaceId, nodeId));
|
|
184
|
+
typesIndexName = (spaceId) => `spaces/${spaceId}/types/_index`;
|
|
185
|
+
typesIndexPull = (spaceId) => pull(typesIndexName(spaceId));
|
|
186
|
+
typesIndexPush = (spaceId) => push(typesIndexName(spaceId));
|
|
187
|
+
objectDirName = (shard = "public") => `_index/objects/${shard}`;
|
|
188
|
+
objectDirPull = (shard = "public") => pull(objectDirName(shard));
|
|
189
|
+
OBJECT_COLLECTIONS = [
|
|
190
|
+
"spacekeyring",
|
|
191
|
+
"objindex",
|
|
192
|
+
"objlog",
|
|
193
|
+
"objsnap",
|
|
194
|
+
"objdoc",
|
|
195
|
+
"objblob",
|
|
196
|
+
"typeindex",
|
|
197
|
+
"objpub"
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
});
|
|
158
201
|
|
|
159
202
|
// src/sync/fetch-timeout.ts
|
|
160
|
-
var CONNECT_TIMEOUT_MS = 12e3;
|
|
161
203
|
function fetchWithTimeout(timeoutMs = CONNECT_TIMEOUT_MS) {
|
|
162
204
|
return (input, init) => {
|
|
163
205
|
const ctrl = new AbortController();
|
|
@@ -170,20 +212,32 @@ function fetchWithTimeout(timeoutMs = CONNECT_TIMEOUT_MS) {
|
|
|
170
212
|
return fetch(input, { ...init, signal: ctrl.signal }).finally(() => clearTimeout(timer));
|
|
171
213
|
};
|
|
172
214
|
}
|
|
215
|
+
var CONNECT_TIMEOUT_MS;
|
|
216
|
+
var init_fetch_timeout = __esm({
|
|
217
|
+
"src/sync/fetch-timeout.ts"() {
|
|
218
|
+
"use strict";
|
|
219
|
+
CONNECT_TIMEOUT_MS = 12e3;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
173
222
|
|
|
174
223
|
// 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
224
|
function pullCache() {
|
|
179
225
|
return shared ?? (shared = {
|
|
180
226
|
get: (key2) => kvGet(PREFIX + key2),
|
|
181
227
|
set: (key2, value) => kvSet(PREFIX + key2, value)
|
|
182
228
|
});
|
|
183
229
|
}
|
|
230
|
+
var PREFIX, PULL_CACHE_MAX_AGE_MS, shared;
|
|
231
|
+
var init_pull_cache = __esm({
|
|
232
|
+
"src/sync/pull-cache.ts"() {
|
|
233
|
+
"use strict";
|
|
234
|
+
init_adapters();
|
|
235
|
+
PREFIX = "octospaces.pullcache.";
|
|
236
|
+
PULL_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
184
239
|
|
|
185
240
|
// src/sync/profile-cache.ts
|
|
186
|
-
var key = (userId) => `octospaces.profile.v1.${userId}`;
|
|
187
241
|
function cacheProfile(userId, profile) {
|
|
188
242
|
void kvSet(key(userId), JSON.stringify(profile)).catch(() => {
|
|
189
243
|
});
|
|
@@ -203,16 +257,33 @@ async function loadCachedProfile(userId) {
|
|
|
203
257
|
return null;
|
|
204
258
|
}
|
|
205
259
|
}
|
|
260
|
+
var key;
|
|
261
|
+
var init_profile_cache = __esm({
|
|
262
|
+
"src/sync/profile-cache.ts"() {
|
|
263
|
+
"use strict";
|
|
264
|
+
init_adapters();
|
|
265
|
+
key = (userId) => `octospaces.profile.v1.${userId}`;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
206
268
|
|
|
207
269
|
// src/core/space-access-error.ts
|
|
208
|
-
var SpaceAccessError
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
270
|
+
var SpaceAccessError;
|
|
271
|
+
var init_space_access_error = __esm({
|
|
272
|
+
"src/core/space-access-error.ts"() {
|
|
273
|
+
"use strict";
|
|
274
|
+
SpaceAccessError = class extends Error {
|
|
275
|
+
constructor(message) {
|
|
276
|
+
super(message);
|
|
277
|
+
this.name = "SpaceAccessError";
|
|
278
|
+
}
|
|
279
|
+
};
|
|
212
280
|
}
|
|
213
|
-
};
|
|
281
|
+
});
|
|
214
282
|
|
|
215
283
|
// src/sync/client.ts
|
|
284
|
+
import { StarfishClient } from "@drakkar.software/starfish-client";
|
|
285
|
+
import { createKeyring, createKeyringEncryptor } from "@drakkar.software/starfish-keyring";
|
|
286
|
+
import { signRequest, stableStringify } from "@drakkar.software/starfish-protocol";
|
|
216
287
|
function capProviderFor(cap, devEdPrivHex) {
|
|
217
288
|
return {
|
|
218
289
|
async getCap() {
|
|
@@ -232,13 +303,13 @@ function makeClient(cap, devEdPrivHex, namespaceOverride) {
|
|
|
232
303
|
onRevalidated: () => getOnServerReachable()?.()
|
|
233
304
|
});
|
|
234
305
|
}
|
|
235
|
-
async function openEncryptor(client, keys,
|
|
236
|
-
const res = await client.pull(
|
|
237
|
-
throw new Error("Could not reach the server to fetch
|
|
306
|
+
async function openEncryptor(client, keys, keyringPullPath, trustedAdders) {
|
|
307
|
+
const res = await client.pull(keyringPullPath).catch(() => {
|
|
308
|
+
throw new Error("Could not reach the server to fetch node keys.");
|
|
238
309
|
});
|
|
239
310
|
const keyring = res?.data;
|
|
240
311
|
if (!keyring || !keyring.epochs) {
|
|
241
|
-
throw new SpaceAccessError("This
|
|
312
|
+
throw new SpaceAccessError("This node has no keyring yet \u2014 ask the owner to create it first.");
|
|
242
313
|
}
|
|
243
314
|
try {
|
|
244
315
|
const enc = await createKeyringEncryptor(
|
|
@@ -248,25 +319,25 @@ async function openEncryptor(client, keys, spaceId, trustedAdders) {
|
|
|
248
319
|
);
|
|
249
320
|
return enc;
|
|
250
321
|
} catch {
|
|
251
|
-
throw new SpaceAccessError("You're not a recipient of this
|
|
322
|
+
throw new SpaceAccessError("You're not a recipient of this node's keyring yet \u2014 ask the owner to invite you.");
|
|
252
323
|
}
|
|
253
324
|
}
|
|
254
|
-
async function buildEncryptor(client, keys,
|
|
325
|
+
async function buildEncryptor(client, keys, keyringPullPath, trustedAdders) {
|
|
255
326
|
try {
|
|
256
|
-
return await openEncryptor(client, keys,
|
|
327
|
+
return await openEncryptor(client, keys, keyringPullPath, trustedAdders);
|
|
257
328
|
} catch {
|
|
258
329
|
return null;
|
|
259
330
|
}
|
|
260
331
|
}
|
|
261
|
-
async function ownerEnsureKeyring(client, keys,
|
|
262
|
-
const krRes = await client.pull(
|
|
332
|
+
async function ownerEnsureKeyring(client, keys, keyringPullPath, keyringPushPath, trustedAdders = [keys.edPub]) {
|
|
333
|
+
const krRes = await client.pull(keyringPullPath).catch(() => null);
|
|
263
334
|
let keyring = krRes?.data;
|
|
264
335
|
if (!keyring || !keyring.epochs) {
|
|
265
336
|
const created = await createKeyring({ edPrivHex: keys.edPriv, edPubHex: keys.edPub }, [
|
|
266
337
|
{ subKemHex: keys.kemPub }
|
|
267
338
|
]);
|
|
268
339
|
keyring = created.keyring;
|
|
269
|
-
await client.push(
|
|
340
|
+
await client.push(keyringPushPath, keyring, krRes?.hash ?? null);
|
|
270
341
|
}
|
|
271
342
|
const enc = await createKeyringEncryptor(
|
|
272
343
|
keyring,
|
|
@@ -296,14 +367,12 @@ async function readProfile(userId) {
|
|
|
296
367
|
async function readPseudo(userId) {
|
|
297
368
|
return (await readProfile(userId)).pseudo;
|
|
298
369
|
}
|
|
299
|
-
var profileBatchClient;
|
|
300
370
|
function getProfileBatchClient() {
|
|
301
371
|
if (!profileBatchClient) {
|
|
302
372
|
profileBatchClient = new StarfishClient({ baseUrl: getSyncBase(), namespace: getSyncNamespace(), fetch: fetchWithTimeout() });
|
|
303
373
|
}
|
|
304
374
|
return profileBatchClient;
|
|
305
375
|
}
|
|
306
|
-
var PROFILE_BATCH_CHUNK = 24;
|
|
307
376
|
async function readProfiles(ids) {
|
|
308
377
|
const out = /* @__PURE__ */ new Map();
|
|
309
378
|
const client = getProfileBatchClient();
|
|
@@ -399,6 +468,19 @@ async function ensurePseudo(client, userId, fallback) {
|
|
|
399
468
|
await writeProfile(client, userId, { pseudo: fallback });
|
|
400
469
|
return fallback;
|
|
401
470
|
}
|
|
471
|
+
var profileBatchClient, PROFILE_BATCH_CHUNK;
|
|
472
|
+
var init_client = __esm({
|
|
473
|
+
"src/sync/client.ts"() {
|
|
474
|
+
"use strict";
|
|
475
|
+
init_config();
|
|
476
|
+
init_fetch_timeout();
|
|
477
|
+
init_pull_cache();
|
|
478
|
+
init_profile_cache();
|
|
479
|
+
init_paths();
|
|
480
|
+
init_space_access_error();
|
|
481
|
+
PROFILE_BATCH_CHUNK = 24;
|
|
482
|
+
}
|
|
483
|
+
});
|
|
402
484
|
|
|
403
485
|
// src/sync/identity.ts
|
|
404
486
|
import { generateMnemonic, validateMnemonic } from "@scure/bip39";
|
|
@@ -474,62 +556,16 @@ async function deriveSession(seedWords, name) {
|
|
|
474
556
|
function rootIdentityOf(s) {
|
|
475
557
|
return { userId: s.userId, keys: s.keys };
|
|
476
558
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
}
|
|
559
|
+
var init_identity = __esm({
|
|
560
|
+
"src/sync/identity.ts"() {
|
|
561
|
+
"use strict";
|
|
562
|
+
init_client();
|
|
563
|
+
init_paths();
|
|
564
|
+
init_config();
|
|
565
|
+
}
|
|
566
|
+
});
|
|
528
567
|
|
|
529
568
|
// src/sync/space-access-store.ts
|
|
530
|
-
var keyFor = (userId) => `octospaces.spaceaccess.${userId}`;
|
|
531
|
-
var cache = {};
|
|
532
|
-
var activeKey = null;
|
|
533
569
|
async function hydrateSpaceAccessStore(userId, serverCaps, serverLinkAccess) {
|
|
534
570
|
const key2 = keyFor(userId);
|
|
535
571
|
if (activeKey === key2) return;
|
|
@@ -572,6 +608,15 @@ function removeSpaceAccessEntry(spaceId) {
|
|
|
572
608
|
cache = next;
|
|
573
609
|
persist();
|
|
574
610
|
}
|
|
611
|
+
function getNodeAccessEntry(spaceId, nodeId) {
|
|
612
|
+
return cache[`${spaceId}:${nodeId}`] ?? null;
|
|
613
|
+
}
|
|
614
|
+
function saveNodeAccessEntry(spaceId, nodeId, entry) {
|
|
615
|
+
saveSpaceAccessEntry(`${spaceId}:${nodeId}`, entry);
|
|
616
|
+
}
|
|
617
|
+
function removeNodeAccessEntry(spaceId, nodeId) {
|
|
618
|
+
removeSpaceAccessEntry(`${spaceId}:${nodeId}`);
|
|
619
|
+
}
|
|
575
620
|
function localSpaceAccessEntries() {
|
|
576
621
|
return cache;
|
|
577
622
|
}
|
|
@@ -591,310 +636,153 @@ function clearSpaceAccessStore() {
|
|
|
591
636
|
cache = {};
|
|
592
637
|
activeKey = null;
|
|
593
638
|
}
|
|
639
|
+
var keyFor, cache, activeKey;
|
|
640
|
+
var init_space_access_store = __esm({
|
|
641
|
+
"src/sync/space-access-store.ts"() {
|
|
642
|
+
"use strict";
|
|
643
|
+
init_adapters();
|
|
644
|
+
keyFor = (userId) => `octospaces.spaceaccess.${userId}`;
|
|
645
|
+
cache = {};
|
|
646
|
+
activeKey = null;
|
|
647
|
+
}
|
|
648
|
+
});
|
|
594
649
|
|
|
595
650
|
// src/sync/space-access.ts
|
|
596
|
-
|
|
597
|
-
function clearSpaceAccessCache() {
|
|
651
|
+
function clearNodeAccessCache() {
|
|
598
652
|
cache2.clear();
|
|
599
653
|
}
|
|
600
|
-
function
|
|
601
|
-
const
|
|
654
|
+
function getSpaceClient(spaceId, session) {
|
|
655
|
+
const entry = getSpaceAccessEntry(spaceId);
|
|
656
|
+
if (entry?.kind === "link") return makeClient(entry.cap, entry.key);
|
|
657
|
+
if (entry?.kind === "member") {
|
|
658
|
+
const cap = JSON.parse(entry.cap);
|
|
659
|
+
return makeClient(cap, session.keys.edPriv);
|
|
660
|
+
}
|
|
661
|
+
return session.chatClient;
|
|
662
|
+
}
|
|
663
|
+
function getNodeAccess(spaceId, nodeId, node, session, reg) {
|
|
664
|
+
const cacheKey = `${spaceId}:${nodeId}`;
|
|
665
|
+
const hit = cache2.get(cacheKey);
|
|
602
666
|
if (hit) return hit;
|
|
603
667
|
const p = (async () => {
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
668
|
+
const nodeEntry = getNodeAccessEntry(spaceId, nodeId);
|
|
669
|
+
const spaceEntry = getSpaceAccessEntry(spaceId);
|
|
670
|
+
const activeEntry = nodeEntry ?? spaceEntry;
|
|
671
|
+
let client;
|
|
672
|
+
let capIss;
|
|
673
|
+
if (activeEntry?.kind === "link") {
|
|
674
|
+
client = makeClient(activeEntry.cap, activeEntry.key);
|
|
675
|
+
} else if (activeEntry?.kind === "member") {
|
|
676
|
+
const cap = JSON.parse(activeEntry.cap);
|
|
677
|
+
capIss = cap.iss;
|
|
678
|
+
client = makeClient(cap, session.keys.edPriv);
|
|
679
|
+
} else {
|
|
680
|
+
client = session.chatClient;
|
|
609
681
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const encryptor2 = await openEncryptor(client, session.keys, spaceId, cap.iss ? [cap.iss] : []);
|
|
614
|
-
return { encryptor: encryptor2, client, isOwnerOpen: false };
|
|
682
|
+
const isOwnerOpen = reg != null ? reg.owner === session.userId : activeEntry == null;
|
|
683
|
+
if (!node.enc) {
|
|
684
|
+
return { encryptor: null, client, isOwnerOpen };
|
|
615
685
|
}
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
686
|
+
const spacePullPath = keyringPull(spaceId);
|
|
687
|
+
const trustedAdders = capIss ? [capIss] : reg?.owner ? [reg.owner] : ownerTrustedAdders(session);
|
|
688
|
+
if (activeEntry?.kind === "member" || activeEntry?.kind === "link") {
|
|
689
|
+
const encryptor2 = await openEncryptor(client, session.keys, spacePullPath, trustedAdders);
|
|
690
|
+
return { encryptor: encryptor2, client, isOwnerOpen: false };
|
|
619
691
|
}
|
|
620
692
|
const owner = reg?.owner ?? null;
|
|
621
693
|
const members = reg?.members ?? [];
|
|
622
694
|
if (owner !== null && owner !== session.userId) {
|
|
623
695
|
throw new SpaceAccessError(
|
|
624
|
-
members.includes(session.userId) ? "You're a member of this space, but
|
|
696
|
+
members.includes(session.userId) ? "You're a member of this space, but the space key isn't on this device yet \u2014 ask the owner to invite you." : "You don't have access to this node."
|
|
625
697
|
);
|
|
626
698
|
}
|
|
627
699
|
const encryptor = await ownerEnsureKeyring(
|
|
628
700
|
session.chatClient,
|
|
629
701
|
session.keys,
|
|
630
|
-
|
|
702
|
+
spacePullPath,
|
|
703
|
+
keyringPush(spaceId),
|
|
631
704
|
ownerTrustedAdders(session)
|
|
632
705
|
);
|
|
633
706
|
return { encryptor, client: session.chatClient, isOwnerOpen: true };
|
|
634
707
|
})();
|
|
635
|
-
cache2.set(
|
|
636
|
-
p.catch(() => cache2.delete(
|
|
708
|
+
cache2.set(cacheKey, p);
|
|
709
|
+
p.catch(() => cache2.delete(cacheKey));
|
|
637
710
|
return p;
|
|
638
711
|
}
|
|
639
|
-
async function
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
let client = session.chatClient;
|
|
712
|
+
async function buildNodeAccess(session, spaceId, nodeId, node) {
|
|
713
|
+
const nodeEntry = getNodeAccessEntry(spaceId, nodeId);
|
|
714
|
+
const spaceEntry = getSpaceAccessEntry(spaceId);
|
|
715
|
+
const activeEntry = nodeEntry ?? spaceEntry;
|
|
716
|
+
let client;
|
|
646
717
|
let trustedAdders = ownerTrustedAdders(session);
|
|
647
|
-
if (
|
|
648
|
-
|
|
718
|
+
if (activeEntry?.kind === "link") {
|
|
719
|
+
client = makeClient(activeEntry.cap, activeEntry.key);
|
|
720
|
+
} else if (activeEntry?.kind === "member") {
|
|
721
|
+
const cap = JSON.parse(activeEntry.cap);
|
|
649
722
|
client = makeClient(cap, session.keys.edPriv);
|
|
650
723
|
if (cap.iss) trustedAdders = [cap.iss];
|
|
651
|
-
|
|
652
|
-
|
|
724
|
+
} else {
|
|
725
|
+
client = session.chatClient;
|
|
653
726
|
}
|
|
654
|
-
if (
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const encryptor = await buildEncryptor(client, session.keys, spaceId, trustedAdders);
|
|
727
|
+
if (!node.enc) return { client, encryptor: null };
|
|
728
|
+
const spacePullPath = keyringPull(spaceId);
|
|
729
|
+
const encryptor = await buildEncryptor(client, session.keys, spacePullPath, trustedAdders);
|
|
658
730
|
return encryptor ? { client, encryptor } : null;
|
|
659
731
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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 });
|
|
732
|
+
var cache2;
|
|
733
|
+
var init_space_access = __esm({
|
|
734
|
+
"src/sync/space-access.ts"() {
|
|
735
|
+
"use strict";
|
|
736
|
+
init_client();
|
|
737
|
+
init_identity();
|
|
738
|
+
init_space_access_store();
|
|
739
|
+
init_space_access_error();
|
|
740
|
+
init_paths();
|
|
741
|
+
cache2 = /* @__PURE__ */ new Map();
|
|
831
742
|
}
|
|
832
|
-
|
|
833
|
-
}
|
|
743
|
+
});
|
|
834
744
|
|
|
835
745
|
// src/spaces/object-index.ts
|
|
836
746
|
import { ConflictError } from "@drakkar.software/starfish-client";
|
|
837
|
-
function
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
|
|
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;
|
|
747
|
+
function serializeForIndex(node) {
|
|
748
|
+
if (node.access === "invite") {
|
|
749
|
+
const { emoji: _e, ...rest } = node;
|
|
750
|
+
return { ...rest, title: "" };
|
|
850
751
|
}
|
|
752
|
+
return node;
|
|
851
753
|
}
|
|
852
|
-
async function
|
|
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) {
|
|
754
|
+
async function pushIndexSeed(client, spaceId, nodes = []) {
|
|
868
755
|
const res = await client.pull(objIndexPull(spaceId)).catch(() => null);
|
|
869
756
|
const existing = res?.data;
|
|
870
|
-
if (
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
});
|
|
881
|
-
await pushIndexSeed(client, encryptor, spaceId, rooms);
|
|
757
|
+
if (Array.isArray(existing?.objects)) return;
|
|
758
|
+
await client.push(
|
|
759
|
+
objIndexPush(spaceId),
|
|
760
|
+
{ v: 2, objects: nodes.map(serializeForIndex), updatedAt: Date.now() },
|
|
761
|
+
res?.hash ?? null
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
async function seedSpaceObjectIndex(session, spaceId, nodes = []) {
|
|
765
|
+
const client = getSpaceClient(spaceId, session);
|
|
766
|
+
await pushIndexSeed(client, spaceId, nodes);
|
|
882
767
|
}
|
|
883
768
|
async function updateObjectIndex(session, spaceId, mutator, reg) {
|
|
884
|
-
|
|
769
|
+
void reg;
|
|
770
|
+
const client = getSpaceClient(spaceId, session);
|
|
885
771
|
const pullPath = objIndexPull(spaceId);
|
|
886
772
|
const pushPath = objIndexPush(spaceId);
|
|
887
773
|
const MAX_ATTEMPTS = 3;
|
|
888
774
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
889
775
|
const res = await client.pull(pullPath).catch(() => null);
|
|
890
776
|
const raw = res?.data;
|
|
891
|
-
const
|
|
892
|
-
const cur = Array.isArray(plain.objects) ? plain.objects : [];
|
|
777
|
+
const cur = Array.isArray(raw?.objects) ? raw.objects : [];
|
|
893
778
|
const next = mutator(cur, Date.now());
|
|
894
779
|
if (!next) return;
|
|
895
|
-
const payload = encryptor ? await encryptor.encrypt({ objects: next }) : { objects: next };
|
|
896
780
|
try {
|
|
897
|
-
await client.push(
|
|
781
|
+
await client.push(
|
|
782
|
+
pushPath,
|
|
783
|
+
{ v: 2, objects: next.map(serializeForIndex), updatedAt: Date.now() },
|
|
784
|
+
res?.hash ?? null
|
|
785
|
+
);
|
|
898
786
|
return;
|
|
899
787
|
} catch (err) {
|
|
900
788
|
if (err instanceof ConflictError && attempt < MAX_ATTEMPTS - 1) continue;
|
|
@@ -902,9 +790,46 @@ async function updateObjectIndex(session, spaceId, mutator, reg) {
|
|
|
902
790
|
}
|
|
903
791
|
}
|
|
904
792
|
}
|
|
793
|
+
async function readObjectTree(session, spaceId) {
|
|
794
|
+
const client = getSpaceClient(spaceId, session);
|
|
795
|
+
const res = await client.pull(objIndexPull(spaceId)).catch(() => null);
|
|
796
|
+
const raw = res?.data;
|
|
797
|
+
return Array.isArray(raw?.objects) ? raw.objects : [];
|
|
798
|
+
}
|
|
799
|
+
var init_object_index = __esm({
|
|
800
|
+
"src/spaces/object-index.ts"() {
|
|
801
|
+
"use strict";
|
|
802
|
+
init_paths();
|
|
803
|
+
init_space_access();
|
|
804
|
+
}
|
|
805
|
+
});
|
|
905
806
|
|
|
906
807
|
// src/spaces/registry.ts
|
|
907
|
-
var
|
|
808
|
+
var registry_exports = {};
|
|
809
|
+
__export(registry_exports, {
|
|
810
|
+
addJoinedSpace: () => addJoinedSpace,
|
|
811
|
+
addJoinedSpaceWithCap: () => addJoinedSpaceWithCap,
|
|
812
|
+
addJoinedSpaceWithLinkAccess: () => addJoinedSpaceWithLinkAccess,
|
|
813
|
+
addSpaceMember: () => addSpaceMember,
|
|
814
|
+
broadcastSpaceMeta: () => broadcastSpaceMeta,
|
|
815
|
+
createSpace: () => createSpace,
|
|
816
|
+
onSpaceMeta: () => onSpaceMeta,
|
|
817
|
+
readSpaceAccess: () => readSpaceAccess,
|
|
818
|
+
readSpaces: () => readSpaces,
|
|
819
|
+
reconcileSpaceMeta: () => reconcileSpaceMeta,
|
|
820
|
+
removeSpaceMember: () => removeSpaceMember,
|
|
821
|
+
reorderSpaces: () => reorderSpaces,
|
|
822
|
+
setDmMapping: () => setDmMapping,
|
|
823
|
+
updateArchivedDmsDoc: () => updateArchivedDmsDoc,
|
|
824
|
+
updateDmsDoc: () => updateDmsDoc,
|
|
825
|
+
updateMutesDoc: () => updateMutesDoc,
|
|
826
|
+
updateQuickReactionsDoc: () => updateQuickReactionsDoc,
|
|
827
|
+
updateReadsDoc: () => updateReadsDoc,
|
|
828
|
+
updateSpacesDoc: () => updateSpacesDoc,
|
|
829
|
+
writeSpaceAccess: () => writeSpaceAccess,
|
|
830
|
+
writeSpaces: () => writeSpaces
|
|
831
|
+
});
|
|
832
|
+
import { ConflictError as ConflictError2, StarfishHttpError } from "@drakkar.software/starfish-client";
|
|
908
833
|
function onSpaceMeta(fn) {
|
|
909
834
|
spaceMetaListeners.add(fn);
|
|
910
835
|
return () => {
|
|
@@ -1098,17 +1023,8 @@ async function reorderSpaces(client, userId, order) {
|
|
|
1098
1023
|
function newSpaceId() {
|
|
1099
1024
|
return `sp-${randomId()}`;
|
|
1100
1025
|
}
|
|
1101
|
-
function
|
|
1102
|
-
const
|
|
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) => {
|
|
1026
|
+
async function readSpaceAccess(client, spaceId) {
|
|
1027
|
+
const res = await client.pull(spaceAccessPull(spaceId)).catch((err) => {
|
|
1112
1028
|
if (err instanceof StarfishHttpError && err.status === 404) return null;
|
|
1113
1029
|
throw err;
|
|
1114
1030
|
});
|
|
@@ -1116,31 +1032,35 @@ async function readRooms(client, spaceId) {
|
|
|
1116
1032
|
return {
|
|
1117
1033
|
owner: typeof data?.owner === "string" ? data.owner : null,
|
|
1118
1034
|
members: Array.isArray(data?.members) ? data.members.filter((m) => typeof m === "string") : [],
|
|
1119
|
-
visibility: data?.visibility === "public" ? "public" : null,
|
|
1120
1035
|
name: typeof data?.name === "string" ? data.name : null,
|
|
1121
1036
|
image: typeof data?.image === "string" ? data.image : null,
|
|
1122
1037
|
hash: res?.hash ?? null
|
|
1123
1038
|
};
|
|
1124
1039
|
}
|
|
1125
|
-
async function
|
|
1040
|
+
async function writeSpaceAccess(client, spaceId, owner, members, hash, meta) {
|
|
1126
1041
|
const name = meta?.name?.trim() || void 0;
|
|
1127
1042
|
const image = meta?.image || void 0;
|
|
1128
|
-
const visibility = meta?.visibility === "public" ? "public" : void 0;
|
|
1129
1043
|
await client.push(
|
|
1130
|
-
|
|
1131
|
-
{
|
|
1044
|
+
spaceAccessPush(spaceId),
|
|
1045
|
+
{
|
|
1046
|
+
v: 1,
|
|
1047
|
+
owner,
|
|
1048
|
+
members,
|
|
1049
|
+
...name ? { name } : {},
|
|
1050
|
+
...image ? { image } : {}
|
|
1051
|
+
},
|
|
1132
1052
|
hash
|
|
1133
1053
|
);
|
|
1134
1054
|
}
|
|
1135
1055
|
async function addSpaceMember(client, spaceId, ownerUserId, memberUserId) {
|
|
1136
|
-
const { owner, members,
|
|
1056
|
+
const { owner, members, name, image, hash } = await readSpaceAccess(client, spaceId);
|
|
1137
1057
|
if (memberUserId === (owner ?? ownerUserId) || members.includes(memberUserId)) return;
|
|
1138
|
-
await
|
|
1058
|
+
await writeSpaceAccess(client, spaceId, owner ?? ownerUserId, [...members, memberUserId], hash, { name, image });
|
|
1139
1059
|
}
|
|
1140
1060
|
async function removeSpaceMember(client, spaceId, memberUserId) {
|
|
1141
|
-
const { owner, members,
|
|
1061
|
+
const { owner, members, name, image, hash } = await readSpaceAccess(client, spaceId);
|
|
1142
1062
|
if (!members.includes(memberUserId)) return;
|
|
1143
|
-
await
|
|
1063
|
+
await writeSpaceAccess(client, spaceId, owner ?? memberUserId, members.filter((m) => m !== memberUserId), hash, { name, image });
|
|
1144
1064
|
}
|
|
1145
1065
|
async function addJoinedSpace(client, userId, space) {
|
|
1146
1066
|
await updateSpacesDoc(
|
|
@@ -1163,26 +1083,22 @@ async function addJoinedSpaceWithLinkAccess(client, userId, space, sealed) {
|
|
|
1163
1083
|
pubAccess: { ...cur.pubAccess, [space.id]: sealed }
|
|
1164
1084
|
}));
|
|
1165
1085
|
}
|
|
1166
|
-
async function createSpace(session, name
|
|
1086
|
+
async function createSpace(session, name) {
|
|
1167
1087
|
const { accountClient, userId } = session;
|
|
1168
1088
|
const { spaces, hash } = await readSpaces(accountClient, userId);
|
|
1169
1089
|
const trimmed = name.trim() || "New Space";
|
|
1170
|
-
const visibility = opts?.visibility ?? "private";
|
|
1171
1090
|
const id = newSpaceId();
|
|
1172
1091
|
const space = {
|
|
1173
1092
|
id,
|
|
1174
1093
|
name: trimmed,
|
|
1175
1094
|
short: trimmed.slice(0, 2).toUpperCase(),
|
|
1176
|
-
members: 1
|
|
1177
|
-
...visibility === "public" ? { visibility: "public", ownerId: userId, write: true } : {}
|
|
1095
|
+
members: 1
|
|
1178
1096
|
};
|
|
1179
|
-
await
|
|
1180
|
-
await seedSpaceObjectIndex(session, id
|
|
1097
|
+
await writeSpaceAccess(accountClient, id, userId, [], null, { name: trimmed });
|
|
1098
|
+
await seedSpaceObjectIndex(session, id);
|
|
1181
1099
|
await writeSpaces(accountClient, userId, [...spaces, space], hash);
|
|
1182
1100
|
return space;
|
|
1183
1101
|
}
|
|
1184
|
-
var CategoryError = class extends Error {
|
|
1185
|
-
};
|
|
1186
1102
|
async function reconcileSpaceMeta(client, userId, spaceId, shared2, knownSpaces) {
|
|
1187
1103
|
const sharedName = typeof shared2.name === "string" && shared2.name.trim() ? shared2.name : null;
|
|
1188
1104
|
const sharedImage = typeof shared2.image === "string" && shared2.image ? shared2.image : null;
|
|
@@ -1205,32 +1121,109 @@ async function reconcileSpaceMeta(client, userId, spaceId, shared2, knownSpaces)
|
|
|
1205
1121
|
await writeSpaces(client, userId, next, hash);
|
|
1206
1122
|
broadcastSpaceMeta(spaceId, { name, short, image });
|
|
1207
1123
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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);
|
|
1124
|
+
var spaceMetaListeners;
|
|
1125
|
+
var init_registry = __esm({
|
|
1126
|
+
"src/spaces/registry.ts"() {
|
|
1127
|
+
"use strict";
|
|
1128
|
+
init_ids();
|
|
1129
|
+
init_object_index();
|
|
1130
|
+
init_paths();
|
|
1131
|
+
spaceMetaListeners = /* @__PURE__ */ new Set();
|
|
1229
1132
|
}
|
|
1230
|
-
|
|
1231
|
-
}
|
|
1133
|
+
});
|
|
1232
1134
|
|
|
1233
|
-
// src/
|
|
1135
|
+
// src/index.ts
|
|
1136
|
+
init_config();
|
|
1137
|
+
init_adapters();
|
|
1138
|
+
init_ids();
|
|
1139
|
+
init_paths();
|
|
1140
|
+
init_client();
|
|
1141
|
+
init_identity();
|
|
1142
|
+
|
|
1143
|
+
// src/sync/account-seal.ts
|
|
1144
|
+
import {
|
|
1145
|
+
bytesToHex as bytesToHex2,
|
|
1146
|
+
hexToBytes,
|
|
1147
|
+
unwrapFromEntry,
|
|
1148
|
+
verifyEntrySignature,
|
|
1149
|
+
wrapForRecipient
|
|
1150
|
+
} from "@drakkar.software/starfish-keyring";
|
|
1151
|
+
var SELF_EPOCH = 0;
|
|
1152
|
+
var subtle = () => globalThis.crypto.subtle;
|
|
1153
|
+
async function seal(session, recipientKemPub, plaintext) {
|
|
1154
|
+
const cek = globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
1155
|
+
const entry = await wrapForRecipient(cek, recipientKemPub, {
|
|
1156
|
+
adderEdPrivHex: session.keys.edPriv,
|
|
1157
|
+
adderEdPubHex: session.keys.edPub,
|
|
1158
|
+
addedAt: Math.floor(Date.now() / 1e3),
|
|
1159
|
+
epoch: SELF_EPOCH
|
|
1160
|
+
});
|
|
1161
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
1162
|
+
const key2 = await subtle().importKey("raw", cek, { name: "AES-GCM" }, false, ["encrypt"]);
|
|
1163
|
+
const ctBuf = await subtle().encrypt({ name: "AES-GCM", iv }, key2, new TextEncoder().encode(plaintext));
|
|
1164
|
+
const packed = new Uint8Array(iv.length + ctBuf.byteLength);
|
|
1165
|
+
packed.set(iv, 0);
|
|
1166
|
+
packed.set(new Uint8Array(ctBuf), iv.length);
|
|
1167
|
+
return { entry, ct: bytesToHex2(packed) };
|
|
1168
|
+
}
|
|
1169
|
+
async function open(session, blob) {
|
|
1170
|
+
const cek = await unwrapFromEntry(blob.entry, session.keys.kemPriv);
|
|
1171
|
+
const packed = hexToBytes(blob.ct);
|
|
1172
|
+
const iv = new Uint8Array(packed.subarray(0, 12));
|
|
1173
|
+
const ctBytes = new Uint8Array(packed.subarray(12));
|
|
1174
|
+
const key2 = await subtle().importKey("raw", new Uint8Array(cek), { name: "AES-GCM" }, false, ["decrypt"]);
|
|
1175
|
+
const out = await subtle().decrypt({ name: "AES-GCM", iv }, key2, ctBytes);
|
|
1176
|
+
return new TextDecoder().decode(out);
|
|
1177
|
+
}
|
|
1178
|
+
function sealToSelf(session, plaintext) {
|
|
1179
|
+
return seal(session, session.keys.kemPub, plaintext);
|
|
1180
|
+
}
|
|
1181
|
+
async function unsealFromSelf(session, blob) {
|
|
1182
|
+
if (blob.entry.addedBy !== session.keys.edPub) throw new Error("sealed blob not self-signed");
|
|
1183
|
+
if (!await verifyEntrySignature(blob.entry, SELF_EPOCH)) throw new Error("sealed blob signature invalid");
|
|
1184
|
+
return open(session, blob);
|
|
1185
|
+
}
|
|
1186
|
+
function sealToRecipient(session, recipientKemPub, plaintext) {
|
|
1187
|
+
return seal(session, recipientKemPub, plaintext);
|
|
1188
|
+
}
|
|
1189
|
+
async function unsealFromRecipient(session, blob) {
|
|
1190
|
+
if (!await verifyEntrySignature(blob.entry, SELF_EPOCH)) throw new Error("sealed blob signature invalid");
|
|
1191
|
+
return open(session, blob);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/index.ts
|
|
1195
|
+
init_space_access();
|
|
1196
|
+
init_space_access_store();
|
|
1197
|
+
init_registry();
|
|
1198
|
+
|
|
1199
|
+
// src/spaces/members.ts
|
|
1200
|
+
init_space_access_store();
|
|
1201
|
+
init_paths();
|
|
1202
|
+
init_registry();
|
|
1203
|
+
import { generateDeviceKeys } from "@drakkar.software/starfish-identities";
|
|
1204
|
+
import { addCollectionRecipient } from "@drakkar.software/starfish-keyring";
|
|
1205
|
+
import { mintMemberCap } from "@drakkar.software/starfish-sharing";
|
|
1206
|
+
|
|
1207
|
+
// src/sync/base64url.ts
|
|
1208
|
+
function toBase64Url(json) {
|
|
1209
|
+
const bytes = new TextEncoder().encode(json);
|
|
1210
|
+
let bin = "";
|
|
1211
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
1212
|
+
const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(json, "utf-8").toString("base64");
|
|
1213
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1214
|
+
}
|
|
1215
|
+
function fromBase64Url(b64url) {
|
|
1216
|
+
const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
1217
|
+
if (typeof atob === "function") {
|
|
1218
|
+
const bin = atob(b64);
|
|
1219
|
+
const bytes = new Uint8Array(bin.length);
|
|
1220
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1221
|
+
return new TextDecoder().decode(bytes);
|
|
1222
|
+
}
|
|
1223
|
+
return Buffer.from(b64, "base64").toString("utf-8");
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/spaces/members.ts
|
|
1234
1227
|
function makeJoinRequest(session) {
|
|
1235
1228
|
const req2 = { edPub: session.keys.edPub, kemPub: session.keys.kemPub, userId: session.userId };
|
|
1236
1229
|
return JSON.stringify(req2);
|
|
@@ -1238,24 +1231,26 @@ function makeJoinRequest(session) {
|
|
|
1238
1231
|
function isAlreadyPresentRecipient(err) {
|
|
1239
1232
|
return err instanceof Error && /already present in epoch/.test(err.message);
|
|
1240
1233
|
}
|
|
1241
|
-
|
|
1234
|
+
function isKeyringMissing(err) {
|
|
1235
|
+
return err instanceof Error && /not found|404|does not exist/i.test(err.message);
|
|
1236
|
+
}
|
|
1237
|
+
async function inviteToSpace(session, spaceId, requestJson, canWrite = true, spaceName) {
|
|
1238
|
+
const req2 = JSON.parse(requestJson);
|
|
1239
|
+
if (!req2.edPub || !req2.kemPub || !req2.userId) throw new Error("That is not a valid join request.");
|
|
1240
|
+
await addSpaceMember(session.accountClient, spaceId, session.userId, req2.userId);
|
|
1242
1241
|
try {
|
|
1243
1242
|
await addCollectionRecipient(
|
|
1244
1243
|
session.chatClient,
|
|
1245
1244
|
keyringName(spaceId),
|
|
1246
|
-
{ subKem:
|
|
1245
|
+
{ subKem: req2.kemPub, userId: req2.userId, label: req2.userId.slice(0, 8) },
|
|
1247
1246
|
{ edPriv: session.keys.edPriv, edPub: session.keys.edPub, kemPriv: session.keys.kemPriv },
|
|
1248
1247
|
{ trustedAdders: [session.keys.edPub] }
|
|
1249
1248
|
);
|
|
1250
1249
|
} catch (err) {
|
|
1251
|
-
if (!isAlreadyPresentRecipient(err)
|
|
1250
|
+
if (!isAlreadyPresentRecipient(err) && !isKeyringMissing(err)) {
|
|
1251
|
+
console.warn("[octospaces] inviteToSpace: keyring add skipped", err);
|
|
1252
|
+
}
|
|
1252
1253
|
}
|
|
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
1254
|
const cap = await mintMemberCap(
|
|
1260
1255
|
session.keys.edPriv,
|
|
1261
1256
|
session.keys.edPub,
|
|
@@ -1279,11 +1274,7 @@ async function acceptSpaceInvite(session, inviteJson) {
|
|
|
1279
1274
|
if (!cap.sub || cap.sub !== session.keys.edPub) {
|
|
1280
1275
|
throw new Error("This invite was issued for a different identity.");
|
|
1281
1276
|
}
|
|
1282
|
-
if (!cap.iss) throw new Error("This invite is missing its issuer.");
|
|
1283
1277
|
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
1278
|
const capJson = JSON.stringify(cap);
|
|
1288
1279
|
const name = inv.spaceName?.trim() || `space-${spaceId.slice(-6)}`;
|
|
1289
1280
|
const space = { id: spaceId, name, short: name.slice(0, 2).toUpperCase(), members: 1 };
|
|
@@ -1321,21 +1312,29 @@ async function createSpaceInviteLink(session, spaceId, spaceName, write, origin)
|
|
|
1321
1312
|
spaceMemberScope(spaceId, write)
|
|
1322
1313
|
);
|
|
1323
1314
|
await addSpaceMember(session.accountClient, spaceId, session.userId, ephemeralUserId);
|
|
1315
|
+
try {
|
|
1316
|
+
await addCollectionRecipient(
|
|
1317
|
+
session.chatClient,
|
|
1318
|
+
keyringName(spaceId),
|
|
1319
|
+
{ subKem: ek.kemPub, userId: ephemeralUserId, label: ephemeralUserId.slice(0, 8) },
|
|
1320
|
+
{ edPriv: session.keys.edPriv, edPub: session.keys.edPub, kemPriv: session.keys.kemPriv },
|
|
1321
|
+
{ trustedAdders: [session.keys.edPub] }
|
|
1322
|
+
);
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
if (!isAlreadyPresentRecipient(err) && !isKeyringMissing(err)) {
|
|
1325
|
+
console.warn("[octospaces] createSpaceInviteLink: keyring add skipped", err);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1324
1328
|
const token = { v: 1, spaceId, spaceName, cap, key: ek.edPriv, write };
|
|
1325
1329
|
return { token, link: encodeSpaceInviteLink(origin, token) };
|
|
1326
1330
|
}
|
|
1327
1331
|
async function joinSpaceByLink(session, token) {
|
|
1328
|
-
const cap = token.cap;
|
|
1329
|
-
const ownerId = cap.iss ? await userIdFromEdPub(cap.iss) : void 0;
|
|
1330
1332
|
const name = token.spaceName.trim() || `space-${token.spaceId.slice(-6)}`;
|
|
1331
1333
|
const space = {
|
|
1332
1334
|
id: token.spaceId,
|
|
1333
1335
|
name,
|
|
1334
1336
|
short: name.slice(0, 2).toUpperCase(),
|
|
1335
|
-
members: 1
|
|
1336
|
-
visibility: "public",
|
|
1337
|
-
...ownerId ? { ownerId } : {},
|
|
1338
|
-
write: token.write
|
|
1337
|
+
members: 1
|
|
1339
1338
|
};
|
|
1340
1339
|
const accessPayload = { cap: token.cap, key: token.key, write: token.write };
|
|
1341
1340
|
const sealed = await sealToSelf(session, JSON.stringify(accessPayload));
|
|
@@ -1343,6 +1342,19 @@ async function joinSpaceByLink(session, token) {
|
|
|
1343
1342
|
saveSpaceAccessEntry(token.spaceId, { kind: "link", cap: token.cap, key: token.key, write: token.write });
|
|
1344
1343
|
return space;
|
|
1345
1344
|
}
|
|
1345
|
+
async function addDeviceToSpaceKeyring(session, spaceId, device) {
|
|
1346
|
+
try {
|
|
1347
|
+
await addCollectionRecipient(
|
|
1348
|
+
session.chatClient,
|
|
1349
|
+
keyringName(spaceId),
|
|
1350
|
+
{ subKem: device.kemPub, userId: device.userId, label: device.userId.slice(0, 8) },
|
|
1351
|
+
{ edPriv: session.keys.edPriv, edPub: session.keys.edPub, kemPriv: session.keys.kemPriv },
|
|
1352
|
+
{ trustedAdders: [session.keys.edPub] }
|
|
1353
|
+
);
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
if (!isAlreadyPresentRecipient(err) && !isKeyringMissing(err)) throw err;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1346
1358
|
async function recoverSpaceAccess(session, server) {
|
|
1347
1359
|
const linkAccess = {};
|
|
1348
1360
|
for (const [spaceId, sealed] of Object.entries(server.pubAccess)) {
|
|
@@ -1378,7 +1390,333 @@ async function recoverSpaceAccess(session, server) {
|
|
|
1378
1390
|
}
|
|
1379
1391
|
}
|
|
1380
1392
|
|
|
1393
|
+
// src/spaces/nodes.ts
|
|
1394
|
+
init_client();
|
|
1395
|
+
init_identity();
|
|
1396
|
+
init_paths();
|
|
1397
|
+
init_space_access();
|
|
1398
|
+
init_space_access_store();
|
|
1399
|
+
import { generateDeviceKeys as generateDeviceKeys2 } from "@drakkar.software/starfish-identities";
|
|
1400
|
+
import { addCollectionRecipient as addCollectionRecipient2 } from "@drakkar.software/starfish-keyring";
|
|
1401
|
+
import { mintMemberCap as mintMemberCap2 } from "@drakkar.software/starfish-sharing";
|
|
1402
|
+
|
|
1403
|
+
// src/objects/objects.ts
|
|
1404
|
+
init_ids();
|
|
1405
|
+
function compareSiblings(a, b) {
|
|
1406
|
+
if (a.order !== b.order) return a.order - b.order;
|
|
1407
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
1408
|
+
}
|
|
1409
|
+
function nextOrder(siblings) {
|
|
1410
|
+
let max = 0;
|
|
1411
|
+
for (const s of siblings) if (s.order > max) max = s.order;
|
|
1412
|
+
return max + 1;
|
|
1413
|
+
}
|
|
1414
|
+
function buildTree(nodes, includeArchived = false) {
|
|
1415
|
+
const live = includeArchived ? nodes : nodes.filter((n) => !n.archived);
|
|
1416
|
+
const byId = new Map(live.map((n) => [n.id, n]));
|
|
1417
|
+
const effectiveParent = (n) => {
|
|
1418
|
+
if (n.parentId == null) return null;
|
|
1419
|
+
if (!byId.has(n.parentId)) return null;
|
|
1420
|
+
const seen = /* @__PURE__ */ new Set([n.id]);
|
|
1421
|
+
let cur = n.parentId;
|
|
1422
|
+
while (cur != null) {
|
|
1423
|
+
if (seen.has(cur)) return null;
|
|
1424
|
+
seen.add(cur);
|
|
1425
|
+
const parent = byId.get(cur);
|
|
1426
|
+
if (!parent) return null;
|
|
1427
|
+
cur = parent.parentId;
|
|
1428
|
+
}
|
|
1429
|
+
return n.parentId;
|
|
1430
|
+
};
|
|
1431
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
1432
|
+
for (const n of live) {
|
|
1433
|
+
const p = effectiveParent(n);
|
|
1434
|
+
const bucket = childrenOf.get(p) ?? [];
|
|
1435
|
+
bucket.push(n);
|
|
1436
|
+
childrenOf.set(p, bucket);
|
|
1437
|
+
}
|
|
1438
|
+
function attach(parent, depth) {
|
|
1439
|
+
return (childrenOf.get(parent) ?? []).slice().sort(compareSiblings).map((n) => ({ ...n, depth, children: attach(n.id, depth + 1) }));
|
|
1440
|
+
}
|
|
1441
|
+
return attach(null, 0);
|
|
1442
|
+
}
|
|
1443
|
+
function breadcrumbs(nodes, id) {
|
|
1444
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
1445
|
+
const trail = [];
|
|
1446
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1447
|
+
let cur = id;
|
|
1448
|
+
while (cur != null && byId.has(cur) && !seen.has(cur)) {
|
|
1449
|
+
seen.add(cur);
|
|
1450
|
+
const node = byId.get(cur);
|
|
1451
|
+
trail.unshift(node);
|
|
1452
|
+
cur = node.parentId;
|
|
1453
|
+
}
|
|
1454
|
+
return trail;
|
|
1455
|
+
}
|
|
1456
|
+
function ancestors(nodes, id) {
|
|
1457
|
+
return breadcrumbs(nodes, id).slice(0, -1);
|
|
1458
|
+
}
|
|
1459
|
+
function subtreeIds(nodes, rootId) {
|
|
1460
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
1461
|
+
for (const n of nodes) {
|
|
1462
|
+
const bucket = childrenOf.get(n.parentId) ?? [];
|
|
1463
|
+
bucket.push(n.id);
|
|
1464
|
+
childrenOf.set(n.parentId, bucket);
|
|
1465
|
+
}
|
|
1466
|
+
const out = /* @__PURE__ */ new Set();
|
|
1467
|
+
const walk = (id) => {
|
|
1468
|
+
if (out.has(id)) return;
|
|
1469
|
+
out.add(id);
|
|
1470
|
+
for (const child of childrenOf.get(id) ?? []) walk(child);
|
|
1471
|
+
};
|
|
1472
|
+
walk(rootId);
|
|
1473
|
+
return out;
|
|
1474
|
+
}
|
|
1475
|
+
function addObject(nodes, input, now) {
|
|
1476
|
+
const parentId = input.parentId ?? null;
|
|
1477
|
+
const siblings = nodes.filter((n) => n.parentId === parentId);
|
|
1478
|
+
const node = {
|
|
1479
|
+
id: input.id ?? `obj-${randomId()}`,
|
|
1480
|
+
type: input.type,
|
|
1481
|
+
parentId,
|
|
1482
|
+
order: nextOrder(siblings),
|
|
1483
|
+
title: input.title,
|
|
1484
|
+
...input.emoji ? { emoji: input.emoji } : {},
|
|
1485
|
+
updatedAt: now,
|
|
1486
|
+
...input.meta ? { meta: input.meta } : {},
|
|
1487
|
+
...input.access && input.access !== "space" ? { access: input.access } : {},
|
|
1488
|
+
...input.enc ? { enc: true } : {}
|
|
1489
|
+
};
|
|
1490
|
+
return { nodes: [...nodes, node], node };
|
|
1491
|
+
}
|
|
1492
|
+
function patchObject(nodes, id, patch, now) {
|
|
1493
|
+
return nodes.map((n) => n.id === id ? { ...n, ...patch, updatedAt: now } : n);
|
|
1494
|
+
}
|
|
1495
|
+
function reparentObject(nodes, id, parentId, now) {
|
|
1496
|
+
if (id === parentId) return nodes;
|
|
1497
|
+
if (parentId != null && subtreeIds(nodes, id).has(parentId)) return nodes;
|
|
1498
|
+
const siblings = nodes.filter((n) => n.parentId === parentId && n.id !== id);
|
|
1499
|
+
return nodes.map((n) => n.id === id ? { ...n, parentId, order: nextOrder(siblings), updatedAt: now } : n);
|
|
1500
|
+
}
|
|
1501
|
+
function reorderObjects(nodes, orderById, now) {
|
|
1502
|
+
return nodes.map((n) => n.id in orderById ? { ...n, order: orderById[n.id], updatedAt: now } : n);
|
|
1503
|
+
}
|
|
1504
|
+
function archiveObject(nodes, id, now) {
|
|
1505
|
+
const ids = subtreeIds(nodes, id);
|
|
1506
|
+
return nodes.map((n) => ids.has(n.id) ? { ...n, archived: true, updatedAt: now } : n);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/spaces/nodes.ts
|
|
1510
|
+
init_object_index();
|
|
1511
|
+
init_registry();
|
|
1512
|
+
init_ids();
|
|
1513
|
+
function isAlreadyPresentRecipient2(err) {
|
|
1514
|
+
return err instanceof Error && /already present in epoch/.test(err.message);
|
|
1515
|
+
}
|
|
1516
|
+
async function createNode(session, spaceId, input, reg) {
|
|
1517
|
+
const access = input.access ?? "space";
|
|
1518
|
+
const enc = input.enc ?? false;
|
|
1519
|
+
if (access === "public" && enc) throw new Error("public+enc is not a valid combination.");
|
|
1520
|
+
const nodeId = `obj-${randomId()}`;
|
|
1521
|
+
if (enc) {
|
|
1522
|
+
const client = getSpaceClient(spaceId, session);
|
|
1523
|
+
await ownerEnsureKeyring(
|
|
1524
|
+
client,
|
|
1525
|
+
session.keys,
|
|
1526
|
+
keyringPull(spaceId),
|
|
1527
|
+
keyringPush(spaceId),
|
|
1528
|
+
ownerTrustedAdders(session)
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
let createdNode = null;
|
|
1532
|
+
await updateObjectIndex(session, spaceId, (nodes, now) => {
|
|
1533
|
+
const { nodes: next, node } = addObject(nodes, {
|
|
1534
|
+
id: nodeId,
|
|
1535
|
+
type: input.type,
|
|
1536
|
+
title: input.title,
|
|
1537
|
+
...input.emoji ? { emoji: input.emoji } : {},
|
|
1538
|
+
parentId: input.parentId ?? null,
|
|
1539
|
+
...input.meta ? { meta: input.meta } : {},
|
|
1540
|
+
access,
|
|
1541
|
+
enc: enc || void 0
|
|
1542
|
+
}, now);
|
|
1543
|
+
createdNode = next.find((n) => n.id === nodeId) ?? node;
|
|
1544
|
+
return next;
|
|
1545
|
+
}, reg);
|
|
1546
|
+
if (!createdNode) throw new Error("createNode: index update did not produce a node");
|
|
1547
|
+
return createdNode;
|
|
1548
|
+
}
|
|
1549
|
+
async function setNodeAccess(session, spaceId, nodeId, patch, reg) {
|
|
1550
|
+
if (patch.access === "public" && patch.enc) throw new Error("public+enc is not valid.");
|
|
1551
|
+
if (patch.enc) {
|
|
1552
|
+
const client = getSpaceClient(spaceId, session);
|
|
1553
|
+
await ownerEnsureKeyring(
|
|
1554
|
+
client,
|
|
1555
|
+
session.keys,
|
|
1556
|
+
keyringPull(spaceId),
|
|
1557
|
+
keyringPush(spaceId),
|
|
1558
|
+
ownerTrustedAdders(session)
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
await updateObjectIndex(session, spaceId, (nodes, now) => {
|
|
1562
|
+
const idx = nodes.findIndex((n) => n.id === nodeId);
|
|
1563
|
+
if (idx < 0) return null;
|
|
1564
|
+
const cur = nodes[idx];
|
|
1565
|
+
const next = { ...cur, updatedAt: now };
|
|
1566
|
+
if (patch.access !== void 0) {
|
|
1567
|
+
if (patch.access === "space") {
|
|
1568
|
+
delete next.access;
|
|
1569
|
+
} else {
|
|
1570
|
+
next.access = patch.access;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
if (patch.enc !== void 0) {
|
|
1574
|
+
if (!patch.enc) {
|
|
1575
|
+
delete next.enc;
|
|
1576
|
+
} else {
|
|
1577
|
+
next.enc = true;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (next.access === "public" && next.enc) throw new Error("public+enc is not valid.");
|
|
1581
|
+
const unchanged = next.access === cur.access && (next.enc ?? false) === (cur.enc ?? false);
|
|
1582
|
+
if (unchanged) return null;
|
|
1583
|
+
return nodes.map((n, i) => i === idx ? next : n);
|
|
1584
|
+
}, reg);
|
|
1585
|
+
}
|
|
1586
|
+
async function inviteToNode(session, spaceId, nodeId, requestJson, node, nodeName) {
|
|
1587
|
+
const req2 = JSON.parse(requestJson);
|
|
1588
|
+
if (!req2.edPub || !req2.kemPub || !req2.userId) throw new Error("Invalid join request.");
|
|
1589
|
+
if (node.enc) {
|
|
1590
|
+
try {
|
|
1591
|
+
await addCollectionRecipient2(
|
|
1592
|
+
session.chatClient,
|
|
1593
|
+
keyringName(spaceId),
|
|
1594
|
+
{ subKem: req2.kemPub, userId: req2.userId, label: req2.userId.slice(0, 8) },
|
|
1595
|
+
{ edPriv: session.keys.edPriv, edPub: session.keys.edPub, kemPriv: session.keys.kemPriv },
|
|
1596
|
+
{ trustedAdders: [session.keys.edPub] }
|
|
1597
|
+
);
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
if (!isAlreadyPresentRecipient2(err)) throw err;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
await addSpaceMember(session.accountClient, spaceId, session.userId, req2.userId);
|
|
1603
|
+
const spaceCap = await mintMemberCap2(
|
|
1604
|
+
session.keys.edPriv,
|
|
1605
|
+
session.keys.edPub,
|
|
1606
|
+
{ edPubHex: req2.edPub, kemPubHex: req2.kemPub, userIdHex: req2.userId },
|
|
1607
|
+
"chat",
|
|
1608
|
+
spaceMemberScope(spaceId, true)
|
|
1609
|
+
);
|
|
1610
|
+
const bundle = {
|
|
1611
|
+
spaceId,
|
|
1612
|
+
nodeId,
|
|
1613
|
+
nodeName: nodeName ?? nodeId,
|
|
1614
|
+
cap: spaceCap
|
|
1615
|
+
};
|
|
1616
|
+
if (!node.enc) {
|
|
1617
|
+
const perNodeCap = await mintMemberCap2(
|
|
1618
|
+
session.keys.edPriv,
|
|
1619
|
+
session.keys.edPub,
|
|
1620
|
+
{ edPubHex: req2.edPub, kemPubHex: req2.kemPub, userIdHex: req2.userId },
|
|
1621
|
+
"chat",
|
|
1622
|
+
nodeMemberScope(spaceId, nodeId, true)
|
|
1623
|
+
);
|
|
1624
|
+
bundle.nodeCap = perNodeCap;
|
|
1625
|
+
}
|
|
1626
|
+
return JSON.stringify(bundle);
|
|
1627
|
+
}
|
|
1628
|
+
async function acceptNodeInvite(session, bundleJson) {
|
|
1629
|
+
const bundle = JSON.parse(bundleJson);
|
|
1630
|
+
const cap = bundle.cap;
|
|
1631
|
+
if (!cap || !bundle.spaceId || !bundle.nodeId) throw new Error("Invalid node invite.");
|
|
1632
|
+
if (cap.kind !== "member") throw new Error("Invalid node invite.");
|
|
1633
|
+
if (!cap.sub || cap.sub !== session.keys.edPub) {
|
|
1634
|
+
throw new Error("This invite was issued for a different identity.");
|
|
1635
|
+
}
|
|
1636
|
+
const capJson = JSON.stringify(cap);
|
|
1637
|
+
saveSpaceAccessEntry(bundle.spaceId, { kind: "member", cap: capJson });
|
|
1638
|
+
if (bundle.nodeCap) {
|
|
1639
|
+
const nodeCapJson = JSON.stringify(bundle.nodeCap);
|
|
1640
|
+
saveNodeAccessEntry(bundle.spaceId, bundle.nodeId, { kind: "member", cap: nodeCapJson });
|
|
1641
|
+
}
|
|
1642
|
+
return bundle.nodeId;
|
|
1643
|
+
}
|
|
1644
|
+
function encodeNodeInviteLink(origin, token) {
|
|
1645
|
+
const base = origin.replace(/\/+$/, "");
|
|
1646
|
+
return `${base}/join/node#${toBase64Url(JSON.stringify(token))}`;
|
|
1647
|
+
}
|
|
1648
|
+
function decodeNodeInviteLink(fragment) {
|
|
1649
|
+
const frag = fragment.startsWith("#") ? fragment.slice(1) : fragment;
|
|
1650
|
+
const tok = JSON.parse(fromBase64Url(frag));
|
|
1651
|
+
if (!tok || !tok.spaceId || !tok.nodeId || !tok.cap || !tok.key) {
|
|
1652
|
+
throw new Error("That node invite link is malformed or incomplete.");
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
v: 1,
|
|
1656
|
+
spaceId: tok.spaceId,
|
|
1657
|
+
nodeId: tok.nodeId,
|
|
1658
|
+
nodeName: tok.nodeName ?? tok.nodeId,
|
|
1659
|
+
cap: tok.cap,
|
|
1660
|
+
key: tok.key,
|
|
1661
|
+
write: !!tok.write
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
async function createNodeInviteLink(session, spaceId, nodeId, nodeName, node, write, origin) {
|
|
1665
|
+
const ek = generateDeviceKeys2();
|
|
1666
|
+
const ephemeralUserId = await userIdFromEdPub(ek.edPub);
|
|
1667
|
+
await addSpaceMember(session.accountClient, spaceId, session.userId, ephemeralUserId);
|
|
1668
|
+
if (node.enc) {
|
|
1669
|
+
try {
|
|
1670
|
+
await addCollectionRecipient2(
|
|
1671
|
+
session.chatClient,
|
|
1672
|
+
keyringName(spaceId),
|
|
1673
|
+
{ subKem: ek.kemPub, userId: ephemeralUserId, label: ephemeralUserId.slice(0, 8) },
|
|
1674
|
+
{ edPriv: session.keys.edPriv, edPub: session.keys.edPub, kemPriv: session.keys.kemPriv },
|
|
1675
|
+
{ trustedAdders: [session.keys.edPub] }
|
|
1676
|
+
);
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
if (!isAlreadyPresentRecipient2(err)) throw err;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const cap = await mintMemberCap2(
|
|
1682
|
+
session.keys.edPriv,
|
|
1683
|
+
session.keys.edPub,
|
|
1684
|
+
{ edPubHex: ek.edPub, kemPubHex: ek.kemPub, userIdHex: ephemeralUserId },
|
|
1685
|
+
"chat",
|
|
1686
|
+
node.enc ? spaceMemberScope(spaceId, write) : nodeMemberScope(spaceId, nodeId, write)
|
|
1687
|
+
);
|
|
1688
|
+
const token = { v: 1, spaceId, nodeId, nodeName, cap, key: ek.edPriv, write };
|
|
1689
|
+
return { token, link: encodeNodeInviteLink(origin, token) };
|
|
1690
|
+
}
|
|
1691
|
+
async function joinNodeByLink(session, token) {
|
|
1692
|
+
const accessPayload = { cap: token.cap, key: token.key, write: token.write };
|
|
1693
|
+
const sealed = await sealToSelf(session, JSON.stringify(accessPayload));
|
|
1694
|
+
const { updateSpacesDoc: updateSpacesDoc2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
|
|
1695
|
+
await updateSpacesDoc2(session.accountClient, session.userId, (cur) => ({
|
|
1696
|
+
spaces: cur.spaces,
|
|
1697
|
+
caps: cur.caps,
|
|
1698
|
+
pubAccess: {
|
|
1699
|
+
...cur.pubAccess,
|
|
1700
|
+
[`${token.spaceId}:${token.nodeId}`]: sealed
|
|
1701
|
+
}
|
|
1702
|
+
}));
|
|
1703
|
+
saveNodeAccessEntry(token.spaceId, token.nodeId, {
|
|
1704
|
+
kind: "link",
|
|
1705
|
+
cap: token.cap,
|
|
1706
|
+
key: token.key,
|
|
1707
|
+
write: token.write
|
|
1708
|
+
});
|
|
1709
|
+
return token.nodeId;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/index.ts
|
|
1713
|
+
init_object_index();
|
|
1714
|
+
|
|
1381
1715
|
// src/sync/pairing.ts
|
|
1716
|
+
init_config();
|
|
1717
|
+
init_fetch_timeout();
|
|
1718
|
+
init_identity();
|
|
1719
|
+
init_paths();
|
|
1382
1720
|
import { StarfishClient as StarfishClient2 } from "@drakkar.software/starfish-client";
|
|
1383
1721
|
import {
|
|
1384
1722
|
installPairingBundle,
|
|
@@ -1401,15 +1739,6 @@ async function startDevicePairing(session, pin) {
|
|
|
1401
1739
|
{ edPriv: session.keys.edPriv, edPub: session.keys.edPub },
|
|
1402
1740
|
{ scope: linkedDeviceScope(session.userId), ttlSec: LINKED_DEVICE_TTL_SEC }
|
|
1403
1741
|
);
|
|
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
1742
|
const blob = JSON.stringify({ v: 1, keys: deviceKeys, bundle });
|
|
1414
1743
|
const sealed = await sealWithPassphrase(pin, new TextEncoder().encode(blob));
|
|
1415
1744
|
const nonce = randomNonce();
|
|
@@ -1444,6 +1773,11 @@ async function completeDevicePairing(payload, pin) {
|
|
|
1444
1773
|
};
|
|
1445
1774
|
}
|
|
1446
1775
|
|
|
1776
|
+
// src/index.ts
|
|
1777
|
+
init_pull_cache();
|
|
1778
|
+
init_profile_cache();
|
|
1779
|
+
init_fetch_timeout();
|
|
1780
|
+
|
|
1447
1781
|
// src/sync/base64.ts
|
|
1448
1782
|
var CHUNK = 24576;
|
|
1449
1783
|
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
@@ -1507,14 +1841,160 @@ function decodePure(encoded) {
|
|
|
1507
1841
|
return o === out.length ? out : out.subarray(0, o);
|
|
1508
1842
|
}
|
|
1509
1843
|
var starfishBase64 = nativeCodec ? { encode: encodeViaBtoa, decode: decodeViaAtob } : { encode: encodePure, decode: decodePure };
|
|
1844
|
+
|
|
1845
|
+
// src/utils/search-match.ts
|
|
1846
|
+
var TIER_PREFIX = 4e3;
|
|
1847
|
+
var TIER_WORD = 3e3;
|
|
1848
|
+
var TIER_SUBSTRING = 2e3;
|
|
1849
|
+
var TIER_FUZZY = 1e3;
|
|
1850
|
+
function fold(s) {
|
|
1851
|
+
let out = "";
|
|
1852
|
+
for (let i = 0; i < s.length; i++) {
|
|
1853
|
+
const base = s[i].normalize("NFD")[0];
|
|
1854
|
+
const lower = base.toLowerCase();
|
|
1855
|
+
out += lower.length === 1 ? lower : lower[0];
|
|
1856
|
+
}
|
|
1857
|
+
return out;
|
|
1858
|
+
}
|
|
1859
|
+
function isWordStart(folded, i) {
|
|
1860
|
+
if (i === 0) return true;
|
|
1861
|
+
return !/[a-z0-9]/.test(folded[i - 1]);
|
|
1862
|
+
}
|
|
1863
|
+
var startPenalty = (i) => Math.min(i * 8, 600);
|
|
1864
|
+
var lengthPenalty = (titleLen, queryLen) => Math.min(Math.max(titleLen - queryLen, 0), 100);
|
|
1865
|
+
function matchTitle(query, title) {
|
|
1866
|
+
const q = fold(query.trim());
|
|
1867
|
+
if (!q) return null;
|
|
1868
|
+
const t = fold(title);
|
|
1869
|
+
let first = -1;
|
|
1870
|
+
let wordAt = -1;
|
|
1871
|
+
for (let i = t.indexOf(q); i !== -1; i = t.indexOf(q, i + 1)) {
|
|
1872
|
+
if (first === -1) first = i;
|
|
1873
|
+
if (isWordStart(t, i)) {
|
|
1874
|
+
wordAt = i;
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
if (first === 0) {
|
|
1879
|
+
return { score: TIER_PREFIX - lengthPenalty(t.length, q.length), ranges: [{ start: 0, end: q.length }] };
|
|
1880
|
+
}
|
|
1881
|
+
if (wordAt !== -1) {
|
|
1882
|
+
return {
|
|
1883
|
+
score: TIER_WORD - startPenalty(wordAt) - lengthPenalty(t.length, q.length),
|
|
1884
|
+
ranges: [{ start: wordAt, end: wordAt + q.length }]
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
if (first !== -1) {
|
|
1888
|
+
return {
|
|
1889
|
+
score: TIER_SUBSTRING - startPenalty(first) - lengthPenalty(t.length, q.length),
|
|
1890
|
+
ranges: [{ start: first, end: first + q.length }]
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
const chars = q.replace(/\s+/g, "");
|
|
1894
|
+
if (!chars) return null;
|
|
1895
|
+
const ranges = [];
|
|
1896
|
+
let from = 0;
|
|
1897
|
+
for (let ci = 0; ci < chars.length; ci++) {
|
|
1898
|
+
const at = t.indexOf(chars[ci], from);
|
|
1899
|
+
if (at === -1) return null;
|
|
1900
|
+
const last = ranges[ranges.length - 1];
|
|
1901
|
+
if (last && last.end === at) last.end = at + 1;
|
|
1902
|
+
else ranges.push({ start: at, end: at + 1 });
|
|
1903
|
+
from = at + 1;
|
|
1904
|
+
}
|
|
1905
|
+
const firstHit = ranges[0].start;
|
|
1906
|
+
const spread = ranges[ranges.length - 1].end - firstHit - chars.length;
|
|
1907
|
+
const score = TIER_FUZZY - Math.min(spread * 8, 600) - Math.min(firstHit * 2, 200) - lengthPenalty(t.length, chars.length);
|
|
1908
|
+
return { score, ranges };
|
|
1909
|
+
}
|
|
1910
|
+
function rankResults(query, items, limit = 50) {
|
|
1911
|
+
const out = [];
|
|
1912
|
+
for (const item of items) {
|
|
1913
|
+
const m = matchTitle(query, item.title);
|
|
1914
|
+
if (m) out.push({ item, score: m.score, ranges: m.ranges });
|
|
1915
|
+
}
|
|
1916
|
+
out.sort((a, b) => b.score - a.score || b.item.updatedAt - a.item.updatedAt);
|
|
1917
|
+
return out.slice(0, limit);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// src/utils/live-sync-bus.ts
|
|
1921
|
+
var pullRegistry = /* @__PURE__ */ new Map();
|
|
1922
|
+
var statusListeners = /* @__PURE__ */ new Set();
|
|
1923
|
+
var sseUp = false;
|
|
1924
|
+
function registerPull(docPath, fn) {
|
|
1925
|
+
pullRegistry.set(docPath, fn);
|
|
1926
|
+
return () => {
|
|
1927
|
+
if (pullRegistry.get(docPath) === fn) pullRegistry.delete(docPath);
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
function dispatchDocChange(docPath) {
|
|
1931
|
+
const pull2 = pullRegistry.get(docPath);
|
|
1932
|
+
if (!pull2) return false;
|
|
1933
|
+
pull2();
|
|
1934
|
+
return true;
|
|
1935
|
+
}
|
|
1936
|
+
function emitSseStatus(up) {
|
|
1937
|
+
sseUp = up;
|
|
1938
|
+
for (const l of statusListeners) l(up);
|
|
1939
|
+
}
|
|
1940
|
+
function onSseStatus(cb) {
|
|
1941
|
+
statusListeners.add(cb);
|
|
1942
|
+
cb(sseUp);
|
|
1943
|
+
return () => statusListeners.delete(cb);
|
|
1944
|
+
}
|
|
1945
|
+
function clearLiveSyncBus() {
|
|
1946
|
+
pullRegistry.clear();
|
|
1947
|
+
sseUp = false;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/utils/invite-preview.ts
|
|
1951
|
+
function previewInvite(raw) {
|
|
1952
|
+
const text = raw.trim();
|
|
1953
|
+
if (!text) throw new Error("Paste an invite link or code first.");
|
|
1954
|
+
if (text.includes("#")) {
|
|
1955
|
+
const fragment = text.slice(text.indexOf("#"));
|
|
1956
|
+
try {
|
|
1957
|
+
const token = decodeNodeInviteLink(fragment);
|
|
1958
|
+
return {
|
|
1959
|
+
kind: "node-link",
|
|
1960
|
+
spaceName: `space-${token.spaceId.slice(-6)}`,
|
|
1961
|
+
nodeTitle: token.nodeName,
|
|
1962
|
+
token
|
|
1963
|
+
};
|
|
1964
|
+
} catch {
|
|
1965
|
+
}
|
|
1966
|
+
try {
|
|
1967
|
+
const token = decodeSpaceInviteLink(fragment);
|
|
1968
|
+
return { kind: "space-link", spaceName: token.spaceName, write: token.write, token };
|
|
1969
|
+
} catch {
|
|
1970
|
+
throw new Error("That invite link appears to be invalid or expired.");
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
let parsed;
|
|
1974
|
+
try {
|
|
1975
|
+
parsed = JSON.parse(text);
|
|
1976
|
+
} catch {
|
|
1977
|
+
throw new Error("That doesn't look like an invite. Paste the full invite code or link.");
|
|
1978
|
+
}
|
|
1979
|
+
if (!parsed?.spaceId || parsed.cap?.kind !== "member") {
|
|
1980
|
+
throw new Error("That is not a valid space invite.");
|
|
1981
|
+
}
|
|
1982
|
+
const iss = parsed.cap?.iss;
|
|
1983
|
+
return {
|
|
1984
|
+
kind: "member-bundle",
|
|
1985
|
+
spaceName: parsed.spaceName?.trim() || `space-${parsed.spaceId.slice(-6)}`,
|
|
1986
|
+
spaceId: parsed.spaceId,
|
|
1987
|
+
issuerKey: typeof iss === "string" && iss.length >= 8 ? `${iss.slice(0, 8)}\u2026${iss.slice(-8)}` : null,
|
|
1988
|
+
inviteJson: text
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1510
1991
|
export {
|
|
1511
1992
|
CONNECT_TIMEOUT_MS,
|
|
1512
|
-
CategoryError,
|
|
1513
|
-
DEFAULT_CATEGORY,
|
|
1514
1993
|
OBJECT_COLLECTIONS,
|
|
1515
1994
|
PAIR_PREFIX,
|
|
1516
1995
|
PULL_CACHE_MAX_AGE_MS,
|
|
1517
1996
|
SpaceAccessError,
|
|
1997
|
+
acceptNodeInvite,
|
|
1518
1998
|
acceptSpaceInvite,
|
|
1519
1999
|
accountScope,
|
|
1520
2000
|
addDeviceToSpaceKeyring,
|
|
@@ -1525,6 +2005,7 @@ export {
|
|
|
1525
2005
|
addSpaceMember,
|
|
1526
2006
|
ancestors,
|
|
1527
2007
|
archiveObject,
|
|
2008
|
+
attachmentName,
|
|
1528
2009
|
attachmentPull,
|
|
1529
2010
|
attachmentPush,
|
|
1530
2011
|
breadcrumbs,
|
|
@@ -1532,39 +2013,50 @@ export {
|
|
|
1532
2013
|
buildAuthHeaders,
|
|
1533
2014
|
buildEncryptor,
|
|
1534
2015
|
buildLinkedSession,
|
|
2016
|
+
buildNodeAccess,
|
|
1535
2017
|
buildSession,
|
|
1536
|
-
buildSpaceAccess,
|
|
1537
2018
|
buildTree,
|
|
1538
2019
|
bytesToHex,
|
|
1539
2020
|
cacheProfile,
|
|
1540
2021
|
capProviderFor,
|
|
1541
|
-
|
|
1542
|
-
|
|
2022
|
+
clearLiveSyncBus,
|
|
2023
|
+
clearNodeAccessCache,
|
|
1543
2024
|
clearSpaceAccessStore,
|
|
1544
2025
|
completeDevicePairing,
|
|
1545
2026
|
configureKv,
|
|
1546
2027
|
configureOctoSpaces,
|
|
2028
|
+
createNode,
|
|
2029
|
+
createNodeInviteLink,
|
|
1547
2030
|
createSpace,
|
|
1548
2031
|
createSpaceInviteLink,
|
|
2032
|
+
decodeNodeInviteLink,
|
|
1549
2033
|
decodeSpaceInviteLink,
|
|
1550
2034
|
deriveSession,
|
|
2035
|
+
dispatchDocChange,
|
|
2036
|
+
emitSseStatus,
|
|
2037
|
+
encodeNodeInviteLink,
|
|
1551
2038
|
encodeSpaceInviteLink,
|
|
1552
2039
|
ensureProfileKeys,
|
|
1553
2040
|
ensurePseudo,
|
|
1554
|
-
excludeAutomatedRooms,
|
|
1555
2041
|
fetchWithTimeout,
|
|
1556
2042
|
fingerprintFromUserId,
|
|
2043
|
+
fold,
|
|
1557
2044
|
fromBase64Url,
|
|
1558
2045
|
generateSeedWords,
|
|
2046
|
+
getNodeAccess,
|
|
2047
|
+
getNodeAccessEntry,
|
|
1559
2048
|
getSharedSpacesNamespace,
|
|
1560
|
-
getSpaceAccess,
|
|
1561
2049
|
getSpaceAccessEntry,
|
|
2050
|
+
getSpaceClient,
|
|
1562
2051
|
getSyncBase,
|
|
1563
2052
|
getSyncNamespace,
|
|
1564
2053
|
getSyncPrefix,
|
|
1565
2054
|
hydrateSpaceAccessStore,
|
|
2055
|
+
inviteToNode,
|
|
1566
2056
|
inviteToSpace,
|
|
1567
2057
|
isValidSeed,
|
|
2058
|
+
isWordStart,
|
|
2059
|
+
joinNodeByLink,
|
|
1568
2060
|
joinSpaceByLink,
|
|
1569
2061
|
keyringName,
|
|
1570
2062
|
keyringPull,
|
|
@@ -1578,64 +2070,79 @@ export {
|
|
|
1578
2070
|
localSpaceAccessEntries,
|
|
1579
2071
|
makeClient,
|
|
1580
2072
|
makeJoinRequest,
|
|
2073
|
+
matchTitle,
|
|
1581
2074
|
memberCapsFromStore,
|
|
1582
2075
|
nextOrder,
|
|
1583
|
-
|
|
2076
|
+
nodeMemberScope,
|
|
2077
|
+
objDocName,
|
|
1584
2078
|
objDocPull,
|
|
1585
2079
|
objDocPush,
|
|
2080
|
+
objIndexName,
|
|
1586
2081
|
objIndexPull,
|
|
1587
2082
|
objIndexPush,
|
|
2083
|
+
objInvName,
|
|
2084
|
+
objInvPull,
|
|
2085
|
+
objInvPush,
|
|
2086
|
+
objLogName,
|
|
1588
2087
|
objLogPull,
|
|
1589
2088
|
objLogPush,
|
|
2089
|
+
objPubName,
|
|
2090
|
+
objPubPull,
|
|
2091
|
+
objPubPush,
|
|
2092
|
+
objectBlobName,
|
|
1590
2093
|
objectBlobPull,
|
|
1591
2094
|
objectBlobPush,
|
|
1592
|
-
|
|
2095
|
+
objectDirName,
|
|
2096
|
+
objectDirPull,
|
|
1593
2097
|
onSpaceMeta,
|
|
2098
|
+
onSseStatus,
|
|
1594
2099
|
openEncryptor,
|
|
1595
2100
|
ownerEnsureKeyring,
|
|
1596
2101
|
ownerScope,
|
|
1597
2102
|
ownerTrustedAdders,
|
|
1598
2103
|
patchObject,
|
|
2104
|
+
previewInvite,
|
|
1599
2105
|
profilePull,
|
|
1600
2106
|
profilePush,
|
|
1601
2107
|
pullCache,
|
|
1602
2108
|
pushIndexSeed,
|
|
1603
2109
|
randomId,
|
|
1604
|
-
|
|
2110
|
+
rankResults,
|
|
2111
|
+
readObjectTree,
|
|
1605
2112
|
readProfile,
|
|
1606
2113
|
readProfiles,
|
|
1607
2114
|
readPseudo,
|
|
1608
|
-
|
|
1609
|
-
readSpaceIndexRooms,
|
|
1610
|
-
readSpaceRooms,
|
|
2115
|
+
readSpaceAccess,
|
|
1611
2116
|
readSpaces,
|
|
1612
2117
|
reconcileSpaceMeta,
|
|
1613
2118
|
recoverSpaceAccess,
|
|
2119
|
+
registerPull,
|
|
2120
|
+
removeNodeAccessEntry,
|
|
1614
2121
|
removeSpaceAccessEntry,
|
|
1615
2122
|
removeSpaceMember,
|
|
1616
2123
|
reorderObjects,
|
|
1617
2124
|
reorderSpaces,
|
|
1618
2125
|
reparentObject,
|
|
1619
|
-
roomKindToSubtype,
|
|
1620
2126
|
roomSlug,
|
|
1621
|
-
roomsRegistryPull,
|
|
1622
|
-
roomsRegistryPush,
|
|
1623
2127
|
rootIdentityOf,
|
|
2128
|
+
saveNodeAccessEntry,
|
|
1624
2129
|
saveSpaceAccessEntry,
|
|
1625
2130
|
sealToRecipient,
|
|
1626
2131
|
sealToSelf,
|
|
1627
|
-
seedIndexNodes,
|
|
1628
2132
|
seedSpaceObjectIndex,
|
|
1629
2133
|
setDmMapping,
|
|
1630
|
-
|
|
2134
|
+
setNodeAccess,
|
|
2135
|
+
spaceAccessPull,
|
|
2136
|
+
spaceAccessPush,
|
|
2137
|
+
spaceIdFromRoomId,
|
|
1631
2138
|
spaceMemberScope,
|
|
1632
2139
|
spacesPull,
|
|
1633
2140
|
spacesPush,
|
|
1634
2141
|
starfishBase64,
|
|
1635
2142
|
startDevicePairing,
|
|
1636
2143
|
subtreeIds,
|
|
1637
|
-
subtypeToRoomKind,
|
|
1638
2144
|
toBase64Url,
|
|
2145
|
+
typesIndexName,
|
|
1639
2146
|
typesIndexPull,
|
|
1640
2147
|
typesIndexPush,
|
|
1641
2148
|
unsealFromRecipient,
|
|
@@ -1650,7 +2157,7 @@ export {
|
|
|
1650
2157
|
userIdFromEdPub,
|
|
1651
2158
|
writeProfile,
|
|
1652
2159
|
writePseudo,
|
|
1653
|
-
|
|
2160
|
+
writeSpaceAccess,
|
|
1654
2161
|
writeSpaces
|
|
1655
2162
|
};
|
|
1656
2163
|
//# sourceMappingURL=index.js.map
|