Files
archy/tests/apps/blossom/upload-size.ts

91 lines
5.0 KiB
TypeScript

// Synthetic streaming payload only. No real signer, relay or wallet involved.
import { finalizeEvent } from 'nostr-tools';
import { crypto as stdCrypto } from '@std/crypto';
import { request } from 'node:http';
import { Readable } from 'node:stream';
const base = 'http://127.0.0.1:3000';
const max = 2 * 1024 * 1024 * 1024;
const phase = Deno.env.get('BLOSSOM_SIZE_PHASE');
const key = new Uint8Array(32).fill(1);
const pattern = Uint8Array.from({ length: 65536 }, (_, i) => i % 251);
const hex = (buffer: ArrayBuffer) => Array.from(new Uint8Array(buffer), b => b.toString(16).padStart(2, '0')).join('');
function payload() {
let remaining = max;
return new ReadableStream<Uint8Array<ArrayBuffer>>({ pull(controller) {
if (!remaining) { controller.close(); return; }
controller.enqueue(pattern.slice()); remaining -= pattern.length;
} });
}
function auth(action: string, hash: string, secret = key) {
const now = Math.floor(Date.now() / 1000);
return 'Nostr ' + btoa(JSON.stringify(finalizeEvent({ kind: 24242, created_at: now,
content: 'Disposable streaming-size fixture only', tags: [['t', action], ['x', hash],
['server', '127.0.0.1'], ['expiration', String(now + 600)]] }, secret)));
}
async function status(response: Response, expected: number) {
if (response.status !== expected) throw new Error(`Expected ${expected}, got ${response.status}: ${await response.text()}`);
return response;
}
async function preflight(size: number, expected: number, hash = '0'.repeat(64), secret = key) {
await status(await fetch(base + '/upload', { method: 'HEAD', headers: {
authorization: auth('upload', hash, secret), 'x-content-length': String(size),
'x-content-type': 'application/octet-stream', 'x-sha-256': hash,
} }), expected);
}
if (phase === 'default') {
await preflight(16 * 1024 * 1024, 200);
await preflight(16 * 1024 * 1024 + 1, 413);
await preflight(max, 413);
console.log('PASS unchanged 16MiB default boundary; 2GiB is opt-in only');
} else if (phase === 'seed' || phase === 'verify-cleanup') {
const hash = hex(await stdCrypto.subtle.digest('SHA-256', payload()));
await preflight(max, 200, hash);
await preflight(max + 1, 413, hash);
await preflight(max, 401, hash, new Uint8Array(32).fill(2));
if (phase === 'seed') {
// Deno fetch streams use chunked encoding despite a supplied length. Use
// fixed-length HTTP with backpressure, not a 2GiB in-memory Blob workaround.
const descriptor = await new Promise<{ sha256: string; size: number }>((resolve, reject) => {
const source = Readable.fromWeb(payload());
const req = request(base + '/upload', { method: 'PUT', headers: {
authorization: auth('upload', hash), 'content-type': 'application/octet-stream',
'content-length': String(max), 'x-sha-256': hash,
} }, response => {
source.unpipe(req);
source.destroy();
let body = '';
response.on('error', reject);
response.on('data', chunk => {
body += chunk.toString();
if (body.length > 65536) response.destroy(new Error('Oversized upload response'));
});
response.on('end', () => {
if (response.statusCode !== 201) { reject(new Error(`Expected 201, got ${response.statusCode}: ${body}`)); return; }
try { resolve(JSON.parse(body)); } catch (error) { reject(error); }
});
});
req.on('error', error => { source.destroy(); reject(error); });
req.setTimeout(180000, () => req.destroy(new Error('Upload transport timeout')));
source.on('error', error => req.destroy(error));
source.pipe(req);
});
if (descriptor.sha256 !== hash || descriptor.size !== max) throw new Error('Wrong full-size descriptor');
const tail = await status(await fetch(base + '/' + hash, { headers: { range: `bytes=${max - 64}-${max - 1}` } }), 206);
const bytes = new Uint8Array(await tail.arrayBuffer());
if (bytes.length !== 64 || bytes.some((value, i) => value !== pattern[pattern.length - 64 + i])) throw new Error('Wrong tail range');
console.log('PASS actual signed 2GiB streaming upload, descriptor and exact tail range; upper-bound/account negatives');
} else {
const response = await status(await fetch(base + '/' + hash), 200);
let received = 0;
const counted = response.body!.pipeThrough(new TransformStream<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>({ transform(chunk, controller) {
received += chunk.length; controller.enqueue(chunk);
} }));
const actual = hex(await stdCrypto.subtle.digest('SHA-256', counted));
if (received !== max || actual !== hash) throw new Error('Full-size retained bytes differ');
const removed = await status(await fetch(base + '/' + hash, { method: 'DELETE', headers: { authorization: auth('delete', hash) } }), 204);
await removed.arrayBuffer();
await status(await fetch(base + '/' + hash), 404);
console.log('PASS recreated service retains exact 2GiB bytes; explicit owner cleanup only');
}
} else throw new Error('Unknown size phase');