@drakkar.software/octospaces-sdk 0.1.0 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +200 -0
- package/dist/index.d.ts +481 -274
- package/dist/index.js +1000 -493
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/types.ts +50 -83
- package/src/index.ts +62 -28
- package/src/objects/objects.test.ts +55 -95
- package/src/objects/objects.ts +23 -136
- package/src/spaces/members.test.ts +10 -3
- package/src/spaces/members.ts +86 -49
- package/src/spaces/nodes.test.ts +225 -0
- package/src/spaces/nodes.ts +427 -0
- package/src/spaces/object-index.test.ts +127 -71
- package/src/spaces/object-index.ts +61 -107
- package/src/spaces/registry.test.ts +59 -46
- package/src/spaces/registry.ts +28 -47
- package/src/sync/client.ts +20 -15
- package/src/sync/pairing.ts +10 -12
- package/src/sync/paths.test.ts +124 -16
- package/src/sync/paths.ts +73 -32
- package/src/sync/space-access-store.ts +17 -0
- package/src/sync/space-access.ts +112 -67
- package/src/utils/invite-preview.test.ts +169 -0
- package/src/utils/invite-preview.ts +101 -0
- package/src/utils/live-sync-bus.test.ts +116 -0
- package/src/utils/live-sync-bus.ts +71 -0
- package/src/utils/search-match.test.ts +149 -0
- package/src/utils/search-match.ts +145 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WrappedKeyEntry } from '@drakkar.software/starfish-keyring';
|
|
2
|
+
import * as _drakkar_software_starfish_client from '@drakkar.software/starfish-client';
|
|
2
3
|
import { StarfishClient, Encryptor, StarfishCapProvider, PullCache } from '@drakkar.software/starfish-client';
|
|
3
4
|
import { CapCert, Base64Provider } from '@drakkar.software/starfish-protocol';
|
|
4
5
|
import { BootstrapOrigin, ScopePreset } from '@drakkar.software/starfish-identities';
|
|
@@ -89,19 +90,23 @@ declare function capProviderFor(cap: unknown, devEdPrivHex: string): StarfishCap
|
|
|
89
90
|
*/
|
|
90
91
|
declare function makeClient(cap: unknown, devEdPrivHex: string, namespaceOverride?: string): StarfishClient;
|
|
91
92
|
/**
|
|
92
|
-
* Open a
|
|
93
|
+
* Open a node's decryptor, throwing a descriptive error per failure mode
|
|
93
94
|
* (unreachable server / no keyring yet / not a recipient).
|
|
94
95
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
96
|
+
* `keyringPullPath` is the full `/pull/.../_keyring` path (e.g. from
|
|
97
|
+
* `keyringPull(spaceId)`). A `SpaceAccessError` is a hard access
|
|
98
|
+
* denial; any other thrown error is a transient offline state.
|
|
97
99
|
*/
|
|
98
|
-
declare function openEncryptor(client: StarfishClient, keys: DeviceKeys,
|
|
100
|
+
declare function openEncryptor(client: StarfishClient, keys: DeviceKeys, keyringPullPath: string, trustedAdders: string[]): Promise<Encryptor>;
|
|
99
101
|
/** Soft variant of {@link openEncryptor}: returns null instead of throwing. */
|
|
100
|
-
declare function buildEncryptor(client: StarfishClient, keys: DeviceKeys,
|
|
102
|
+
declare function buildEncryptor(client: StarfishClient, keys: DeviceKeys, keyringPullPath: string, trustedAdders: string[]): Promise<Encryptor | null>;
|
|
101
103
|
/**
|
|
102
|
-
* Owner-side: create
|
|
104
|
+
* Owner-side: create a per-node keyring if missing, return an encryptor.
|
|
105
|
+
*
|
|
106
|
+
* `keyringPullPath` / `keyringPushPath` are the full `/pull|push/.../_keyring`
|
|
107
|
+
* paths (e.g. from `keyringPull`/`keyringPush`).
|
|
103
108
|
*/
|
|
104
|
-
declare function ownerEnsureKeyring(client: StarfishClient, keys: DeviceKeys,
|
|
109
|
+
declare function ownerEnsureKeyring(client: StarfishClient, keys: DeviceKeys, keyringPullPath: string, keyringPushPath: string, trustedAdders?: string[]): Promise<Encryptor>;
|
|
105
110
|
/** A user's public profile: display pseudo + optional inline avatar + public identity keys. */
|
|
106
111
|
interface PublicProfile {
|
|
107
112
|
pseudo: string | null;
|
|
@@ -297,16 +302,19 @@ declare function sealToRecipient(session: Session, recipientKemPub: string, plai
|
|
|
297
302
|
declare function unsealFromRecipient(session: Session, blob: SealedBlob): Promise<string>;
|
|
298
303
|
|
|
299
304
|
type ID = string;
|
|
300
|
-
/**
|
|
301
|
-
|
|
302
|
-
|
|
305
|
+
/** A user's presence indicator. The theme maps each to a color (app-side). */
|
|
306
|
+
type PresenceStatus = 'online' | 'away' | 'dnd' | 'offline';
|
|
307
|
+
/** A security item's verification state. The theme maps each to a color (app-side).
|
|
308
|
+
* `none` = unknown / not yet verified; maps to a neutral/muted color in the theme. */
|
|
309
|
+
type VerificationLevel = 'verified' | 'pending' | 'unverified' | 'none';
|
|
310
|
+
/** Maps a joined space's id → its owner-issued member cap-cert (serialized JSON).
|
|
311
|
+
* Persisted both in device-local kv and, for durability, in the user's own synced
|
|
312
|
+
* `_spaces` doc so a fresh device re-hydrates it. */
|
|
303
313
|
type CapMap = Record<string, string>;
|
|
304
|
-
/** Maps a joined
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* doc. Recovered on any device with the same seed. See `account-seal.ts` and
|
|
309
|
-
* `space-access-store.ts`. */
|
|
314
|
+
/** Maps a joined link-access key → its sealed invitation credential (cap + ephemeral
|
|
315
|
+
* private key) SEALED to the account's own key. Keys are either `spaceId` (space-level
|
|
316
|
+
* link) or `${spaceId}:${nodeId}` (per-node invite link). Sealed because it embeds a
|
|
317
|
+
* bearer secret; recovered on any device with the same seed. */
|
|
310
318
|
type PubAccessMap = Record<string, SealedBlob>;
|
|
311
319
|
/** Maps a DM peer's userId → the private DM-space id shared with them. */
|
|
312
320
|
type DmMap = Record<string, string>;
|
|
@@ -325,8 +333,8 @@ type ReadValue = number;
|
|
|
325
333
|
interface ReadPrefs {
|
|
326
334
|
rooms: Record<string, ReadValue>;
|
|
327
335
|
}
|
|
328
|
-
/**
|
|
329
|
-
|
|
336
|
+
/** A joined or listed space. Visibility and encryption are per-node (see ObjectNode),
|
|
337
|
+
* not per-space — a space is a neutral container. */
|
|
330
338
|
interface Space {
|
|
331
339
|
id: ID;
|
|
332
340
|
name: string;
|
|
@@ -336,93 +344,51 @@ interface Space {
|
|
|
336
344
|
image?: string;
|
|
337
345
|
members: number;
|
|
338
346
|
unread?: number;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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. */
|
|
347
|
+
}
|
|
348
|
+
/** Any string an app assigns as an object's type. No builtins are defined here —
|
|
349
|
+
* each app declares its own type strings in its local SDK. */
|
|
350
|
+
type ObjectType = string;
|
|
351
|
+
/** How an object's content syncs. Apps may set this per-type explicitly. */
|
|
401
352
|
type ObjectContentKind = 'merge' | 'append' | 'none';
|
|
402
|
-
/**
|
|
403
|
-
|
|
353
|
+
/** Who may read a node's content (independent from whether it is E2EE):
|
|
354
|
+
* - `'public'` — world-readable; anonymous users may access the content. The node
|
|
355
|
+
* is listed in the global object directory (`_index/objects/{shard}`).
|
|
356
|
+
* - `'space'` — any member of the parent space. The default for new nodes.
|
|
357
|
+
* - `'invite'` — only members explicitly invited to this node (via its own per-node
|
|
358
|
+
* keyring for E2EE nodes, or via a per-node cap for plaintext nodes).
|
|
359
|
+
* Non-invited space members see a placeholder row (no title/emoji). */
|
|
360
|
+
type NodeAccess = 'public' | 'space' | 'invite';
|
|
404
361
|
/**
|
|
405
362
|
* One entry in a space's object index (`spaces/{spaceId}/objects/_index`).
|
|
406
363
|
* Identity + tree position + light metadata ONLY — heavy content (messages, doc
|
|
407
|
-
* blocks,
|
|
364
|
+
* blocks, event logs) lives in per-object content docs keyed by `id`.
|
|
365
|
+
*
|
|
366
|
+
* The index doc is always **plaintext** (member-gated). For `invite` nodes the
|
|
367
|
+
* `title` and `emoji` fields are stored empty in the index — only invited members
|
|
368
|
+
* can recover the real title from the node's content doc or encrypted keyring entry.
|
|
408
369
|
*/
|
|
409
370
|
interface ObjectNode {
|
|
410
371
|
id: ID;
|
|
411
372
|
type: ObjectType;
|
|
412
|
-
subtype?: RoomSubtype;
|
|
413
373
|
parentId: ID | null;
|
|
414
374
|
order: number;
|
|
415
375
|
title: string;
|
|
416
376
|
emoji?: string;
|
|
417
377
|
updatedAt: number;
|
|
418
378
|
archived?: boolean;
|
|
419
|
-
automation?: AutomationMeta;
|
|
420
379
|
contentKind?: ObjectContentKind;
|
|
380
|
+
/** Who may access this node's content. Absent ⇒ `'space'`. */
|
|
381
|
+
access?: NodeAccess;
|
|
382
|
+
/** True ⇒ this node's content is E2EE under the SPACE-WIDE keyring at
|
|
383
|
+
* `spaces/{spaceId}/_keyring`. All `enc` nodes in a space share one CEK.
|
|
384
|
+
* The combination `public + enc` is invalid. */
|
|
385
|
+
enc?: boolean;
|
|
386
|
+
/** App-specific fields. Apps store type-specific metadata here. */
|
|
421
387
|
meta?: Record<string, unknown>;
|
|
422
388
|
}
|
|
423
|
-
/** The object-index doc
|
|
389
|
+
/** The object-index doc stored at `spaces/{spaceId}/objects/_index`. */
|
|
424
390
|
interface ObjectsIndex {
|
|
425
|
-
v: 1;
|
|
391
|
+
v: 1 | 2;
|
|
426
392
|
objects: ObjectNode[];
|
|
427
393
|
updatedAt: number;
|
|
428
394
|
}
|
|
@@ -443,7 +409,7 @@ declare function randomId(): string;
|
|
|
443
409
|
declare function roomSlug(name: string): string;
|
|
444
410
|
|
|
445
411
|
/**
|
|
446
|
-
* Collection path + cap-scope helpers
|
|
412
|
+
* Collection path + cap-scope helpers for OctoSpaces.
|
|
447
413
|
*
|
|
448
414
|
* Paths are signed relative to SYNC_BASE; the server mounts the sync router at
|
|
449
415
|
* root, so they start with /pull or /push. Everything for a space is nested under
|
|
@@ -452,42 +418,72 @@ declare function roomSlug(name: string): string;
|
|
|
452
418
|
* covers a whole space.
|
|
453
419
|
*
|
|
454
420
|
* **Generic object collections** — scopes use the `obj*` collection names (the
|
|
455
|
-
* domain-neutral storage layer
|
|
456
|
-
*
|
|
457
|
-
*
|
|
421
|
+
* domain-neutral storage layer). The access record lives at
|
|
422
|
+
* `spaces/{spaceId}/_access` (collection `spaceregistry`); the space-wide keyring at
|
|
423
|
+
* `spaces/{spaceId}/_keyring` (collection `spacekeyring`). ONE keyring per space
|
|
424
|
+
* encrypts ALL the space's `enc` nodes.
|
|
425
|
+
*
|
|
426
|
+
* Note: `objinv` (invite-plaintext content) is intentionally EXCLUDED from
|
|
427
|
+
* OBJECT_COLLECTIONS / spaceMemberScope — only a per-node cap can reach it.
|
|
458
428
|
*/
|
|
459
429
|
|
|
430
|
+
/** A room id is `sp-<rand>-<name>`; the space is its first two `-` segments. */
|
|
431
|
+
declare const spaceIdFromRoomId: (roomId: string) => string;
|
|
432
|
+
/** Base name used as the `collectionName` arg to `addCollectionRecipient`.
|
|
433
|
+
* Appending `/_keyring` gives the full storage path. */
|
|
460
434
|
declare const keyringName: (spaceId: string) => string;
|
|
461
435
|
declare const keyringPull: (spaceId: string) => string;
|
|
462
436
|
declare const keyringPush: (spaceId: string) => string;
|
|
437
|
+
/** Storage path of one attachment blob — also the AAD bound into its seal. */
|
|
438
|
+
declare const attachmentName: (roomId: string, blobId: string) => string;
|
|
463
439
|
declare const attachmentPull: (roomId: string, blobId: string) => string;
|
|
464
440
|
declare const attachmentPush: (roomId: string, blobId: string) => string;
|
|
465
441
|
declare const profilePull: (userId: string) => string;
|
|
466
442
|
declare const profilePush: (userId: string) => string;
|
|
467
443
|
declare const spacesPull: (userId: string) => string;
|
|
468
444
|
declare const spacesPush: (userId: string) => string;
|
|
469
|
-
declare const
|
|
470
|
-
declare const
|
|
445
|
+
declare const spaceAccessPull: (spaceId: string) => string;
|
|
446
|
+
declare const spaceAccessPush: (spaceId: string) => string;
|
|
447
|
+
declare const objIndexName: (spaceId: string) => string;
|
|
471
448
|
declare const objIndexPull: (spaceId: string) => string;
|
|
472
449
|
declare const objIndexPush: (spaceId: string) => string;
|
|
450
|
+
declare const objLogName: (spaceId: string, objectId: string) => string;
|
|
473
451
|
declare const objLogPull: (spaceId: string, objectId: string) => string;
|
|
474
452
|
declare const objLogPush: (spaceId: string, objectId: string) => string;
|
|
453
|
+
declare const objDocName: (spaceId: string, objectId: string) => string;
|
|
475
454
|
declare const objDocPull: (spaceId: string, objectId: string) => string;
|
|
476
455
|
declare const objDocPush: (spaceId: string, objectId: string) => string;
|
|
456
|
+
/** Storage path of one sealed object blob — also the AAD bound into its seal. */
|
|
457
|
+
declare const objectBlobName: (spaceId: string, blobId: string) => string;
|
|
477
458
|
declare const objectBlobPull: (spaceId: string, blobId: string) => string;
|
|
478
459
|
declare const objectBlobPush: (spaceId: string, blobId: string) => string;
|
|
460
|
+
declare const objPubName: (spaceId: string, nodeId: string) => string;
|
|
461
|
+
declare const objPubPull: (spaceId: string, nodeId: string) => string;
|
|
462
|
+
declare const objPubPush: (spaceId: string, nodeId: string) => string;
|
|
463
|
+
declare const objInvName: (spaceId: string, nodeId: string) => string;
|
|
464
|
+
declare const objInvPull: (spaceId: string, nodeId: string) => string;
|
|
465
|
+
declare const objInvPush: (spaceId: string, nodeId: string) => string;
|
|
466
|
+
declare const typesIndexName: (spaceId: string) => string;
|
|
479
467
|
declare const typesIndexPull: (spaceId: string) => string;
|
|
480
468
|
declare const typesIndexPush: (spaceId: string) => string;
|
|
481
|
-
declare const
|
|
469
|
+
declare const objectDirName: (shard?: string) => string;
|
|
470
|
+
declare const objectDirPull: (shard?: string) => string;
|
|
482
471
|
declare const OBJECT_COLLECTIONS: string[];
|
|
483
472
|
/** Full owner/device access to every space the identity owns. */
|
|
484
473
|
declare function ownerScope(): ScopePreset;
|
|
485
474
|
/**
|
|
486
|
-
* Member access to one SPACE —
|
|
487
|
-
* attachments
|
|
488
|
-
* covers current AND
|
|
475
|
+
* Member access to one SPACE — the space keyring, every node's content docs and
|
|
476
|
+
* attachments, all under `spaces/{spaceId}/**`. Does NOT cover `objinv` (invite-
|
|
477
|
+
* plaintext content) — use `nodeMemberScope` for that. One cap covers current AND
|
|
478
|
+
* future nodes.
|
|
489
479
|
*/
|
|
490
480
|
declare function spaceMemberScope(spaceId: string, canWrite: boolean): ScopePreset;
|
|
481
|
+
/**
|
|
482
|
+
* Narrow per-node cap for `invite+plaintext` nodes. Covers ONLY the node's `objinv`
|
|
483
|
+
* content path — the space keyring is space-wide and is covered by the broader space
|
|
484
|
+
* member scope. Use `spaceMemberScope` when the bearer also needs to decrypt enc content.
|
|
485
|
+
*/
|
|
486
|
+
declare function nodeMemberScope(spaceId: string, nodeId: string, canWrite: boolean): ScopePreset;
|
|
491
487
|
/**
|
|
492
488
|
* Personal cap: profile + space registry + device directory + all spaces.
|
|
493
489
|
* Note: app-specific collections like `'dminbox'` (chat) are NOT included here —
|
|
@@ -516,46 +512,62 @@ declare class SpaceAccessError extends Error {
|
|
|
516
512
|
}
|
|
517
513
|
|
|
518
514
|
/**
|
|
519
|
-
* Space access resolver
|
|
520
|
-
* space regardless of whether it is private (E2EE) or public (plaintext).
|
|
515
|
+
* Space and node access resolver.
|
|
521
516
|
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
517
|
+
* Encryption is per-node (each node has an `enc` flag) but keyed under ONE
|
|
518
|
+
* space-wide keyring at `spaces/{spaceId}/_keyring`. All `enc` nodes in a space
|
|
519
|
+
* share the same CEK; `access` gates *fetching*, the keyring gates *decryption*.
|
|
524
520
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
521
|
+
* Two entry points:
|
|
522
|
+
* - `getSpaceClient` — returns the right StarfishClient for member-gated
|
|
523
|
+
* space docs (index, _access). No encryptor.
|
|
524
|
+
* - `getNodeAccess` — resolves the (client, encryptor) for a specific node's
|
|
525
|
+
* CONTENT. Encryptor is null for plaintext nodes; for enc nodes the encryptor
|
|
526
|
+
* opens the SPACE keyring (not a per-node keyring).
|
|
527
|
+
*
|
|
528
|
+
* Resolution order for `getNodeAccess`:
|
|
529
|
+
* 1. Per-node link entry → sign as ephemeral identity; encryptor from space keyring.
|
|
530
|
+
* 2. Per-node member entry → open space keyring as recipient.
|
|
531
|
+
* 3. Space-level link entry → same client; open space keyring if enc.
|
|
532
|
+
* 4. Space-level member entry → open space keyring if enc.
|
|
533
|
+
* 5. No entry, owner → mint space keyring if enc; plain client otherwise.
|
|
534
|
+
* 6. No entry, non-owner → SpaceAccessError if enc; plain client otherwise.
|
|
532
535
|
*/
|
|
533
536
|
|
|
534
|
-
interface
|
|
537
|
+
interface NodeAccessHandle {
|
|
535
538
|
encryptor: Encryptor | null;
|
|
536
539
|
client: StarfishClient;
|
|
537
|
-
/** True when opened as the space OWNER (
|
|
540
|
+
/** True when opened as the space OWNER (may seed / mint the space keyring). */
|
|
538
541
|
isOwnerOpen: boolean;
|
|
539
542
|
}
|
|
540
543
|
/** Drop every cached handle (on account switch — keys are per-identity). */
|
|
541
|
-
declare function
|
|
544
|
+
declare function clearNodeAccessCache(): void;
|
|
545
|
+
/**
|
|
546
|
+
* Return the right StarfishClient for reading/writing member-gated space docs
|
|
547
|
+
* (e.g. the `_index`, `_access`). Spaces are always plaintext — no encryptor.
|
|
548
|
+
*/
|
|
549
|
+
declare function getSpaceClient(spaceId: string, session: Session): StarfishClient;
|
|
542
550
|
/**
|
|
543
|
-
* Resolve the right (client, encryptor) for a
|
|
551
|
+
* Resolve the right (client, encryptor) for a node's CONTENT, opening and
|
|
552
|
+
* caching on first use.
|
|
544
553
|
*
|
|
545
|
-
* `
|
|
546
|
-
*
|
|
554
|
+
* `node` carries `{ access?, enc? }` — the plaintext flags from the index.
|
|
555
|
+
* `reg` is the space's access record if already known; used to determine
|
|
556
|
+
* ownership. Pass null if unknown.
|
|
547
557
|
*/
|
|
548
|
-
declare function
|
|
558
|
+
declare function getNodeAccess(spaceId: string, nodeId: string, node: {
|
|
559
|
+
access?: NodeAccess;
|
|
560
|
+
enc?: boolean;
|
|
561
|
+
}, session: Session, reg?: {
|
|
549
562
|
owner: string | null;
|
|
550
563
|
members: string[];
|
|
551
|
-
|
|
552
|
-
} | null): Promise<SpaceAccessHandle>;
|
|
564
|
+
} | null): Promise<NodeAccessHandle>;
|
|
553
565
|
/**
|
|
554
566
|
* SOFT resolve — never mints a keyring, never throws on missing access.
|
|
555
|
-
* Returns null when the identity has no usable access for the
|
|
567
|
+
* Returns null when the identity has no usable access for the node yet.
|
|
556
568
|
*/
|
|
557
|
-
declare function
|
|
558
|
-
|
|
569
|
+
declare function buildNodeAccess(session: Session, spaceId: string, nodeId: string, node: {
|
|
570
|
+
enc?: boolean;
|
|
559
571
|
}): Promise<{
|
|
560
572
|
client: StarfishClient;
|
|
561
573
|
encryptor: Encryptor | null;
|
|
@@ -604,6 +616,12 @@ declare function getSpaceAccessEntry(spaceId: string): SpaceAccessEntry | null;
|
|
|
604
616
|
declare function saveSpaceAccessEntry(spaceId: string, entry: SpaceAccessEntry): void;
|
|
605
617
|
/** Forget one space's access (on leaving that space). */
|
|
606
618
|
declare function removeSpaceAccessEntry(spaceId: string): void;
|
|
619
|
+
/** Look up a per-node invite access entry. Returns null if not invited or unknown. */
|
|
620
|
+
declare function getNodeAccessEntry(spaceId: string, nodeId: string): SpaceAccessEntry | null;
|
|
621
|
+
/** Persist an invite access entry for one node. */
|
|
622
|
+
declare function saveNodeAccessEntry(spaceId: string, nodeId: string, entry: SpaceAccessEntry): void;
|
|
623
|
+
/** Forget a node's invite access entry (e.g. on leaving the node). */
|
|
624
|
+
declare function removeNodeAccessEntry(spaceId: string, nodeId: string): void;
|
|
607
625
|
/** A snapshot of the in-memory cache — used by `recoverSpaceAccess` to find entries
|
|
608
626
|
* not yet on the server. */
|
|
609
627
|
declare function localSpaceAccessEntries(): SpaceAccessMap;
|
|
@@ -618,102 +636,11 @@ declare function linkAccessFromStore(): Record<string, {
|
|
|
618
636
|
/** Drop the in-memory cache (on account switch / sign-out). */
|
|
619
637
|
declare function clearSpaceAccessStore(): void;
|
|
620
638
|
|
|
621
|
-
/**
|
|
622
|
-
*
|
|
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. */
|
|
639
|
+
/** Owner-set, SHARED space identity, persisted in the `_access` registry doc
|
|
640
|
+
* (plaintext — NOT E2EE). `image` is a data URI. All fields optional for back-compat. */
|
|
713
641
|
interface SpaceMeta {
|
|
714
642
|
name?: string | null;
|
|
715
643
|
image?: string | null;
|
|
716
|
-
visibility?: SpaceVisibility;
|
|
717
644
|
}
|
|
718
645
|
/** A resolved name/image update fanned out so the SpacesProvider adopts a
|
|
719
646
|
* freshly-reconciled value without waiting for its next navigation refresh. */
|
|
@@ -753,16 +680,14 @@ declare function updateArchivedDmsDoc(client: StarfishClient, userId: string, mu
|
|
|
753
680
|
declare function setDmMapping(client: StarfishClient, userId: string, peerUserId: string, spaceId: string): Promise<void>;
|
|
754
681
|
declare function writeSpaces(client: StarfishClient, userId: string, spaces: Space[], _hash: string | null): Promise<void>;
|
|
755
682
|
declare function reorderSpaces(client: StarfishClient, userId: string, order: string[]): Promise<void>;
|
|
756
|
-
declare function
|
|
757
|
-
declare function readRooms(client: StarfishClient, spaceId: string): Promise<{
|
|
683
|
+
declare function readSpaceAccess(client: StarfishClient, spaceId: string): Promise<{
|
|
758
684
|
owner: string | null;
|
|
759
685
|
members: string[];
|
|
760
|
-
visibility: SpaceVisibility | null;
|
|
761
686
|
name: string | null;
|
|
762
687
|
image: string | null;
|
|
763
688
|
hash: string | null;
|
|
764
689
|
}>;
|
|
765
|
-
declare function
|
|
690
|
+
declare function writeSpaceAccess(client: StarfishClient, spaceId: string, owner: string, members: string[], hash: string | null, meta?: SpaceMeta): Promise<void>;
|
|
766
691
|
declare function addSpaceMember(client: StarfishClient, spaceId: string, ownerUserId: string, memberUserId: string): Promise<void>;
|
|
767
692
|
/** Remove a member from the space roster (used for link revocation). */
|
|
768
693
|
declare function removeSpaceMember(client: StarfishClient, spaceId: string, memberUserId: string): Promise<void>;
|
|
@@ -770,16 +695,10 @@ declare function addJoinedSpace(client: StarfishClient, userId: string, space: S
|
|
|
770
695
|
declare function addJoinedSpaceWithCap(client: StarfishClient, userId: string, space: Space, capJson: string): Promise<void>;
|
|
771
696
|
declare function addJoinedSpaceWithLinkAccess(client: StarfishClient, userId: string, space: Space, sealed: SealedBlob): Promise<void>;
|
|
772
697
|
/**
|
|
773
|
-
* Create a new space owned by the identity. Seeds
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
* `opts.visibility` defaults to `'private'`.
|
|
698
|
+
* Create a new space owned by the identity. Seeds an empty plaintext object index.
|
|
699
|
+
* Apps populate the index with their own object types after creation using `createNode`.
|
|
777
700
|
*/
|
|
778
|
-
declare function createSpace(session: Session, name: string
|
|
779
|
-
visibility?: SpaceVisibility;
|
|
780
|
-
}): Promise<Space>;
|
|
781
|
-
declare class CategoryError extends Error {
|
|
782
|
-
}
|
|
701
|
+
declare function createSpace(session: Session, name: string): Promise<Space>;
|
|
783
702
|
declare function reconcileSpaceMeta(client: StarfishClient, userId: string, spaceId: string, shared: SpaceMeta, knownSpaces?: Space[]): Promise<void>;
|
|
784
703
|
|
|
785
704
|
interface JoinRequest {
|
|
@@ -789,22 +708,15 @@ interface JoinRequest {
|
|
|
789
708
|
}
|
|
790
709
|
declare function makeJoinRequest(session: Session): string;
|
|
791
710
|
/**
|
|
792
|
-
* Owner
|
|
793
|
-
*
|
|
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.
|
|
711
|
+
* Owner: invite an identity into a space. Records them in the roster, mints a
|
|
712
|
+
* space-scoped member cap, and adds them to the space-wide keyring if it exists
|
|
713
|
+
* (so they can decrypt `enc` nodes from the start).
|
|
802
714
|
* Returns the invite bundle JSON.
|
|
803
715
|
*/
|
|
804
716
|
declare function inviteToSpace(session: Session, spaceId: string, requestJson: string, canWrite?: boolean, spaceName?: string): Promise<string>;
|
|
805
717
|
/**
|
|
806
|
-
* Invitee: accept a
|
|
807
|
-
*
|
|
718
|
+
* Invitee: accept a space invite — store the cap and register the space.
|
|
719
|
+
* Returns the joined space.
|
|
808
720
|
*/
|
|
809
721
|
declare function acceptSpaceInvite(session: Session, inviteJson: string): Promise<Space>;
|
|
810
722
|
/** A space invite link token (v:1, no ownerId — derive from cap.iss instead). */
|
|
@@ -831,10 +743,23 @@ declare function createSpaceInviteLink(session: Session, spaceId: string, spaceN
|
|
|
831
743
|
link: string;
|
|
832
744
|
}>;
|
|
833
745
|
/**
|
|
834
|
-
* Any user: join a
|
|
746
|
+
* Any user: join a space by redeeming an invite link token.
|
|
835
747
|
* Stores the link credential locally and seals it into the synced `_spaces` doc.
|
|
836
748
|
*/
|
|
837
749
|
declare function joinSpaceByLink(session: Session, token: SpaceInviteLinkToken): Promise<Space>;
|
|
750
|
+
/**
|
|
751
|
+
* Add a device's KEM key as a recipient of a space's keyring.
|
|
752
|
+
*
|
|
753
|
+
* Call this after device pairing (for each space the new device should be able to
|
|
754
|
+
* decrypt). ONE space keyring encrypts ALL the space's `enc` nodes — adding the device
|
|
755
|
+
* once unlocks the whole space's E2EE content. Silently a no-op if the keyring doesn't
|
|
756
|
+
* exist yet.
|
|
757
|
+
*/
|
|
758
|
+
declare function addDeviceToSpaceKeyring(session: Session, spaceId: string, device: {
|
|
759
|
+
kemPub: string;
|
|
760
|
+
edPub: string;
|
|
761
|
+
userId: string;
|
|
762
|
+
}): Promise<void>;
|
|
838
763
|
/**
|
|
839
764
|
* Single sign-in hydration: merges server-side caps (plaintext member caps from
|
|
840
765
|
* `_spaces.caps`) and sealed link access (from `_spaces.pubAccess`) into the
|
|
@@ -846,60 +771,214 @@ declare function recoverSpaceAccess(session: Session, server: {
|
|
|
846
771
|
pubAccess: Record<string, SealedBlob>;
|
|
847
772
|
}): Promise<void>;
|
|
848
773
|
|
|
774
|
+
interface CreateNodeInput {
|
|
775
|
+
type: ObjectType;
|
|
776
|
+
title: string;
|
|
777
|
+
emoji?: string;
|
|
778
|
+
parentId?: string | null;
|
|
779
|
+
/** Who may reach this node. Default: `'space'`. */
|
|
780
|
+
access?: NodeAccess;
|
|
781
|
+
/** Whether node content is E2EE under the space-wide keyring. Default: `false`. */
|
|
782
|
+
enc?: boolean;
|
|
783
|
+
/** App-specific metadata. */
|
|
784
|
+
meta?: Record<string, unknown>;
|
|
785
|
+
}
|
|
849
786
|
/**
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
787
|
+
* Create a new node in a space's object index.
|
|
788
|
+
*
|
|
789
|
+
* - Rejects the invalid combo `public+enc`.
|
|
790
|
+
* - For `enc` nodes, ensures the space-wide keyring exists (minted once per space,
|
|
791
|
+
* idempotent on subsequent creates).
|
|
792
|
+
* - Returns the created node as it was inserted into the index.
|
|
853
793
|
*/
|
|
854
|
-
declare function
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
} | null>;
|
|
794
|
+
declare function createNode(session: Session, spaceId: string, input: CreateNodeInput, reg?: {
|
|
795
|
+
owner: string | null;
|
|
796
|
+
members: string[];
|
|
797
|
+
} | null): Promise<ObjectNode>;
|
|
858
798
|
/**
|
|
859
|
-
*
|
|
860
|
-
*
|
|
799
|
+
* Patch the `access`/`enc` axes of a node in the index.
|
|
800
|
+
*
|
|
801
|
+
* - Rejects `public+enc`.
|
|
802
|
+
* - For enabling `enc`, ensures the space keyring exists (idempotent).
|
|
803
|
+
* - Content migration (moving between `objpub`/`objdoc`/`objinv`) is the caller's
|
|
804
|
+
* responsibility — this only flips the metadata flags.
|
|
861
805
|
*/
|
|
862
|
-
declare function
|
|
806
|
+
declare function setNodeAccess(session: Session, spaceId: string, nodeId: string, patch: {
|
|
807
|
+
access?: NodeAccess;
|
|
808
|
+
enc?: boolean;
|
|
809
|
+
}, reg?: {
|
|
863
810
|
owner: string | null;
|
|
864
811
|
members: string[];
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
812
|
+
} | null): Promise<void>;
|
|
813
|
+
interface NodeInviteBundle {
|
|
814
|
+
spaceId: string;
|
|
815
|
+
nodeId: string;
|
|
816
|
+
nodeName: string;
|
|
817
|
+
/** Space-level member cap (always present — grants index read access). */
|
|
818
|
+
cap: unknown;
|
|
819
|
+
/** Per-node narrow cap (only for `invite+plaintext` nodes). */
|
|
820
|
+
nodeCap?: unknown;
|
|
821
|
+
}
|
|
874
822
|
/**
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
823
|
+
* Owner: invite an identity to a specific node.
|
|
824
|
+
*
|
|
825
|
+
* - For `enc` nodes: adds the invitee to the space-wide keyring (granting decryption
|
|
826
|
+
* access to ALL enc nodes in the space) and mints a space-level member cap.
|
|
827
|
+
* - For `invite+plaintext` nodes: mints both a space-level cap (index) and a
|
|
828
|
+
* narrow per-node cap (`nodeMemberScope`, covers `objinv` content).
|
|
829
|
+
*
|
|
830
|
+
* Returns the invite bundle JSON; pass to the invitee who calls `acceptNodeInvite`.
|
|
878
831
|
*/
|
|
879
|
-
declare function
|
|
832
|
+
declare function inviteToNode(session: Session, spaceId: string, nodeId: string, requestJson: string, node: {
|
|
833
|
+
enc?: boolean;
|
|
834
|
+
}, nodeName?: string): Promise<string>;
|
|
880
835
|
/**
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
* For public spaces: pushes plaintext nodes.
|
|
836
|
+
* Invitee: accept a direct node invite — store the cap(s) and register access.
|
|
837
|
+
* Returns the nodeId.
|
|
884
838
|
*/
|
|
885
|
-
declare function
|
|
886
|
-
|
|
887
|
-
|
|
839
|
+
declare function acceptNodeInvite(session: Session, bundleJson: string): Promise<string>;
|
|
840
|
+
/** A node invite link token (v:1). */
|
|
841
|
+
interface NodeInviteLinkToken {
|
|
842
|
+
v: 1;
|
|
843
|
+
spaceId: string;
|
|
844
|
+
nodeId: string;
|
|
845
|
+
nodeName: string;
|
|
846
|
+
/** Cap scope depends on `enc`: spaceMemberScope for enc nodes, nodeMemberScope for plaintext. */
|
|
847
|
+
cap: unknown;
|
|
848
|
+
/** The ephemeral subject's Ed25519 private key (hex). */
|
|
849
|
+
key: string;
|
|
850
|
+
write: boolean;
|
|
851
|
+
}
|
|
852
|
+
declare function encodeNodeInviteLink(origin: string, token: NodeInviteLinkToken): string;
|
|
853
|
+
declare function decodeNodeInviteLink(fragment: string): NodeInviteLinkToken;
|
|
888
854
|
/**
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
855
|
+
* Owner: create a shareable invite link for a specific node.
|
|
856
|
+
*
|
|
857
|
+
* - For `enc` nodes: adds ephemeral KEM to the space-wide keyring; the link cap uses
|
|
858
|
+
* `spaceMemberScope` so the bearer can read the keyring and decrypt enc content.
|
|
859
|
+
* - For `invite+plaintext` nodes: narrow per-node cap (`nodeMemberScope`), no keyring.
|
|
860
|
+
*
|
|
861
|
+
* Anyone with the link can access the node; revoke by calling
|
|
862
|
+
* `removeSpaceMember(ephemeralUserId)` (and rotating the space keyring for enc nodes).
|
|
863
|
+
*/
|
|
864
|
+
declare function createNodeInviteLink(session: Session, spaceId: string, nodeId: string, nodeName: string, node: {
|
|
865
|
+
enc?: boolean;
|
|
866
|
+
}, write: boolean, origin: string): Promise<{
|
|
867
|
+
token: NodeInviteLinkToken;
|
|
868
|
+
link: string;
|
|
869
|
+
}>;
|
|
870
|
+
/**
|
|
871
|
+
* Any user: access a node by redeeming an invite link token.
|
|
872
|
+
* Stores the per-node link entry locally and seals it into the synced `_spaces` doc.
|
|
873
|
+
*/
|
|
874
|
+
declare function joinNodeByLink(session: Session, token: NodeInviteLinkToken): Promise<string>;
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Generic object-tree model — pure logic over a space's object index.
|
|
878
|
+
*
|
|
879
|
+
* A space's contents are {@link ObjectNode}s in one union-merged index doc at
|
|
880
|
+
* `spaces/{spaceId}/objects/_index`. This module is the pure, testable core:
|
|
881
|
+
* the tree builder + merge-artifact guards, breadcrumbs, ordering, and the node
|
|
882
|
+
* reducers a `store.set` applies.
|
|
883
|
+
*
|
|
884
|
+
* Because the index is union-merged (per-node last-write-wins keyed on `updatedAt`),
|
|
885
|
+
* the tree is eventually consistent — two devices can concurrently produce a cycle
|
|
886
|
+
* or an orphan. The builder below is the single place those are repaired so every
|
|
887
|
+
* consumer renders a well-formed tree.
|
|
888
|
+
*
|
|
889
|
+
* No domain types (room, category, task, …) are defined here. Apps define their own.
|
|
890
|
+
*/
|
|
891
|
+
|
|
892
|
+
/** A node plus its resolved children — the shape a tree view renders. */
|
|
893
|
+
interface ObjectTreeNode extends ObjectNode {
|
|
894
|
+
depth: number;
|
|
895
|
+
children: ObjectTreeNode[];
|
|
896
|
+
}
|
|
897
|
+
/** The order value for a new node appended after `siblings`. */
|
|
898
|
+
declare function nextOrder(siblings: ObjectNode[]): number;
|
|
899
|
+
/**
|
|
900
|
+
* Build the render tree from a flat node list, repairing merge artifacts:
|
|
901
|
+
* - **archived** nodes (and their subtrees) are dropped.
|
|
902
|
+
* - **orphans** — a `parentId` that is missing or archived — reparent to root.
|
|
903
|
+
* - **cycles** — a node reachable from itself via `parentId` — reparent to root.
|
|
904
|
+
* - **siblings** sort by {@link compareSiblings} for cross-device determinism.
|
|
905
|
+
*/
|
|
906
|
+
declare function buildTree(nodes: ObjectNode[], includeArchived?: boolean): ObjectTreeNode[];
|
|
907
|
+
/** The root→node trail (inclusive) for breadcrumbs. Returns `[]` if unknown. */
|
|
908
|
+
declare function breadcrumbs(nodes: ObjectNode[], id: ID): ObjectNode[];
|
|
909
|
+
/** The root→parent trail (EXCLUSIVE of the node itself). */
|
|
910
|
+
declare function ancestors(nodes: ObjectNode[], id: ID): ObjectNode[];
|
|
911
|
+
/** The ids of a node and its whole subtree (for cascade-archive). */
|
|
912
|
+
declare function subtreeIds(nodes: ObjectNode[], rootId: ID): Set<ID>;
|
|
913
|
+
interface NewObjectInput {
|
|
914
|
+
type: ObjectType;
|
|
915
|
+
parentId?: ID | null;
|
|
916
|
+
title: string;
|
|
917
|
+
emoji?: string;
|
|
918
|
+
/** App-specific metadata passed through to node.meta. */
|
|
919
|
+
meta?: Record<string, unknown>;
|
|
920
|
+
/** Provide to reuse an id (e.g. a node id derived elsewhere); else minted. */
|
|
921
|
+
id?: ID;
|
|
922
|
+
/** Who may reach this node. Absent ⇒ `'space'` (all space members). */
|
|
923
|
+
access?: NodeAccess;
|
|
924
|
+
/** Whether the node's content is E2EE under its own per-node keyring. Absent ⇒ false. */
|
|
925
|
+
enc?: boolean;
|
|
926
|
+
}
|
|
927
|
+
/** Append a new node under `parentId` at the end of its sibling order. */
|
|
928
|
+
declare function addObject(nodes: ObjectNode[], input: NewObjectInput, now: number): {
|
|
929
|
+
nodes: ObjectNode[];
|
|
930
|
+
node: ObjectNode;
|
|
931
|
+
};
|
|
932
|
+
/** Patch a node's mutable metadata (title/emoji/meta/access/enc), bumping `updatedAt`. */
|
|
933
|
+
declare function patchObject(nodes: ObjectNode[], id: ID, patch: Partial<Pick<ObjectNode, 'title' | 'emoji' | 'meta' | 'access' | 'enc'>>, now: number): ObjectNode[];
|
|
934
|
+
/** Reparent a node (move in the tree). Rejects making a node its own descendant. */
|
|
935
|
+
declare function reparentObject(nodes: ObjectNode[], id: ID, parentId: ID | null, now: number): ObjectNode[];
|
|
936
|
+
/** Set explicit sibling order (drag-reorder). */
|
|
937
|
+
declare function reorderObjects(nodes: ObjectNode[], orderById: Record<ID, number>, now: number): ObjectNode[];
|
|
938
|
+
/** Cascade-archive a node and its whole subtree (soft delete). */
|
|
939
|
+
declare function archiveObject(nodes: ObjectNode[], id: ID, now: number): ObjectNode[];
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Write the create-time seed into a space's index doc.
|
|
943
|
+
* Idempotent: a no-op if the index doc already exists.
|
|
944
|
+
* Pass `nodes` to seed with initial objects; defaults to an empty index.
|
|
945
|
+
*/
|
|
946
|
+
declare function pushIndexSeed(client: _drakkar_software_starfish_client.StarfishClient, spaceId: string, nodes?: ObjectNode[]): Promise<void>;
|
|
947
|
+
/**
|
|
948
|
+
* Seed a brand-new space's index as the OWNER. Always plaintext.
|
|
949
|
+
* Pass `nodes` to seed with initial objects; defaults to an empty index.
|
|
950
|
+
*/
|
|
951
|
+
declare function seedSpaceObjectIndex(session: Session, spaceId: string, nodes?: ObjectNode[]): Promise<void>;
|
|
952
|
+
/**
|
|
953
|
+
* Headless read-modify-write of a space's unified OBJECT INDEX.
|
|
954
|
+
* Always plaintext. Retries up to 3 times on ConflictError.
|
|
955
|
+
*
|
|
956
|
+
* The mutator receives the current nodes with real (or empty, for invite) titles.
|
|
957
|
+
* Before writing back, invite nodes have their title/emoji stripped again.
|
|
892
958
|
*/
|
|
893
959
|
declare function updateObjectIndex(session: Session, spaceId: string, mutator: (nodes: ObjectNode[], now: number) => ObjectNode[] | null, reg?: {
|
|
894
960
|
owner: string | null;
|
|
895
961
|
members: string[];
|
|
896
|
-
visibility?: SpaceVisibility;
|
|
897
962
|
} | null): Promise<void>;
|
|
963
|
+
/**
|
|
964
|
+
* Read the current object tree (read-only, no mutation). Returns the stored
|
|
965
|
+
* nodes (titles are empty for invite nodes the caller is not invited to).
|
|
966
|
+
*/
|
|
967
|
+
declare function readObjectTree(session: Session, spaceId: string): Promise<ObjectNode[]>;
|
|
898
968
|
|
|
899
969
|
/** The QR-payload prefix this SDK uses. Kept distinct from `octochat-pair:` so apps
|
|
900
970
|
* can route QR payloads to the correct handler during their migration window. */
|
|
901
971
|
declare const PAIR_PREFIX = "octospaces-pair:";
|
|
902
|
-
/**
|
|
972
|
+
/**
|
|
973
|
+
* Existing device: provision + PIN-seal a new device, publish to rendezvous, return
|
|
974
|
+
* the QR payload.
|
|
975
|
+
*
|
|
976
|
+
* After pairing, call `addDeviceToSpaceKeyring(session, spaceId, newDeviceKeys)` for
|
|
977
|
+
* each space whose E2EE content the new device should decrypt. ONE space keyring
|
|
978
|
+
* encrypts ALL `enc` nodes in a space — one call per space unlocks everything.
|
|
979
|
+
* Plaintext (`space` / `public`) nodes are immediately accessible via the linked-device
|
|
980
|
+
* cap-cert (no extra keyring step).
|
|
981
|
+
*/
|
|
903
982
|
declare function startDevicePairing(session: Session, pin: string): Promise<string>;
|
|
904
983
|
interface PairResult {
|
|
905
984
|
userId: string;
|
|
@@ -969,4 +1048,132 @@ declare const starfishBase64: Base64Provider;
|
|
|
969
1048
|
declare function toBase64Url(json: string): string;
|
|
970
1049
|
declare function fromBase64Url(b64url: string): string;
|
|
971
1050
|
|
|
972
|
-
|
|
1051
|
+
/**
|
|
1052
|
+
* Pure title matcher + ranker for Quick Find / Search. No React, no I/O.
|
|
1053
|
+
*
|
|
1054
|
+
* Relevance is tiered the way a human reads a match, strongest first:
|
|
1055
|
+
*
|
|
1056
|
+
* 1. PREFIX — the title starts with the query ("not" → "Notes").
|
|
1057
|
+
* 2. WORD boundary — some word starts with the query ("pa" → "New page").
|
|
1058
|
+
* 3. SUBSTRING — the query appears mid-word ("page" → "Homepage").
|
|
1059
|
+
* 4. FUZZY — the query is a subsequence ("rdm" → "Roadmap").
|
|
1060
|
+
*
|
|
1061
|
+
* Within a tier, earlier and tighter matches in shorter titles score higher;
|
|
1062
|
+
* tier gaps are wider than any intra-tier penalty, so a fuzzy hit can never
|
|
1063
|
+
* outrank a real substring. Ties (same score) fall back to `updatedAt` DESC in
|
|
1064
|
+
* {@link rankResults} — between two objects named "Notes", the one touched last
|
|
1065
|
+
* is almost always the one wanted.
|
|
1066
|
+
*
|
|
1067
|
+
* Matching is case- and diacritic-insensitive via a per-UTF-16-unit fold that
|
|
1068
|
+
* PRESERVES STRING LENGTH, so the returned ranges index straight into the
|
|
1069
|
+
* ORIGINAL title for highlight rendering.
|
|
1070
|
+
*/
|
|
1071
|
+
/** Half-open [start, end) span into the original title. */
|
|
1072
|
+
interface MatchRange {
|
|
1073
|
+
start: number;
|
|
1074
|
+
end: number;
|
|
1075
|
+
}
|
|
1076
|
+
interface TitleMatch {
|
|
1077
|
+
score: number;
|
|
1078
|
+
ranges: MatchRange[];
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Lowercase + strip diacritics WITHOUT changing length: each UTF-16 unit maps
|
|
1082
|
+
* to exactly one folded unit (NFD base char, first lowercase unit). Surrogate
|
|
1083
|
+
* halves pass through unchanged — they can't match an ASCII query, which is
|
|
1084
|
+
* exactly right for emoji-bearing titles.
|
|
1085
|
+
*/
|
|
1086
|
+
declare function fold(s: string): string;
|
|
1087
|
+
/** A word starts where the previous folded char is not alphanumeric. */
|
|
1088
|
+
declare function isWordStart(folded: string, i: number): boolean;
|
|
1089
|
+
/**
|
|
1090
|
+
* Match one title against a query. Returns `null` for an empty query or a miss.
|
|
1091
|
+
* Ranges cover every highlighted span (one for contiguous tiers, several merged
|
|
1092
|
+
* runs for fuzzy).
|
|
1093
|
+
*/
|
|
1094
|
+
declare function matchTitle(query: string, title: string): TitleMatch | null;
|
|
1095
|
+
interface RankedResult<T> {
|
|
1096
|
+
item: T;
|
|
1097
|
+
score: number;
|
|
1098
|
+
ranges: MatchRange[];
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Rank a candidate list against a query: score every title, drop misses, sort
|
|
1102
|
+
* by score DESC then `updatedAt` DESC (recency breaks ties), cap at `limit`.
|
|
1103
|
+
*/
|
|
1104
|
+
declare function rankResults<T extends {
|
|
1105
|
+
title: string;
|
|
1106
|
+
updatedAt: number;
|
|
1107
|
+
}>(query: string, items: readonly T[], limit?: number): RankedResult<T>[];
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Single dispatch point for live-sync events from a global SSE connection.
|
|
1111
|
+
*
|
|
1112
|
+
* When a server-sent event arrives, the unread/notification layer calls
|
|
1113
|
+
* `dispatchDocChange(docPath)`:
|
|
1114
|
+
* - if a hook has registered a pull for that path → call it (the user is
|
|
1115
|
+
* actively viewing that doc) and return `true` — the caller skips the
|
|
1116
|
+
* unread bump.
|
|
1117
|
+
* - otherwise return `false` → the caller bumps unread.
|
|
1118
|
+
*
|
|
1119
|
+
* Hooks register/unregister via `registerPull`. SSE connection health is
|
|
1120
|
+
* broadcast via `emitSseStatus` so hooks can gate their fallback polling.
|
|
1121
|
+
*
|
|
1122
|
+
* Call `clearLiveSyncBus()` on account switch to flush all registrations.
|
|
1123
|
+
*/
|
|
1124
|
+
type PullFn = () => void;
|
|
1125
|
+
type StatusListener = (up: boolean) => void;
|
|
1126
|
+
/**
|
|
1127
|
+
* Register a pull function keyed by `docPath`. Returns an unsubscribe
|
|
1128
|
+
* function — call it when the hook unmounts.
|
|
1129
|
+
*/
|
|
1130
|
+
declare function registerPull(docPath: string, fn: PullFn): () => void;
|
|
1131
|
+
/**
|
|
1132
|
+
* Dispatch a doc-change event. If a pull is registered for `docPath`, calls
|
|
1133
|
+
* it and returns `true`. Returns `false` if no listener is registered
|
|
1134
|
+
* (the caller should bump unread).
|
|
1135
|
+
*/
|
|
1136
|
+
declare function dispatchDocChange(docPath: string): boolean;
|
|
1137
|
+
/** Broadcast the current SSE health to all subscribers. */
|
|
1138
|
+
declare function emitSseStatus(up: boolean): void;
|
|
1139
|
+
/**
|
|
1140
|
+
* Subscribe to SSE health changes. Fires immediately with the current state.
|
|
1141
|
+
* Returns an unsubscribe function.
|
|
1142
|
+
*/
|
|
1143
|
+
declare function onSseStatus(cb: StatusListener): () => void;
|
|
1144
|
+
/**
|
|
1145
|
+
* Flush all registered doc pulls and reset SSE health. Call on account
|
|
1146
|
+
* switch. `statusListeners` are React subscriptions that self-unsubscribe on
|
|
1147
|
+
* unmount and are intentionally left intact.
|
|
1148
|
+
*/
|
|
1149
|
+
declare function clearLiveSyncBus(): void;
|
|
1150
|
+
|
|
1151
|
+
type InvitePreview = {
|
|
1152
|
+
kind: 'space-link';
|
|
1153
|
+
spaceName: string;
|
|
1154
|
+
/** True if the link grants write access, false for read-only. */
|
|
1155
|
+
write: boolean;
|
|
1156
|
+
token: SpaceInviteLinkToken;
|
|
1157
|
+
} | {
|
|
1158
|
+
kind: 'node-link';
|
|
1159
|
+
spaceName: string;
|
|
1160
|
+
/** The node's display name, absent for legacy tokens that omit it. */
|
|
1161
|
+
nodeTitle?: string;
|
|
1162
|
+
token: NodeInviteLinkToken;
|
|
1163
|
+
} | {
|
|
1164
|
+
kind: 'member-bundle';
|
|
1165
|
+
spaceName: string;
|
|
1166
|
+
spaceId: string;
|
|
1167
|
+
/** Short hex fingerprint of the issuing owner's signing key, or null if absent. */
|
|
1168
|
+
issuerKey: string | null;
|
|
1169
|
+
/** The raw cap-bundle JSON — pass verbatim to `acceptSpaceInvite` on consent. */
|
|
1170
|
+
inviteJson: string;
|
|
1171
|
+
};
|
|
1172
|
+
/**
|
|
1173
|
+
* Classify and decode an invite string into a typed {@link InvitePreview}.
|
|
1174
|
+
* Throws a human-readable `Error` on invalid input (safe to surface verbatim
|
|
1175
|
+
* in a toast or inline error message).
|
|
1176
|
+
*/
|
|
1177
|
+
declare function previewInvite(raw: string): InvitePreview;
|
|
1178
|
+
|
|
1179
|
+
export { type ArchivedDms, CONNECT_TIMEOUT_MS, type CapMap, type CreateNodeInput, type DerivedIdentity, type DeviceKeys, type DmMap, type ID, type InvitePreview, type JoinRequest, type KvAdapter, type LinkedIdentity, type MatchRange, type MutePrefs, type MuteValue, type NewObjectInput, type NodeAccess, type NodeAccessHandle, type NodeInviteBundle, type NodeInviteLinkToken, 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 PresenceStatus, type PubAccessMap, type PublicProfile, type RankedResult, type ReadPrefs, type ReadValue, type SealedBlob, type SeedLock, type Session, type Space, type SpaceAccessEntry, SpaceAccessError, type SpaceAccessMap, type SpaceInviteLinkToken, type SpaceMeta, type SpaceMetaUpdate, type TitleMatch, type UnlockMethod, type Vault, type VaultLoad, type VerificationLevel, acceptNodeInvite, acceptSpaceInvite, accountScope, addDeviceToSpaceKeyring, addJoinedSpace, addJoinedSpaceWithCap, addJoinedSpaceWithLinkAccess, addObject, addSpaceMember, ancestors, archiveObject, attachmentName, attachmentPull, attachmentPush, breadcrumbs, broadcastSpaceMeta, buildAuthHeaders, buildEncryptor, buildLinkedSession, buildNodeAccess, buildSession, buildTree, bytesToHex, cacheProfile, capProviderFor, clearLiveSyncBus, clearNodeAccessCache, clearSpaceAccessStore, completeDevicePairing, configureKv, configureOctoSpaces, createNode, createNodeInviteLink, createSpace, createSpaceInviteLink, decodeNodeInviteLink, decodeSpaceInviteLink, deriveSession, dispatchDocChange, emitSseStatus, encodeNodeInviteLink, encodeSpaceInviteLink, ensureProfileKeys, ensurePseudo, fetchWithTimeout, fingerprintFromUserId, fold, fromBase64Url, generateSeedWords, getNodeAccess, getNodeAccessEntry, getSharedSpacesNamespace, getSpaceAccessEntry, getSpaceClient, getSyncBase, getSyncNamespace, getSyncPrefix, hydrateSpaceAccessStore, inviteToNode, inviteToSpace, isValidSeed, isWordStart, joinNodeByLink, joinSpaceByLink, keyringName, keyringPull, keyringPush, kvGet, kvRemove, kvSet, linkAccessFromStore, linkedDeviceScope, loadCachedProfile, localSpaceAccessEntries, makeClient, makeJoinRequest, matchTitle, memberCapsFromStore, nextOrder, nodeMemberScope, objDocName, objDocPull, objDocPush, objIndexName, objIndexPull, objIndexPush, objInvName, objInvPull, objInvPush, objLogName, objLogPull, objLogPush, objPubName, objPubPull, objPubPush, objectBlobName, objectBlobPull, objectBlobPush, objectDirName, objectDirPull, onSpaceMeta, onSseStatus, openEncryptor, ownerEnsureKeyring, ownerScope, ownerTrustedAdders, patchObject, previewInvite, profilePull, profilePush, pullCache, pushIndexSeed, randomId, rankResults, readObjectTree, readProfile, readProfiles, readPseudo, readSpaceAccess, readSpaces, reconcileSpaceMeta, recoverSpaceAccess, registerPull, removeNodeAccessEntry, removeSpaceAccessEntry, removeSpaceMember, reorderObjects, reorderSpaces, reparentObject, roomSlug, rootIdentityOf, saveNodeAccessEntry, saveSpaceAccessEntry, sealToRecipient, sealToSelf, seedSpaceObjectIndex, setDmMapping, setNodeAccess, spaceAccessPull, spaceAccessPush, spaceIdFromRoomId, spaceMemberScope, spacesPull, spacesPush, starfishBase64, startDevicePairing, subtreeIds, toBase64Url, typesIndexName, typesIndexPull, typesIndexPush, unsealFromRecipient, unsealFromSelf, updateArchivedDmsDoc, updateDmsDoc, updateMutesDoc, updateObjectIndex, updateQuickReactionsDoc, updateReadsDoc, updateSpacesDoc, userIdFromEdPub, writeProfile, writePseudo, writeSpaceAccess, writeSpaces };
|