cojson 0.8.49 → 0.9.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 +3 -3
- package/CHANGELOG.md +13 -0
- package/dist/native/coValueCore.js +3 -1
- package/dist/native/coValueCore.js.map +1 -1
- package/dist/native/coValues/coList.js +14 -7
- package/dist/native/coValues/coList.js.map +1 -1
- package/dist/native/coValues/coMap.js +11 -0
- package/dist/native/coValues/coMap.js.map +1 -1
- package/dist/native/coValues/group.js +25 -7
- package/dist/native/coValues/group.js.map +1 -1
- package/dist/native/crypto/WasmCrypto.js +130 -0
- package/dist/native/crypto/WasmCrypto.js.map +1 -0
- package/dist/native/crypto/export.js +3 -0
- package/dist/native/crypto/export.js.map +1 -0
- package/dist/native/permissions.js +15 -4
- package/dist/native/permissions.js.map +1 -1
- package/dist/web/coValueCore.js +3 -1
- package/dist/web/coValueCore.js.map +1 -1
- package/dist/web/coValues/coList.js +14 -7
- package/dist/web/coValues/coList.js.map +1 -1
- package/dist/web/coValues/coMap.js +11 -0
- package/dist/web/coValues/coMap.js.map +1 -1
- package/dist/web/coValues/group.js +25 -7
- package/dist/web/coValues/group.js.map +1 -1
- package/dist/web/crypto/export.js +3 -0
- package/dist/web/crypto/export.js.map +1 -0
- package/dist/web/permissions.js +15 -4
- package/dist/web/permissions.js.map +1 -1
- package/package.json +6 -1
- package/src/coValueCore.ts +4 -1
- package/src/coValues/coList.ts +24 -11
- package/src/coValues/coMap.ts +20 -0
- package/src/coValues/group.ts +31 -7
- package/src/crypto/export.ts +2 -0
- package/src/permissions.ts +29 -2
- package/src/tests/coList.test.ts +40 -0
- package/src/tests/coMap.test.ts +32 -0
- package/src/tests/group.test.ts +29 -0
- package/src/tests/permissions.test.ts +54 -0
package/src/coValues/coList.ts
CHANGED
|
@@ -288,6 +288,7 @@ export class RawCoListView<
|
|
|
288
288
|
) {
|
|
289
289
|
const entry =
|
|
290
290
|
this.insertions[opID.sessionID]?.[opID.txIndex]?.[opID.changeIdx];
|
|
291
|
+
|
|
291
292
|
if (!entry) {
|
|
292
293
|
throw new Error("Missing op " + opID);
|
|
293
294
|
}
|
|
@@ -412,6 +413,14 @@ export class RawCoList<
|
|
|
412
413
|
item: Item,
|
|
413
414
|
after?: number,
|
|
414
415
|
privacy: "private" | "trusting" = "private",
|
|
416
|
+
) {
|
|
417
|
+
this.appendItems([item], after, privacy);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
appendItems(
|
|
421
|
+
items: Item[],
|
|
422
|
+
after?: number,
|
|
423
|
+
privacy: "private" | "trusting" = "private",
|
|
415
424
|
) {
|
|
416
425
|
const entries = this.entries();
|
|
417
426
|
after =
|
|
@@ -420,7 +429,7 @@ export class RawCoList<
|
|
|
420
429
|
? entries.length - 1
|
|
421
430
|
: 0
|
|
422
431
|
: after;
|
|
423
|
-
let opIDBefore;
|
|
432
|
+
let opIDBefore: OpID | "start";
|
|
424
433
|
if (entries.length > 0) {
|
|
425
434
|
const entryBefore = entries[after];
|
|
426
435
|
if (!entryBefore) {
|
|
@@ -433,16 +442,20 @@ export class RawCoList<
|
|
|
433
442
|
}
|
|
434
443
|
opIDBefore = "start";
|
|
435
444
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
445
|
+
|
|
446
|
+
const changes = items.map((item) => ({
|
|
447
|
+
op: "app",
|
|
448
|
+
value: isCoValue(item) ? item.id : item,
|
|
449
|
+
after: opIDBefore,
|
|
450
|
+
}));
|
|
451
|
+
|
|
452
|
+
if (opIDBefore !== "start") {
|
|
453
|
+
// When added as successors we need to reverse the items
|
|
454
|
+
// to keep the same insertion order
|
|
455
|
+
changes.reverse();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
this.core.makeTransaction(changes, privacy);
|
|
446
459
|
|
|
447
460
|
const listAfter = new RawCoList(this.core) as this;
|
|
448
461
|
|
package/src/coValues/coMap.ts
CHANGED
|
@@ -383,6 +383,26 @@ export class RawCoMap<
|
|
|
383
383
|
this.processNewTransactions();
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
assign(
|
|
387
|
+
entries: Partial<Shape>,
|
|
388
|
+
privacy: "private" | "trusting" = "private",
|
|
389
|
+
): void {
|
|
390
|
+
if (this.isTimeTravelEntity()) {
|
|
391
|
+
throw new Error("Cannot set value on a time travel entity");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this.core.makeTransaction(
|
|
395
|
+
Object.entries(entries).map(([key, value]) => ({
|
|
396
|
+
op: "set",
|
|
397
|
+
key,
|
|
398
|
+
value: isCoValue(value) ? value.id : value,
|
|
399
|
+
})),
|
|
400
|
+
privacy,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
this.processNewTransactions();
|
|
404
|
+
}
|
|
405
|
+
|
|
386
406
|
/** Delete the given key (setting it to undefined).
|
|
387
407
|
*
|
|
388
408
|
* If `privacy` is `"private"` **(default)**, `key` is encrypted in the transaction, only readable by other members of the group this `CoMap` belongs to. Not even sync servers can see the content in plaintext.
|
package/src/coValues/group.ts
CHANGED
|
@@ -513,11 +513,39 @@ export class RawGroup<
|
|
|
513
513
|
}
|
|
514
514
|
|
|
515
515
|
for (const child of childGroups) {
|
|
516
|
+
// Since child references are mantained only for the key rotation,
|
|
517
|
+
// circular references are skipped here because it's more performant
|
|
518
|
+
// than always checking for circular references in childs inside the permission checks
|
|
519
|
+
if (child.isSelfExtension(this)) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
|
|
516
523
|
child.rotateReadKey();
|
|
517
524
|
}
|
|
518
525
|
}
|
|
519
526
|
|
|
527
|
+
/** Detect circular references in group inheritance */
|
|
528
|
+
isSelfExtension(parent: RawGroup) {
|
|
529
|
+
if (parent.id === this.id) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const childGroups = this.getChildGroups();
|
|
534
|
+
|
|
535
|
+
for (const child of childGroups) {
|
|
536
|
+
if (child.isSelfExtension(parent)) {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
|
|
520
544
|
extend(parent: RawGroup) {
|
|
545
|
+
if (this.isSelfExtension(parent)) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
521
549
|
if (this.myRole() !== "admin") {
|
|
522
550
|
throw new Error(
|
|
523
551
|
"To extend a group, the current account must be an admin in the child group",
|
|
@@ -634,9 +662,7 @@ export class RawGroup<
|
|
|
634
662
|
.getCurrentContent() as M;
|
|
635
663
|
|
|
636
664
|
if (init) {
|
|
637
|
-
|
|
638
|
-
map.set(key, value, initPrivacy);
|
|
639
|
-
}
|
|
665
|
+
map.assign(init, initPrivacy);
|
|
640
666
|
}
|
|
641
667
|
|
|
642
668
|
return map;
|
|
@@ -666,10 +692,8 @@ export class RawGroup<
|
|
|
666
692
|
})
|
|
667
693
|
.getCurrentContent() as L;
|
|
668
694
|
|
|
669
|
-
if (init) {
|
|
670
|
-
|
|
671
|
-
list.append(item, undefined, initPrivacy);
|
|
672
|
-
}
|
|
695
|
+
if (init?.length) {
|
|
696
|
+
list.appendItems(init, undefined, initPrivacy);
|
|
673
697
|
}
|
|
674
698
|
|
|
675
699
|
return list;
|
package/src/permissions.ts
CHANGED
|
@@ -137,6 +137,7 @@ function resolveMemberStateFromParentReference(
|
|
|
137
137
|
coValue: CoValueCore,
|
|
138
138
|
memberState: MemberState,
|
|
139
139
|
parentReference: ParentGroupReference,
|
|
140
|
+
extendChain: Set<CoValueCore["id"]>,
|
|
140
141
|
) {
|
|
141
142
|
const parentGroup = coValue.node.expectCoValueLoaded(
|
|
142
143
|
getParentGroupId(parentReference),
|
|
@@ -147,14 +148,21 @@ function resolveMemberStateFromParentReference(
|
|
|
147
148
|
return;
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
// Skip circular references
|
|
152
|
+
if (extendChain.has(parentGroup.id)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
150
156
|
const initialAdmin = parentGroup.header.ruleset.initialAdmin;
|
|
151
157
|
|
|
152
158
|
if (!initialAdmin) {
|
|
153
159
|
throw new Error("Group must have initialAdmin");
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
extendChain.add(parentGroup.id);
|
|
163
|
+
|
|
156
164
|
const { memberState: parentGroupMemberState } =
|
|
157
|
-
determineValidTransactionsForGroup(parentGroup, initialAdmin);
|
|
165
|
+
determineValidTransactionsForGroup(parentGroup, initialAdmin, extendChain);
|
|
158
166
|
|
|
159
167
|
for (const agent of Object.keys(parentGroupMemberState) as Array<
|
|
160
168
|
keyof MemberState
|
|
@@ -171,6 +179,7 @@ function resolveMemberStateFromParentReference(
|
|
|
171
179
|
function determineValidTransactionsForGroup(
|
|
172
180
|
coValue: CoValueCore,
|
|
173
181
|
initialAdmin: RawAccountID | AgentID,
|
|
182
|
+
extendChain?: Set<CoValueCore["id"]>,
|
|
174
183
|
): { validTransactions: ValidTransactionsResult[]; memberState: MemberState } {
|
|
175
184
|
const allTransactionsSorted: {
|
|
176
185
|
sessionID: SessionID;
|
|
@@ -305,7 +314,24 @@ function determineValidTransactionsForGroup(
|
|
|
305
314
|
logPermissionError("Only admins can set parent extensions");
|
|
306
315
|
continue;
|
|
307
316
|
}
|
|
308
|
-
|
|
317
|
+
|
|
318
|
+
extendChain = extendChain ?? new Set([]);
|
|
319
|
+
|
|
320
|
+
resolveMemberStateFromParentReference(
|
|
321
|
+
coValue,
|
|
322
|
+
memberState,
|
|
323
|
+
change.key,
|
|
324
|
+
extendChain,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// Circular reference detected, drop all the transactions involved
|
|
328
|
+
if (extendChain.has(coValue.id)) {
|
|
329
|
+
logPermissionError(
|
|
330
|
+
"Circular extend detected, dropping the transaction",
|
|
331
|
+
);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
309
335
|
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
310
336
|
continue;
|
|
311
337
|
} else if (isChildExtension(change.key)) {
|
|
@@ -320,6 +346,7 @@ function determineValidTransactionsForGroup(
|
|
|
320
346
|
);
|
|
321
347
|
continue;
|
|
322
348
|
}
|
|
349
|
+
|
|
323
350
|
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
324
351
|
continue;
|
|
325
352
|
} else if (isWriteKeyForMember(change.key)) {
|
package/src/tests/coList.test.ts
CHANGED
|
@@ -75,6 +75,26 @@ test("Push is equivalent to append after last item", () => {
|
|
|
75
75
|
expect(content.toJSON()).toEqual(["hello", "world", "hooray"]);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
test("appendItems add an array of items at the end of the list", () => {
|
|
79
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
|
80
|
+
|
|
81
|
+
const coValue = node.createCoValue({
|
|
82
|
+
type: "colist",
|
|
83
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
84
|
+
meta: null,
|
|
85
|
+
...Crypto.createdNowUnique(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const content = expectList(coValue.getCurrentContent());
|
|
89
|
+
|
|
90
|
+
expect(content.type).toEqual("colist");
|
|
91
|
+
|
|
92
|
+
content.append("hello", 0, "trusting");
|
|
93
|
+
expect(content.toJSON()).toEqual(["hello"]);
|
|
94
|
+
content.appendItems(["world", "hooray", "universe"], undefined, "trusting");
|
|
95
|
+
expect(content.toJSON()).toEqual(["hello", "world", "hooray", "universe"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
78
98
|
test("Can push into empty list", () => {
|
|
79
99
|
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
|
80
100
|
|
|
@@ -92,3 +112,23 @@ test("Can push into empty list", () => {
|
|
|
92
112
|
content.append("hello", undefined, "trusting");
|
|
93
113
|
expect(content.toJSON()).toEqual(["hello"]);
|
|
94
114
|
});
|
|
115
|
+
|
|
116
|
+
test("init the list correctly", () => {
|
|
117
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
|
118
|
+
|
|
119
|
+
const group = node.createGroup();
|
|
120
|
+
|
|
121
|
+
const content = group.createList(["hello", "world", "hooray", "universe"]);
|
|
122
|
+
|
|
123
|
+
expect(content.type).toEqual("colist");
|
|
124
|
+
expect(content.toJSON()).toEqual(["hello", "world", "hooray", "universe"]);
|
|
125
|
+
|
|
126
|
+
content.append("hello", content.toJSON().length - 1, "trusting");
|
|
127
|
+
expect(content.toJSON()).toEqual([
|
|
128
|
+
"hello",
|
|
129
|
+
"world",
|
|
130
|
+
"hooray",
|
|
131
|
+
"universe",
|
|
132
|
+
"hello",
|
|
133
|
+
]);
|
|
134
|
+
});
|
package/src/tests/coMap.test.ts
CHANGED
|
@@ -175,3 +175,35 @@ test("Can get last tx ID for a key in CoMap", () => {
|
|
|
175
175
|
content.set("hello", "C", "trusting");
|
|
176
176
|
expect(content.lastEditAt("hello")?.tx.txIndex).toEqual(2);
|
|
177
177
|
});
|
|
178
|
+
|
|
179
|
+
test("Can set items in bulk with assign", () => {
|
|
180
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
|
181
|
+
|
|
182
|
+
const coValue = node.createCoValue({
|
|
183
|
+
type: "comap",
|
|
184
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
185
|
+
meta: null,
|
|
186
|
+
...Crypto.createdNowUnique(),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const content = expectMap(coValue.getCurrentContent());
|
|
190
|
+
|
|
191
|
+
expect(content.type).toEqual("comap");
|
|
192
|
+
|
|
193
|
+
content.set("key1", "set1", "trusting");
|
|
194
|
+
|
|
195
|
+
content.assign(
|
|
196
|
+
{
|
|
197
|
+
key1: "assign1",
|
|
198
|
+
key2: "assign2",
|
|
199
|
+
key3: "assign3",
|
|
200
|
+
},
|
|
201
|
+
"trusting",
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(content.toJSON()).toEqual({
|
|
205
|
+
key1: "assign1",
|
|
206
|
+
key2: "assign2",
|
|
207
|
+
key3: "assign3",
|
|
208
|
+
});
|
|
209
|
+
});
|
package/src/tests/group.test.ts
CHANGED
|
@@ -609,4 +609,33 @@ describe("writeOnly", () => {
|
|
|
609
609
|
|
|
610
610
|
expect(mapOnNode2.get("test")).toEqual("Written from node2");
|
|
611
611
|
});
|
|
612
|
+
|
|
613
|
+
test("self-extend a group should not break anything", async () => {
|
|
614
|
+
const { node1 } = await createTwoConnectedNodes("server", "server");
|
|
615
|
+
|
|
616
|
+
const group = node1.node.createGroup();
|
|
617
|
+
group.extend(group);
|
|
618
|
+
|
|
619
|
+
const map = group.createMap();
|
|
620
|
+
map.set("test", "Hello!");
|
|
621
|
+
|
|
622
|
+
expect(map.get("test")).toEqual("Hello!");
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("should not break when introducing extend cycles", async () => {
|
|
626
|
+
const { node1 } = await createTwoConnectedNodes("server", "server");
|
|
627
|
+
|
|
628
|
+
const group = node1.node.createGroup();
|
|
629
|
+
const group2 = node1.node.createGroup();
|
|
630
|
+
const group3 = node1.node.createGroup();
|
|
631
|
+
|
|
632
|
+
group.extend(group2);
|
|
633
|
+
group2.extend(group3);
|
|
634
|
+
group3.extend(group);
|
|
635
|
+
|
|
636
|
+
const map = group.createMap();
|
|
637
|
+
map.set("test", "Hello!");
|
|
638
|
+
|
|
639
|
+
expect(map.get("test")).toEqual("Hello!");
|
|
640
|
+
});
|
|
612
641
|
});
|
|
@@ -2854,3 +2854,57 @@ test("High-level permissions work correctly when a group is extended", async ()
|
|
|
2854
2854
|
|
|
2855
2855
|
expect(mapAsReaderAfterRemove.get("foo")).not.toEqual("baz");
|
|
2856
2856
|
});
|
|
2857
|
+
|
|
2858
|
+
test("self-extensions should not break the permissions checks", () => {
|
|
2859
|
+
const { group } = newGroupHighLevel();
|
|
2860
|
+
|
|
2861
|
+
group.set(`child_${group.id}`, "extend", "trusting");
|
|
2862
|
+
group.set(`parent_${group.id}`, "extend", "trusting");
|
|
2863
|
+
|
|
2864
|
+
const map = group.createMap();
|
|
2865
|
+
map.set("test", "Hello!");
|
|
2866
|
+
|
|
2867
|
+
expect(map.get("test")).toEqual("Hello!");
|
|
2868
|
+
});
|
|
2869
|
+
|
|
2870
|
+
test("extend cycles should not break the permissions checks", () => {
|
|
2871
|
+
const { group, node } = newGroupHighLevel();
|
|
2872
|
+
|
|
2873
|
+
const group2 = node.createGroup();
|
|
2874
|
+
const group3 = node.createGroup();
|
|
2875
|
+
|
|
2876
|
+
group.set(`child_${group2.id}`, "extend", "trusting");
|
|
2877
|
+
group2.set(`child_${group3.id}`, "extend", "trusting");
|
|
2878
|
+
group3.set(`child_${group.id}`, "extend", "trusting");
|
|
2879
|
+
|
|
2880
|
+
group.set(`parent_${group2.id}`, "extend", "trusting");
|
|
2881
|
+
group2.set(`parent_${group3.id}`, "extend", "trusting");
|
|
2882
|
+
group3.set(`parent_${group.id}`, "extend", "trusting");
|
|
2883
|
+
|
|
2884
|
+
const map = group.createMap();
|
|
2885
|
+
map.set("test", "Hello!");
|
|
2886
|
+
|
|
2887
|
+
expect(map.get("test")).toEqual("Hello!");
|
|
2888
|
+
});
|
|
2889
|
+
|
|
2890
|
+
test("extend cycles should not break the keys rotation", () => {
|
|
2891
|
+
const { group, node } = newGroupHighLevel();
|
|
2892
|
+
|
|
2893
|
+
const group2 = node.createGroup();
|
|
2894
|
+
const group3 = node.createGroup();
|
|
2895
|
+
|
|
2896
|
+
group.set(`child_${group2.id}`, "extend", "trusting");
|
|
2897
|
+
group2.set(`child_${group3.id}`, "extend", "trusting");
|
|
2898
|
+
group3.set(`child_${group.id}`, "extend", "trusting");
|
|
2899
|
+
|
|
2900
|
+
group.set(`parent_${group2.id}`, "extend", "trusting");
|
|
2901
|
+
group2.set(`parent_${group3.id}`, "extend", "trusting");
|
|
2902
|
+
group3.set(`parent_${group.id}`, "extend", "trusting");
|
|
2903
|
+
|
|
2904
|
+
group.rotateReadKey();
|
|
2905
|
+
|
|
2906
|
+
const map = group.createMap();
|
|
2907
|
+
map.set("test", "Hello!");
|
|
2908
|
+
|
|
2909
|
+
expect(map.get("test")).toEqual("Hello!");
|
|
2910
|
+
});
|