cojson 0.1.8 → 0.1.10

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.
Files changed (76) hide show
  1. package/dist/account.d.ts +6 -3
  2. package/dist/account.js +4 -2
  3. package/dist/account.js.map +1 -1
  4. package/dist/coValue.d.ts +44 -80
  5. package/dist/coValue.js +4 -348
  6. package/dist/coValue.js.map +1 -1
  7. package/dist/coValueCore.d.ts +84 -0
  8. package/dist/coValueCore.js +356 -0
  9. package/dist/coValueCore.js.map +1 -0
  10. package/dist/coValues/coList.d.ts +114 -0
  11. package/dist/{contentTypes → coValues}/coList.js +59 -19
  12. package/dist/coValues/coList.js.map +1 -0
  13. package/dist/{contentTypes → coValues}/coMap.d.ts +25 -7
  14. package/dist/{contentTypes → coValues}/coMap.js +34 -15
  15. package/dist/coValues/coMap.js.map +1 -0
  16. package/dist/coValues/coStream.d.ts +69 -0
  17. package/dist/coValues/coStream.js +131 -0
  18. package/dist/coValues/coStream.js.map +1 -0
  19. package/dist/coValues/static.d.ts +14 -0
  20. package/dist/coValues/static.js +20 -0
  21. package/dist/coValues/static.js.map +1 -0
  22. package/dist/group.d.ts +57 -9
  23. package/dist/group.js +94 -28
  24. package/dist/group.js.map +1 -1
  25. package/dist/index.d.ts +19 -10
  26. package/dist/index.js +7 -5
  27. package/dist/index.js.map +1 -1
  28. package/dist/node.d.ts +59 -5
  29. package/dist/node.js +36 -15
  30. package/dist/node.js.map +1 -1
  31. package/dist/permissions.d.ts +2 -2
  32. package/dist/permissions.js +1 -1
  33. package/dist/permissions.js.map +1 -1
  34. package/dist/sync.d.ts +3 -3
  35. package/dist/sync.js +2 -2
  36. package/dist/sync.js.map +1 -1
  37. package/dist/testUtils.d.ts +2 -2
  38. package/dist/testUtils.js +1 -1
  39. package/dist/testUtils.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/account.test.ts +1 -1
  42. package/src/account.ts +8 -5
  43. package/src/coValue.test.ts +335 -129
  44. package/src/coValue.ts +52 -576
  45. package/src/coValueCore.test.ts +180 -0
  46. package/src/coValueCore.ts +592 -0
  47. package/src/{contentTypes → coValues}/coList.ts +91 -42
  48. package/src/{contentTypes → coValues}/coMap.ts +40 -20
  49. package/src/coValues/coStream.ts +249 -0
  50. package/src/coValues/static.ts +31 -0
  51. package/src/group.test.ts +47 -0
  52. package/src/group.ts +120 -50
  53. package/src/index.ts +43 -28
  54. package/src/node.ts +48 -27
  55. package/src/permissions.test.ts +32 -32
  56. package/src/permissions.ts +5 -5
  57. package/src/sync.test.ts +77 -77
  58. package/src/sync.ts +5 -5
  59. package/src/testUtils.ts +1 -1
  60. package/tsconfig.json +1 -2
  61. package/dist/contentType.d.ts +0 -15
  62. package/dist/contentType.js +0 -7
  63. package/dist/contentType.js.map +0 -1
  64. package/dist/contentTypes/coList.d.ts +0 -77
  65. package/dist/contentTypes/coList.js.map +0 -1
  66. package/dist/contentTypes/coMap.js.map +0 -1
  67. package/dist/contentTypes/coStream.d.ts +0 -11
  68. package/dist/contentTypes/coStream.js +0 -16
  69. package/dist/contentTypes/coStream.js.map +0 -1
  70. package/dist/contentTypes/static.d.ts +0 -11
  71. package/dist/contentTypes/static.js +0 -14
  72. package/dist/contentTypes/static.js.map +0 -1
  73. package/src/contentType.test.ts +0 -284
  74. package/src/contentType.ts +0 -26
  75. package/src/contentTypes/coStream.ts +0 -24
  76. package/src/contentTypes/static.ts +0 -22
