cojson 0.8.17 → 0.8.19-group-inheritance.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.
- package/.turbo/turbo-build.log +12 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +1001 -0
- package/CHANGELOG.md +12 -0
- package/dist/native/coValueCore.js +34 -3
- package/dist/native/coValueCore.js.map +1 -1
- package/dist/native/coValues/group.js +89 -3
- package/dist/native/coValues/group.js.map +1 -1
- package/dist/native/permissions.js +174 -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 +6 -1
- package/dist/native/sync.js.map +1 -1
- package/dist/web/coValueCore.js +34 -3
- package/dist/web/coValueCore.js.map +1 -1
- package/dist/web/coValues/group.js +89 -3
- package/dist/web/coValues/group.js.map +1 -1
- package/dist/web/permissions.js +174 -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 +6 -1
- package/dist/web/sync.js.map +1 -1
- package/package.json +1 -1
- package/src/coValueCore.ts +50 -4
- package/src/coValues/group.ts +159 -4
- package/src/permissions.ts +244 -203
- package/src/storage/index.ts +12 -4
- package/src/sync.ts +7 -2
- package/src/tests/permissions.test.ts +748 -0
- package/src/tests/sync.test.ts +4 -4
package/src/coValueCore.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Result, err, ok } from "neverthrow";
|
|
2
|
-
import { AnyRawCoValue, RawCoValue } from "./coValue.js";
|
|
2
|
+
import { AnyRawCoValue, CoID, RawCoValue } from "./coValue.js";
|
|
3
3
|
import { ControlledAccountOrAgent, RawAccountID } from "./coValues/account.js";
|
|
4
4
|
import { RawGroup } from "./coValues/group.js";
|
|
5
5
|
import { coreToCoValue } from "./coreToCoValue.js";
|
|
@@ -785,6 +785,46 @@ export class CoValueCore {
|
|
|
785
785
|
}
|
|
786
786
|
}
|
|
787
787
|
|
|
788
|
+
// try to find revelation to parent group read keys
|
|
789
|
+
|
|
790
|
+
for (const co of content.keys()) {
|
|
791
|
+
if (co.startsWith("parent_")) {
|
|
792
|
+
const parentGroupID = co.slice("parent_".length) as CoID<RawGroup>;
|
|
793
|
+
const parentGroup = this.node.expectCoValueLoaded(
|
|
794
|
+
parentGroupID,
|
|
795
|
+
"Expected parent group to be loaded",
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
const parentKey = parentGroup.getCurrentReadKey();
|
|
799
|
+
if (!parentKey.secret) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const revelationForParentKey = content.get(
|
|
804
|
+
`${keyID}_for_${parentKey.id}`,
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
if (revelationForParentKey) {
|
|
808
|
+
const secret = parentGroup.crypto.decryptKeySecret(
|
|
809
|
+
{
|
|
810
|
+
encryptedID: keyID,
|
|
811
|
+
encryptingID: parentKey.id,
|
|
812
|
+
encrypted: revelationForParentKey,
|
|
813
|
+
},
|
|
814
|
+
parentKey.secret,
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
if (secret) {
|
|
818
|
+
return secret as KeySecret;
|
|
819
|
+
} else {
|
|
820
|
+
console.error(
|
|
821
|
+
`Encrypting parent ${parentKey.id} key didn't decrypt ${keyID}`,
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
788
828
|
return undefined;
|
|
789
829
|
} else if (this.header.ruleset.type === "ownedByGroup") {
|
|
790
830
|
return this.node
|
|
@@ -950,9 +990,15 @@ export class CoValueCore {
|
|
|
950
990
|
/** @internal */
|
|
951
991
|
getDependedOnCoValuesUncached(): RawCoID[] {
|
|
952
992
|
return this.header.ruleset.type === "group"
|
|
953
|
-
?
|
|
954
|
-
.
|
|
955
|
-
|
|
993
|
+
? [
|
|
994
|
+
...expectGroup(this.getCurrentContent())
|
|
995
|
+
.keys()
|
|
996
|
+
.filter((k): k is RawAccountID => k.startsWith("co_")),
|
|
997
|
+
...expectGroup(this.getCurrentContent())
|
|
998
|
+
.keys()
|
|
999
|
+
.filter((k) => k.startsWith("parent_"))
|
|
1000
|
+
.map((k) => k.replace("parent_", "") as RawCoID),
|
|
1001
|
+
]
|
|
956
1002
|
: this.header.ruleset.type === "ownedByGroup"
|
|
957
1003
|
? [
|
|
958
1004
|
this.header.ruleset.group,
|
package/src/coValues/group.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Encrypted, KeyID, KeySecret, Sealed } from "../crypto/crypto.js";
|
|
|
5
5
|
import { AgentID, isAgentID } from "../ids.js";
|
|
6
6
|
import { JsonObject } from "../jsonValue.js";
|
|
7
7
|
import { Role } from "../permissions.js";
|
|
8
|
+
import { expectGroup } from "../typeUtils/expectGroup.js";
|
|
8
9
|
import {
|
|
9
10
|
ControlledAccountOrAgent,
|
|
10
11
|
RawAccount,
|
|
@@ -29,6 +30,8 @@ export type GroupShape = {
|
|
|
29
30
|
KeySecret,
|
|
30
31
|
{ encryptedID: KeyID; encryptingID: KeyID }
|
|
31
32
|
>;
|
|
33
|
+
[parent: `parent_${CoID<RawGroup>}`]: "extend";
|
|
34
|
+
[child: `child_${CoID<RawGroup>}`]: "extend";
|
|
32
35
|
};
|
|
33
36
|
|
|
34
37
|
/** A `Group` is a scope for permissions of its members (`"reader" | "writer" | "admin"`), applying to objects owned by that group.
|
|
@@ -61,12 +64,68 @@ export class RawGroup<
|
|
|
61
64
|
* @category 1. Role reading
|
|
62
65
|
*/
|
|
63
66
|
roleOf(accountID: RawAccountID): Role | undefined {
|
|
64
|
-
return this.roleOfInternal(accountID);
|
|
67
|
+
return this.roleOfInternal(accountID)?.role;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
/** @internal */
|
|
68
|
-
roleOfInternal(
|
|
69
|
-
|
|
71
|
+
roleOfInternal(
|
|
72
|
+
accountID: RawAccountID | AgentID | typeof EVERYONE,
|
|
73
|
+
): { role: Role; via: CoID<RawGroup> | undefined } | undefined {
|
|
74
|
+
const roleHere = this.get(accountID);
|
|
75
|
+
if (roleHere === "revoked") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let roleInfo:
|
|
80
|
+
| {
|
|
81
|
+
role: Exclude<Role, "revoked">;
|
|
82
|
+
via: CoID<RawGroup> | undefined;
|
|
83
|
+
}
|
|
84
|
+
| undefined = roleHere && { role: roleHere, via: undefined };
|
|
85
|
+
|
|
86
|
+
const parentGroups = this.getParentGroups();
|
|
87
|
+
|
|
88
|
+
for (const parentGroup of parentGroups) {
|
|
89
|
+
const roleInParent = parentGroup.roleOfInternal(accountID);
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
roleInParent &&
|
|
93
|
+
roleInParent.role !== "revoked" &&
|
|
94
|
+
isMorePermissiveAndShouldInherit(roleInParent.role, roleInfo?.role)
|
|
95
|
+
) {
|
|
96
|
+
roleInfo = { role: roleInParent.role, via: parentGroup.id };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return roleInfo;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getParentGroups(): RawGroup[] {
|
|
104
|
+
return (
|
|
105
|
+
this.keys().filter((key) =>
|
|
106
|
+
key.startsWith("parent_"),
|
|
107
|
+
) as `parent_${CoID<RawGroup>}`[]
|
|
108
|
+
).map((parentKey) => {
|
|
109
|
+
const parent = this.core.node.expectCoValueLoaded(
|
|
110
|
+
parentKey.slice("parent_".length) as CoID<RawGroup>,
|
|
111
|
+
"Expected parent group to be loaded",
|
|
112
|
+
);
|
|
113
|
+
return expectGroup(parent.getCurrentContent());
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getChildGroups(): RawGroup[] {
|
|
118
|
+
return (
|
|
119
|
+
this.keys().filter((key) =>
|
|
120
|
+
key.startsWith("child_"),
|
|
121
|
+
) as `child_${CoID<RawGroup>}`[]
|
|
122
|
+
).map((childKey) => {
|
|
123
|
+
const child = this.core.node.expectCoValueLoaded(
|
|
124
|
+
childKey.slice("child_".length) as CoID<RawGroup>,
|
|
125
|
+
"Expected child group to be loaded",
|
|
126
|
+
);
|
|
127
|
+
return expectGroup(child.getCurrentContent());
|
|
128
|
+
});
|
|
70
129
|
}
|
|
71
130
|
|
|
72
131
|
/**
|
|
@@ -75,7 +134,7 @@ export class RawGroup<
|
|
|
75
134
|
* @category 1. Role reading
|
|
76
135
|
*/
|
|
77
136
|
myRole(): Role | undefined {
|
|
78
|
-
return this.roleOfInternal(this.core.node.account.id);
|
|
137
|
+
return this.roleOfInternal(this.core.node.account.id)?.role;
|
|
79
138
|
}
|
|
80
139
|
|
|
81
140
|
/**
|
|
@@ -203,7 +262,75 @@ export class RawGroup<
|
|
|
203
262
|
"trusting",
|
|
204
263
|
);
|
|
205
264
|
|
|
265
|
+
console.log("Setting", `readKey`, "to", newReadKey.id, "in", this.id);
|
|
266
|
+
|
|
206
267
|
this.set("readKey", newReadKey.id, "trusting");
|
|
268
|
+
|
|
269
|
+
for (const parent of this.getParentGroups()) {
|
|
270
|
+
const { id: parentReadKeyID, secret: parentReadKeySecret } =
|
|
271
|
+
parent.core.getCurrentReadKey();
|
|
272
|
+
if (!parentReadKeySecret) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"Can't reveal new child key to parent where we don't have access to the parent read key",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(
|
|
279
|
+
"Setting",
|
|
280
|
+
`${newReadKey.id}_for_${parentReadKeyID}`,
|
|
281
|
+
"in",
|
|
282
|
+
this.id,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
this.set(
|
|
286
|
+
`${newReadKey.id}_for_${parentReadKeyID}`,
|
|
287
|
+
this.core.crypto.encryptKeySecret({
|
|
288
|
+
encrypting: {
|
|
289
|
+
id: parentReadKeyID,
|
|
290
|
+
secret: parentReadKeySecret,
|
|
291
|
+
},
|
|
292
|
+
toEncrypt: newReadKey,
|
|
293
|
+
}).encrypted,
|
|
294
|
+
"trusting",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const child of this.getChildGroups()) {
|
|
299
|
+
console.log("Rotating child", child.id);
|
|
300
|
+
child.rotateReadKey();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
extend(parent: RawGroup) {
|
|
305
|
+
this.set(`parent_${parent.id}`, "extend", "trusting");
|
|
306
|
+
parent.set(`child_${this.id}`, "extend", "trusting");
|
|
307
|
+
|
|
308
|
+
const { id: parentReadKeyID, secret: parentReadKeySecret } =
|
|
309
|
+
parent.core.getCurrentReadKey();
|
|
310
|
+
if (!parentReadKeySecret) {
|
|
311
|
+
throw new Error("Can't extend group without parent read key secret");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const { id: childReadKeyID, secret: childReadKeySecret } =
|
|
315
|
+
this.core.getCurrentReadKey();
|
|
316
|
+
if (!childReadKeySecret) {
|
|
317
|
+
throw new Error("Can't extend group without child read key secret");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this.set(
|
|
321
|
+
`${childReadKeyID}_for_${parentReadKeyID}`,
|
|
322
|
+
this.core.crypto.encryptKeySecret({
|
|
323
|
+
encrypting: {
|
|
324
|
+
id: parentReadKeyID,
|
|
325
|
+
secret: parentReadKeySecret,
|
|
326
|
+
},
|
|
327
|
+
toEncrypt: {
|
|
328
|
+
id: childReadKeyID,
|
|
329
|
+
secret: childReadKeySecret,
|
|
330
|
+
},
|
|
331
|
+
}).encrypted,
|
|
332
|
+
"trusting",
|
|
333
|
+
);
|
|
207
334
|
}
|
|
208
335
|
|
|
209
336
|
/**
|
|
@@ -347,6 +474,34 @@ export class RawGroup<
|
|
|
347
474
|
}
|
|
348
475
|
}
|
|
349
476
|
|
|
477
|
+
function isMorePermissiveAndShouldInherit(
|
|
478
|
+
roleInParent: Role,
|
|
479
|
+
roleInChild: Exclude<Role, "revoked"> | undefined,
|
|
480
|
+
) {
|
|
481
|
+
// invites should never be inherited
|
|
482
|
+
if (
|
|
483
|
+
roleInParent === "adminInvite" ||
|
|
484
|
+
roleInParent === "writerInvite" ||
|
|
485
|
+
roleInParent === "readerInvite"
|
|
486
|
+
) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (roleInParent === "admin") {
|
|
491
|
+
return !roleInChild || roleInChild !== "admin";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (roleInParent === "writer") {
|
|
495
|
+
return !roleInChild || roleInChild === "reader";
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (roleInParent === "reader") {
|
|
499
|
+
return !roleInChild;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
|
|
350
505
|
export type InviteSecret = `inviteSecret_z${string}`;
|
|
351
506
|
|
|
352
507
|
function inviteSecretFromSecretSeed(secretSeed: Uint8Array): InviteSecret {
|