@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/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 = () => 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;
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 = (key2) => getKv().get(key2);
46
- var kvSet = (key2, value) => getKv().set(key2, value);
47
- var kvRemove = (key2) => getKv().remove(key2);
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", "rooms"],
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", "rooms"],
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
- // 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";
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 = class extends Error {
209
- constructor(message) {
210
- super(message);
211
- this.name = "SpaceAccessError";
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, spaceId, trustedAdders) {
236
- const res = await client.pull(keyringPull(spaceId)).catch(() => {
237
- throw new Error("Could not reach the server to fetch space keys.");
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 space has no keyring yet \u2014 ask the owner to open it first.");
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 space's keyring yet \u2014 ask the owner to re-invite.");
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, spaceId, trustedAdders) {
325
+ async function buildEncryptor(client, keys, keyringPullPath, trustedAdders) {
255
326
  try {
256
- return await openEncryptor(client, keys, spaceId, trustedAdders);
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, spaceId, trustedAdders = [keys.edPub]) {
262
- const krRes = await client.pull(keyringPull(spaceId)).catch(() => null);
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(keyringPush(spaceId), keyring, krRes?.hash ?? null);
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
- // 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
- }
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
- var cache2 = /* @__PURE__ */ new Map();
597
- function clearSpaceAccessCache() {
651
+ function clearNodeAccessCache() {
598
652
  cache2.clear();
599
653
  }
600
- function getSpaceAccess(spaceId, session, reg) {
601
- const hit = cache2.get(spaceId);
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 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 };
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
- 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 };
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 visibility = reg?.visibility;
617
- if (visibility === "public") {
618
- return { encryptor: null, client: session.chatClient, isOwnerOpen: reg.owner === session.userId };
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 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."
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
- spaceId,
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(spaceId, p);
636
- p.catch(() => cache2.delete(spaceId));
708
+ cache2.set(cacheKey, p);
709
+ p.catch(() => cache2.delete(cacheKey));
637
710
  return p;
638
711
  }
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;
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 (entry?.kind === "member") {
648
- const cap = JSON.parse(entry.cap);
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
- const encryptor2 = await buildEncryptor(client, session.keys, spaceId, trustedAdders);
652
- return encryptor2 ? { client, encryptor: encryptor2 } : null;
724
+ } else {
725
+ client = session.chatClient;
653
726
  }
654
- if (hint?.visibility === "public") {
655
- return { client, encryptor: null };
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
- // 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 });
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
- return out;
833
- }
743
+ });
834
744
 
835
745
  // src/spaces/object-index.ts
836
746
  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;
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 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) {
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 (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);
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
- const { client, encryptor } = await getSpaceAccess(spaceId, session, reg ?? null);
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 plain = raw ? encryptor ? await encryptor.decrypt(raw) : raw : {};
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(pushPath, payload, res?.hash ?? null);
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 spaceMetaListeners = /* @__PURE__ */ new Set();
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 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) => {
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 writeRooms(client, spaceId, owner, members, hash, meta) {
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
- roomsRegistryPush(spaceId),
1131
- { v: 1, owner, members, ...visibility ? { visibility } : {}, ...name ? { name } : {}, ...image ? { image } : {} },
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, visibility, name, image, hash } = await readRooms(client, spaceId);
1056
+ const { owner, members, name, image, hash } = await readSpaceAccess(client, spaceId);
1137
1057
  if (memberUserId === (owner ?? ownerUserId) || members.includes(memberUserId)) return;
1138
- await writeRooms(client, spaceId, owner ?? ownerUserId, [...members, memberUserId], hash, { name, image, visibility: visibility ?? void 0 });
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, visibility, name, image, hash } = await readRooms(client, spaceId);
1061
+ const { owner, members, name, image, hash } = await readSpaceAccess(client, spaceId);
1142
1062
  if (!members.includes(memberUserId)) return;
1143
- await writeRooms(client, spaceId, owner ?? memberUserId, members.filter((m) => m !== memberUserId), hash, { name, image, visibility: visibility ?? void 0 });
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, opts) {
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 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 });
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
- // 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);
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
- return Buffer.from(b64, "base64").toString("utf-8");
1231
- }
1133
+ });
1232
1134
 
1233
- // src/spaces/members.ts
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
- async function addDeviceToSpaceKeyring(session, spaceId, recipient) {
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: recipient.kemPub, userId: recipient.userId, label: recipient.userId.slice(0, 8) },
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)) throw 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
- categoryId,
1542
- clearSpaceAccessCache,
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
- normalizeCategories,
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
- objectsToRoomCategories,
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
- readIndexRooms,
2110
+ rankResults,
2111
+ readObjectTree,
1605
2112
  readProfile,
1606
2113
  readProfiles,
1607
2114
  readPseudo,
1608
- readRooms,
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
- spaceIndexPull,
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
- writeRooms,
2160
+ writeSpaceAccess,
1654
2161
  writeSpaces
1655
2162
  };
1656
2163
  //# sourceMappingURL=index.js.map