cojson 0.1.11 → 0.2.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/dist/base64url.js +15 -2
- package/dist/base64url.js.map +1 -1
- package/dist/coValue.d.ts +1 -1
- package/dist/coValueCore.d.ts +9 -3
- package/dist/coValueCore.js +100 -7
- package/dist/coValueCore.js.map +1 -1
- package/dist/coValues/coList.js +2 -1
- package/dist/coValues/coList.js.map +1 -1
- package/dist/coValues/coMap.d.ts +7 -12
- package/dist/coValues/coMap.js +2 -1
- package/dist/coValues/coMap.js.map +1 -1
- package/dist/coValues/coStream.d.ts +11 -2
- package/dist/coValues/coStream.js +59 -15
- package/dist/coValues/coStream.js.map +1 -1
- package/dist/crypto.d.ts +11 -3
- package/dist/crypto.js +51 -16
- package/dist/crypto.js.map +1 -1
- package/dist/group.d.ts +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/jsonStringify.d.ts +6 -0
- package/dist/jsonStringify.js +57 -0
- package/dist/jsonStringify.js.map +1 -0
- package/dist/jsonValue.d.ts +1 -1
- package/dist/media.d.ts +8 -0
- package/dist/media.js +2 -0
- package/dist/media.js.map +1 -0
- package/dist/node.js +1 -1
- package/dist/permissions.js +4 -2
- package/dist/permissions.js.map +1 -1
- package/dist/sync.js +8 -2
- package/dist/sync.js.map +1 -1
- package/package.json +3 -4
- package/src/account.test.ts +5 -0
- package/src/base64url.ts +16 -6
- package/src/coValue.test.ts +28 -0
- package/src/coValue.ts +1 -1
- package/src/coValueCore.test.ts +16 -10
- package/src/coValueCore.ts +148 -9
- package/src/coValues/coList.ts +2 -1
- package/src/coValues/coMap.ts +11 -12
- package/src/coValues/coStream.ts +83 -21
- package/src/crypto.test.ts +5 -0
- package/src/crypto.ts +68 -19
- package/src/group.test.ts +5 -1
- package/src/group.ts +1 -1
- package/src/index.ts +7 -1
- package/src/jsonStringify.ts +66 -0
- package/src/jsonValue.ts +1 -1
- package/src/media.ts +9 -0
- package/src/node.ts +1 -1
- package/src/permissions.test.ts +5 -1
- package/src/permissions.ts +5 -2
- package/src/sync.test.ts +22 -24
- package/src/sync.ts +16 -2
package/src/coValues/coStream.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { JsonObject, JsonValue } from "../jsonValue.js";
|
|
2
2
|
import { CoID, ReadableCoValue, WriteableCoValue } from "../coValue.js";
|
|
3
|
-
import { CoValueCore } from "../coValueCore.js";
|
|
3
|
+
import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
|
|
4
4
|
import { Group } from "../group.js";
|
|
5
5
|
import { SessionID } from "../ids.js";
|
|
6
6
|
import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
|
|
7
|
+
import { AccountID } from "../index.js";
|
|
8
|
+
import { isAccountID } from "../account.js";
|
|
9
|
+
import { parseJSON } from "../jsonStringify.js";
|
|
7
10
|
|
|
8
11
|
export type BinaryChunkInfo = {
|
|
9
12
|
mimeType: string;
|
|
@@ -17,7 +20,7 @@ export type BinaryStreamStart = {
|
|
|
17
20
|
|
|
18
21
|
export type BinaryStreamChunk = {
|
|
19
22
|
type: "chunk";
|
|
20
|
-
chunk: `
|
|
23
|
+
chunk: `binary_U${string}`;
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
export type BinaryStreamEnd = {
|
|
@@ -40,7 +43,7 @@ export class CoStream<
|
|
|
40
43
|
type = "costream" as const;
|
|
41
44
|
core: CoValueCore;
|
|
42
45
|
items: {
|
|
43
|
-
[key: SessionID]: T[];
|
|
46
|
+
[key: SessionID]: {item: T, madeAt: number}[];
|
|
44
47
|
};
|
|
45
48
|
|
|
46
49
|
constructor(core: CoValueCore) {
|
|
@@ -64,16 +67,17 @@ export class CoStream<
|
|
|
64
67
|
|
|
65
68
|
for (const {
|
|
66
69
|
txID,
|
|
70
|
+
madeAt,
|
|
67
71
|
changes,
|
|
68
72
|
} of this.core.getValidSortedTransactions()) {
|
|
69
|
-
for (const changeUntyped of changes) {
|
|
73
|
+
for (const changeUntyped of parseJSON(changes)) {
|
|
70
74
|
const change = changeUntyped as T;
|
|
71
75
|
let entries = this.items[txID.sessionID];
|
|
72
76
|
if (!entries) {
|
|
73
77
|
entries = [];
|
|
74
78
|
this.items[txID.sessionID] = entries;
|
|
75
79
|
}
|
|
76
|
-
entries.push(change);
|
|
80
|
+
entries.push({item: change, madeAt});
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
}
|
|
@@ -87,13 +91,57 @@ export class CoStream<
|
|
|
87
91
|
);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
return Object.values(this.items)[0];
|
|
94
|
+
return Object.values(this.items)[0]?.map(item => item.item);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getLastItemsPerAccount(): {[account: AccountID]: T | undefined} {
|
|
98
|
+
const result: {[account: AccountID]: {item: T, madeAt: number} | undefined} = {};
|
|
99
|
+
|
|
100
|
+
for (const [sessionID, items] of Object.entries(this.items)) {
|
|
101
|
+
const account = accountOrAgentIDfromSessionID(sessionID as SessionID);
|
|
102
|
+
if (!isAccountID(account)) continue;
|
|
103
|
+
if (items.length > 0) {
|
|
104
|
+
const lastItemOfSession = items[items.length - 1]!;
|
|
105
|
+
if (!result[account] || lastItemOfSession.madeAt > result[account]!.madeAt) {
|
|
106
|
+
result[account] = lastItemOfSession;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return Object.fromEntries(Object.entries(result).map(([account, item]) =>
|
|
112
|
+
[account, item?.item]
|
|
113
|
+
));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getLastItemFrom(account: AccountID): T | undefined {
|
|
117
|
+
let lastItem: {item: T, madeAt: number} | undefined;
|
|
118
|
+
|
|
119
|
+
for (const [sessionID, items] of Object.entries(this.items)) {
|
|
120
|
+
if (sessionID.startsWith(account)) {
|
|
121
|
+
if (items.length > 0) {
|
|
122
|
+
const lastItemOfSession = items[items.length - 1]!;
|
|
123
|
+
if (!lastItem || lastItemOfSession.madeAt > lastItem.madeAt) {
|
|
124
|
+
lastItem = lastItemOfSession;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return lastItem?.item;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getLastItemFromMe(): T | undefined {
|
|
134
|
+
const myAccountID = this.core.node.account.id;
|
|
135
|
+
if (!isAccountID(myAccountID)) return undefined;
|
|
136
|
+
return this.getLastItemFrom(myAccountID);
|
|
91
137
|
}
|
|
92
138
|
|
|
93
139
|
toJSON(): {
|
|
94
140
|
[key: SessionID]: T[];
|
|
95
141
|
} {
|
|
96
|
-
return this.items
|
|
142
|
+
return Object.fromEntries(Object.entries(this.items).map(([sessionID, items]) =>
|
|
143
|
+
[sessionID, items.map(item => item.item)]
|
|
144
|
+
));
|
|
97
145
|
}
|
|
98
146
|
|
|
99
147
|
subscribe(listener: (coMap: CoStream<T, Meta>) => void): () => void {
|
|
@@ -111,6 +159,8 @@ export class CoStream<
|
|
|
111
159
|
}
|
|
112
160
|
}
|
|
113
161
|
|
|
162
|
+
const binary_U_prefixLength = 8; // "binary_U".length;
|
|
163
|
+
|
|
114
164
|
export class BinaryCoStream<
|
|
115
165
|
Meta extends BinaryCoStreamMeta = { type: "binary" }
|
|
116
166
|
>
|
|
@@ -122,6 +172,7 @@ export class BinaryCoStream<
|
|
|
122
172
|
getBinaryChunks():
|
|
123
173
|
| (BinaryChunkInfo & { chunks: Uint8Array[]; finished: boolean })
|
|
124
174
|
| undefined {
|
|
175
|
+
const before = performance.now();
|
|
125
176
|
const items = this.getSingleStream();
|
|
126
177
|
|
|
127
178
|
if (!items) return;
|
|
@@ -135,15 +186,13 @@ export class BinaryCoStream<
|
|
|
135
186
|
|
|
136
187
|
const chunks: Uint8Array[] = [];
|
|
137
188
|
|
|
189
|
+
let finished = false;
|
|
190
|
+
let totalLength = 0;
|
|
191
|
+
|
|
138
192
|
for (const item of items.slice(1)) {
|
|
139
193
|
if (item.type === "end") {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
fileName: start.fileName,
|
|
143
|
-
totalSizeBytes: start.totalSizeBytes,
|
|
144
|
-
chunks,
|
|
145
|
-
finished: true,
|
|
146
|
-
};
|
|
194
|
+
finished = true;
|
|
195
|
+
break;
|
|
147
196
|
}
|
|
148
197
|
|
|
149
198
|
if (item.type !== "chunk") {
|
|
@@ -151,15 +200,25 @@ export class BinaryCoStream<
|
|
|
151
200
|
return undefined;
|
|
152
201
|
}
|
|
153
202
|
|
|
154
|
-
|
|
203
|
+
const chunk = base64URLtoBytes(
|
|
204
|
+
item.chunk.slice(binary_U_prefixLength)
|
|
205
|
+
);
|
|
206
|
+
totalLength += chunk.length;
|
|
207
|
+
chunks.push(chunk);
|
|
155
208
|
}
|
|
156
209
|
|
|
210
|
+
const after = performance.now();
|
|
211
|
+
console.log(
|
|
212
|
+
"getBinaryChunks bandwidth in MB/s",
|
|
213
|
+
(1000 * totalLength) / (after - before) / (1024 * 1024)
|
|
214
|
+
);
|
|
215
|
+
|
|
157
216
|
return {
|
|
158
217
|
mimeType: start.mimeType,
|
|
159
218
|
fileName: start.fileName,
|
|
160
219
|
totalSizeBytes: start.totalSizeBytes,
|
|
161
220
|
chunks,
|
|
162
|
-
finished
|
|
221
|
+
finished,
|
|
163
222
|
};
|
|
164
223
|
}
|
|
165
224
|
|
|
@@ -206,10 +265,7 @@ export class WriteableBinaryCoStream<
|
|
|
206
265
|
}
|
|
207
266
|
|
|
208
267
|
/** @internal */
|
|
209
|
-
push(
|
|
210
|
-
item: BinaryStreamItem,
|
|
211
|
-
privacy: "private" | "trusting" = "private"
|
|
212
|
-
) {
|
|
268
|
+
push(item: BinaryStreamItem, privacy: "private" | "trusting" = "private") {
|
|
213
269
|
WriteableCoStream.prototype.push.call(this, item, privacy);
|
|
214
270
|
}
|
|
215
271
|
|
|
@@ -230,13 +286,19 @@ export class WriteableBinaryCoStream<
|
|
|
230
286
|
chunk: Uint8Array,
|
|
231
287
|
privacy: "private" | "trusting" = "private"
|
|
232
288
|
) {
|
|
289
|
+
const before = performance.now();
|
|
233
290
|
this.push(
|
|
234
291
|
{
|
|
235
292
|
type: "chunk",
|
|
236
|
-
chunk: `
|
|
293
|
+
chunk: `binary_U${bytesToBase64url(chunk)}`,
|
|
237
294
|
} satisfies BinaryStreamChunk,
|
|
238
295
|
privacy
|
|
239
296
|
);
|
|
297
|
+
const after = performance.now();
|
|
298
|
+
console.log(
|
|
299
|
+
"pushBinaryStreamChunk bandwidth in MB/s",
|
|
300
|
+
(1000 * chunk.length) / (after - before) / (1024 * 1024)
|
|
301
|
+
);
|
|
240
302
|
}
|
|
241
303
|
|
|
242
304
|
endBinaryStream(privacy: "private" | "trusting" = "private") {
|
package/src/crypto.test.ts
CHANGED
|
@@ -21,6 +21,11 @@ import { xsalsa20_poly1305 } from "@noble/ciphers/salsa";
|
|
|
21
21
|
import { blake3 } from "@noble/hashes/blake3";
|
|
22
22
|
import stableStringify from "fast-json-stable-stringify";
|
|
23
23
|
import { SessionID } from './ids.js';
|
|
24
|
+
import { cojsonReady } from './index.js';
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
await cojsonReady;
|
|
28
|
+
});
|
|
24
29
|
|
|
25
30
|
test("Signatures round-trip and use stable stringify", () => {
|
|
26
31
|
const data = { b: "world", a: "hello" };
|
package/src/crypto.ts
CHANGED
|
@@ -2,12 +2,39 @@ import { ed25519, x25519 } from "@noble/curves/ed25519";
|
|
|
2
2
|
import { xsalsa20_poly1305, xsalsa20 } from "@noble/ciphers/salsa";
|
|
3
3
|
import { JsonValue } from "./jsonValue.js";
|
|
4
4
|
import { base58 } from "@scure/base";
|
|
5
|
-
import stableStringify from "fast-json-stable-stringify";
|
|
6
|
-
import { blake3 } from "@noble/hashes/blake3";
|
|
7
5
|
import { randomBytes } from "@noble/ciphers/webcrypto/utils";
|
|
8
6
|
import { AgentID, RawCoID, TransactionID } from "./ids.js";
|
|
9
7
|
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
|
|
10
8
|
|
|
9
|
+
import { createBLAKE3 } from 'hash-wasm';
|
|
10
|
+
import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
|
|
11
|
+
|
|
12
|
+
let blake3Instance: Awaited<ReturnType<typeof createBLAKE3>>;
|
|
13
|
+
let blake3HashOnce: (data: Uint8Array) => Uint8Array;
|
|
14
|
+
let blake3HashOnceWithContext: (data: Uint8Array, {context}: {context: Uint8Array}) => Uint8Array;
|
|
15
|
+
let blake3incrementalUpdateSLOW_WITH_DEVTOOLS: (state: Uint8Array, data: Uint8Array) => Uint8Array;
|
|
16
|
+
let blake3digestForState: (state: Uint8Array) => Uint8Array;
|
|
17
|
+
|
|
18
|
+
export const cryptoReady = new Promise<void>((resolve) => {
|
|
19
|
+
createBLAKE3().then(bl3 => {
|
|
20
|
+
blake3Instance = bl3;
|
|
21
|
+
blake3HashOnce = (data) => {
|
|
22
|
+
return bl3.init().update(data).digest('binary');
|
|
23
|
+
}
|
|
24
|
+
blake3HashOnceWithContext = (data, {context}) => {
|
|
25
|
+
return bl3.init().update(context).update(data).digest('binary');
|
|
26
|
+
}
|
|
27
|
+
blake3incrementalUpdateSLOW_WITH_DEVTOOLS = (state, data) => {
|
|
28
|
+
bl3.load(state).update(data);
|
|
29
|
+
return bl3.save();
|
|
30
|
+
}
|
|
31
|
+
blake3digestForState = (state) => {
|
|
32
|
+
return bl3.load(state).digest('binary');
|
|
33
|
+
}
|
|
34
|
+
resolve();
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
|
|
11
38
|
export type SignerSecret = `signerSecret_z${string}`;
|
|
12
39
|
export type SignerID = `signer_z${string}`;
|
|
13
40
|
export type Signature = `signature_z${string}`;
|
|
@@ -128,7 +155,7 @@ export function seal<T extends JsonValue>(
|
|
|
128
155
|
to: SealerID,
|
|
129
156
|
nOnceMaterial: { in: RawCoID; tx: TransactionID }
|
|
130
157
|
): Sealed<T> {
|
|
131
|
-
const nOnce =
|
|
158
|
+
const nOnce = blake3HashOnce(
|
|
132
159
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
|
133
160
|
).slice(0, 24);
|
|
134
161
|
|
|
@@ -153,7 +180,7 @@ export function unseal<T extends JsonValue>(
|
|
|
153
180
|
from: SealerID,
|
|
154
181
|
nOnceMaterial: { in: RawCoID; tx: TransactionID }
|
|
155
182
|
): T | undefined {
|
|
156
|
-
const nOnce =
|
|
183
|
+
const nOnce = blake3HashOnce(
|
|
157
184
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
|
158
185
|
).slice(0, 24);
|
|
159
186
|
|
|
@@ -181,28 +208,32 @@ export type Hash = `hash_z${string}`;
|
|
|
181
208
|
|
|
182
209
|
export function secureHash(value: JsonValue): Hash {
|
|
183
210
|
return `hash_z${base58.encode(
|
|
184
|
-
|
|
211
|
+
blake3HashOnce(textEncoder.encode(stableStringify(value)))
|
|
185
212
|
)}`;
|
|
186
213
|
}
|
|
187
214
|
|
|
188
215
|
export class StreamingHash {
|
|
189
|
-
state:
|
|
216
|
+
state: Uint8Array;
|
|
190
217
|
|
|
191
|
-
constructor(fromClone?:
|
|
192
|
-
this.state = fromClone ||
|
|
218
|
+
constructor(fromClone?: Uint8Array) {
|
|
219
|
+
this.state = fromClone || blake3Instance.init().save();
|
|
193
220
|
}
|
|
194
221
|
|
|
195
222
|
update(value: JsonValue) {
|
|
196
|
-
|
|
223
|
+
const encoded = textEncoder.encode(stableStringify(value))
|
|
224
|
+
// const before = performance.now();
|
|
225
|
+
this.state = blake3incrementalUpdateSLOW_WITH_DEVTOOLS(this.state, encoded);
|
|
226
|
+
// const after = performance.now();
|
|
227
|
+
// console.log(`Hashing throughput in MB/s`, 1000 * (encoded.length / (after - before)) / (1024 * 1024));
|
|
197
228
|
}
|
|
198
229
|
|
|
199
230
|
digest(): Hash {
|
|
200
|
-
const hash = this.state
|
|
231
|
+
const hash = blake3digestForState(this.state);
|
|
201
232
|
return `hash_z${base58.encode(hash)}`;
|
|
202
233
|
}
|
|
203
234
|
|
|
204
235
|
clone(): StreamingHash {
|
|
205
|
-
return new StreamingHash(this.state
|
|
236
|
+
return new StreamingHash(new Uint8Array(this.state));
|
|
206
237
|
}
|
|
207
238
|
}
|
|
208
239
|
|
|
@@ -211,7 +242,7 @@ export const shortHashLength = 19;
|
|
|
211
242
|
|
|
212
243
|
export function shortHash(value: JsonValue): ShortHash {
|
|
213
244
|
return `shortHash_z${base58.encode(
|
|
214
|
-
|
|
245
|
+
blake3HashOnce(textEncoder.encode(stableStringify(value))).slice(
|
|
215
246
|
0,
|
|
216
247
|
shortHashLength
|
|
217
248
|
)
|
|
@@ -241,7 +272,7 @@ function encrypt<T extends JsonValue, N extends JsonValue>(
|
|
|
241
272
|
const keySecretBytes = base58.decode(
|
|
242
273
|
keySecret.substring("keySecret_z".length)
|
|
243
274
|
);
|
|
244
|
-
const nOnce =
|
|
275
|
+
const nOnce = blake3HashOnce(
|
|
245
276
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
|
246
277
|
).slice(0, 24);
|
|
247
278
|
|
|
@@ -285,15 +316,15 @@ export function encryptKeySecret(keys: {
|
|
|
285
316
|
};
|
|
286
317
|
}
|
|
287
318
|
|
|
288
|
-
function
|
|
319
|
+
function decryptRaw<T extends JsonValue, N extends JsonValue>(
|
|
289
320
|
encrypted: Encrypted<T, N>,
|
|
290
321
|
keySecret: KeySecret,
|
|
291
322
|
nOnceMaterial: N
|
|
292
|
-
): T
|
|
323
|
+
): Stringified<T> {
|
|
293
324
|
const keySecretBytes = base58.decode(
|
|
294
325
|
keySecret.substring("keySecret_z".length)
|
|
295
326
|
);
|
|
296
|
-
const nOnce =
|
|
327
|
+
const nOnce = blake3HashOnce(
|
|
297
328
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
|
298
329
|
).slice(0, 24);
|
|
299
330
|
|
|
@@ -302,13 +333,31 @@ function decrypt<T extends JsonValue, N extends JsonValue>(
|
|
|
302
333
|
);
|
|
303
334
|
const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
|
|
304
335
|
|
|
336
|
+
return textDecoder.decode(plaintext) as Stringified<T>;
|
|
337
|
+
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function decrypt<T extends JsonValue, N extends JsonValue>(
|
|
341
|
+
encrypted: Encrypted<T, N>,
|
|
342
|
+
keySecret: KeySecret,
|
|
343
|
+
nOnceMaterial: N
|
|
344
|
+
): T | undefined {
|
|
305
345
|
try {
|
|
306
|
-
return
|
|
346
|
+
return parseJSON(decryptRaw(encrypted, keySecret, nOnceMaterial));
|
|
307
347
|
} catch (e) {
|
|
348
|
+
console.error("Decryption error", e)
|
|
308
349
|
return undefined;
|
|
309
350
|
}
|
|
310
351
|
}
|
|
311
352
|
|
|
353
|
+
export function decryptRawForTransaction<T extends JsonValue>(
|
|
354
|
+
encrypted: Encrypted<T, { in: RawCoID; tx: TransactionID }>,
|
|
355
|
+
keySecret: KeySecret,
|
|
356
|
+
nOnceMaterial: { in: RawCoID; tx: TransactionID }
|
|
357
|
+
): Stringified<T> | undefined {
|
|
358
|
+
return decryptRaw(encrypted, keySecret, nOnceMaterial);
|
|
359
|
+
}
|
|
360
|
+
|
|
312
361
|
export function decryptForTransaction<T extends JsonValue>(
|
|
313
362
|
encrypted: Encrypted<T, { in: RawCoID; tx: TransactionID }>,
|
|
314
363
|
keySecret: KeySecret,
|
|
@@ -365,11 +414,11 @@ export function agentSecretFromSecretSeed(secretSeed: Uint8Array): AgentSecret {
|
|
|
365
414
|
}
|
|
366
415
|
|
|
367
416
|
return `sealerSecret_z${base58.encode(
|
|
368
|
-
|
|
417
|
+
blake3HashOnceWithContext(secretSeed, {
|
|
369
418
|
context: textEncoder.encode("seal"),
|
|
370
419
|
})
|
|
371
420
|
)}/signerSecret_z${base58.encode(
|
|
372
|
-
|
|
421
|
+
blake3HashOnceWithContext(secretSeed, {
|
|
373
422
|
context: textEncoder.encode("sign"),
|
|
374
423
|
})
|
|
375
424
|
)}`;
|
package/src/group.test.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { LocalNode, CoMap, CoList, CoStream, BinaryCoStream } from "./index";
|
|
1
|
+
import { LocalNode, CoMap, CoList, CoStream, BinaryCoStream, cojsonReady } from "./index";
|
|
2
2
|
import { randomAnonymousAccountAndSessionID } from "./testUtils";
|
|
3
3
|
|
|
4
|
+
beforeEach(async () => {
|
|
5
|
+
await cojsonReady;
|
|
6
|
+
});
|
|
7
|
+
|
|
4
8
|
test("Can create a CoMap in a group", () => {
|
|
5
9
|
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
|
6
10
|
|
package/src/group.ts
CHANGED
|
@@ -238,7 +238,7 @@ export class Group {
|
|
|
238
238
|
|
|
239
239
|
/** Creates a new `CoMap` within this group, with the specified specialized
|
|
240
240
|
* `CoMap` type `M` and optional static metadata. */
|
|
241
|
-
createMap<M extends CoMap<{ [key: string]: JsonValue }, JsonObject | null>>(
|
|
241
|
+
createMap<M extends CoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>>(
|
|
242
242
|
meta?: M["meta"]
|
|
243
243
|
): M {
|
|
244
244
|
return this.node
|
package/src/index.ts
CHANGED
|
@@ -18,12 +18,14 @@ import {
|
|
|
18
18
|
agentSecretFromSecretSeed,
|
|
19
19
|
secretSeedLength,
|
|
20
20
|
shortHashLength,
|
|
21
|
+
cryptoReady
|
|
21
22
|
} from "./crypto.js";
|
|
22
23
|
import { connectedPeers } from "./streamUtils.js";
|
|
23
24
|
import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
|
|
24
25
|
import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
|
|
25
26
|
import { Group, expectGroupContent } from "./group.js";
|
|
26
27
|
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
|
|
28
|
+
import { parseJSON } from "./jsonStringify.js";
|
|
27
29
|
|
|
28
30
|
import type { SessionID, AgentID } from "./ids.js";
|
|
29
31
|
import type { CoID, CoValueImpl } from "./coValue.js";
|
|
@@ -33,6 +35,7 @@ import type { SyncMessage, Peer } from "./sync.js";
|
|
|
33
35
|
import type { AgentSecret } from "./crypto.js";
|
|
34
36
|
import type { AccountID, Profile } from "./account.js";
|
|
35
37
|
import type { InviteSecret } from "./group.js";
|
|
38
|
+
import type * as Media from "./media.js";
|
|
36
39
|
|
|
37
40
|
type Value = JsonValue | CoValueImpl;
|
|
38
41
|
|
|
@@ -52,7 +55,8 @@ export const cojsonInternals = {
|
|
|
52
55
|
shortHashLength,
|
|
53
56
|
expectGroupContent,
|
|
54
57
|
base64URLtoBytes,
|
|
55
|
-
bytesToBase64url
|
|
58
|
+
bytesToBase64url,
|
|
59
|
+
parseJSON
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
export {
|
|
@@ -69,6 +73,7 @@ export {
|
|
|
69
73
|
CoValueCore,
|
|
70
74
|
AnonymousControlledAccount,
|
|
71
75
|
ControlledAccount,
|
|
76
|
+
cryptoReady as cojsonReady,
|
|
72
77
|
};
|
|
73
78
|
|
|
74
79
|
export type {
|
|
@@ -88,6 +93,7 @@ export type {
|
|
|
88
93
|
AgentSecret,
|
|
89
94
|
InviteSecret,
|
|
90
95
|
SyncMessage,
|
|
96
|
+
Media
|
|
91
97
|
};
|
|
92
98
|
|
|
93
99
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// adapted from fast-json-stable-stringify (https://github.com/epoberezkin/fast-json-stable-stringify)
|
|
2
|
+
|
|
3
|
+
export type Stringified<T> = string & { __type: T };
|
|
4
|
+
|
|
5
|
+
export function stableStringify<T>(data: T): Stringified<T>
|
|
6
|
+
export function stableStringify(data: undefined): undefined
|
|
7
|
+
export function stableStringify<T>(data: T | undefined): Stringified<T> | undefined {
|
|
8
|
+
const cycles = false;
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
const seen: any[] = [];
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
let node = data as any;
|
|
14
|
+
|
|
15
|
+
if (node && node.toJSON && typeof node.toJSON === "function") {
|
|
16
|
+
node = node.toJSON();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (node === undefined) return;
|
|
20
|
+
if (typeof node == "number")
|
|
21
|
+
return (isFinite(node) ? "" + node : "null") as Stringified<T>;
|
|
22
|
+
if (typeof node !== "object") {
|
|
23
|
+
if (
|
|
24
|
+
typeof node === "string" &&
|
|
25
|
+
(node.startsWith("encrypted_U") || node.startsWith("binary_U"))
|
|
26
|
+
) {
|
|
27
|
+
return `"${node}"` as Stringified<T>;
|
|
28
|
+
}
|
|
29
|
+
return JSON.stringify(node) as Stringified<T>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let i, out;
|
|
33
|
+
if (Array.isArray(node)) {
|
|
34
|
+
out = "[";
|
|
35
|
+
for (i = 0; i < node.length; i++) {
|
|
36
|
+
if (i) out += ",";
|
|
37
|
+
out += stableStringify(node[i]) || "null";
|
|
38
|
+
}
|
|
39
|
+
return (out + "]") as Stringified<T>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (node === null) return "null" as Stringified<T>;
|
|
43
|
+
|
|
44
|
+
if (seen.indexOf(node) !== -1) {
|
|
45
|
+
if (cycles) return JSON.stringify("__cycle__") as Stringified<T>;
|
|
46
|
+
throw new TypeError("Converting circular structure to JSON");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const seenIndex = seen.push(node) - 1;
|
|
50
|
+
const keys = Object.keys(node).sort();
|
|
51
|
+
out = "";
|
|
52
|
+
for (i = 0; i < keys.length; i++) {
|
|
53
|
+
const key = keys[i]!;
|
|
54
|
+
const value = stableStringify(node[key]);
|
|
55
|
+
|
|
56
|
+
if (!value) continue;
|
|
57
|
+
if (out) out += ",";
|
|
58
|
+
out += JSON.stringify(key) + ":" + value;
|
|
59
|
+
}
|
|
60
|
+
seen.splice(seenIndex, 1);
|
|
61
|
+
return ("{" + out + "}") as Stringified<T>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseJSON<T>(json: Stringified<T>): T {
|
|
65
|
+
return JSON.parse(json);
|
|
66
|
+
}
|
package/src/jsonValue.ts
CHANGED
|
@@ -3,4 +3,4 @@ import { RawCoID } from './ids.js';
|
|
|
3
3
|
export type JsonAtom = string | number | boolean | null;
|
|
4
4
|
export type JsonValue = JsonAtom | JsonArray | JsonObject | RawCoID;
|
|
5
5
|
export type JsonArray = JsonValue[];
|
|
6
|
-
export type JsonObject = { [key: string]: JsonValue; };
|
|
6
|
+
export type JsonObject = { [key: string]: JsonValue | undefined; };
|
package/src/media.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CoMap } from './coValues/coMap.js'
|
|
2
|
+
import { CoID } from './coValue.js'
|
|
3
|
+
import { BinaryCoStream } from './coValues/coStream.js'
|
|
4
|
+
|
|
5
|
+
export type ImageDefinition = CoMap<{
|
|
6
|
+
originalSize: [number, number];
|
|
7
|
+
placeholderDataURL?: string;
|
|
8
|
+
[res: `${number}x${number}`]: CoID<BinaryCoStream>;
|
|
9
|
+
}>;
|
package/src/node.ts
CHANGED
package/src/permissions.test.ts
CHANGED
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
groupWithTwoAdmins,
|
|
18
18
|
groupWithTwoAdminsHighLevel,
|
|
19
19
|
} from "./testUtils.js";
|
|
20
|
-
import { AnonymousControlledAccount } from "./index.js";
|
|
20
|
+
import { AnonymousControlledAccount, cojsonReady } from "./index.js";
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
await cojsonReady;
|
|
24
|
+
});
|
|
21
25
|
|
|
22
26
|
test("Initial admin can add another admin to a group", () => {
|
|
23
27
|
groupWithTwoAdmins();
|
package/src/permissions.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
AccountID,
|
|
16
16
|
Profile,
|
|
17
17
|
} from "./account.js";
|
|
18
|
+
import { parseJSON } from "./jsonStringify.js";
|
|
18
19
|
|
|
19
20
|
export type PermissionsDef =
|
|
20
21
|
| { type: "group"; initialAdmin: AccountID | AgentID }
|
|
@@ -76,11 +77,13 @@ export function determineValidTransactions(
|
|
|
76
77
|
// console.log("before", { memberState, validTransactions });
|
|
77
78
|
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
|
78
79
|
|
|
79
|
-
const
|
|
80
|
+
const changes = parseJSON(tx.changes)
|
|
81
|
+
|
|
82
|
+
const change = changes[0] as
|
|
80
83
|
| MapOpPayload<AccountID | AgentID, Role>
|
|
81
84
|
| MapOpPayload<"readKey", JsonValue>
|
|
82
85
|
| MapOpPayload<"profile", CoID<Profile>>;
|
|
83
|
-
if (
|
|
86
|
+
if (changes.length !== 1) {
|
|
84
87
|
console.warn("Group transaction must have exactly one change");
|
|
85
88
|
continue;
|
|
86
89
|
}
|