cojson 0.0.1 → 0.0.2

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.
@@ -0,0 +1,196 @@
1
+ import {
2
+ agentIDfromSessionID,
3
+ getAgent,
4
+ getAgentID,
5
+ newRandomAgentCredential,
6
+ newRandomSessionID,
7
+ } from "./coValue";
8
+ import { LocalNode } from "./node";
9
+
10
+ test("Empty COJSON Map works", () => {
11
+ const agentCredential = newRandomAgentCredential("agent1");
12
+ const node = new LocalNode(
13
+ agentCredential,
14
+ newRandomSessionID(getAgentID(getAgent(agentCredential)))
15
+ );
16
+
17
+ const coValue = node.createCoValue({
18
+ type: "comap",
19
+ ruleset: { type: "unsafeAllowAll" },
20
+ meta: null,
21
+ });
22
+
23
+ const content = coValue.getCurrentContent();
24
+
25
+ if (content.type !== "comap") {
26
+ throw new Error("Expected map");
27
+ }
28
+
29
+ expect(content.type).toEqual("comap");
30
+ expect([...content.keys()]).toEqual([]);
31
+ expect(content.toJSON()).toEqual({});
32
+ });
33
+
34
+ test("Can insert and delete Map entries in edit()", () => {
35
+ const agentCredential = newRandomAgentCredential("agent1");
36
+ const node = new LocalNode(
37
+ agentCredential,
38
+ newRandomSessionID(getAgentID(getAgent(agentCredential)))
39
+ );
40
+
41
+ const coValue = node.createCoValue({
42
+ type: "comap",
43
+ ruleset: { type: "unsafeAllowAll" },
44
+ meta: null,
45
+ });
46
+
47
+ const content = coValue.getCurrentContent();
48
+
49
+ if (content.type !== "comap") {
50
+ throw new Error("Expected map");
51
+ }
52
+
53
+ expect(content.type).toEqual("comap");
54
+
55
+ content.edit((editable) => {
56
+ editable.set("hello", "world", "trusting");
57
+ expect(editable.get("hello")).toEqual("world");
58
+ editable.set("foo", "bar", "trusting");
59
+ expect(editable.get("foo")).toEqual("bar");
60
+ expect([...editable.keys()]).toEqual(["hello", "foo"]);
61
+ editable.delete("foo", "trusting");
62
+ expect(editable.get("foo")).toEqual(undefined);
63
+ });
64
+ });
65
+
66
+ test("Can get map entry values at different points in time", () => {
67
+ const agentCredential = newRandomAgentCredential("agent1");
68
+ const node = new LocalNode(
69
+ agentCredential,
70
+ newRandomSessionID(getAgentID(getAgent(agentCredential)))
71
+ );
72
+
73
+ const coValue = node.createCoValue({
74
+ type: "comap",
75
+ ruleset: { type: "unsafeAllowAll" },
76
+ meta: null,
77
+ });
78
+
79
+ const content = coValue.getCurrentContent();
80
+
81
+ if (content.type !== "comap") {
82
+ throw new Error("Expected map");
83
+ }
84
+
85
+ expect(content.type).toEqual("comap");
86
+
87
+ content.edit((editable) => {
88
+ const beforeA = Date.now();
89
+ while(Date.now() < beforeA + 10){}
90
+ editable.set("hello", "A", "trusting");
91
+ const beforeB = Date.now();
92
+ while(Date.now() < beforeB + 10){}
93
+ editable.set("hello", "B", "trusting");
94
+ const beforeC = Date.now();
95
+ while(Date.now() < beforeC + 10){}
96
+ editable.set("hello", "C", "trusting");
97
+ expect(editable.get("hello")).toEqual("C");
98
+ expect(editable.getAtTime("hello", Date.now())).toEqual("C");
99
+ expect(editable.getAtTime("hello", beforeA)).toEqual(undefined);
100
+ expect(editable.getAtTime("hello", beforeB)).toEqual("A");
101
+ expect(editable.getAtTime("hello", beforeC)).toEqual("B");
102
+ });
103
+ });
104
+
105
+ test("Can get all historic values of key", () => {
106
+ const agentCredential = newRandomAgentCredential("agent1");
107
+ const node = new LocalNode(
108
+ agentCredential,
109
+ newRandomSessionID(getAgentID(getAgent(agentCredential)))
110
+ );
111
+
112
+ const coValue = node.createCoValue({
113
+ type: "comap",
114
+ ruleset: { type: "unsafeAllowAll" },
115
+ meta: null,
116
+ });
117
+
118
+ const content = coValue.getCurrentContent();
119
+
120
+ if (content.type !== "comap") {
121
+ throw new Error("Expected map");
122
+ }
123
+
124
+ expect(content.type).toEqual("comap");
125
+
126
+ content.edit((editable) => {
127
+ editable.set("hello", "A", "trusting");
128
+ const txA = editable.getLastTxID("hello");
129
+ editable.set("hello", "B", "trusting");
130
+ const txB = editable.getLastTxID("hello");
131
+ editable.delete("hello", "trusting");
132
+ const txDel = editable.getLastTxID("hello");
133
+ editable.set("hello", "C", "trusting");
134
+ const txC = editable.getLastTxID("hello");
135
+ expect(
136
+ editable.getHistory("hello")
137
+ ).toEqual([
138
+ {
139
+ txID: txA,
140
+ value: "A",
141
+ at: txA && coValue.getTx(txA)?.madeAt,
142
+ },
143
+ {
144
+ txID: txB,
145
+ value: "B",
146
+ at: txB && coValue.getTx(txB)?.madeAt,
147
+ },
148
+ {
149
+ txID: txDel,
150
+ value: undefined,
151
+ at: txDel && coValue.getTx(txDel)?.madeAt,
152
+ },
153
+ {
154
+ txID: txC,
155
+ value: "C",
156
+ at: txC && coValue.getTx(txC)?.madeAt,
157
+ },
158
+ ]);
159
+ });
160
+ });
161
+
162
+ test("Can get last tx ID for a key", () => {
163
+ const agentCredential = newRandomAgentCredential("agent1");
164
+ const node = new LocalNode(
165
+ agentCredential,
166
+ newRandomSessionID(getAgentID(getAgent(agentCredential)))
167
+ );
168
+
169
+ const coValue = node.createCoValue({
170
+ type: "comap",
171
+ ruleset: { type: "unsafeAllowAll" },
172
+ meta: null,
173
+ });
174
+
175
+ const content = coValue.getCurrentContent();
176
+
177
+ if (content.type !== "comap") {
178
+ throw new Error("Expected map");
179
+ }
180
+
181
+ expect(content.type).toEqual("comap");
182
+
183
+ content.edit((editable) => {
184
+ expect(editable.getLastTxID("hello")).toEqual(undefined);
185
+ editable.set("hello", "A", "trusting");
186
+ const sessionID = editable.getLastTxID("hello")?.sessionID;
187
+ expect(sessionID && agentIDfromSessionID(sessionID)).toEqual(
188
+ getAgentID(getAgent(agentCredential))
189
+ );
190
+ expect(editable.getLastTxID("hello")?.txIndex).toEqual(0);
191
+ editable.set("hello", "B", "trusting");
192
+ expect(editable.getLastTxID("hello")?.txIndex).toEqual(1);
193
+ editable.set("hello", "C", "trusting");
194
+ expect(editable.getLastTxID("hello")?.txIndex).toEqual(2);
195
+ });
196
+ });
@@ -0,0 +1,239 @@
1
+ import { JsonAtom, JsonObject, JsonValue } from "./jsonValue";
2
+ import { CoValue, RawCoValueID, TransactionID } from "./coValue";
3
+
4
+ export type CoValueID<T extends ContentType> = RawCoValueID & {
5
+ readonly __type: T;
6
+ };
7
+
8
+ export type ContentType =
9
+ | CoMap<{[key: string]: JsonValue}, JsonValue>
10
+ | CoList<JsonValue, JsonValue>
11
+ | CoStream<JsonValue, JsonValue>
12
+ | Static<JsonValue>;
13
+
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
+ export function expectMap(content: ContentType): CoMap<{ [key: string]: string }, {}> {
234
+ if (content.type !== "comap") {
235
+ throw new Error("Expected map");
236
+ }
237
+
238
+ return content as CoMap<{ [key: string]: string }, {}>;
239
+ }
@@ -0,0 +1,189 @@
1
+ import {
2
+ getRecipientID,
3
+ getSignatoryID,
4
+ secureHash,
5
+ newRandomRecipient,
6
+ newRandomSignatory,
7
+ seal,
8
+ sign,
9
+ openAs,
10
+ verify,
11
+ shortHash,
12
+ newRandomKeySecret,
13
+ encryptForTransaction,
14
+ decryptForTransaction,
15
+ sealKeySecret,
16
+ unsealKeySecret,
17
+ } from "./crypto";
18
+ import { base58, base64url } from "@scure/base";
19
+ import { x25519 } from "@noble/curves/ed25519";
20
+ import { xsalsa20_poly1305 } from "@noble/ciphers/salsa";
21
+ import { blake3 } from "@noble/hashes/blake3";
22
+ import stableStringify from "fast-json-stable-stringify";
23
+
24
+ test("Signatures round-trip and use stable stringify", () => {
25
+ const data = { b: "world", a: "hello" };
26
+ const signatory = newRandomSignatory();
27
+ const signature = sign(signatory, data);
28
+
29
+ expect(signature).toMatch(/^signature_z/);
30
+ expect(
31
+ verify(signature, { a: "hello", b: "world" }, getSignatoryID(signatory))
32
+ ).toBe(true);
33
+ });
34
+
35
+ test("Invalid signatures don't verify", () => {
36
+ const data = { b: "world", a: "hello" };
37
+ const signatory = newRandomSignatory();
38
+ const signatory2 = newRandomSignatory();
39
+ const wrongSignature = sign(signatory2, data);
40
+
41
+ expect(verify(wrongSignature, data, getSignatoryID(signatory))).toBe(false);
42
+ });
43
+
44
+ test("Sealing round-trips, but invalid receiver can't unseal", () => {
45
+ const data = { b: "world", a: "hello" };
46
+ const sender = newRandomRecipient();
47
+ const recipient1 = newRandomRecipient();
48
+ const recipient2 = newRandomRecipient();
49
+ const recipient3 = newRandomRecipient();
50
+
51
+ const nOnceMaterial = {
52
+ in: "co_zTEST",
53
+ tx: { sessionID: "co_agent_zTEST_session_zTEST", txIndex: 0 },
54
+ } as const;
55
+
56
+ const sealed = seal(
57
+ data,
58
+ sender,
59
+ new Set([getRecipientID(recipient1), getRecipientID(recipient2)]),
60
+ nOnceMaterial
61
+ );
62
+
63
+ expect(sealed[getRecipientID(recipient1)]).toMatch(/^sealed_U/);
64
+ expect(sealed[getRecipientID(recipient2)]).toMatch(/^sealed_U/);
65
+ expect(
66
+ openAs(sealed, recipient1, getRecipientID(sender), nOnceMaterial)
67
+ ).toEqual(data);
68
+ expect(
69
+ openAs(sealed, recipient2, getRecipientID(sender), nOnceMaterial)
70
+ ).toEqual(data);
71
+ expect(
72
+ openAs(sealed, recipient3, getRecipientID(sender), nOnceMaterial)
73
+ ).toBeUndefined();
74
+
75
+ // trying with wrong recipient secret, by hand
76
+ const nOnce = blake3(
77
+ new TextEncoder().encode(stableStringify(nOnceMaterial))
78
+ ).slice(0, 24);
79
+ const recipient3priv = base58.decode(
80
+ recipient3.substring("recipientSecret_z".length)
81
+ );
82
+ const senderPub = base58.decode(
83
+ getRecipientID(sender).substring("recipient_z".length)
84
+ );
85
+ const sealedBytes = base64url.decode(
86
+ sealed[getRecipientID(recipient1)]!.substring("sealed_U".length)
87
+ );
88
+ const sharedSecret = x25519.getSharedSecret(recipient3priv, senderPub);
89
+
90
+ expect(() => {
91
+ const _ = xsalsa20_poly1305(sharedSecret, nOnce).decrypt(sealedBytes);
92
+ }).toThrow("Wrong tag");
93
+ });
94
+
95
+ test("Hashing is deterministic", () => {
96
+ expect(secureHash({ b: "world", a: "hello" })).toEqual(
97
+ secureHash({ a: "hello", b: "world" })
98
+ );
99
+
100
+ expect(shortHash({ b: "world", a: "hello" })).toEqual(
101
+ shortHash({ a: "hello", b: "world" })
102
+ );
103
+ });
104
+
105
+ test("Encryption for transactions round-trips", () => {
106
+ const { secret } = newRandomKeySecret();
107
+
108
+ const encrypted1 = encryptForTransaction({ a: "hello" }, secret, {
109
+ in: "co_zTEST",
110
+ tx: { sessionID: "co_agent_zTEST_session_zTEST", txIndex: 0 },
111
+ });
112
+
113
+ const encrypted2 = encryptForTransaction({ b: "world" }, secret, {
114
+ in: "co_zTEST",
115
+ tx: { sessionID: "co_agent_zTEST_session_zTEST", txIndex: 1 },
116
+ });
117
+
118
+ const decrypted1 = decryptForTransaction(encrypted1, secret, {
119
+ in: "co_zTEST",
120
+ tx: { sessionID: "co_agent_zTEST_session_zTEST", txIndex: 0 },
121
+ });
122
+
123
+ const decrypted2 = decryptForTransaction(encrypted2, secret, {
124
+ in: "co_zTEST",
125
+ tx: { sessionID: "co_agent_zTEST_session_zTEST", txIndex: 1 },
126
+ });
127
+
128
+ expect([decrypted1, decrypted2]).toEqual([{ a: "hello" }, { b: "world" }]);
129
+ });
130
+
131
+ test("Encryption for transactions doesn't decrypt with a wrong key", () => {
132
+ const { secret } = newRandomKeySecret();
133
+ const { secret: secret2 } = newRandomKeySecret();
134
+
135
+ const encrypted1 = encryptForTransaction({ a: "hello" }, secret, {
136
+ in: "co_zTEST",
137
+ tx: { sessionID: "co_agent_zTEST_session_zTEST", txIndex: 0 },
138
+ });
139
+
140
+ const encrypted2 = encryptForTransaction({ b: "world" }, secret, {
141
+ in: "co_zTEST",
142
+ tx: { sessionID: "co_agent_zTEST_session_zTEST", txIndex: 1 },
143
+ });
144
+
145
+ const decrypted1 = decryptForTransaction(encrypted1, secret2, {
146
+ in: "co_zTEST",
147
+ tx: { sessionID: "co_agent_zTEST_session_zTEST", txIndex: 0 },
148
+ });
149
+
150
+ const decrypted2 = decryptForTransaction(encrypted2, secret2, {
151
+ in: "co_zTEST",
152
+ tx: { sessionID: "co_agent_zTEST_session_zTEST", txIndex: 1 },
153
+ });
154
+
155
+ expect([decrypted1, decrypted2]).toEqual([undefined, undefined]);
156
+ });
157
+
158
+ test("Encryption of keySecrets round-trips", () => {
159
+ const toSeal = newRandomKeySecret();
160
+ const sealing = newRandomKeySecret();
161
+
162
+ const keys = {
163
+ toSeal,
164
+ sealing,
165
+ };
166
+
167
+ const sealed = sealKeySecret(keys);
168
+
169
+ const unsealed = unsealKeySecret(sealed, sealing.secret);
170
+
171
+ expect(unsealed).toEqual(toSeal.secret);
172
+ });
173
+
174
+ test("Encryption of keySecrets doesn't unseal with a wrong key", () => {
175
+ const toSeal = newRandomKeySecret();
176
+ const sealing = newRandomKeySecret();
177
+ const sealingWrong = newRandomKeySecret();
178
+
179
+ const keys = {
180
+ toSeal,
181
+ sealing,
182
+ };
183
+
184
+ const sealed = sealKeySecret(keys);
185
+
186
+ const unsealed = unsealKeySecret(sealed, sealingWrong.secret);
187
+
188
+ expect(unsealed).toBeUndefined();
189
+ });