cojson 0.1.9 → 0.1.11

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.
@@ -1,16 +1,53 @@
1
- import { JsonObject, JsonValue } from '../jsonValue.js';
2
- import { CoID, ReadableCoValue } from '../coValue.js';
3
- import { CoValueCore } from '../coValueCore.js';
4
- import { Group } from '../index.js';
1
+ import { JsonObject, JsonValue } from "../jsonValue.js";
2
+ import { CoID, ReadableCoValue, WriteableCoValue } from "../coValue.js";
3
+ import { CoValueCore } from "../coValueCore.js";
4
+ import { Group } from "../group.js";
5
+ import { SessionID } from "../ids.js";
6
+ import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
5
7
 
6
- export class CoStream<T extends JsonValue, Meta extends JsonObject | null = null> implements ReadableCoValue {
8
+ export type BinaryChunkInfo = {
9
+ mimeType: string;
10
+ fileName?: string;
11
+ totalSizeBytes?: number;
12
+ };
13
+
14
+ export type BinaryStreamStart = {
15
+ type: "start";
16
+ } & BinaryChunkInfo;
17
+
18
+ export type BinaryStreamChunk = {
19
+ type: "chunk";
20
+ chunk: `U${string}`;
21
+ };
22
+
23
+ export type BinaryStreamEnd = {
24
+ type: "end";
25
+ };
26
+
27
+ export type BinaryCoStreamMeta = JsonObject & { type: "binary" };
28
+
29
+ export type BinaryStreamItem =
30
+ | BinaryStreamStart
31
+ | BinaryStreamChunk
32
+ | BinaryStreamEnd;
33
+
34
+ export class CoStream<
35
+ T extends JsonValue,
36
+ Meta extends JsonObject | null = null
37
+ > implements ReadableCoValue
38
+ {
7
39
  id: CoID<CoStream<T, Meta>>;
8
40
  type = "costream" as const;
9
41
  core: CoValueCore;
42
+ items: {
43
+ [key: SessionID]: T[];
44
+ };
10
45
 
11
46
  constructor(core: CoValueCore) {
12
47
  this.id = core.id as CoID<CoStream<T, Meta>>;
13
48
  this.core = core;
49
+ this.items = {};
50
+ this.fillFromCoValue();
14
51
  }
15
52
 
16
53
  get meta(): Meta {
@@ -21,8 +58,42 @@ export class CoStream<T extends JsonValue, Meta extends JsonObject | null = null
21
58
  return this.core.getGroup();
22
59
  }
23
60
 
24
- toJSON(): JsonObject {
25
- throw new Error("Method not implemented.");
61
+ /** @internal */
62
+ protected fillFromCoValue() {
63
+ this.items = {};
64
+
65
+ for (const {
66
+ txID,
67
+ changes,
68
+ } of this.core.getValidSortedTransactions()) {
69
+ for (const changeUntyped of changes) {
70
+ const change = changeUntyped as T;
71
+ let entries = this.items[txID.sessionID];
72
+ if (!entries) {
73
+ entries = [];
74
+ this.items[txID.sessionID] = entries;
75
+ }
76
+ entries.push(change);
77
+ }
78
+ }
79
+ }
80
+
81
+ getSingleStream(): T[] | undefined {
82
+ if (Object.keys(this.items).length === 0) {
83
+ return undefined;
84
+ } else if (Object.keys(this.items).length !== 1) {
85
+ throw new Error(
86
+ "CoStream.getSingleStream() can only be called when there is exactly one stream"
87
+ );
88
+ }
89
+
90
+ return Object.values(this.items)[0];
91
+ }
92
+
93
+ toJSON(): {
94
+ [key: SessionID]: T[];
95
+ } {
96
+ return this.items;
26
97
  }
27
98
 
28
99
  subscribe(listener: (coMap: CoStream<T, Meta>) => void): () => void {
@@ -30,4 +101,150 @@ export class CoStream<T extends JsonValue, Meta extends JsonObject | null = null
30
101
  listener(content as CoStream<T, Meta>);
31
102
  });
32
103
  }
104
+
105
+ edit(
106
+ changer: (editable: WriteableCoStream<T, Meta>) => void
107
+ ): CoStream<T, Meta> {
108
+ const editable = new WriteableCoStream<T, Meta>(this.core);
109
+ changer(editable);
110
+ return new CoStream(this.core);
111
+ }
112
+ }
113
+
114
+ export class BinaryCoStream<
115
+ Meta extends BinaryCoStreamMeta = { type: "binary" }
116
+ >
117
+ extends CoStream<BinaryStreamItem, Meta>
118
+ implements ReadableCoValue
119
+ {
120
+ id!: CoID<BinaryCoStream<Meta>>;
121
+
122
+ getBinaryChunks():
123
+ | (BinaryChunkInfo & { chunks: Uint8Array[]; finished: boolean })
124
+ | undefined {
125
+ const items = this.getSingleStream();
126
+
127
+ if (!items) return;
128
+
129
+ const start = items[0];
130
+
131
+ if (start?.type !== "start") {
132
+ console.error("Invalid binary stream start", start);
133
+ return;
134
+ }
135
+
136
+ const chunks: Uint8Array[] = [];
137
+
138
+ for (const item of items.slice(1)) {
139
+ if (item.type === "end") {
140
+ return {
141
+ mimeType: start.mimeType,
142
+ fileName: start.fileName,
143
+ totalSizeBytes: start.totalSizeBytes,
144
+ chunks,
145
+ finished: true,
146
+ };
147
+ }
148
+
149
+ if (item.type !== "chunk") {
150
+ console.error("Invalid binary stream chunk", item);
151
+ return undefined;
152
+ }
153
+
154
+ chunks.push(base64URLtoBytes(item.chunk.slice(1)));
155
+ }
156
+
157
+ return {
158
+ mimeType: start.mimeType,
159
+ fileName: start.fileName,
160
+ totalSizeBytes: start.totalSizeBytes,
161
+ chunks,
162
+ finished: false,
163
+ };
164
+ }
165
+
166
+ edit(
167
+ changer: (editable: WriteableBinaryCoStream<Meta>) => void
168
+ ): BinaryCoStream<Meta> {
169
+ const editable = new WriteableBinaryCoStream<Meta>(this.core);
170
+ changer(editable);
171
+ return new BinaryCoStream(this.core);
172
+ }
173
+ }
174
+
175
+ export class WriteableCoStream<
176
+ T extends JsonValue,
177
+ Meta extends JsonObject | null = null
178
+ >
179
+ extends CoStream<T, Meta>
180
+ implements WriteableCoValue
181
+ {
182
+ /** @internal */
183
+ edit(
184
+ _changer: (editable: WriteableCoStream<T, Meta>) => void
185
+ ): CoStream<T, Meta> {
186
+ throw new Error("Already editing.");
187
+ }
188
+
189
+ push(item: T, privacy: "private" | "trusting" = "private") {
190
+ this.core.makeTransaction([item], privacy);
191
+ this.fillFromCoValue();
192
+ }
193
+ }
194
+
195
+ export class WriteableBinaryCoStream<
196
+ Meta extends BinaryCoStreamMeta = { type: "binary" }
197
+ >
198
+ extends BinaryCoStream<Meta>
199
+ implements WriteableCoValue
200
+ {
201
+ /** @internal */
202
+ edit(
203
+ _changer: (editable: WriteableBinaryCoStream<Meta>) => void
204
+ ): BinaryCoStream<Meta> {
205
+ throw new Error("Already editing.");
206
+ }
207
+
208
+ /** @internal */
209
+ push(
210
+ item: BinaryStreamItem,
211
+ privacy: "private" | "trusting" = "private"
212
+ ) {
213
+ WriteableCoStream.prototype.push.call(this, item, privacy);
214
+ }
215
+
216
+ startBinaryStream(
217
+ settings: BinaryChunkInfo,
218
+ privacy: "private" | "trusting" = "private"
219
+ ) {
220
+ this.push(
221
+ {
222
+ type: "start",
223
+ ...settings,
224
+ } satisfies BinaryStreamStart,
225
+ privacy
226
+ );
227
+ }
228
+
229
+ pushBinaryStreamChunk(
230
+ chunk: Uint8Array,
231
+ privacy: "private" | "trusting" = "private"
232
+ ) {
233
+ this.push(
234
+ {
235
+ type: "chunk",
236
+ chunk: `U${bytesToBase64url(chunk)}`,
237
+ } satisfies BinaryStreamChunk,
238
+ privacy
239
+ );
240
+ }
241
+
242
+ endBinaryStream(privacy: "private" | "trusting" = "private") {
243
+ this.push(
244
+ {
245
+ type: "end",
246
+ } satisfies BinaryStreamEnd,
247
+ privacy
248
+ );
249
+ }
33
250
  }
