91 lines
5.0 KiB
TypeScript
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');
|