@storacha/clawracha 0.2.3 → 0.3.1

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.
@@ -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, delegationArg: string, pluginConfig: Partial<SyncPluginConfig>, gatewayConfig?: NonNullable<OpenClawConfig["gateway"]>): Promise<SetupResult>;
41
- export declare function doJoin(workspace: string, agentId: string, uploadDelegationArg: string, nameDelegationArg: string, pluginConfig: Partial<SyncPluginConfig>, gatewayConfig?: NonNullable<OpenClawConfig["gateway"]>): Promise<JoinResult>;
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
@@ -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;AAS3C,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,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,OAAO,CAAC,gBAAgB,CAAC,EACvC,aAAa,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,GACrD,OAAO,CAAC,WAAW,CAAC,CA0CtB;AAED,wBAAsB,MAAM,CAC1B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,mBAAmB,EAAE,MAAM,EAC3B,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,OAAO,CAAC,gBAAgB,CAAC,EACvC,aAAa,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,GACrD,OAAO,CAAC,UAAU,CAAC,CA8CrB"}
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, readDelegationArg, } from "./utils/delegation.js";
12
- import { Agent } from "@storacha/ucn/pail";
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, delegationArg, pluginConfig, gatewayConfig) {
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 delegation = await readDelegationArg(delegationArg);
127
- const spaceDID = delegation.capabilities[0]?.with;
128
- const { ok: archiveBytes } = await delegation.archive();
129
- if (!archiveBytes)
130
- throw new Error("Failed to archive delegation");
131
- deviceConfig.uploadDelegation = encodeDelegation(archiveBytes);
132
- deviceConfig.spaceDID = spaceDID ?? undefined;
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
- const agent = Agent.parse(deviceConfig.agentKey);
147
- return { agentDID: agent.did(), spaceDID: spaceDID ?? undefined };
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, uploadDelegationArg, nameDelegationArg, pluginConfig, gatewayConfig) {
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 { agentDID: agent.did(), spaceDID: deviceConfig.spaceDID, pullCount: 0 };
223
+ return {
224
+ agentDID: agent.did(),
225
+ spaceDID: deviceConfig.spaceDID,
226
+ pullCount: 0,
227
+ };
157
228
  }
