midsummer-sol 0.1.4 → 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/index.js +818 -51
- package/package.json +9 -33
- package/sol-mcp.js +373 -28
- package/sol-secret-mcp.js +3250 -0
- package/sol.js +9174 -1963
package/index.js
CHANGED
|
@@ -11,7 +11,10 @@ function hashNode(node) {
|
|
|
11
11
|
canon = node.encoding ? `blob\x00${node.encoding}\x00${node.content}` : `blob\x00${node.content}`;
|
|
12
12
|
} else if (node.kind === "sealed")
|
|
13
13
|
canon = `sealed\x00${node.box}`;
|
|
14
|
-
else
|
|
14
|
+
else if (node.kind === "prov") {
|
|
15
|
+
const f = (k, v) => v === undefined ? "" : `\x00${k}\x00${v}`;
|
|
16
|
+
canon = "prov" + f("agentId", node.agentId) + f("sessionId", node.sessionId) + f("model", node.model) + f("intent", node.intent) + f("tool", node.tool) + f("rationale", node.rationale) + f("parent", node.parent);
|
|
17
|
+
} else
|
|
15
18
|
canon = "tree\x00" + Object.keys(node.entries).sort().map((k) => {
|
|
16
19
|
const e = node.entries[k];
|
|
17
20
|
return e.mode === undefined ? `${k}\x00${e.kind}\x00${e.hash}` : `${k}\x00${e.kind}\x00${e.hash}\x00${e.mode}`;
|
|
@@ -44,13 +47,161 @@ class Store {
|
|
|
44
47
|
return this.objects.size;
|
|
45
48
|
}
|
|
46
49
|
}
|
|
50
|
+
// src/struct.ts
|
|
51
|
+
import { randomBytes } from "node:crypto";
|
|
52
|
+
|
|
53
|
+
// src/async-store.ts
|
|
54
|
+
async function getTree(store, hash) {
|
|
55
|
+
const node = await store.get(hash);
|
|
56
|
+
if (!node || node.kind !== "tree")
|
|
57
|
+
throw new Error(`not a tree: ${hash}`);
|
|
58
|
+
return node;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class MemoryAsyncStore {
|
|
62
|
+
nodes = new Map;
|
|
63
|
+
async put(node) {
|
|
64
|
+
const h = hashNode(node);
|
|
65
|
+
if (!this.nodes.has(h))
|
|
66
|
+
this.nodes.set(h, node);
|
|
67
|
+
return h;
|
|
68
|
+
}
|
|
69
|
+
async get(hash) {
|
|
70
|
+
return this.nodes.get(hash);
|
|
71
|
+
}
|
|
72
|
+
async has(hash) {
|
|
73
|
+
return this.nodes.has(hash);
|
|
74
|
+
}
|
|
75
|
+
get size() {
|
|
76
|
+
return this.nodes.size;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/struct.ts
|
|
81
|
+
var SLOT_PREFIX = "\x00slot\x00";
|
|
82
|
+
var HIDDEN_KEY = "\x00hidden";
|
|
83
|
+
function isReservedKey(key) {
|
|
84
|
+
return key.startsWith("\x00");
|
|
85
|
+
}
|
|
86
|
+
function slotKey(slotId) {
|
|
87
|
+
return SLOT_PREFIX + slotId;
|
|
88
|
+
}
|
|
89
|
+
function slotIdFromKey(key) {
|
|
90
|
+
return key.startsWith(SLOT_PREFIX) ? key.slice(SLOT_PREFIX.length) : undefined;
|
|
91
|
+
}
|
|
92
|
+
function mintSlotId() {
|
|
93
|
+
return randomBytes(16).toString("base64url");
|
|
94
|
+
}
|
|
95
|
+
var STRUCT_HEADER = "__solStruct";
|
|
96
|
+
function parseStruct(plaintext) {
|
|
97
|
+
if (!plaintext.startsWith("{"))
|
|
98
|
+
return;
|
|
99
|
+
let v;
|
|
100
|
+
try {
|
|
101
|
+
v = JSON.parse(plaintext);
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!v || typeof v !== "object")
|
|
106
|
+
return;
|
|
107
|
+
const tag = v[STRUCT_HEADER];
|
|
108
|
+
if (tag === "subtree" || tag === "name" || tag === "entries")
|
|
109
|
+
return v;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
function nextBucket(n) {
|
|
113
|
+
let b = 256;
|
|
114
|
+
while (b < n)
|
|
115
|
+
b *= 2;
|
|
116
|
+
return b;
|
|
117
|
+
}
|
|
118
|
+
function padPlaintext(p) {
|
|
119
|
+
const bare = JSON.stringify(p);
|
|
120
|
+
const target = nextBucket(bare.length);
|
|
121
|
+
if (bare.length >= target)
|
|
122
|
+
return bare;
|
|
123
|
+
const overhead = `,"${"__pad"}":""`.length;
|
|
124
|
+
const fill = Math.max(0, target - bare.length - overhead);
|
|
125
|
+
return JSON.stringify({ ...p, __pad: " ".repeat(fill) });
|
|
126
|
+
}
|
|
127
|
+
async function collectAsync(store, hash, into) {
|
|
128
|
+
if (into[hash])
|
|
129
|
+
return;
|
|
130
|
+
const node = await store.get(hash);
|
|
131
|
+
if (!node)
|
|
132
|
+
throw new Error(`sealSubtree: node missing from store: ${hash}`);
|
|
133
|
+
into[hash] = node;
|
|
134
|
+
if (node.kind === "tree")
|
|
135
|
+
for (const e of Object.values(node.entries))
|
|
136
|
+
await collectAsync(store, e.hash, into);
|
|
137
|
+
}
|
|
138
|
+
async function serializeSubtreeAsync(store, dirRoot) {
|
|
139
|
+
const nodes = {};
|
|
140
|
+
await collectAsync(store, dirRoot, nodes);
|
|
141
|
+
return padPlaintext({ __solStruct: "subtree", bundle: { root: dirRoot, nodes } });
|
|
142
|
+
}
|
|
143
|
+
async function subtreeRootAt(store, root, dirPath) {
|
|
144
|
+
const segs = dirPath.split("/").filter(Boolean);
|
|
145
|
+
let cur = root;
|
|
146
|
+
for (let i = 0;i < segs.length; i++) {
|
|
147
|
+
const entry = (await getTree(store, cur)).entries[segs[i]];
|
|
148
|
+
if (!entry || entry.kind !== "tree")
|
|
149
|
+
return;
|
|
150
|
+
cur = entry.hash;
|
|
151
|
+
}
|
|
152
|
+
return cur;
|
|
153
|
+
}
|
|
154
|
+
async function entryAt(store, root, path) {
|
|
155
|
+
const segs = path.split("/").filter(Boolean);
|
|
156
|
+
let cur = root;
|
|
157
|
+
for (let i = 0;i < segs.length; i++) {
|
|
158
|
+
const entry = (await getTree(store, cur)).entries[segs[i]];
|
|
159
|
+
if (!entry)
|
|
160
|
+
return;
|
|
161
|
+
if (i === segs.length - 1)
|
|
162
|
+
return entry;
|
|
163
|
+
if (entry.kind !== "tree")
|
|
164
|
+
return;
|
|
165
|
+
cur = entry.hash;
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
async function serializeNameAsync(store, name, entry) {
|
|
170
|
+
const nodes = {};
|
|
171
|
+
await collectAsync(store, entry.hash, nodes);
|
|
172
|
+
const payload = {
|
|
173
|
+
__solStruct: "name",
|
|
174
|
+
name,
|
|
175
|
+
entryKind: entry.kind,
|
|
176
|
+
...entry.mode === undefined ? {} : { entryMode: entry.mode },
|
|
177
|
+
bundle: { root: entry.hash, nodes }
|
|
178
|
+
};
|
|
179
|
+
return padPlaintext(payload);
|
|
180
|
+
}
|
|
181
|
+
async function hiddenEntryAsync(store, entry) {
|
|
182
|
+
const nodes = {};
|
|
183
|
+
await collectAsync(store, entry.hash, nodes);
|
|
184
|
+
return { entryKind: entry.kind, ...entry.mode === undefined ? {} : { entryMode: entry.mode }, bundle: { root: entry.hash, nodes } };
|
|
185
|
+
}
|
|
186
|
+
function serializeHiddenEntries(slots) {
|
|
187
|
+
return padPlaintext({ __solStruct: "entries", slots });
|
|
188
|
+
}
|
|
189
|
+
|
|
47
190
|
// src/tree.ts
|
|
48
191
|
var EMPTY_TREE = { kind: "tree", entries: {} };
|
|
49
192
|
function emptyRoot(store) {
|
|
50
193
|
return store.put(EMPTY_TREE);
|
|
51
194
|
}
|
|
52
195
|
function segments(path) {
|
|
53
|
-
|
|
196
|
+
const segs = path.split("/").filter(Boolean);
|
|
197
|
+
for (const s of segs) {
|
|
198
|
+
if (!s.includes("\x00"))
|
|
199
|
+
continue;
|
|
200
|
+
const reserved = s.startsWith("\x00slot\x00") ? s.slice("\x00slot\x00".length) : s === "\x00hidden" ? "" : undefined;
|
|
201
|
+
if (reserved === undefined || reserved.includes("\x00"))
|
|
202
|
+
throw new Error("illegal NUL byte in path");
|
|
203
|
+
}
|
|
204
|
+
return segs;
|
|
54
205
|
}
|
|
55
206
|
function writeFile(store, root, path, content, opts = {}) {
|
|
56
207
|
const blob = opts.encoding ? { kind: "blob", content, encoding: opts.encoding } : { kind: "blob", content };
|
|
@@ -84,6 +235,21 @@ function fileAt(store, root, path) {
|
|
|
84
235
|
}
|
|
85
236
|
return;
|
|
86
237
|
}
|
|
238
|
+
function sealedBoxAt(store, root, path) {
|
|
239
|
+
const segs = segments(path);
|
|
240
|
+
let cur = root;
|
|
241
|
+
for (let i = 0;i < segs.length; i++) {
|
|
242
|
+
const entry = store.getTree(cur).entries[segs[i]];
|
|
243
|
+
if (!entry)
|
|
244
|
+
return;
|
|
245
|
+
if (i === segs.length - 1)
|
|
246
|
+
return entry.kind === "sealed" ? store.get(entry.hash).box : undefined;
|
|
247
|
+
if (entry.kind !== "tree")
|
|
248
|
+
return;
|
|
249
|
+
cur = entry.hash;
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
87
253
|
function modeAt(store, root, path) {
|
|
88
254
|
const segs = segments(path);
|
|
89
255
|
let cur = root;
|
|
@@ -135,6 +301,10 @@ function listAll(store, root, prefix = "") {
|
|
|
135
301
|
const tree = store.getTree(root);
|
|
136
302
|
const out = [];
|
|
137
303
|
for (const [name, entry] of Object.entries(tree.entries)) {
|
|
304
|
+
if (isReservedKey(name)) {
|
|
305
|
+
out.push(prefix ? `${prefix}/${name}` : name);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
138
308
|
const p = prefix ? `${prefix}/${name}` : name;
|
|
139
309
|
if (entry.kind === "tree")
|
|
140
310
|
out.push(...listAll(store, entry.hash, p));
|
|
@@ -198,7 +368,7 @@ function covers(prefix, path) {
|
|
|
198
368
|
return path === prefix || path.startsWith(prefix + "/");
|
|
199
369
|
}
|
|
200
370
|
// src/crypto.ts
|
|
201
|
-
import { createCipheriv, createDecipheriv, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, timingSafeEqual } from "node:crypto";
|
|
371
|
+
import { createCipheriv, createDecipheriv, createHash as createHash2, createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes as randomBytes2, scryptSync, sign as edSign, timingSafeEqual, verify as edVerify } from "node:crypto";
|
|
202
372
|
var UNREADABLE = "<<unreadable>>";
|
|
203
373
|
|
|
204
374
|
class KeyRing {
|
|
@@ -206,7 +376,7 @@ class KeyRing {
|
|
|
206
376
|
ensure(actor) {
|
|
207
377
|
let k = this.keys.get(actor);
|
|
208
378
|
if (!k) {
|
|
209
|
-
k =
|
|
379
|
+
k = randomBytes2(32);
|
|
210
380
|
this.keys.set(actor, k);
|
|
211
381
|
}
|
|
212
382
|
return k;
|
|
@@ -226,7 +396,7 @@ class KeyRing {
|
|
|
226
396
|
}
|
|
227
397
|
}
|
|
228
398
|
function aeadSeal(key, plain) {
|
|
229
|
-
const iv =
|
|
399
|
+
const iv = randomBytes2(12);
|
|
230
400
|
const c = createCipheriv("aes-256-gcm", key, iv);
|
|
231
401
|
const ct = Buffer.concat([c.update(plain), c.final()]);
|
|
232
402
|
return { iv: iv.toString("base64"), ct: ct.toString("base64"), tag: c.getAuthTag().toString("base64") };
|
|
@@ -237,14 +407,25 @@ function aeadOpen(key, box) {
|
|
|
237
407
|
return Buffer.concat([d.update(Buffer.from(box.ct, "base64")), d.final()]);
|
|
238
408
|
}
|
|
239
409
|
function sealContent(ring, content, recipients, epoch = 1) {
|
|
240
|
-
const cek =
|
|
410
|
+
const cek = randomBytes2(32);
|
|
241
411
|
const body = aeadSeal(cek, Buffer.from(content, "utf8"));
|
|
242
412
|
const lockboxes = {};
|
|
243
413
|
for (const r of recipients)
|
|
244
414
|
lockboxes[r] = aeadSeal(ring.ensure(r), cek);
|
|
245
415
|
return { body, lockboxes, epoch };
|
|
246
416
|
}
|
|
247
|
-
function openContent(ring, box, actor) {
|
|
417
|
+
function openContent(ring, box, actor, self) {
|
|
418
|
+
if (self) {
|
|
419
|
+
const alb = box.asym?.[self.accountId];
|
|
420
|
+
if (alb) {
|
|
421
|
+
try {
|
|
422
|
+
const cek = unwrapCekWith(self.privateKey, alb);
|
|
423
|
+
return aeadOpen(cek, box.body).toString("utf8");
|
|
424
|
+
} catch {
|
|
425
|
+
return UNREADABLE;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
248
429
|
const lb = box.lockboxes[actor];
|
|
249
430
|
const key = ring.key(actor);
|
|
250
431
|
if (!lb || !key)
|
|
@@ -257,7 +438,7 @@ function openContent(ring, box, actor) {
|
|
|
257
438
|
}
|
|
258
439
|
}
|
|
259
440
|
function isRecipient(box, actor) {
|
|
260
|
-
return actor in box.lockboxes;
|
|
441
|
+
return actor in box.lockboxes || !!box.asym?.[actor];
|
|
261
442
|
}
|
|
262
443
|
function ciphertextOf(box) {
|
|
263
444
|
return box.body.ct;
|
|
@@ -295,7 +476,7 @@ function unwrapCekWith(privateKey, lb) {
|
|
|
295
476
|
return aeadOpen(deriveWrapKey(shared, lb.epk, recipientPubB64), lb.box);
|
|
296
477
|
}
|
|
297
478
|
function sealWithRecovery(ring, content, recipients, recoveryPubKeys = {}, epoch = 1) {
|
|
298
|
-
const cek =
|
|
479
|
+
const cek = randomBytes2(32);
|
|
299
480
|
const body = aeadSeal(cek, Buffer.from(content, "utf8"));
|
|
300
481
|
const lockboxes = {};
|
|
301
482
|
for (const r of recipients)
|
|
@@ -316,6 +497,68 @@ function openWithRecovery(privateKey, box, recoveryId) {
|
|
|
316
497
|
return UNREADABLE;
|
|
317
498
|
}
|
|
318
499
|
}
|
|
500
|
+
function sealToAccounts(content, recipientPubKeys, opts = {}) {
|
|
501
|
+
const cek = randomBytes2(32);
|
|
502
|
+
const body = aeadSeal(cek, Buffer.from(content, "utf8"));
|
|
503
|
+
const asym = {};
|
|
504
|
+
for (const [acct, pub] of Object.entries(recipientPubKeys))
|
|
505
|
+
asym[acct] = wrapCekTo(pub, cek);
|
|
506
|
+
const lockboxes = {};
|
|
507
|
+
if (opts.ring && opts.localRecipients)
|
|
508
|
+
for (const r of opts.localRecipients)
|
|
509
|
+
lockboxes[r] = aeadSeal(opts.ring.ensure(r), cek);
|
|
510
|
+
const recovery = {};
|
|
511
|
+
for (const [id, pub] of Object.entries(opts.recoveryPubKeys ?? {}))
|
|
512
|
+
recovery[id] = wrapCekTo(pub, cek);
|
|
513
|
+
const box = { body, lockboxes, asym, epoch: opts.epoch ?? 1 };
|
|
514
|
+
if (Object.keys(recovery).length)
|
|
515
|
+
box.recovery = recovery;
|
|
516
|
+
return box;
|
|
517
|
+
}
|
|
518
|
+
var X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
|
|
519
|
+
var ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
520
|
+
var KDF_PARAMS = { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
|
|
521
|
+
function x25519FromSeed(seed) {
|
|
522
|
+
const privateKey = createPrivateKey({ key: Buffer.concat([X25519_PKCS8_PREFIX, seed]), format: "der", type: "pkcs8" });
|
|
523
|
+
return { publicKey: createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString("base64"), privateKey };
|
|
524
|
+
}
|
|
525
|
+
function ed25519FromSeed(seed) {
|
|
526
|
+
const privateKey = createPrivateKey({ key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]), format: "der", type: "pkcs8" });
|
|
527
|
+
return { publicKey: createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString("base64"), privateKey };
|
|
528
|
+
}
|
|
529
|
+
var WORDLIST = "abandon ability able about above absorb abstract access accident account accuse achieve acid acoustic acquire across action actor actual adapt add address adjust admit adult advance advice aerobic affair afford afraid again age agent agree ahead aim air airport aisle alarm album alert alien all alley allow almost alone alpha already also alter always amateur amazing among amount amused anchor ancient anger angle angry animal ankle announce annual another answer antenna antique anxiety apart apology appear apple approve april arch arctic area arena argue arm armed armor army around arrange arrest arrive arrow art artist artwork ask aspect assault asset assist assume asthma athlete atom attack attend attitude attract auction audit august aunt author auto autumn average avocado avoid awake aware away awesome awful awkward axis".split(" ");
|
|
530
|
+
function generateRecoveryCode(words = 12) {
|
|
531
|
+
const out = [];
|
|
532
|
+
for (let i = 0;i < words; i++)
|
|
533
|
+
out.push(WORDLIST[randomBytes2(2).readUInt16BE(0) % WORDLIST.length]);
|
|
534
|
+
return out.join(" ");
|
|
535
|
+
}
|
|
536
|
+
function deriveIdentity(accountId, recoveryCode, keyEpoch = 1) {
|
|
537
|
+
const salt = Buffer.from(`sol/identity/v1\x00${accountId}\x00epoch=${keyEpoch}`);
|
|
538
|
+
const xSeed = scryptSync(recoveryCode.normalize("NFKD"), Buffer.concat([salt, Buffer.from("\x00x25519")]), 32, KDF_PARAMS);
|
|
539
|
+
const eSeed = scryptSync(recoveryCode.normalize("NFKD"), Buffer.concat([salt, Buffer.from("\x00ed25519")]), 32, KDF_PARAMS);
|
|
540
|
+
const x = x25519FromSeed(xSeed);
|
|
541
|
+
const e = ed25519FromSeed(eSeed);
|
|
542
|
+
return { accountId, x25519Pub: x.publicKey, x25519Priv: x.privateKey, edPub: e.publicKey, edPriv: e.privateKey, keyEpoch };
|
|
543
|
+
}
|
|
544
|
+
function pubkeyBindingBytes(accountId, x25519Pub, keyEpoch) {
|
|
545
|
+
return Buffer.from(`sol/keydir/v1\x00${accountId}\x00${x25519Pub}\x00epoch=${keyEpoch}`);
|
|
546
|
+
}
|
|
547
|
+
function signPubkey(id) {
|
|
548
|
+
return edSign(null, pubkeyBindingBytes(id.accountId, id.x25519Pub, id.keyEpoch), id.edPriv).toString("base64");
|
|
549
|
+
}
|
|
550
|
+
function verifyPubkeySig(entry) {
|
|
551
|
+
try {
|
|
552
|
+
const edPub = createPublicKey({ key: Buffer.from(entry.edPub, "base64"), type: "spki", format: "der" });
|
|
553
|
+
return edVerify(null, pubkeyBindingBytes(entry.accountId, entry.x25519Pub, entry.keyEpoch), edPub, Buffer.from(entry.sig, "base64"));
|
|
554
|
+
} catch {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function pubFingerprint(x25519Pub) {
|
|
559
|
+
const h = createHash2("sha256").update(Buffer.from(x25519Pub, "base64")).digest("hex");
|
|
560
|
+
return h.slice(0, 32).match(/.{4}/g).join("-");
|
|
561
|
+
}
|
|
319
562
|
// src/log.ts
|
|
320
563
|
function groupIntoUnits(ops) {
|
|
321
564
|
const units = [];
|
|
@@ -354,7 +597,7 @@ function filterLog(ops, opts = {}) {
|
|
|
354
597
|
}
|
|
355
598
|
function formatLog(ops) {
|
|
356
599
|
return ops.map((op) => {
|
|
357
|
-
const by = op.by ?? "?";
|
|
600
|
+
const by = op.kind === "agent" ? `agent:${op.by ?? "?"}` : op.by ?? "?";
|
|
358
601
|
const head = `${op.seq} ${by} ${op.type} ${op.path}`;
|
|
359
602
|
return op.message ? `${head}: ${op.message}` : head;
|
|
360
603
|
});
|
|
@@ -600,11 +843,11 @@ function resolvePath(base, ours, theirs) {
|
|
|
600
843
|
const m = merge3(base, ours, theirs);
|
|
601
844
|
if (m.clean)
|
|
602
845
|
return { content: m.text };
|
|
603
|
-
return { content: m.text, conflict: { path: "", ours, theirs } };
|
|
846
|
+
return { content: m.text, conflict: { path: "", base, ours, theirs } };
|
|
604
847
|
}
|
|
605
848
|
return {
|
|
606
849
|
content: ours,
|
|
607
|
-
conflict: { path: "", ours, theirs }
|
|
850
|
+
conflict: { path: "", base, ours, theirs }
|
|
608
851
|
};
|
|
609
852
|
}
|
|
610
853
|
function merge(repo, baseHead, oursHead, theirsHead) {
|
|
@@ -617,6 +860,14 @@ function merge(repo, baseHead, oursHead, theirsHead) {
|
|
|
617
860
|
const conflicts = [];
|
|
618
861
|
let root = emptyRoot(store);
|
|
619
862
|
for (const path of [...paths].sort()) {
|
|
863
|
+
if (entryKindAt(store, baseHead, path) === "sealed" || entryKindAt(store, oursHead, path) === "sealed" || entryKindAt(store, theirsHead, path) === "sealed") {
|
|
864
|
+
const { box, conflict: conflict2 } = resolveSealed(store, baseHead, oursHead, theirsHead, path);
|
|
865
|
+
if (conflict2)
|
|
866
|
+
conflicts.push({ ...conflict2, path });
|
|
867
|
+
if (box !== undefined)
|
|
868
|
+
root = writeSealedLeaf(store, root, path, box);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
620
871
|
const base = readFile(store, baseHead, path);
|
|
621
872
|
const ours = readFile(store, oursHead, path);
|
|
622
873
|
const theirs = readFile(store, theirsHead, path);
|
|
@@ -629,6 +880,40 @@ function merge(repo, baseHead, oursHead, theirsHead) {
|
|
|
629
880
|
}
|
|
630
881
|
return { head: root, conflicts };
|
|
631
882
|
}
|
|
883
|
+
function resolveSealed(store, baseHead, oursHead, theirsHead, path) {
|
|
884
|
+
const base = sealedBoxAt(store, baseHead, path);
|
|
885
|
+
const ours = sealedBoxAt(store, oursHead, path);
|
|
886
|
+
const theirs = sealedBoxAt(store, theirsHead, path);
|
|
887
|
+
const oursChanged = ours !== base;
|
|
888
|
+
const theirsChanged = theirs !== base;
|
|
889
|
+
if (!oursChanged && !theirsChanged)
|
|
890
|
+
return { box: base };
|
|
891
|
+
if (oursChanged && !theirsChanged)
|
|
892
|
+
return { box: ours };
|
|
893
|
+
if (!oursChanged && theirsChanged)
|
|
894
|
+
return { box: theirs };
|
|
895
|
+
if (ours === theirs)
|
|
896
|
+
return { box: ours };
|
|
897
|
+
return { box: ours, conflict: { path: "", base, ours, theirs, sealed: true } };
|
|
898
|
+
}
|
|
899
|
+
function writeSealedLeaf(store, root, path, box) {
|
|
900
|
+
const segs = path.split("/").filter(Boolean);
|
|
901
|
+
const hash = store.put({ kind: "sealed", box });
|
|
902
|
+
const go = (treeHash, segments2) => {
|
|
903
|
+
const tree = store.getTree(treeHash);
|
|
904
|
+
const entries = { ...tree.entries };
|
|
905
|
+
const [head, ...rest] = segments2;
|
|
906
|
+
if (rest.length === 0)
|
|
907
|
+
entries[head] = { kind: "sealed", hash };
|
|
908
|
+
else {
|
|
909
|
+
const child = entries[head];
|
|
910
|
+
const childHash = child?.kind === "tree" ? child.hash : emptyRoot(store);
|
|
911
|
+
entries[head] = { kind: "tree", hash: go(childHash, rest) };
|
|
912
|
+
}
|
|
913
|
+
return store.put({ kind: "tree", entries });
|
|
914
|
+
};
|
|
915
|
+
return go(root, segs);
|
|
916
|
+
}
|
|
632
917
|
// src/labels.ts
|
|
633
918
|
class Labels {
|
|
634
919
|
labels = new Map;
|
|
@@ -763,32 +1048,6 @@ function cmdFork(view) {
|
|
|
763
1048
|
function short(hash) {
|
|
764
1049
|
return hash.slice(0, 12);
|
|
765
1050
|
}
|
|
766
|
-
// src/async-store.ts
|
|
767
|
-
async function getTree(store, hash) {
|
|
768
|
-
const node = await store.get(hash);
|
|
769
|
-
if (!node || node.kind !== "tree")
|
|
770
|
-
throw new Error(`not a tree: ${hash}`);
|
|
771
|
-
return node;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
class MemoryAsyncStore {
|
|
775
|
-
nodes = new Map;
|
|
776
|
-
async put(node) {
|
|
777
|
-
const h = hashNode(node);
|
|
778
|
-
if (!this.nodes.has(h))
|
|
779
|
-
this.nodes.set(h, node);
|
|
780
|
-
return h;
|
|
781
|
-
}
|
|
782
|
-
async get(hash) {
|
|
783
|
-
return this.nodes.get(hash);
|
|
784
|
-
}
|
|
785
|
-
async has(hash) {
|
|
786
|
-
return this.nodes.has(hash);
|
|
787
|
-
}
|
|
788
|
-
get size() {
|
|
789
|
-
return this.nodes.size;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
1051
|
// src/attest.ts
|
|
793
1052
|
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
794
1053
|
function canonicalOp(op) {
|
|
@@ -798,7 +1057,9 @@ function canonicalOp(op) {
|
|
|
798
1057
|
path: op.path,
|
|
799
1058
|
rootAfter: op.rootAfter,
|
|
800
1059
|
by: op.by ?? null,
|
|
801
|
-
message: op.message ?? null
|
|
1060
|
+
message: op.message ?? null,
|
|
1061
|
+
...op.kind !== undefined ? { kind: op.kind } : {},
|
|
1062
|
+
...op.prov !== undefined ? { prov: op.prov } : {}
|
|
802
1063
|
});
|
|
803
1064
|
}
|
|
804
1065
|
function signOp(key, op) {
|
|
@@ -816,6 +1077,31 @@ function verifyOp(key, op, sig) {
|
|
|
816
1077
|
return false;
|
|
817
1078
|
return timingSafeEqual2(expected, claimed);
|
|
818
1079
|
}
|
|
1080
|
+
function canonicalAtt(op) {
|
|
1081
|
+
return JSON.stringify({
|
|
1082
|
+
entryHash: op.entryHash ?? null,
|
|
1083
|
+
prov: op.prov ?? null,
|
|
1084
|
+
pushedBy: op.pushedBy ?? null,
|
|
1085
|
+
at: op.at ?? null
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
function attestOp(key, op) {
|
|
1089
|
+
return createHmac("sha256", key).update(canonicalAtt(op)).digest("base64");
|
|
1090
|
+
}
|
|
1091
|
+
function verifyAtt(key, op, sig = op.att) {
|
|
1092
|
+
if (!sig)
|
|
1093
|
+
return false;
|
|
1094
|
+
const expected = Buffer.from(attestOp(key, op), "base64");
|
|
1095
|
+
let claimed;
|
|
1096
|
+
try {
|
|
1097
|
+
claimed = Buffer.from(sig, "base64");
|
|
1098
|
+
} catch {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
if (expected.length !== claimed.length)
|
|
1102
|
+
return false;
|
|
1103
|
+
return timingSafeEqual2(expected, claimed);
|
|
1104
|
+
}
|
|
819
1105
|
|
|
820
1106
|
// src/errors.ts
|
|
821
1107
|
class CorruptRepoError extends Error {
|
|
@@ -835,7 +1121,15 @@ function chainOp(prevHash, op) {
|
|
|
835
1121
|
|
|
836
1122
|
// src/async-tree.ts
|
|
837
1123
|
function segments2(path) {
|
|
838
|
-
|
|
1124
|
+
const segs = path.split("/").filter(Boolean);
|
|
1125
|
+
for (const s of segs) {
|
|
1126
|
+
if (!s.includes("\x00"))
|
|
1127
|
+
continue;
|
|
1128
|
+
const reserved = s.startsWith("\x00slot\x00") ? s.slice("\x00slot\x00".length) : s === "\x00hidden" ? "" : undefined;
|
|
1129
|
+
if (reserved === undefined || reserved.includes("\x00"))
|
|
1130
|
+
throw new Error("illegal NUL byte in path");
|
|
1131
|
+
}
|
|
1132
|
+
return segs;
|
|
839
1133
|
}
|
|
840
1134
|
async function emptyRoot2(store) {
|
|
841
1135
|
return store.put({ kind: "tree", entries: {} });
|
|
@@ -849,6 +1143,70 @@ async function writeSealed(store, root, path, box) {
|
|
|
849
1143
|
const hash = await store.put({ kind: "sealed", box });
|
|
850
1144
|
return writeInto2(store, root, segments2(path), hash, "sealed");
|
|
851
1145
|
}
|
|
1146
|
+
async function hideName(store, root, path, box, slotId) {
|
|
1147
|
+
const hash = await store.put({ kind: "sealed", box });
|
|
1148
|
+
return hideInto(store, root, segments2(path), hash, slotKey(slotId));
|
|
1149
|
+
}
|
|
1150
|
+
async function reHideName(store, root, dirSegs, key, box) {
|
|
1151
|
+
const hash = await store.put({ kind: "sealed", box });
|
|
1152
|
+
if (dirSegs.length === 0) {
|
|
1153
|
+
const tree2 = await getTree(store, root);
|
|
1154
|
+
return store.put({ kind: "tree", entries: { ...tree2.entries, [key]: { kind: "sealed", hash } } });
|
|
1155
|
+
}
|
|
1156
|
+
const tree = await getTree(store, root);
|
|
1157
|
+
const entries = { ...tree.entries };
|
|
1158
|
+
const [head, ...rest] = dirSegs;
|
|
1159
|
+
const child = entries[head];
|
|
1160
|
+
if (!child || child.kind !== "tree")
|
|
1161
|
+
return root;
|
|
1162
|
+
entries[head] = { kind: "tree", hash: await reHideName(store, child.hash, rest, key, box) };
|
|
1163
|
+
return store.put({ kind: "tree", entries });
|
|
1164
|
+
}
|
|
1165
|
+
async function hideInto(store, treeHash, segs, sealedHash, key) {
|
|
1166
|
+
const tree = await getTree(store, treeHash);
|
|
1167
|
+
const entries = { ...tree.entries };
|
|
1168
|
+
const [head, ...rest] = segs;
|
|
1169
|
+
if (rest.length === 0) {
|
|
1170
|
+
delete entries[head];
|
|
1171
|
+
entries[key] = { kind: "sealed", hash: sealedHash };
|
|
1172
|
+
} else {
|
|
1173
|
+
const child = entries[head];
|
|
1174
|
+
if (!child || child.kind !== "tree")
|
|
1175
|
+
return treeHash;
|
|
1176
|
+
entries[head] = { kind: "tree", hash: await hideInto(store, child.hash, rest, sealedHash, key) };
|
|
1177
|
+
}
|
|
1178
|
+
return store.put({ kind: "tree", entries });
|
|
1179
|
+
}
|
|
1180
|
+
async function hideExistence(store, root, path, hiddenBox) {
|
|
1181
|
+
const hash = await store.put({ kind: "sealed", box: hiddenBox });
|
|
1182
|
+
return hideExistenceInto(store, root, segments2(path), hash);
|
|
1183
|
+
}
|
|
1184
|
+
async function hideExistenceInto(store, treeHash, segs, sidecarHash) {
|
|
1185
|
+
const tree = await getTree(store, treeHash);
|
|
1186
|
+
const entries = { ...tree.entries };
|
|
1187
|
+
const [head, ...rest] = segs;
|
|
1188
|
+
if (rest.length === 0) {
|
|
1189
|
+
delete entries[head];
|
|
1190
|
+
entries[HIDDEN_KEY] = { kind: "sealed", hash: sidecarHash };
|
|
1191
|
+
} else {
|
|
1192
|
+
const child = entries[head];
|
|
1193
|
+
if (!child || child.kind !== "tree")
|
|
1194
|
+
return treeHash;
|
|
1195
|
+
entries[head] = { kind: "tree", hash: await hideExistenceInto(store, child.hash, rest, sidecarHash) };
|
|
1196
|
+
}
|
|
1197
|
+
return store.put({ kind: "tree", entries });
|
|
1198
|
+
}
|
|
1199
|
+
async function dirEntriesAt(store, root, dirPath) {
|
|
1200
|
+
const segs = segments2(dirPath);
|
|
1201
|
+
let cur = root;
|
|
1202
|
+
for (const seg of segs) {
|
|
1203
|
+
const entry = (await getTree(store, cur)).entries[seg];
|
|
1204
|
+
if (!entry || entry.kind !== "tree")
|
|
1205
|
+
return;
|
|
1206
|
+
cur = entry.hash;
|
|
1207
|
+
}
|
|
1208
|
+
return (await getTree(store, cur)).entries;
|
|
1209
|
+
}
|
|
852
1210
|
async function writeInto2(store, treeHash, segs, hash, leafKind, mode) {
|
|
853
1211
|
const tree = await getTree(store, treeHash);
|
|
854
1212
|
const entries = { ...tree.entries };
|
|
@@ -976,6 +1334,10 @@ async function listAll2(store, root, prefix = "") {
|
|
|
976
1334
|
const tree = await getTree(store, root);
|
|
977
1335
|
const out = [];
|
|
978
1336
|
for (const [name, entry] of Object.entries(tree.entries)) {
|
|
1337
|
+
if (isReservedKey(name)) {
|
|
1338
|
+
out.push(prefix ? `${prefix}/${name}` : name);
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
979
1341
|
const p = prefix ? `${prefix}/${name}` : name;
|
|
980
1342
|
if (entry.kind === "tree")
|
|
981
1343
|
out.push(...await listAll2(store, entry.hash, p));
|
|
@@ -985,6 +1347,224 @@ async function listAll2(store, root, prefix = "") {
|
|
|
985
1347
|
return out.sort();
|
|
986
1348
|
}
|
|
987
1349
|
|
|
1350
|
+
// src/sign.ts
|
|
1351
|
+
import { createHash as createHash3, generateKeyPairSync as generateKeyPairSync2, createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, sign as edSign2, verify as edVerify2 } from "node:crypto";
|
|
1352
|
+
function canonicalAuthor(op) {
|
|
1353
|
+
return JSON.stringify({
|
|
1354
|
+
type: op.type,
|
|
1355
|
+
path: op.path,
|
|
1356
|
+
rootAfter: op.rootAfter,
|
|
1357
|
+
by: op.by ?? null,
|
|
1358
|
+
message: op.message ?? null,
|
|
1359
|
+
...op.kind !== undefined ? { kind: op.kind } : {},
|
|
1360
|
+
...op.prov !== undefined ? { prov: op.prov } : {}
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
function fingerprintOf(pub) {
|
|
1364
|
+
const der = Buffer.from(pub, "base64");
|
|
1365
|
+
return "sol1:" + createHash3("sha256").update(der).digest("hex").slice(0, 16);
|
|
1366
|
+
}
|
|
1367
|
+
var FINGERPRINT_RE = /^sol1:[0-9a-f]{16}$/;
|
|
1368
|
+
function normalizeFingerprint(fp) {
|
|
1369
|
+
const t = fp.trim();
|
|
1370
|
+
const body = (t.toLowerCase().startsWith("sol1:") ? t.slice(5) : t).replace(/[-:\s]/g, "").toLowerCase();
|
|
1371
|
+
const candidate = "sol1:" + body;
|
|
1372
|
+
return FINGERPRINT_RE.test(candidate) ? candidate : undefined;
|
|
1373
|
+
}
|
|
1374
|
+
function generateKeypair() {
|
|
1375
|
+
const { publicKey, privateKey } = generateKeyPairSync2("ed25519");
|
|
1376
|
+
const pub = publicKey.export({ type: "spki", format: "der" }).toString("base64");
|
|
1377
|
+
const seed = privateKey.export({ type: "pkcs8", format: "der" }).subarray(16).toString("base64");
|
|
1378
|
+
return { seed, pub, fingerprint: fingerprintOf(pub) };
|
|
1379
|
+
}
|
|
1380
|
+
var ED25519_PKCS8_PREFIX2 = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
1381
|
+
function loadPrivateKey(secret) {
|
|
1382
|
+
const s = secret.trim();
|
|
1383
|
+
if (s.includes("-----BEGIN"))
|
|
1384
|
+
return createPrivateKey2(s);
|
|
1385
|
+
const raw = Buffer.from(s, "base64");
|
|
1386
|
+
if (raw.length === 32)
|
|
1387
|
+
return createPrivateKey2({ key: Buffer.concat([ED25519_PKCS8_PREFIX2, raw]), format: "der", type: "pkcs8" });
|
|
1388
|
+
return createPrivateKey2({ key: raw, format: "der", type: "pkcs8" });
|
|
1389
|
+
}
|
|
1390
|
+
function publicOf(priv) {
|
|
1391
|
+
return createPublicKey2(priv).export({ type: "spki", format: "der" }).toString("base64");
|
|
1392
|
+
}
|
|
1393
|
+
function signerFrom(secret) {
|
|
1394
|
+
const priv = loadPrivateKey(secret);
|
|
1395
|
+
const pub = publicOf(priv);
|
|
1396
|
+
return { priv, pub, fingerprint: fingerprintOf(pub) };
|
|
1397
|
+
}
|
|
1398
|
+
function signAuthor(signer, op) {
|
|
1399
|
+
const sig = edSign2(null, Buffer.from(canonicalAuthor(op)), signer.priv).toString("base64");
|
|
1400
|
+
return { sig, pub: signer.pub };
|
|
1401
|
+
}
|
|
1402
|
+
function verifyAuthor(op) {
|
|
1403
|
+
if (!op.sig || !op.pub)
|
|
1404
|
+
return { signed: false, valid: false };
|
|
1405
|
+
try {
|
|
1406
|
+
const ok = edVerify2(null, Buffer.from(canonicalAuthor(op)), createPublicKey2({ key: Buffer.from(op.pub, "base64"), format: "der", type: "spki" }), Buffer.from(op.sig, "base64"));
|
|
1407
|
+
return ok ? { signed: true, valid: true, fingerprint: fingerprintOf(op.pub) } : { signed: true, valid: false };
|
|
1408
|
+
} catch {
|
|
1409
|
+
return { signed: true, valid: false };
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function trustAuthor(op, trust = {}) {
|
|
1413
|
+
const v = verifyAuthor(op);
|
|
1414
|
+
const claimedActor = op.by ?? undefined;
|
|
1415
|
+
if (!v.signed)
|
|
1416
|
+
return { ...v, claimedActor, state: "unsigned" };
|
|
1417
|
+
if (!v.valid)
|
|
1418
|
+
return { ...v, claimedActor, state: "invalid" };
|
|
1419
|
+
const expected = claimedActor ? trust[claimedActor] : undefined;
|
|
1420
|
+
if (!expected)
|
|
1421
|
+
return { ...v, claimedActor, state: "untrusted" };
|
|
1422
|
+
return { ...v, claimedActor, state: expected === v.fingerprint ? "trusted" : "forged" };
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// src/prov.ts
|
|
1426
|
+
function provenanceFromEnv(env = process.env) {
|
|
1427
|
+
const kind = env.SOL_KIND === "agent" ? "agent" : "human";
|
|
1428
|
+
const node = buildProvNode({
|
|
1429
|
+
agentId: env.SOL_AGENT_ID || (kind === "agent" ? env.SOL_ACTOR : undefined),
|
|
1430
|
+
sessionId: env.SOL_SESSION,
|
|
1431
|
+
model: env.SOL_MODEL,
|
|
1432
|
+
intent: env.SOL_INTENT,
|
|
1433
|
+
tool: env.SOL_TOOL
|
|
1434
|
+
});
|
|
1435
|
+
return node ? { kind, node } : { kind };
|
|
1436
|
+
}
|
|
1437
|
+
function buildProvNode(fields) {
|
|
1438
|
+
const clean = { kind: "prov" };
|
|
1439
|
+
let any = false;
|
|
1440
|
+
for (const k of ["agentId", "sessionId", "model", "intent", "tool", "rationale", "parent"]) {
|
|
1441
|
+
const v = fields[k];
|
|
1442
|
+
if (v) {
|
|
1443
|
+
clean[k] = v;
|
|
1444
|
+
any = true;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return any ? clean : undefined;
|
|
1448
|
+
}
|
|
1449
|
+
function provOf(op, reader) {
|
|
1450
|
+
if (!op.prov)
|
|
1451
|
+
return;
|
|
1452
|
+
const n = reader.get(op.prov);
|
|
1453
|
+
return n && n.kind === "prov" ? n : undefined;
|
|
1454
|
+
}
|
|
1455
|
+
function authorLabel(op, reader) {
|
|
1456
|
+
const name = op.by ?? "?";
|
|
1457
|
+
const p = reader ? provOf(op, reader) : undefined;
|
|
1458
|
+
if (op.kind !== "agent" && !p)
|
|
1459
|
+
return name;
|
|
1460
|
+
const who = op.kind === "agent" ? `agent ${p?.agentId ?? p?.model ?? name}` : name;
|
|
1461
|
+
const bits = [];
|
|
1462
|
+
if (p?.sessionId)
|
|
1463
|
+
bits.push(`session ${p.sessionId}`);
|
|
1464
|
+
const tail = p?.intent ? ` — intent: ${p.intent}` : "";
|
|
1465
|
+
return `${who}${bits.length ? ` (${bits.join(", ")})` : ""}${tail}`;
|
|
1466
|
+
}
|
|
1467
|
+
function provJson(op, reader, key, trust) {
|
|
1468
|
+
const p = reader ? provOf(op, reader) : undefined;
|
|
1469
|
+
if (op.kind === undefined && !p && op.att === undefined && op.pushedBy === undefined && op.sig === undefined)
|
|
1470
|
+
return;
|
|
1471
|
+
const out = { kind: op.kind ?? "human" };
|
|
1472
|
+
if (p) {
|
|
1473
|
+
for (const k of ["agentId", "sessionId", "model", "intent", "tool", "rationale", "parent"])
|
|
1474
|
+
if (p[k])
|
|
1475
|
+
out[k] = p[k];
|
|
1476
|
+
}
|
|
1477
|
+
if (op.sig !== undefined) {
|
|
1478
|
+
const t = trustAuthor(op, trust);
|
|
1479
|
+
out.signed = true;
|
|
1480
|
+
out.verified = t.valid;
|
|
1481
|
+
if (t.fingerprint)
|
|
1482
|
+
out.fingerprint = t.fingerprint;
|
|
1483
|
+
if (t.claimedActor !== undefined)
|
|
1484
|
+
out.claimedActor = t.claimedActor;
|
|
1485
|
+
out.trust = t.state;
|
|
1486
|
+
}
|
|
1487
|
+
if (op.pushedBy !== undefined)
|
|
1488
|
+
out.pushedBy = op.pushedBy;
|
|
1489
|
+
if (op.att !== undefined) {
|
|
1490
|
+
out.att = op.att;
|
|
1491
|
+
out.attested = key ? verifyAtt(key, op) : true;
|
|
1492
|
+
} else if (op.pushedBy !== undefined) {
|
|
1493
|
+
out.attested = false;
|
|
1494
|
+
}
|
|
1495
|
+
return out;
|
|
1496
|
+
}
|
|
1497
|
+
function attTag(op, key) {
|
|
1498
|
+
if (op.att === undefined)
|
|
1499
|
+
return "[unattested/local]";
|
|
1500
|
+
if (key && !verifyAtt(key, op))
|
|
1501
|
+
return "[att INVALID]";
|
|
1502
|
+
return `[attested pushedBy=${op.pushedBy ?? "?"}]`;
|
|
1503
|
+
}
|
|
1504
|
+
function signTag(op, trust) {
|
|
1505
|
+
const t = trustAuthor(op, trust);
|
|
1506
|
+
switch (t.state) {
|
|
1507
|
+
case "unsigned":
|
|
1508
|
+
return "unsigned";
|
|
1509
|
+
case "invalid":
|
|
1510
|
+
return "INVALID ✗";
|
|
1511
|
+
case "forged":
|
|
1512
|
+
return `FORGED ${t.claimedActor ?? "?"}!=${t.fingerprint} ✗`;
|
|
1513
|
+
case "trusted":
|
|
1514
|
+
return `signed ${t.fingerprint} ✓ (trusted ${t.claimedActor})`;
|
|
1515
|
+
default:
|
|
1516
|
+
return `signed ${t.fingerprint} ✓`;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
function signTrailerValue(op, trust) {
|
|
1520
|
+
const t = trustAuthor(op, trust);
|
|
1521
|
+
switch (t.state) {
|
|
1522
|
+
case "unsigned":
|
|
1523
|
+
return;
|
|
1524
|
+
case "invalid":
|
|
1525
|
+
return "INVALID";
|
|
1526
|
+
case "forged":
|
|
1527
|
+
return `${t.fingerprint} FORGED (claimed ${t.claimedActor ?? "?"})`;
|
|
1528
|
+
case "trusted":
|
|
1529
|
+
return `${t.fingerprint} verified (trusted ${t.claimedActor})`;
|
|
1530
|
+
default:
|
|
1531
|
+
return `${t.fingerprint} verified`;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function provTrailers(op, reader, key, trust) {
|
|
1535
|
+
const p = reader ? provOf(op, reader) : undefined;
|
|
1536
|
+
const lines2 = [];
|
|
1537
|
+
const signed = trustAuthor(op, trust);
|
|
1538
|
+
const isAgent = op.kind === "agent" || signed.signed && signed.valid;
|
|
1539
|
+
if (isAgent)
|
|
1540
|
+
lines2.push("Sol-Kind: agent");
|
|
1541
|
+
const agentId = p?.agentId || (isAgent ? signed.fingerprint || op.by : undefined);
|
|
1542
|
+
if (agentId)
|
|
1543
|
+
lines2.push(`Sol-Agent: ${agentId}`);
|
|
1544
|
+
if (p?.sessionId)
|
|
1545
|
+
lines2.push(`Sol-Session: ${p.sessionId}`);
|
|
1546
|
+
if (p?.model)
|
|
1547
|
+
lines2.push(`Sol-Model: ${p.model}`);
|
|
1548
|
+
const intent = p?.intent || (isAgent ? op.message?.trim() || undefined : undefined);
|
|
1549
|
+
if (intent)
|
|
1550
|
+
lines2.push(`Sol-Intent: ${intent}`);
|
|
1551
|
+
const sign = signTrailerValue(op, trust);
|
|
1552
|
+
if (sign)
|
|
1553
|
+
lines2.push(`Sol-Sign: ${sign}`);
|
|
1554
|
+
if (op.att !== undefined && !(key && !verifyAtt(key, op)))
|
|
1555
|
+
lines2.push(`Sol-Attested: pushedBy=${op.pushedBy ?? "?"}`);
|
|
1556
|
+
return lines2;
|
|
1557
|
+
}
|
|
1558
|
+
async function captureProv(sink, env = process.env) {
|
|
1559
|
+
const { kind, node } = provenanceFromEnv(env);
|
|
1560
|
+
const out = {};
|
|
1561
|
+
if (kind === "agent")
|
|
1562
|
+
out.kind = "agent";
|
|
1563
|
+
if (node)
|
|
1564
|
+
out.prov = await sink.put(node);
|
|
1565
|
+
return out;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
988
1568
|
// src/async-repo.ts
|
|
989
1569
|
function pageOps(ops, opts = {}) {
|
|
990
1570
|
let out = opts.from !== undefined ? ops.filter((o) => o.seq >= opts.from) : ops.slice();
|
|
@@ -1016,15 +1596,27 @@ class MemoryOpLog {
|
|
|
1016
1596
|
return pageOps(this.entries, opts);
|
|
1017
1597
|
}
|
|
1018
1598
|
}
|
|
1599
|
+
var noSign = (op) => op;
|
|
1019
1600
|
|
|
1020
1601
|
class AsyncRepo {
|
|
1021
1602
|
store;
|
|
1022
1603
|
log;
|
|
1023
1604
|
actor;
|
|
1024
|
-
|
|
1605
|
+
sign;
|
|
1606
|
+
constructor(store, log, actor = "anon", sign = noSign) {
|
|
1025
1607
|
this.store = store;
|
|
1026
1608
|
this.log = log;
|
|
1027
1609
|
this.actor = actor;
|
|
1610
|
+
this.sign = sign;
|
|
1611
|
+
}
|
|
1612
|
+
async appendAuthored(entry, by) {
|
|
1613
|
+
await this.log.append(by === this.actor ? this.sign(entry) : entry);
|
|
1614
|
+
}
|
|
1615
|
+
provFor;
|
|
1616
|
+
async provenance(by) {
|
|
1617
|
+
if (by !== this.actor)
|
|
1618
|
+
return {};
|
|
1619
|
+
return this.provFor ??= captureProv(this.store);
|
|
1028
1620
|
}
|
|
1029
1621
|
async currentRoot() {
|
|
1030
1622
|
const head = await this.log.head();
|
|
@@ -1041,7 +1633,7 @@ class AsyncRepo {
|
|
|
1041
1633
|
}
|
|
1042
1634
|
async writeFile(path, content, at = Date.now(), by = this.actor) {
|
|
1043
1635
|
const rootAfter = await writeFile2(this.store, await this.currentRoot(), path, content);
|
|
1044
|
-
await this.
|
|
1636
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by, ...await this.provenance(by) }, by);
|
|
1045
1637
|
return rootAfter;
|
|
1046
1638
|
}
|
|
1047
1639
|
async applyBatch(changes) {
|
|
@@ -1060,7 +1652,7 @@ class AsyncRepo {
|
|
|
1060
1652
|
}
|
|
1061
1653
|
async writeBytes(path, data, at = Date.now(), by = this.actor) {
|
|
1062
1654
|
const rootAfter = await writeFile2(this.store, await this.currentRoot(), path, Buffer.from(data).toString("base64"), { encoding: "base64" });
|
|
1063
|
-
await this.
|
|
1655
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by, ...await this.provenance(by) }, by);
|
|
1064
1656
|
return rootAfter;
|
|
1065
1657
|
}
|
|
1066
1658
|
async readBytes(path) {
|
|
@@ -1079,7 +1671,7 @@ class AsyncRepo {
|
|
|
1079
1671
|
if (!f)
|
|
1080
1672
|
return this.currentRoot();
|
|
1081
1673
|
const rootAfter = await writeFile2(this.store, await this.currentRoot(), path, f.content, { encoding: f.encoding, mode });
|
|
1082
|
-
await this.
|
|
1674
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "write", path, rootAfter, at, by: this.actor, ...await this.provenance(this.actor) }, this.actor);
|
|
1083
1675
|
return rootAfter;
|
|
1084
1676
|
}
|
|
1085
1677
|
async mode(path) {
|
|
@@ -1087,7 +1679,96 @@ class AsyncRepo {
|
|
|
1087
1679
|
}
|
|
1088
1680
|
async writeSealed(path, box, at = Date.now()) {
|
|
1089
1681
|
const rootAfter = await writeSealed(this.store, await this.currentRoot(), path, box);
|
|
1090
|
-
await this.
|
|
1682
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "seal", path, rootAfter, at, by: this.actor, ...await this.provenance(this.actor) }, this.actor);
|
|
1683
|
+
return rootAfter;
|
|
1684
|
+
}
|
|
1685
|
+
async sealSubtree(dirPath, seal, at = Date.now()) {
|
|
1686
|
+
const subRoot = await subtreeRootAt(this.store, await this.currentRoot(), dirPath);
|
|
1687
|
+
if (subRoot === undefined)
|
|
1688
|
+
return;
|
|
1689
|
+
const plaintext = await serializeSubtreeAsync(this.store, subRoot);
|
|
1690
|
+
return this.writeSealed(dirPath, seal(plaintext), at);
|
|
1691
|
+
}
|
|
1692
|
+
async hideName(path, seal, opts = {}, at = Date.now()) {
|
|
1693
|
+
const root = await this.currentRoot();
|
|
1694
|
+
const segs = path.split("/").filter(Boolean);
|
|
1695
|
+
const name = segs[segs.length - 1];
|
|
1696
|
+
let entry = await entryAt(this.store, root, path);
|
|
1697
|
+
if (!entry) {
|
|
1698
|
+
if (opts.contentSeal && opts.absentContent !== undefined) {
|
|
1699
|
+
const sealedHash = await this.store.put({ kind: "sealed", box: opts.contentSeal(opts.absentContent) });
|
|
1700
|
+
entry = { kind: "sealed", hash: sealedHash };
|
|
1701
|
+
} else {
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (slotIdFromKey(name) !== undefined)
|
|
1706
|
+
return slotIdFromKey(name);
|
|
1707
|
+
if (opts.contentSeal && entry.kind === "blob") {
|
|
1708
|
+
const blob = await this.store.get(entry.hash);
|
|
1709
|
+
if (blob && blob.kind === "blob") {
|
|
1710
|
+
const sealedHash = await this.store.put({ kind: "sealed", box: opts.contentSeal(blob.content) });
|
|
1711
|
+
entry = entry.mode === undefined ? { kind: "sealed", hash: sealedHash } : { kind: "sealed", hash: sealedHash, mode: entry.mode };
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
const slotId = opts.slotId ?? mintSlotId();
|
|
1715
|
+
const plaintext = await serializeNameAsync(this.store, name, entry);
|
|
1716
|
+
const rootAfter = await hideName(this.store, root, path, seal(plaintext), slotId);
|
|
1717
|
+
const slotPath = segs.length > 1 ? `${segs.slice(0, -1).join("/")}/${slotKey(slotId)}` : slotKey(slotId);
|
|
1718
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "seal", path: slotPath, rootAfter, at, by: this.actor, ...await this.provenance(this.actor) }, this.actor);
|
|
1719
|
+
return slotId;
|
|
1720
|
+
}
|
|
1721
|
+
async reHideName(realDir, name, slotId, newContent, seal, contentSeal, at = Date.now()) {
|
|
1722
|
+
const root = await this.currentRoot();
|
|
1723
|
+
const sealedHash = await this.store.put({ kind: "sealed", box: contentSeal(newContent) });
|
|
1724
|
+
const childEntry = { kind: "sealed", hash: sealedHash };
|
|
1725
|
+
const plaintext = await serializeNameAsync(this.store, name, childEntry);
|
|
1726
|
+
const dirSegs = realDir.split("/").filter(Boolean);
|
|
1727
|
+
const rootAfter = await reHideName(this.store, root, dirSegs, slotKey(slotId), seal(plaintext));
|
|
1728
|
+
const slotPath = dirSegs.length ? `${dirSegs.join("/")}/${slotKey(slotId)}` : slotKey(slotId);
|
|
1729
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "seal", path: slotPath, rootAfter, at, by: this.actor, ...await this.provenance(this.actor) }, this.actor);
|
|
1730
|
+
return rootAfter;
|
|
1731
|
+
}
|
|
1732
|
+
async hideExistence(path, seal, opts = {}, at = Date.now()) {
|
|
1733
|
+
const root = await this.currentRoot();
|
|
1734
|
+
const segs = path.split("/").filter(Boolean);
|
|
1735
|
+
const name = segs[segs.length - 1];
|
|
1736
|
+
const dirPath = segs.slice(0, -1).join("/");
|
|
1737
|
+
let entry = await entryAt(this.store, root, path);
|
|
1738
|
+
if (name === HIDDEN_KEY)
|
|
1739
|
+
return root;
|
|
1740
|
+
if (!entry) {
|
|
1741
|
+
if (opts.contentSeal && opts.absentContent !== undefined) {
|
|
1742
|
+
const sealedHash = await this.store.put({ kind: "sealed", box: opts.contentSeal(opts.absentContent) });
|
|
1743
|
+
entry = { kind: "sealed", hash: sealedHash };
|
|
1744
|
+
} else {
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
if (opts.contentSeal && entry.kind === "blob") {
|
|
1749
|
+
const blob = await this.store.get(entry.hash);
|
|
1750
|
+
if (blob && blob.kind === "blob") {
|
|
1751
|
+
const sealedHash = await this.store.put({ kind: "sealed", box: opts.contentSeal(blob.content) });
|
|
1752
|
+
entry = entry.mode === undefined ? { kind: "sealed", hash: sealedHash } : { kind: "sealed", hash: sealedHash, mode: entry.mode };
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
const slots = {};
|
|
1756
|
+
const dirEntries = await dirEntriesAt(this.store, root, dirPath);
|
|
1757
|
+
const prior = dirEntries?.[HIDDEN_KEY];
|
|
1758
|
+
if (prior && prior.kind === "sealed" && opts.openPrior) {
|
|
1759
|
+
const priorNode = await this.store.get(prior.hash);
|
|
1760
|
+
if (priorNode && priorNode.kind === "sealed") {
|
|
1761
|
+
const opened = opts.openPrior(priorNode.box);
|
|
1762
|
+
const payload = opened === undefined ? undefined : parseStruct(opened);
|
|
1763
|
+
if (payload?.__solStruct === "entries")
|
|
1764
|
+
Object.assign(slots, payload.slots);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
slots[name] = await hiddenEntryAsync(this.store, entry);
|
|
1768
|
+
const plaintext = serializeHiddenEntries(slots);
|
|
1769
|
+
const rootAfter = await hideExistence(this.store, root, path, seal(plaintext));
|
|
1770
|
+
const hiddenPath = dirPath ? `${dirPath}/${HIDDEN_KEY}` : HIDDEN_KEY;
|
|
1771
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "seal", path: hiddenPath, rootAfter, at, by: this.actor, ...await this.provenance(this.actor) }, this.actor);
|
|
1091
1772
|
return rootAfter;
|
|
1092
1773
|
}
|
|
1093
1774
|
async read(path) {
|
|
@@ -1095,7 +1776,7 @@ class AsyncRepo {
|
|
|
1095
1776
|
}
|
|
1096
1777
|
async deleteFile(path, at = Date.now(), by = this.actor) {
|
|
1097
1778
|
const rootAfter = await deleteFile2(this.store, await this.currentRoot(), path);
|
|
1098
|
-
await this.
|
|
1779
|
+
await this.appendAuthored({ seq: await this.log.seq() + 1, type: "delete", path, rootAfter, at, by, ...await this.provenance(by) }, by);
|
|
1099
1780
|
return rootAfter;
|
|
1100
1781
|
}
|
|
1101
1782
|
async readFile(path) {
|
|
@@ -1232,14 +1913,33 @@ class SealedClient {
|
|
|
1232
1913
|
const box = sealContent(this.ring, content, recipients);
|
|
1233
1914
|
return this.repo.writeSealed(path, JSON.stringify(box));
|
|
1234
1915
|
}
|
|
1235
|
-
async
|
|
1916
|
+
async sealToAccounts(path, content, recipientPubKeys, opts = {}) {
|
|
1917
|
+
const box = sealToAccounts(content, recipientPubKeys, { ring: this.ring, localRecipients: opts.localRecipients, recoveryPubKeys: opts.recoveryPubKeys, epoch: opts.epoch });
|
|
1918
|
+
return this.repo.writeSealed(path, JSON.stringify(box));
|
|
1919
|
+
}
|
|
1920
|
+
async sealSubtree(dirPath, recipientPubKeys, opts = {}) {
|
|
1921
|
+
return this.repo.sealSubtree(dirPath, (plaintext) => JSON.stringify(sealToAccounts(plaintext, recipientPubKeys, { ring: this.ring, localRecipients: opts.localRecipients, recoveryPubKeys: opts.recoveryPubKeys, epoch: opts.epoch })));
|
|
1922
|
+
}
|
|
1923
|
+
async hideName(path, recipientPubKeys, opts = {}) {
|
|
1924
|
+
const sealOne = (plaintext) => JSON.stringify(sealToAccounts(plaintext, recipientPubKeys, { ring: this.ring, localRecipients: opts.localRecipients, recoveryPubKeys: opts.recoveryPubKeys, epoch: opts.epoch }));
|
|
1925
|
+
return this.repo.hideName(path, sealOne, { slotId: opts.slotId, ...opts.contentSeal ? { contentSeal: sealOne } : {}, ...opts.absentContent !== undefined ? { absentContent: opts.absentContent } : {} });
|
|
1926
|
+
}
|
|
1927
|
+
async hideExistence(path, recipientPubKeys, opts = {}) {
|
|
1928
|
+
const sealOne = (plaintext) => JSON.stringify(sealToAccounts(plaintext, recipientPubKeys, { ring: this.ring, localRecipients: opts.localRecipients, recoveryPubKeys: opts.recoveryPubKeys, epoch: opts.epoch }));
|
|
1929
|
+
return this.repo.hideExistence(path, sealOne, { openPrior: opts.openPrior, ...opts.contentSeal ? { contentSeal: sealOne } : {}, ...opts.absentContent !== undefined ? { absentContent: opts.absentContent } : {} });
|
|
1930
|
+
}
|
|
1931
|
+
async reHideName(realDir, name, slotId, newContent, recipientPubKeys, opts = {}) {
|
|
1932
|
+
const sealOne = (plaintext) => JSON.stringify(sealToAccounts(plaintext, recipientPubKeys, { ring: this.ring, localRecipients: opts.localRecipients, recoveryPubKeys: opts.recoveryPubKeys, epoch: opts.epoch }));
|
|
1933
|
+
return this.repo.reHideName(realDir, name, slotId, newContent, sealOne, sealOne);
|
|
1934
|
+
}
|
|
1935
|
+
async open(path, actor, self) {
|
|
1236
1936
|
const leaf = await this.repo.read(path);
|
|
1237
1937
|
if (!leaf)
|
|
1238
1938
|
return;
|
|
1239
1939
|
if (leaf.kind === "blob")
|
|
1240
1940
|
return leaf.content;
|
|
1241
1941
|
const box = JSON.parse(leaf.box);
|
|
1242
|
-
return openContent(this.ring, box, actor);
|
|
1942
|
+
return openContent(this.ring, box, actor, self);
|
|
1243
1943
|
}
|
|
1244
1944
|
}
|
|
1245
1945
|
// src/sync.ts
|
|
@@ -1542,15 +2242,32 @@ function diffTrees(store, aHead, bHead) {
|
|
|
1542
2242
|
const added = [];
|
|
1543
2243
|
const removed = [];
|
|
1544
2244
|
const modified = [];
|
|
2245
|
+
const sealedPaths = new Set;
|
|
2246
|
+
const sealedAt = (head, p) => entryKindAt(store, head, p) === "sealed";
|
|
1545
2247
|
for (const p of bPaths)
|
|
1546
|
-
if (!aPaths.has(p))
|
|
2248
|
+
if (!aPaths.has(p)) {
|
|
1547
2249
|
added.push(p);
|
|
2250
|
+
if (sealedAt(bHead, p))
|
|
2251
|
+
sealedPaths.add(p);
|
|
2252
|
+
}
|
|
1548
2253
|
for (const p of aPaths)
|
|
1549
|
-
if (!bPaths.has(p))
|
|
2254
|
+
if (!bPaths.has(p)) {
|
|
1550
2255
|
removed.push(p);
|
|
2256
|
+
if (sealedAt(aHead, p))
|
|
2257
|
+
sealedPaths.add(p);
|
|
2258
|
+
}
|
|
1551
2259
|
for (const p of aPaths) {
|
|
1552
2260
|
if (!bPaths.has(p))
|
|
1553
2261
|
continue;
|
|
2262
|
+
if (sealedAt(aHead, p) || sealedAt(bHead, p)) {
|
|
2263
|
+
const ah = blobHashAtAny(store, aHead, p);
|
|
2264
|
+
const bh = blobHashAtAny(store, bHead, p);
|
|
2265
|
+
if (ah !== bh) {
|
|
2266
|
+
modified.push({ path: p, hunks: "", sealed: true });
|
|
2267
|
+
sealedPaths.add(p);
|
|
2268
|
+
}
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
1554
2271
|
const a = fileAt(store, aHead, p);
|
|
1555
2272
|
const b = fileAt(store, bHead, p);
|
|
1556
2273
|
if (!a || !b)
|
|
@@ -1564,7 +2281,28 @@ function diffTrees(store, aHead, bHead) {
|
|
|
1564
2281
|
added.sort();
|
|
1565
2282
|
removed.sort();
|
|
1566
2283
|
modified.sort((x, y) => x.path < y.path ? -1 : x.path > y.path ? 1 : 0);
|
|
1567
|
-
|
|
2284
|
+
const out = { added, removed, modified };
|
|
2285
|
+
if (sealedPaths.size)
|
|
2286
|
+
out.sealedPaths = [...sealedPaths].sort();
|
|
2287
|
+
return out;
|
|
2288
|
+
}
|
|
2289
|
+
function blobHashAtAny(store, root, path) {
|
|
2290
|
+
const b = blobHashAt(store, root, path);
|
|
2291
|
+
if (b !== undefined)
|
|
2292
|
+
return b;
|
|
2293
|
+
const segs = path.split("/").filter(Boolean);
|
|
2294
|
+
let cur = root;
|
|
2295
|
+
for (let i = 0;i < segs.length; i++) {
|
|
2296
|
+
const entry = store.getTree(cur).entries[segs[i]];
|
|
2297
|
+
if (!entry)
|
|
2298
|
+
return;
|
|
2299
|
+
if (i === segs.length - 1)
|
|
2300
|
+
return entry.kind === "sealed" ? entry.hash : undefined;
|
|
2301
|
+
if (entry.kind !== "tree")
|
|
2302
|
+
return;
|
|
2303
|
+
cur = entry.hash;
|
|
2304
|
+
}
|
|
2305
|
+
return;
|
|
1568
2306
|
}
|
|
1569
2307
|
function diffFile(store, aHead, bHead, path) {
|
|
1570
2308
|
const a = readFile(store, aHead, path);
|
|
@@ -2133,14 +2871,24 @@ function toAttestation(runner, r) {
|
|
|
2133
2871
|
export {
|
|
2134
2872
|
writeFile,
|
|
2135
2873
|
wrapCekTo,
|
|
2874
|
+
verifyPubkeySig,
|
|
2136
2875
|
verifyOp,
|
|
2876
|
+
verifyAuthor,
|
|
2137
2877
|
verifyAttestation,
|
|
2878
|
+
verifyAtt,
|
|
2138
2879
|
unwrapCekWith,
|
|
2880
|
+
trustAuthor,
|
|
2139
2881
|
toAttestation,
|
|
2882
|
+
signerFrom,
|
|
2883
|
+
signTag,
|
|
2884
|
+
signPubkey,
|
|
2140
2885
|
signOp,
|
|
2886
|
+
signAuthor,
|
|
2141
2887
|
signAttestation,
|
|
2142
2888
|
semanticMerge,
|
|
2889
|
+
sealedBoxAt,
|
|
2143
2890
|
sealWithRecovery,
|
|
2891
|
+
sealToAccounts,
|
|
2144
2892
|
sealContent,
|
|
2145
2893
|
sameActor,
|
|
2146
2894
|
safePath,
|
|
@@ -2153,14 +2901,22 @@ export {
|
|
|
2153
2901
|
pullSubtree,
|
|
2154
2902
|
pull,
|
|
2155
2903
|
publishPaths,
|
|
2904
|
+
publicOf,
|
|
2905
|
+
pubFingerprint,
|
|
2906
|
+
provenanceFromEnv,
|
|
2907
|
+
provTrailers,
|
|
2908
|
+
provOf,
|
|
2909
|
+
provJson,
|
|
2156
2910
|
pageOps,
|
|
2157
2911
|
openWithRecovery,
|
|
2158
2912
|
openContent,
|
|
2159
2913
|
openChange,
|
|
2914
|
+
normalizeFingerprint,
|
|
2160
2915
|
modeAt,
|
|
2161
2916
|
merge3,
|
|
2162
2917
|
merge,
|
|
2163
2918
|
localTarget,
|
|
2919
|
+
loadPrivateKey,
|
|
2164
2920
|
listAll,
|
|
2165
2921
|
lineHunks,
|
|
2166
2922
|
isRecipient,
|
|
@@ -2169,8 +2925,11 @@ export {
|
|
|
2169
2925
|
hashNode,
|
|
2170
2926
|
groupIntoUnits,
|
|
2171
2927
|
getTree,
|
|
2928
|
+
generateRecoveryCode,
|
|
2929
|
+
generateKeypair,
|
|
2172
2930
|
garbage,
|
|
2173
2931
|
formatLog,
|
|
2932
|
+
fingerprintOf,
|
|
2174
2933
|
filterLog,
|
|
2175
2934
|
fileAt,
|
|
2176
2935
|
exportFiles,
|
|
@@ -2179,16 +2938,24 @@ export {
|
|
|
2179
2938
|
duplicateExportCheck,
|
|
2180
2939
|
diffTrees,
|
|
2181
2940
|
diffFile,
|
|
2941
|
+
deriveIdentity,
|
|
2182
2942
|
deleteFile,
|
|
2183
2943
|
converge,
|
|
2184
2944
|
compact,
|
|
2185
2945
|
clone,
|
|
2186
2946
|
ciphertextOf,
|
|
2947
|
+
captureProv,
|
|
2187
2948
|
canonicalOp,
|
|
2949
|
+
canonicalAuthor,
|
|
2188
2950
|
canonicalAttestation,
|
|
2951
|
+
canonicalAtt,
|
|
2952
|
+
buildProvNode,
|
|
2189
2953
|
blobHashAt,
|
|
2190
2954
|
blame,
|
|
2955
|
+
authorLabel,
|
|
2191
2956
|
attestSigned,
|
|
2957
|
+
attestOp,
|
|
2958
|
+
attTag,
|
|
2192
2959
|
View,
|
|
2193
2960
|
UNREADABLE,
|
|
2194
2961
|
Store,
|