cojson 0.8.32 → 0.8.35
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 +15 -0
- package/dist/native/coValueCore.js +73 -37
- package/dist/native/coValueCore.js.map +1 -1
- package/dist/native/coValues/coMap.js +2 -2
- package/dist/native/coValues/coMap.js.map +1 -1
- package/dist/native/coValues/group.js +132 -5
- package/dist/native/coValues/group.js.map +1 -1
- package/dist/native/exports.js +5 -2
- package/dist/native/exports.js.map +1 -1
- package/dist/native/ids.js +33 -0
- package/dist/native/ids.js.map +1 -1
- package/dist/native/permissions.js +206 -145
- package/dist/native/permissions.js.map +1 -1
- package/dist/native/storage/index.js +8 -4
- package/dist/native/storage/index.js.map +1 -1
- package/dist/native/sync.js +41 -31
- package/dist/native/sync.js.map +1 -1
- package/dist/web/coValueCore.js +73 -37
- package/dist/web/coValueCore.js.map +1 -1
- package/dist/web/coValues/coMap.js +2 -2
- package/dist/web/coValues/coMap.js.map +1 -1
- package/dist/web/coValues/group.js +132 -5
- package/dist/web/coValues/group.js.map +1 -1
- package/dist/web/exports.js +5 -2
- package/dist/web/exports.js.map +1 -1
- package/dist/web/ids.js +33 -0
- package/dist/web/ids.js.map +1 -1
- package/dist/web/permissions.js +206 -145
- package/dist/web/permissions.js.map +1 -1
- package/dist/web/storage/index.js +8 -4
- package/dist/web/storage/index.js.map +1 -1
- package/dist/web/sync.js +41 -31
- package/dist/web/sync.js.map +1 -1
- package/package.json +1 -1
- package/src/coValueCore.ts +119 -46
- package/src/coValues/coMap.ts +3 -6
- package/src/coValues/group.ts +219 -6
- package/src/exports.ts +18 -3
- package/src/ids.ts +48 -0
- package/src/permissions.ts +297 -204
- package/src/storage/index.ts +12 -4
- package/src/sync.ts +43 -34
- package/src/tests/group.test.ts +152 -1
- package/src/tests/permissions.test.ts +785 -2
- package/src/tests/sync.test.ts +29 -0
- package/src/tests/testUtils.ts +102 -1
package/src/coValues/group.ts
CHANGED
|
@@ -2,9 +2,19 @@ import { base58 } from "@scure/base";
|
|
|
2
2
|
import { CoID } from "../coValue.js";
|
|
3
3
|
import { CoValueUniqueness } from "../coValueCore.js";
|
|
4
4
|
import { Encrypted, KeyID, KeySecret, Sealed } from "../crypto/crypto.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
AgentID,
|
|
7
|
+
ChildGroupReference,
|
|
8
|
+
ParentGroupReference,
|
|
9
|
+
getChildGroupId,
|
|
10
|
+
getParentGroupId,
|
|
11
|
+
isAgentID,
|
|
12
|
+
isChildGroupReference,
|
|
13
|
+
isParentGroupReference,
|
|
14
|
+
} from "../ids.js";
|
|
6
15
|
import { JsonObject } from "../jsonValue.js";
|
|
7
16
|
import { Role } from "../permissions.js";
|
|
17
|
+
import { expectGroup } from "../typeUtils/expectGroup.js";
|
|
8
18
|
import {
|
|
9
19
|
ControlledAccountOrAgent,
|
|
10
20
|
RawAccount,
|
|
@@ -29,6 +39,8 @@ export type GroupShape = {
|
|
|
29
39
|
KeySecret,
|
|
30
40
|
{ encryptedID: KeyID; encryptingID: KeyID }
|
|
31
41
|
>;
|
|
42
|
+
[parent: ParentGroupReference]: "extend";
|
|
43
|
+
[child: ChildGroupReference]: "extend";
|
|
32
44
|
};
|
|
33
45
|
|
|
34
46
|
/** A `Group` is a scope for permissions of its members (`"reader" | "writer" | "admin"`), applying to objects owned by that group.
|
|
@@ -61,12 +73,109 @@ export class RawGroup<
|
|
|
61
73
|
* @category 1. Role reading
|
|
62
74
|
*/
|
|
63
75
|
roleOf(accountID: RawAccountID): Role | undefined {
|
|
64
|
-
return this.roleOfInternal(accountID);
|
|
76
|
+
return this.roleOfInternal(accountID)?.role;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
/** @internal */
|
|
68
|
-
roleOfInternal(
|
|
69
|
-
|
|
80
|
+
roleOfInternal(
|
|
81
|
+
accountID: RawAccountID | AgentID | typeof EVERYONE,
|
|
82
|
+
): { role: Role; via: CoID<RawGroup> | undefined } | undefined {
|
|
83
|
+
const roleHere = this.get(accountID);
|
|
84
|
+
if (roleHere === "revoked") {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let roleInfo:
|
|
89
|
+
| {
|
|
90
|
+
role: Exclude<Role, "revoked">;
|
|
91
|
+
via: CoID<RawGroup> | undefined;
|
|
92
|
+
}
|
|
93
|
+
| undefined = roleHere && { role: roleHere, via: undefined };
|
|
94
|
+
|
|
95
|
+
const parentGroups = this.getParentGroups();
|
|
96
|
+
|
|
97
|
+
for (const parentGroup of parentGroups) {
|
|
98
|
+
const roleInParent = parentGroup.roleOfInternal(accountID);
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
roleInParent &&
|
|
102
|
+
roleInParent.role !== "revoked" &&
|
|
103
|
+
isMorePermissiveAndShouldInherit(roleInParent.role, roleInfo?.role)
|
|
104
|
+
) {
|
|
105
|
+
roleInfo = { role: roleInParent.role, via: parentGroup.id };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return roleInfo;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getParentGroups() {
|
|
113
|
+
const groups: RawGroup[] = [];
|
|
114
|
+
|
|
115
|
+
for (const key of this.keys()) {
|
|
116
|
+
if (isParentGroupReference(key)) {
|
|
117
|
+
const parent = this.core.node.expectCoValueLoaded(
|
|
118
|
+
getParentGroupId(key),
|
|
119
|
+
"Expected parent group to be loaded",
|
|
120
|
+
);
|
|
121
|
+
groups.push(expectGroup(parent.getCurrentContent()));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return groups;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
loadAllChildGroups() {
|
|
129
|
+
const requests: Promise<unknown>[] = [];
|
|
130
|
+
const store = this.core.node.coValuesStore;
|
|
131
|
+
const peers = this.core.node.syncManager.getServerAndStoragePeers();
|
|
132
|
+
|
|
133
|
+
for (const key of this.keys()) {
|
|
134
|
+
if (!isChildGroupReference(key)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const id = getChildGroupId(key);
|
|
139
|
+
const child = store.get(id);
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
child.state.type === "unknown" ||
|
|
143
|
+
child.state.type === "unavailable"
|
|
144
|
+
) {
|
|
145
|
+
child.loadFromPeers(peers).catch(() => {
|
|
146
|
+
console.error(`Failed to load child group ${id}`);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
requests.push(
|
|
151
|
+
child.getCoValue().then((coValue) => {
|
|
152
|
+
if (coValue === "unavailable") {
|
|
153
|
+
throw new Error(`Child group ${child.id} is unavailable`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Recursively load child groups
|
|
157
|
+
return expectGroup(coValue.getCurrentContent()).loadAllChildGroups();
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return Promise.all(requests);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getChildGroups() {
|
|
166
|
+
const groups: RawGroup[] = [];
|
|
167
|
+
|
|
168
|
+
for (const key of this.keys()) {
|
|
169
|
+
if (isChildGroupReference(key)) {
|
|
170
|
+
const child = this.core.node.expectCoValueLoaded(
|
|
171
|
+
getChildGroupId(key),
|
|
172
|
+
"Expected child group to be loaded",
|
|
173
|
+
);
|
|
174
|
+
groups.push(expectGroup(child.getCurrentContent()));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return groups;
|
|
70
179
|
}
|
|
71
180
|
|
|
72
181
|
/**
|
|
@@ -75,7 +184,7 @@ export class RawGroup<
|
|
|
75
184
|
* @category 1. Role reading
|
|
76
185
|
*/
|
|
77
186
|
myRole(): Role | undefined {
|
|
78
|
-
return this.roleOfInternal(this.core.node.account.id);
|
|
187
|
+
return this.roleOfInternal(this.core.node.account.id)?.role;
|
|
79
188
|
}
|
|
80
189
|
|
|
81
190
|
/**
|
|
@@ -158,6 +267,10 @@ export class RawGroup<
|
|
|
158
267
|
}
|
|
159
268
|
}) as (RawAccountID | AgentID)[];
|
|
160
269
|
|
|
270
|
+
// Get these early, so we fail fast if they are unavailable
|
|
271
|
+
const parentGroups = this.getParentGroups();
|
|
272
|
+
const childGroups = this.getChildGroups();
|
|
273
|
+
|
|
161
274
|
const maybeCurrentReadKey = this.core.getCurrentReadKey();
|
|
162
275
|
|
|
163
276
|
if (!maybeCurrentReadKey.secret) {
|
|
@@ -204,6 +317,73 @@ export class RawGroup<
|
|
|
204
317
|
);
|
|
205
318
|
|
|
206
319
|
this.set("readKey", newReadKey.id, "trusting");
|
|
320
|
+
|
|
321
|
+
// when we rotate our readKey (because someone got kicked out), we also need to (recursively)
|
|
322
|
+
// rotate the readKeys of all child groups (so they are kicked out there as well)
|
|
323
|
+
for (const parent of parentGroups) {
|
|
324
|
+
const { id: parentReadKeyID, secret: parentReadKeySecret } =
|
|
325
|
+
parent.core.getCurrentReadKey();
|
|
326
|
+
|
|
327
|
+
if (!parentReadKeySecret) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
"Can't reveal new child key to parent where we don't have access to the parent read key",
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.set(
|
|
334
|
+
`${newReadKey.id}_for_${parentReadKeyID}`,
|
|
335
|
+
this.core.crypto.encryptKeySecret({
|
|
336
|
+
encrypting: {
|
|
337
|
+
id: parentReadKeyID,
|
|
338
|
+
secret: parentReadKeySecret,
|
|
339
|
+
},
|
|
340
|
+
toEncrypt: newReadKey,
|
|
341
|
+
}).encrypted,
|
|
342
|
+
"trusting",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const child of childGroups) {
|
|
347
|
+
child.rotateReadKey();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
extend(parent: RawGroup) {
|
|
352
|
+
if (parent.myRole() !== "admin" || this.myRole() !== "admin") {
|
|
353
|
+
throw new Error(
|
|
354
|
+
"To extend a group, the current account must have admin role in both groups",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this.set(`parent_${parent.id}`, "extend", "trusting");
|
|
359
|
+
parent.set(`child_${this.id}`, "extend", "trusting");
|
|
360
|
+
|
|
361
|
+
const { id: parentReadKeyID, secret: parentReadKeySecret } =
|
|
362
|
+
parent.core.getCurrentReadKey();
|
|
363
|
+
if (!parentReadKeySecret) {
|
|
364
|
+
throw new Error("Can't extend group without parent read key secret");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const { id: childReadKeyID, secret: childReadKeySecret } =
|
|
368
|
+
this.core.getCurrentReadKey();
|
|
369
|
+
if (!childReadKeySecret) {
|
|
370
|
+
throw new Error("Can't extend group without child read key secret");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
this.set(
|
|
374
|
+
`${childReadKeyID}_for_${parentReadKeyID}`,
|
|
375
|
+
this.core.crypto.encryptKeySecret({
|
|
376
|
+
encrypting: {
|
|
377
|
+
id: parentReadKeyID,
|
|
378
|
+
secret: parentReadKeySecret,
|
|
379
|
+
},
|
|
380
|
+
toEncrypt: {
|
|
381
|
+
id: childReadKeyID,
|
|
382
|
+
secret: childReadKeySecret,
|
|
383
|
+
},
|
|
384
|
+
}).encrypted,
|
|
385
|
+
"trusting",
|
|
386
|
+
);
|
|
207
387
|
}
|
|
208
388
|
|
|
209
389
|
/**
|
|
@@ -213,7 +393,12 @@ export class RawGroup<
|
|
|
213
393
|
*
|
|
214
394
|
* @category 2. Role changing
|
|
215
395
|
*/
|
|
216
|
-
removeMember(
|
|
396
|
+
async removeMember(
|
|
397
|
+
account: RawAccount | ControlledAccountOrAgent | Everyone,
|
|
398
|
+
) {
|
|
399
|
+
// Ensure all child groups are loaded before removing a member
|
|
400
|
+
await this.loadAllChildGroups();
|
|
401
|
+
|
|
217
402
|
this.removeMemberInternal(account);
|
|
218
403
|
}
|
|
219
404
|
|
|
@@ -347,6 +532,34 @@ export class RawGroup<
|
|
|
347
532
|
}
|
|
348
533
|
}
|
|
349
534
|
|
|
535
|
+
function isMorePermissiveAndShouldInherit(
|
|
536
|
+
roleInParent: Role,
|
|
537
|
+
roleInChild: Exclude<Role, "revoked"> | undefined,
|
|
538
|
+
) {
|
|
539
|
+
// invites should never be inherited
|
|
540
|
+
if (
|
|
541
|
+
roleInParent === "adminInvite" ||
|
|
542
|
+
roleInParent === "writerInvite" ||
|
|
543
|
+
roleInParent === "readerInvite"
|
|
544
|
+
) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (roleInParent === "admin") {
|
|
549
|
+
return !roleInChild || roleInChild !== "admin";
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (roleInParent === "writer") {
|
|
553
|
+
return !roleInChild || roleInChild === "reader";
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (roleInParent === "reader") {
|
|
557
|
+
return !roleInChild;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
|
|
350
563
|
export type InviteSecret = `inviteSecret_z${string}`;
|
|
351
564
|
|
|
352
565
|
function inviteSecretFromSecretSeed(secretSeed: Uint8Array): InviteSecret {
|
package/src/exports.ts
CHANGED
|
@@ -23,8 +23,14 @@ import {
|
|
|
23
23
|
secretSeedLength,
|
|
24
24
|
shortHashLength,
|
|
25
25
|
} from "./crypto/crypto.js";
|
|
26
|
-
import {
|
|
27
|
-
|
|
26
|
+
import {
|
|
27
|
+
getGroupDependentKey,
|
|
28
|
+
getGroupDependentKeyList,
|
|
29
|
+
isRawCoID,
|
|
30
|
+
rawCoIDfromBytes,
|
|
31
|
+
rawCoIDtoBytes,
|
|
32
|
+
} from "./ids.js";
|
|
33
|
+
import { Stringified, parseJSON } from "./jsonStringify.js";
|
|
28
34
|
import { LocalNode } from "./localNode.js";
|
|
29
35
|
import type { Role } from "./permissions.js";
|
|
30
36
|
import { Channel, connectedPeers } from "./streamUtils.js";
|
|
@@ -53,7 +59,11 @@ import type {
|
|
|
53
59
|
Peer,
|
|
54
60
|
SyncMessage,
|
|
55
61
|
} from "./sync.js";
|
|
56
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
DisconnectedError,
|
|
64
|
+
PingTimeoutError,
|
|
65
|
+
emptyKnownState,
|
|
66
|
+
} from "./sync.js";
|
|
57
67
|
|
|
58
68
|
type Value = JsonValue | AnyRawCoValue;
|
|
59
69
|
|
|
@@ -79,6 +89,8 @@ export const cojsonInternals = {
|
|
|
79
89
|
StreamingHash,
|
|
80
90
|
Channel,
|
|
81
91
|
getPriorityFromHeader,
|
|
92
|
+
getGroupDependentKeyList,
|
|
93
|
+
getGroupDependentKey,
|
|
82
94
|
};
|
|
83
95
|
|
|
84
96
|
export {
|
|
@@ -116,6 +128,7 @@ export {
|
|
|
116
128
|
SyncMessage,
|
|
117
129
|
isRawCoID,
|
|
118
130
|
LSMStorage,
|
|
131
|
+
emptyKnownState,
|
|
119
132
|
};
|
|
120
133
|
|
|
121
134
|
export type {
|
|
@@ -128,6 +141,7 @@ export type {
|
|
|
128
141
|
DisconnectedError,
|
|
129
142
|
PingTimeoutError,
|
|
130
143
|
CoValueUniqueness,
|
|
144
|
+
Stringified,
|
|
131
145
|
};
|
|
132
146
|
|
|
133
147
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
@@ -137,6 +151,7 @@ export namespace CojsonInternalTypes {
|
|
|
137
151
|
export type KnownStateMessage = import("./sync.js").KnownStateMessage;
|
|
138
152
|
export type LoadMessage = import("./sync.js").LoadMessage;
|
|
139
153
|
export type NewContentMessage = import("./sync.js").NewContentMessage;
|
|
154
|
+
export type SessionNewContent = import("./sync.js").SessionNewContent;
|
|
140
155
|
export type CoValueHeader = import("./coValueCore.js").CoValueHeader;
|
|
141
156
|
export type Transaction = import("./coValueCore.js").Transaction;
|
|
142
157
|
export type TransactionID = import("./ids.js").TransactionID;
|
package/src/ids.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { base58 } from "@scure/base";
|
|
2
|
+
import { CoID } from "./coValue.js";
|
|
2
3
|
import { RawAccountID } from "./coValues/account.js";
|
|
3
4
|
import { shortHashLength } from "./crypto/crypto.js";
|
|
5
|
+
import { RawGroup } from "./exports.js";
|
|
4
6
|
|
|
5
7
|
export type RawCoID = `co_z${string}`;
|
|
8
|
+
export type ParentGroupReference = `parent_${CoID<RawGroup>}`;
|
|
9
|
+
export type ChildGroupReference = `child_${CoID<RawGroup>}`;
|
|
6
10
|
|
|
7
11
|
export function isRawCoID(id: unknown): id is RawCoID {
|
|
8
12
|
return typeof id === "string" && id.startsWith("co_z");
|
|
@@ -29,3 +33,47 @@ export function isAgentID(id: string): id is AgentID {
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export type SessionID = `${RawAccountID | AgentID}_session_z${string}`;
|
|
36
|
+
|
|
37
|
+
export function isParentGroupReference(
|
|
38
|
+
key: string,
|
|
39
|
+
): key is ParentGroupReference {
|
|
40
|
+
return key.startsWith("parent_");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getParentGroupId(key: ParentGroupReference): CoID<RawGroup> {
|
|
44
|
+
return key.slice("parent_".length) as CoID<RawGroup>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isChildGroupReference(key: string): key is ChildGroupReference {
|
|
48
|
+
return key.startsWith("child_");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getChildGroupId(key: ChildGroupReference): CoID<RawGroup> {
|
|
52
|
+
return key.slice("child_".length) as CoID<RawGroup>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getGroupDependentKey(key: unknown) {
|
|
56
|
+
if (typeof key !== "string") return undefined;
|
|
57
|
+
|
|
58
|
+
if (isParentGroupReference(key)) {
|
|
59
|
+
return getParentGroupId(key);
|
|
60
|
+
} else if (key.startsWith("co_")) {
|
|
61
|
+
return key as RawCoID;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getGroupDependentKeyList(keys: unknown[]) {
|
|
68
|
+
const groupDependentKeys: RawCoID[] = [];
|
|
69
|
+
|
|
70
|
+
for (const key of keys) {
|
|
71
|
+
const value = getGroupDependentKey(key);
|
|
72
|
+
|
|
73
|
+
if (value) {
|
|
74
|
+
groupDependentKeys.push(value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return groupDependentKeys;
|
|
79
|
+
}
|