package/src/crypto.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  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
- import { base58, base64url } from "@scure/base";
4
+ import { base58 } from "@scure/base";
5
5
  import stableStringify from "fast-json-stable-stringify";
6
6
  import { blake3 } from "@noble/hashes/blake3";
7
7
  import { randomBytes } from "@noble/ciphers/webcrypto/utils";
8
8
  import { AgentID, RawCoID, TransactionID } from "./ids.js";
9
+ import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
9
10
 
10
11
  export type SignerSecret = `signerSecret_z${string}`;
11
12
  export type SignerID = `signer_z${string}`;
@@ -143,7 +144,7 @@ export function seal<T extends JsonValue>(
143
144
  plaintext
144
145
  );
145
146
 
146
- return `sealed_U${base64url.encode(sealedBytes)}` as Sealed<T>;
147
+ return `sealed_U${bytesToBase64url(sealedBytes)}` as Sealed<T>;
147
148
  }
148
149
 
149
150
  export function unseal<T extends JsonValue>(
@@ -160,7 +161,7 @@ export function unseal<T extends JsonValue>(
160
161
 
161
162
  const senderPub = base58.decode(from.substring("sealer_z".length));
162
163
 
163
- const sealedBytes = base64url.decode(sealed.substring("sealed_U".length));
164
+ const sealedBytes = base64URLtoBytes(sealed.substring("sealed_U".length));
164
165
 
165
166
  const sharedSecret = x25519.getSharedSecret(sealerPriv, senderPub);
166
167
 
@@ -210,7 +211,10 @@ export const shortHashLength = 19;
210
211
 
211
212
  export function shortHash(value: JsonValue): ShortHash {
212
213
  return `shortHash_z${base58.encode(
213
- blake3(textEncoder.encode(stableStringify(value))).slice(0, shortHashLength)
214
+ blake3(textEncoder.encode(stableStringify(value))).slice(
215
+ 0,
216
+ shortHashLength
217
+ )
214
218
  )}`;
215
219
  }
216
220
 
@@ -243,7 +247,7 @@ function encrypt<T extends JsonValue, N extends JsonValue>(
243
247
 
244
248
  const plaintext = textEncoder.encode(stableStringify(value));
245
249
  const ciphertext = xsalsa20(keySecretBytes, nOnce, plaintext);
246
- return `encrypted_U${base64url.encode(ciphertext)}` as Encrypted<T, N>;
250
+ return `encrypted_U${bytesToBase64url(ciphertext)}` as Encrypted<T, N>;
247
251
  }
248
252
 
249
253
  export function encryptForTransaction<T extends JsonValue>(
@@ -293,7 +297,7 @@ function decrypt<T extends JsonValue, N extends JsonValue>(
293
297
  textEncoder.encode(stableStringify(nOnceMaterial))
294
298
  ).slice(0, 24);
295
299
 
296
- const ciphertext = base64url.decode(
300
+ const ciphertext = base64URLtoBytes(
297
301
  encrypted.substring("encrypted_U".length)
298
302
  );
299
303
  const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
@@ -355,7 +359,9 @@ export function newRandomSecretSeed(): Uint8Array {
355
359
 
356
360
  export function agentSecretFromSecretSeed(secretSeed: Uint8Array): AgentSecret {
357
361
  if (secretSeed.length !== secretSeedLength) {
358
- throw new Error(`Secret seed needs to be ${secretSeedLength} bytes long`);
362
+ throw new Error(
363
+ `Secret seed needs to be ${secretSeedLength} bytes long`
364
+ );
359
365
  }
360
366
 
361
367
  return `sealerSecret_z${base58.encode(
@@ -0,0 +1,47 @@
1
+ import { LocalNode, CoMap, CoList, CoStream, BinaryCoStream } from "./index";
2
+ import { randomAnonymousAccountAndSessionID } from "./testUtils";
3
+
4
+ test("Can create a CoMap in a group", () => {
5
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID());
6
+
7
+ const group = node.createGroup();
8
+
9
+ const map = group.createMap();
10
+
11
+ expect(map.core.getCurrentContent().type).toEqual("comap");
12
+ expect(map instanceof CoMap).toEqual(true);
13
+ });
14
+
15
+ test("Can create a CoList in a group", () => {
16
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID());
17
+
18
+ const group = node.createGroup();
19
+
20
+ const list = group.createList();
21
+
22
+ expect(list.core.getCurrentContent().type).toEqual("colist");
23
+ expect(list instanceof CoList).toEqual(true);
24
+ })
25
+
26
+ test("Can create a CoStream in a group", () => {
27
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID());
28
+
29
+ const group = node.createGroup();
30
+
31
+ const stream = group.createStream();
32
+
33
+ expect(stream.core.getCurrentContent().type).toEqual("costream");
34
+ expect(stream instanceof CoStream).toEqual(true);
35
+ });
36
+
37
+ test("Can create a BinaryCoStream in a group", () => {
38
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID());
39
+
40
+ const group = node.createGroup();
41
+
42
+ const stream = group.createBinaryStream();
43
+
44
+ expect(stream.core.getCurrentContent().type).toEqual("costream");
45
+ expect(stream.meta.type).toEqual("binary");
46
+ expect(stream instanceof BinaryCoStream).toEqual(true);
47
+ })
package/src/group.ts CHANGED
@@ -21,6 +21,7 @@ import { AccountID, GeneralizedControlledAccount, Profile } from "./account.js";
21
21
  import { Role } from "./permissions.js";
22
22
  import { base58 } from "@scure/base";
23
23
  import { CoList } from "./coValues/coList.js";
24
+ import { BinaryCoStream, BinaryCoStreamMeta, CoStream } from "./coValues/coStream.js";
24
25
 
25
26
  export type GroupContent = {
26
27
  profile: CoID<Profile> | null;
@@ -271,6 +272,38 @@ export class Group {
271
272
  .getCurrentContent() as L;
272
273
  }
273
274
 
275
+ createStream<C extends CoStream<JsonValue, JsonObject | null>>(
276
+ meta?: C["meta"]
277
+ ): C {
278
+ return this.node
279
+ .createCoValue({
280
+ type: "costream",
281
+ ruleset: {
282
+ type: "ownedByGroup",
283
+ group: this.underlyingMap.id,
284
+ },
285
+ meta: meta || null,
286
+ ...createdNowUnique(),
287
+ })
288
+ .getCurrentContent() as C;
289
+ }
290
+
291
+ createBinaryStream<
292
+ C extends BinaryCoStream<BinaryCoStreamMeta>
293
+ >(meta: C["meta"] = { type: "binary" }): C {
294
+ return this.node
295
+ .createCoValue({
296
+ type: "costream",
297
+ ruleset: {
298
+ type: "ownedByGroup",
299
+ group: this.underlyingMap.id,
300
+ },
301
+ meta: meta,
302
+ ...createdNowUnique(),
303
+ })
304
+ .getCurrentContent() as C;
305
+ }
306
+
274
307
  /** @internal */
275
308
  testWithDifferentAccount(
276
309
  account: GeneralizedControlledAccount,
package/src/index.ts CHANGED
@@ -3,6 +3,12 @@ import { LocalNode } from "./node.js";
3
3
  import type { CoValue, ReadableCoValue } from "./coValue.js";
4
4
  import { CoMap, WriteableCoMap } from "./coValues/coMap.js";
5
5
  import { CoList, WriteableCoList } from "./coValues/coList.js";
6
+ import {
7
+ CoStream,
8
+ WriteableCoStream,
9
+ BinaryCoStream,
10
+ WriteableBinaryCoStream,
11
+ } from "./coValues/coStream.js";
6
12
  import {
7
13
  agentSecretFromBytes,
8
14
  agentSecretToBytes,
@@ -17,9 +23,11 @@ import { connectedPeers } from "./streamUtils.js";
17
23
  import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
18
24
  import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
19
25
  import { Group, expectGroupContent } from "./group.js";
26
+ import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
20
27
 
21
28
  import type { SessionID, AgentID } from "./ids.js";
22
29
  import type { CoID, CoValueImpl } from "./coValue.js";
30
+ import type { BinaryChunkInfo, BinaryCoStreamMeta } from "./coValues/coStream.js";
23
31
  import type { JsonValue } from "./jsonValue.js";
24
32
  import type { SyncMessage, Peer } from "./sync.js";
25
33
  import type { AgentSecret } from "./crypto.js";
@@ -43,6 +51,8 @@ export const cojsonInternals = {
43
51
  secretSeedLength,
44
52
  shortHashLength,
45
53
  expectGroupContent,
54
+ base64URLtoBytes,
55
+ bytesToBase64url
46
56
  };
47
57
 
48
58
  export {
@@ -52,6 +62,10 @@ export {
52
62
  WriteableCoMap,
53
63
  CoList,
54
64
  WriteableCoList,
65
+ CoStream,
66
+ WriteableCoStream,
67
+ BinaryCoStream,
68
+ WriteableBinaryCoStream,
55
69
  CoValueCore,
56
70
  AnonymousControlledAccount,
57
71
  ControlledAccount,
@@ -68,6 +82,8 @@ export type {
68
82
  Profile,
69
83
  SessionID,
70
84
  Peer,
85
+ BinaryChunkInfo,
86
+ BinaryCoStreamMeta,
71
87
  AgentID,
72
88
  AgentSecret,
73
89
  InviteSecret,
package/src/node.ts CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  AccountContent,
33
33
  AccountMap,
34
34
  } from "./account.js";
35
- import { CoMap } from "./index.js";
35
+ import { CoMap } from "./coValues/coMap.js";
36
36
 
37
37
  /** A `LocalNode` represents a local view of a set of loaded `CoValue`s, from the perspective of a particular account (or primitive cryptographic agent).
38
38