@storacha/clawracha 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands.d.ts +3 -2
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +160 -25
- package/dist/handlers/process.d.ts +5 -1
- package/dist/handlers/process.d.ts.map +1 -1
- package/dist/handlers/process.js +68 -25
- package/dist/handlers/remote.d.ts +4 -0
- package/dist/handlers/remote.d.ts.map +1 -1
- package/dist/handlers/remote.js +36 -4
- package/dist/mdsync/index.d.ts +58 -15
- package/dist/mdsync/index.d.ts.map +1 -1
- package/dist/mdsync/index.js +87 -162
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +52 -106
- package/dist/sync.d.ts +3 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +28 -2
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/bundle.d.ts +19 -0
- package/dist/utils/bundle.d.ts.map +1 -0
- package/dist/utils/bundle.js +67 -0
- package/dist/utils/crypto.d.ts +32 -0
- package/dist/utils/crypto.d.ts.map +1 -0
- package/dist/utils/crypto.js +90 -0
- package/package.json +4 -2
package/dist/plugin.js
CHANGED
|
@@ -7,15 +7,11 @@
|
|
|
7
7
|
* - Agent tools for manual sync control
|
|
8
8
|
*/
|
|
9
9
|
import { json as consumeJson } from "stream/consumers";
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
10
11
|
import { SyncEngine } from "./sync.js";
|
|
11
|
-
import { createStorachaClient } from "./utils/client.js";
|
|
12
|
-
import { decodeDelegation, encodeDelegation, } from "./utils/delegation.js";
|
|
13
12
|
import { resolveAgentWorkspace, getAgentIds } from "./utils/workspace.js";
|
|
14
|
-
import { Agent, Name } from "@storacha/ucn/pail";
|
|
15
|
-
import { extract } from "@storacha/client/delegation";
|
|
16
13
|
import * as z from "zod";
|
|
17
|
-
import {
|
|
18
|
-
import { loadDeviceConfig, startWorkspaceSync, doInit, doSetup, doJoin, } from "./commands.js";
|
|
14
|
+
import { loadDeviceConfig, startWorkspaceSync, doInit, doSetup, doJoin, doGrant, } from "./commands.js";
|
|
19
15
|
const activeSyncers = new Map();
|
|
20
16
|
// --- Config helpers ---
|
|
21
17
|
const UpdateParams = z.object({
|
|
@@ -49,8 +45,6 @@ export default function plugin(api) {
|
|
|
49
45
|
const updateParams = paramsResult.data;
|
|
50
46
|
const sync = activeSyncers.get(updateParams.workspace);
|
|
51
47
|
if (sync) {
|
|
52
|
-
// stop active sync engine if present
|
|
53
|
-
// waiting for any active syncs to flush.
|
|
54
48
|
await sync.watcher.stop();
|
|
55
49
|
await sync.watcher.forceFlush();
|
|
56
50
|
await sync.engine.stop();
|
|
@@ -74,8 +68,7 @@ export default function plugin(api) {
|
|
|
74
68
|
for (const agentId of agentIds) {
|
|
75
69
|
const workspace = resolveAgentWorkspace(ctx.config, agentId);
|
|
76
70
|
try {
|
|
77
|
-
const sync = await startWorkspaceSync(workspace, agentId, pluginConfig, false,
|
|
78
|
-
ctx.logger);
|
|
71
|
+
const sync = await startWorkspaceSync(workspace, agentId, pluginConfig, false, ctx.logger);
|
|
79
72
|
if (sync) {
|
|
80
73
|
activeSyncers.set(workspace, sync);
|
|
81
74
|
}
|
|
@@ -101,7 +94,7 @@ export default function plugin(api) {
|
|
|
101
94
|
activeSyncers.clear();
|
|
102
95
|
},
|
|
103
96
|
});
|
|
104
|
-
// --- Agent tools
|
|
97
|
+
// --- Agent tools ---
|
|
105
98
|
api.registerTool({
|
|
106
99
|
name: "storacha_sync_status",
|
|
107
100
|
label: "Storacha Sync Status",
|
|
@@ -167,7 +160,6 @@ export default function plugin(api) {
|
|
|
167
160
|
const clawracha = program
|
|
168
161
|
.command("clawracha")
|
|
169
162
|
.description("Storacha workspace sync commands");
|
|
170
|
-
// Helper to resolve workspace from --agent
|
|
171
163
|
function requireAgent(agentId) {
|
|
172
164
|
if (!agentId) {
|
|
173
165
|
console.error("Error: --agent <id> is required. Specify which agent workspace to configure.");
|
|
@@ -195,16 +187,16 @@ export default function plugin(api) {
|
|
|
195
187
|
}
|
|
196
188
|
else {
|
|
197
189
|
console.log("\nNext step — choose one:");
|
|
198
|
-
console.log(` New workspace: openclaw clawracha setup
|
|
199
|
-
console.log(` Join existing: openclaw clawracha join <
|
|
190
|
+
console.log(` New workspace: openclaw clawracha setup --agent ${agentId}`);
|
|
191
|
+
console.log(` Join existing: openclaw clawracha join <bundle> --agent ${agentId}`);
|
|
200
192
|
}
|
|
201
193
|
return;
|
|
202
194
|
}
|
|
203
195
|
console.log(`🔥 Agent initialized for ${agentId}!`);
|
|
204
196
|
console.log(`Agent DID: ${result.agentDID}`);
|
|
205
197
|
console.log("\nNext step — choose one:");
|
|
206
|
-
console.log(` New workspace: openclaw clawracha setup
|
|
207
|
-
console.log(` Join existing: openclaw clawracha join <
|
|
198
|
+
console.log(` New workspace: openclaw clawracha setup --agent ${agentId}`);
|
|
199
|
+
console.log(` Join existing: openclaw clawracha join <bundle> --agent ${agentId}`);
|
|
208
200
|
}
|
|
209
201
|
catch (err) {
|
|
210
202
|
console.error(`Error: ${err.message}`);
|
|
@@ -213,12 +205,12 @@ export default function plugin(api) {
|
|
|
213
205
|
process.exit(1);
|
|
214
206
|
}
|
|
215
207
|
});
|
|
216
|
-
// --- setup ---
|
|
208
|
+
// --- setup (login-based only) ---
|
|
217
209
|
clawracha
|
|
218
|
-
.command("setup
|
|
219
|
-
.description("Set up a NEW workspace
|
|
210
|
+
.command("setup")
|
|
211
|
+
.description("Set up a NEW workspace via Storacha login (creates space, generates delegations)")
|
|
220
212
|
.requiredOption("--agent <id>", "Agent ID")
|
|
221
|
-
.action(async (
|
|
213
|
+
.action(async (opts) => {
|
|
222
214
|
try {
|
|
223
215
|
const { agentId, workspace } = requireAgent(opts.agent);
|
|
224
216
|
const deviceConfig = await loadDeviceConfig(workspace);
|
|
@@ -226,12 +218,16 @@ export default function plugin(api) {
|
|
|
226
218
|
console.error(`Run \`openclaw clawracha init --agent ${agentId}\` first.`);
|
|
227
219
|
process.exit(1);
|
|
228
220
|
}
|
|
229
|
-
const
|
|
230
|
-
|
|
221
|
+
const { prompt } = await import("./prompts.js");
|
|
222
|
+
const email = await prompt("Storacha email: ");
|
|
223
|
+
const spaceName = await prompt("Space name: ");
|
|
224
|
+
console.log("\n⏳ Setting up workspace...");
|
|
225
|
+
const result = await doSetup(workspace, agentId, email, spaceName, pluginConfig, config.gateway);
|
|
226
|
+
console.log(`\n🔥 Storacha workspace ready for ${agentId}!`);
|
|
231
227
|
console.log(`Agent DID: ${result.agentDID}`);
|
|
232
228
|
console.log(`Space: ${result.spaceDID ?? "unknown"}`);
|
|
233
229
|
console.log(`\nTo add another device, run \`openclaw clawracha grant <their-DID> --agent ${agentId}\` here,`);
|
|
234
|
-
console.log(`then \`openclaw clawracha join <
|
|
230
|
+
console.log(`then \`openclaw clawracha join <bundle> --agent <id>\` on the other device.`);
|
|
235
231
|
console.log("\nSync is now active (no gateway restart needed).");
|
|
236
232
|
}
|
|
237
233
|
catch (err) {
|
|
@@ -243,12 +239,12 @@ export default function plugin(api) {
|
|
|
243
239
|
process.exit(1);
|
|
244
240
|
}
|
|
245
241
|
});
|
|
246
|
-
// --- join ---
|
|
242
|
+
// --- join (single bundle arg) ---
|
|
247
243
|
clawracha
|
|
248
|
-
.command("join <
|
|
249
|
-
.description("Join an existing workspace from another device.
|
|
244
|
+
.command("join <bundle>")
|
|
245
|
+
.description("Join an existing workspace from another device. Bundle is a file path or base64 string from `grant`.")
|
|
250
246
|
.requiredOption("--agent <id>", "Agent ID")
|
|
251
|
-
.action(async (
|
|
247
|
+
.action(async (bundleArg, opts) => {
|
|
252
248
|
try {
|
|
253
249
|
const { agentId, workspace } = requireAgent(opts.agent);
|
|
254
250
|
const deviceConfig = await loadDeviceConfig(workspace);
|
|
@@ -256,8 +252,9 @@ export default function plugin(api) {
|
|
|
256
252
|
console.error(`Run \`openclaw clawracha init --agent ${agentId}\` first.`);
|
|
257
253
|
process.exit(1);
|
|
258
254
|
}
|
|
259
|
-
|
|
260
|
-
|
|
255
|
+
console.log("⏳ Joining workspace...");
|
|
256
|
+
const result = await doJoin(workspace, agentId, bundleArg, pluginConfig, config.gateway);
|
|
257
|
+
console.log(`\n🔥 Joined Storacha workspace for ${agentId}!`);
|
|
261
258
|
console.log(`Agent DID: ${result.agentDID}`);
|
|
262
259
|
console.log(`Space: ${result.spaceDID ?? "unknown"}`);
|
|
263
260
|
console.log(`Pulled ${result.pullCount} files from remote.`);
|
|
@@ -272,11 +269,12 @@ export default function plugin(api) {
|
|
|
272
269
|
process.exit(1);
|
|
273
270
|
}
|
|
274
271
|
});
|
|
275
|
-
// --- grant ---
|
|
272
|
+
// --- grant (produces bundle) ---
|
|
276
273
|
clawracha
|
|
277
274
|
.command("grant <target-DID>")
|
|
278
|
-
.description("Grant another device access to this workspace")
|
|
275
|
+
.description("Grant another device access to this workspace (outputs a delegation bundle)")
|
|
279
276
|
.requiredOption("--agent <id>", "Agent ID")
|
|
277
|
+
.option("-o, --output <file>", "Write bundle to file instead of base64 stdout")
|
|
280
278
|
.action(async (targetDID, opts) => {
|
|
281
279
|
try {
|
|
282
280
|
const { agentId, workspace } = requireAgent(opts.agent);
|
|
@@ -284,60 +282,19 @@ export default function plugin(api) {
|
|
|
284
282
|
console.error("Error: target must be a DID (did:key:z...)");
|
|
285
283
|
process.exit(1);
|
|
286
284
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const results = [];
|
|
293
|
-
// Re-delegate upload capability
|
|
294
|
-
if (deviceConfig.uploadDelegation) {
|
|
295
|
-
const storachaClient = await createStorachaClient(deviceConfig);
|
|
296
|
-
const audience = {
|
|
297
|
-
did: () => targetDID,
|
|
298
|
-
};
|
|
299
|
-
const uploadDel = await storachaClient.createDelegation(audience,
|
|
300
|
-
// @ts-expect-error createDelegation should validate abilities
|
|
301
|
-
Object.keys(spaceAccess), { expiration: Infinity });
|
|
302
|
-
const { ok: archiveBytes } = await uploadDel.archive();
|
|
303
|
-
if (archiveBytes) {
|
|
304
|
-
results.push(`Upload delegation:\n${encodeDelegation(archiveBytes)}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
results.push("⚠️ No upload delegation to re-delegate.");
|
|
309
|
-
}
|
|
310
|
-
// Re-delegate name capability
|
|
311
|
-
const agent = Agent.parse(deviceConfig.agentKey);
|
|
312
|
-
let name;
|
|
313
|
-
if (deviceConfig.nameArchive) {
|
|
314
|
-
const archiveBytes = decodeDelegation(deviceConfig.nameArchive);
|
|
315
|
-
name = await Name.extract(agent, archiveBytes);
|
|
316
|
-
}
|
|
317
|
-
else if (deviceConfig.nameDelegation) {
|
|
318
|
-
const nameBytes = decodeDelegation(deviceConfig.nameDelegation);
|
|
319
|
-
const { ok: nameDel } = await extract(nameBytes);
|
|
320
|
-
if (nameDel) {
|
|
321
|
-
name = Name.from(agent, [nameDel]);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (name) {
|
|
325
|
-
const nameDel = await name.grant(targetDID);
|
|
326
|
-
const { ok: archiveBytes } = await nameDel.archive();
|
|
327
|
-
if (archiveBytes) {
|
|
328
|
-
results.push(`Name delegation:\n${encodeDelegation(archiveBytes)}`);
|
|
329
|
-
}
|
|
285
|
+
console.log("⏳ Creating delegation bundle...");
|
|
286
|
+
const bundleBytes = await doGrant(workspace, targetDID);
|
|
287
|
+
if (opts.output) {
|
|
288
|
+
await fs.writeFile(opts.output, bundleBytes);
|
|
289
|
+
console.log(`\n🔥 Delegation bundle written to ${opts.output}`);
|
|
330
290
|
}
|
|
331
291
|
else {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
for (const r of results) {
|
|
336
|
-
console.log(r);
|
|
337
|
-
console.log();
|
|
292
|
+
const base64 = Buffer.from(bundleBytes).toString("base64");
|
|
293
|
+
console.log(`\n🔥 Delegation bundle for ${targetDID}:\n`);
|
|
294
|
+
console.log(base64);
|
|
338
295
|
}
|
|
339
|
-
console.log("
|
|
340
|
-
console.log(` openclaw clawracha join <
|
|
296
|
+
console.log("\nThe target device should run:");
|
|
297
|
+
console.log(` openclaw clawracha join <bundle> --agent <id>`);
|
|
341
298
|
}
|
|
342
299
|
catch (err) {
|
|
343
300
|
console.error(`Error: ${err.message}`);
|
|
@@ -363,7 +320,9 @@ export default function plugin(api) {
|
|
|
363
320
|
console.log(`Workspace: ${workspace}`);
|
|
364
321
|
console.log(`Upload delegation: ${deviceConfig.uploadDelegation ? "✅" : "❌ not set"}`);
|
|
365
322
|
console.log(`Name delegation: ${deviceConfig.nameDelegation ? "✅" : "❌ not set"}`);
|
|
323
|
+
console.log(`Plan delegation: ${deviceConfig.planDelegation ? "✅" : "❌ not set"}`);
|
|
366
324
|
console.log(`Space DID: ${deviceConfig.spaceDID ?? "unknown"}`);
|
|
325
|
+
console.log(`Access: ${deviceConfig.access?.type ?? "unknown"}`);
|
|
367
326
|
console.log(`Name Archive: ${deviceConfig.nameArchive ? "saved" : "not created"}`);
|
|
368
327
|
console.log(`Setup complete: ${deviceConfig.setupComplete ? "✅" : "❌"}`);
|
|
369
328
|
const sync = activeSyncers.get(workspace);
|
|
@@ -398,7 +357,6 @@ export default function plugin(api) {
|
|
|
398
357
|
console.log(`Not set up. Run \`openclaw clawracha init --agent ${agentId}\` first.`);
|
|
399
358
|
return;
|
|
400
359
|
}
|
|
401
|
-
// Use active syncer if available, otherwise spin up temporary engine
|
|
402
360
|
let engine;
|
|
403
361
|
const activeSync = activeSyncers.get(workspace);
|
|
404
362
|
if (activeSync) {
|
|
@@ -439,7 +397,7 @@ export default function plugin(api) {
|
|
|
439
397
|
.description("Interactive guided setup for Storacha workspace sync")
|
|
440
398
|
.requiredOption("--agent <id>", "Agent ID")
|
|
441
399
|
.action(async (opts) => {
|
|
442
|
-
const { promptMultiline, choose } = await import("./prompts.js");
|
|
400
|
+
const { prompt, promptMultiline, choose } = await import("./prompts.js");
|
|
443
401
|
try {
|
|
444
402
|
const { agentId, workspace } = requireAgent(opts.agent);
|
|
445
403
|
console.log("🔥 Welcome to Clawracha — Storacha workspace sync!\n");
|
|
@@ -460,18 +418,12 @@ export default function plugin(api) {
|
|
|
460
418
|
// Step 2: Path choice
|
|
461
419
|
const choice = await choose("Are you setting up a NEW workspace or JOINING an existing one?", ["NEW workspace", "JOIN existing"]);
|
|
462
420
|
if (choice === "NEW workspace") {
|
|
463
|
-
// --- Setup path ---
|
|
421
|
+
// --- Setup path (login only) ---
|
|
464
422
|
console.log("\n📦 New Workspace Setup\n");
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
console.log(` storacha delegation create ${initResult.agentDID} --base64\n`);
|
|
468
|
-
const delegationInput = await promptMultiline("Paste your upload delegation here:");
|
|
469
|
-
if (!delegationInput) {
|
|
470
|
-
console.error("No delegation provided. Aborting.");
|
|
471
|
-
process.exit(1);
|
|
472
|
-
}
|
|
423
|
+
const email = await prompt("Storacha email: ");
|
|
424
|
+
const spaceName = await prompt("Space name: ");
|
|
473
425
|
console.log("\n⏳ Setting up workspace...");
|
|
474
|
-
const result = await doSetup(workspace, agentId,
|
|
426
|
+
const result = await doSetup(workspace, agentId, email, spaceName, pluginConfig, config.gateway);
|
|
475
427
|
console.log(`\n🔥 Storacha workspace ready for ${agentId}!`);
|
|
476
428
|
console.log(` Agent DID: ${result.agentDID}`);
|
|
477
429
|
console.log(` Space: ${result.spaceDID ?? "unknown"}`);
|
|
@@ -480,24 +432,18 @@ export default function plugin(api) {
|
|
|
480
432
|
console.log("\nSync is now active! 🎉");
|
|
481
433
|
}
|
|
482
434
|
else {
|
|
483
|
-
// --- Join path ---
|
|
435
|
+
// --- Join path (single bundle) ---
|
|
484
436
|
console.log("\n🤝 Join Existing Workspace\n");
|
|
485
|
-
console.log("You need
|
|
437
|
+
console.log("You need a delegation bundle from someone with access.");
|
|
486
438
|
console.log("Ask them to run:\n");
|
|
487
439
|
console.log(` openclaw clawracha grant ${initResult.agentDID} --agent <their-agent-id>\n`);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
console.error("No upload delegation provided. Aborting.");
|
|
492
|
-
process.exit(1);
|
|
493
|
-
}
|
|
494
|
-
const nameInput = await promptMultiline("Paste the name delegation here:");
|
|
495
|
-
if (!nameInput) {
|
|
496
|
-
console.error("No name delegation provided. Aborting.");
|
|
440
|
+
const bundleInput = await promptMultiline("Paste the delegation bundle here:");
|
|
441
|
+
if (!bundleInput) {
|
|
442
|
+
console.error("No bundle provided. Aborting.");
|
|
497
443
|
process.exit(1);
|
|
498
444
|
}
|
|
499
445
|
console.log("\n⏳ Joining workspace...");
|
|
500
|
-
const result = await doJoin(workspace, agentId,
|
|
446
|
+
const result = await doJoin(workspace, agentId, bundleInput, pluginConfig, config.gateway);
|
|
501
447
|
console.log(`\n🔥 Joined Storacha workspace for ${agentId}!`);
|
|
502
448
|
console.log(` Agent DID: ${result.agentDID}`);
|
|
503
449
|
console.log(` Space: ${result.spaceDID ?? "unknown"}`);
|
package/dist/sync.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export declare class SyncEngine {
|
|
|
19
19
|
private carFile;
|
|
20
20
|
private lastSync;
|
|
21
21
|
private syncLock;
|
|
22
|
+
private encryptionConfig?;
|
|
23
|
+
private decryptionConfig?;
|
|
24
|
+
private encryptedClient?;
|
|
22
25
|
constructor(workspace: string);
|
|
23
26
|
/**
|
|
24
27
|
* Initialize sync engine with device config.
|
package/dist/sync.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EAEV,YAAY,EACb,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EAEV,YAAY,EACb,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAmBxE,qBAAa,UAAU;IACrB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,KAAK,CAAiD;IAC9D,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,QAAQ,CAAoC;IACpD,OAAO,CAAC,gBAAgB,CAAC,CAAmB;IAC5C,OAAO,CAAC,gBAAgB,CAAC,CAAmB;IAC5C,OAAO,CAAC,eAAe,CAAC,CAAkB;gBAE9B,SAAS,EAAE,MAAM;IAK7B;;;OAGG;IACG,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA6D/C;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB1D;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAMb,UAAU;IAwDxB;;OAEG;YACW,iBAAiB;IAe/B;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;IAc5C;;OAEG;YACW,kBAAkB;IAYhC;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAc3B;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;YAMrB,gBAAgB;IA4B9B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC;QACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,SAAS,EAAE;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC/B,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,UAAU,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC5D,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC;IAkBI,MAAM,IAAI,OAAO,CAAC,SAAS,CAAC;IAW5B,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;YAM5B,WAAW;CAK1B"}
|
package/dist/sync.js
CHANGED
|
@@ -18,6 +18,8 @@ import { diffRemoteChanges } from "./utils/differ.js";
|
|
|
18
18
|
import { makeTempCar } from "./utils/tempcar.js";
|
|
19
19
|
import { createStorachaClient } from "./utils/client.js";
|
|
20
20
|
import { decodeDelegation, encodeDelegation } from "./utils/delegation.js";
|
|
21
|
+
import { extract } from "@storacha/client/delegation";
|
|
22
|
+
import { makeEncryptionConfig, makeDecryptionConfig, getEncryptedClient } from "./utils/crypto.js";
|
|
21
23
|
export class SyncEngine {
|
|
22
24
|
workspace;
|
|
23
25
|
blocks;
|
|
@@ -27,6 +29,9 @@ export class SyncEngine {
|
|
|
27
29
|
carFile = null;
|
|
28
30
|
lastSync = null;
|
|
29
31
|
syncLock = Promise.resolve();
|
|
32
|
+
encryptionConfig;
|
|
33
|
+
decryptionConfig;
|
|
34
|
+
encryptedClient;
|
|
30
35
|
constructor(workspace) {
|
|
31
36
|
this.workspace = workspace;
|
|
32
37
|
this.blocks = createWorkspaceBlockstore(workspace);
|
|
@@ -46,7 +51,6 @@ export class SyncEngine {
|
|
|
46
51
|
}
|
|
47
52
|
else if (config.nameDelegation) {
|
|
48
53
|
// Reconstruct from delegation (granted by another device)
|
|
49
|
-
const { extract } = await import("@storacha/client/delegation");
|
|
50
54
|
const nameBytes = decodeDelegation(config.nameDelegation);
|
|
51
55
|
const { ok: delegation } = await extract(nameBytes);
|
|
52
56
|
if (!delegation)
|
|
@@ -58,6 +62,24 @@ export class SyncEngine {
|
|
|
58
62
|
name = await Name.create(agent);
|
|
59
63
|
}
|
|
60
64
|
this.state = { initialized: true, running: true, name, storachaClient };
|
|
65
|
+
// Set up encryption for private spaces
|
|
66
|
+
if (config.access?.type === "private") {
|
|
67
|
+
if (!config.planDelegation) {
|
|
68
|
+
throw new Error("Private space requires a plan delegation for KMS access");
|
|
69
|
+
}
|
|
70
|
+
const planBytes = decodeDelegation(config.planDelegation);
|
|
71
|
+
const { ok: planDel } = await extract(planBytes);
|
|
72
|
+
if (!planDel)
|
|
73
|
+
throw new Error("Failed to extract plan delegation");
|
|
74
|
+
this.encryptionConfig = makeEncryptionConfig(agent, config.spaceDID, [planDel]);
|
|
75
|
+
// For decrypt, uploadDelegation covers space/content/decrypt
|
|
76
|
+
const uploadBytes = decodeDelegation(config.uploadDelegation);
|
|
77
|
+
const { ok: uploadDel } = await extract(uploadBytes);
|
|
78
|
+
if (!uploadDel)
|
|
79
|
+
throw new Error("Failed to extract upload delegation");
|
|
80
|
+
this.decryptionConfig = makeDecryptionConfig(config.spaceDID, uploadDel);
|
|
81
|
+
this.encryptedClient = await getEncryptedClient(storachaClient);
|
|
82
|
+
}
|
|
61
83
|
try {
|
|
62
84
|
const result = await Revision.resolve(this.blocks, name);
|
|
63
85
|
this.current = result.value;
|
|
@@ -98,7 +120,7 @@ export class SyncEngine {
|
|
|
98
120
|
if (!this.carFile) {
|
|
99
121
|
this.carFile = await makeTempCar();
|
|
100
122
|
}
|
|
101
|
-
const pendingOps = await processChanges(changes, this.workspace, this.current, this.blocks, (block) => this.carFile.put(block), (block) => this.blocks.put(block));
|
|
123
|
+
const pendingOps = await processChanges(changes, this.workspace, this.current, this.blocks, (block) => this.carFile.put(block), (block) => this.blocks.put(block), this.encryptionConfig);
|
|
102
124
|
this.pendingOps.push(...pendingOps);
|
|
103
125
|
}
|
|
104
126
|
/**
|
|
@@ -203,6 +225,8 @@ export class SyncEngine {
|
|
|
203
225
|
await applyRemoteChanges(changedPaths, entries, this.workspace, {
|
|
204
226
|
blocks: this.blocks,
|
|
205
227
|
current: this.current ?? undefined,
|
|
228
|
+
encryptedClient: this.encryptedClient,
|
|
229
|
+
decryptionConfig: this.decryptionConfig,
|
|
206
230
|
});
|
|
207
231
|
}
|
|
208
232
|
/**
|
|
@@ -249,6 +273,8 @@ export class SyncEngine {
|
|
|
249
273
|
await applyRemoteChanges(allPaths, entries, this.workspace, {
|
|
250
274
|
blocks: this.blocks,
|
|
251
275
|
current: this.current ?? undefined,
|
|
276
|
+
encryptedClient: this.encryptedClient,
|
|
277
|
+
decryptionConfig: this.decryptionConfig,
|
|
252
278
|
});
|
|
253
279
|
}
|
|
254
280
|
this.lastSync = Date.now();
|
package/dist/types/index.d.ts
CHANGED
|
@@ -10,6 +10,17 @@ export interface SyncPluginConfig {
|
|
|
10
10
|
watchPatterns: string[];
|
|
11
11
|
ignorePatterns: string[];
|
|
12
12
|
}
|
|
13
|
+
export interface PublicAccess {
|
|
14
|
+
type: "public";
|
|
15
|
+
}
|
|
16
|
+
export interface PrivateAccess {
|
|
17
|
+
type: "private";
|
|
18
|
+
encryption: {
|
|
19
|
+
provider: string;
|
|
20
|
+
algorithm: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export type SpaceAccess = PublicAccess | PrivateAccess;
|
|
13
24
|
/** Stored in .storacha/config.json — device-specific, not synced */
|
|
14
25
|
export interface DeviceConfig {
|
|
15
26
|
/** Ed25519 agent private key (base64) */
|
|
@@ -20,8 +31,12 @@ export interface DeviceConfig {
|
|
|
20
31
|
uploadDelegation?: string;
|
|
21
32
|
/** Name → agent delegation for pail sync (base64 archive) */
|
|
22
33
|
nameDelegation?: string;
|
|
34
|
+
/** Plan/get delegation for KMS access in private spaces (base64 archive) */
|
|
35
|
+
planDelegation?: string;
|
|
23
36
|
/** Space DID extracted from upload delegation */
|
|
24
37
|
spaceDID?: string;
|
|
38
|
+
/** Space access type — determines if content is encrypted */
|
|
39
|
+
access?: SpaceAccess;
|
|
25
40
|
/** Whether setup is complete (watcher won't start without this) */
|
|
26
41
|
setupComplete?: boolean;
|
|
27
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAE5C,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,yBAAyB;AACzB,MAAM,WAAW,SAAS;IACxB,iCAAiC;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,4BAA4B;IAC5B,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qCAAqC;AACrC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0BAA0B;AAC1B,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,8BAA8B;AAC9B,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,GAAG,CAAC;CACb"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAE5C,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,UAAU,EAAE;QACV,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,aAAa,CAAC;AAEvD,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,mEAAmE;IACnE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,yBAAyB;AACzB,MAAM,WAAW,SAAS;IACxB,iCAAiC;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,4BAA4B;IAC5B,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qCAAqC;AACrC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0BAA0B;AAC1B,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,8BAA8B;AAC9B,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,GAAG,CAAC;CACb"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation bundle — packs upload, name, and plan delegations
|
|
3
|
+
* into a single CAR file for the `join` workflow.
|
|
4
|
+
*/
|
|
5
|
+
export interface DelegationBundle {
|
|
6
|
+
upload: Uint8Array;
|
|
7
|
+
name: Uint8Array;
|
|
8
|
+
plan: Uint8Array;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Pack three delegation archives into a single CAR file.
|
|
12
|
+
* Root block is DAG-CBOR: { upload, name, plan } as byte arrays.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createDelegationBundle(bundle: DelegationBundle): Promise<Uint8Array>;
|
|
15
|
+
/**
|
|
16
|
+
* Extract three delegation archives from a bundle CAR file.
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractDelegationBundle(carBytes: Uint8Array): Promise<DelegationBundle>;
|
|
19
|
+
//# sourceMappingURL=bundle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/utils/bundle.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,UAAU,CAAC,CA2BrB;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,UAAU,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAwB3B"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation bundle — packs upload, name, and plan delegations
|
|
3
|
+
* into a single CAR file for the `join` workflow.
|
|
4
|
+
*/
|
|
5
|
+
import * as cbor from "@ipld/dag-cbor";
|
|
6
|
+
import { encode, decode } from "multiformats/block";
|
|
7
|
+
import { sha256 } from "multiformats/hashes/sha2";
|
|
8
|
+
import { CarWriter } from "@ipld/car/writer";
|
|
9
|
+
import { CarReader } from "@ipld/car/reader";
|
|
10
|
+
/**
|
|
11
|
+
* Pack three delegation archives into a single CAR file.
|
|
12
|
+
* Root block is DAG-CBOR: { upload, name, plan } as byte arrays.
|
|
13
|
+
*/
|
|
14
|
+
export async function createDelegationBundle(bundle) {
|
|
15
|
+
const rootBlock = await encode({
|
|
16
|
+
value: bundle,
|
|
17
|
+
codec: cbor,
|
|
18
|
+
hasher: sha256,
|
|
19
|
+
});
|
|
20
|
+
const { writer, out } = CarWriter.create([rootBlock.cid]);
|
|
21
|
+
const chunks = [];
|
|
22
|
+
const drain = (async () => {
|
|
23
|
+
for await (const chunk of out) {
|
|
24
|
+
chunks.push(chunk);
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
await writer.put(rootBlock);
|
|
28
|
+
await writer.close();
|
|
29
|
+
await drain;
|
|
30
|
+
// Concatenate chunks
|
|
31
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
32
|
+
const result = new Uint8Array(totalLength);
|
|
33
|
+
let offset = 0;
|
|
34
|
+
for (const chunk of chunks) {
|
|
35
|
+
result.set(chunk, offset);
|
|
36
|
+
offset += chunk.length;
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Extract three delegation archives from a bundle CAR file.
|
|
42
|
+
*/
|
|
43
|
+
export async function extractDelegationBundle(carBytes) {
|
|
44
|
+
const reader = await CarReader.fromBytes(carBytes);
|
|
45
|
+
const roots = await reader.getRoots();
|
|
46
|
+
if (roots.length !== 1) {
|
|
47
|
+
throw new Error(`Expected 1 root in delegation bundle, got ${roots.length}`);
|
|
48
|
+
}
|
|
49
|
+
const rootBlock = await reader.get(roots[0]);
|
|
50
|
+
if (!rootBlock) {
|
|
51
|
+
throw new Error("Could not read root block from delegation bundle");
|
|
52
|
+
}
|
|
53
|
+
const decoded = await decode({
|
|
54
|
+
bytes: rootBlock.bytes,
|
|
55
|
+
codec: cbor,
|
|
56
|
+
hasher: sha256,
|
|
57
|
+
});
|
|
58
|
+
const value = decoded.value;
|
|
59
|
+
if (!value.upload || !value.name || !value.plan) {
|
|
60
|
+
throw new Error("Delegation bundle missing required keys (upload, name, plan)");
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
upload: value.upload,
|
|
64
|
+
name: value.name,
|
|
65
|
+
plan: value.plan,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption helpers for private spaces.
|
|
3
|
+
* Uses @storacha/encrypt-upload-client with KMS-based key management.
|
|
4
|
+
*/
|
|
5
|
+
import type { Block } from "multiformats";
|
|
6
|
+
import type { CID } from "multiformats/cid";
|
|
7
|
+
import type { Client } from "@storacha/client";
|
|
8
|
+
import type { Proof } from "@ucanto/interface";
|
|
9
|
+
type SpaceDID = `did:key:${string}`;
|
|
10
|
+
import type { CryptoAdapter, EncryptionConfig, DecryptionConfig, BlobLike, EncryptedClient } from "@storacha/encrypt-upload-client/types";
|
|
11
|
+
export declare function getKMSCryptoAdapter(): CryptoAdapter;
|
|
12
|
+
export declare function getEncryptedClient(storachaClient: Client): Promise<EncryptedClient>;
|
|
13
|
+
export declare function makeEncryptionConfig(issuer: {
|
|
14
|
+
did: () => `did:key:${string}`;
|
|
15
|
+
}, spaceDID: SpaceDID, proofs: Proof[]): EncryptionConfig;
|
|
16
|
+
export declare function makeDecryptionConfig(spaceDID: SpaceDID, decryptDelegation: Proof, proofs?: Proof[]): DecryptionConfig;
|
|
17
|
+
/**
|
|
18
|
+
* Encrypt a BlobLike and return a block stream
|
|
19
|
+
* (UnixFS-encoded encrypted content + metadata block appended).
|
|
20
|
+
*/
|
|
21
|
+
export declare function encryptToBlockStream(file: BlobLike, encryptionConfig: EncryptionConfig): Promise<ReadableStream<Block>>;
|
|
22
|
+
/**
|
|
23
|
+
* Wrap raw bytes as a BlobLike for encryption.
|
|
24
|
+
*/
|
|
25
|
+
export declare function bytesToBlobLike(bytes: Uint8Array): BlobLike;
|
|
26
|
+
/**
|
|
27
|
+
* Create a decrypt function for mdsync resolveValue.
|
|
28
|
+
* Fetches encrypted content by CID via EncryptedClient and returns decrypted bytes.
|
|
29
|
+
*/
|
|
30
|
+
export declare function makeDecryptFn(encryptedClient: EncryptedClient, decryptionConfig: DecryptionConfig): (cid: CID) => Promise<Uint8Array>;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/utils/crypto.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,KAAK,QAAQ,GAAG,WAAW,MAAM,EAAE,CAAC;AACpC,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,QAAQ,EACR,eAAe,EAChB,MAAM,uCAAuC,CAAC;AAU/C,wBAAgB,mBAAmB,IAAI,aAAa,CAInD;AAED,wBAAsB,kBAAkB,CACtC,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,eAAe,CAAC,CAM1B;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,WAAW,MAAM,EAAE,CAAA;CAAE,EAC1C,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,KAAK,EAAE,GACd,gBAAgB,CAMlB;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,iBAAiB,EAAE,KAAK,EACxB,MAAM,CAAC,EAAE,KAAK,EAAE,GACf,gBAAgB,CAMlB;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,QAAQ,EACd,gBAAgB,EAAE,gBAAgB,GACjC,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAIhC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,QAAQ,CAU3D;AAwBD;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,eAAe,EAAE,eAAe,EAChC,gBAAgB,EAAE,gBAAgB,GACjC,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,UAAU,CAAC,CAQnC"}
|