@drakkar.software/octospaces-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/index.d.ts +972 -0
  2. package/dist/index.js +1656 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/platform/index.d.ts +9 -0
  5. package/dist/platform/index.js +111 -0
  6. package/dist/platform/index.js.map +1 -0
  7. package/dist/platform/index.native.d.ts +9 -0
  8. package/dist/platform/index.native.js +106 -0
  9. package/dist/platform/index.native.js.map +1 -0
  10. package/package.json +50 -0
  11. package/src/core/adapters.ts +34 -0
  12. package/src/core/config.ts +87 -0
  13. package/src/core/ids.test.ts +45 -0
  14. package/src/core/ids.ts +29 -0
  15. package/src/core/space-access-error.ts +13 -0
  16. package/src/core/storage-types.ts +71 -0
  17. package/src/core/types.ts +162 -0
  18. package/src/index.ts +221 -0
  19. package/src/objects/objects.test.ts +288 -0
  20. package/src/objects/objects.ts +296 -0
  21. package/src/platform/index.native.ts +3 -0
  22. package/src/platform/index.ts +3 -0
  23. package/src/platform/kv.native.ts +23 -0
  24. package/src/platform/kv.ts +29 -0
  25. package/src/platform/platform.native.ts +16 -0
  26. package/src/platform/platform.ts +10 -0
  27. package/src/spaces/members.test.ts +87 -0
  28. package/src/spaces/members.ts +271 -0
  29. package/src/spaces/object-index.test.ts +105 -0
  30. package/src/spaces/object-index.ts +160 -0
  31. package/src/spaces/registry.test.ts +111 -0
  32. package/src/spaces/registry.ts +466 -0
  33. package/src/sync/account-seal.test.ts +70 -0
  34. package/src/sync/account-seal.ts +80 -0
  35. package/src/sync/base64.ts +89 -0
  36. package/src/sync/base64url.ts +22 -0
  37. package/src/sync/client.ts +301 -0
  38. package/src/sync/fetch-timeout.test.ts +26 -0
  39. package/src/sync/fetch-timeout.ts +23 -0
  40. package/src/sync/identity.ts +158 -0
  41. package/src/sync/pairing.ts +103 -0
  42. package/src/sync/paths.test.ts +135 -0
  43. package/src/sync/paths.ts +177 -0
  44. package/src/sync/profile-cache.ts +34 -0
  45. package/src/sync/pull-cache.test.ts +55 -0
  46. package/src/sync/pull-cache.ts +33 -0
  47. package/src/sync/space-access-store.test.ts +129 -0
  48. package/src/sync/space-access-store.ts +117 -0
  49. package/src/sync/space-access.ts +136 -0
  50. package/tsconfig.json +17 -0
  51. package/tsup.config.ts +40 -0
  52. package/vitest.config.ts +12 -0
