@storacha/clawracha 0.2.3 → 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/commands.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface JoinResult {
|
|
|
37
37
|
pullCount: number;
|
|
38
38
|
}
|
|
39
39
|
export declare function doInit(workspace: string): Promise<InitResult>;
|
|
40
|
-
export declare function doSetup(workspace: string, agentId: string,
|
|
41
|
-
export declare function doJoin(workspace: string, agentId: string,
|
|
40
|
+
export declare function doSetup(workspace: string, agentId: string, email: string, spaceName: string, pluginConfig: Partial<SyncPluginConfig>, gatewayConfig?: NonNullable<OpenClawConfig["gateway"]>): Promise<SetupResult>;
|
|
41
|
+
export declare function doJoin(workspace: string, agentId: string, bundleArg: string, pluginConfig: Partial<SyncPluginConfig>, gatewayConfig?: NonNullable<OpenClawConfig["gateway"]>): Promise<JoinResult>;
|
|
42
|
+
export declare function doGrant(workspace: string, targetDID: `did:${string}:${string}`): Promise<Uint8Array>;
|
|
42
43
|
//# sourceMappingURL=commands.d.ts.map
|
package/dist/commands.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAiB3C,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAID,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAS9B;AAED,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,GACpD,OAAO,CAAC,IAAI,CAAC,CAqCf;AAID,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,OAAO,CAAC,gBAAgB,CAAC,EACvC,UAAU,EAAE,OAAO,EACnB,MAAM,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACnE,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAmC/B;AAID,MAAM,WAAW,UAAU;IACzB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAID,wBAAsB,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAsBnE;AAED,wBAAsB,OAAO,CAC3B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,OAAO,CAAC,gBAAgB,CAAC,EACvC,aAAa,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,GACrD,OAAO,CAAC,WAAW,CAAC,CAoGtB;AAoBD,wBAAsB,MAAM,CAC1B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,OAAO,CAAC,gBAAgB,CAAC,EACvC,aAAa,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,GACrD,OAAO,CAAC,UAAU,CAAC,CA+DrB;AAED,wBAAsB,OAAO,CAC3B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,OAAO,MAAM,IAAI,MAAM,EAAE,GACnC,OAAO,CAAC,UAAU,CAAC,CAyDrB"}
|
package/dist/commands.js
CHANGED
|
@@ -8,8 +8,12 @@ import * as fs from "node:fs/promises";
|
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { SyncEngine } from "./sync.js";
|
|
10
10
|
import { FileWatcher } from "./watcher.js";
|
|
11
|
-
import { encodeDelegation,
|
|
12
|
-
import {
|
|
11
|
+
import { encodeDelegation, decodeDelegation, } from "./utils/delegation.js";
|
|
12
|
+
import { createStorachaClient } from "./utils/client.js";
|
|
13
|
+
import { createDelegationBundle, extractDelegationBundle, } from "./utils/bundle.js";
|
|
14
|
+
import { Agent, Name } from "@storacha/ucn/pail";
|
|
15
|
+
import { extract } from "@storacha/client/delegation";
|
|
16
|
+
import { delegate } from "@ucanto/core";
|
|
13
17
|
// --- Config helpers ---
|
|
14
18
|
export async function loadDeviceConfig(workspace) {
|
|
15
19
|
const configPath = path.join(workspace, ".storacha", "config.json");
|
|
@@ -114,7 +118,7 @@ export async function doInit(workspace) {
|
|
|
114
118
|
agentKey,
|
|
115
119
|
};
|
|
116
120
|
}
|
|
117
|
-
export async function doSetup(workspace, agentId,
|
|
121
|
+
export async function doSetup(workspace, agentId, email, spaceName, pluginConfig, gatewayConfig) {
|
|
118
122
|
const deviceConfig = await loadDeviceConfig(workspace);
|
|
119
123
|
if (!deviceConfig?.agentKey) {
|
|
120
124
|
throw new Error("Not initialized. Run init first.");
|
|
@@ -123,13 +127,59 @@ export async function doSetup(workspace, agentId, delegationArg, pluginConfig, g
|
|
|
123
127
|
const agent = Agent.parse(deviceConfig.agentKey);
|
|
124
128
|
return { agentDID: agent.did(), spaceDID: deviceConfig.spaceDID };
|
|
125
129
|
}
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
const { create: createClient } = await import("@storacha/client");
|
|
131
|
+
const { spaceAccess } = await import("@storacha/client/capability/access");
|
|
132
|
+
const tempClient = await createClient();
|
|
133
|
+
console.log(`\nA confirmation email will be sent to ${email}.`);
|
|
134
|
+
console.log("Please click the link in the email to continue...");
|
|
135
|
+
const account = await tempClient.login(email);
|
|
136
|
+
console.log("✅ Email confirmed!");
|
|
137
|
+
console.log("Checking payment plan...");
|
|
138
|
+
await account.plan.wait();
|
|
139
|
+
console.log("✅ Payment plan active!");
|
|
140
|
+
const { choose } = await import("./prompts.js");
|
|
141
|
+
const accessChoice = await choose("\nSpace access type:\n" +
|
|
142
|
+
" ⚠️ Public — workspace data is accessible by anyone with the CID\n" +
|
|
143
|
+
" 🔒 Private — data is encrypted (requires paid plan)", ["Public", "Private (encrypted)"]);
|
|
144
|
+
const access = accessChoice === "Private (encrypted)"
|
|
145
|
+
? {
|
|
146
|
+
type: "private",
|
|
147
|
+
encryption: {
|
|
148
|
+
provider: "google-kms",
|
|
149
|
+
algorithm: "RSA_DECRYPT_OAEP_3072_SHA256",
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
: { type: "public" };
|
|
153
|
+
console.log(`Creating space "${spaceName}"...`);
|
|
154
|
+
const space = await tempClient.createSpace(spaceName, { account, access });
|
|
155
|
+
await tempClient.setCurrentSpace(space.did());
|
|
156
|
+
// Delegate space access from the new space to our clawracha agent
|
|
157
|
+
const agent = Agent.parse(deviceConfig.agentKey);
|
|
158
|
+
const audience = { did: () => agent.did() };
|
|
159
|
+
const uploadDelegation = await tempClient.createDelegation(audience, Object.keys(spaceAccess), { expiration: Infinity });
|
|
160
|
+
const { ok: uploadArchive } = await uploadDelegation.archive();
|
|
161
|
+
if (!uploadArchive)
|
|
162
|
+
throw new Error("Failed to archive upload delegation");
|
|
163
|
+
// Delegate plan/get from account → clawracha agent
|
|
164
|
+
const accountDID = account.did();
|
|
165
|
+
const planProofs = tempClient.agent.proofs([{
|
|
166
|
+
can: "plan/get",
|
|
167
|
+
with: accountDID,
|
|
168
|
+
}]);
|
|
169
|
+
const planDelegation = await delegate({
|
|
170
|
+
issuer: tempClient.agent.issuer,
|
|
171
|
+
audience: { did: () => agent.did() },
|
|
172
|
+
capabilities: [{ can: "plan/get", with: accountDID }],
|
|
173
|
+
proofs: planProofs,
|
|
174
|
+
expiration: Infinity,
|
|
175
|
+
});
|
|
176
|
+
const { ok: planArchive } = await planDelegation.archive();
|
|
177
|
+
if (!planArchive)
|
|
178
|
+
throw new Error("Failed to archive plan delegation");
|
|
179
|
+
deviceConfig.uploadDelegation = encodeDelegation(uploadArchive);
|
|
180
|
+
deviceConfig.planDelegation = encodeDelegation(planArchive);
|
|
181
|
+
deviceConfig.spaceDID = space.did();
|
|
182
|
+
deviceConfig.access = access;
|
|
133
183
|
deviceConfig.setupComplete = true;
|
|
134
184
|
await saveDeviceConfig(workspace, deviceConfig);
|
|
135
185
|
// One-shot sync: scan existing files, upload, stop
|
|
@@ -143,29 +193,54 @@ export async function doSetup(workspace, agentId, delegationArg, pluginConfig, g
|
|
|
143
193
|
if (gatewayConfig) {
|
|
144
194
|
await requestWorkspaceUpdate(workspace, agentId, gatewayConfig);
|
|
145
195
|
}
|
|
146
|
-
|
|
147
|
-
|
|
196
|
+
return { agentDID: agent.did(), spaceDID: space.did() };
|
|
197
|
+
}
|
|
198
|
+
// --- Bundle arg helper ---
|
|
199
|
+
/**
|
|
200
|
+
* Read a delegation bundle from a file path or base64-encoded string.
|
|
201
|
+
* Returns raw CAR bytes.
|
|
202
|
+
*/
|
|
203
|
+
async function readBundleArg(arg) {
|
|
204
|
+
// Try as file path first
|
|
205
|
+
try {
|
|
206
|
+
const bytes = await fs.readFile(arg);
|
|
207
|
+
return new Uint8Array(bytes);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
if (err.code !== "ENOENT")
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
// Try as base64
|
|
214
|
+
return Uint8Array.from(Buffer.from(arg, "base64"));
|
|
148
215
|
}
|
|
149
|
-
export async function doJoin(workspace, agentId,
|
|
216
|
+
export async function doJoin(workspace, agentId, bundleArg, pluginConfig, gatewayConfig) {
|
|
150
217
|
const deviceConfig = await loadDeviceConfig(workspace);
|
|
151
218
|
if (!deviceConfig?.agentKey) {
|
|
152
219
|
throw new Error("Not initialized. Run init first.");
|
|
153
220
|
}
|
|
154
221
|
if (deviceConfig.setupComplete) {
|
|
155
222
|
const agent = Agent.parse(deviceConfig.agentKey);
|
|
156
|
-
return {
|
|
223
|
+
return {
|
|
224
|
+
agentDID: agent.did(),
|
|
225
|
+
spaceDID: deviceConfig.spaceDID,
|
|
226
|
+
pullCount: 0,
|
|
227
|
+
};
|
|
157
228
|
}
|
|
158
|
-
const
|
|
159
|
-
const
|
|
229
|
+
const bundleBytes = await readBundleArg(bundleArg);
|
|
230
|
+
const bundle = await extractDelegationBundle(bundleBytes);
|
|
231
|
+
const { ok: uploadDelegation, error: uploadErr } = await extract(bundle.upload);
|
|
232
|
+
if (!uploadDelegation)
|
|
233
|
+
throw new Error(`Failed to extract upload delegation: ${uploadErr}`);
|
|
234
|
+
const { ok: nameDelegation, error: nameErr } = await extract(bundle.name);
|
|
235
|
+
if (!nameDelegation)
|
|
236
|
+
throw new Error(`Failed to extract name delegation: ${nameErr}`);
|
|
237
|
+
const { ok: planDelegation, error: planErr } = await extract(bundle.plan);
|
|
238
|
+
if (!planDelegation)
|
|
239
|
+
throw new Error(`Failed to extract plan delegation: ${planErr}`);
|
|
160
240
|
const spaceDID = uploadDelegation.capabilities[0]?.with;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const { ok: nameArchiveBytes } = await nameDelegation.archive();
|
|
165
|
-
if (!nameArchiveBytes)
|
|
166
|
-
throw new Error("Failed to archive name delegation");
|
|
167
|
-
deviceConfig.uploadDelegation = encodeDelegation(uploadArchive);
|
|
168
|
-
deviceConfig.nameDelegation = encodeDelegation(nameArchiveBytes);
|
|
241
|
+
deviceConfig.uploadDelegation = encodeDelegation(bundle.upload);
|
|
242
|
+
deviceConfig.nameDelegation = encodeDelegation(bundle.name);
|
|
243
|
+
deviceConfig.planDelegation = encodeDelegation(bundle.plan);
|
|
169
244
|
deviceConfig.spaceDID = spaceDID ?? undefined;
|
|
170
245
|
deviceConfig.setupComplete = true;
|
|
171
246
|
await saveDeviceConfig(workspace, deviceConfig);
|
|
@@ -181,5 +256,65 @@ export async function doJoin(workspace, agentId, uploadDelegationArg, nameDelega
|
|
|
181
256
|
await requestWorkspaceUpdate(workspace, agentId, gatewayConfig);
|
|
182
257
|
}
|
|
183
258
|
const agent = Agent.parse(deviceConfig.agentKey);
|
|
184
|
-
return {
|
|
259
|
+
return {
|
|
260
|
+
agentDID: agent.did(),
|
|
261
|
+
spaceDID: spaceDID ?? undefined,
|
|
262
|
+
pullCount,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
export async function doGrant(workspace, targetDID) {
|
|
266
|
+
const deviceConfig = await loadDeviceConfig(workspace);
|
|
267
|
+
if (!deviceConfig?.setupComplete) {
|
|
268
|
+
throw new Error("Workspace not set up. Run setup first.");
|
|
269
|
+
}
|
|
270
|
+
// Upload delegation: re-delegate space access
|
|
271
|
+
const storachaClient = await createStorachaClient(deviceConfig);
|
|
272
|
+
const { spaceAccess } = await import("@storacha/client/capability/access");
|
|
273
|
+
const audience = { did: () => targetDID };
|
|
274
|
+
const uploadDel = await storachaClient.createDelegation(audience, Object.keys(spaceAccess), { expiration: Infinity });
|
|
275
|
+
const { ok: uploadArchive } = await uploadDel.archive();
|
|
276
|
+
if (!uploadArchive)
|
|
277
|
+
throw new Error("Failed to archive upload delegation");
|
|
278
|
+
// Name delegation: re-delegate via name.grant()
|
|
279
|
+
const agent = Agent.parse(deviceConfig.agentKey);
|
|
280
|
+
let name;
|
|
281
|
+
if (deviceConfig.nameArchive) {
|
|
282
|
+
const archiveBytes = decodeDelegation(deviceConfig.nameArchive);
|
|
283
|
+
name = await Name.extract(agent, archiveBytes);
|
|
284
|
+
}
|
|
285
|
+
else if (deviceConfig.nameDelegation) {
|
|
286
|
+
const nameBytes = decodeDelegation(deviceConfig.nameDelegation);
|
|
287
|
+
const { ok: nameDel } = await extract(nameBytes);
|
|
288
|
+
if (nameDel)
|
|
289
|
+
name = Name.from(agent, [nameDel]);
|
|
290
|
+
}
|
|
291
|
+
if (!name)
|
|
292
|
+
throw new Error("No name state available to grant from");
|
|
293
|
+
const nameDel = await name.grant(targetDID);
|
|
294
|
+
const { ok: nameArchive } = await nameDel.archive();
|
|
295
|
+
if (!nameArchive)
|
|
296
|
+
throw new Error("Failed to archive name delegation");
|
|
297
|
+
// Plan delegation: re-delegate from stored plan delegation
|
|
298
|
+
if (!deviceConfig.planDelegation) {
|
|
299
|
+
throw new Error("No plan delegation to re-delegate");
|
|
300
|
+
}
|
|
301
|
+
const planBytes = decodeDelegation(deviceConfig.planDelegation);
|
|
302
|
+
const { ok: existingPlanDel } = await extract(planBytes);
|
|
303
|
+
if (!existingPlanDel)
|
|
304
|
+
throw new Error("Failed to extract plan delegation");
|
|
305
|
+
const planDel = await delegate({
|
|
306
|
+
issuer: agent,
|
|
307
|
+
audience,
|
|
308
|
+
capabilities: existingPlanDel.capabilities,
|
|
309
|
+
proofs: [existingPlanDel],
|
|
310
|
+
expiration: Infinity,
|
|
311
|
+
});
|
|
312
|
+
const { ok: planArchive } = await planDel.archive();
|
|
313
|
+
if (!planArchive)
|
|
314
|
+
throw new Error("Failed to archive plan delegation");
|
|
315
|
+
return createDelegationBundle({
|
|
316
|
+
upload: uploadArchive,
|
|
317
|
+
name: nameArchive,
|
|
318
|
+
plan: planArchive,
|
|
319
|
+
});
|
|
185
320
|
}
|
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Markdown files (.md) are handled via mdsync — CRDT merge rather than
|
|
5
5
|
* whole-file UnixFS replacement. Regular files go through encodeFiles.
|
|
6
|
+
*
|
|
7
|
+
* When encryptionConfig is provided (private spaces), content is encrypted
|
|
8
|
+
* before upload via encryptToBlockStream.
|
|
6
9
|
*/
|
|
7
10
|
import type { BlockFetcher, ValueView } from "@storacha/ucn/pail/api";
|
|
8
11
|
import type { Block } from "multiformats";
|
|
9
12
|
import type { FileChange, PailOp } from "../types/index.js";
|
|
13
|
+
import type { EncryptionConfig } from "@storacha/encrypt-upload-client/types";
|
|
10
14
|
/** Callback to persist a block to the CAR file for upload. */
|
|
11
15
|
export type BlockSink = (block: Block) => Promise<void>;
|
|
12
16
|
/** Callback to persist a block to the local blockstore for future reads. */
|
|
13
17
|
export type BlockStore = (block: Block) => Promise<void>;
|
|
14
|
-
export declare function processChanges(changes: FileChange[], workspace: string, current: ValueView | null, blocks: BlockFetcher, sink: BlockSink, store?: BlockStore): Promise<PailOp[]>;
|
|
18
|
+
export declare function processChanges(changes: FileChange[], workspace: string, current: ValueView | null, blocks: BlockFetcher, sink: BlockSink, store?: BlockStore, encryptionConfig?: EncryptionConfig): Promise<PailOp[]>;
|
|
15
19
|
//# sourceMappingURL=process.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/handlers/process.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/handlers/process.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EACV,YAAY,EAEZ,SAAS,EACV,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAO9E,8DAA8D;AAC9D,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAExD,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAqBzD,wBAAsB,cAAc,CAClC,OAAO,EAAE,UAAU,EAAE,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,GAAG,IAAI,EACzB,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,SAAS,EACf,KAAK,CAAC,EAAE,UAAU,EAClB,gBAAgB,CAAC,EAAE,gBAAgB,GAClC,OAAO,CAAC,MAAM,EAAE,CAAC,CAyGnB"}
|
package/dist/handlers/process.js
CHANGED
|
@@ -3,37 +3,67 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Markdown files (.md) are handled via mdsync — CRDT merge rather than
|
|
5
5
|
* whole-file UnixFS replacement. Regular files go through encodeFiles.
|
|
6
|
+
*
|
|
7
|
+
* When encryptionConfig is provided (private spaces), content is encrypted
|
|
8
|
+
* before upload via encryptToBlockStream.
|
|
6
9
|
*/
|
|
7
10
|
import { Revision } from "@storacha/ucn/pail";
|
|
8
11
|
import { encodeFiles } from "../utils/encoder.js";
|
|
12
|
+
import { encryptToBlockStream, bytesToBlobLike } from "../utils/crypto.js";
|
|
9
13
|
import * as mdsync from "../mdsync/index.js";
|
|
10
14
|
import * as fs from "node:fs/promises";
|
|
11
15
|
import * as path from "node:path";
|
|
12
16
|
const isMarkdown = (filePath) => filePath.endsWith(".md");
|
|
13
|
-
|
|
17
|
+
/** Read all blocks from a ReadableStream, sinking each, return the last CID (root). */
|
|
18
|
+
async function drainBlockStream(stream, sink) {
|
|
19
|
+
const reader = stream.getReader();
|
|
20
|
+
let root = null;
|
|
21
|
+
while (true) {
|
|
22
|
+
const { done, value } = await reader.read();
|
|
23
|
+
if (done)
|
|
24
|
+
break;
|
|
25
|
+
root = value.cid;
|
|
26
|
+
await sink(value);
|
|
27
|
+
}
|
|
28
|
+
if (!root)
|
|
29
|
+
throw new Error("Empty block stream");
|
|
30
|
+
return root;
|
|
31
|
+
}
|
|
32
|
+
export async function processChanges(changes, workspace, current, blocks, sink, store, encryptionConfig) {
|
|
14
33
|
const pendingOps = [];
|
|
15
34
|
const mdChanges = changes.filter((c) => isMarkdown(c.path));
|
|
16
35
|
const regularChanges = changes.filter((c) => !isMarkdown(c.path));
|
|
17
|
-
// --- Regular files
|
|
36
|
+
// --- Regular files ---
|
|
18
37
|
const toEncode = regularChanges
|
|
19
38
|
.filter((c) => c.type !== "unlink")
|
|
20
39
|
.map((c) => c.path);
|
|
21
|
-
const encoded = await encodeFiles(workspace, toEncode);
|
|
22
40
|
const files = [];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
if (encryptionConfig) {
|
|
42
|
+
// Private space: encrypt each file
|
|
43
|
+
for (const relPath of toEncode) {
|
|
44
|
+
try {
|
|
45
|
+
const fileBytes = await fs.readFile(path.join(workspace, relPath));
|
|
46
|
+
const blob = bytesToBlobLike(fileBytes);
|
|
47
|
+
const encStream = await encryptToBlockStream(blob, encryptionConfig);
|
|
48
|
+
const rootCID = await drainBlockStream(encStream, sink);
|
|
49
|
+
files.push({ path: relPath, rootCID });
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (err.code === "ENOENT") {
|
|
53
|
+
console.warn(`File not found during encoding: ${relPath}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
32
58
|
}
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Public space: UnixFS encode
|
|
62
|
+
const encoded = await encodeFiles(workspace, toEncode);
|
|
63
|
+
for (const file of encoded) {
|
|
64
|
+
const rootCID = await drainBlockStream(file.blocks, sink);
|
|
65
|
+
files.push({ path: file.path, rootCID });
|
|
35
66
|
}
|
|
36
|
-
files.push({ path: file.path, rootCID: root });
|
|
37
67
|
}
|
|
38
68
|
for (const file of files) {
|
|
39
69
|
const existing = current
|
|
@@ -51,24 +81,37 @@ export async function processChanges(changes, workspace, current, blocks, sink,
|
|
|
51
81
|
const mdPuts = mdChanges.filter((c) => c.type !== "unlink");
|
|
52
82
|
for (const change of mdPuts) {
|
|
53
83
|
const content = await fs.readFile(path.join(workspace, change.path), "utf-8");
|
|
54
|
-
const
|
|
84
|
+
const block = current
|
|
55
85
|
? await mdsync.put(blocks, current, change.path, content)
|
|
56
86
|
: await mdsync.v0Put(content);
|
|
57
|
-
if (!
|
|
87
|
+
if (!block) {
|
|
58
88
|
continue; // No change detected, skip writing a new entry.
|
|
59
89
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
90
|
+
if (encryptionConfig) {
|
|
91
|
+
// Private space: encrypt the single-block entry
|
|
92
|
+
const blob = bytesToBlobLike(block.bytes);
|
|
93
|
+
const encStream = await encryptToBlockStream(blob, encryptionConfig);
|
|
94
|
+
const rootCID = await drainBlockStream(encStream, sink);
|
|
95
|
+
// Store unencrypted block locally for future resolveValue calls
|
|
96
|
+
if (store)
|
|
97
|
+
await store(block);
|
|
98
|
+
pendingOps.push({
|
|
99
|
+
type: "put",
|
|
100
|
+
key: change.path,
|
|
101
|
+
value: rootCID,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Public space: store block directly
|
|
63
106
|
await sink(block);
|
|
64
107
|
if (store)
|
|
65
108
|
await store(block);
|
|
109
|
+
pendingOps.push({
|
|
110
|
+
type: "put",
|
|
111
|
+
key: change.path,
|
|
112
|
+
value: block.cid,
|
|
113
|
+
});
|
|
66
114
|
}
|
|
67
|
-
pendingOps.push({
|
|
68
|
-
type: "put",
|
|
69
|
-
key: change.path,
|
|
70
|
-
value: mdEntryCid,
|
|
71
|
-
});
|
|
72
115
|
}
|
|
73
116
|
// --- Deletes (both regular and markdown) ---
|
|
74
117
|
const toDelete = changes
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
* Apply remote changes to local filesystem.
|
|
3
3
|
*
|
|
4
4
|
* Regular files are fetched from the IPFS gateway (handles UnixFS reassembly).
|
|
5
|
+
* For private spaces, regular files are decrypted via EncryptedClient.
|
|
5
6
|
* Markdown files (.md) are resolved via mdsync CRDT merge — the tiered
|
|
6
7
|
* blockstore's gateway layer handles fetching any missing blocks.
|
|
7
8
|
*/
|
|
8
9
|
import type { CID } from "multiformats/cid";
|
|
9
10
|
import type { BlockFetcher, ValueView } from "@storacha/ucn/pail/api";
|
|
11
|
+
import type { DecryptionConfig, EncryptedClient } from "@storacha/encrypt-upload-client/types";
|
|
10
12
|
export declare function applyRemoteChanges(changedPaths: string[], entries: Map<string, CID>, workspace: string, options?: {
|
|
11
13
|
gateway?: string;
|
|
12
14
|
blocks?: BlockFetcher;
|
|
13
15
|
current?: ValueView;
|
|
16
|
+
encryptedClient?: EncryptedClient;
|
|
17
|
+
decryptionConfig?: DecryptionConfig;
|
|
14
18
|
}): Promise<void>;
|
|
15
19
|
//# sourceMappingURL=remote.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../../src/handlers/remote.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../../src/handlers/remote.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AA2B/F,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC,GACA,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
|
package/dist/handlers/remote.js
CHANGED
|
@@ -2,16 +2,38 @@
|
|
|
2
2
|
* Apply remote changes to local filesystem.
|
|
3
3
|
*
|
|
4
4
|
* Regular files are fetched from the IPFS gateway (handles UnixFS reassembly).
|
|
5
|
+
* For private spaces, regular files are decrypted via EncryptedClient.
|
|
5
6
|
* Markdown files (.md) are resolved via mdsync CRDT merge — the tiered
|
|
6
7
|
* blockstore's gateway layer handles fetching any missing blocks.
|
|
7
8
|
*/
|
|
8
9
|
import * as fs from "node:fs/promises";
|
|
9
10
|
import * as path from "node:path";
|
|
10
11
|
import * as mdsync from "../mdsync/index.js";
|
|
12
|
+
import { makeDecryptFn } from "../utils/crypto.js";
|
|
11
13
|
const DEFAULT_GATEWAY = "https://storacha.link";
|
|
12
14
|
const isMarkdown = (filePath) => filePath.endsWith(".md");
|
|
15
|
+
/** Drain a ReadableStream into a single Uint8Array. */
|
|
16
|
+
async function drainStream(stream) {
|
|
17
|
+
const reader = stream.getReader();
|
|
18
|
+
const chunks = [];
|
|
19
|
+
while (true) {
|
|
20
|
+
const { done, value } = await reader.read();
|
|
21
|
+
if (done)
|
|
22
|
+
break;
|
|
23
|
+
chunks.push(value);
|
|
24
|
+
}
|
|
25
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
26
|
+
const result = new Uint8Array(totalLength);
|
|
27
|
+
let offset = 0;
|
|
28
|
+
for (const chunk of chunks) {
|
|
29
|
+
result.set(chunk, offset);
|
|
30
|
+
offset += chunk.length;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
13
34
|
export async function applyRemoteChanges(changedPaths, entries, workspace, options) {
|
|
14
35
|
const gateway = options?.gateway ?? DEFAULT_GATEWAY;
|
|
36
|
+
const isEncrypted = !!(options?.encryptedClient && options?.decryptionConfig);
|
|
15
37
|
for (const relativePath of changedPaths) {
|
|
16
38
|
const cid = entries.get(relativePath);
|
|
17
39
|
const fullPath = path.join(workspace, relativePath);
|
|
@@ -27,16 +49,26 @@ export async function applyRemoteChanges(changedPaths, entries, workspace, optio
|
|
|
27
49
|
}
|
|
28
50
|
else if (isMarkdown(relativePath) && options?.blocks && options?.current) {
|
|
29
51
|
// Markdown: resolve via mdsync CRDT merge.
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
const
|
|
52
|
+
// For single-device, unencrypted blocks are stored locally.
|
|
53
|
+
// TODO: For multi-device private spaces, add decrypt layer to resolveValue.
|
|
54
|
+
const decrypt = isEncrypted
|
|
55
|
+
? makeDecryptFn(options.encryptedClient, options.decryptionConfig)
|
|
56
|
+
: undefined;
|
|
57
|
+
const content = await mdsync.get(options.blocks, options.current, relativePath, decrypt);
|
|
33
58
|
if (content != null) {
|
|
34
59
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
35
60
|
await fs.writeFile(fullPath, content);
|
|
36
61
|
}
|
|
37
62
|
}
|
|
63
|
+
else if (isEncrypted) {
|
|
64
|
+
// Private space: retrieve and decrypt via EncryptedClient
|
|
65
|
+
const { stream } = await options.encryptedClient.retrieveAndDecryptFile(cid, options.decryptionConfig);
|
|
66
|
+
const bytes = await drainStream(stream);
|
|
67
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
68
|
+
await fs.writeFile(fullPath, bytes);
|
|
69
|
+
}
|
|
38
70
|
else {
|
|
39
|
-
//
|
|
71
|
+
// Public space: fetch full file from gateway (handles UnixFS reassembly)
|
|
40
72
|
const res = await fetch(`${gateway}/ipfs/${cid}`);
|
|
41
73
|
if (!res.ok) {
|
|
42
74
|
throw new Error(`Gateway fetch failed for ${cid}: ${res.status}`);
|
package/dist/mdsync/index.d.ts
CHANGED
|
@@ -1,29 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* mdsync — CRDT markdown storage on top of UCN Pail.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Each key's value is a single DAG-CBOR block (DeserializedMarkdownEntry)
|
|
5
|
+
* containing:
|
|
6
6
|
* - The current RGA tree (full document state)
|
|
7
7
|
* - An RGA of MarkdownEvents (causal history scoped to this key)
|
|
8
8
|
* - The last changeset applied (for incremental replay during merge)
|
|
9
9
|
*
|
|
10
10
|
* On read, if the Pail has multiple heads (concurrent writes), we resolve
|
|
11
11
|
* by walking from the common ancestor forward, replaying each branch's
|
|
12
|
-
* changesets and merging event RGAs
|
|
13
|
-
* concurrent root updates.
|
|
12
|
+
* changesets and merging event RGAs.
|
|
14
13
|
*/
|
|
15
|
-
import { BlockFetcher, ValueView } from "@storacha/ucn/pail/api";
|
|
14
|
+
import { BlockFetcher, EventLink, ValueView } from "@storacha/ucn/pail/api";
|
|
16
15
|
import { Block, CID } from "multiformats";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
import { RGA, RGATreeRoot, RGAChangeSet } from "@storacha/md-merge";
|
|
17
|
+
/**
|
|
18
|
+
* A MarkdownEvent ties an RGA tree mutation to its position in the Pail's
|
|
19
|
+
* merkle clock. `parents` are the EventLinks of the Pail revision that was
|
|
20
|
+
* current when this edit was made — this is what links the md-merge causal
|
|
21
|
+
* history to the Pail's causal history.
|
|
22
|
+
*
|
|
23
|
+
* Implements RGAEvent (toString) so it can be used as the event type in
|
|
24
|
+
* md-merge's RGA and RGATree.
|
|
25
|
+
*/
|
|
26
|
+
declare class MarkdownEvent {
|
|
27
|
+
parents: Array<EventLink>;
|
|
28
|
+
constructor(parents: Array<EventLink>);
|
|
29
|
+
toString(): string;
|
|
30
|
+
}
|
|
31
|
+
/** Shorthand for the event RGA type used throughout. */
|
|
32
|
+
type EventRGA = RGA<MarkdownEvent, MarkdownEvent>;
|
|
33
|
+
interface DeserializedMarkdownEntryBase {
|
|
34
|
+
type: string;
|
|
35
|
+
/** The full RGA tree for the document at this key. */
|
|
36
|
+
root: RGATreeRoot<MarkdownEvent>;
|
|
37
|
+
/**
|
|
38
|
+
* Causal history as an RGA. Each node is a MarkdownEvent; the RGA's
|
|
39
|
+
* afterId links encode causal ordering. toWeightedArray() gives a
|
|
40
|
+
* BFS linearization suitable for building a comparator.
|
|
41
|
+
*/
|
|
42
|
+
events: EventRGA;
|
|
43
|
+
}
|
|
44
|
+
interface DeserializedMarkdownEntryInitial extends DeserializedMarkdownEntryBase {
|
|
45
|
+
type: "initial";
|
|
20
46
|
}
|
|
47
|
+
interface DeserializedMarkdownEntryUpdate extends DeserializedMarkdownEntryBase {
|
|
48
|
+
type: "update";
|
|
49
|
+
/** The changeset that was applied to produce this version of the tree. */
|
|
50
|
+
changeset: RGAChangeSet<MarkdownEvent>;
|
|
51
|
+
}
|
|
52
|
+
type DeserializedMarkdownEntry = DeserializedMarkdownEntryInitial | DeserializedMarkdownEntryUpdate;
|
|
53
|
+
/**
|
|
54
|
+
* Encode a DeserializedMarkdownEntry as a single DAG-CBOR block.
|
|
55
|
+
* All data (tree, events, changeset) is inlined — no CID references.
|
|
56
|
+
*/
|
|
57
|
+
export declare const encodeMarkdownEntry: (entry: DeserializedMarkdownEntry) => Promise<Block>;
|
|
58
|
+
/**
|
|
59
|
+
* Decode a single DAG-CBOR block back to a DeserializedMarkdownEntry.
|
|
60
|
+
*/
|
|
61
|
+
export declare const decodeMarkdownEntry: (block: {
|
|
62
|
+
bytes: Uint8Array;
|
|
63
|
+
}) => Promise<DeserializedMarkdownEntry>;
|
|
21
64
|
/**
|
|
22
65
|
* First put into an empty Pail (v0 = no existing revision).
|
|
23
|
-
* Returns
|
|
24
|
-
*
|
|
66
|
+
* Returns a single block to store. Caller is responsible for
|
|
67
|
+
* creating the Pail revision via Revision.v0Put.
|
|
25
68
|
*/
|
|
26
|
-
export declare const v0Put: (newMarkdown: string) => Promise<
|
|
69
|
+
export declare const v0Put: (newMarkdown: string) => Promise<Block>;
|
|
27
70
|
/**
|
|
28
71
|
* Put markdown content at a key in an existing Pail.
|
|
29
72
|
*
|
|
@@ -32,14 +75,14 @@ export declare const v0Put: (newMarkdown: string) => Promise<MarkdownResult>;
|
|
|
32
75
|
* an RGA changeset against the resolved tree, applies it, and stores the
|
|
33
76
|
* updated entry.
|
|
34
77
|
*
|
|
35
|
-
* Returns
|
|
36
|
-
* responsible for creating the Pail revision via Revision.put.
|
|
78
|
+
* Returns a single block to store, or null if no changes detected.
|
|
79
|
+
* Caller is responsible for creating the Pail revision via Revision.put.
|
|
37
80
|
*/
|
|
38
|
-
export declare const put: (blocks: BlockFetcher, current: ValueView, key: string, newMarkdown: string) => Promise<
|
|
81
|
+
export declare const put: (blocks: BlockFetcher, current: ValueView, key: string, newMarkdown: string) => Promise<Block | null>;
|
|
39
82
|
/**
|
|
40
83
|
* Get the current markdown string for a key, resolving concurrent heads.
|
|
41
84
|
* Returns undefined if the key doesn't exist.
|
|
42
85
|
*/
|
|
43
|
-
export declare const get: (blocks: BlockFetcher, current: ValueView, key: string) => Promise<string | undefined>;
|
|
86
|
+
export declare const get: (blocks: BlockFetcher, current: ValueView, key: string, decrypt?: (cid: CID) => Promise<Uint8Array>) => Promise<string | undefined>;
|
|
44
87
|
export {};
|
|
45
88
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mdsync/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mdsync/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EACL,YAAY,EACZ,SAAS,EAET,SAAS,EACV,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,EAEL,GAAG,EAGH,WAAW,EACX,YAAY,EAUb,MAAM,oBAAoB,CAAC;AAQ5B;;;;;;;;GAQG;AACH,cAAM,aAAa;IACjB,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;gBAEd,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;IAIrC,QAAQ;CAGT;AAyBD,wDAAwD;AACxD,KAAK,QAAQ,GAAG,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;AAElD,UAAU,6BAA6B;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,EAAE,WAAW,CAAC,aAAa,CAAC,CAAC;IACjC;;;;OAIG;IACH,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED,UAAU,gCACR,SAAQ,6BAA6B;IACrC,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,UAAU,+BACR,SAAQ,6BAA6B;IACrC,IAAI,EAAE,QAAQ,CAAC;IACf,0EAA0E;IAC1E,SAAS,EAAE,YAAY,CAAC,aAAa,CAAC,CAAC;CACxC;AAED,KAAK,yBAAyB,GAC1B,gCAAgC,GAChC,+BAA+B,CAAC;AA8BpC;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC9B,OAAO,yBAAyB,KAC/B,OAAO,CAAC,KAAK,CAcf,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC9B,OAAO;IAAE,KAAK,EAAE,UAAU,CAAA;CAAE,KAC3B,OAAO,CAAC,yBAAyB,CA+BnC,CAAC;AA4BF;;;;GAIG;AACH,eAAO,MAAM,KAAK,GAAU,aAAa,MAAM,KAAG,OAAO,CAAC,KAAK,CAG9D,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,GAAG,GACd,QAAQ,YAAY,EACpB,SAAS,SAAS,EAClB,KAAK,MAAM,EACX,aAAa,MAAM,KAClB,OAAO,CAAC,KAAK,GAAG,IAAI,CAiCtB,CAAC;AA4IF;;;GAGG;AACH,eAAO,MAAM,GAAG,GACd,QAAQ,YAAY,EACpB,SAAS,SAAS,EAClB,KAAK,MAAM,EACX,UAAU,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,UAAU,CAAC,KAC1C,OAAO,CAAC,MAAM,GAAG,SAAS,CAM5B,CAAC"}
|