cojson 0.0.2 → 0.0.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/package.json +2 -1
- package/src/coValue.test.ts +4 -1
- package/src/coValue.ts +94 -17
- package/src/contentType.test.ts +6 -0
- package/src/contentType.ts +6 -221
- package/src/contentTypes/coList.ts +24 -0
- package/src/contentTypes/coMap.ts +195 -0
- package/src/contentTypes/coStream.ts +24 -0
- package/src/contentTypes/static.ts +22 -0
- package/src/crypto.ts +29 -1
- package/src/ids.ts +7 -0
- package/src/index.ts +24 -10
- package/src/node.ts +8 -4
- package/src/permissions.test.ts +32 -0
- package/src/permissions.ts +5 -6
- package/src/sync.test.ts +5 -4
- package/src/sync.ts +12 -31
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cojson",
|
|
3
3
|
"module": "src/index.ts",
|
|
4
|
+
"types": "src/index.ts",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"license": "MIT",
|
|
6
|
-
"version": "0.0.
|
|
7
|
+
"version": "0.0.3",
|
|
7
8
|
"devDependencies": {
|
|
8
9
|
"@types/jest": "^29.5.3",
|
|
9
10
|
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
package/src/coValue.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
newRandomSessionID,
|
|
8
8
|
} from "./coValue";
|
|
9
9
|
import { LocalNode } from "./node";
|
|
10
|
-
import { sign } from "./crypto";
|
|
10
|
+
import { createdNowUnique, sign, uniquenessForHeader } from "./crypto";
|
|
11
11
|
|
|
12
12
|
test("Can create coValue with new agent credentials and add transaction to it", () => {
|
|
13
13
|
const agentCredential = newRandomAgentCredential("agent1");
|
|
@@ -20,6 +20,7 @@ test("Can create coValue with new agent credentials and add transaction to it",
|
|
|
20
20
|
type: "costream",
|
|
21
21
|
ruleset: { type: "unsafeAllowAll" },
|
|
22
22
|
meta: null,
|
|
23
|
+
...createdNowUnique()
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
const transaction: Transaction = {
|
|
@@ -59,6 +60,7 @@ test("transactions with wrong signature are rejected", () => {
|
|
|
59
60
|
type: "costream",
|
|
60
61
|
ruleset: { type: "unsafeAllowAll" },
|
|
61
62
|
meta: null,
|
|
63
|
+
...createdNowUnique()
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
const transaction: Transaction = {
|
|
@@ -97,6 +99,7 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
|
|
|
97
99
|
type: "costream",
|
|
98
100
|
ruleset: { type: "unsafeAllowAll" },
|
|
99
101
|
meta: null,
|
|
102
|
+
...createdNowUnique()
|
|
100
103
|
});
|
|
101
104
|
|
|
102
105
|
const transaction: Transaction = {
|
package/src/coValue.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "@noble/hashes/utils";
|
|
2
|
-
import {
|
|
2
|
+
import { ContentType } from "./contentType";
|
|
3
|
+
import { Static } from "./contentTypes/static";
|
|
4
|
+
import { CoStream } from "./contentTypes/coStream";
|
|
5
|
+
import { CoMap } from "./contentTypes/coMap";
|
|
3
6
|
import {
|
|
4
7
|
Encrypted,
|
|
5
8
|
Hash,
|
|
@@ -22,23 +25,30 @@ import {
|
|
|
22
25
|
decryptForTransaction,
|
|
23
26
|
KeyID,
|
|
24
27
|
unsealKeySecret,
|
|
28
|
+
signatorySecretToBytes,
|
|
29
|
+
recipientSecretToBytes,
|
|
30
|
+
signatorySecretFromBytes,
|
|
31
|
+
recipientSecretFromBytes,
|
|
25
32
|
} from "./crypto";
|
|
26
33
|
import { JsonValue } from "./jsonValue";
|
|
27
34
|
import { base58 } from "@scure/base";
|
|
28
35
|
import {
|
|
29
36
|
PermissionsDef as RulesetDef,
|
|
37
|
+
Team,
|
|
30
38
|
determineValidTransactions,
|
|
31
39
|
expectTeamContent,
|
|
32
40
|
} from "./permissions";
|
|
33
41
|
import { LocalNode } from "./node";
|
|
34
42
|
import { CoValueKnownState, NewContentMessage } from "./sync";
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
import { AgentID, RawCoValueID, SessionID, TransactionID } from "./ids";
|
|
44
|
+
import { CoList } from "./contentTypes/coList";
|
|
37
45
|
|
|
38
46
|
export type CoValueHeader = {
|
|
39
47
|
type: ContentType["type"];
|
|
40
48
|
ruleset: RulesetDef;
|
|
41
49
|
meta: JsonValue;
|
|
50
|
+
createdAt: `2${string}` | null;
|
|
51
|
+
uniqueness: `z${string}` | null;
|
|
42
52
|
publicNickname?: string;
|
|
43
53
|
};
|
|
44
54
|
|
|
@@ -53,8 +63,6 @@ function coValueIDforHeader(header: CoValueHeader): RawCoValueID {
|
|
|
53
63
|
}
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
export type SessionID = `${AgentID}_session_z${string}`;
|
|
57
|
-
|
|
58
66
|
export function agentIDfromSessionID(sessionID: SessionID): AgentID {
|
|
59
67
|
return sessionID.split("_session")[0] as AgentID;
|
|
60
68
|
}
|
|
@@ -94,14 +102,13 @@ export type DecryptedTransaction = {
|
|
|
94
102
|
madeAt: number;
|
|
95
103
|
};
|
|
96
104
|
|
|
97
|
-
export type TransactionID = { sessionID: SessionID; txIndex: number };
|
|
98
|
-
|
|
99
105
|
export class CoValue {
|
|
100
106
|
id: RawCoValueID;
|
|
101
107
|
node: LocalNode;
|
|
102
108
|
header: CoValueHeader;
|
|
103
109
|
sessions: { [key: SessionID]: SessionLog };
|
|
104
110
|
content?: ContentType;
|
|
111
|
+
listeners: Set<(content?: ContentType) => void> = new Set();
|
|
105
112
|
|
|
106
113
|
constructor(header: CoValueHeader, node: LocalNode) {
|
|
107
114
|
this.id = coValueIDforHeader(header);
|
|
@@ -185,6 +192,8 @@ export class CoValue {
|
|
|
185
192
|
|
|
186
193
|
const transactions = this.sessions[sessionID]?.transactions ?? [];
|
|
187
194
|
|
|
195
|
+
console.log("transactions before", this.id, transactions.length, this.getValidSortedTransactions().length);
|
|
196
|
+
|
|
188
197
|
transactions.push(...newTransactions);
|
|
189
198
|
|
|
190
199
|
this.sessions[sessionID] = {
|
|
@@ -196,11 +205,28 @@ export class CoValue {
|
|
|
196
205
|
|
|
197
206
|
this.content = undefined;
|
|
198
207
|
|
|
199
|
-
|
|
208
|
+
console.log("transactions after", this.id, transactions.length, this.getValidSortedTransactions().length);
|
|
209
|
+
|
|
210
|
+
const content = this.getCurrentContent();
|
|
211
|
+
|
|
212
|
+
for (const listener of this.listeners) {
|
|
213
|
+
console.log("Calling listener (update)", this.id, content.toJSON());
|
|
214
|
+
listener(content);
|
|
215
|
+
}
|
|
200
216
|
|
|
201
217
|
return true;
|
|
202
218
|
}
|
|
203
219
|
|
|
220
|
+
subscribe(listener: (content?: ContentType) => void): () => void {
|
|
221
|
+
this.listeners.add(listener);
|
|
222
|
+
console.log("Calling listener (initial)", this.id, this.getCurrentContent().toJSON());
|
|
223
|
+
listener(this.getCurrentContent());
|
|
224
|
+
|
|
225
|
+
return () => {
|
|
226
|
+
this.listeners.delete(listener);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
204
230
|
expectedNewHashAfter(
|
|
205
231
|
sessionID: SessionID,
|
|
206
232
|
newTransactions: Transaction[]
|
|
@@ -232,7 +258,9 @@ export class CoValue {
|
|
|
232
258
|
const { secret: keySecret, id: keyID } = this.getCurrentReadKey();
|
|
233
259
|
|
|
234
260
|
if (!keySecret) {
|
|
235
|
-
throw new Error(
|
|
261
|
+
throw new Error(
|
|
262
|
+
"Can't make transaction without read key secret"
|
|
263
|
+
);
|
|
236
264
|
}
|
|
237
265
|
|
|
238
266
|
transaction = {
|
|
@@ -300,8 +328,8 @@ export class CoValue {
|
|
|
300
328
|
getValidSortedTransactions(): DecryptedTransaction[] {
|
|
301
329
|
const validTransactions = determineValidTransactions(this);
|
|
302
330
|
|
|
303
|
-
const allTransactions: DecryptedTransaction[] = validTransactions
|
|
304
|
-
({ txID, tx }) => {
|
|
331
|
+
const allTransactions: DecryptedTransaction[] = validTransactions
|
|
332
|
+
.map(({ txID, tx }) => {
|
|
305
333
|
if (tx.privacy === "trusting") {
|
|
306
334
|
return {
|
|
307
335
|
txID,
|
|
@@ -324,7 +352,9 @@ export class CoValue {
|
|
|
324
352
|
);
|
|
325
353
|
|
|
326
354
|
if (!decrytedChanges) {
|
|
327
|
-
console.error(
|
|
355
|
+
console.error(
|
|
356
|
+
"Failed to decrypt transaction despite having key"
|
|
357
|
+
);
|
|
328
358
|
return undefined;
|
|
329
359
|
}
|
|
330
360
|
return {
|
|
@@ -334,8 +364,8 @@ export class CoValue {
|
|
|
334
364
|
};
|
|
335
365
|
}
|
|
336
366
|
}
|
|
337
|
-
}
|
|
338
|
-
|
|
367
|
+
})
|
|
368
|
+
.filter((x): x is Exclude<typeof x, undefined> => !!x);
|
|
339
369
|
allTransactions.sort(
|
|
340
370
|
(a, b) =>
|
|
341
371
|
a.madeAt - b.madeAt ||
|
|
@@ -446,6 +476,21 @@ export class CoValue {
|
|
|
446
476
|
}
|
|
447
477
|
}
|
|
448
478
|
|
|
479
|
+
getTeam(): Team {
|
|
480
|
+
if (this.header.ruleset.type !== "ownedByTeam") {
|
|
481
|
+
throw new Error("Only values owned by teams have teams");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return new Team(
|
|
485
|
+
expectTeamContent(
|
|
486
|
+
this.node
|
|
487
|
+
.expectCoValueLoaded(this.header.ruleset.team)
|
|
488
|
+
.getCurrentContent()
|
|
489
|
+
),
|
|
490
|
+
this.node
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
449
494
|
getTx(txID: TransactionID): Transaction | undefined {
|
|
450
495
|
return this.sessions[txID.sessionID]?.transactions[txID.txIndex];
|
|
451
496
|
}
|
|
@@ -510,8 +555,6 @@ export class CoValue {
|
|
|
510
555
|
}
|
|
511
556
|
}
|
|
512
557
|
|
|
513
|
-
export type AgentID = `co_agent${string}_z${string}`;
|
|
514
|
-
|
|
515
558
|
export type Agent = {
|
|
516
559
|
signatoryID: SignatoryID;
|
|
517
560
|
recipientID: RecipientID;
|
|
@@ -535,6 +578,8 @@ export function getAgentCoValueHeader(agent: Agent): CoValueHeader {
|
|
|
535
578
|
initialRecipientID: agent.recipientID,
|
|
536
579
|
},
|
|
537
580
|
meta: null,
|
|
581
|
+
createdAt: null,
|
|
582
|
+
uniqueness: null,
|
|
538
583
|
publicNickname:
|
|
539
584
|
"agent" + (agent.publicNickname ? `-${agent.publicNickname}` : ""),
|
|
540
585
|
};
|
|
@@ -551,13 +596,45 @@ export type AgentCredential = {
|
|
|
551
596
|
};
|
|
552
597
|
|
|
553
598
|
export function newRandomAgentCredential(
|
|
554
|
-
publicNickname
|
|
599
|
+
publicNickname?: string
|
|
555
600
|
): AgentCredential {
|
|
556
601
|
const signatorySecret = newRandomSignatory();
|
|
557
602
|
const recipientSecret = newRandomRecipient();
|
|
558
603
|
return { signatorySecret, recipientSecret, publicNickname };
|
|
559
604
|
}
|
|
560
605
|
|
|
606
|
+
export function agentCredentialToBytes(cred: AgentCredential): Uint8Array {
|
|
607
|
+
if (cred.publicNickname) {
|
|
608
|
+
throw new Error("Can't convert agent credential with publicNickname");
|
|
609
|
+
}
|
|
610
|
+
const bytes = new Uint8Array(64);
|
|
611
|
+
const signatorySecretBytes = signatorySecretToBytes(cred.signatorySecret);
|
|
612
|
+
if (signatorySecretBytes.length !== 32) {
|
|
613
|
+
throw new Error("Invalid signatorySecret length");
|
|
614
|
+
}
|
|
615
|
+
bytes.set(signatorySecretBytes);
|
|
616
|
+
const recipientSecretBytes = recipientSecretToBytes(cred.recipientSecret);
|
|
617
|
+
if (recipientSecretBytes.length !== 32) {
|
|
618
|
+
throw new Error("Invalid recipientSecret length");
|
|
619
|
+
}
|
|
620
|
+
bytes.set(recipientSecretBytes, 32);
|
|
621
|
+
|
|
622
|
+
return bytes;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function agentCredentialFromBytes(
|
|
626
|
+
bytes: Uint8Array
|
|
627
|
+
): AgentCredential | undefined {
|
|
628
|
+
if (bytes.length !== 64) {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const signatorySecret = signatorySecretFromBytes(bytes.slice(0, 32));
|
|
633
|
+
const recipientSecret = recipientSecretFromBytes(bytes.slice(32));
|
|
634
|
+
|
|
635
|
+
return { signatorySecret, recipientSecret };
|
|
636
|
+
}
|
|
637
|
+
|
|
561
638
|
// type Role = "admin" | "writer" | "reader";
|
|
562
639
|
|
|
563
640
|
// type PermissionsDef = CJMap<AgentID, Role, {[agent: AgentID]: Role}>;
|
package/src/contentType.test.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
newRandomAgentCredential,
|
|
6
6
|
newRandomSessionID,
|
|
7
7
|
} from "./coValue";
|
|
8
|
+
import { createdNowUnique } from "./crypto";
|
|
8
9
|
import { LocalNode } from "./node";
|
|
9
10
|
|
|
10
11
|
test("Empty COJSON Map works", () => {
|
|
@@ -18,6 +19,7 @@ test("Empty COJSON Map works", () => {
|
|
|
18
19
|
type: "comap",
|
|
19
20
|
ruleset: { type: "unsafeAllowAll" },
|
|
20
21
|
meta: null,
|
|
22
|
+
...createdNowUnique()
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
const content = coValue.getCurrentContent();
|
|
@@ -42,6 +44,7 @@ test("Can insert and delete Map entries in edit()", () => {
|
|
|
42
44
|
type: "comap",
|
|
43
45
|
ruleset: { type: "unsafeAllowAll" },
|
|
44
46
|
meta: null,
|
|
47
|
+
...createdNowUnique()
|
|
45
48
|
});
|
|
46
49
|
|
|
47
50
|
const content = coValue.getCurrentContent();
|
|
@@ -74,6 +77,7 @@ test("Can get map entry values at different points in time", () => {
|
|
|
74
77
|
type: "comap",
|
|
75
78
|
ruleset: { type: "unsafeAllowAll" },
|
|
76
79
|
meta: null,
|
|
80
|
+
...createdNowUnique()
|
|
77
81
|
});
|
|
78
82
|
|
|
79
83
|
const content = coValue.getCurrentContent();
|
|
@@ -113,6 +117,7 @@ test("Can get all historic values of key", () => {
|
|
|
113
117
|
type: "comap",
|
|
114
118
|
ruleset: { type: "unsafeAllowAll" },
|
|
115
119
|
meta: null,
|
|
120
|
+
...createdNowUnique()
|
|
116
121
|
});
|
|
117
122
|
|
|
118
123
|
const content = coValue.getCurrentContent();
|
|
@@ -170,6 +175,7 @@ test("Can get last tx ID for a key", () => {
|
|
|
170
175
|
type: "comap",
|
|
171
176
|
ruleset: { type: "unsafeAllowAll" },
|
|
172
177
|
meta: null,
|
|
178
|
+
...createdNowUnique()
|
|
173
179
|
});
|
|
174
180
|
|
|
175
181
|
const content = coValue.getCurrentContent();
|
package/src/contentType.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { JsonValue } from "./jsonValue";
|
|
2
|
+
import { RawCoValueID } from "./ids";
|
|
3
|
+
import { CoMap } from "./contentTypes/coMap";
|
|
4
|
+
import { CoStream } from "./contentTypes/coStream";
|
|
5
|
+
import { Static } from "./contentTypes/static";
|
|
6
|
+
import { CoList } from "./contentTypes/coList";
|
|
3
7
|
|
|
4
8
|
export type CoValueID<T extends ContentType> = RawCoValueID & {
|
|
5
9
|
readonly __type: T;
|
|
@@ -11,225 +15,6 @@ export type ContentType =
|
|
|
11
15
|
| CoStream<JsonValue, JsonValue>
|
|
12
16
|
| Static<JsonValue>;
|
|
13
17
|
|
|
14
|
-
type MapOp<K extends string, V extends JsonValue> = {
|
|
15
|
-
txID: TransactionID;
|
|
16
|
-
madeAt: number;
|
|
17
|
-
changeIdx: number;
|
|
18
|
-
} & MapOpPayload<K, V>;
|
|
19
|
-
|
|
20
|
-
// TODO: add after TransactionID[] for conflicts/ordering
|
|
21
|
-
export type MapOpPayload<K extends string, V extends JsonValue> =
|
|
22
|
-
| {
|
|
23
|
-
op: "insert";
|
|
24
|
-
key: K;
|
|
25
|
-
value: V;
|
|
26
|
-
}
|
|
27
|
-
| {
|
|
28
|
-
op: "delete";
|
|
29
|
-
key: K;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export class CoMap<
|
|
33
|
-
M extends {[key: string]: JsonValue},
|
|
34
|
-
Meta extends JsonValue,
|
|
35
|
-
K extends string = keyof M & string,
|
|
36
|
-
V extends JsonValue = M[K],
|
|
37
|
-
MM extends {[key: string]: JsonValue} = {[KK in K]: M[KK]}
|
|
38
|
-
> {
|
|
39
|
-
id: CoValueID<CoMap<MM, Meta>>;
|
|
40
|
-
coValue: CoValue;
|
|
41
|
-
type: "comap" = "comap";
|
|
42
|
-
ops: {[KK in K]?: MapOp<K, M[KK]>[]};
|
|
43
|
-
|
|
44
|
-
constructor(coValue: CoValue) {
|
|
45
|
-
this.id = coValue.id as CoValueID<CoMap<MM, Meta>>;
|
|
46
|
-
this.coValue = coValue;
|
|
47
|
-
this.ops = {};
|
|
48
|
-
|
|
49
|
-
this.fillOpsFromCoValue();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
protected fillOpsFromCoValue() {
|
|
53
|
-
this.ops = {};
|
|
54
|
-
|
|
55
|
-
for (const { txID, changes, madeAt } of this.coValue.getValidSortedTransactions()) {
|
|
56
|
-
for (const [changeIdx, changeUntyped] of (
|
|
57
|
-
changes
|
|
58
|
-
).entries()) {
|
|
59
|
-
const change = changeUntyped as MapOpPayload<K, V>
|
|
60
|
-
let entries = this.ops[change.key];
|
|
61
|
-
if (!entries) {
|
|
62
|
-
entries = [];
|
|
63
|
-
this.ops[change.key] = entries;
|
|
64
|
-
}
|
|
65
|
-
entries.push({
|
|
66
|
-
txID,
|
|
67
|
-
madeAt,
|
|
68
|
-
changeIdx,
|
|
69
|
-
...(change as any),
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
keys(): K[] {
|
|
76
|
-
return Object.keys(this.ops) as K[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
get<KK extends K>(key: KK): M[KK] | undefined {
|
|
80
|
-
const ops = this.ops[key];
|
|
81
|
-
if (!ops) {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const lastEntry = ops[ops.length - 1]!;
|
|
86
|
-
|
|
87
|
-
if (lastEntry.op === "delete") {
|
|
88
|
-
return undefined;
|
|
89
|
-
} else {
|
|
90
|
-
return lastEntry.value;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
getAtTime<KK extends K>(key: KK, time: number): M[KK] | undefined {
|
|
95
|
-
const ops = this.ops[key];
|
|
96
|
-
if (!ops) {
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const lastOpBeforeOrAtTime = ops.findLast((op) => op.madeAt <= time);
|
|
101
|
-
|
|
102
|
-
if (!lastOpBeforeOrAtTime) {
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (lastOpBeforeOrAtTime.op === "delete") {
|
|
107
|
-
return undefined;
|
|
108
|
-
} else {
|
|
109
|
-
return lastOpBeforeOrAtTime.value;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
getLastTxID<KK extends K>(key: KK): TransactionID | undefined {
|
|
114
|
-
const ops = this.ops[key];
|
|
115
|
-
if (!ops) {
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const lastEntry = ops[ops.length - 1]!;
|
|
120
|
-
|
|
121
|
-
return lastEntry.txID;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
getHistory<KK extends K>(key: KK): {at: number, txID: TransactionID, value: M[KK] | undefined}[] {
|
|
125
|
-
const ops = this.ops[key];
|
|
126
|
-
if (!ops) {
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const history: {at: number, txID: TransactionID, value: M[KK] | undefined}[] = [];
|
|
131
|
-
|
|
132
|
-
for (const op of ops) {
|
|
133
|
-
if (op.op === "delete") {
|
|
134
|
-
history.push({at: op.madeAt, txID: op.txID, value: undefined});
|
|
135
|
-
} else {
|
|
136
|
-
history.push({at: op.madeAt, txID: op.txID, value: op.value});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return history;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
toJSON(): JsonObject {
|
|
144
|
-
const json: JsonObject = {};
|
|
145
|
-
|
|
146
|
-
for (const key of this.keys()) {
|
|
147
|
-
const value = this.get(key);
|
|
148
|
-
if (value !== undefined) {
|
|
149
|
-
json[key] = value;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return json;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
edit(changer: (editable: WriteableCoMap<M, Meta>) => void): CoMap<M, Meta> {
|
|
157
|
-
const editable = new WriteableCoMap<M, Meta>(this.coValue);
|
|
158
|
-
changer(editable);
|
|
159
|
-
return new CoMap(this.coValue);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export class WriteableCoMap<
|
|
164
|
-
M extends {[key: string]: JsonValue},
|
|
165
|
-
Meta extends JsonValue,
|
|
166
|
-
K extends string = keyof M & string,
|
|
167
|
-
V extends JsonValue = M[K],
|
|
168
|
-
MM extends {[key: string]: JsonValue} = {[KK in K]: M[KK]}
|
|
169
|
-
> extends CoMap<M, Meta, K, V, MM> {
|
|
170
|
-
set<KK extends K>(key: KK, value: M[KK], privacy: "private" | "trusting" = "private"): void {
|
|
171
|
-
this.coValue.makeTransaction([
|
|
172
|
-
{
|
|
173
|
-
op: "insert",
|
|
174
|
-
key,
|
|
175
|
-
value,
|
|
176
|
-
},
|
|
177
|
-
], privacy);
|
|
178
|
-
|
|
179
|
-
this.fillOpsFromCoValue();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
delete(key: K, privacy: "private" | "trusting" = "private"): void {
|
|
183
|
-
this.coValue.makeTransaction([
|
|
184
|
-
{
|
|
185
|
-
op: "delete",
|
|
186
|
-
key,
|
|
187
|
-
},
|
|
188
|
-
], privacy);
|
|
189
|
-
|
|
190
|
-
this.fillOpsFromCoValue();
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export class CoList<T extends JsonValue, Meta extends JsonValue> {
|
|
195
|
-
id: CoValueID<CoList<T, Meta>>;
|
|
196
|
-
type: "colist" = "colist";
|
|
197
|
-
|
|
198
|
-
constructor(coValue: CoValue) {
|
|
199
|
-
this.id = coValue.id as CoValueID<CoList<T, Meta>>;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
toJSON(): JsonObject {
|
|
203
|
-
throw new Error("Method not implemented.");
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export class CoStream<T extends JsonValue, Meta extends JsonValue> {
|
|
208
|
-
id: CoValueID<CoStream<T, Meta>>;
|
|
209
|
-
type: "costream" = "costream";
|
|
210
|
-
|
|
211
|
-
constructor(coValue: CoValue) {
|
|
212
|
-
this.id = coValue.id as CoValueID<CoStream<T, Meta>>;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
toJSON(): JsonObject {
|
|
216
|
-
throw new Error("Method not implemented.");
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export class Static<T extends JsonValue> {
|
|
221
|
-
id: CoValueID<Static<T>>;
|
|
222
|
-
type: "static" = "static";
|
|
223
|
-
|
|
224
|
-
constructor(coValue: CoValue) {
|
|
225
|
-
this.id = coValue.id as CoValueID<Static<T>>;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
toJSON(): JsonObject {
|
|
229
|
-
throw new Error("Method not implemented.");
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
18
|
export function expectMap(content: ContentType): CoMap<{ [key: string]: string }, {}> {
|
|
234
19
|
if (content.type !== "comap") {
|
|
235
20
|
throw new Error("Expected map");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { JsonObject, JsonValue } from "../jsonValue";
|
|
2
|
+
import { CoValueID } from "../contentType";
|
|
3
|
+
import { CoValue } from "../coValue";
|
|
4
|
+
|
|
5
|
+
export class CoList<T extends JsonValue, Meta extends JsonValue> {
|
|
6
|
+
id: CoValueID<CoList<T, Meta>>;
|
|
7
|
+
type: "colist" = "colist";
|
|
8
|
+
coValue: CoValue;
|
|
9
|
+
|
|
10
|
+
constructor(coValue: CoValue) {
|
|
11
|
+
this.id = coValue.id as CoValueID<CoList<T, Meta>>;
|
|
12
|
+
this.coValue = coValue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
toJSON(): JsonObject {
|
|
16
|
+
throw new Error("Method not implemented.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
subscribe(listener: (coMap: CoList<T, Meta>) => void): () => void {
|
|
20
|
+
return this.coValue.subscribe((content) => {
|
|
21
|
+
listener(content as CoList<T, Meta>);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { JsonObject, JsonValue } from "../jsonValue";
|
|
2
|
+
import { TransactionID } from "../ids";
|
|
3
|
+
import { CoValueID } from "../contentType";
|
|
4
|
+
import { CoValue } from "../coValue";
|
|
5
|
+
|
|
6
|
+
type MapOp<K extends string, V extends JsonValue> = {
|
|
7
|
+
txID: TransactionID;
|
|
8
|
+
madeAt: number;
|
|
9
|
+
changeIdx: number;
|
|
10
|
+
} & MapOpPayload<K, V>;
|
|
11
|
+
// TODO: add after TransactionID[] for conflicts/ordering
|
|
12
|
+
|
|
13
|
+
export type MapOpPayload<K extends string, V extends JsonValue> = {
|
|
14
|
+
op: "insert";
|
|
15
|
+
key: K;
|
|
16
|
+
value: V;
|
|
17
|
+
} |
|
|
18
|
+
{
|
|
19
|
+
op: "delete";
|
|
20
|
+
key: K;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class CoMap<
|
|
24
|
+
M extends { [key: string]: JsonValue; },
|
|
25
|
+
Meta extends JsonValue,
|
|
26
|
+
K extends string = keyof M & string,
|
|
27
|
+
V extends JsonValue = M[K],
|
|
28
|
+
MM extends { [key: string]: JsonValue; } = {
|
|
29
|
+
[KK in K]: M[KK];
|
|
30
|
+
}
|
|
31
|
+
> {
|
|
32
|
+
id: CoValueID<CoMap<MM, Meta>>;
|
|
33
|
+
coValue: CoValue;
|
|
34
|
+
type: "comap" = "comap";
|
|
35
|
+
ops: {
|
|
36
|
+
[KK in K]?: MapOp<K, M[KK]>[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
constructor(coValue: CoValue) {
|
|
40
|
+
this.id = coValue.id as CoValueID<CoMap<MM, Meta>>;
|
|
41
|
+
this.coValue = coValue;
|
|
42
|
+
this.ops = {};
|
|
43
|
+
|
|
44
|
+
this.fillOpsFromCoValue();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected fillOpsFromCoValue() {
|
|
48
|
+
this.ops = {};
|
|
49
|
+
|
|
50
|
+
for (const { txID, changes, madeAt } of this.coValue.getValidSortedTransactions()) {
|
|
51
|
+
for (const [changeIdx, changeUntyped] of (
|
|
52
|
+
changes
|
|
53
|
+
).entries()) {
|
|
54
|
+
const change = changeUntyped as MapOpPayload<K, V>;
|
|
55
|
+
let entries = this.ops[change.key];
|
|
56
|
+
if (!entries) {
|
|
57
|
+
entries = [];
|
|
58
|
+
this.ops[change.key] = entries;
|
|
59
|
+
}
|
|
60
|
+
entries.push({
|
|
61
|
+
txID,
|
|
62
|
+
madeAt,
|
|
63
|
+
changeIdx,
|
|
64
|
+
...(change as any),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
keys(): K[] {
|
|
71
|
+
return Object.keys(this.ops) as K[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get<KK extends K>(key: KK): M[KK] | undefined {
|
|
75
|
+
const ops = this.ops[key];
|
|
76
|
+
if (!ops) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const lastEntry = ops[ops.length - 1]!;
|
|
81
|
+
|
|
82
|
+
if (lastEntry.op === "delete") {
|
|
83
|
+
return undefined;
|
|
84
|
+
} else {
|
|
85
|
+
return lastEntry.value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getAtTime<KK extends K>(key: KK, time: number): M[KK] | undefined {
|
|
90
|
+
const ops = this.ops[key];
|
|
91
|
+
if (!ops) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const lastOpBeforeOrAtTime = ops.findLast((op) => op.madeAt <= time);
|
|
96
|
+
|
|
97
|
+
if (!lastOpBeforeOrAtTime) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (lastOpBeforeOrAtTime.op === "delete") {
|
|
102
|
+
return undefined;
|
|
103
|
+
} else {
|
|
104
|
+
return lastOpBeforeOrAtTime.value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getLastTxID<KK extends K>(key: KK): TransactionID | undefined {
|
|
109
|
+
const ops = this.ops[key];
|
|
110
|
+
if (!ops) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const lastEntry = ops[ops.length - 1]!;
|
|
115
|
+
|
|
116
|
+
return lastEntry.txID;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getHistory<KK extends K>(key: KK): { at: number; txID: TransactionID; value: M[KK] | undefined; }[] {
|
|
120
|
+
const ops = this.ops[key];
|
|
121
|
+
if (!ops) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const history: { at: number; txID: TransactionID; value: M[KK] | undefined; }[] = [];
|
|
126
|
+
|
|
127
|
+
for (const op of ops) {
|
|
128
|
+
if (op.op === "delete") {
|
|
129
|
+
history.push({ at: op.madeAt, txID: op.txID, value: undefined });
|
|
130
|
+
} else {
|
|
131
|
+
history.push({ at: op.madeAt, txID: op.txID, value: op.value });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return history;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
toJSON(): JsonObject {
|
|
139
|
+
const json: JsonObject = {};
|
|
140
|
+
|
|
141
|
+
for (const key of this.keys()) {
|
|
142
|
+
const value = this.get(key);
|
|
143
|
+
if (value !== undefined) {
|
|
144
|
+
json[key] = value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return json;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
edit(changer: (editable: WriteableCoMap<M, Meta>) => void): CoMap<M, Meta> {
|
|
152
|
+
const editable = new WriteableCoMap<M, Meta>(this.coValue);
|
|
153
|
+
changer(editable);
|
|
154
|
+
return new CoMap(this.coValue);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
subscribe(listener: (coMap: CoMap<M, Meta>) => void): () => void {
|
|
158
|
+
return this.coValue.subscribe((content) => {
|
|
159
|
+
listener(content as CoMap<M, Meta>);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class WriteableCoMap<
|
|
165
|
+
M extends { [key: string]: JsonValue; },
|
|
166
|
+
Meta extends JsonValue,
|
|
167
|
+
K extends string = keyof M & string,
|
|
168
|
+
V extends JsonValue = M[K],
|
|
169
|
+
MM extends { [key: string]: JsonValue; } = {
|
|
170
|
+
[KK in K]: M[KK];
|
|
171
|
+
}
|
|
172
|
+
> extends CoMap<M, Meta, K, V, MM> {
|
|
173
|
+
set<KK extends K>(key: KK, value: M[KK], privacy: "private" | "trusting" = "private"): void {
|
|
174
|
+
this.coValue.makeTransaction([
|
|
175
|
+
{
|
|
176
|
+
op: "insert",
|
|
177
|
+
key,
|
|
178
|
+
value,
|
|
179
|
+
},
|
|
180
|
+
], privacy);
|
|
181
|
+
|
|
182
|
+
this.fillOpsFromCoValue();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
delete(key: K, privacy: "private" | "trusting" = "private"): void {
|
|
186
|
+
this.coValue.makeTransaction([
|
|
187
|
+
{
|
|
188
|
+
op: "delete",
|
|
189
|
+
key,
|
|
190
|
+
},
|
|
191
|
+
], privacy);
|
|
192
|
+
|
|
193
|
+
this.fillOpsFromCoValue();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { JsonObject, JsonValue } from "../jsonValue";
|
|
2
|
+
import { CoValueID } from "../contentType";
|
|
3
|
+
import { CoValue } from "../coValue";
|
|
4
|
+
|
|
5
|
+
export class CoStream<T extends JsonValue, Meta extends JsonValue> {
|
|
6
|
+
id: CoValueID<CoStream<T, Meta>>;
|
|
7
|
+
type: "costream" = "costream";
|
|
8
|
+
coValue: CoValue;
|
|
9
|
+
|
|
10
|
+
constructor(coValue: CoValue) {
|
|
11
|
+
this.id = coValue.id as CoValueID<CoStream<T, Meta>>;
|
|
12
|
+
this.coValue = coValue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
toJSON(): JsonObject {
|
|
16
|
+
throw new Error("Method not implemented.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
subscribe(listener: (coMap: CoStream<T, Meta>) => void): () => void {
|
|
20
|
+
return this.coValue.subscribe((content) => {
|
|
21
|
+
listener(content as CoStream<T, Meta>);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { JsonObject, JsonValue } from "../jsonValue";
|
|
2
|
+
import { CoValueID } from "../contentType";
|
|
3
|
+
import { CoValue } from "../coValue";
|
|
4
|
+
|
|
5
|
+
export class Static<T extends JsonValue> {
|
|
6
|
+
id: CoValueID<Static<T>>;
|
|
7
|
+
type: "static" = "static";
|
|
8
|
+
coValue: CoValue;
|
|
9
|
+
|
|
10
|
+
constructor(coValue: CoValue) {
|
|
11
|
+
this.id = coValue.id as CoValueID<Static<T>>;
|
|
12
|
+
this.coValue = coValue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
toJSON(): JsonObject {
|
|
16
|
+
throw new Error("Method not implemented.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
subscribe(listener: (coMap: Static<T>) => void): () => void {
|
|
20
|
+
throw new Error("Method not implemented.");
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/crypto.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { base58, base64url } from "@scure/base";
|
|
|
5
5
|
import { default as stableStringify } from "fast-json-stable-stringify";
|
|
6
6
|
import { blake3 } from "@noble/hashes/blake3";
|
|
7
7
|
import { randomBytes } from "@noble/ciphers/webcrypto/utils";
|
|
8
|
-
import { RawCoValueID,
|
|
8
|
+
import { RawCoValueID, TransactionID } from "./ids";
|
|
9
9
|
|
|
10
10
|
export type SignatorySecret = `signatorySecret_z${string}`;
|
|
11
11
|
export type SignatoryID = `signatory_z${string}`;
|
|
@@ -24,6 +24,14 @@ export function newRandomSignatory(): SignatorySecret {
|
|
|
24
24
|
)}`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export function signatorySecretToBytes(secret: SignatorySecret): Uint8Array {
|
|
28
|
+
return base58.decode(secret.substring("signatorySecret_z".length));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function signatorySecretFromBytes(bytes: Uint8Array): SignatorySecret {
|
|
32
|
+
return `signatorySecret_z${base58.encode(bytes)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
export function getSignatoryID(secret: SignatorySecret): SignatoryID {
|
|
28
36
|
return `signatory_z${base58.encode(
|
|
29
37
|
ed25519.getPublicKey(
|
|
@@ -56,6 +64,14 @@ export function newRandomRecipient(): RecipientSecret {
|
|
|
56
64
|
return `recipientSecret_z${base58.encode(x25519.utils.randomPrivateKey())}`;
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
export function recipientSecretToBytes(secret: RecipientSecret): Uint8Array {
|
|
68
|
+
return base58.decode(secret.substring("recipientSecret_z".length));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function recipientSecretFromBytes(bytes: Uint8Array): RecipientSecret {
|
|
72
|
+
return `recipientSecret_z${base58.encode(bytes)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
export function getRecipientID(secret: RecipientSecret): RecipientID {
|
|
60
76
|
return `recipient_z${base58.encode(
|
|
61
77
|
x25519.getPublicKey(
|
|
@@ -295,3 +311,15 @@ export function unsealKeySecret(
|
|
|
295
311
|
|
|
296
312
|
return decrypt(sealedInfo.encrypted, sealingSecret, nOnceMaterial);
|
|
297
313
|
}
|
|
314
|
+
|
|
315
|
+
export function uniquenessForHeader(): `z${string}` {
|
|
316
|
+
return `z${base58.encode(randomBytes(12))}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function createdNowUnique(): {createdAt: `2${string}`, uniqueness: `z${string}`} {
|
|
320
|
+
const createdAt = (new Date()).toISOString() as `2${string}`;
|
|
321
|
+
return {
|
|
322
|
+
createdAt,
|
|
323
|
+
uniqueness: uniquenessForHeader(),
|
|
324
|
+
}
|
|
325
|
+
}
|
package/src/ids.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
|
-
import { ContentType } from "./contentType";
|
|
2
|
-
import { JsonValue } from "./jsonValue";
|
|
3
|
-
import {
|
|
1
|
+
import type { CoValueID, ContentType } from "./contentType";
|
|
2
|
+
import type { JsonValue } from "./jsonValue";
|
|
3
|
+
import {
|
|
4
|
+
CoValue,
|
|
5
|
+
agentCredentialFromBytes,
|
|
6
|
+
agentCredentialToBytes,
|
|
7
|
+
getAgent,
|
|
8
|
+
getAgentID,
|
|
9
|
+
newRandomAgentCredential,
|
|
10
|
+
newRandomSessionID,
|
|
11
|
+
} from "./coValue";
|
|
4
12
|
import { LocalNode } from "./node";
|
|
13
|
+
import { CoMap } from "./contentTypes/coMap";
|
|
5
14
|
|
|
6
15
|
type Value = JsonValue | ContentType;
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const internals = {
|
|
18
|
+
agentCredentialToBytes,
|
|
19
|
+
agentCredentialFromBytes,
|
|
20
|
+
getAgent,
|
|
21
|
+
getAgentID,
|
|
22
|
+
newRandomAgentCredential,
|
|
23
|
+
newRandomSessionID,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { LocalNode, CoValue, CoMap, internals };
|
|
27
|
+
|
|
28
|
+
export type { Value, JsonValue, ContentType, CoValueID };
|
package/src/node.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { newRandomKeySecret, seal } from "./crypto";
|
|
1
|
+
import { createdNowUnique, newRandomKeySecret, seal } from "./crypto";
|
|
2
2
|
import {
|
|
3
|
-
RawCoValueID,
|
|
4
3
|
CoValue,
|
|
5
4
|
AgentCredential,
|
|
6
|
-
AgentID,
|
|
7
|
-
SessionID,
|
|
8
5
|
Agent,
|
|
9
6
|
getAgent,
|
|
10
7
|
getAgentID,
|
|
@@ -15,6 +12,8 @@ import {
|
|
|
15
12
|
} from "./coValue";
|
|
16
13
|
import { Team, expectTeamContent } from "./permissions";
|
|
17
14
|
import { SyncManager } from "./sync";
|
|
15
|
+
import { AgentID, RawCoValueID, SessionID } from "./ids";
|
|
16
|
+
import { CoValueID, ContentType } from ".";
|
|
18
17
|
|
|
19
18
|
export class LocalNode {
|
|
20
19
|
coValues: { [key: RawCoValueID]: CoValueState } = {};
|
|
@@ -61,6 +60,10 @@ export class LocalNode {
|
|
|
61
60
|
return entry.done;
|
|
62
61
|
}
|
|
63
62
|
|
|
63
|
+
async load<T extends ContentType>(id: CoValueID<T>): Promise<T> {
|
|
64
|
+
return (await this.loadCoValue(id)).getCurrentContent() as T;
|
|
65
|
+
}
|
|
66
|
+
|
|
64
67
|
expectCoValueLoaded(id: RawCoValueID, expectation?: string): CoValue {
|
|
65
68
|
const entry = this.coValues[id];
|
|
66
69
|
if (!entry) {
|
|
@@ -112,6 +115,7 @@ export class LocalNode {
|
|
|
112
115
|
type: "comap",
|
|
113
116
|
ruleset: { type: "team", initialAdmin: this.agentID },
|
|
114
117
|
meta: null,
|
|
118
|
+
...createdNowUnique(),
|
|
115
119
|
publicNickname: "team",
|
|
116
120
|
});
|
|
117
121
|
|
package/src/permissions.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { LocalNode } from "./node";
|
|
|
8
8
|
import { expectMap } from "./contentType";
|
|
9
9
|
import { expectTeamContent } from "./permissions";
|
|
10
10
|
import {
|
|
11
|
+
createdNowUnique,
|
|
11
12
|
getRecipientID,
|
|
12
13
|
newRandomKeySecret,
|
|
13
14
|
seal,
|
|
@@ -47,6 +48,7 @@ function newTeam() {
|
|
|
47
48
|
type: "comap",
|
|
48
49
|
ruleset: { type: "team", initialAdmin: adminID },
|
|
49
50
|
meta: null,
|
|
51
|
+
...createdNowUnique(),
|
|
50
52
|
publicNickname: "team",
|
|
51
53
|
});
|
|
52
54
|
|
|
@@ -343,6 +345,7 @@ test("Admins can write to an object that is owned by their team", () => {
|
|
|
343
345
|
type: "comap",
|
|
344
346
|
ruleset: { type: "ownedByTeam", team: team.id },
|
|
345
347
|
meta: null,
|
|
348
|
+
...createdNowUnique(),
|
|
346
349
|
publicNickname: "childObject",
|
|
347
350
|
});
|
|
348
351
|
|
|
@@ -386,6 +389,7 @@ test("Writers can write to an object that is owned by their team", () => {
|
|
|
386
389
|
type: "comap",
|
|
387
390
|
ruleset: { type: "ownedByTeam", team: team.id },
|
|
388
391
|
meta: null,
|
|
392
|
+
...createdNowUnique(),
|
|
389
393
|
publicNickname: "childObject",
|
|
390
394
|
});
|
|
391
395
|
|
|
@@ -447,6 +451,7 @@ test("Readers can not write to an object that is owned by their team", () => {
|
|
|
447
451
|
type: "comap",
|
|
448
452
|
ruleset: { type: "ownedByTeam", team: team.id },
|
|
449
453
|
meta: null,
|
|
454
|
+
...createdNowUnique(),
|
|
450
455
|
publicNickname: "childObject",
|
|
451
456
|
});
|
|
452
457
|
|
|
@@ -521,6 +526,7 @@ test("Admins can set team read key and then use it to create and read private tr
|
|
|
521
526
|
type: "comap",
|
|
522
527
|
ruleset: { type: "ownedByTeam", team: team.id },
|
|
523
528
|
meta: null,
|
|
529
|
+
...createdNowUnique(),
|
|
524
530
|
publicNickname: "childObject",
|
|
525
531
|
});
|
|
526
532
|
|
|
@@ -580,6 +586,7 @@ test("Admins can set team read key and then writers can use it to create and rea
|
|
|
580
586
|
type: "comap",
|
|
581
587
|
ruleset: { type: "ownedByTeam", team: team.id },
|
|
582
588
|
meta: null,
|
|
589
|
+
...createdNowUnique(),
|
|
583
590
|
publicNickname: "childObject",
|
|
584
591
|
});
|
|
585
592
|
|
|
@@ -660,6 +667,7 @@ test("Admins can set team read key and then use it to create private transaction
|
|
|
660
667
|
type: "comap",
|
|
661
668
|
ruleset: { type: "ownedByTeam", team: team.id },
|
|
662
669
|
meta: null,
|
|
670
|
+
...createdNowUnique(),
|
|
663
671
|
publicNickname: "childObject",
|
|
664
672
|
});
|
|
665
673
|
|
|
@@ -759,6 +767,7 @@ test("Admins can set team read key and then use it to create private transaction
|
|
|
759
767
|
type: "comap",
|
|
760
768
|
ruleset: { type: "ownedByTeam", team: team.id },
|
|
761
769
|
meta: null,
|
|
770
|
+
...createdNowUnique(),
|
|
762
771
|
publicNickname: "childObject",
|
|
763
772
|
});
|
|
764
773
|
|
|
@@ -864,6 +873,7 @@ test("Admins can set team read key, make a private transaction in an owned objec
|
|
|
864
873
|
type: "comap",
|
|
865
874
|
ruleset: { type: "ownedByTeam", team: team.id },
|
|
866
875
|
meta: null,
|
|
876
|
+
...createdNowUnique(),
|
|
867
877
|
publicNickname: "childObject",
|
|
868
878
|
});
|
|
869
879
|
|
|
@@ -944,6 +954,7 @@ test("Admins can set team read key, make a private transaction in an owned objec
|
|
|
944
954
|
type: "comap",
|
|
945
955
|
ruleset: { type: "ownedByTeam", team: team.id },
|
|
946
956
|
meta: null,
|
|
957
|
+
...createdNowUnique(),
|
|
947
958
|
publicNickname: "childObject",
|
|
948
959
|
});
|
|
949
960
|
|
|
@@ -1085,6 +1096,7 @@ test("Admins can set team read rey, make a private transaction in an owned objec
|
|
|
1085
1096
|
type: "comap",
|
|
1086
1097
|
ruleset: { type: "ownedByTeam", team: team.id },
|
|
1087
1098
|
meta: null,
|
|
1099
|
+
...createdNowUnique(),
|
|
1088
1100
|
publicNickname: "childObject",
|
|
1089
1101
|
});
|
|
1090
1102
|
|
|
@@ -1267,3 +1279,23 @@ test("Admins can set team read rey, make a private transaction in an owned objec
|
|
|
1267
1279
|
).get("foo3")
|
|
1268
1280
|
).toBeUndefined();
|
|
1269
1281
|
});
|
|
1282
|
+
|
|
1283
|
+
test("Can create two owned objects in the same team and they will have different ids", () => {
|
|
1284
|
+
const { node, team, admin, adminID } = newTeam();
|
|
1285
|
+
|
|
1286
|
+
const childObject1 = node.createCoValue({
|
|
1287
|
+
type: "comap",
|
|
1288
|
+
ruleset: { type: "ownedByTeam", team: team.id },
|
|
1289
|
+
meta: null,
|
|
1290
|
+
...createdNowUnique()
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
const childObject2 = node.createCoValue({
|
|
1294
|
+
type: "comap",
|
|
1295
|
+
ruleset: { type: "ownedByTeam", team: team.id },
|
|
1296
|
+
meta: null,
|
|
1297
|
+
...createdNowUnique()
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
expect(childObject1.id).not.toEqual(childObject2.id);
|
|
1301
|
+
});
|
package/src/permissions.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ContentType } from "./contentType";
|
|
2
|
+
import { CoMap, MapOpPayload } from "./contentTypes/coMap";
|
|
2
3
|
import { JsonValue } from "./jsonValue";
|
|
3
4
|
import {
|
|
4
5
|
Encrypted,
|
|
@@ -7,23 +8,20 @@ import {
|
|
|
7
8
|
RecipientID,
|
|
8
9
|
SealedSet,
|
|
9
10
|
SignatoryID,
|
|
10
|
-
|
|
11
|
+
createdNowUnique,
|
|
11
12
|
newRandomKeySecret,
|
|
12
13
|
seal,
|
|
13
14
|
sealKeySecret,
|
|
14
15
|
} from "./crypto";
|
|
15
16
|
import {
|
|
16
17
|
AgentCredential,
|
|
17
|
-
AgentID,
|
|
18
18
|
CoValue,
|
|
19
|
-
RawCoValueID,
|
|
20
|
-
SessionID,
|
|
21
19
|
Transaction,
|
|
22
|
-
TransactionID,
|
|
23
20
|
TrustingTransaction,
|
|
24
21
|
agentIDfromSessionID,
|
|
25
22
|
} from "./coValue";
|
|
26
23
|
import { LocalNode } from ".";
|
|
24
|
+
import { AgentID, RawCoValueID, SessionID, TransactionID } from "./ids";
|
|
27
25
|
|
|
28
26
|
export type PermissionsDef =
|
|
29
27
|
| { type: "team"; initialAdmin: AgentID; parentTeams?: RawCoValueID[] }
|
|
@@ -355,6 +353,7 @@ export class Team {
|
|
|
355
353
|
team: this.teamMap.id,
|
|
356
354
|
},
|
|
357
355
|
meta: meta || null,
|
|
356
|
+
...createdNowUnique(),
|
|
358
357
|
publicNickname: "map",
|
|
359
358
|
})
|
|
360
359
|
.getCurrentContent() as CoMap<M, Meta>;
|
package/src/sync.test.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AgentID,
|
|
3
2
|
getAgent,
|
|
4
3
|
getAgentID,
|
|
5
4
|
newRandomAgentCredential,
|
|
@@ -7,13 +6,15 @@ import {
|
|
|
7
6
|
} from "./coValue";
|
|
8
7
|
import { LocalNode } from "./node";
|
|
9
8
|
import { Peer, PeerID, SyncMessage } from "./sync";
|
|
10
|
-
import {
|
|
9
|
+
import { expectMap } from "./contentType";
|
|
10
|
+
import { MapOpPayload } from "./contentTypes/coMap";
|
|
11
11
|
import { Team } from "./permissions";
|
|
12
12
|
import {
|
|
13
13
|
ReadableStream,
|
|
14
14
|
WritableStream,
|
|
15
15
|
TransformStream,
|
|
16
16
|
} from "isomorphic-streams";
|
|
17
|
+
import { AgentID } from "./ids";
|
|
17
18
|
|
|
18
19
|
test(
|
|
19
20
|
"Node replies with initial tx and header to empty subscribe",
|
|
@@ -73,6 +74,8 @@ test(
|
|
|
73
74
|
type: "comap",
|
|
74
75
|
ruleset: { type: "ownedByTeam", team: team.id },
|
|
75
76
|
meta: null,
|
|
77
|
+
createdAt: map.coValue.header.createdAt,
|
|
78
|
+
uniqueness: map.coValue.header.uniqueness,
|
|
76
79
|
publicNickname: "map",
|
|
77
80
|
},
|
|
78
81
|
newContent: {
|
|
@@ -609,8 +612,6 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a
|
|
|
609
612
|
|
|
610
613
|
const team = node.createTeam();
|
|
611
614
|
|
|
612
|
-
team.createMap();
|
|
613
|
-
|
|
614
615
|
const [inRx, _inTx] = newStreamPair<SyncMessage>();
|
|
615
616
|
const [outRx, outTx] = newStreamPair<SyncMessage>();
|
|
616
617
|
|
package/src/sync.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Hash, Signature } from "./crypto";
|
|
2
|
-
import { CoValueHeader,
|
|
2
|
+
import { CoValueHeader, Transaction } from "./coValue";
|
|
3
3
|
import { CoValue } from "./coValue";
|
|
4
4
|
import { LocalNode } from "./node";
|
|
5
5
|
import { newLoadingState } from "./node";
|
|
6
6
|
import { ReadableStream, WritableStream, WritableStreamDefaultWriter } from "isomorphic-streams";
|
|
7
|
+
import { RawCoValueID, SessionID } from "./ids";
|
|
7
8
|
|
|
8
9
|
export type CoValueKnownState = {
|
|
9
10
|
coValueID: RawCoValueID;
|
|
@@ -79,31 +80,6 @@ export interface PeerState {
|
|
|
79
80
|
role: "peer" | "server" | "client";
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
export function weAreStrictlyAhead(
|
|
83
|
-
ourKnownState: CoValueKnownState,
|
|
84
|
-
theirKnownState: CoValueKnownState
|
|
85
|
-
): boolean {
|
|
86
|
-
if (theirKnownState.header && !ourKnownState.header) {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const allSessions = new Set([
|
|
91
|
-
...(Object.keys(ourKnownState.sessions) as SessionID[]),
|
|
92
|
-
...(Object.keys(theirKnownState.sessions) as SessionID[]),
|
|
93
|
-
]);
|
|
94
|
-
|
|
95
|
-
for (const sessionID of allSessions) {
|
|
96
|
-
const ourSession = ourKnownState.sessions[sessionID];
|
|
97
|
-
const theirSession = theirKnownState.sessions[sessionID];
|
|
98
|
-
|
|
99
|
-
if ((ourSession || 0) < (theirSession || 0)) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
83
|
export function combinedKnownStates(
|
|
108
84
|
stateA: CoValueKnownState,
|
|
109
85
|
stateB: CoValueKnownState
|
|
@@ -480,11 +456,7 @@ export class SyncManager {
|
|
|
480
456
|
for (const peer of Object.values(this.peers)) {
|
|
481
457
|
const optimisticKnownState = peer.optimisticKnownStates[coValue.id];
|
|
482
458
|
|
|
483
|
-
|
|
484
|
-
optimisticKnownState ||
|
|
485
|
-
peer.role === "server";
|
|
486
|
-
|
|
487
|
-
if (shouldSync) {
|
|
459
|
+
if (optimisticKnownState) {
|
|
488
460
|
await this.tellUntoldKnownStateIncludingDependencies(
|
|
489
461
|
coValue.id,
|
|
490
462
|
peer
|
|
@@ -493,6 +465,15 @@ export class SyncManager {
|
|
|
493
465
|
coValue.id,
|
|
494
466
|
peer
|
|
495
467
|
);
|
|
468
|
+
} else if (peer.role === "server") {
|
|
469
|
+
await this.subscribeToIncludingDependencies(
|
|
470
|
+
coValue.id,
|
|
471
|
+
peer
|
|
472
|
+
);
|
|
473
|
+
await this.sendNewContentIncludingDependencies(
|
|
474
|
+
coValue.id,
|
|
475
|
+
peer
|
|
476
|
+
);
|
|
496
477
|
}
|
|
497
478
|
}
|
|
498
479
|
}
|