@@ -0,0 +1,972 @@
1
+ import { WrappedKeyEntry } from '@drakkar.software/starfish-keyring';
2
+ import { StarfishClient, Encryptor, StarfishCapProvider, PullCache } from '@drakkar.software/starfish-client';
3
+ import { CapCert, Base64Provider } from '@drakkar.software/starfish-protocol';
4
+ import { BootstrapOrigin, ScopePreset } from '@drakkar.software/starfish-identities';
5
+
6
+ /**
7
+ * Runtime configuration for the OctoSpaces SDK — the Starfish sync server URL,
8
+ * optional namespace, events-stream URL, and public web origin.
9
+ *
10
+ * The SDK is headless and platform-agnostic, so it does NOT read environment
11
+ * variables itself. The host app reads its own env (e.g. Expo `EXPO_PUBLIC_*`) and
12
+ * calls {@link configureOctoSpaces} once at boot, before any sync/identity API runs.
13
+ * Getters throw a clear error if called before configuration so a misconfigured
14
+ * host fails fast rather than silently signing the wrong path.
15
+ */
16
+ interface OctoSpacesConfig {
17
+ /** Starfish sync server base URL (e.g. `http://localhost:8787`). */
18
+ syncBase: string;
19
+ /** Bare namespace name; the SDK prepends `/v1/<namespace>` to signed paths.
20
+ * Unset for a root-mounted (local dev) server. */
21
+ syncNamespace?: string;
22
+ /**
23
+ * Optional SEPARATE namespace for cross-app shared-spaces storage. When set,
24
+ * space registry ops use this namespace instead of `syncNamespace`, enabling a
25
+ * single shared space list across multiple app namespaces (e.g. OctoChat and
26
+ * OctoVault sharing spaces at `/v1/shared`). If unset, falls back to the default
27
+ * namespace for all operations (single-app behavior).
28
+ */
29
+ sharedSpacesNamespace?: string;
30
+ /** Override the live change-event SSE endpoint. Defaults to
31
+ * `${syncBase}${syncPrefix}/events`. */
32
+ eventsUrl?: string;
33
+ /** Public origin of the web app, used to build shareable invite links on
34
+ * platforms without `window.location` (native). Empty by default. */
35
+ webBase?: string;
36
+ /**
37
+ * Called when a background Starfish revalidation succeeds after a 429/5xx
38
+ * cache-fallback (stale-while-revalidate). Use it to signal that the server
39
+ * is reachable again so any stale views re-pull and recover.
40
+ */
41
+ onServerReachable?: () => void;
42
+ }
43
+ /** Configure the SDK. Call once at app boot before any sync/identity API. */
44
+ declare function configureOctoSpaces(config: OctoSpacesConfig): void;
45
+ /** Starfish sync server base URL. */
46
+ declare const getSyncBase: () => string;
47
+ /** Bare namespace name (or `undefined` for a root-mounted server). */
48
+ declare const getSyncNamespace: () => string | undefined;
49
+ /** Namespaced path prefix (`/v1/<namespace>`, or `''` locally). */
50
+ declare const getSyncPrefix: () => string;
51
+ /** Optional separate namespace for shared-spaces storage. `undefined` means use the default namespace. */
52
+ declare const getSharedSpacesNamespace: () => string | undefined;
53
+
54
+ /**
55
+ * Platform adapters the headless SDK needs the host app to provide.
56
+ *
57
+ * The SDK can't do Metro `.native.ts` file-extension resolution and must not bind
58
+ * to localStorage / AsyncStorage / SecureStore directly, so the host injects a
59
+ * key/value store at boot via {@link configureKv}. This holds account-scoped state
60
+ * the SDK persists offline (joined-space member caps, the public-space access map,
61
+ * read marks, mutes, profile/pull caches).
62
+ */
63
+ /** Async key/value store — web `localStorage`, native `AsyncStorage`, etc. */
64
+ interface KvAdapter {
65
+ get(key: string): Promise<string | null>;
66
+ set(key: string, value: string): Promise<void>;
67
+ remove(key: string): Promise<void>;
68
+ }
69
+ /** Install the host's key/value store. Call once at app boot. */
70
+ declare function configureKv(adapter: KvAdapter): void;
71
+ declare const kvGet: (key: string) => Promise<string | null>;
72
+ declare const kvSet: (key: string, value: string) => Promise<void>;
73
+ declare const kvRemove: (key: string) => Promise<void>;
74
+
75
+ /**
76
+ * Starfish client construction + space keyring/encryptor helpers.
77
+ */
78
+
79
+ interface DeviceKeys {
80
+ edPriv: string;
81
+ edPub: string;
82
+ kemPriv: string;
83
+ kemPub: string;
84
+ }
85
+ declare function capProviderFor(cap: unknown, devEdPrivHex: string): StarfishCapProvider;
86
+ /**
87
+ * Build a Starfish client. `namespaceOverride` overrides the configured namespace,
88
+ * enabling the shared-spaces feature (a separate namespace for cross-app registry ops).
89
+ */
90
+ declare function makeClient(cap: unknown, devEdPrivHex: string, namespaceOverride?: string): StarfishClient;
91
+ /**
92
+ * Open a SPACE's decryptor, throwing a descriptive error per failure mode
93
+ * (unreachable server / no keyring yet / not a recipient).
94
+ *
95
+ * A `SpaceAccessError` is a hard access denial; any other thrown error is a
96
+ * transient offline state.
97
+ */
98
+ declare function openEncryptor(client: StarfishClient, keys: DeviceKeys, spaceId: string, trustedAdders: string[]): Promise<Encryptor>;
99
+ /** Soft variant of {@link openEncryptor}: returns null instead of throwing. */
100
+ declare function buildEncryptor(client: StarfishClient, keys: DeviceKeys, spaceId: string, trustedAdders: string[]): Promise<Encryptor | null>;
101
+ /**
102
+ * Owner-side: create the SPACE keyring if missing, return an encryptor.
103
+ */
104
+ declare function ownerEnsureKeyring(client: StarfishClient, keys: DeviceKeys, spaceId: string, trustedAdders?: string[]): Promise<Encryptor>;
105
+ /** A user's public profile: display pseudo + optional inline avatar + public identity keys. */
106
+ interface PublicProfile {
107
+ pseudo: string | null;
108
+ avatar: string | null;
109
+ edPub: string | null;
110
+ kemPub: string | null;
111
+ }
112
+ /** Read any user's public profile. */
113
+ declare function readProfile(userId: string): Promise<PublicProfile>;
114
+ /** Read any user's public profile pseudo. */
115
+ declare function readPseudo(userId: string): Promise<string | null>;
116
+ /**
117
+ * Read MANY users' public profiles in one /batch/pull round-trip per chunk.
118
+ */
119
+ declare function readProfiles(ids: string[]): Promise<Map<string, PublicProfile>>;
120
+ /**
121
+ * Merge a patch into the caller's own profile doc.
122
+ */
123
+ declare function writeProfile(client: StarfishClient, userId: string, patch: {
124
+ pseudo?: string;
125
+ avatar?: string | null;
126
+ edPub?: string;
127
+ kemPub?: string;
128
+ }): Promise<void>;
129
+ /** Write the caller's own profile pseudo. */
130
+ declare function writePseudo(client: StarfishClient, userId: string, pseudo: string): Promise<void>;
131
+ /**
132
+ * Publish this identity's PUBLIC keys in its profile so a peer can start an E2EE DM.
133
+ * One-time + idempotent. ROOT-DEVICE ONLY — `profile` is `device:root`-write.
134
+ */
135
+ declare function ensureProfileKeys(client: StarfishClient, userId: string, keys: {
136
+ edPub: string;
137
+ kemPub: string;
138
+ }): Promise<void>;
139
+ /**
140
+ * Build cap-cert auth headers for a raw `fetch` outside the StarfishClient.
141
+ */
142
+ declare function buildAuthHeaders(cap: unknown, devEdPrivHex: string, method: string, pathAndQuery: string): Promise<Record<string, string>>;
143
+ /**
144
+ * Seed the caller's profile pseudo only if none exists yet, returning the
145
+ * authoritative server value.
146
+ */
147
+ declare function ensurePseudo(client: StarfishClient, userId: string, fallback: string): Promise<string>;
148
+
149
+ /**
150
+ * Shared types for the persisted-session storage layer. Both platform variants
151
+ * (`storage.ts` web, `storage.native.ts` native) implement the same contract so
152
+ * the session context stays platform-agnostic.
153
+ */
154
+
155
+ /**
156
+ * The root identity already derived from the seed (userId + device keys). Caching
157
+ * it lets unlock/cold-start skip the heavy `bootstrapRootIdentity` Argon2id.
158
+ * Equivalent in sensitivity to the seed, so it lives inside the same sealed blob.
159
+ */
160
+ interface DerivedIdentity {
161
+ userId: string;
162
+ keys: DeviceKeys;
163
+ }
164
+ /** The recovery seed + display name — the minimum needed to re-derive an identity. */
165
+ interface PersistedSession {
166
+ /** BIP-39 recovery seed. Absent for non-seed origins. */
167
+ seed?: string[];
168
+ name: string;
169
+ /** Cached root identity so restore skips the bootstrap Argon2id. */
170
+ derived?: DerivedIdentity;
171
+ /** How this identity was bootstrapped. Absent for seed-derived identities. */
172
+ bootstrapOrigin?: BootstrapOrigin;
173
+ /** Root-signed cap-cert for a PAIRED (linked) device. */
174
+ capCert?: CapCert;
175
+ }
176
+ /**
177
+ * Every account held on this device plus which one is active. The whole vault is
178
+ * sealed as a unit (web: under one app-lock via a vault master key; native: a
179
+ * single secure-store entry).
180
+ */
181
+ interface Vault {
182
+ accounts: PersistedSession[];
183
+ activeId: string;
184
+ }
185
+ /** Ways the web-persisted seed can be unlocked. */
186
+ type UnlockMethod = 'pin' | 'passkey';
187
+ /** A registered passkey + the PRF secret used to seal the seed for it. */
188
+ interface PasskeyEnrollment {
189
+ credentialId: string;
190
+ salt: string;
191
+ secretHex: string;
192
+ }
193
+ /** How to lock the seed when persisting it (web only). */
194
+ interface SeedLock {
195
+ pin: string;
196
+ passkey?: PasskeyEnrollment;
197
+ }
198
+ /**
199
+ * Result of probing storage at launch:
200
+ * - `none` — nothing stored; start signed-out.
201
+ * - `ready` — vault available immediately (native Keychain path).
202
+ * - `locked` — a sealed vault exists; unlock with one of `methods` (web path).
203
+ * - `error` — storage read failed.
204
+ */
205
+ type VaultLoad = {
206
+ kind: 'none';
207
+ } | {
208
+ kind: 'ready';
209
+ vault: Vault;
210
+ } | {
211
+ kind: 'locked';
212
+ methods: UnlockMethod[];
213
+ } | {
214
+ kind: 'error';
215
+ error: unknown;
216
+ };
217
+
218
+ interface Session {
219
+ userId: string;
220
+ name: string;
221
+ keys: DeviceKeys;
222
+ chatCap: unknown;
223
+ accountCap: unknown;
224
+ /**
225
+ * The primary Starfish client for space content (keyring, channels, objects).
226
+ * Uses the app's default namespace.
227
+ */
228
+ chatClient: StarfishClient;
229
+ /**
230
+ * The Starfish client for account-scoped content (profile, _spaces registry).
231
+ * Uses the app's default namespace.
232
+ */
233
+ accountClient: StarfishClient;
234
+ /**
235
+ * Starfish client for cross-app shared-spaces registry operations.
236
+ * When `sharedSpacesNamespace` is configured, uses that namespace override so
237
+ * the spaces list lives in a separate namespace shared across multiple apps.
238
+ * Falls back to `accountClient` when no shared namespace is configured.
239
+ */
240
+ spacesRegistryClient: StarfishClient;
241
+ /**
242
+ * Starfish client for cross-app shared-spaces keyring operations.
243
+ * Same namespace logic as `spacesRegistryClient`, scoped to space content.
244
+ * Falls back to `chatClient` when no shared namespace is configured.
245
+ */
246
+ spacesKeyringClient: StarfishClient;
247
+ fingerprint: string;
248
+ /**
249
+ * The Ed25519 pubkey that signs this identity's OWNED-space keyring entries —
250
+ * the trusted-adder provenance anchor for opening them.
251
+ */
252
+ ownerEdPub: string;
253
+ }
254
+ /**
255
+ * Trusted-adder allow-list for opening an OWNED space's keyring.
256
+ */
257
+ declare function ownerTrustedAdders(session: Session): string[];
258
+ /** Fresh 12-word recovery seed. */
259
+ declare function generateSeedWords(): string[];
260
+ declare function isValidSeed(words: string[]): boolean;
261
+ /** Human-readable fingerprint derived from the identity's user id. */
262
+ declare function fingerprintFromUserId(userId: string): string;
263
+ /**
264
+ * Build a full owner session (caps + clients + pseudo) from an already-derived
265
+ * root identity. No Argon2id — only fast Ed25519 cap-minting plus a profile fetch.
266
+ */
267
+ declare function buildSession({ userId, keys }: DerivedIdentity, name?: string): Promise<Session>;
268
+ /** A paired device's credentials: its own keypair + the root-signed cap-cert. */
269
+ interface LinkedIdentity {
270
+ userId: string;
271
+ keys: DeviceKeys;
272
+ capCert: CapCert;
273
+ }
274
+ /**
275
+ * Build a session for a PAIRED (linked) device. Unlike {@link buildSession}, the
276
+ * device keypair is NOT the root, so it cannot self-mint caps — both clients are
277
+ * driven by the single root-signed `capCert` from the pairing bundle.
278
+ */
279
+ declare function buildLinkedSession({ userId, keys, capCert }: LinkedIdentity, name?: string): Promise<Session>;
280
+ /** Derive a full owner session (identity + caps + clients) from a seed. */
281
+ declare function deriveSession(seedWords: string[], name?: string): Promise<Session>;
282
+ /** The cached root identity (userId + keys) carried by a built session. */
283
+ declare function rootIdentityOf(s: Session): DerivedIdentity;
284
+
285
+ /** A payload sealed to a KEM key: the wrapped CEK + hex(iv ‖ AES-GCM ct). */
286
+ interface SealedBlob {
287
+ entry: WrappedKeyEntry;
288
+ ct: string;
289
+ }
290
+ /** Seal `plaintext` so only this account (its seed) can open it. */
291
+ declare function sealToSelf(session: Session, plaintext: string): Promise<SealedBlob>;
292
+ /** Open a {@link SealedBlob} sealed by {@link sealToSelf} for this account. */
293
+ declare function unsealFromSelf(session: Session, blob: SealedBlob): Promise<string>;
294
+ /** Seal `plaintext` to ANOTHER user's published KEM key, signed by this session. */
295
+ declare function sealToRecipient(session: Session, recipientKemPub: string, plaintext: string): Promise<SealedBlob>;
296
+ /** Open a {@link SealedBlob} sealed to THIS account by some (arbitrary) sender. */
297
+ declare function unsealFromRecipient(session: Session, blob: SealedBlob): Promise<string>;
298
+
299
+ type ID = string;
300
+ /** Maps a joined private space's id → its owner-issued member cap-cert (serialized
301
+ * JSON). Persisted both in device-local kv (`member-caps.ts`) and, for durability,
302
+ * in the user's own synced `_spaces` doc so a fresh device re-hydrates it. */
303
+ type CapMap = Record<string, string>;
304
+ /** Maps a joined PUBLIC space's id → its invitation credential (the owner-signed cap
305
+ * plus the link's ephemeral private key) SEALED to the account's own key. Unlike a
306
+ * member cap (safe in the clear — see {@link CapMap}), a public-join credential
307
+ * embeds a bearer secret, so it is sealed before riding in the plaintext `_spaces`
308
+ * doc. Recovered on any device with the same seed. See `account-seal.ts` and
309
+ * `space-access-store.ts`. */
310
+ type PubAccessMap = Record<string, SealedBlob>;
311
+ /** Maps a DM peer's userId → the private DM-space id shared with them. */
312
+ type DmMap = Record<string, string>;
313
+ /** The set of DM-space ids the user has archived (hidden from the DM list). */
314
+ type ArchivedDms = Record<string, true>;
315
+ /** A mute entry. `true` = muted indefinitely; a number = muted UNTIL that epoch-ms instant. */
316
+ type MuteValue = true | number;
317
+ /** Per-user mute preferences: which rooms and which whole spaces are silenced. */
318
+ interface MutePrefs {
319
+ rooms: Record<string, MuteValue>;
320
+ spaces: Record<string, MuteValue>;
321
+ }
322
+ /** A per-room read mark: the epoch-ms instant the viewer last read that room. */
323
+ type ReadValue = number;
324
+ /** Per-user read marks — the timestamp each room was last read. */
325
+ interface ReadPrefs {
326
+ rooms: Record<string, ReadValue>;
327
+ }
328
+ /** Whether a space encrypts its content client-side. */
329
+ type SpaceVisibility = 'private' | 'public';
330
+ interface Space {
331
+ id: ID;
332
+ name: string;
333
+ /** 2-letter monogram used in the space rail. */
334
+ short: string;
335
+ /** Uploaded space image as a data URI; absent → render the `short` monogram. */
336
+ image?: string;
337
+ members: number;
338
+ unread?: number;
339
+ /** 'private' (E2EE keyring, the default) or 'public' (plaintext, joined via a
340
+ * space-wide invitation link). Absent ⇒ treat as 'private' (back-compat). */
341
+ visibility?: SpaceVisibility;
342
+ /** Public spaces only: the owner's userId (derived from the cap issuer). */
343
+ ownerId?: string;
344
+ /** Public spaces only (joiner side): whether this identity's invite link grants write. */
345
+ write?: boolean;
346
+ }
347
+ /** Legacy room kind — used while apps migrate their content onto objects.
348
+ * New code should use {@link RoomSubtype} directly. */
349
+ type RoomKind = 'channel' | 'dm' | 'automated';
350
+ /** Scheduled-fetch cadence for automated rooms. */
351
+ type AutomationSchedule = {
352
+ kind: 'interval';
353
+ everyMin: number;
354
+ } | {
355
+ kind: 'daily';
356
+ hour: number;
357
+ minute: number;
358
+ } | {
359
+ kind: 'weekly';
360
+ weekday: number;
361
+ hour: number;
362
+ minute: number;
363
+ } | {
364
+ kind: 'cron';
365
+ expression: string;
366
+ };
367
+ /** Stored, synced configuration of an `automated` room. */
368
+ interface AutomationMeta {
369
+ providerId: string;
370
+ params: Record<string, unknown>;
371
+ intervalMin: number;
372
+ schedule?: AutomationSchedule;
373
+ onOpen?: boolean;
374
+ enabled: boolean;
375
+ credential: SealedBlob;
376
+ botUserId?: string;
377
+ runOnDeviceId: string | null;
378
+ lastRunAt: number | null;
379
+ lastFetchHash?: string | null;
380
+ lastError: string | null;
381
+ }
382
+ /** A transitional Room shape (used while apps migrate onto the object model). */
383
+ interface Room {
384
+ id: ID;
385
+ spaceId: ID;
386
+ category: string;
387
+ name: string;
388
+ kind: RoomKind;
389
+ topic?: string;
390
+ unread?: number;
391
+ mention?: boolean;
392
+ avatar?: string;
393
+ automation?: AutomationMeta;
394
+ }
395
+ /** The builtin object types. Custom types ride the same `string` field. */
396
+ type BuiltinObjectType = 'room' | 'category' | 'automation' | 'doc' | 'project' | 'task';
397
+ type ObjectType = BuiltinObjectType | (string & {});
398
+ /** Runtime set of builtin type strings for renderer branching. */
399
+ declare const BUILTIN_OBJECT_TYPES: readonly BuiltinObjectType[];
400
+ /** How an object's content syncs. Builtins infer this; custom types set it explicitly. */
401
+ type ObjectContentKind = 'merge' | 'append' | 'none';
402
+ /** When `type === 'room'`, the room flavour. */
403
+ type RoomSubtype = 'channel' | 'dm' | 'automation';
404
+ /**
405
+ * One entry in a space's object index (`spaces/{spaceId}/objects/_index`).
406
+ * Identity + tree position + light metadata ONLY — heavy content (messages, doc
407
+ * blocks, project event log) lives in a per-object content doc keyed by `id`.
408
+ */
409
+ interface ObjectNode {
410
+ id: ID;
411
+ type: ObjectType;
412
+ subtype?: RoomSubtype;
413
+ parentId: ID | null;
414
+ order: number;
415
+ title: string;
416
+ emoji?: string;
417
+ updatedAt: number;
418
+ archived?: boolean;
419
+ automation?: AutomationMeta;
420
+ contentKind?: ObjectContentKind;
421
+ meta?: Record<string, unknown>;
422
+ }
423
+ /** The object-index doc: the union-merged list of every object in a space. */
424
+ interface ObjectsIndex {
425
+ v: 1;
426
+ objects: ObjectNode[];
427
+ updatedAt: number;
428
+ }
429
+
430
+ /**
431
+ * Identifier helpers — one source for unguessable ids.
432
+ *
433
+ * `randomId()` is a CSPRNG-backed 128-bit id (16 random bytes, hex). Use it for
434
+ * EVERY storage/space/room/object/blob id. Hex output is path-safe and server-safe.
435
+ */
436
+ declare function randomId(): string;
437
+ /**
438
+ * Slug for the human part of an id (e.g. `<spaceId>-<slug>-<ts>`). Restricted to
439
+ * URL-clean `[a-z0-9-]` so the id is safe as both a URL path segment and a
440
+ * server storage-path leaf (the server's FilesystemObjectStore rejects any key
441
+ * outside `[a-zA-Z0-9._:@/-]`). Falls back to `'room'` when a name strips to nothing.
442
+ */
443
+ declare function roomSlug(name: string): string;
444
+
445
+ /**
446
+ * Collection path + cap-scope helpers (merged from OctoChat + OctoVault).
447
+ *
448
+ * Paths are signed relative to SYNC_BASE; the server mounts the sync router at
449
+ * root, so they start with /pull or /push. Everything for a space is nested under
450
+ * `spaces/{spaceId}/…` so the `{spaceId}` segment gates it all uniformly through the
451
+ * space:owner/space:member enricher, and a single `spaces/{spaceId}/**` member cap
452
+ * covers a whole space.
453
+ *
454
+ * **Generic object collections** — scopes use the `obj*` collection names (the
455
+ * domain-neutral storage layer both apps migrate onto). App-specific collection
456
+ * names like `'chat'` are left for the consumer's own `paths.ts` extension until
457
+ * that app finishes migrating.
458
+ */
459
+
460
+ declare const keyringName: (spaceId: string) => string;
461
+ declare const keyringPull: (spaceId: string) => string;
462
+ declare const keyringPush: (spaceId: string) => string;
463
+ declare const attachmentPull: (roomId: string, blobId: string) => string;
464
+ declare const attachmentPush: (roomId: string, blobId: string) => string;
465
+ declare const profilePull: (userId: string) => string;
466
+ declare const profilePush: (userId: string) => string;
467
+ declare const spacesPull: (userId: string) => string;
468
+ declare const spacesPush: (userId: string) => string;
469
+ declare const roomsRegistryPull: (spaceId: string) => string;
470
+ declare const roomsRegistryPush: (spaceId: string) => string;
471
+ declare const objIndexPull: (spaceId: string) => string;
472
+ declare const objIndexPush: (spaceId: string) => string;
473
+ declare const objLogPull: (spaceId: string, objectId: string) => string;
474
+ declare const objLogPush: (spaceId: string, objectId: string) => string;
475
+ declare const objDocPull: (spaceId: string, objectId: string) => string;
476
+ declare const objDocPush: (spaceId: string, objectId: string) => string;
477
+ declare const objectBlobPull: (spaceId: string, blobId: string) => string;
478
+ declare const objectBlobPush: (spaceId: string, blobId: string) => string;
479
+ declare const typesIndexPull: (spaceId: string) => string;
480
+ declare const typesIndexPush: (spaceId: string) => string;
481
+ declare const spaceIndexPull: (shard: "public") => string;
482
+ declare const OBJECT_COLLECTIONS: string[];
483
+ /** Full owner/device access to every space the identity owns. */
484
+ declare function ownerScope(): ScopePreset;
485
+ /**
486
+ * Member access to one SPACE — its keyring + every channel's messages and
487
+ * attachments + the room registry, all under `spaces/{spaceId}/**`. One cap
488
+ * covers current AND future channels.
489
+ */
490
+ declare function spaceMemberScope(spaceId: string, canWrite: boolean): ScopePreset;
491
+ /**
492
+ * Personal cap: profile + space registry + device directory + all spaces.
493
+ * Note: app-specific collections like `'dminbox'` (chat) are NOT included here —
494
+ * add them in the consumer's own `paths.ts` extension.
495
+ */
496
+ declare function accountScope(userId: string): ScopePreset;
497
+ /**
498
+ * The single cap-cert scope granted to a PAIRED (linked) device. Covers both the
499
+ * object-store client (ownerScope) and the account client (accountScope), deduped,
500
+ * because a paired device cannot self-mint — it presents one root-signed cap-cert.
501
+ */
502
+ declare function linkedDeviceScope(userId: string): ScopePreset;
503
+ declare function bytesToHex(b: Uint8Array): string;
504
+ /** The canonical identity derivation: `userId = sha256(edPub)[0:32]` (hex). */
505
+ declare function userIdFromEdPub(edPubHex: string): Promise<string>;
506
+
507
+ /**
508
+ * {@link SpaceAccessError} — a GENUINE access denial (not a transient connectivity failure).
509
+ *
510
+ * Lives in its own dependency-free module so both the low-level keyring opener
511
+ * (`client.ts`) and the higher-level space-encryptor cache (`space-encryptor.ts`) can
512
+ * throw it without an import cycle.
513
+ */
514
+ declare class SpaceAccessError extends Error {
515
+ constructor(message: string);
516
+ }
517
+
518
+ /**
519
+ * Space access resolver — returns the right (client, encryptor) pair for any
520
+ * space regardless of whether it is private (E2EE) or public (plaintext).
521
+ *
522
+ * Replaces `space-encryptor.ts`. The key invariant: public spaces have
523
+ * `encryptor: null`; private spaces always have a live `Encryptor`.
524
+ *
525
+ * Resolution order (same semantics as the old `getSpaceEncryptor`):
526
+ * 1. Link entry in the access store → sign requests as the ephemeral identity;
527
+ * no keyring, encryptor null.
528
+ * 2. Member entry → open the keyring as a recipient with the stored cap.
529
+ * 3. No entry + visibility === 'public' (from `reg`) → owner mode, no keyring.
530
+ * 4. No entry, private → either owner (open/mint keyring) or SpaceAccessError
531
+ * if the space's roster shows we're a member but we're not holding a cap yet.
532
+ */
533
+
534
+ interface SpaceAccessHandle {
535
+ encryptor: Encryptor | null;
536
+ client: StarfishClient;
537
+ /** True when opened as the space OWNER (so the caller must seed the room doc). */
538
+ isOwnerOpen: boolean;
539
+ }
540
+ /** Drop every cached handle (on account switch — keys are per-identity). */
541
+ declare function clearSpaceAccessCache(): void;
542
+ /**
543
+ * Resolve the right (client, encryptor) for a space, opening and caching on first use.
544
+ *
545
+ * `reg` is the space's `_rooms` access record if already known. Pass null when the
546
+ * caller has not yet read the registry; the resolver will probe if needed.
547
+ */
548
+ declare function getSpaceAccess(spaceId: string, session: Session, reg: {
549
+ owner: string | null;
550
+ members: string[];
551
+ visibility?: SpaceVisibility;
552
+ } | null): Promise<SpaceAccessHandle>;
553
+ /**
554
+ * SOFT resolve — never mints a keyring, never throws on missing access.
555
+ * Returns null when the identity has no usable access for the space yet.
556
+ */
557
+ declare function buildSpaceAccess(session: Session, spaceId: string, hint?: {
558
+ visibility?: SpaceVisibility;
559
+ }): Promise<{
560
+ client: StarfishClient;
561
+ encryptor: Encryptor | null;
562
+ } | null>;
563
+
564
+ /**
565
+ * Unified local access store for spaces this identity has joined.
566
+ *
567
+ * Replaces the separate `member-caps.ts` (private spaces) and `pubspace-caps.ts`
568
+ * (public/link spaces). Two entry kinds:
569
+ * - `member`: a member cap-cert (plain JSON, no bearer secret — safe to store
570
+ * in the clear). Used for PRIVATE space keyring opens.
571
+ * - `link`: an ephemeral-subject cap + the link's Ed25519 private key. Embeds a
572
+ * bearer secret so it is SEALED in the synced `_spaces.pubAccess` field before
573
+ * leaving this device; the local kv stores it plaintext only on the owning device.
574
+ *
575
+ * Two tiers (same as old member-caps): device-local kv (fast, offline) and the
576
+ * user's synced `_spaces` doc (durable source of truth; merged over local on hydrate).
577
+ * Keyed PER-USER so multiple accounts on one device never see each other's entries.
578
+ */
579
+
580
+ type SpaceAccessEntry = {
581
+ kind: 'member';
582
+ cap: string;
583
+ } | {
584
+ kind: 'link';
585
+ cap: unknown;
586
+ key: string;
587
+ write: boolean;
588
+ };
589
+ type SpaceAccessMap = Record<string, SpaceAccessEntry>;
590
+ /**
591
+ * Load the active account's space-access entries into memory. Call (and await) on
592
+ * sign-in and on every account switch, before opening rooms.
593
+ *
594
+ * `serverCaps` (private member caps from `_spaces.caps`) and `serverPubAccess`
595
+ * (sealed link credentials from `_spaces.pubAccess`, already unsealed by the caller)
596
+ * are merged OVER the local kv cache (server wins).
597
+ */
598
+ declare function hydrateSpaceAccessStore(userId: string, serverCaps: CapMap, serverLinkAccess: Record<string, {
599
+ cap: unknown;
600
+ key: string;
601
+ write: boolean;
602
+ }>): Promise<void>;
603
+ declare function getSpaceAccessEntry(spaceId: string): SpaceAccessEntry | null;
604
+ declare function saveSpaceAccessEntry(spaceId: string, entry: SpaceAccessEntry): void;
605
+ /** Forget one space's access (on leaving that space). */
606
+ declare function removeSpaceAccessEntry(spaceId: string): void;
607
+ /** A snapshot of the in-memory cache — used by `recoverSpaceAccess` to find entries
608
+ * not yet on the server. */
609
+ declare function localSpaceAccessEntries(): SpaceAccessMap;
610
+ /** Build the `CapMap` slice (member entries only) for persisting into `_spaces.caps`. */
611
+ declare function memberCapsFromStore(): CapMap;
612
+ /** Build the `PubAccessMap` slice (link entries already sealed by the caller). */
613
+ declare function linkAccessFromStore(): Record<string, {
614
+ cap: unknown;
615
+ key: string;
616
+ write: boolean;
617
+ }>;
618
+ /** Drop the in-memory cache (on account switch / sign-out). */
619
+ declare function clearSpaceAccessStore(): void;
620
+
621
+ /** The bucket new/unfiled objects land in, and the fallback a deleted category's
622
+ * objects are reassigned to. */
623
+ declare const DEFAULT_CATEGORY = "CHANNELS";
624
+ /** Deterministic category-node id from its name, so two devices that concurrently
625
+ * create the SAME category mint the SAME id → the union-merge dedupes them. */
626
+ declare const categoryId: (name: string) => ID;
627
+ /** A node plus its resolved children — the shape a tree view renders. */
628
+ interface ObjectTreeNode extends ObjectNode {
629
+ depth: number;
630
+ children: ObjectTreeNode[];
631
+ }
632
+ /** Map a legacy {@link Room} `kind` to the unified room {@link RoomSubtype}. */
633
+ declare function roomKindToSubtype(kind: Room['kind']): RoomSubtype;
634
+ /** Inverse of {@link roomKindToSubtype}. A legacy persisted `'stream'` subtype hits
635
+ * the `default` and reads back as a plain `'channel'` (normalization). */
636
+ declare function subtypeToRoomKind(subtype: RoomSubtype | undefined): Room['kind'];
637
+ /** The order value for a new node appended after `siblings`. */
638
+ declare function nextOrder(siblings: ObjectNode[]): number;
639
+ /**
640
+ * Build the render tree from a flat node list, repairing merge artifacts:
641
+ * - **archived** nodes (and their subtrees) are dropped.
642
+ * - **orphans** — a `parentId` that is missing or archived — reparent to root.
643
+ * - **cycles** — a node reachable from itself via `parentId` — reparent to root.
644
+ * - **siblings** sort by {@link compareSiblings} for cross-device determinism.
645
+ */
646
+ declare function buildTree(nodes: ObjectNode[], includeArchived?: boolean): ObjectTreeNode[];
647
+ /** The root→node trail (inclusive) for breadcrumbs. Returns `[]` if unknown. */
648
+ declare function breadcrumbs(nodes: ObjectNode[], id: ID): ObjectNode[];
649
+ /** The root→parent trail (EXCLUSIVE of the node itself). */
650
+ declare function ancestors(nodes: ObjectNode[], id: ID): ObjectNode[];
651
+ /** The ids of a node and its whole subtree (for cascade-archive). */
652
+ declare function subtreeIds(nodes: ObjectNode[], rootId: ID): Set<ID>;
653
+ interface NewObjectInput {
654
+ type: ObjectType;
655
+ subtype?: RoomSubtype;
656
+ parentId?: ID | null;
657
+ title: string;
658
+ emoji?: string;
659
+ automation?: AutomationMeta;
660
+ /** Provide to reuse an id (e.g. a room id derived elsewhere); else minted. */
661
+ id?: ID;
662
+ }
663
+ /** Append a new node under `parentId` at the end of its sibling order. */
664
+ declare function addObject(nodes: ObjectNode[], input: NewObjectInput, now: number): {
665
+ nodes: ObjectNode[];
666
+ node: ObjectNode;
667
+ };
668
+ /** Patch a node's mutable metadata (title/emoji/automation), bumping `updatedAt`. */
669
+ declare function patchObject(nodes: ObjectNode[], id: ID, patch: Partial<Pick<ObjectNode, 'title' | 'emoji' | 'automation'>>, now: number): ObjectNode[];
670
+ /** Reparent a node (move in the tree). Rejects making a node its own descendant. */
671
+ declare function reparentObject(nodes: ObjectNode[], id: ID, parentId: ID | null, now: number): ObjectNode[];
672
+ /** Set explicit sibling order (drag-reorder). */
673
+ declare function reorderObjects(nodes: ObjectNode[], orderById: Record<ID, number>, now: number): ObjectNode[];
674
+ /** Cascade-archive a node and its whole subtree (soft delete). */
675
+ declare function archiveObject(nodes: ObjectNode[], id: ID, now: number): ObjectNode[];
676
+ /** The category→rooms grouping the legacy UI consumes. */
677
+ interface AdaptedCategory {
678
+ name: string;
679
+ rooms: Room[];
680
+ }
681
+ /**
682
+ * Project the room/category nodes of an index into the legacy `{ name, rooms }[]`
683
+ * shape that app UIs still consume. Category nodes become buckets; room nodes become
684
+ * {@link Room}s grouped under their parent category (or `fallbackCategory` at root).
685
+ * Returns null when the index holds no room/category nodes yet.
686
+ *
687
+ * @deprecated Use the object tree directly once apps complete their migration.
688
+ */
689
+ declare function objectsToRoomCategories(nodes: ObjectNode[], spaceId: string, fallbackCategory: string): AdaptedCategory[] | null;
690
+ /**
691
+ * Drop `kind: 'automated'` rooms from a category list (they belong to an Agents
692
+ * view, not the main room list). A category that held only agents is removed too.
693
+ *
694
+ * @deprecated Use the object tree directly once apps complete their migration.
695
+ */
696
+ declare function excludeAutomatedRooms(categories: AdaptedCategory[]): AdaptedCategory[];
697
+ /** A minimal object descriptor the {@link seedIndexNodes} builder turns into nodes. */
698
+ interface SeedRoom {
699
+ id: ID;
700
+ name: string;
701
+ kind: Room['kind'];
702
+ category: string;
703
+ }
704
+ /**
705
+ * Build the initial `ObjectNode[]` for a brand-new space's index: a `category` node
706
+ * per distinct category and a `room` node per seed object parented under it. Pure +
707
+ * deterministic (category ids via {@link categoryId}).
708
+ */
709
+ declare function seedIndexNodes(rooms: SeedRoom[], now: number): ObjectNode[];
710
+
711
+ /** Owner-set, SHARED space identity, persisted in the `_rooms` registry doc
712
+ * (plaintext — NOT E2EE). `image` is a data URI. Both optional for back-compat. */
713
+ interface SpaceMeta {
714
+ name?: string | null;
715
+ image?: string | null;
716
+ visibility?: SpaceVisibility;
717
+ }
718
+ /** A resolved name/image update fanned out so the SpacesProvider adopts a
719
+ * freshly-reconciled value without waiting for its next navigation refresh. */
720
+ interface SpaceMetaUpdate {
721
+ name: string;
722
+ short: string;
723
+ image?: string;
724
+ }
725
+ declare function onSpaceMeta(fn: (spaceId: string, meta: SpaceMetaUpdate) => void): () => void;
726
+ declare function broadcastSpaceMeta(spaceId: string, meta: SpaceMetaUpdate): void;
727
+ interface SpacesDoc {
728
+ spaces: Space[];
729
+ caps: CapMap;
730
+ mutes: MutePrefs;
731
+ reads: ReadPrefs;
732
+ pubAccess: PubAccessMap;
733
+ dms: DmMap;
734
+ quickReactions: string[];
735
+ archivedDms: ArchivedDms;
736
+ hash: string | null;
737
+ }
738
+ declare function readSpaces(client: StarfishClient, userId: string): Promise<SpacesDoc>;
739
+ declare function updateSpacesDoc(client: StarfishClient, userId: string, mutator: (cur: {
740
+ spaces: Space[];
741
+ caps: CapMap;
742
+ pubAccess: PubAccessMap;
743
+ }) => {
744
+ spaces: Space[];
745
+ caps: CapMap;
746
+ pubAccess: PubAccessMap;
747
+ }): Promise<void>;
748
+ declare function updateMutesDoc(client: StarfishClient, userId: string, mutator: (cur: MutePrefs) => MutePrefs | null): Promise<void>;
749
+ declare function updateReadsDoc(client: StarfishClient, userId: string, mutator: (cur: ReadPrefs) => ReadPrefs | null): Promise<void>;
750
+ declare function updateDmsDoc(client: StarfishClient, userId: string, mutator: (cur: DmMap) => DmMap | null): Promise<void>;
751
+ declare function updateQuickReactionsDoc(client: StarfishClient, userId: string, mutator: (cur: string[]) => string[] | null): Promise<void>;
752
+ declare function updateArchivedDmsDoc(client: StarfishClient, userId: string, mutator: (cur: ArchivedDms) => ArchivedDms | null): Promise<void>;
753
+ declare function setDmMapping(client: StarfishClient, userId: string, peerUserId: string, spaceId: string): Promise<void>;
754
+ declare function writeSpaces(client: StarfishClient, userId: string, spaces: Space[], _hash: string | null): Promise<void>;
755
+ declare function reorderSpaces(client: StarfishClient, userId: string, order: string[]): Promise<void>;
756
+ declare function normalizeCategories(rooms: Room[], stored: unknown): string[];
757
+ declare function readRooms(client: StarfishClient, spaceId: string): Promise<{
758
+ owner: string | null;
759
+ members: string[];
760
+ visibility: SpaceVisibility | null;
761
+ name: string | null;
762
+ image: string | null;
763
+ hash: string | null;
764
+ }>;
765
+ declare function writeRooms(client: StarfishClient, spaceId: string, owner: string, members: string[], hash: string | null, meta?: SpaceMeta): Promise<void>;
766
+ declare function addSpaceMember(client: StarfishClient, spaceId: string, ownerUserId: string, memberUserId: string): Promise<void>;
767
+ /** Remove a member from the space roster (used for link revocation). */
768
+ declare function removeSpaceMember(client: StarfishClient, spaceId: string, memberUserId: string): Promise<void>;
769
+ declare function addJoinedSpace(client: StarfishClient, userId: string, space: Space): Promise<void>;
770
+ declare function addJoinedSpaceWithCap(client: StarfishClient, userId: string, space: Space, capJson: string): Promise<void>;
771
+ declare function addJoinedSpaceWithLinkAccess(client: StarfishClient, userId: string, space: Space, sealed: SealedBlob): Promise<void>;
772
+ /**
773
+ * Create a new space owned by the identity. Seeds ONE generic `general` object node
774
+ * into the object index (encrypted for private, plaintext for public).
775
+ *
776
+ * `opts.visibility` defaults to `'private'`.
777
+ */
778
+ declare function createSpace(session: Session, name: string, opts?: {
779
+ visibility?: SpaceVisibility;
780
+ }): Promise<Space>;
781
+ declare class CategoryError extends Error {
782
+ }
783
+ declare function reconcileSpaceMeta(client: StarfishClient, userId: string, spaceId: string, shared: SpaceMeta, knownSpaces?: Space[]): Promise<void>;
784
+
785
+ interface JoinRequest {
786
+ edPub: string;
787
+ kemPub: string;
788
+ userId: string;
789
+ }
790
+ declare function makeJoinRequest(session: Session): string;
791
+ /**
792
+ * Owner-side: add a recipient's KEM key to a SPACE keyring (one keyring → every
793
+ * object in the space). Reused by {@link inviteToSpace} and by device pairing.
794
+ */
795
+ declare function addDeviceToSpaceKeyring(session: Session, spaceId: string, recipient: {
796
+ kemPub: string;
797
+ userId: string;
798
+ }): Promise<void>;
799
+ /**
800
+ * Owner: invite an identity into a PRIVATE space. Adds them to the keyring, records
801
+ * them in the roster, and mints a single space-scoped member cap.
802
+ * Returns the invite bundle JSON.
803
+ */
804
+ declare function inviteToSpace(session: Session, spaceId: string, requestJson: string, canWrite?: boolean, spaceName?: string): Promise<string>;
805
+ /**
806
+ * Invitee: accept a PRIVATE space invite — verify keyring access, store the cap,
807
+ * and register the space. Returns the joined space.
808
+ */
809
+ declare function acceptSpaceInvite(session: Session, inviteJson: string): Promise<Space>;
810
+ /** A space invite link token (v:1, no ownerId — derive from cap.iss instead). */
811
+ interface SpaceInviteLinkToken {
812
+ v: 1;
813
+ spaceId: string;
814
+ spaceName: string;
815
+ cap: unknown;
816
+ /** The throwaway ephemeral subject's Ed25519 private key (hex). */
817
+ key: string;
818
+ write: boolean;
819
+ }
820
+ declare function encodeSpaceInviteLink(origin: string, token: SpaceInviteLinkToken): string;
821
+ declare function decodeSpaceInviteLink(fragment: string): SpaceInviteLinkToken;
822
+ /**
823
+ * Owner: create a shareable invite link for a PUBLIC space.
824
+ *
825
+ * Mints an ephemeral Ed/KEM keypair, adds its userId to the roster (so the server
826
+ * grants `space:member` to any bearer), and encodes the private key + cap in the URL.
827
+ * Anyone with the link can join; revoke by calling `removeSpaceMember(ephemeralUserId)`.
828
+ */
829
+ declare function createSpaceInviteLink(session: Session, spaceId: string, spaceName: string, write: boolean, origin: string): Promise<{
830
+ token: SpaceInviteLinkToken;
831
+ link: string;
832
+ }>;
833
+ /**
834
+ * Any user: join a PUBLIC space by redeeming an invite link token.
835
+ * Stores the link credential locally and seals it into the synced `_spaces` doc.
836
+ */
837
+ declare function joinSpaceByLink(session: Session, token: SpaceInviteLinkToken): Promise<Space>;
838
+ /**
839
+ * Single sign-in hydration: merges server-side caps (plaintext member caps from
840
+ * `_spaces.caps`) and sealed link access (from `_spaces.pubAccess`) into the
841
+ * unified space-access store. Call once on sign-in / account switch.
842
+ * Backfills any local-only entries to the server.
843
+ */
844
+ declare function recoverSpaceAccess(session: Session, server: {
845
+ caps: Record<string, string>;
846
+ pubAccess: Record<string, SealedBlob>;
847
+ }): Promise<void>;
848
+
849
+ /**
850
+ * Pull + (private: decrypt) + project a space's object index into the legacy
851
+ * `{ rooms, categories }` shape. `encryptor` is null for a PUBLIC space.
852
+ * Returns null on failure or an empty index.
853
+ */
854
+ declare function readIndexRooms(client: StarfishClient, encryptor: Encryptor | null, indexPath: string, spaceId: string): Promise<{
855
+ rooms: Room[];
856
+ categories: string[];
857
+ } | null>;
858
+ /**
859
+ * Read a space's index rooms + categories, resolving access automatically.
860
+ * Pass `reg` (from `readRooms`) so the accessor picks the right auth mode.
861
+ */
862
+ declare function readSpaceIndexRooms(session: Session, spaceId: string, reg: {
863
+ owner: string | null;
864
+ members: string[];
865
+ visibility?: SpaceVisibility;
866
+ }): Promise<{
867
+ rooms: Room[];
868
+ categories: string[];
869
+ } | null>;
870
+ /** SOFT read — never throws, never mints. Returns an empty array on any failure. */
871
+ declare function readSpaceRooms(session: Session, spaceId: string, hint?: {
872
+ visibility?: SpaceVisibility;
873
+ }): Promise<Room[]>;
874
+ /**
875
+ * Write the create-time seed into a space's index doc with an already-open access handle.
876
+ * Accepts a nullable encryptor — plaintext push when null (public spaces).
877
+ * Idempotent: a no-op if the index doc already exists (either encrypted or plaintext).
878
+ */
879
+ declare function pushIndexSeed(client: StarfishClient, encryptor: Encryptor | null, spaceId: string, rooms: SeedRoom[]): Promise<void>;
880
+ /**
881
+ * Seed a brand-new space's index as the OWNER.
882
+ * For private spaces: opens (minting if needed) the space keyring.
883
+ * For public spaces: pushes plaintext nodes.
884
+ */
885
+ declare function seedSpaceObjectIndex(session: Session, spaceId: string, rooms: SeedRoom[], opts?: {
886
+ visibility?: SpaceVisibility;
887
+ }): Promise<void>;
888
+ /**
889
+ * Headless read-modify-write of a space's unified OBJECT INDEX. Works for both
890
+ * private (encrypt round-trip) and public (plaintext) spaces. Retries up to 3 times
891
+ * on ConflictError.
892
+ */
893
+ declare function updateObjectIndex(session: Session, spaceId: string, mutator: (nodes: ObjectNode[], now: number) => ObjectNode[] | null, reg?: {
894
+ owner: string | null;
895
+ members: string[];
896
+ visibility?: SpaceVisibility;
897
+ } | null): Promise<void>;
898
+
899
+ /** The QR-payload prefix this SDK uses. Kept distinct from `octochat-pair:` so apps
900
+ * can route QR payloads to the correct handler during their migration window. */
901
+ declare const PAIR_PREFIX = "octospaces-pair:";
902
+ /** Existing device: provision + PIN-seal a new device, publish to rendezvous, return the QR payload. */
903
+ declare function startDevicePairing(session: Session, pin: string): Promise<string>;
904
+ interface PairResult {
905
+ userId: string;
906
+ fingerprint: string;
907
+ deviceKeys: DeviceKeys;
908
+ capCert: CapCert;
909
+ }
910
+ /** New device: fetch the sealed blob by nonce, open with PIN, validate the bundle. */
911
+ declare function completeDevicePairing(payload: string, pin: string): Promise<PairResult>;
912
+
913
+ /**
914
+ * The app's offline-first read cache for every {@link StarfishClient}.
915
+ *
916
+ * Backs the SDK's {@link PullCache} (read-through pull cache) with the kv layer
917
+ * (localStorage on web, AsyncStorage on native). When a client is built with this
918
+ * cache, every successful structured `pull()` is written through, and a pull that
919
+ * fails because the transport is unreachable falls back to the last-synced snapshot.
920
+ *
921
+ * SECURITY: the SDK caches the RAW server response only. For E2E collections that
922
+ * payload is the SEALED ciphertext the server holds — never the decrypted form —
923
+ * so this cache is ciphertext-at-rest by construction.
924
+ */
925
+
926
+ /**
927
+ * Max age for a cached snapshot before it's treated as a miss. Generous (30 days)
928
+ * because for an offline-first app any last-synced data beats none.
929
+ */
930
+ declare const PULL_CACHE_MAX_AGE_MS: number;
931
+ /** The shared app-wide pull cache (one instance, reused across every client). */
932
+ declare function pullCache(): PullCache;
933
+
934
+ /** Persist a freshly-read profile (fire-and-forget). */
935
+ declare function cacheProfile(userId: string, profile: PublicProfile): void;
936
+ /** Last-known profile for a user, or null if never cached / unparseable. */
937
+ declare function loadCachedProfile(userId: string): Promise<PublicProfile | null>;
938
+
939
+ /**
940
+ * A `fetch` wrapper that bounds the CONNECT/TTFB phase only.
941
+ *
942
+ * Aborts a request that hasn't RESPONDED within {@link CONNECT_TIMEOUT_MS}, turning
943
+ * an opaque infinite spinner into a normal rejection the open path can surface as a
944
+ * retriable error. Clears the timer once response headers arrive, so it bounds ONLY
945
+ * the connect phase — body downloads and long-lived streams stay unbounded.
946
+ */
947
+ declare const CONNECT_TIMEOUT_MS = 12000;
948
+ declare function fetchWithTimeout(timeoutMs?: number): typeof fetch;
949
+
950
+ /**
951
+ * A chunked base64 provider for the Starfish platform.
952
+ *
953
+ * The SDK's default web encoder is `btoa(String.fromCharCode(...data))`, which
954
+ * spreads the entire byte array into one call — a multi-megabyte attachment
955
+ * overflows the argument/stack limit and throws "Maximum call stack size exceeded".
956
+ * This provider walks the bytes in fixed windows instead, so it scales to large blobs.
957
+ *
958
+ * Prefers the platform's own `btoa`/`atob` (web) and falls back to a pure
959
+ * implementation where they're absent (Hermes/native).
960
+ */
961
+
962
+ /** Spread-free, chunked base64 — a drop-in for the SDK's default provider. */
963
+ declare const starfishBase64: Base64Provider;
964
+
965
+ /**
966
+ * base64url for link fragments (UTF-8 safe, web + native) — the encoding both
967
+ * invitation-link kinds ride in a URL `#fragment`. No padding, `+/` → `-_`.
968
+ */
969
+ declare function toBase64Url(json: string): string;
970
+ declare function fromBase64Url(b64url: string): string;
971
+
972
+ export { type AdaptedCategory, type ArchivedDms, type AutomationMeta, BUILTIN_OBJECT_TYPES, CONNECT_TIMEOUT_MS, type CapMap, CategoryError, DEFAULT_CATEGORY, type DerivedIdentity, type DeviceKeys, type DmMap, type ID, type JoinRequest, type KvAdapter, type LinkedIdentity, type MutePrefs, type NewObjectInput, OBJECT_COLLECTIONS, type ObjectContentKind, type ObjectNode, type ObjectTreeNode, type ObjectType, type ObjectsIndex, type OctoSpacesConfig, PAIR_PREFIX, PULL_CACHE_MAX_AGE_MS, type PairResult, type PasskeyEnrollment, type PersistedSession, type PubAccessMap, type PublicProfile, type ReadPrefs, type Room, type RoomKind, type RoomSubtype, type SealedBlob, type SeedLock, type SeedRoom, type Session, type Space, type SpaceAccessEntry, SpaceAccessError, type SpaceAccessHandle, type SpaceAccessMap, type SpaceInviteLinkToken, type SpaceMeta, type SpaceMetaUpdate, type SpaceVisibility, type UnlockMethod, type Vault, type VaultLoad, acceptSpaceInvite, accountScope, addDeviceToSpaceKeyring, addJoinedSpace, addJoinedSpaceWithCap, addJoinedSpaceWithLinkAccess, addObject, addSpaceMember, ancestors, archiveObject, attachmentPull, attachmentPush, breadcrumbs, broadcastSpaceMeta, buildAuthHeaders, buildEncryptor, buildLinkedSession, buildSession, buildSpaceAccess, buildTree, bytesToHex, cacheProfile, capProviderFor, categoryId, clearSpaceAccessCache, clearSpaceAccessStore, completeDevicePairing, configureKv, configureOctoSpaces, createSpace, createSpaceInviteLink, decodeSpaceInviteLink, deriveSession, encodeSpaceInviteLink, ensureProfileKeys, ensurePseudo, excludeAutomatedRooms, fetchWithTimeout, fingerprintFromUserId, fromBase64Url, generateSeedWords, getSharedSpacesNamespace, getSpaceAccess, getSpaceAccessEntry, getSyncBase, getSyncNamespace, getSyncPrefix, hydrateSpaceAccessStore, inviteToSpace, isValidSeed, joinSpaceByLink, keyringName, keyringPull, keyringPush, kvGet, kvRemove, kvSet, linkAccessFromStore, linkedDeviceScope, loadCachedProfile, localSpaceAccessEntries, makeClient, makeJoinRequest, memberCapsFromStore, nextOrder, normalizeCategories, objDocPull, objDocPush, objIndexPull, objIndexPush, objLogPull, objLogPush, objectBlobPull, objectBlobPush, objectsToRoomCategories, onSpaceMeta, openEncryptor, ownerEnsureKeyring, ownerScope, ownerTrustedAdders, patchObject, profilePull, profilePush, pullCache, pushIndexSeed, randomId, readIndexRooms, readProfile, readProfiles, readPseudo, readRooms, readSpaceIndexRooms, readSpaceRooms, readSpaces, reconcileSpaceMeta, recoverSpaceAccess, removeSpaceAccessEntry, removeSpaceMember, reorderObjects, reorderSpaces, reparentObject, roomKindToSubtype, roomSlug, roomsRegistryPull, roomsRegistryPush, rootIdentityOf, saveSpaceAccessEntry, sealToRecipient, sealToSelf, seedIndexNodes, seedSpaceObjectIndex, setDmMapping, spaceIndexPull, spaceMemberScope, spacesPull, spacesPush, starfishBase64, startDevicePairing, subtreeIds, subtypeToRoomKind, toBase64Url, typesIndexPull, typesIndexPush, unsealFromRecipient, unsealFromSelf, updateArchivedDmsDoc, updateDmsDoc, updateMutesDoc, updateObjectIndex, updateQuickReactionsDoc, updateReadsDoc, updateSpacesDoc, userIdFromEdPub, writeProfile, writePseudo, writeRooms, writeSpaces };