package/src/coValue.ts CHANGED
@@ -1,588 +1,64 @@
1
- import { randomBytes } from "@noble/hashes/utils";
2
- import { ContentType } from "./contentType.js";
3
- import { Static } from "./contentTypes/static.js";
4
- import { CoStream } from "./contentTypes/coStream.js";
5
- import { CoMap } from "./contentTypes/coMap.js";
6
- import {
7
- Encrypted,
8
- Hash,
9
- KeySecret,
10
- Signature,
11
- StreamingHash,
12
- unseal,
13
- shortHash,
14
- sign,
15
- verify,
16
- encryptForTransaction,
17
- decryptForTransaction,
18
- KeyID,
19
- decryptKeySecret,
20
- getAgentSignerID,
21
- getAgentSealerID,
22
- } from "./crypto.js";
23
1
  import { JsonObject, JsonValue } from "./jsonValue.js";
24
- import { base58 } from "@scure/base";
25
- import {
26
- PermissionsDef as RulesetDef,
27
- determineValidTransactions,
28
- isKeyForKeyField,
29
- } from "./permissions.js";
30
- import { Group, expectGroupContent } from "./group.js";
31
- import { LocalNode } from "./node.js";
32
- import { CoValueKnownState, NewContentMessage } from "./sync.js";
33
- import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
34
- import { CoList } from "./contentTypes/coList.js";
35
- import {
36
- AccountID,
37
- GeneralizedControlledAccount,
38
- } from "./account.js";
39
-
40
- export type CoValueHeader = {
41
- type: ContentType["type"];
42
- ruleset: RulesetDef;
43
- meta: JsonObject | null;
44
- createdAt: `2${string}` | null;
45
- uniqueness: `z${string}` | null;
2
+ import { RawCoID } from "./ids.js";
3
+ import { CoMap } from "./coValues/coMap.js";
4
+ import { BinaryCoStream, BinaryCoStreamMeta, CoStream } from "./coValues/coStream.js";
5
+ import { Static } from "./coValues/static.js";
6
+ import { CoList } from "./coValues/coList.js";
7
+ import { CoValueCore } from "./coValueCore.js";
8
+ import { Group } from "./group.js";
9
+
10
+ export type CoID<T extends CoValueImpl> = RawCoID & {
11
+ readonly __type: T;
46
12
  };
47
13
 
48
- export function idforHeader(header: CoValueHeader): RawCoID {
49
- const hash = shortHash(header);
50
- return `co_z${hash.slice("shortHash_z".length)}`;
51
- }
52
-
53
- export function accountOrAgentIDfromSessionID(
54
- sessionID: SessionID
55
- ): AccountID | AgentID {
56
- return sessionID.split("_session")[0] as AccountID | AgentID;
14
+ export interface ReadableCoValue extends CoValue {
15
+ /** Lets you subscribe to future updates to this CoValue (whether made locally or by other users).
16
+ *
17
+ * Takes a listener function that will be called with the current state for each update.
18
+ *
19
+ * Returns an unsubscribe function.
20
+ *
21
+ * Used internally by `useTelepathicData()` for reactive updates on changes to a `CoValue`. */
22
+ subscribe(listener: (coValue: CoValueImpl) => void): () => void;
23
+ /** Lets you apply edits to a `CoValue`, inside the changer callback, which receives a `WriteableCoValue`.
24
+ *
25
+ * A `WritableCoValue` has all the same methods as a `CoValue`, but all edits made to it (with its additional mutator methods)
26
+ * are reflected in it immediately - so it behaves mutably, whereas a `CoValue` is always immutable
27
+ * (you need to use `subscribe` to receive new versions of it). */
28
+ edit?:
29
+ | ((changer: (editable: WriteableCoValue) => void) => CoValueImpl)
30
+ | undefined;
57
31
  }
58
32
 
59
- export function newRandomSessionID(accountID: AccountID | AgentID): SessionID {
60
- return `${accountID}_session_z${base58.encode(randomBytes(8))}`;
33
+ export interface CoValue {
34
+ /** The `CoValue`'s (precisely typed) `CoID` */
35
+ id: CoID<CoValueImpl>;
36
+ core: CoValueCore;
37
+ /** Specifies which kind of `CoValue` this is */
38
+ type: CoValueImpl["type"];
39
+ /** The `CoValue`'s (precisely typed) static metadata */
40
+ meta: JsonObject | null;
41
+ /** The `Group` this `CoValue` belongs to (determining permissions) */
42
+ group: Group;
43
+ /** Returns an immutable JSON presentation of this `CoValue` */
44
+ toJSON(): JsonValue;
61
45
  }
62
46
 
63
- type SessionLog = {
64
- transactions: Transaction[];
65
- lastHash?: Hash;
66
- streamingHash: StreamingHash;
67
- lastSignature: Signature;
68
- };
69
-
70
- export type PrivateTransaction = {
71
- privacy: "private";
72
- madeAt: number;
73
- keyUsed: KeyID;
74
- encryptedChanges: Encrypted<
75
- JsonValue[],
76
- { in: RawCoID; tx: TransactionID }
77
- >;
78
- };
79
-
80
- export type TrustingTransaction = {
81
- privacy: "trusting";
82
- madeAt: number;
83
- changes: JsonValue[];
84
- };
85
-
86
- export type Transaction = PrivateTransaction | TrustingTransaction;
87
-
88
- export type DecryptedTransaction = {
89
- txID: TransactionID;
90
- changes: JsonValue[];
91
- madeAt: number;
92
- };
93
-
94
- const readKeyCache = new WeakMap<CoValue, { [id: KeyID]: KeySecret }>();
95
-
96
- export class CoValue {
97
- id: RawCoID;
98
- node: LocalNode;
99
- header: CoValueHeader;
100
- _sessions: { [key: SessionID]: SessionLog };
101
- _cachedContent?: ContentType;
102
- listeners: Set<(content?: ContentType) => void> = new Set();
103
-
104
- constructor(
105
- header: CoValueHeader,
106
- node: LocalNode,
107
- internalInitSessions: { [key: SessionID]: SessionLog } = {}
108
- ) {
109
- this.id = idforHeader(header);
110
- this.header = header;
111
- this._sessions = internalInitSessions;
112
- this.node = node;
113
-
114
- if (header.ruleset.type == "ownedByGroup") {
115
- this.node
116
- .expectCoValueLoaded(header.ruleset.group)
117
- .subscribe((_groupUpdate) => {
118
- this._cachedContent = undefined;
119
- const newContent = this.getCurrentContent();
120
- for (const listener of this.listeners) {
121
- listener(newContent);
122
- }
123
- });
124
- }
125
- }
126
-
127
- get sessions(): Readonly<{ [key: SessionID]: SessionLog }> {
128
- return this._sessions;
129
- }
130
-
131
- testWithDifferentAccount(
132
- account: GeneralizedControlledAccount,
133
- currentSessionID: SessionID
134
- ): CoValue {
135
- const newNode = this.node.testWithDifferentAccount(
136
- account,
137
- currentSessionID
138
- );
139
-
140
- return newNode.expectCoValueLoaded(this.id);
141
- }
142
-
143
- knownState(): CoValueKnownState {
144
- return {
145
- id: this.id,
146
- header: true,
147
- sessions: Object.fromEntries(
148
- Object.entries(this.sessions).map(([k, v]) => [
149
- k,
150
- v.transactions.length,
151
- ])
152
- ),
153
- };
154
- }
155
-
156
- get meta(): JsonValue {
157
- return this.header?.meta ?? null;
158
- }
159
-
160
- nextTransactionID(): TransactionID {
161
- const sessionID = this.node.currentSessionID;
162
- return {
163
- sessionID,
164
- txIndex: this.sessions[sessionID]?.transactions.length || 0,
165
- };
166
- }
167
-
168
- tryAddTransactions(
169
- sessionID: SessionID,
170
- newTransactions: Transaction[],
171
- givenExpectedNewHash: Hash | undefined,
172
- newSignature: Signature
173
- ): boolean {
174
- const signerID = getAgentSignerID(
175
- this.node.resolveAccountAgent(
176
- accountOrAgentIDfromSessionID(sessionID),
177
- "Expected to know signer of transaction"
178
- )
179
- );
180
-
181
- if (!signerID) {
182
- console.warn(
183
- "Unknown agent",
184
- accountOrAgentIDfromSessionID(sessionID)
185
- );
186
- return false;
187
- }
188
-
189
- const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
190
- sessionID,
191
- newTransactions
192
- );
193
-
194
- if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
195
- console.warn("Invalid hash", {
196
- expectedNewHash,
197
- givenExpectedNewHash,
198
- });
199
- return false;
200
- }
201
-
202
- if (!verify(newSignature, expectedNewHash, signerID)) {
203
- console.warn(
204
- "Invalid signature",
205
- newSignature,
206
- expectedNewHash,
207
- signerID
208
- );
209
- return false;
210
- }
211
-
212
- const transactions = this.sessions[sessionID]?.transactions ?? [];
213
-
214
- transactions.push(...newTransactions);
215
-
216
- this._sessions[sessionID] = {
217
- transactions,
218
- lastHash: expectedNewHash,
219
- streamingHash: newStreamingHash,
220
- lastSignature: newSignature,
221
- };
222
-
223
- this._cachedContent = undefined;
224
-
225
- const content = this.getCurrentContent();
226
-
227
- for (const listener of this.listeners) {
228
- listener(content);
229
- }
230
-
231
- return true;
232
- }
233
-
234
- subscribe(listener: (content?: ContentType) => void): () => void {
235
- this.listeners.add(listener);
236
- listener(this.getCurrentContent());
237
-
238
- return () => {
239
- this.listeners.delete(listener);
240
- };
241
- }
242
-
243
- expectedNewHashAfter(
244
- sessionID: SessionID,
245
- newTransactions: Transaction[]
246
- ): { expectedNewHash: Hash; newStreamingHash: StreamingHash } {
247
- const streamingHash =
248
- this.sessions[sessionID]?.streamingHash.clone() ??
249
- new StreamingHash();
250
- for (const transaction of newTransactions) {
251
- streamingHash.update(transaction);
252
- }
253
-
254
- const newStreamingHash = streamingHash.clone();
255
-
256
- return {
257
- expectedNewHash: streamingHash.digest(),
258
- newStreamingHash,
259
- };
260
- }
261
-
262
- makeTransaction(
263
- changes: JsonValue[],
264
- privacy: "private" | "trusting"
265
- ): boolean {
266
- const madeAt = Date.now();
267
-
268
- let transaction: Transaction;
47
+ export interface WriteableCoValue extends CoValue {}
269
48
 
270
- if (privacy === "private") {
271
- const { secret: keySecret, id: keyID } = this.getCurrentReadKey();
49
+ export type CoValueImpl =
50
+ | CoMap<{ [key: string]: JsonValue }, JsonObject | null>
51
+ | CoList<JsonValue, JsonObject | null>
52
+ | CoStream<JsonValue, JsonObject | null>
53
+ | BinaryCoStream<BinaryCoStreamMeta>
54
+ | Static<JsonObject>;
272
55
 
273
- if (!keySecret) {
274
- throw new Error(
275
- "Can't make transaction without read key secret"
276
- );
277
- }
278
-
279
- transaction = {
280
- privacy: "private",
281
- madeAt,
282
- keyUsed: keyID,
283
- encryptedChanges: encryptForTransaction(changes, keySecret, {
284
- in: this.id,
285
- tx: this.nextTransactionID(),
286
- }),
287
- };
288
- } else {
289
- transaction = {
290
- privacy: "trusting",
291
- madeAt,
292
- changes,
293
- };
294
- }
295
-
296
- const sessionID = this.node.currentSessionID;
297
-
298
- const { expectedNewHash } = this.expectedNewHashAfter(sessionID, [
299
- transaction,
300
- ]);
301
-
302
- const signature = sign(
303
- this.node.account.currentSignerSecret(),
304
- expectedNewHash
305
- );
306
-
307
- const success = this.tryAddTransactions(
308
- sessionID,
309
- [transaction],
310
- expectedNewHash,
311
- signature
312
- );
313
-
314
- if (success) {
315
- void this.node.sync.syncCoValue(this);
316
- }
317
-
318
- return success;
319
- }
320
-
321
- getCurrentContent(): ContentType {
322
- if (this._cachedContent) {
323
- return this._cachedContent;
324
- }
325
-
326
- if (this.header.type === "comap") {
327
- this._cachedContent = new CoMap(this);
328
- } else if (this.header.type === "colist") {
329
- this._cachedContent = new CoList(this);
330
- } else if (this.header.type === "costream") {
331
- this._cachedContent = new CoStream(this);
332
- } else if (this.header.type === "static") {
333
- this._cachedContent = new Static(this);
334
- } else {
335
- throw new Error(`Unknown coValue type ${this.header.type}`);
336
- }
337
-
338
- return this._cachedContent;
339
- }
340
-
341
- getValidSortedTransactions(): DecryptedTransaction[] {
342
- const validTransactions = determineValidTransactions(this);
343
-
344
- const allTransactions: DecryptedTransaction[] = validTransactions
345
- .map(({ txID, tx }) => {
346
- if (tx.privacy === "trusting") {
347
- return {
348
- txID,
349
- madeAt: tx.madeAt,
350
- changes: tx.changes,
351
- };
352
- } else {
353
- const readKey = this.getReadKey(tx.keyUsed);
354
-
355
- if (!readKey) {
356
- return undefined;
357
- } else {
358
- const decrytedChanges = decryptForTransaction(
359
- tx.encryptedChanges,
360
- readKey,
361
- {
362
- in: this.id,
363
- tx: txID,
364
- }
365
- );
366
-
367
- if (!decrytedChanges) {
368
- console.error(
369
- "Failed to decrypt transaction despite having key"
370
- );
371
- return undefined;
372
- }
373
- return {
374
- txID,
375
- madeAt: tx.madeAt,
376
- changes: decrytedChanges,
377
- };
378
- }
379
- }
380
- })
381
- .filter((x): x is Exclude<typeof x, undefined> => !!x);
382
- allTransactions.sort(
383
- (a, b) =>
384
- a.madeAt - b.madeAt ||
385
- (a.txID.sessionID < b.txID.sessionID ? -1 : 1) ||
386
- a.txID.txIndex - b.txID.txIndex
387
- );
388
-
389
- return allTransactions;
56
+ export function expectMap(
57
+ content: CoValueImpl
58
+ ): CoMap<{ [key: string]: string }, JsonObject | null> {
59
+ if (content.type !== "comap") {
60
+ throw new Error("Expected map");
390
61
  }
391
62
 
392
- getCurrentReadKey(): { secret: KeySecret | undefined; id: KeyID } {
393
- if (this.header.ruleset.type === "group") {
394
- const content = expectGroupContent(this.getCurrentContent());
395
-
396
- const currentKeyId = content.get("readKey");
397
-
398
- if (!currentKeyId) {
399
- throw new Error("No readKey set");
400
- }
401
-
402
- const secret = this.getReadKey(currentKeyId);
403
-
404
- return {
405
- secret: secret,
406
- id: currentKeyId,
407
- };
408
- } else if (this.header.ruleset.type === "ownedByGroup") {
409
- return this.node
410
- .expectCoValueLoaded(this.header.ruleset.group)
411
- .getCurrentReadKey();
412
- } else {
413
- throw new Error(
414
- "Only groups or values owned by groups have read secrets"
415
- );
416
- }
417
- }
418
-
419
- getReadKey(keyID: KeyID): KeySecret | undefined {
420
- if (readKeyCache.get(this)?.[keyID]) {
421
- return readKeyCache.get(this)?.[keyID];
422
- }
423
- if (this.header.ruleset.type === "group") {
424
- const content = expectGroupContent(this.getCurrentContent());
425
-
426
- // Try to find key revelation for us
427
-
428
- const readKeyEntry = content.getLastEntry(
429
- `${keyID}_for_${this.node.account.id}`
430
- );
431
-
432
- if (readKeyEntry) {
433
- const revealer = accountOrAgentIDfromSessionID(
434
- readKeyEntry.txID.sessionID
435
- );
436
- const revealerAgent = this.node.resolveAccountAgent(
437
- revealer,
438
- "Expected to know revealer"
439
- );
440
-
441
- const secret = unseal(
442
- readKeyEntry.value,
443
- this.node.account.currentSealerSecret(),
444
- getAgentSealerID(revealerAgent),
445
- {
446
- in: this.id,
447
- tx: readKeyEntry.txID,
448
- }
449
- );
450
-
451
- if (secret) {
452
- let cache = readKeyCache.get(this);
453
- if (!cache) {
454
- cache = {};
455
- readKeyCache.set(this, cache);
456
- }
457
- cache[keyID] = secret;
458
-
459
- return secret as KeySecret;
460
- }
461
- }
462
-
463
- // Try to find indirect revelation through previousKeys
464
-
465
- for (const field of content.keys()) {
466
- if (isKeyForKeyField(field) && field.startsWith(keyID)) {
467
- const encryptingKeyID = field.split("_for_")[1] as KeyID;
468
- const encryptingKeySecret =
469
- this.getReadKey(encryptingKeyID);
470
-
471
- if (!encryptingKeySecret) {
472
- continue;
473
- }
474
-
475
- const encryptedPreviousKey = content.get(field)!;
476
-
477
- const secret = decryptKeySecret(
478
- {
479
- encryptedID: keyID,
480
- encryptingID: encryptingKeyID,
481
- encrypted: encryptedPreviousKey,
482
- },
483
- encryptingKeySecret
484
- );
485
-
486
- if (secret) {
487
- let cache = readKeyCache.get(this);
488
- if (!cache) {
489
- cache = {};
490
- readKeyCache.set(this, cache);
491
- }
492
- cache[keyID] = secret;
493
-
494
- return secret as KeySecret;
495
- } else {
496
- console.error(
497
- `Encrypting ${encryptingKeyID} key didn't decrypt ${keyID}`
498
- );
499
- }
500
- }
501
- }
502
-
503
- return undefined;
504
- } else if (this.header.ruleset.type === "ownedByGroup") {
505
- return this.node
506
- .expectCoValueLoaded(this.header.ruleset.group)
507
- .getReadKey(keyID);
508
- } else {
509
- throw new Error(
510
- "Only groups or values owned by groups have read secrets"
511
- );
512
- }
513
- }
514
-
515
- getGroup(): Group {
516
- if (this.header.ruleset.type !== "ownedByGroup") {
517
- throw new Error("Only values owned by groups have groups");
518
- }
519
-
520
- return new Group(
521
- expectGroupContent(
522
- this.node
523
- .expectCoValueLoaded(this.header.ruleset.group)
524
- .getCurrentContent()
525
- ),
526
- this.node
527
- );
528
- }
529
-
530
- getTx(txID: TransactionID): Transaction | undefined {
531
- return this.sessions[txID.sessionID]?.transactions[txID.txIndex];
532
- }
533
-
534
- newContentSince(
535
- knownState: CoValueKnownState | undefined
536
- ): NewContentMessage | undefined {
537
- const newContent: NewContentMessage = {
538
- action: "content",
539
- id: this.id,
540
- header: knownState?.header ? undefined : this.header,
541
- new: Object.fromEntries(
542
- Object.entries(this.sessions)
543
- .map(([sessionID, log]) => {
544
- const newTransactions = log.transactions.slice(
545
- knownState?.sessions[sessionID as SessionID] || 0
546
- );
547
-
548
- if (
549
- newTransactions.length === 0 ||
550
- !log.lastHash ||
551
- !log.lastSignature
552
- ) {
553
- return undefined;
554
- }
555
-
556
- return [
557
- sessionID,
558
- {
559
- after:
560
- knownState?.sessions[
561
- sessionID as SessionID
562
- ] || 0,
563
- newTransactions,
564
- lastSignature: log.lastSignature,
565
- },
566
- ];
567
- })
568
- .filter((x): x is Exclude<typeof x, undefined> => !!x)
569
- ),
570
- };
571
-
572
- if (!newContent.header && Object.keys(newContent.new).length === 0) {
573
- return undefined;
574
- }
575
-
576
- return newContent;
577
- }
578
-
579
- getDependedOnCoValues(): RawCoID[] {
580
- return this.header.ruleset.type === "group"
581
- ? expectGroupContent(this.getCurrentContent())
582
- .keys()
583
- .filter((k): k is AccountID => k.startsWith("co_"))
584
- : this.header.ruleset.type === "ownedByGroup"
585
- ? [this.header.ruleset.group]
586
- : [];
587
- }
63
+ return content as CoMap<{ [key: string]: string }, JsonObject | null>;
588
64
  }