@storacha/encrypt-upload-client 1.1.80 → 1.1.82
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/crypto/adapters/kms-crypto-adapter.d.ts +4 -10
- package/dist/crypto/adapters/kms-crypto-adapter.d.ts.map +1 -1
- package/dist/crypto/adapters/kms-crypto-adapter.js +4 -4
- package/dist/examples/decrypt-test.d.ts +2 -0
- package/dist/examples/decrypt-test.d.ts.map +1 -0
- package/dist/examples/decrypt-test.js +102 -0
- package/dist/examples/encrypt-test.d.ts +2 -0
- package/dist/examples/encrypt-test.d.ts.map +1 -0
- package/dist/examples/encrypt-test.js +93 -0
- package/dist/test/cid-verification.spec.d.ts +2 -0
- package/dist/test/cid-verification.spec.d.ts.map +1 -0
- package/dist/test/cid-verification.spec.js +314 -0
- package/dist/test/crypto-compatibility.spec.d.ts +2 -0
- package/dist/test/crypto-compatibility.spec.d.ts.map +1 -0
- package/dist/test/crypto-compatibility.spec.js +124 -0
- package/dist/test/crypto-counter-security.spec.d.ts +2 -0
- package/dist/test/crypto-counter-security.spec.d.ts.map +1 -0
- package/dist/test/crypto-counter-security.spec.js +147 -0
- package/dist/test/crypto-streaming.spec.d.ts +2 -0
- package/dist/test/crypto-streaming.spec.d.ts.map +1 -0
- package/dist/test/crypto-streaming.spec.js +129 -0
- package/dist/test/encrypted-metadata.spec.d.ts +2 -0
- package/dist/test/encrypted-metadata.spec.d.ts.map +1 -0
- package/dist/test/encrypted-metadata.spec.js +68 -0
- package/dist/test/factories.spec.d.ts +2 -0
- package/dist/test/factories.spec.d.ts.map +1 -0
- package/dist/test/factories.spec.js +129 -0
- package/dist/test/file-metadata.spec.d.ts +2 -0
- package/dist/test/file-metadata.spec.d.ts.map +1 -0
- package/dist/test/file-metadata.spec.js +433 -0
- package/dist/test/fixtures/test-fixtures.d.ts +28 -0
- package/dist/test/fixtures/test-fixtures.d.ts.map +1 -0
- package/dist/test/fixtures/test-fixtures.js +63 -0
- package/dist/test/helpers/test-file-utils.d.ts +60 -0
- package/dist/test/helpers/test-file-utils.d.ts.map +1 -0
- package/dist/test/helpers/test-file-utils.js +139 -0
- package/dist/test/https-enforcement.spec.d.ts +2 -0
- package/dist/test/https-enforcement.spec.d.ts.map +1 -0
- package/dist/test/https-enforcement.spec.js +125 -0
- package/dist/test/kms-crypto-adapter.spec.d.ts +2 -0
- package/dist/test/kms-crypto-adapter.spec.d.ts.map +1 -0
- package/dist/test/kms-crypto-adapter.spec.js +305 -0
- package/dist/test/lit-crypto-adapter.spec.d.ts +2 -0
- package/dist/test/lit-crypto-adapter.spec.d.ts.map +1 -0
- package/dist/test/lit-crypto-adapter.spec.js +76 -0
- package/dist/test/memory-efficiency.spec.d.ts +2 -0
- package/dist/test/memory-efficiency.spec.d.ts.map +1 -0
- package/dist/test/memory-efficiency.spec.js +93 -0
- package/dist/test/mocks/key-manager.d.ts +58 -0
- package/dist/test/mocks/key-manager.d.ts.map +1 -0
- package/dist/test/mocks/key-manager.js +137 -0
- package/dist/test/node-crypto-adapter.spec.d.ts +2 -0
- package/dist/test/node-crypto-adapter.spec.d.ts.map +1 -0
- package/dist/test/node-crypto-adapter.spec.js +103 -0
- package/dist/test/node-generic-crypto-adapter.spec.d.ts +2 -0
- package/dist/test/node-generic-crypto-adapter.spec.d.ts.map +1 -0
- package/dist/test/node-generic-crypto-adapter.spec.js +95 -0
- package/dist/test/setup.d.ts +2 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/setup.js +12 -0
- package/dist/tsconfig.spec.tsbuildinfo +1 -0
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
import { createFileWithMetadata, extractFileMetadata, } from '../src/utils/file-metadata.js';
|
|
4
|
+
/**
|
|
5
|
+
* Create a mock file for testing
|
|
6
|
+
*
|
|
7
|
+
* @param {string} content - The file content
|
|
8
|
+
* @param {string} filename - The filename
|
|
9
|
+
* @returns {Blob} Mock file blob
|
|
10
|
+
*/
|
|
11
|
+
const createMockFile = (content = 'test file content', filename = 'test.txt') => {
|
|
12
|
+
return new Blob([content], { type: 'text/plain' });
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Convert stream to array buffer for testing
|
|
16
|
+
*
|
|
17
|
+
* @param {ReadableStream} stream - The stream to convert
|
|
18
|
+
* @returns {Promise<ArrayBuffer>} The converted array buffer
|
|
19
|
+
*/
|
|
20
|
+
const streamToArrayBuffer = async (stream) => {
|
|
21
|
+
const reader = stream.getReader();
|
|
22
|
+
const chunks = [];
|
|
23
|
+
let done = false;
|
|
24
|
+
while (!done) {
|
|
25
|
+
const result = await reader.read();
|
|
26
|
+
done = result.done;
|
|
27
|
+
if (!done) {
|
|
28
|
+
chunks.push(result.value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
32
|
+
const result = new Uint8Array(totalLength);
|
|
33
|
+
let offset = 0;
|
|
34
|
+
for (const chunk of chunks) {
|
|
35
|
+
result.set(chunk, offset);
|
|
36
|
+
offset += chunk.length;
|
|
37
|
+
}
|
|
38
|
+
return result.buffer;
|
|
39
|
+
};
|
|
40
|
+
await describe('File Metadata Utils', async () => {
|
|
41
|
+
await describe('createFileWithMetadata', async () => {
|
|
42
|
+
await it('should create file without metadata', () => {
|
|
43
|
+
const originalFile = createMockFile();
|
|
44
|
+
const result = createFileWithMetadata(originalFile);
|
|
45
|
+
assert(result instanceof Blob);
|
|
46
|
+
assert.equal(result.size, originalFile.size);
|
|
47
|
+
});
|
|
48
|
+
await it('should create file with metadata', () => {
|
|
49
|
+
const originalFile = createMockFile();
|
|
50
|
+
const metadata = {
|
|
51
|
+
name: 'test.txt',
|
|
52
|
+
type: 'text/plain',
|
|
53
|
+
extension: 'txt',
|
|
54
|
+
};
|
|
55
|
+
const result = createFileWithMetadata(originalFile, metadata);
|
|
56
|
+
assert(result instanceof Blob);
|
|
57
|
+
assert.equal(result.size, originalFile.size + 1024); // Original file + 1KB header
|
|
58
|
+
});
|
|
59
|
+
await it('should reject metadata that is too large', () => {
|
|
60
|
+
const originalFile = createMockFile();
|
|
61
|
+
const largeMetadata = {
|
|
62
|
+
name: 'test.txt',
|
|
63
|
+
type: 'text/plain',
|
|
64
|
+
extension: 'txt',
|
|
65
|
+
metadata: {
|
|
66
|
+
description: 'a'.repeat(1000), // Very long description
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
assert.throws(() => createFileWithMetadata(originalFile, largeMetadata), /Metadata too large/);
|
|
70
|
+
});
|
|
71
|
+
await it('should handle empty files', () => {
|
|
72
|
+
const emptyFile = new Blob(['']);
|
|
73
|
+
const metadata = {
|
|
74
|
+
name: 'empty.txt',
|
|
75
|
+
type: 'text/plain',
|
|
76
|
+
extension: 'txt',
|
|
77
|
+
};
|
|
78
|
+
const result = createFileWithMetadata(emptyFile, metadata);
|
|
79
|
+
assert.equal(result.size, 1024); // Just the header
|
|
80
|
+
});
|
|
81
|
+
await it('should handle binary files', () => {
|
|
82
|
+
const binaryData = new Uint8Array([
|
|
83
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
84
|
+
]); // PNG header
|
|
85
|
+
const binaryFile = new Blob([binaryData], { type: 'image/png' });
|
|
86
|
+
const metadata = {
|
|
87
|
+
name: 'image.png',
|
|
88
|
+
type: 'image/png',
|
|
89
|
+
extension: 'png',
|
|
90
|
+
};
|
|
91
|
+
const result = createFileWithMetadata(binaryFile, metadata);
|
|
92
|
+
assert.equal(result.size, binaryData.length + 1024);
|
|
93
|
+
});
|
|
94
|
+
await it('should handle international characters in metadata', () => {
|
|
95
|
+
const originalFile = createMockFile();
|
|
96
|
+
const metadata = {
|
|
97
|
+
name: '测试文件.txt', // Chinese characters
|
|
98
|
+
type: 'text/plain',
|
|
99
|
+
extension: 'txt',
|
|
100
|
+
metadata: {
|
|
101
|
+
author: '作者',
|
|
102
|
+
emoji: '🎉📁',
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
const result = createFileWithMetadata(originalFile, metadata);
|
|
106
|
+
assert(result instanceof Blob);
|
|
107
|
+
assert.equal(result.size, originalFile.size + 1024);
|
|
108
|
+
});
|
|
109
|
+
await it('should reject invalid metadata structure - null', () => {
|
|
110
|
+
const originalFile = createMockFile();
|
|
111
|
+
assert.throws(() => createFileWithMetadata(originalFile, /** @type {any} */ (null)), /Invalid metadata structure/);
|
|
112
|
+
});
|
|
113
|
+
await it('should reject invalid metadata structure - array', () => {
|
|
114
|
+
const originalFile = createMockFile();
|
|
115
|
+
assert.throws(() => createFileWithMetadata(originalFile,
|
|
116
|
+
/** @type {any} */ (['name', 'type'])), /Invalid metadata structure/);
|
|
117
|
+
});
|
|
118
|
+
await it('should reject missing required fields', () => {
|
|
119
|
+
const originalFile = createMockFile();
|
|
120
|
+
const incompleteMetadata = { name: 'test.txt' }; // Missing type and extension
|
|
121
|
+
assert.throws(() => createFileWithMetadata(originalFile,
|
|
122
|
+
/** @type {any} */ (incompleteMetadata)), /Invalid metadata structure/);
|
|
123
|
+
});
|
|
124
|
+
await it('should reject fields that are too long', () => {
|
|
125
|
+
const originalFile = createMockFile();
|
|
126
|
+
const longFieldMetadata = {
|
|
127
|
+
name: 'a'.repeat(250), // Exceeds MAX_FIELD_LENGTH
|
|
128
|
+
type: 'text/plain',
|
|
129
|
+
extension: 'txt',
|
|
130
|
+
};
|
|
131
|
+
assert.throws(() => createFileWithMetadata(originalFile, longFieldMetadata), /Metadata field too long/);
|
|
132
|
+
});
|
|
133
|
+
await it('should reject wrong field types', () => {
|
|
134
|
+
const originalFile = createMockFile();
|
|
135
|
+
const wrongTypeMetadata = {
|
|
136
|
+
name: 123, // Should be string
|
|
137
|
+
type: 'text/plain',
|
|
138
|
+
extension: 'txt',
|
|
139
|
+
};
|
|
140
|
+
assert.throws(() => createFileWithMetadata(originalFile,
|
|
141
|
+
/** @type {any} */ (wrongTypeMetadata)), /Invalid metadata structure/);
|
|
142
|
+
});
|
|
143
|
+
await it('should handle Uint8Array input without metadata', () => {
|
|
144
|
+
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
|
145
|
+
const result = createFileWithMetadata(/** @type {any} */ (data));
|
|
146
|
+
assert(result instanceof Blob);
|
|
147
|
+
assert.equal(result.size, data.length);
|
|
148
|
+
});
|
|
149
|
+
await it('should handle Uint8Array input with metadata', () => {
|
|
150
|
+
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
|
151
|
+
const metadata = {
|
|
152
|
+
name: 'data.bin',
|
|
153
|
+
type: 'application/octet-stream',
|
|
154
|
+
extension: 'bin',
|
|
155
|
+
};
|
|
156
|
+
const result = createFileWithMetadata(/** @type {any} */ (data), metadata);
|
|
157
|
+
assert(result instanceof Blob);
|
|
158
|
+
assert.equal(result.size, data.length + 1024); // Data + 1KB header
|
|
159
|
+
});
|
|
160
|
+
await it('should handle ArrayBuffer input without metadata', () => {
|
|
161
|
+
const buffer = new ArrayBuffer(10);
|
|
162
|
+
const result = createFileWithMetadata(/** @type {any} */ (buffer));
|
|
163
|
+
assert(result instanceof Blob);
|
|
164
|
+
assert.equal(result.size, buffer.byteLength);
|
|
165
|
+
});
|
|
166
|
+
await it('should handle ArrayBuffer input with metadata', () => {
|
|
167
|
+
const buffer = new ArrayBuffer(10);
|
|
168
|
+
const metadata = {
|
|
169
|
+
name: 'buffer.dat',
|
|
170
|
+
type: 'application/octet-stream',
|
|
171
|
+
extension: 'dat',
|
|
172
|
+
};
|
|
173
|
+
const result = createFileWithMetadata(
|
|
174
|
+
/** @type {any} */ (buffer), metadata);
|
|
175
|
+
assert(result instanceof Blob);
|
|
176
|
+
assert.equal(result.size, buffer.byteLength + 1024); // Buffer + 1KB header
|
|
177
|
+
});
|
|
178
|
+
await it('should throw error for unsupported BlobLike type', () => {
|
|
179
|
+
const unsupportedBlobLike = {
|
|
180
|
+
stream: () => new ReadableStream(),
|
|
181
|
+
size: 100,
|
|
182
|
+
};
|
|
183
|
+
assert.throws(() => createFileWithMetadata(/** @type {any} */ (unsupportedBlobLike)), /Unsupported BlobLike type - must be Blob, Uint8Array, or ArrayBuffer/);
|
|
184
|
+
});
|
|
185
|
+
await it('should throw error for unsupported BlobLike type with metadata', () => {
|
|
186
|
+
const unsupportedBlobLike = {
|
|
187
|
+
stream: () => new ReadableStream(),
|
|
188
|
+
size: 100,
|
|
189
|
+
};
|
|
190
|
+
const metadata = {
|
|
191
|
+
name: 'test.txt',
|
|
192
|
+
type: 'text/plain',
|
|
193
|
+
extension: 'txt',
|
|
194
|
+
};
|
|
195
|
+
assert.throws(() => createFileWithMetadata(
|
|
196
|
+
/** @type {any} */ (unsupportedBlobLike), metadata), /Unsupported BlobLike type - must be Blob, Uint8Array, or ArrayBuffer/);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
await describe('extractFileMetadata', async () => {
|
|
200
|
+
await it('should extract metadata from file with header', async () => {
|
|
201
|
+
const originalContent = 'test file content';
|
|
202
|
+
const originalFile = createMockFile(originalContent);
|
|
203
|
+
const metadata = {
|
|
204
|
+
name: 'document.pdf',
|
|
205
|
+
type: 'application/pdf',
|
|
206
|
+
extension: 'pdf',
|
|
207
|
+
};
|
|
208
|
+
// Create file with metadata
|
|
209
|
+
const fileWithMetadata = createFileWithMetadata(originalFile, metadata);
|
|
210
|
+
// Convert to stream (simulating decrypted stream)
|
|
211
|
+
const stream = fileWithMetadata.stream();
|
|
212
|
+
// Extract metadata
|
|
213
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
214
|
+
// Verify metadata was extracted correctly
|
|
215
|
+
assert.deepEqual(fileMetadata, metadata);
|
|
216
|
+
// Verify file content is preserved
|
|
217
|
+
const extractedContent = await streamToArrayBuffer(fileStream);
|
|
218
|
+
const originalArrayBuffer = await originalFile.arrayBuffer();
|
|
219
|
+
assert.deepEqual(new Uint8Array(extractedContent), new Uint8Array(originalArrayBuffer));
|
|
220
|
+
});
|
|
221
|
+
await it('should handle file without metadata', async () => {
|
|
222
|
+
const originalFile = createMockFile();
|
|
223
|
+
const stream = originalFile.stream();
|
|
224
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
225
|
+
assert.equal(fileMetadata, undefined);
|
|
226
|
+
assert(fileStream instanceof ReadableStream);
|
|
227
|
+
});
|
|
228
|
+
await it('should handle malformed metadata gracefully', async () => {
|
|
229
|
+
// Create a file with invalid header
|
|
230
|
+
const invalidHeader = new Uint8Array(1024);
|
|
231
|
+
invalidHeader[0] = 255; // Invalid length
|
|
232
|
+
invalidHeader[1] = 255;
|
|
233
|
+
invalidHeader[2] = 255;
|
|
234
|
+
invalidHeader[3] = 255;
|
|
235
|
+
const originalContent = 'test content';
|
|
236
|
+
const combined = new Blob([invalidHeader, originalContent]);
|
|
237
|
+
const stream = combined.stream();
|
|
238
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
239
|
+
// Should gracefully handle error and return stream without metadata
|
|
240
|
+
assert.equal(fileMetadata, undefined);
|
|
241
|
+
assert(fileStream instanceof ReadableStream);
|
|
242
|
+
});
|
|
243
|
+
// NEW EDGE CASE TESTS
|
|
244
|
+
await it('should handle empty streams', async () => {
|
|
245
|
+
const emptyBlob = new Blob([]);
|
|
246
|
+
const stream = emptyBlob.stream();
|
|
247
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
248
|
+
assert.equal(fileMetadata, undefined);
|
|
249
|
+
assert(fileStream instanceof ReadableStream);
|
|
250
|
+
});
|
|
251
|
+
await it('should handle streams smaller than header size', async () => {
|
|
252
|
+
const smallBlob = new Blob(['small']);
|
|
253
|
+
const stream = smallBlob.stream();
|
|
254
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
255
|
+
assert.equal(fileMetadata, undefined);
|
|
256
|
+
assert(fileStream instanceof ReadableStream);
|
|
257
|
+
});
|
|
258
|
+
await it('should handle malformed JSON in metadata', async () => {
|
|
259
|
+
const header = new Uint8Array(1024);
|
|
260
|
+
const malformedJson = '{name:"test",invalid}';
|
|
261
|
+
const jsonBytes = new TextEncoder().encode(malformedJson);
|
|
262
|
+
// Set valid length but invalid JSON
|
|
263
|
+
const lengthBytes = new Uint8Array(new Uint32Array([jsonBytes.length]).buffer);
|
|
264
|
+
header.set(lengthBytes, 0);
|
|
265
|
+
header.set(jsonBytes, 4);
|
|
266
|
+
const combined = new Blob([header, 'content']);
|
|
267
|
+
const stream = combined.stream();
|
|
268
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
269
|
+
// Should handle malformed JSON gracefully
|
|
270
|
+
assert.equal(fileMetadata, undefined);
|
|
271
|
+
assert(fileStream instanceof ReadableStream);
|
|
272
|
+
});
|
|
273
|
+
await it('should handle JSON that is too large', async () => {
|
|
274
|
+
const header = new Uint8Array(1024);
|
|
275
|
+
const largeJson = JSON.stringify({
|
|
276
|
+
name: 'test.txt',
|
|
277
|
+
type: 'text/plain',
|
|
278
|
+
extension: 'txt',
|
|
279
|
+
metadata: { data: 'x'.repeat(800) }, // Creates JSON > 800 chars
|
|
280
|
+
});
|
|
281
|
+
const jsonBytes = new TextEncoder().encode(largeJson);
|
|
282
|
+
if (jsonBytes.length <= 1020) {
|
|
283
|
+
// If it fits in header
|
|
284
|
+
const lengthBytes = new Uint8Array(new Uint32Array([jsonBytes.length]).buffer);
|
|
285
|
+
header.set(lengthBytes, 0);
|
|
286
|
+
header.set(jsonBytes, 4);
|
|
287
|
+
const combined = new Blob([header, 'content']);
|
|
288
|
+
const stream = combined.stream();
|
|
289
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
290
|
+
// Should handle oversized JSON gracefully
|
|
291
|
+
assert.equal(fileMetadata, undefined);
|
|
292
|
+
assert(fileStream instanceof ReadableStream);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
await it('should handle deeply nested JSON', async () => {
|
|
296
|
+
const header = new Uint8Array(1024);
|
|
297
|
+
const deeplyNested = {
|
|
298
|
+
name: 'test.txt',
|
|
299
|
+
type: 'text/plain',
|
|
300
|
+
extension: 'txt',
|
|
301
|
+
metadata: {
|
|
302
|
+
level1: {
|
|
303
|
+
level2: {
|
|
304
|
+
level3: {
|
|
305
|
+
level4: {
|
|
306
|
+
level5: {
|
|
307
|
+
level6: 'too deep', // Exceeds MAX_JSON_DEPTH (5)
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
const jsonBytes = new TextEncoder().encode(JSON.stringify(deeplyNested));
|
|
316
|
+
if (jsonBytes.length <= 1020) {
|
|
317
|
+
// If it fits in header
|
|
318
|
+
const lengthBytes = new Uint8Array(new Uint32Array([jsonBytes.length]).buffer);
|
|
319
|
+
header.set(lengthBytes, 0);
|
|
320
|
+
header.set(jsonBytes, 4);
|
|
321
|
+
const combined = new Blob([header, 'content']);
|
|
322
|
+
const stream = combined.stream();
|
|
323
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
324
|
+
// Should handle deeply nested JSON gracefully
|
|
325
|
+
assert.equal(fileMetadata, undefined);
|
|
326
|
+
assert(fileStream instanceof ReadableStream);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
await it('should handle binary file content after metadata', async () => {
|
|
330
|
+
const binaryData = new Uint8Array([
|
|
331
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
332
|
+
]);
|
|
333
|
+
const binaryFile = new Blob([binaryData]);
|
|
334
|
+
const metadata = { name: 'test.png', type: 'image/png', extension: 'png' };
|
|
335
|
+
const fileWithMetadata = createFileWithMetadata(binaryFile, metadata);
|
|
336
|
+
const stream = fileWithMetadata.stream();
|
|
337
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
338
|
+
assert.deepEqual(fileMetadata, metadata);
|
|
339
|
+
// Verify binary content is preserved
|
|
340
|
+
const extractedContent = await streamToArrayBuffer(fileStream);
|
|
341
|
+
assert.deepEqual(new Uint8Array(extractedContent), binaryData);
|
|
342
|
+
});
|
|
343
|
+
await it('should handle international characters in extracted metadata', async () => {
|
|
344
|
+
const originalFile = createMockFile('内容');
|
|
345
|
+
const metadata = {
|
|
346
|
+
name: '测试文件.txt',
|
|
347
|
+
type: 'text/plain',
|
|
348
|
+
extension: 'txt',
|
|
349
|
+
metadata: {
|
|
350
|
+
author: '作者',
|
|
351
|
+
emoji: '🎉📁',
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
const fileWithMetadata = createFileWithMetadata(originalFile, metadata);
|
|
355
|
+
const stream = fileWithMetadata.stream();
|
|
356
|
+
const { fileMetadata } = await extractFileMetadata(stream);
|
|
357
|
+
assert.deepEqual(fileMetadata, metadata);
|
|
358
|
+
});
|
|
359
|
+
await it('should handle zero-length metadata', async () => {
|
|
360
|
+
const header = new Uint8Array(1024);
|
|
361
|
+
// Set length to 0
|
|
362
|
+
const lengthBytes = new Uint8Array(new Uint32Array([0]).buffer);
|
|
363
|
+
header.set(lengthBytes, 0);
|
|
364
|
+
const combined = new Blob([header, 'content']);
|
|
365
|
+
const stream = combined.stream();
|
|
366
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
367
|
+
assert.equal(fileMetadata, undefined);
|
|
368
|
+
assert(fileStream instanceof ReadableStream);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
await describe('round-trip test', async () => {
|
|
372
|
+
await it('should preserve file content and metadata through full cycle', async () => {
|
|
373
|
+
const originalContent = 'This is a test file with some content.';
|
|
374
|
+
const originalFile = createMockFile(originalContent);
|
|
375
|
+
const metadata = {
|
|
376
|
+
name: 'test-document.txt',
|
|
377
|
+
type: 'text/plain',
|
|
378
|
+
extension: 'txt',
|
|
379
|
+
metadata: {
|
|
380
|
+
author: 'Test Author',
|
|
381
|
+
created: '2024-01-15',
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
// Step 1: Create file with metadata
|
|
385
|
+
const fileWithMetadata = createFileWithMetadata(originalFile, metadata);
|
|
386
|
+
// Step 2: Extract metadata (simulating decryption)
|
|
387
|
+
const stream = fileWithMetadata.stream();
|
|
388
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
389
|
+
// Step 3: Verify metadata
|
|
390
|
+
assert.deepEqual(fileMetadata, metadata);
|
|
391
|
+
// Step 4: Verify file content
|
|
392
|
+
const extractedContent = await streamToArrayBuffer(fileStream);
|
|
393
|
+
const originalArrayBuffer = await originalFile.arrayBuffer();
|
|
394
|
+
assert.deepEqual(new Uint8Array(extractedContent), new Uint8Array(originalArrayBuffer));
|
|
395
|
+
});
|
|
396
|
+
await it('should preserve binary files with international metadata', async () => {
|
|
397
|
+
const binaryData = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); // JPEG header
|
|
398
|
+
const originalFile = new Blob([binaryData], { type: 'image/jpeg' });
|
|
399
|
+
const metadata = {
|
|
400
|
+
name: '照片.jpg',
|
|
401
|
+
type: 'image/jpeg',
|
|
402
|
+
extension: 'jpg',
|
|
403
|
+
metadata: {
|
|
404
|
+
camera: 'Canon EOS 📷',
|
|
405
|
+
location: 'Tokyo 🗾',
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
const fileWithMetadata = createFileWithMetadata(originalFile, metadata);
|
|
409
|
+
const stream = fileWithMetadata.stream();
|
|
410
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
411
|
+
assert.deepEqual(fileMetadata, metadata);
|
|
412
|
+
const extractedContent = await streamToArrayBuffer(fileStream);
|
|
413
|
+
assert.deepEqual(new Uint8Array(extractedContent), binaryData);
|
|
414
|
+
});
|
|
415
|
+
await it('should handle large files efficiently', async () => {
|
|
416
|
+
// Create a 1MB file
|
|
417
|
+
const largeContent = new Uint8Array(1024 * 1024).fill(42);
|
|
418
|
+
const largeFile = new Blob([largeContent]);
|
|
419
|
+
const metadata = {
|
|
420
|
+
name: 'large-file.bin',
|
|
421
|
+
type: 'application/octet-stream',
|
|
422
|
+
extension: 'bin',
|
|
423
|
+
};
|
|
424
|
+
const fileWithMetadata = createFileWithMetadata(largeFile, metadata);
|
|
425
|
+
const stream = fileWithMetadata.stream();
|
|
426
|
+
const { fileStream, fileMetadata } = await extractFileMetadata(stream);
|
|
427
|
+
assert.deepEqual(fileMetadata, metadata);
|
|
428
|
+
const extractedContent = await streamToArrayBuffer(fileStream);
|
|
429
|
+
assert.deepEqual(new Uint8Array(extractedContent), largeContent);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
//# sourceMappingURL=file-metadata.spec.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate mock RSA key pair for testing that works with Web Crypto API
|
|
3
|
+
*/
|
|
4
|
+
export function generateMockRSAKeyPair(): Promise<{
|
|
5
|
+
keyPair: CryptoKeyPair;
|
|
6
|
+
publicKeyPem: string;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Helper to create test fixtures
|
|
10
|
+
*/
|
|
11
|
+
export function createTestFixtures(): Promise<{
|
|
12
|
+
keyManagerServiceDID: ed25519.EdSigner;
|
|
13
|
+
spaceDID: `did:key:${string}`;
|
|
14
|
+
spaceSigner: ed25519.EdSigner;
|
|
15
|
+
issuer: ed25519.EdSigner;
|
|
16
|
+
keyPair: CryptoKeyPair;
|
|
17
|
+
publicKeyPem: string;
|
|
18
|
+
delegationProof: Server.API.Delegation<[{
|
|
19
|
+
with: `did:key:${string}`;
|
|
20
|
+
can: "space/encryption/setup";
|
|
21
|
+
}, {
|
|
22
|
+
with: `did:key:${string}`;
|
|
23
|
+
can: "space/encryption/key/decrypt";
|
|
24
|
+
}]>;
|
|
25
|
+
}>;
|
|
26
|
+
import { ed25519 } from '@ucanto/principal';
|
|
27
|
+
import * as Server from '@ucanto/server';
|
|
28
|
+
//# sourceMappingURL=test-fixtures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-fixtures.d.ts","sourceRoot":"","sources":["../../../test/fixtures/test-fixtures.js"],"names":[],"mappings":"AAGA;;GAEG;AACH;;;GA+BC;AAED;;GAEG;AACH;;;;;;;;;;;;;;GAqCC;wBA9EuB,mBAAmB;wBADnB,gBAAgB"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as Server from '@ucanto/server';
|
|
2
|
+
import { ed25519 } from '@ucanto/principal';
|
|
3
|
+
/**
|
|
4
|
+
* Generate mock RSA key pair for testing that works with Web Crypto API
|
|
5
|
+
*/
|
|
6
|
+
export async function generateMockRSAKeyPair() {
|
|
7
|
+
// Generate key pair using Web Crypto API first
|
|
8
|
+
const keyPair = await globalThis.crypto.subtle.generateKey({
|
|
9
|
+
name: 'RSA-OAEP',
|
|
10
|
+
modulusLength: 2048,
|
|
11
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
12
|
+
hash: 'SHA-256',
|
|
13
|
+
}, true, ['encrypt', 'decrypt']);
|
|
14
|
+
// Export public key to SPKI format (this will work with our adapter)
|
|
15
|
+
const publicKeyBuffer = await globalThis.crypto.subtle.exportKey('spki', keyPair.publicKey);
|
|
16
|
+
// Convert to proper PEM format using standard base64 (not multibase)
|
|
17
|
+
const base64String = Buffer.from(publicKeyBuffer).toString('base64');
|
|
18
|
+
// Format as proper PEM with line breaks every 64 characters like real KMS
|
|
19
|
+
const formattedBase64 = base64String.match(/.{1,64}/g)?.join('\n') || base64String;
|
|
20
|
+
const publicKeyPem = `-----BEGIN PUBLIC KEY-----\n${formattedBase64}\n-----END PUBLIC KEY-----`;
|
|
21
|
+
return {
|
|
22
|
+
keyPair,
|
|
23
|
+
publicKeyPem,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Helper to create test fixtures
|
|
28
|
+
*/
|
|
29
|
+
export async function createTestFixtures() {
|
|
30
|
+
// Create mock key manager service DID
|
|
31
|
+
const keyManagerServiceDID = await ed25519.generate();
|
|
32
|
+
// Create mock space DID - this will be the issuer
|
|
33
|
+
const spaceSigner = await ed25519.generate();
|
|
34
|
+
const spaceDID = spaceSigner.did();
|
|
35
|
+
// Generate mock RSA key pair
|
|
36
|
+
const { keyPair, publicKeyPem } = await generateMockRSAKeyPair();
|
|
37
|
+
// Create mock delegation proof - space delegates to itself (self-issued)
|
|
38
|
+
const delegationProof = await Server.delegate({
|
|
39
|
+
issuer: spaceSigner,
|
|
40
|
+
audience: spaceSigner, // Self-delegation for testing
|
|
41
|
+
capabilities: [
|
|
42
|
+
{
|
|
43
|
+
with: spaceDID,
|
|
44
|
+
can: 'space/encryption/setup',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
with: spaceDID,
|
|
48
|
+
can: 'space/encryption/key/decrypt',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
expiration: Infinity,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
keyManagerServiceDID,
|
|
55
|
+
spaceDID,
|
|
56
|
+
spaceSigner,
|
|
57
|
+
issuer: spaceSigner, // Use space signer as issuer
|
|
58
|
+
keyPair,
|
|
59
|
+
publicKeyPem,
|
|
60
|
+
delegationProof,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=test-fixtures.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create test data with specific patterns for easy verification
|
|
3
|
+
*
|
|
4
|
+
* @param {number} sizeMB - Size of the test file in megabytes
|
|
5
|
+
* @returns {Blob} A Blob containing test data with predictable patterns
|
|
6
|
+
*/
|
|
7
|
+
export function createTestFile(sizeMB: number): Blob;
|
|
8
|
+
/**
|
|
9
|
+
* Convert ReadableStream to Uint8Array
|
|
10
|
+
*
|
|
11
|
+
* @param {ReadableStream} stream - The stream to convert
|
|
12
|
+
* @returns {Promise<Uint8Array>} The stream content as a Uint8Array
|
|
13
|
+
*/
|
|
14
|
+
export function streamToUint8Array(stream: ReadableStream): Promise<Uint8Array>;
|
|
15
|
+
/**
|
|
16
|
+
* @param {Uint8Array} arr
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function uint8ArrayToString(arr: Uint8Array): string;
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} str
|
|
22
|
+
* @returns {Uint8Array}
|
|
23
|
+
*/
|
|
24
|
+
export function stringToUint8Array(str: string): Uint8Array;
|
|
25
|
+
/**
|
|
26
|
+
* Check if an error is a memory-related error (out of heap space, etc.)
|
|
27
|
+
*
|
|
28
|
+
* @param {unknown} error - The error to check
|
|
29
|
+
* @returns {boolean} True if the error appears to be memory-related
|
|
30
|
+
*/
|
|
31
|
+
export function isMemoryError(error: unknown): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Test an encryption operation and expect it might fail with memory errors
|
|
34
|
+
*
|
|
35
|
+
* @param {Function} encryptOperation - Function that performs encryption
|
|
36
|
+
* @param {string} operationName - Name of the operation for logging
|
|
37
|
+
* @returns {Promise<{success: boolean, error?: Error}>} Result of the operation
|
|
38
|
+
*/
|
|
39
|
+
export function testEncryptionWithMemoryHandling(encryptOperation: Function, operationName: string): Promise<{
|
|
40
|
+
success: boolean;
|
|
41
|
+
error?: Error;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Create a CAR file with KMS metadata content
|
|
45
|
+
*
|
|
46
|
+
* @param {any} content - The KMS metadata content
|
|
47
|
+
* @returns {Promise<{car: Uint8Array, actualRootCID: import('multiformats').UnknownLink}>}
|
|
48
|
+
*/
|
|
49
|
+
export function createTestCar(content: any): Promise<{
|
|
50
|
+
car: Uint8Array;
|
|
51
|
+
actualRootCID: import("multiformats").UnknownLink;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Create a mock BlobLike object for testing
|
|
55
|
+
*
|
|
56
|
+
* @param {Uint8Array} data
|
|
57
|
+
* @returns {import('../../src/types.js').BlobLike}
|
|
58
|
+
*/
|
|
59
|
+
export function createMockBlob(data: Uint8Array): import("../../src/types.js").BlobLike;
|
|
60
|
+
//# sourceMappingURL=test-file-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-file-utils.d.ts","sourceRoot":"","sources":["../../../test/helpers/test-file-utils.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,uCAHW,MAAM,GACJ,IAAI,CAuBhB;AAED;;;;;GAKG;AACH,2CAHW,cAAc,GACZ,OAAO,CAAC,UAAU,CAAC,CAmB/B;AAED;;;GAGG;AACH,wCAHW,UAAU,GACR,MAAM,CAIlB;AAED;;;GAGG;AACH,wCAHW,MAAM,GACJ,UAAU,CAItB;AAED;;;;;GAKG;AACH,qCAHW,OAAO,GACL,OAAO,CAUnB;AAED;;;;;;GAMG;AACH,4FAHW,MAAM,GACJ,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAC,CAAC,CAqBtD;AAED;;;;;GAKG;AACH,uCAHW,GAAG,GACD,OAAO,CAAC;IAAC,GAAG,EAAE,UAAU,CAAC;IAAC,aAAa,EAAE,OAAO,cAAc,EAAE,WAAW,CAAA;CAAC,CAAC,CAYzF;AAED;;;;;GAKG;AACH,qCAHW,UAAU,GACR,OAAO,oBAAoB,EAAE,QAAQ,CAajD"}
|