companion-download-adapter.mjs
77 lines 2.4 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 19 hours ago
1 /**
2 * Companion App — Phase 5 download adapter.
3 *
4 * A deliberately dumb HTTPS byte pump: it verifies transport, streams chunks in
5 * order, and makes no integrity decision. The shell owns hashing and finalization.
6 */
7
8 import https from 'node:https';
9
10 export const DOWNLOAD_ADAPTER_REASONS = Object.freeze({
11 HTTPS_REQUIRED: 'https_required',
12 DOWNLOAD_FAILED: 'download_failed',
13 BAD_STATUS: 'bad_status',
14 BAD_CHUNK: 'bad_chunk',
15 });
16
17 /**
18 * Validate that a model download URL uses HTTPS.
19 * @param {unknown} url
20 * @returns {URL}
21 */
22 export function requireHttpsDownloadUrl(url) {
23 if (typeof url !== 'string' || url.length === 0) {
24 throw new TypeError(DOWNLOAD_ADAPTER_REASONS.HTTPS_REQUIRED);
25 }
26 let parsed;
27 try {
28 parsed = new URL(url);
29 } catch {
30 throw new TypeError(DOWNLOAD_ADAPTER_REASONS.HTTPS_REQUIRED);
31 }
32 if (parsed.protocol !== 'https:') {
33 throw new TypeError(DOWNLOAD_ADAPTER_REASONS.HTTPS_REQUIRED);
34 }
35 return parsed;
36 }
37
38 /**
39 * Create the Phase 5 HTTPS streaming adapter.
40 * @param {{ get?: typeof https.get }} [deps]
41 * @returns {{ download: (url: string, onChunk: (chunk: Uint8Array) => void) => Promise<void> }}
42 */
43 export function createCompanionDownloadAdapter(deps = {}) {
44 const get = deps.get ?? https.get;
45 return {
46 async download(url, onChunk) {
47 const parsed = requireHttpsDownloadUrl(url);
48 if (typeof onChunk !== 'function') {
49 throw new TypeError(DOWNLOAD_ADAPTER_REASONS.BAD_CHUNK);
50 }
51 await new Promise((resolve, reject) => {
52 const req = get(parsed, { rejectUnauthorized: true }, (res) => {
53 if (!res || typeof res.statusCode !== 'number' || res.statusCode < 200 || res.statusCode >= 300) {
54 res?.resume?.();
55 reject(new Error(DOWNLOAD_ADAPTER_REASONS.BAD_STATUS));
56 return;
57 }
58 res.on('data', (chunk) => {
59 if (!(chunk instanceof Uint8Array) && !Buffer.isBuffer(chunk)) {
60 reject(new Error(DOWNLOAD_ADAPTER_REASONS.BAD_CHUNK));
61 return;
62 }
63 try {
64 onChunk(chunk);
65 } catch {
66 reject(new Error(DOWNLOAD_ADAPTER_REASONS.BAD_CHUNK));
67 }
68 });
69 res.on('end', resolve);
70 res.on('error', () => reject(new Error(DOWNLOAD_ADAPTER_REASONS.DOWNLOAD_FAILED)));
71 });
72 req.on('error', () => reject(new Error(DOWNLOAD_ADAPTER_REASONS.DOWNLOAD_FAILED)));
73 req.end();
74 });
75 },
76 };
77 }
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 19 hours ago