@storacha/clawracha 0.0.1 → 0.0.2
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/handlers/apply.d.ts +18 -0
- package/dist/handlers/apply.d.ts.map +1 -0
- package/dist/handlers/apply.js +50 -0
- package/dist/handlers/process.d.ts +15 -0
- package/dist/handlers/process.d.ts.map +1 -0
- package/dist/handlers/process.js +82 -0
- package/dist/handlers/remote.d.ts +15 -0
- package/dist/handlers/remote.d.ts.map +1 -0
- package/dist/handlers/remote.js +48 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/mdsync/index.d.ts +45 -0
- package/dist/mdsync/index.d.ts.map +1 -0
- package/dist/mdsync/index.js +365 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +224 -26
- package/dist/sync.d.ts +12 -5
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +82 -157
- package/dist/types/index.d.ts +58 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/client.d.ts +14 -0
- package/dist/utils/client.d.ts.map +1 -0
- package/dist/utils/client.js +38 -0
- package/dist/utils/differ.d.ts +29 -0
- package/dist/utils/differ.d.ts.map +1 -0
- package/dist/utils/differ.js +47 -0
- package/dist/utils/encoder.d.ts +16 -0
- package/dist/utils/encoder.d.ts.map +1 -0
- package/dist/utils/encoder.js +51 -0
- package/dist/utils/tempcar.d.ts +22 -0
- package/dist/utils/tempcar.d.ts.map +1 -0
- package/dist/utils/tempcar.js +44 -0
- package/dist/watcher.d.ts +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/sync.js
CHANGED
|
@@ -8,25 +8,26 @@
|
|
|
8
8
|
* 5. Upload all blocks as CAR
|
|
9
9
|
* 6. Apply remote changes to local filesystem
|
|
10
10
|
*/
|
|
11
|
-
import * as fs from "node:fs/promises";
|
|
12
|
-
import * as path from "node:path";
|
|
13
|
-
import { CarWriter } from "@ipld/car/writer";
|
|
14
11
|
// UCN Pail imports
|
|
15
|
-
import { Agent, Name,
|
|
16
|
-
import * as Batch from "@storacha/ucn/pail/batch";
|
|
12
|
+
import { Agent, Name, NoValueError, Revision } from "@storacha/ucn/pail";
|
|
17
13
|
import { createWorkspaceBlockstore, } from "./blockstore/index.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
14
|
+
import { applyPendingOps } from "./handlers/apply.js";
|
|
15
|
+
import { applyRemoteChanges } from "./handlers/remote.js";
|
|
16
|
+
import { processChanges } from "./handlers/process.js";
|
|
17
|
+
import { diffRemoteChanges } from "./utils/differ.js";
|
|
18
|
+
import { makeTempCar } from "./utils/tempcar.js";
|
|
20
19
|
export class SyncEngine {
|
|
21
20
|
workspace;
|
|
22
21
|
blocks;
|
|
23
22
|
name = null;
|
|
24
23
|
current = null;
|
|
25
24
|
pendingOps = [];
|
|
26
|
-
|
|
25
|
+
carFile = null;
|
|
27
26
|
running = false;
|
|
28
27
|
lastSync = null;
|
|
29
|
-
|
|
28
|
+
storachaClient;
|
|
29
|
+
constructor(storachaClient, workspace) {
|
|
30
|
+
this.storachaClient = storachaClient;
|
|
30
31
|
this.workspace = workspace;
|
|
31
32
|
this.blocks = createWorkspaceBlockstore(workspace);
|
|
32
33
|
}
|
|
@@ -36,10 +37,21 @@ export class SyncEngine {
|
|
|
36
37
|
async init(config) {
|
|
37
38
|
const agent = Agent.parse(config.agentKey);
|
|
38
39
|
if (config.nameArchive) {
|
|
40
|
+
// Restore from previously saved archive (has full state)
|
|
39
41
|
const archiveBytes = Buffer.from(config.nameArchive, "base64");
|
|
40
42
|
this.name = await Name.extract(agent, archiveBytes);
|
|
41
43
|
}
|
|
44
|
+
else if (config.nameDelegation) {
|
|
45
|
+
// Reconstruct from delegation (granted by another device)
|
|
46
|
+
const { extract } = await import("@storacha/client/delegation");
|
|
47
|
+
const nameBytes = Buffer.from(config.nameDelegation, "base64");
|
|
48
|
+
const { ok: delegation } = await extract(nameBytes);
|
|
49
|
+
if (!delegation)
|
|
50
|
+
throw new Error("Failed to extract name delegation");
|
|
51
|
+
this.name = Name.from(agent, [delegation]);
|
|
52
|
+
}
|
|
42
53
|
else {
|
|
54
|
+
// First device — create a new name
|
|
43
55
|
this.name = await Name.create(agent);
|
|
44
56
|
}
|
|
45
57
|
try {
|
|
@@ -48,7 +60,7 @@ export class SyncEngine {
|
|
|
48
60
|
await this.storeBlocks(result.additions);
|
|
49
61
|
}
|
|
50
62
|
catch (err) {
|
|
51
|
-
if (err
|
|
63
|
+
if (err instanceof NoValueError) {
|
|
52
64
|
this.current = null;
|
|
53
65
|
}
|
|
54
66
|
else {
|
|
@@ -60,34 +72,11 @@ export class SyncEngine {
|
|
|
60
72
|
* Process a batch of file changes
|
|
61
73
|
*/
|
|
62
74
|
async processChanges(changes) {
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
.map((c) => c.path);
|
|
66
|
-
const toDelete = changes
|
|
67
|
-
.filter((c) => c.type === "unlink")
|
|
68
|
-
.map((c) => c.path);
|
|
69
|
-
// Encode changed files
|
|
70
|
-
const encoded = await encodeFiles(this.workspace, toEncode);
|
|
71
|
-
// Accumulate file blocks for CAR upload only (not persisted to blockstore)
|
|
72
|
-
// TODO: spill to temporary CAR file if memory becomes an issue
|
|
73
|
-
for (const file of encoded) {
|
|
74
|
-
this.allBlocks.push(...file.blocks);
|
|
75
|
-
}
|
|
76
|
-
// Check current pail state to skip no-ops
|
|
77
|
-
const currentEntries = await this.getPailEntries();
|
|
78
|
-
// Generate puts for changed files (skip if CID unchanged)
|
|
79
|
-
for (const file of encoded) {
|
|
80
|
-
const existing = currentEntries.get(file.path);
|
|
81
|
-
if (!existing || !existing.equals(file.rootCID)) {
|
|
82
|
-
this.pendingOps.push({ type: 'put', key: file.path, value: file.rootCID });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// Generate dels for removed files (skip if not in pail)
|
|
86
|
-
for (const deletePath of toDelete) {
|
|
87
|
-
if (currentEntries.has(deletePath)) {
|
|
88
|
-
this.pendingOps.push({ type: 'del', key: deletePath });
|
|
89
|
-
}
|
|
75
|
+
if (!this.carFile) {
|
|
76
|
+
this.carFile = await makeTempCar();
|
|
90
77
|
}
|
|
78
|
+
const pendingOps = await processChanges(changes, this.workspace, this.current, this.blocks, (block) => this.carFile.put(block), (block) => this.blocks.put(block));
|
|
79
|
+
this.pendingOps.push(...pendingOps);
|
|
91
80
|
}
|
|
92
81
|
/**
|
|
93
82
|
* Execute sync: generate revision, publish, upload, apply remote changes
|
|
@@ -97,78 +86,19 @@ export class SyncEngine {
|
|
|
97
86
|
throw new Error("Sync engine not initialized");
|
|
98
87
|
}
|
|
99
88
|
const beforeEntries = await this.getPailEntries();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const dels = this.pendingOps.filter((op) => op.type === "del");
|
|
104
|
-
// Process puts with batch
|
|
105
|
-
if (puts.length > 0) {
|
|
106
|
-
if (this.current) {
|
|
107
|
-
const batcher = await Batch.create(this.blocks, this.current);
|
|
108
|
-
for (const op of puts) {
|
|
109
|
-
if (op.value)
|
|
110
|
-
await batcher.put(op.key, op.value);
|
|
111
|
-
}
|
|
112
|
-
const result = await batcher.commit();
|
|
113
|
-
revisionBlocks.push(...result.additions);
|
|
114
|
-
await this.storeBlocks(result.additions);
|
|
115
|
-
const opResult = result.revision.operation;
|
|
116
|
-
this.current = Value.create(this.name, opResult.root, [
|
|
117
|
-
result.revision,
|
|
118
|
-
]);
|
|
89
|
+
if (this.pendingOps.length > 0) {
|
|
90
|
+
if (!this.carFile) {
|
|
91
|
+
throw new Error("CAR file not initialized");
|
|
119
92
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const result = await Revision.v0Put(this.blocks, firstPut.key, firstPut.value);
|
|
125
|
-
revisionBlocks.push(...result.additions);
|
|
126
|
-
await this.storeBlocks(result.additions);
|
|
127
|
-
const opResult = result.revision.operation;
|
|
128
|
-
this.current = Value.create(this.name, opResult.root, [
|
|
129
|
-
result.revision,
|
|
130
|
-
]);
|
|
131
|
-
// Batch remaining puts
|
|
132
|
-
if (puts.length > 1) {
|
|
133
|
-
const batcher = await Batch.create(this.blocks, this.current);
|
|
134
|
-
for (const op of puts.slice(1)) {
|
|
135
|
-
if (op.value)
|
|
136
|
-
await batcher.put(op.key, op.value);
|
|
137
|
-
}
|
|
138
|
-
const batchResult = await batcher.commit();
|
|
139
|
-
revisionBlocks.push(...batchResult.additions);
|
|
140
|
-
await this.storeBlocks(batchResult.additions);
|
|
141
|
-
const batchOpResult = batchResult.revision.operation;
|
|
142
|
-
this.current = Value.create(this.name, batchOpResult.root, [
|
|
143
|
-
batchResult.revision,
|
|
144
|
-
]);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
93
|
+
const result = await applyPendingOps(this.blocks, this.name, this.current, this.pendingOps);
|
|
94
|
+
this.current = result.current;
|
|
95
|
+
for (const block of result.revisionBlocks) {
|
|
96
|
+
await this.carFile.put(block);
|
|
147
97
|
}
|
|
148
|
-
|
|
149
|
-
// Process dels individually (batch doesn't support del)
|
|
150
|
-
if (dels.length > 0 && this.current) {
|
|
151
|
-
for (const op of dels) {
|
|
152
|
-
const result = await Revision.del(this.blocks, this.current, op.key);
|
|
153
|
-
revisionBlocks.push(...result.additions);
|
|
154
|
-
await this.storeBlocks(result.additions);
|
|
155
|
-
const opResult = result.revision.operation;
|
|
156
|
-
this.current = Value.create(this.name, opResult.root, [
|
|
157
|
-
result.revision,
|
|
158
|
-
]);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
this.pendingOps = [];
|
|
162
|
-
// Publish to network
|
|
163
|
-
if (this.current && this.current.revision.length > 0) {
|
|
164
|
-
const latestRevision = this.current.revision[this.current.revision.length - 1];
|
|
165
|
-
const pubResult = await Revision.publish(this.blocks, this.name, latestRevision);
|
|
166
|
-
revisionBlocks.push(...pubResult.additions);
|
|
167
|
-
await this.storeBlocks(pubResult.additions);
|
|
168
|
-
this.current = pubResult.value;
|
|
98
|
+
await this.storeBlocks(result.revisionBlocks);
|
|
169
99
|
}
|
|
170
100
|
else {
|
|
171
|
-
//
|
|
101
|
+
// No pending ops — just pull remote
|
|
172
102
|
try {
|
|
173
103
|
const result = await Revision.resolve(this.blocks, this.name, {
|
|
174
104
|
base: this.current ?? undefined,
|
|
@@ -177,16 +107,13 @@ export class SyncEngine {
|
|
|
177
107
|
this.current = result.value;
|
|
178
108
|
}
|
|
179
109
|
catch (err) {
|
|
180
|
-
if (err
|
|
110
|
+
if (!(err instanceof NoValueError))
|
|
181
111
|
throw err;
|
|
182
112
|
}
|
|
183
113
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
await this.uploadCAR(allUploadBlocks);
|
|
188
|
-
this.allBlocks = []; // Clear accumulated blocks
|
|
189
|
-
}
|
|
114
|
+
this.pendingOps = [];
|
|
115
|
+
// Upload all accumulated blocks as CAR
|
|
116
|
+
await this.possiblyUploadCAR();
|
|
190
117
|
// Apply remote changes
|
|
191
118
|
const afterEntries = await this.getPailEntries();
|
|
192
119
|
const remoteChanges = diffRemoteChanges(beforeEntries, afterEntries);
|
|
@@ -198,35 +125,19 @@ export class SyncEngine {
|
|
|
198
125
|
/**
|
|
199
126
|
* Create CAR and upload to Storacha
|
|
200
127
|
*/
|
|
201
|
-
async
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
chunks.push(chunk);
|
|
128
|
+
async possiblyUploadCAR() {
|
|
129
|
+
if (this.carFile) {
|
|
130
|
+
const readableCar = await this.carFile.switchToReadable();
|
|
131
|
+
this.carFile = null;
|
|
132
|
+
if (readableCar) {
|
|
133
|
+
try {
|
|
134
|
+
await this.storachaClient.uploadCAR(readableCar.readable);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await readableCar.cleanup();
|
|
138
|
+
}
|
|
213
139
|
}
|
|
214
|
-
})();
|
|
215
|
-
// Write blocks
|
|
216
|
-
for (const block of blocks) {
|
|
217
|
-
await writer.put(block);
|
|
218
|
-
}
|
|
219
|
-
await writer.close();
|
|
220
|
-
await collectPromise;
|
|
221
|
-
const carBytes = new Uint8Array(chunks.reduce((acc, c) => acc + c.length, 0));
|
|
222
|
-
let offset = 0;
|
|
223
|
-
for (const chunk of chunks) {
|
|
224
|
-
carBytes.set(chunk, offset);
|
|
225
|
-
offset += chunk.length;
|
|
226
140
|
}
|
|
227
|
-
// TODO: Upload to Storacha using client.uploadCAR
|
|
228
|
-
// For now, just log
|
|
229
|
-
console.log(`[storacha-sync] Would upload CAR: ${blocks.length} blocks, ${carBytes.length} bytes, root: ${rootCID}`);
|
|
230
141
|
}
|
|
231
142
|
/**
|
|
232
143
|
* Get current pail entries as map
|
|
@@ -246,25 +157,39 @@ export class SyncEngine {
|
|
|
246
157
|
* Apply remote changes to local filesystem
|
|
247
158
|
*/
|
|
248
159
|
async applyRemoteChanges(changedPaths, entries) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
160
|
+
await applyRemoteChanges(changedPaths, entries, this.workspace, {
|
|
161
|
+
blocks: this.blocks,
|
|
162
|
+
current: this.current ?? undefined,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Pull all remote state and write to local filesystem.
|
|
167
|
+
* Used by /storacha-join to overwrite local with remote before watcher starts.
|
|
168
|
+
*/
|
|
169
|
+
async pullRemote() {
|
|
170
|
+
if (!this.name)
|
|
171
|
+
throw new Error("Sync engine not initialized");
|
|
172
|
+
try {
|
|
173
|
+
const result = await Revision.resolve(this.blocks, this.name, {
|
|
174
|
+
base: this.current ?? undefined,
|
|
175
|
+
});
|
|
176
|
+
await this.storeBlocks(result.additions);
|
|
177
|
+
this.current = result.value;
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (!(err instanceof NoValueError))
|
|
181
|
+
throw err;
|
|
267
182
|
}
|
|
183
|
+
const entries = await this.getPailEntries();
|
|
184
|
+
if (entries.size > 0) {
|
|
185
|
+
const allPaths = [...entries.keys()];
|
|
186
|
+
await applyRemoteChanges(allPaths, entries, this.workspace, {
|
|
187
|
+
blocks: this.blocks,
|
|
188
|
+
current: this.current ?? undefined,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
this.lastSync = Date.now();
|
|
192
|
+
return entries.size;
|
|
268
193
|
}
|
|
269
194
|
async status() {
|
|
270
195
|
const entries = await this.getPailEntries();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Storacha workspace sync
|
|
3
|
+
*/
|
|
4
|
+
import type { UnknownLink } from "multiformats";
|
|
5
|
+
import type { Block } from "multiformats";
|
|
6
|
+
import type { CID } from "multiformats/cid";
|
|
7
|
+
/** Plugin configuration (from openclaw.plugin.json schema) */
|
|
8
|
+
export interface SyncPluginConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
watchPatterns: string[];
|
|
11
|
+
ignorePatterns: string[];
|
|
12
|
+
}
|
|
13
|
+
/** Stored in .storacha/config.json — device-specific, not synced */
|
|
14
|
+
export interface DeviceConfig {
|
|
15
|
+
/** Ed25519 agent private key (base64) */
|
|
16
|
+
agentKey: string;
|
|
17
|
+
/** UCN name archive (base64 CAR) */
|
|
18
|
+
nameArchive?: string;
|
|
19
|
+
/** Space → agent upload delegation (base64 archive) */
|
|
20
|
+
uploadDelegation?: string;
|
|
21
|
+
/** Name → agent delegation for pail sync (base64 archive) */
|
|
22
|
+
nameDelegation?: string;
|
|
23
|
+
/** Space DID extracted from upload delegation */
|
|
24
|
+
spaceDID?: string;
|
|
25
|
+
/** Whether setup is complete (watcher won't start without this) */
|
|
26
|
+
setupComplete?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Current sync state */
|
|
29
|
+
export interface SyncState {
|
|
30
|
+
/** Is sync currently running? */
|
|
31
|
+
running: boolean;
|
|
32
|
+
/** Last successful sync timestamp */
|
|
33
|
+
lastSync: number | null;
|
|
34
|
+
/** Current pail root CID */
|
|
35
|
+
root: UnknownLink | null;
|
|
36
|
+
/** Number of tracked entries */
|
|
37
|
+
entryCount: number;
|
|
38
|
+
/** Pending local changes not yet pushed */
|
|
39
|
+
pendingChanges: number;
|
|
40
|
+
}
|
|
41
|
+
/** File change event from watcher */
|
|
42
|
+
export interface FileChange {
|
|
43
|
+
type: "add" | "change" | "unlink";
|
|
44
|
+
path: string;
|
|
45
|
+
}
|
|
46
|
+
/** Encoded file result */
|
|
47
|
+
export interface EncodedFile {
|
|
48
|
+
path: string;
|
|
49
|
+
blocks: ReadableStream<Block>;
|
|
50
|
+
size: number;
|
|
51
|
+
}
|
|
52
|
+
/** Diff operation for pail */
|
|
53
|
+
export interface PailOp {
|
|
54
|
+
type: "put" | "del";
|
|
55
|
+
key: string;
|
|
56
|
+
value?: CID;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a @storacha/client instance from an agent private key and upload delegation.
|
|
3
|
+
*
|
|
4
|
+
* Uses the top-level `create()` factory which accepts principal + store directly,
|
|
5
|
+
* avoiding internal @storacha/access imports.
|
|
6
|
+
*/
|
|
7
|
+
import type { DeviceConfig } from "../types/index.js";
|
|
8
|
+
import type { Client } from "@storacha/client";
|
|
9
|
+
/**
|
|
10
|
+
* Build a Storacha Client from device config.
|
|
11
|
+
* Requires agentKey and uploadDelegation to be present.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createStorachaClient(config: DeviceConfig): Promise<Client>;
|
|
14
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/utils/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,MAAM,CAAC,CA8BjB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a @storacha/client instance from an agent private key and upload delegation.
|
|
3
|
+
*
|
|
4
|
+
* Uses the top-level `create()` factory which accepts principal + store directly,
|
|
5
|
+
* avoiding internal @storacha/access imports.
|
|
6
|
+
*/
|
|
7
|
+
import { Agent as PailAgent } from "@storacha/ucn/pail";
|
|
8
|
+
import { create as createClient } from "@storacha/client";
|
|
9
|
+
import { StoreMemory } from "@storacha/client/stores/memory";
|
|
10
|
+
import { extract } from "@storacha/client/delegation";
|
|
11
|
+
/**
|
|
12
|
+
* Build a Storacha Client from device config.
|
|
13
|
+
* Requires agentKey and uploadDelegation to be present.
|
|
14
|
+
*/
|
|
15
|
+
export async function createStorachaClient(config) {
|
|
16
|
+
if (!config.uploadDelegation) {
|
|
17
|
+
throw new Error("No upload delegation in device config");
|
|
18
|
+
}
|
|
19
|
+
const agent = PailAgent.parse(config.agentKey);
|
|
20
|
+
const client = await createClient({
|
|
21
|
+
principal: agent,
|
|
22
|
+
store: new StoreMemory(),
|
|
23
|
+
});
|
|
24
|
+
// Import the upload delegation (space → agent)
|
|
25
|
+
const uploadBytes = Buffer.from(config.uploadDelegation, "base64");
|
|
26
|
+
const { ok: uploadDelegation, error: uploadErr } = await extract(uploadBytes);
|
|
27
|
+
if (!uploadDelegation) {
|
|
28
|
+
throw new Error(`Failed to extract upload delegation: ${uploadErr}`);
|
|
29
|
+
}
|
|
30
|
+
// addSpace registers the space and adds the delegation as a proof
|
|
31
|
+
await client.addSpace(uploadDelegation);
|
|
32
|
+
// Set the space as current so uploads target it
|
|
33
|
+
const spaceDID = uploadDelegation.capabilities[0]?.with;
|
|
34
|
+
if (spaceDID) {
|
|
35
|
+
await client.setCurrentSpace(spaceDID);
|
|
36
|
+
}
|
|
37
|
+
return client;
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Differ - compares local directory tree with pail entries
|
|
3
|
+
*
|
|
4
|
+
* Generates put/del operations to sync local state to pail.
|
|
5
|
+
*/
|
|
6
|
+
import type { CID } from "multiformats/cid";
|
|
7
|
+
import type { PailOp } from "../types/index.js";
|
|
8
|
+
/** Map of path → CID from pail entries */
|
|
9
|
+
export type PailEntries = Map<string, CID>;
|
|
10
|
+
/** Map of path → CID from local encoded files */
|
|
11
|
+
export type LocalEntries = Map<string, CID>;
|
|
12
|
+
/**
|
|
13
|
+
* Compute diff between local files and pail entries
|
|
14
|
+
*
|
|
15
|
+
* @param local - Encoded local files (path → rootCID)
|
|
16
|
+
* @param pail - Current pail entries (path → CID)
|
|
17
|
+
* @returns Operations to apply to pail
|
|
18
|
+
*/
|
|
19
|
+
export declare function diffEntries(local: LocalEntries, pail: PailEntries): PailOp[];
|
|
20
|
+
/**
|
|
21
|
+
* Diff two pail states to find files that changed remotely
|
|
22
|
+
* (Used after publish to determine what to download)
|
|
23
|
+
*
|
|
24
|
+
* @param before - Pail entries before publish
|
|
25
|
+
* @param after - Pail entries after publish (may include remote changes)
|
|
26
|
+
* @returns Paths that changed remotely (need to download)
|
|
27
|
+
*/
|
|
28
|
+
export declare function diffRemoteChanges(before: PailEntries, after: PailEntries): string[];
|
|
29
|
+
//# sourceMappingURL=differ.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../src/utils/differ.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,0CAA0C;AAC1C,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE3C,iDAAiD;AACjD,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE5C;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,CAmB5E;AAGD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,WAAW,GACjB,MAAM,EAAE,CAWV"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Differ - compares local directory tree with pail entries
|
|
3
|
+
*
|
|
4
|
+
* Generates put/del operations to sync local state to pail.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Compute diff between local files and pail entries
|
|
8
|
+
*
|
|
9
|
+
* @param local - Encoded local files (path → rootCID)
|
|
10
|
+
* @param pail - Current pail entries (path → CID)
|
|
11
|
+
* @returns Operations to apply to pail
|
|
12
|
+
*/
|
|
13
|
+
export function diffEntries(local, pail) {
|
|
14
|
+
const ops = [];
|
|
15
|
+
// Find puts: files in local that are new or changed
|
|
16
|
+
for (const [path, localCID] of local) {
|
|
17
|
+
const pailCID = pail.get(path);
|
|
18
|
+
if (!pailCID || !localCID.equals(pailCID)) {
|
|
19
|
+
ops.push({ type: "put", key: path, value: localCID });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Find deletes: files in pail that aren't in local
|
|
23
|
+
for (const path of pail.keys()) {
|
|
24
|
+
if (!local.has(path)) {
|
|
25
|
+
ops.push({ type: "del", key: path });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return ops;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Diff two pail states to find files that changed remotely
|
|
32
|
+
* (Used after publish to determine what to download)
|
|
33
|
+
*
|
|
34
|
+
* @param before - Pail entries before publish
|
|
35
|
+
* @param after - Pail entries after publish (may include remote changes)
|
|
36
|
+
* @returns Paths that changed remotely (need to download)
|
|
37
|
+
*/
|
|
38
|
+
export function diffRemoteChanges(before, after) {
|
|
39
|
+
const changed = [];
|
|
40
|
+
for (const [path, afterCID] of after) {
|
|
41
|
+
const beforeCID = before.get(path);
|
|
42
|
+
if (!beforeCID || !afterCID.equals(beforeCID)) {
|
|
43
|
+
changed.push(path);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return changed;
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File encoder - converts files to UnixFS DAG with root CID
|
|
3
|
+
*
|
|
4
|
+
* Uses @storacha/upload-client's UnixFS encoding to generate
|
|
5
|
+
* content-addressed blocks for each file.
|
|
6
|
+
*/
|
|
7
|
+
import type { EncodedFile } from "../types/index.js";
|
|
8
|
+
/**
|
|
9
|
+
* Encode a single file to UnixFS blocks
|
|
10
|
+
*/
|
|
11
|
+
export declare function encodeWorkspaceFile(workspacePath: string, relativePath: string): Promise<EncodedFile>;
|
|
12
|
+
/**
|
|
13
|
+
* Encode multiple files, returning all encoded results
|
|
14
|
+
*/
|
|
15
|
+
export declare function encodeFiles(workspacePath: string, relativePaths: string[]): Promise<EncodedFile[]>;
|
|
16
|
+
//# sourceMappingURL=encoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoder.d.ts","sourceRoot":"","sources":["../../src/utils/encoder.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,WAAW,CAAC,CAkBtB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EAAE,GACtB,OAAO,CAAC,WAAW,EAAE,CAAC,CAkBxB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File encoder - converts files to UnixFS DAG with root CID
|
|
3
|
+
*
|
|
4
|
+
* Uses @storacha/upload-client's UnixFS encoding to generate
|
|
5
|
+
* content-addressed blocks for each file.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import * as stream from "node:stream";
|
|
10
|
+
import { createFileEncoderStream } from "@storacha/upload-client/unixfs";
|
|
11
|
+
/**
|
|
12
|
+
* Encode a single file to UnixFS blocks
|
|
13
|
+
*/
|
|
14
|
+
export async function encodeWorkspaceFile(workspacePath, relativePath) {
|
|
15
|
+
const fullPath = path.join(workspacePath, relativePath);
|
|
16
|
+
const fileHandle = await fs.open(fullPath);
|
|
17
|
+
const stat = await fs.stat(fullPath);
|
|
18
|
+
// Encode to UnixFS - returns { cid, blocks }
|
|
19
|
+
const blocks = createFileEncoderStream({
|
|
20
|
+
// @ts-expect-error node web stream not type compatible with web stream
|
|
21
|
+
stream() {
|
|
22
|
+
return stream.Readable.toWeb(fileHandle.createReadStream());
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
path: relativePath,
|
|
27
|
+
size: stat.size,
|
|
28
|
+
blocks: blocks,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Encode multiple files, returning all encoded results
|
|
33
|
+
*/
|
|
34
|
+
export async function encodeFiles(workspacePath, relativePaths) {
|
|
35
|
+
const results = [];
|
|
36
|
+
for (const relativePath of relativePaths) {
|
|
37
|
+
try {
|
|
38
|
+
const encoded = await encodeWorkspaceFile(workspacePath, relativePath);
|
|
39
|
+
results.push(encoded);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (err.code === "ENOENT") {
|
|
43
|
+
// File was deleted between detection and encoding - skip
|
|
44
|
+
console.warn(`File not found during encoding: ${relativePath}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Block } from "multiformats";
|
|
2
|
+
import type { BlobLike } from "@storacha/client/types";
|
|
3
|
+
export interface WritableCar {
|
|
4
|
+
/**
|
|
5
|
+
* Write a block to the CAR file.
|
|
6
|
+
* Must not be called after close/switchToReadable.
|
|
7
|
+
*/
|
|
8
|
+
put(block: Block): Promise<void>;
|
|
9
|
+
cleanup(): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Close the writer and return a readable for upload.
|
|
12
|
+
* Returns null if nothing was written.
|
|
13
|
+
*/
|
|
14
|
+
switchToReadable(): Promise<ReadableCar | null>;
|
|
15
|
+
}
|
|
16
|
+
interface ReadableCar {
|
|
17
|
+
readable: BlobLike;
|
|
18
|
+
cleanup(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare const makeTempCar: () => Promise<WritableCar>;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=tempcar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tempcar.d.ts","sourceRoot":"","sources":["../../src/utils/tempcar.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,GAAG,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB;;;OAGG;IACH,gBAAgB,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CACjD;AAED,UAAU,WAAW;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,WAAW,CAkDvD,CAAC"}
|