158
- const uploadDelegation = await readDelegationArg(uploadDelegationArg);
159
- const nameDelegation = await readDelegationArg(nameDelegationArg);
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
- const { ok: uploadArchive } = await uploadDelegation.archive();
162
- if (!uploadArchive)
163
- throw new Error("Failed to archive upload delegation");
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 { agentDID: agent.did(), spaceDID: spaceDID ?? undefined, pullCount };
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;;;;;GAKG;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;AAM5D,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;AAIzD,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,GACjB,OAAO,CAAC,MAAM,EAAE,CAAC,CAoFnB"}
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,CA6GnB"}
@@ -3,37 +3,66 @@
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 } 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
- export async function processChanges(changes, workspace, current, blocks, sink, store) {
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 (UnixFS encode) ---
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
- for (const file of encoded) {
24
- let root = null;
25
- const reader = file.blocks.getReader();
26
- while (true) {
27
- const { done, value } = await reader.read();
28
- if (done)
29
- break;
30
- root = value.cid;
31
- await sink(value);
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 encStream = await encryptToBlockStream(new Blob([fileBytes]), encryptionConfig);
47
+ const rootCID = await drainBlockStream(encStream, sink);
48
+ files.push({ path: relPath, rootCID });
49
+ }
50
+ catch (err) {
51
+ if (err.code === "ENOENT") {
52
+ console.warn(`File not found during encoding: ${relPath}`);
53
+ continue;
54
+ }
55
+ throw err;
56
+ }
32
57
  }
33
- if (!root) {
34
- throw new Error(`Failed to encode file: ${file.path}`);
58
+ }
59
+ else {
60
+ // Public space: UnixFS encode
61
+ const encoded = await encodeFiles(workspace, toEncode);
62
+ for (const file of encoded) {
63
+ const rootCID = await drainBlockStream(file.blocks, sink);
64
+ files.push({ path: file.path, rootCID });
35
65
  }
36
- files.push({ path: file.path, rootCID: root });
37
66
  }
38
67
  for (const file of files) {
39
68
  const existing = current
@@ -51,24 +80,36 @@ export async function processChanges(changes, workspace, current, blocks, sink,
51
80
  const mdPuts = mdChanges.filter((c) => c.type !== "unlink");
52
81
  for (const change of mdPuts) {
53
82
  const content = await fs.readFile(path.join(workspace, change.path), "utf-8");
54
- const newEntry = current
83
+ const block = current
55
84
  ? await mdsync.put(blocks, current, change.path, content)
56
85
  : await mdsync.v0Put(content);
57
- if (!newEntry) {
86
+ if (!block) {
58
87
  continue; // No change detected, skip writing a new entry.
59
88
  }
60
- const { mdEntryCid, additions } = newEntry;
61
- // Sink blocks to CAR for upload, and store locally for future resolveValue calls.
62
- for (const block of additions) {
89
+ if (encryptionConfig) {
90
+ // Private space: encrypt the single-block entry
91
+ const encStream = await encryptToBlockStream(new Blob([block.bytes]), encryptionConfig);
92
+ const rootCID = await drainBlockStream(encStream, sink);
93
+ // Store unencrypted block locally for future resolveValue calls
94
+ if (store)
95
+ await store(block);
96
+ pendingOps.push({
97
+ type: "put",
98
+ key: change.path,
99
+ value: rootCID,
100
+ });
101
+ }
102
+ else {
103
+ // Public space: store block directly
63
104
  await sink(block);
64
105
  if (store)
65
106
  await store(block);
107
+ pendingOps.push({
108
+ type: "put",
109
+ key: change.path,
110
+ value: block.cid,
111
+ });
66
112
  }
67
- pendingOps.push({
68
- type: "put",
69
- key: change.path,
70
- value: mdEntryCid,
71
- });
72
113
  }
73
114
  // --- Deletes (both regular and markdown) ---
74
115
  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;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAOtE,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;CACrB,GACA,OAAO,CAAC,IAAI,CAAC,CAiCf"}
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"}
@@ -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
- // The blockstore's lowest tier is a gateway fetcher, so any blocks
31
- // we don't have locally will be fetched transparently.
32
- const content = await mdsync.get(options.blocks, options.current, relativePath);
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
- // Regular file: fetch full file from gateway (handles UnixFS reassembly)
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}`);
@@ -1,29 +1,72 @@
1
1
  /**
2
2
  * mdsync — CRDT markdown storage on top of UCN Pail.
3
3
  *
4
- * Stores RGA-backed markdown trees at Pail keys. Each key's value is a
5
- * MarkdownEntry containing:
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 — analogous to how Pail itself resolves
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
- interface MarkdownResult {
18
- mdEntryCid: CID;
19
- additions: Block[];
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 the markdown entry CID and blocks to store. Caller is
24
- * responsible for creating the Pail revision via Revision.v0Put.
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<MarkdownResult>;
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 the markdown entry CID and blocks to store. Caller is
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<MarkdownResult | null>;
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;;;;;;;;;;;;;GAaG;AAGH,OAAO,EACL,YAAY,EAGZ,SAAS,EACV,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAuJ1C,UAAU,cAAc;IACtB,UAAU,EAAE,GAAG,CAAC;IAChB,SAAS,EAAE,KAAK,EAAE,CAAC;CACpB;AAyBD;;;;GAIG;AACH,eAAO,MAAM,KAAK,GAAU,aAAa,MAAM,KAAG,OAAO,CAAC,cAAc,CAEvE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,GAAG,GACd,QAAQ,YAAY,EACpB,SAAS,SAAS,EAClB,KAAK,MAAM,EACX,aAAa,MAAM,KAClB,OAAO,CAAC,cAAc,GAAG,IAAI,CAkC/B,CAAC;AAkSF;;;GAGG;AACH,eAAO,MAAM,GAAG,GACd,QAAQ,YAAY,EACpB,SAAS,SAAS,EAClB,KAAK,MAAM,KACV,OAAO,CAAC,MAAM,GAAG,SAAS,CAM5B,CAAC"}
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"}