Skip to content

Commit 63a0b67

Browse files
author
Developer
committed
fix: resolve Advanced CID API integration issues
Fixed two critical bugs discovered during real S5 portal testing: 1. CID-only storage path resolution - putByCID now stores files in home/.cid/ (valid S5 path) - cidToPath second pass now searches home/.cid instead of .cid - Fixes "CID not found" error for CID-only stored data 2. Encryption metadata deserialization - CBOR converts plain objects to Maps during encoding - Updated FS5.get to handle Map format for encryption metadata - Now correctly accesses algorithm and key via .get() method - Fixes "Unsupported encryption algorithm: undefined" error Added comprehensive integration tests: - test/fs/fs5-advanced.integration.test.ts (18 Vitest tests, skipped) - test/integration/test-advanced-cid-real.js (18 real portal tests) All 18 integration tests now pass with real S5 portal (s5.vup.cx).
1 parent f851d0c commit 63a0b67

File tree

4 files changed

+816
-6
lines changed

4 files changed

+816
-6
lines changed

src/fs/fs5-advanced.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export class FS5Advanced {
139139

140140
// Second pass: if not found, search .cid directory only
141141
if (!foundPath) {
142-
foundPath = await this._searchForCID(cid, '.cid', false);
142+
foundPath = await this._searchForCID(cid, 'home/.cid', false);
143143
}
144144

145145
return foundPath;
@@ -192,10 +192,10 @@ export class FS5Advanced {
192192
*/
193193
async putByCID(data: any): Promise<Uint8Array> {
194194
// Generate a temporary unique path for CID-only storage
195-
// Use a special .cid/ directory to avoid conflicts
195+
// Use home/.cid/ directory (paths must start with home/ or archive/)
196196
const timestamp = Date.now();
197197
const random = Math.random().toString(36).substring(2, 15);
198-
const tempPath = `.cid/${timestamp}-${random}`;
198+
const tempPath = `home/.cid/${timestamp}-${random}`;
199199

200200
// Store the data
201201
await this.fs5.put(tempPath, data);

src/fs/fs5.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,17 +180,20 @@ export class FS5 {
180180
let data: Uint8Array;
181181
if (fileRef.extra && fileRef.extra.has('encryption')) {
182182
const encryptionMeta = fileRef.extra.get('encryption');
183-
if (encryptionMeta && encryptionMeta.algorithm === 'xchacha20-poly1305') {
183+
// encryptionMeta is a Map after CBOR deserialization
184+
const algorithm = encryptionMeta instanceof Map ? encryptionMeta.get('algorithm') : encryptionMeta?.algorithm;
185+
if (algorithm === 'xchacha20-poly1305') {
184186
// Convert array back to Uint8Array
185-
const encryptionKey = new Uint8Array(encryptionMeta.key);
187+
const keyData = encryptionMeta instanceof Map ? encryptionMeta.get('key') : encryptionMeta.key;
188+
const encryptionKey = new Uint8Array(keyData);
186189
// Download and decrypt
187190
data = await this.downloadAndDecryptBlob(
188191
fileRef.hash,
189192
encryptionKey,
190193
Number(fileRef.size)
191194
);
192195
} else {
193-
throw new Error(`Unsupported encryption algorithm: ${encryptionMeta?.algorithm}`);
196+
throw new Error(`Unsupported encryption algorithm: ${algorithm}`);
194197
}
195198
} else {
196199
// Download unencrypted file data
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
import { describe, it, expect, beforeEach } from 'vitest';
2+
import { S5 } from '../../src/index.js';
3+
import { FS5Advanced } from '../../src/fs/fs5-advanced.js';
4+
import { formatCID, parseCID } from '../../src/fs/cid-utils.js';
5+
import WebSocket from 'ws';
6+
7+
// Polyfill WebSocket for Node.js environment
8+
if (!global.WebSocket) {
9+
global.WebSocket = WebSocket as any;
10+
}
11+
12+
// These integration tests use a REAL S5 instance with actual storage
13+
// Unlike the unit tests which mock FS5 internals, these tests verify
14+
// that the Advanced CID API works with real IndexedDB/memory-level and registry operations
15+
//
16+
// ⚠️ IMPORTANT: Real S5 portal testing is better suited for standalone scripts
17+
// due to registry propagation delays, network timing, and test isolation challenges.
18+
//
19+
// For comprehensive Advanced CID API testing with real S5 portals, use:
20+
// node test/integration/test-advanced-cid-real.js
21+
//
22+
// This standalone script properly handles:
23+
// - Portal registration and authentication
24+
// - Registry propagation delays between operations (5+ seconds)
25+
// - Sequential execution with concurrency: 1 to avoid registry conflicts
26+
// - All integration scenarios:
27+
// • putWithCID and dual retrieval (path + CID)
28+
// • pathToCID extraction from stored files
29+
// • cidToPath lookup and verification
30+
// • getByCID without path knowledge
31+
// • CID consistency and verification
32+
// • Integration with encryption
33+
//
34+
// The vitest tests below are SKIPPED for automated CI and kept for reference.
35+
36+
describe.skip('FS5Advanced Integration Tests', () => {
37+
let s5: S5;
38+
let advanced: FS5Advanced;
39+
let testPath: string;
40+
41+
beforeEach(async () => {
42+
// Create S5 instance with in-memory storage
43+
s5 = await S5.create({});
44+
45+
// Generate and recover identity
46+
const seedPhrase = s5.generateSeedPhrase();
47+
await s5.recoverIdentityFromSeedPhrase(seedPhrase);
48+
await s5.fs.ensureIdentityInitialized();
49+
50+
// Create Advanced API instance
51+
advanced = new FS5Advanced(s5.fs);
52+
53+
// Use unique path for each test
54+
testPath = `home/test-${Date.now()}.txt`;
55+
});
56+
57+
describe('putWithCID Integration', () => {
58+
it('should store data and return both path and CID', async () => {
59+
const testData = 'Integration test data';
60+
61+
const result = await advanced.putWithCID(testPath, testData);
62+
63+
expect(result.path).toBe(testPath);
64+
expect(result.cid).toBeInstanceOf(Uint8Array);
65+
expect(result.cid.length).toBe(32);
66+
67+
// Verify we can retrieve by path
68+
const byPath = await s5.fs.get(testPath);
69+
expect(byPath).toBe(testData);
70+
71+
// Verify we can retrieve by CID
72+
const byCID = await advanced.getByCID(result.cid);
73+
expect(byCID).toBe(testData);
74+
});
75+
76+
it('should work with binary data', async () => {
77+
const binaryData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
78+
79+
const result = await advanced.putWithCID(testPath, binaryData);
80+
81+
const retrieved = await advanced.getByCID(result.cid);
82+
expect(retrieved).toEqual(binaryData);
83+
});
84+
85+
it('should work with JSON data', async () => {
86+
const jsonData = { key: 'value', nested: { data: 123 } };
87+
88+
const result = await advanced.putWithCID(testPath, jsonData);
89+
90+
const retrieved = await advanced.getByCID(result.cid);
91+
expect(retrieved).toEqual(jsonData);
92+
});
93+
});
94+
95+
describe('pathToCID Integration', () => {
96+
it('should extract CID from stored file', async () => {
97+
const testData = 'Extract CID test';
98+
await s5.fs.put(testPath, testData);
99+
100+
const cid = await advanced.pathToCID(testPath);
101+
102+
expect(cid).toBeInstanceOf(Uint8Array);
103+
expect(cid.length).toBe(32);
104+
105+
// Verify CID works for retrieval
106+
const retrieved = await advanced.getByCID(cid);
107+
expect(retrieved).toBe(testData);
108+
});
109+
110+
it('should extract CID from directory', async () => {
111+
const dirPath = 'home/testdir';
112+
await s5.fs.put(`${dirPath}/file.txt`, 'content');
113+
114+
const cid = await advanced.pathToCID(dirPath);
115+
116+
expect(cid).toBeInstanceOf(Uint8Array);
117+
expect(cid.length).toBe(32);
118+
});
119+
120+
it('should return consistent CID for same content', async () => {
121+
const content = 'Consistent content';
122+
const path1 = 'home/file1.txt';
123+
const path2 = 'home/file2.txt';
124+
125+
await s5.fs.put(path1, content);
126+
await s5.fs.put(path2, content);
127+
128+
const cid1 = await advanced.pathToCID(path1);
129+
const cid2 = await advanced.pathToCID(path2);
130+
131+
// Same content should have same CID
132+
expect(cid1).toEqual(cid2);
133+
});
134+
});
135+
136+
describe('cidToPath Integration', () => {
137+
it('should find path from CID', async () => {
138+
const testData = 'Find path test';
139+
await s5.fs.put(testPath, testData);
140+
141+
const cid = await advanced.pathToCID(testPath);
142+
const foundPath = await advanced.cidToPath(cid);
143+
144+
expect(foundPath).toBe(testPath);
145+
});
146+
147+
it('should return null for unknown CID', async () => {
148+
const unknownCID = new Uint8Array(32).fill(99);
149+
150+
const foundPath = await advanced.cidToPath(unknownCID);
151+
152+
expect(foundPath).toBeNull();
153+
});
154+
155+
it('should prefer user paths over .cid paths', async () => {
156+
const testData = 'Preference test';
157+
const userPath = 'home/userfile.txt';
158+
159+
// Store at user path
160+
const result = await advanced.putWithCID(userPath, testData);
161+
162+
// Also store via putByCID (creates .cid/ path)
163+
await advanced.putByCID(testData);
164+
165+
// cidToPath should return user path, not .cid/ path
166+
const foundPath = await advanced.cidToPath(result.cid);
167+
168+
expect(foundPath).toBe(userPath);
169+
expect(foundPath).not.toContain('.cid/');
170+
});
171+
});
172+
173+
describe('getByCID Integration', () => {
174+
it('should retrieve data without knowing path', async () => {
175+
const testData = 'Retrieve by CID test';
176+
const result = await advanced.putWithCID(testPath, testData);
177+
178+
// Retrieve without using path
179+
const retrieved = await advanced.getByCID(result.cid);
180+
181+
expect(retrieved).toBe(testData);
182+
});
183+
184+
it('should throw error for non-existent CID', async () => {
185+
const nonExistentCID = new Uint8Array(32).fill(255);
186+
187+
await expect(advanced.getByCID(nonExistentCID)).rejects.toThrow('CID not found');
188+
});
189+
});
190+
191+
describe('getMetadataWithCID Integration', () => {
192+
it('should return metadata and CID for file', async () => {
193+
const testData = 'Metadata test';
194+
await s5.fs.put(testPath, testData);
195+
196+
const result = await advanced.getMetadataWithCID(testPath);
197+
198+
expect(result.metadata).toBeDefined();
199+
expect(result.metadata.type).toBe('file');
200+
expect(result.metadata.size).toBeGreaterThan(0);
201+
expect(result.cid).toBeInstanceOf(Uint8Array);
202+
expect(result.cid.length).toBe(32);
203+
});
204+
205+
it('should return metadata and CID for directory', async () => {
206+
const dirPath = 'home/metadir';
207+
await s5.fs.put(`${dirPath}/file.txt`, 'content');
208+
209+
const result = await advanced.getMetadataWithCID(dirPath);
210+
211+
expect(result.metadata).toBeDefined();
212+
expect(result.metadata.type).toBe('directory');
213+
expect(result.cid).toBeInstanceOf(Uint8Array);
214+
expect(result.cid.length).toBe(32);
215+
});
216+
});
217+
218+
describe('CID Utilities Integration', () => {
219+
it('should format and parse CID correctly', async () => {
220+
const testData = 'Format parse test';
221+
const result = await advanced.putWithCID(testPath, testData);
222+
223+
// Format CID
224+
const formatted = formatCID(result.cid, 'base32');
225+
expect(formatted).toBeTypeOf('string');
226+
expect(formatted.length).toBeGreaterThan(0);
227+
228+
// Parse it back
229+
const parsed = parseCID(formatted);
230+
expect(parsed).toEqual(result.cid);
231+
232+
// Should be able to retrieve with parsed CID
233+
const retrieved = await advanced.getByCID(parsed);
234+
expect(retrieved).toBe(testData);
235+
});
236+
237+
it('should work with different encoding formats', async () => {
238+
const result = await advanced.putWithCID(testPath, 'Encoding test');
239+
240+
// Test all three encodings
241+
const base32 = formatCID(result.cid, 'base32');
242+
const base58 = formatCID(result.cid, 'base58btc');
243+
const base64 = formatCID(result.cid, 'base64');
244+
245+
// All should parse back to same CID
246+
expect(parseCID(base32)).toEqual(result.cid);
247+
expect(parseCID(base58)).toEqual(result.cid);
248+
expect(parseCID(base64)).toEqual(result.cid);
249+
});
250+
});
251+
252+
describe('Encryption Integration', () => {
253+
it('should handle encrypted files with CID operations', async () => {
254+
const sensitiveData = 'Secret information';
255+
256+
// Store with encryption
257+
const result = await advanced.putWithCID(testPath, sensitiveData, {
258+
encryption: { algorithm: 'xchacha20-poly1305' },
259+
});
260+
261+
expect(result.cid).toBeInstanceOf(Uint8Array);
262+
263+
// Should be able to retrieve by CID (will auto-decrypt)
264+
const retrieved = await advanced.getByCID(result.cid);
265+
expect(retrieved).toBe(sensitiveData);
266+
267+
// Should find path from CID
268+
const foundPath = await advanced.cidToPath(result.cid);
269+
expect(foundPath).toBe(testPath);
270+
});
271+
272+
it('should have different CIDs for same content with different encryption', async () => {
273+
const content = 'Same content, different encryption';
274+
const path1 = 'home/encrypted1.txt';
275+
const path2 = 'home/encrypted2.txt';
276+
277+
// Store with different encryption keys
278+
const result1 = await advanced.putWithCID(path1, content, {
279+
encryption: { algorithm: 'xchacha20-poly1305' }
280+
});
281+
const result2 = await advanced.putWithCID(path2, content, {
282+
encryption: { algorithm: 'xchacha20-poly1305' }
283+
});
284+
285+
// Encrypted files should have different CIDs (different keys = different ciphertext)
286+
expect(result1.cid).not.toEqual(result2.cid);
287+
});
288+
});
289+
290+
describe('End-to-End Workflow', () => {
291+
it('should support complete CID-based workflow', async () => {
292+
const originalData = 'Complete workflow test';
293+
294+
// 1. Store data and get CID
295+
const { path, cid } = await advanced.putWithCID(testPath, originalData);
296+
297+
// 2. Format CID for sharing
298+
const cidString = formatCID(cid, 'base58btc');
299+
300+
// 3. Recipient: parse CID from string
301+
const receivedCID = parseCID(cidString);
302+
303+
// 4. Recipient: retrieve data by CID
304+
const retrievedData = await advanced.getByCID(receivedCID);
305+
expect(retrievedData).toBe(originalData);
306+
307+
// 5. Recipient: find path from CID
308+
const foundPath = await advanced.cidToPath(receivedCID);
309+
expect(foundPath).toBe(path);
310+
311+
// 6. Verify metadata includes CID
312+
if (foundPath) {
313+
const metadata = await advanced.getMetadataWithCID(foundPath);
314+
expect(metadata.cid).toEqual(cid);
315+
}
316+
});
317+
});
318+
});

0 commit comments

Comments
 (0)