cojson 0.0.1 → 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/.eslintrc.cjs +18 -0
- package/LICENSE.txt +19 -0
- package/README.md +53 -0
- package/package.json +30 -6
- package/src/coValue.test.ts +138 -0
- package/src/coValue.ts +640 -0
- package/src/contentType.test.ts +202 -0
- package/src/contentType.ts +24 -0
- 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.test.ts +189 -0
- package/src/crypto.ts +325 -0
- package/src/ids.ts +7 -0
- package/src/index.ts +28 -0
- package/src/jsonValue.ts +6 -0
- package/src/node.ts +197 -0
- package/src/permissions.test.ts +1301 -0
- package/src/permissions.ts +375 -0
- package/src/sync.test.ts +1130 -0
- package/src/sync.ts +493 -0
- package/tsconfig.json +21 -0
package/src/coValue.ts
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import { randomBytes } from "@noble/hashes/utils";
|
|
2
|
+
import { ContentType } from "./contentType";
|
|
3
|
+
import { Static } from "./contentTypes/static";
|
|
4
|
+
import { CoStream } from "./contentTypes/coStream";
|
|
5
|
+
import { CoMap } from "./contentTypes/coMap";
|
|
6
|
+
import {
|
|
7
|
+
Encrypted,
|
|
8
|
+
Hash,
|
|
9
|
+
KeySecret,
|
|
10
|
+
RecipientID,
|
|
11
|
+
RecipientSecret,
|
|
12
|
+
SignatoryID,
|
|
13
|
+
SignatorySecret,
|
|
14
|
+
Signature,
|
|
15
|
+
StreamingHash,
|
|
16
|
+
getRecipientID,
|
|
17
|
+
getSignatoryID,
|
|
18
|
+
newRandomRecipient,
|
|
19
|
+
newRandomSignatory,
|
|
20
|
+
openAs,
|
|
21
|
+
shortHash,
|
|
22
|
+
sign,
|
|
23
|
+
verify,
|
|
24
|
+
encryptForTransaction,
|
|
25
|
+
decryptForTransaction,
|
|
26
|
+
KeyID,
|
|
27
|
+
unsealKeySecret,
|
|
28
|
+
signatorySecretToBytes,
|
|
29
|
+
recipientSecretToBytes,
|
|
30
|
+
signatorySecretFromBytes,
|
|
31
|
+
recipientSecretFromBytes,
|
|
32
|
+
} from "./crypto";
|
|
33
|
+
import { JsonValue } from "./jsonValue";
|
|
34
|
+
import { base58 } from "@scure/base";
|
|
35
|
+
import {
|
|
36
|
+
PermissionsDef as RulesetDef,
|
|
37
|
+
Team,
|
|
38
|
+
determineValidTransactions,
|
|
39
|
+
expectTeamContent,
|
|
40
|
+
} from "./permissions";
|
|
41
|
+
import { LocalNode } from "./node";
|
|
42
|
+
import { CoValueKnownState, NewContentMessage } from "./sync";
|
|
43
|
+
import { AgentID, RawCoValueID, SessionID, TransactionID } from "./ids";
|
|
44
|
+
import { CoList } from "./contentTypes/coList";
|
|
45
|
+
|
|
46
|
+
export type CoValueHeader = {
|
|
47
|
+
type: ContentType["type"];
|
|
48
|
+
ruleset: RulesetDef;
|
|
49
|
+
meta: JsonValue;
|
|
50
|
+
createdAt: `2${string}` | null;
|
|
51
|
+
uniqueness: `z${string}` | null;
|
|
52
|
+
publicNickname?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function coValueIDforHeader(header: CoValueHeader): RawCoValueID {
|
|
56
|
+
const hash = shortHash(header);
|
|
57
|
+
if (header.publicNickname) {
|
|
58
|
+
return `co_${header.publicNickname}_z${hash.slice(
|
|
59
|
+
"shortHash_z".length
|
|
60
|
+
)}`;
|
|
61
|
+
} else {
|
|
62
|
+
return `co_z${hash.slice("shortHash_z".length)}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function agentIDfromSessionID(sessionID: SessionID): AgentID {
|
|
67
|
+
return sessionID.split("_session")[0] as AgentID;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function newRandomSessionID(agentID: AgentID): SessionID {
|
|
71
|
+
return `${agentID}_session_z${base58.encode(randomBytes(8))}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type SessionLog = {
|
|
75
|
+
transactions: Transaction[];
|
|
76
|
+
lastHash?: Hash;
|
|
77
|
+
streamingHash: StreamingHash;
|
|
78
|
+
lastSignature: Signature;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type PrivateTransaction = {
|
|
82
|
+
privacy: "private";
|
|
83
|
+
madeAt: number;
|
|
84
|
+
keyUsed: KeyID;
|
|
85
|
+
encryptedChanges: Encrypted<
|
|
86
|
+
JsonValue[],
|
|
87
|
+
{ in: RawCoValueID; tx: TransactionID }
|
|
88
|
+
>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type TrustingTransaction = {
|
|
92
|
+
privacy: "trusting";
|
|
93
|
+
madeAt: number;
|
|
94
|
+
changes: JsonValue[];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type Transaction = PrivateTransaction | TrustingTransaction;
|
|
98
|
+
|
|
99
|
+
export type DecryptedTransaction = {
|
|
100
|
+
txID: TransactionID;
|
|
101
|
+
changes: JsonValue[];
|
|
102
|
+
madeAt: number;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export class CoValue {
|
|
106
|
+
id: RawCoValueID;
|
|
107
|
+
node: LocalNode;
|
|
108
|
+
header: CoValueHeader;
|
|
109
|
+
sessions: { [key: SessionID]: SessionLog };
|
|
110
|
+
content?: ContentType;
|
|
111
|
+
listeners: Set<(content?: ContentType) => void> = new Set();
|
|
112
|
+
|
|
113
|
+
constructor(header: CoValueHeader, node: LocalNode) {
|
|
114
|
+
this.id = coValueIDforHeader(header);
|
|
115
|
+
this.header = header;
|
|
116
|
+
this.sessions = {};
|
|
117
|
+
this.node = node;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
testWithDifferentCredentials(
|
|
121
|
+
agentCredential: AgentCredential,
|
|
122
|
+
ownSessionID: SessionID
|
|
123
|
+
): CoValue {
|
|
124
|
+
const newNode = this.node.testWithDifferentCredentials(
|
|
125
|
+
agentCredential,
|
|
126
|
+
ownSessionID
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return newNode.expectCoValueLoaded(this.id);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
knownState(): CoValueKnownState {
|
|
133
|
+
return {
|
|
134
|
+
coValueID: this.id,
|
|
135
|
+
header: true,
|
|
136
|
+
sessions: Object.fromEntries(
|
|
137
|
+
Object.entries(this.sessions).map(([k, v]) => [
|
|
138
|
+
k,
|
|
139
|
+
v.transactions.length,
|
|
140
|
+
])
|
|
141
|
+
),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get meta(): JsonValue {
|
|
146
|
+
return this.header?.meta ?? null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
nextTransactionID(): TransactionID {
|
|
150
|
+
const sessionID = this.node.ownSessionID;
|
|
151
|
+
return {
|
|
152
|
+
sessionID,
|
|
153
|
+
txIndex: this.sessions[sessionID]?.transactions.length || 0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
tryAddTransactions(
|
|
158
|
+
sessionID: SessionID,
|
|
159
|
+
newTransactions: Transaction[],
|
|
160
|
+
newHash: Hash,
|
|
161
|
+
newSignature: Signature
|
|
162
|
+
): boolean {
|
|
163
|
+
const signatoryID = this.node.expectAgentLoaded(
|
|
164
|
+
agentIDfromSessionID(sessionID),
|
|
165
|
+
"Expected to know signatory of transaction"
|
|
166
|
+
).signatoryID;
|
|
167
|
+
|
|
168
|
+
if (!signatoryID) {
|
|
169
|
+
console.warn("Unknown agent", agentIDfromSessionID(sessionID));
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
|
|
174
|
+
sessionID,
|
|
175
|
+
newTransactions
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (newHash !== expectedNewHash) {
|
|
179
|
+
console.warn("Invalid hash", { newHash, expectedNewHash });
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!verify(newSignature, newHash, signatoryID)) {
|
|
184
|
+
console.warn(
|
|
185
|
+
"Invalid signature",
|
|
186
|
+
newSignature,
|
|
187
|
+
newHash,
|
|
188
|
+
signatoryID
|
|
189
|
+
);
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const transactions = this.sessions[sessionID]?.transactions ?? [];
|
|
194
|
+
|
|
195
|
+
console.log("transactions before", this.id, transactions.length, this.getValidSortedTransactions().length);
|
|
196
|
+
|
|
197
|
+
transactions.push(...newTransactions);
|
|
198
|
+
|
|
199
|
+
this.sessions[sessionID] = {
|
|
200
|
+
transactions,
|
|
201
|
+
lastHash: newHash,
|
|
202
|
+
streamingHash: newStreamingHash,
|
|
203
|
+
lastSignature: newSignature,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
this.content = undefined;
|
|
207
|
+
|
|
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
|
+
}
|
|
216
|
+
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
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
|
+
|
|
230
|
+
expectedNewHashAfter(
|
|
231
|
+
sessionID: SessionID,
|
|
232
|
+
newTransactions: Transaction[]
|
|
233
|
+
): { expectedNewHash: Hash; newStreamingHash: StreamingHash } {
|
|
234
|
+
const streamingHash =
|
|
235
|
+
this.sessions[sessionID]?.streamingHash.clone() ??
|
|
236
|
+
new StreamingHash();
|
|
237
|
+
for (const transaction of newTransactions) {
|
|
238
|
+
streamingHash.update(transaction);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const newStreamingHash = streamingHash.clone();
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
expectedNewHash: streamingHash.digest(),
|
|
245
|
+
newStreamingHash,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
makeTransaction(
|
|
250
|
+
changes: JsonValue[],
|
|
251
|
+
privacy: "private" | "trusting"
|
|
252
|
+
): boolean {
|
|
253
|
+
const madeAt = Date.now();
|
|
254
|
+
|
|
255
|
+
let transaction: Transaction;
|
|
256
|
+
|
|
257
|
+
if (privacy === "private") {
|
|
258
|
+
const { secret: keySecret, id: keyID } = this.getCurrentReadKey();
|
|
259
|
+
|
|
260
|
+
if (!keySecret) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
"Can't make transaction without read key secret"
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
transaction = {
|
|
267
|
+
privacy: "private",
|
|
268
|
+
madeAt,
|
|
269
|
+
keyUsed: keyID,
|
|
270
|
+
encryptedChanges: encryptForTransaction(changes, keySecret, {
|
|
271
|
+
in: this.id,
|
|
272
|
+
tx: this.nextTransactionID(),
|
|
273
|
+
}),
|
|
274
|
+
};
|
|
275
|
+
} else {
|
|
276
|
+
transaction = {
|
|
277
|
+
privacy: "trusting",
|
|
278
|
+
madeAt,
|
|
279
|
+
changes,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const sessionID = this.node.ownSessionID;
|
|
284
|
+
|
|
285
|
+
const { expectedNewHash } = this.expectedNewHashAfter(sessionID, [
|
|
286
|
+
transaction,
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
const signature = sign(
|
|
290
|
+
this.node.agentCredential.signatorySecret,
|
|
291
|
+
expectedNewHash
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const success = this.tryAddTransactions(
|
|
295
|
+
sessionID,
|
|
296
|
+
[transaction],
|
|
297
|
+
expectedNewHash,
|
|
298
|
+
signature
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (success) {
|
|
302
|
+
void this.node.sync.syncCoValue(this);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return success;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getCurrentContent(): ContentType {
|
|
309
|
+
if (this.content) {
|
|
310
|
+
return this.content;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (this.header.type === "comap") {
|
|
314
|
+
this.content = new CoMap(this);
|
|
315
|
+
} else if (this.header.type === "colist") {
|
|
316
|
+
this.content = new CoList(this);
|
|
317
|
+
} else if (this.header.type === "costream") {
|
|
318
|
+
this.content = new CoStream(this);
|
|
319
|
+
} else if (this.header.type === "static") {
|
|
320
|
+
this.content = new Static(this);
|
|
321
|
+
} else {
|
|
322
|
+
throw new Error(`Unknown coValue type ${this.header.type}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return this.content;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
getValidSortedTransactions(): DecryptedTransaction[] {
|
|
329
|
+
const validTransactions = determineValidTransactions(this);
|
|
330
|
+
|
|
331
|
+
const allTransactions: DecryptedTransaction[] = validTransactions
|
|
332
|
+
.map(({ txID, tx }) => {
|
|
333
|
+
if (tx.privacy === "trusting") {
|
|
334
|
+
return {
|
|
335
|
+
txID,
|
|
336
|
+
madeAt: tx.madeAt,
|
|
337
|
+
changes: tx.changes,
|
|
338
|
+
};
|
|
339
|
+
} else {
|
|
340
|
+
const readKey = this.getReadKey(tx.keyUsed);
|
|
341
|
+
|
|
342
|
+
if (!readKey) {
|
|
343
|
+
return undefined;
|
|
344
|
+
} else {
|
|
345
|
+
const decrytedChanges = decryptForTransaction(
|
|
346
|
+
tx.encryptedChanges,
|
|
347
|
+
readKey,
|
|
348
|
+
{
|
|
349
|
+
in: this.id,
|
|
350
|
+
tx: txID,
|
|
351
|
+
}
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
if (!decrytedChanges) {
|
|
355
|
+
console.error(
|
|
356
|
+
"Failed to decrypt transaction despite having key"
|
|
357
|
+
);
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
txID,
|
|
362
|
+
madeAt: tx.madeAt,
|
|
363
|
+
changes: decrytedChanges,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
.filter((x): x is Exclude<typeof x, undefined> => !!x);
|
|
369
|
+
allTransactions.sort(
|
|
370
|
+
(a, b) =>
|
|
371
|
+
a.madeAt - b.madeAt ||
|
|
372
|
+
(a.txID.sessionID < b.txID.sessionID ? -1 : 1) ||
|
|
373
|
+
a.txID.txIndex - b.txID.txIndex
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
return allTransactions;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
getCurrentReadKey(): { secret: KeySecret | undefined; id: KeyID } {
|
|
380
|
+
if (this.header.ruleset.type === "team") {
|
|
381
|
+
const content = expectTeamContent(this.getCurrentContent());
|
|
382
|
+
|
|
383
|
+
const currentKeyId = content.get("readKey")?.keyID;
|
|
384
|
+
|
|
385
|
+
if (!currentKeyId) {
|
|
386
|
+
throw new Error("No readKey set");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const secret = this.getReadKey(currentKeyId);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
secret: secret,
|
|
393
|
+
id: currentKeyId,
|
|
394
|
+
};
|
|
395
|
+
} else if (this.header.ruleset.type === "ownedByTeam") {
|
|
396
|
+
return this.node
|
|
397
|
+
.expectCoValueLoaded(this.header.ruleset.team)
|
|
398
|
+
.getCurrentReadKey();
|
|
399
|
+
} else {
|
|
400
|
+
throw new Error(
|
|
401
|
+
"Only teams or values owned by teams have read secrets"
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
getReadKey(keyID: KeyID): KeySecret | undefined {
|
|
407
|
+
if (this.header.ruleset.type === "team") {
|
|
408
|
+
const content = expectTeamContent(this.getCurrentContent());
|
|
409
|
+
|
|
410
|
+
const readKeyHistory = content.getHistory("readKey");
|
|
411
|
+
|
|
412
|
+
// Try to find direct relevation of key for us
|
|
413
|
+
|
|
414
|
+
for (const entry of readKeyHistory) {
|
|
415
|
+
if (entry.value?.keyID === keyID) {
|
|
416
|
+
const revealer = agentIDfromSessionID(entry.txID.sessionID);
|
|
417
|
+
const revealerAgent = this.node.expectAgentLoaded(
|
|
418
|
+
revealer,
|
|
419
|
+
"Expected to know revealer"
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const secret = openAs(
|
|
423
|
+
entry.value.revelation,
|
|
424
|
+
this.node.agentCredential.recipientSecret,
|
|
425
|
+
revealerAgent.recipientID,
|
|
426
|
+
{
|
|
427
|
+
in: this.id,
|
|
428
|
+
tx: entry.txID,
|
|
429
|
+
}
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
if (secret) return secret as KeySecret;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Try to find indirect revelation through previousKeys
|
|
437
|
+
|
|
438
|
+
for (const entry of readKeyHistory) {
|
|
439
|
+
const encryptedPreviousKey = entry.value?.previousKeys?.[keyID];
|
|
440
|
+
if (entry.value && encryptedPreviousKey) {
|
|
441
|
+
const sealingKeyID = entry.value.keyID;
|
|
442
|
+
const sealingKeySecret = this.getReadKey(sealingKeyID);
|
|
443
|
+
|
|
444
|
+
if (!sealingKeySecret) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const secret = unsealKeySecret(
|
|
449
|
+
{
|
|
450
|
+
sealed: keyID,
|
|
451
|
+
sealing: sealingKeyID,
|
|
452
|
+
encrypted: encryptedPreviousKey,
|
|
453
|
+
},
|
|
454
|
+
sealingKeySecret
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
if (secret) {
|
|
458
|
+
return secret;
|
|
459
|
+
} else {
|
|
460
|
+
console.error(
|
|
461
|
+
`Sealing ${sealingKeyID} key didn't unseal ${keyID}`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return undefined;
|
|
468
|
+
} else if (this.header.ruleset.type === "ownedByTeam") {
|
|
469
|
+
return this.node
|
|
470
|
+
.expectCoValueLoaded(this.header.ruleset.team)
|
|
471
|
+
.getReadKey(keyID);
|
|
472
|
+
} else {
|
|
473
|
+
throw new Error(
|
|
474
|
+
"Only teams or values owned by teams have read secrets"
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
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
|
+
|
|
494
|
+
getTx(txID: TransactionID): Transaction | undefined {
|
|
495
|
+
return this.sessions[txID.sessionID]?.transactions[txID.txIndex];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
newContentSince(
|
|
499
|
+
knownState: CoValueKnownState | undefined
|
|
500
|
+
): NewContentMessage | undefined {
|
|
501
|
+
const newContent: NewContentMessage = {
|
|
502
|
+
action: "newContent",
|
|
503
|
+
coValueID: this.id,
|
|
504
|
+
header: knownState?.header ? undefined : this.header,
|
|
505
|
+
newContent: Object.fromEntries(
|
|
506
|
+
Object.entries(this.sessions)
|
|
507
|
+
.map(([sessionID, log]) => {
|
|
508
|
+
const newTransactions = log.transactions.slice(
|
|
509
|
+
knownState?.sessions[sessionID as SessionID] || 0
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
if (
|
|
513
|
+
newTransactions.length === 0 ||
|
|
514
|
+
!log.lastHash ||
|
|
515
|
+
!log.lastSignature
|
|
516
|
+
) {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return [
|
|
521
|
+
sessionID,
|
|
522
|
+
{
|
|
523
|
+
after:
|
|
524
|
+
knownState?.sessions[
|
|
525
|
+
sessionID as SessionID
|
|
526
|
+
] || 0,
|
|
527
|
+
newTransactions,
|
|
528
|
+
lastHash: log.lastHash,
|
|
529
|
+
lastSignature: log.lastSignature,
|
|
530
|
+
},
|
|
531
|
+
];
|
|
532
|
+
})
|
|
533
|
+
.filter((x): x is Exclude<typeof x, undefined> => !!x)
|
|
534
|
+
),
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
if (
|
|
538
|
+
!newContent.header &&
|
|
539
|
+
Object.keys(newContent.newContent).length === 0
|
|
540
|
+
) {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return newContent;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
getDependedOnCoValues(): RawCoValueID[] {
|
|
548
|
+
return this.header.ruleset.type === "team"
|
|
549
|
+
? expectTeamContent(this.getCurrentContent())
|
|
550
|
+
.keys()
|
|
551
|
+
.filter((k): k is AgentID => k.startsWith("co_agent"))
|
|
552
|
+
: this.header.ruleset.type === "ownedByTeam"
|
|
553
|
+
? [this.header.ruleset.team]
|
|
554
|
+
: [];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export type Agent = {
|
|
559
|
+
signatoryID: SignatoryID;
|
|
560
|
+
recipientID: RecipientID;
|
|
561
|
+
publicNickname?: string;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
export function getAgent(agentCredential: AgentCredential) {
|
|
565
|
+
return {
|
|
566
|
+
signatoryID: getSignatoryID(agentCredential.signatorySecret),
|
|
567
|
+
recipientID: getRecipientID(agentCredential.recipientSecret),
|
|
568
|
+
publicNickname: agentCredential.publicNickname,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function getAgentCoValueHeader(agent: Agent): CoValueHeader {
|
|
573
|
+
return {
|
|
574
|
+
type: "comap",
|
|
575
|
+
ruleset: {
|
|
576
|
+
type: "agent",
|
|
577
|
+
initialSignatoryID: agent.signatoryID,
|
|
578
|
+
initialRecipientID: agent.recipientID,
|
|
579
|
+
},
|
|
580
|
+
meta: null,
|
|
581
|
+
createdAt: null,
|
|
582
|
+
uniqueness: null,
|
|
583
|
+
publicNickname:
|
|
584
|
+
"agent" + (agent.publicNickname ? `-${agent.publicNickname}` : ""),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function getAgentID(agent: Agent): AgentID {
|
|
589
|
+
return coValueIDforHeader(getAgentCoValueHeader(agent)) as AgentID;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export type AgentCredential = {
|
|
593
|
+
signatorySecret: SignatorySecret;
|
|
594
|
+
recipientSecret: RecipientSecret;
|
|
595
|
+
publicNickname?: string;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
export function newRandomAgentCredential(
|
|
599
|
+
publicNickname?: string
|
|
600
|
+
): AgentCredential {
|
|
601
|
+
const signatorySecret = newRandomSignatory();
|
|
602
|
+
const recipientSecret = newRandomRecipient();
|
|
603
|
+
return { signatorySecret, recipientSecret, publicNickname };
|
|
604
|
+
}
|
|
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
|
+
|
|
638
|
+
// type Role = "admin" | "writer" | "reader";
|
|
639
|
+
|
|
640
|
+
// type PermissionsDef = CJMap<AgentID, Role, {[agent: AgentID]: Role}>;
|