cookie-import-native-integrity.ts
95 lines · browse/src/cookie-import-native-integrity.ts at 3f79d81
1import { createHash } from 'node:crypto';
2import { createReadStream } from 'node:fs';
3import path from 'node:path';
5export const NATIVE_QUALIFICATION_DATA = 'browse/src/cookie-import-native-qualification.json';
6export const NATIVE_BROWSER_VERSION_COMMAND = '$ErrorActionPreference = "Stop"; [Diagnostics.FileVersionInfo]::GetVersionInfo($env:GSTACK_QUALIFY_BROWSER_EXE).ProductVersion';
8export const NATIVE_CODE_INPUTS = Object.freeze([
9 'browse/src/cookie-import-browser.ts',
10 'browse/src/cookie-database.ts',
11 'browse/src/cookie-import-native.ts',
12 'browse/src/cookie-import-native-integrity.ts',
13 'browse/src/cookie-import-native-job.ts',
14 'browse/src/cookie-import-native-worker.ts',
15 'browse/src/bun-child-config.ts',
16 'browse/src/bun-polyfill.cjs',
17 'browse/scripts/build-node-server.sh',
18 '.github/scripts/run-cookie-native-qualification.ps1',
19 'browse/dist/server-node.mjs',
20 'browse/dist/bun-polyfill.cjs',
21 'browse/test/cookie-import-native.test.ts',
22 'browse/test/cookie-import-native-job.test.ts',
23 'browse/test/cookie-import-native-qualification.ts',
24 'browse/test/fixtures/native-cookie-process.cjs',
25 'browse/test/fixtures/native-cookie-launch.cjs',
26 'browse/test/fixtures/native-cookie-process-observer.ts',
27 'browse/test/fixtures/native-cookie-file-owners.ts',
28 'browse/test/fixtures/native-cookie-remove-fixture.cjs',
29 'node_modules/playwright/package.json',
30 'node_modules/playwright/index.js',
31 'node_modules/playwright-core/package.json',
32 'node_modules/playwright-core/index.js',
33 'node_modules/playwright-core/lib/bootstrap.js',
34 'node_modules/playwright-core/lib/coreBundle.js',
35 'node_modules/playwright-core/lib/utilsBundle.js',
36]);
38export interface NativeQualifiedBuild {
39 browserName: 'Chrome' | 'Chromium' | 'Brave' | 'Edge';
40 architecture: 'x64' | 'arm64';
41 windowsRelease: string;
42 executableSha256: string;
43 nodeVersion: string;
44 bunVersion: string;
45 playwrightVersion: string;
46 sourceHashes: Record<string, string>;
47}
49async function readBoundedFile(file: string, deadline: number, maximumBytes: number): Promise<Buffer> {
50 if (!Number.isFinite(deadline) || Date.now() >= deadline) throw new Error('native_timeout');
51 const cancellation = new AbortController();
52 const timer = setTimeout(() => cancellation.abort(), Math.max(0, deadline - Date.now()));
53 const stream = createReadStream(file, { signal: cancellation.signal });
54 const chunks: Buffer[] = [];
55 let size = 0;
56 try {
57 for await (const chunk of stream) {
58 size += chunk.length;
59 if (size > maximumBytes) throw new Error('native_unqualified');
60 chunks.push(chunk);
61 }
62 if (Date.now() >= deadline) throw new Error('native_timeout');
63 return Buffer.concat(chunks);
64 } catch (error) {
65 if (cancellation.signal.aborted) throw new Error('native_timeout');
66 throw error;
67 } finally {
68 stream.destroy();
69 clearTimeout(timer);
70 }
71}
73export async function readNativeQualifications(root: string, deadline: number): Promise<NativeQualifiedBuild[]> {
74 const builds = JSON.parse((await readBoundedFile(path.join(root, NATIVE_QUALIFICATION_DATA), deadline, 1024 * 1024)).toString('utf8'));
75 if (!Array.isArray(builds) || builds.some(build => !build || typeof build !== 'object')) throw new Error('native_unqualified');
76 return builds;
77}
79export async function hashNativeFile(file: string, deadline: number): Promise<string> {
80 return createHash('sha256').update(await readBoundedFile(file, deadline, 64 * 1024 * 1024)).digest('hex');
81}
83export async function nativeCodeHashes(root: string, deadline: number): Promise<Record<string, string>> {
84 const hashes: Record<string, string> = {};
85 for (const file of NATIVE_CODE_INPUTS) hashes[file] = await hashNativeFile(path.join(root, file), deadline);
86 return hashes;
87}
89export function nativeCodeMatches(expected: unknown, actual: Record<string, string>): boolean {
90 if (!expected || typeof expected !== 'object' || Array.isArray(expected) || Object.keys(expected).length !== NATIVE_CODE_INPUTS.length) return false;
91 return NATIVE_CODE_INPUTS.every(file => {
92 const hash = (expected as Record<string, unknown>)[file];
93 return typeof hash === 'string' && /^[0-9a-f]{64}$/.test(hash) && hash === actual[file];
94 });
95}