@drakkar.software/octospaces-sdk 0.1.0 → 0.4.1
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 +337 -273
- package/dist/index.js +812 -469
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/types.ts +47 -81
- package/src/index.ts +42 -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/sync/space-access.ts
CHANGED
|
@@ -1,136 +1,181 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Space access resolver
|
|
3
|
-
* space regardless of whether it is private (E2EE) or public (plaintext).
|
|
2
|
+
* Space and node access resolver.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Encryption is per-node (each node has an `enc` flag) but keyed under ONE
|
|
5
|
+
* space-wide keyring at `spaces/{spaceId}/_keyring`. All `enc` nodes in a space
|
|
6
|
+
* share the same CEK; `access` gates *fetching*, the keyring gates *decryption*.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* Two entry points:
|
|
9
|
+
* - `getSpaceClient` — returns the right StarfishClient for member-gated
|
|
10
|
+
* space docs (index, _access). No encryptor.
|
|
11
|
+
* - `getNodeAccess` — resolves the (client, encryptor) for a specific node's
|
|
12
|
+
* CONTENT. Encryptor is null for plaintext nodes; for enc nodes the encryptor
|
|
13
|
+
* opens the SPACE keyring (not a per-node keyring).
|
|
14
|
+
*
|
|
15
|
+
* Resolution order for `getNodeAccess`:
|
|
16
|
+
* 1. Per-node link entry → sign as ephemeral identity; encryptor from space keyring.
|
|
17
|
+
* 2. Per-node member entry → open space keyring as recipient.
|
|
18
|
+
* 3. Space-level link entry → same client; open space keyring if enc.
|
|
19
|
+
* 4. Space-level member entry → open space keyring if enc.
|
|
20
|
+
* 5. No entry, owner → mint space keyring if enc; plain client otherwise.
|
|
21
|
+
* 6. No entry, non-owner → SpaceAccessError if enc; plain client otherwise.
|
|
15
22
|
*/
|
|
16
23
|
import type { Encryptor, StarfishClient } from '@drakkar.software/starfish-client';
|
|
17
24
|
|
|
18
25
|
import { buildEncryptor, makeClient, openEncryptor, ownerEnsureKeyring } from './client.js';
|
|
19
26
|
import type { Session } from './identity.js';
|
|
20
27
|
import { ownerTrustedAdders } from './identity.js';
|
|
21
|
-
import { getSpaceAccessEntry } from './space-access-store.js';
|
|
28
|
+
import { getNodeAccessEntry, getSpaceAccessEntry } from './space-access-store.js';
|
|
22
29
|
import { SpaceAccessError } from '../core/space-access-error.js';
|
|
23
|
-
import
|
|
30
|
+
import { keyringPull, keyringPush } from './paths.js';
|
|
31
|
+
import type { NodeAccess } from '../core/types.js';
|
|
24
32
|
|
|
25
33
|
// Re-export so existing importers keep reaching SpaceAccessError through this module.
|
|
26
34
|
export { SpaceAccessError };
|
|
27
35
|
|
|
28
|
-
export interface
|
|
36
|
+
export interface NodeAccessHandle {
|
|
29
37
|
encryptor: Encryptor | null;
|
|
30
38
|
client: StarfishClient;
|
|
31
|
-
/** True when opened as the space OWNER (
|
|
39
|
+
/** True when opened as the space OWNER (may seed / mint the space keyring). */
|
|
32
40
|
isOwnerOpen: boolean;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
const cache = new Map<string, Promise<
|
|
43
|
+
const cache = new Map<string, Promise<NodeAccessHandle>>();
|
|
36
44
|
|
|
37
45
|
/** Drop every cached handle (on account switch — keys are per-identity). */
|
|
38
|
-
export function
|
|
46
|
+
export function clearNodeAccessCache(): void {
|
|
39
47
|
cache.clear();
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
43
|
-
*
|
|
51
|
+
* Return the right StarfishClient for reading/writing member-gated space docs
|
|
52
|
+
* (e.g. the `_index`, `_access`). Spaces are always plaintext — no encryptor.
|
|
53
|
+
*/
|
|
54
|
+
export function getSpaceClient(spaceId: string, session: Session): StarfishClient {
|
|
55
|
+
const entry = getSpaceAccessEntry(spaceId);
|
|
56
|
+
if (entry?.kind === 'link') return makeClient(entry.cap, entry.key);
|
|
57
|
+
if (entry?.kind === 'member') {
|
|
58
|
+
const cap = JSON.parse(entry.cap) as { iss?: string };
|
|
59
|
+
return makeClient(cap, session.keys.edPriv);
|
|
60
|
+
}
|
|
61
|
+
return session.chatClient;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the right (client, encryptor) for a node's CONTENT, opening and
|
|
66
|
+
* caching on first use.
|
|
44
67
|
*
|
|
45
|
-
* `
|
|
46
|
-
*
|
|
68
|
+
* `node` carries `{ access?, enc? }` — the plaintext flags from the index.
|
|
69
|
+
* `reg` is the space's access record if already known; used to determine
|
|
70
|
+
* ownership. Pass null if unknown.
|
|
47
71
|
*/
|
|
48
|
-
export function
|
|
72
|
+
export function getNodeAccess(
|
|
49
73
|
spaceId: string,
|
|
74
|
+
nodeId: string,
|
|
75
|
+
node: { access?: NodeAccess; enc?: boolean },
|
|
50
76
|
session: Session,
|
|
51
|
-
reg
|
|
52
|
-
): Promise<
|
|
53
|
-
const
|
|
77
|
+
reg?: { owner: string | null; members: string[] } | null,
|
|
78
|
+
): Promise<NodeAccessHandle> {
|
|
79
|
+
const cacheKey = `${spaceId}:${nodeId}`;
|
|
80
|
+
const hit = cache.get(cacheKey);
|
|
54
81
|
if (hit) return hit;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
82
|
+
|
|
83
|
+
const p = (async (): Promise<NodeAccessHandle> => {
|
|
84
|
+
// Prefer a per-node entry, fall back to the space-level entry for the client.
|
|
85
|
+
const nodeEntry = getNodeAccessEntry(spaceId, nodeId);
|
|
86
|
+
const spaceEntry = getSpaceAccessEntry(spaceId);
|
|
87
|
+
const activeEntry = nodeEntry ?? spaceEntry;
|
|
88
|
+
|
|
89
|
+
// Build the client.
|
|
90
|
+
let client: StarfishClient;
|
|
91
|
+
let capIss: string | undefined;
|
|
92
|
+
if (activeEntry?.kind === 'link') {
|
|
93
|
+
client = makeClient(activeEntry.cap, activeEntry.key);
|
|
94
|
+
} else if (activeEntry?.kind === 'member') {
|
|
95
|
+
const cap = JSON.parse(activeEntry.cap) as { iss?: string };
|
|
96
|
+
capIss = cap.iss;
|
|
97
|
+
client = makeClient(cap, session.keys.edPriv);
|
|
98
|
+
} else {
|
|
99
|
+
client = session.chatClient;
|
|
63
100
|
}
|
|
64
101
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return { encryptor, client, isOwnerOpen
|
|
102
|
+
const isOwnerOpen =
|
|
103
|
+
reg != null ? reg.owner === session.userId : activeEntry == null;
|
|
104
|
+
|
|
105
|
+
// Plaintext node — no keyring needed.
|
|
106
|
+
if (!node.enc) {
|
|
107
|
+
return { encryptor: null, client, isOwnerOpen };
|
|
71
108
|
}
|
|
72
109
|
|
|
73
|
-
//
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
// E2EE node — resolve the SPACE-WIDE keyring.
|
|
111
|
+
const spacePullPath = keyringPull(spaceId);
|
|
112
|
+
const trustedAdders = capIss
|
|
113
|
+
? [capIss]
|
|
114
|
+
: reg?.owner
|
|
115
|
+
? [reg.owner]
|
|
116
|
+
: ownerTrustedAdders(session);
|
|
117
|
+
|
|
118
|
+
if (activeEntry?.kind === 'member' || activeEntry?.kind === 'link') {
|
|
119
|
+
const encryptor = await openEncryptor(client, session.keys, spacePullPath, trustedAdders);
|
|
120
|
+
return { encryptor, client, isOwnerOpen: false };
|
|
77
121
|
}
|
|
78
122
|
|
|
79
|
-
//
|
|
123
|
+
// No access entry — owner mints/opens the keyring; non-owner errors.
|
|
80
124
|
const owner = reg?.owner ?? null;
|
|
81
125
|
const members = reg?.members ?? [];
|
|
82
126
|
if (owner !== null && owner !== session.userId) {
|
|
83
127
|
throw new SpaceAccessError(
|
|
84
128
|
members.includes(session.userId)
|
|
85
|
-
? "You're a member of this space, but
|
|
86
|
-
: "You don't have access to this
|
|
129
|
+
? "You're a member of this space, but the space key isn't on this device yet — ask the owner to invite you."
|
|
130
|
+
: "You don't have access to this node.",
|
|
87
131
|
);
|
|
88
132
|
}
|
|
89
133
|
const encryptor = await ownerEnsureKeyring(
|
|
90
134
|
session.chatClient,
|
|
91
135
|
session.keys,
|
|
92
|
-
|
|
136
|
+
spacePullPath,
|
|
137
|
+
keyringPush(spaceId),
|
|
93
138
|
ownerTrustedAdders(session),
|
|
94
139
|
);
|
|
95
140
|
return { encryptor, client: session.chatClient, isOwnerOpen: true };
|
|
96
141
|
})();
|
|
97
|
-
|
|
98
|
-
|
|
142
|
+
|
|
143
|
+
cache.set(cacheKey, p);
|
|
144
|
+
p.catch(() => cache.delete(cacheKey));
|
|
99
145
|
return p;
|
|
100
146
|
}
|
|
101
147
|
|
|
102
148
|
/**
|
|
103
149
|
* SOFT resolve — never mints a keyring, never throws on missing access.
|
|
104
|
-
* Returns null when the identity has no usable access for the
|
|
150
|
+
* Returns null when the identity has no usable access for the node yet.
|
|
105
151
|
*/
|
|
106
|
-
export async function
|
|
152
|
+
export async function buildNodeAccess(
|
|
107
153
|
session: Session,
|
|
108
154
|
spaceId: string,
|
|
109
|
-
|
|
155
|
+
nodeId: string,
|
|
156
|
+
node: { enc?: boolean },
|
|
110
157
|
): Promise<{ client: StarfishClient; encryptor: Encryptor | null } | null> {
|
|
111
|
-
const
|
|
158
|
+
const nodeEntry = getNodeAccessEntry(spaceId, nodeId);
|
|
159
|
+
const spaceEntry = getSpaceAccessEntry(spaceId);
|
|
160
|
+
const activeEntry = nodeEntry ?? spaceEntry;
|
|
112
161
|
|
|
113
|
-
|
|
114
|
-
const client = makeClient(entry.cap, entry.key);
|
|
115
|
-
return { client, encryptor: null };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let client = session.chatClient;
|
|
162
|
+
let client: StarfishClient;
|
|
119
163
|
let trustedAdders = ownerTrustedAdders(session);
|
|
120
164
|
|
|
121
|
-
if (
|
|
122
|
-
|
|
165
|
+
if (activeEntry?.kind === 'link') {
|
|
166
|
+
client = makeClient(activeEntry.cap, activeEntry.key);
|
|
167
|
+
} else if (activeEntry?.kind === 'member') {
|
|
168
|
+
const cap = JSON.parse(activeEntry.cap) as { iss?: string };
|
|
123
169
|
client = makeClient(cap, session.keys.edPriv);
|
|
124
170
|
if (cap.iss) trustedAdders = [cap.iss];
|
|
125
|
-
|
|
126
|
-
|
|
171
|
+
} else {
|
|
172
|
+
client = session.chatClient;
|
|
127
173
|
}
|
|
128
174
|
|
|
129
|
-
if (
|
|
130
|
-
return { client, encryptor: null };
|
|
131
|
-
}
|
|
175
|
+
if (!node.enc) return { client, encryptor: null };
|
|
132
176
|
|
|
133
|
-
//
|
|
134
|
-
const
|
|
177
|
+
// Soft-open the SPACE-WIDE keyring.
|
|
178
|
+
const spacePullPath = keyringPull(spaceId);
|
|
179
|
+
const encryptor = await buildEncryptor(client, session.keys, spacePullPath, trustedAdders);
|
|
135
180
|
return encryptor ? { client, encryptor } : null;
|
|
136
181
|
}
|