enigma-memory 0.1.1 → 0.1.2

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.
@@ -0,0 +1,473 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
4
+ import { dirname, extname, isAbsolute, relative, resolve, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ export const BROWSER_EXTENSION_PACKAGE_SCHEMA = 'enigma.browser_extension_package.v1';
8
+
9
+ const SCRIPT_PATH = fileURLToPath(import.meta.url);
10
+ const PACKAGE_DIR = resolve(dirname(SCRIPT_PATH), '..');
11
+ const DEFAULT_EXTENSION_DIR = resolve(PACKAGE_DIR, 'apps/browser-extension');
12
+ const FIXED_DOS_TIME = 0;
13
+ const FIXED_DOS_DATE = 33;
14
+ const PRIVATE_EXTENSIONS = new Set(['.cer', '.crt', '.db', '.der', '.key', '.kdbx', '.p12', '.pfx', '.pem', '.sqlite']);
15
+ const PRIVATE_BASENAMES = new Set([
16
+ '.env',
17
+ '.env.local',
18
+ '.npmrc',
19
+ 'credentials',
20
+ 'credentials.json',
21
+ 'id_ed25519',
22
+ 'id_rsa',
23
+ 'secrets.json',
24
+ ]);
25
+ const REQUIRED_MANIFEST_VERSION = 3;
26
+ const PACKAGE_ROOT_LABEL = 'apps/browser-extension';
27
+ const UTF8_FLAG = 0x0800;
28
+
29
+ let crcTable;
30
+
31
+ export function usage() {
32
+ return `Usage: node scripts/package-browser-extension.mjs [--extension-dir <path>] [--zip <path>] [--out <path>]\n\nValidates the Enigma browser extension directory and emits a deterministic public-safe package manifest.\n--zip writes a deterministic ZIP archive with fixed timestamps and sorted entries. No store submission is performed.\n`;
33
+ }
34
+
35
+ function readRequiredValue(argv, index, flag) {
36
+ const value = argv[index + 1];
37
+ if (value === undefined || value.startsWith('--')) throw new Error(`Missing value for ${flag}.`);
38
+ return value;
39
+ }
40
+
41
+ export function parseBrowserExtensionPackageArgs(argv = process.argv.slice(2)) {
42
+ const options = {
43
+ extensionDir: DEFAULT_EXTENSION_DIR,
44
+ zipPath: null,
45
+ outPath: null,
46
+ help: false,
47
+ };
48
+
49
+ for (let index = 0; index < argv.length; index += 1) {
50
+ const arg = argv[index];
51
+ if (arg === '--help' || arg === '-h') {
52
+ options.help = true;
53
+ } else if (arg === '--extension-dir') {
54
+ options.extensionDir = readRequiredValue(argv, index, arg);
55
+ index += 1;
56
+ } else if (arg === '--zip') {
57
+ options.zipPath = readRequiredValue(argv, index, arg);
58
+ index += 1;
59
+ } else if (arg === '--out') {
60
+ options.outPath = readRequiredValue(argv, index, arg);
61
+ index += 1;
62
+ } else {
63
+ throw new Error('Unknown argument.');
64
+ }
65
+ }
66
+
67
+ return options;
68
+ }
69
+
70
+ function normalizeRelativePath(root, fullPath) {
71
+ const rel = relative(root, fullPath).split(sep).join('/');
72
+ if (!rel || rel.startsWith('../') || rel === '..' || isAbsolute(rel)) throw new Error('Extension file escaped the extension root.');
73
+ return rel;
74
+ }
75
+
76
+ async function listFiles(root, dir = root, output = []) {
77
+ const entries = await readdir(dir, { withFileTypes: true });
78
+ entries.sort((a, b) => a.name.localeCompare(b.name));
79
+ for (const entry of entries) {
80
+ const fullPath = resolve(dir, entry.name);
81
+ if (entry.isDirectory()) {
82
+ await listFiles(root, fullPath, output);
83
+ } else if (entry.isFile()) {
84
+ output.push({ path: fullPath, relativePath: normalizeRelativePath(root, fullPath) });
85
+ } else {
86
+ throw new Error(`Unsupported extension filesystem entry: ${normalizeRelativePath(root, fullPath)}.`);
87
+ }
88
+ }
89
+ return output;
90
+ }
91
+
92
+ function isForbiddenPrivateFile(relativePath) {
93
+ const parts = relativePath.split('/');
94
+ const basename = parts.at(-1).toLowerCase();
95
+ if (basename.endsWith('.map')) return true;
96
+ if (PRIVATE_BASENAMES.has(basename)) return true;
97
+ if (PRIVATE_EXTENSIONS.has(extname(basename))) return true;
98
+ if (parts.some((part) => part.startsWith('.') && part !== '.well-known')) return true;
99
+ return /(^|[._-])(credential|password|private|secret|token)([._-]|$)/iu.test(basename);
100
+ }
101
+
102
+ function sensitiveContentReason(text) {
103
+ if (/sourceMappingURL\s*=/iu.test(text)) return 'source map reference';
104
+ if (/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/u.test(text)) return 'private key material';
105
+ if (/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{12,}/u.test(text)) return 'credential-shaped bearer value';
106
+ if (/\b[A-Za-z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD)[A-Za-z0-9_]*\s*[:=]\s*['"]?[A-Za-z0-9._~+/=-]{12,}/u.test(text)) return 'credential-shaped assignment';
107
+ if (/[A-Za-z]:\\Users\\[^\r\n'"]+/u.test(text)) return 'local Windows user path';
108
+ if (/\/(?:Users|home)\/[A-Za-z0-9._-]+\//u.test(text)) return 'local user path';
109
+ return null;
110
+ }
111
+
112
+ function publicFileHash(buffer) {
113
+ return createHash('sha256').update(buffer).digest('hex');
114
+ }
115
+
116
+ function requireStringArray(value, label) {
117
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || entry.length === 0)) {
118
+ throw new Error(`${label} must be an array of non-empty strings.`);
119
+ }
120
+ return value;
121
+ }
122
+
123
+ function addReferencedFile(files, path) {
124
+ if (typeof path === 'string' && path.length > 0) files.add(path);
125
+ }
126
+
127
+ function addReferencedIconFiles(files, icons) {
128
+ if (!icons || typeof icons !== 'object' || Array.isArray(icons)) return;
129
+ for (const value of Object.values(icons)) addReferencedFile(files, value);
130
+ }
131
+
132
+ function resolvePackageRelativeImport(fromPath, specifier) {
133
+ if (typeof specifier !== 'string' || !specifier.startsWith('.')) return null;
134
+ const baseParts = fromPath.split('/');
135
+ baseParts.pop();
136
+ const resolved = [];
137
+ for (const part of [...baseParts, ...specifier.split('/')]) {
138
+ if (!part || part === '.') continue;
139
+ if (part === '..') {
140
+ if (resolved.length === 0) throw new Error(`Referenced extension module escapes package root: ${specifier}.`);
141
+ resolved.pop();
142
+ } else {
143
+ resolved.push(part);
144
+ }
145
+ }
146
+ return resolved.join('/');
147
+ }
148
+
149
+ function addStaticModuleImports(referencedFiles, availableFiles) {
150
+ const queue = [...referencedFiles].filter((file) => /\.(?:mjs|js)$/iu.test(file));
151
+ for (let index = 0; index < queue.length; index += 1) {
152
+ const file = queue[index];
153
+ const record = availableFiles.get(file);
154
+ if (!record) continue;
155
+ const importRe = /(?:import\s+(?:[^'"]+\s+from\s*)?|export\s+[^'"]+\s+from\s*)['"](\.[^'"]+)['"]/gu;
156
+ for (const match of record.text.matchAll(importRe)) {
157
+ const imported = resolvePackageRelativeImport(file, match[1]);
158
+ if (!imported) continue;
159
+ if (!availableFiles.has(imported)) throw new Error(`Referenced extension file is missing: ${imported}.`);
160
+ if (!referencedFiles.has(imported)) {
161
+ referencedFiles.add(imported);
162
+ if (/\.(?:mjs|js)$/iu.test(imported)) queue.push(imported);
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+
169
+ function validateManifest(manifest, availableFiles) {
170
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) throw new Error('Extension manifest must be a JSON object.');
171
+ if (manifest.manifest_version !== REQUIRED_MANIFEST_VERSION) throw new Error('Extension manifest_version must be 3.');
172
+ if (typeof manifest.name !== 'string' || manifest.name.length === 0) throw new Error('Extension manifest name is required.');
173
+ if (typeof manifest.version !== 'string' || !/^\d+\.\d+\.\d+(?:\.\d+)?$/u.test(manifest.version)) throw new Error('Extension manifest version must be a dotted numeric version.');
174
+
175
+ const permissions = Array.isArray(manifest.permissions) ? manifest.permissions : [];
176
+ if (!permissions.includes('nativeMessaging')) throw new Error('Extension manifest must request nativeMessaging permission.');
177
+ if (permissions.includes('storage')) throw new Error('Extension manifest must not request browser storage permission.');
178
+ if (JSON.stringify(manifest).includes('chrome.storage.sync')) throw new Error('Extension manifest must not reference sync storage.');
179
+
180
+ const referencedFiles = new Set(['manifest.json']);
181
+ if (manifest.background) addReferencedFile(referencedFiles, manifest.background.service_worker);
182
+ if (Array.isArray(manifest.content_scripts)) {
183
+ for (const [index, script] of manifest.content_scripts.entries()) {
184
+ if (!script || typeof script !== 'object') throw new Error(`content_scripts[${index}] must be an object.`);
185
+ for (const file of requireStringArray(script.js ?? [], `content_scripts[${index}].js`)) referencedFiles.add(file);
186
+ for (const file of requireStringArray(script.css ?? [], `content_scripts[${index}].css`)) referencedFiles.add(file);
187
+ requireStringArray(script.matches ?? [], `content_scripts[${index}].matches`);
188
+ }
189
+ }
190
+ if (manifest.action) {
191
+ addReferencedFile(referencedFiles, manifest.action.default_popup);
192
+ addReferencedIconFiles(referencedFiles, manifest.action.default_icon);
193
+ }
194
+ addReferencedIconFiles(referencedFiles, manifest.icons);
195
+ addReferencedFile(referencedFiles, manifest.options_page);
196
+ addReferencedFile(referencedFiles, manifest.devtools_page);
197
+ if (manifest.options_ui) addReferencedFile(referencedFiles, manifest.options_ui.page);
198
+ if (manifest.side_panel) addReferencedFile(referencedFiles, manifest.side_panel.default_path);
199
+ if (Array.isArray(manifest.web_accessible_resources)) {
200
+ for (const resourceGroup of manifest.web_accessible_resources) {
201
+ for (const file of requireStringArray(resourceGroup?.resources ?? [], 'web_accessible_resources.resources')) referencedFiles.add(file);
202
+ }
203
+ }
204
+
205
+ addStaticModuleImports(referencedFiles, availableFiles);
206
+
207
+ for (const file of referencedFiles) {
208
+ if (file.includes('\\') || file.startsWith('/') || file.startsWith('../') || file.includes('/../')) throw new Error(`Referenced extension file is not package-relative: ${file}.`);
209
+ if (!availableFiles.has(file)) throw new Error(`Referenced extension file is missing: ${file}.`);
210
+ if (isForbiddenPrivateFile(file)) throw new Error(`Referenced extension file is forbidden: ${file}.`);
211
+ }
212
+
213
+ return {
214
+ name: manifest.name,
215
+ version: manifest.version,
216
+ manifest_version: manifest.manifest_version,
217
+ minimum_chrome_version: manifest.minimum_chrome_version ?? null,
218
+ permissions: [...permissions].sort(),
219
+ host_permissions_count: Array.isArray(manifest.host_permissions) ? manifest.host_permissions.length : 0,
220
+ referenced_files: [...referencedFiles].sort(),
221
+ };
222
+ }
223
+
224
+ function assertNoSyncStorage(fileRecords) {
225
+ for (const record of fileRecords) {
226
+ if (/\bchrome\.storage\.sync\b/u.test(record.text)) throw new Error(`Extension source must not use sync storage: ${record.path}.`);
227
+ }
228
+ }
229
+
230
+ export async function collectBrowserExtensionPackageFiles(extensionDir = DEFAULT_EXTENSION_DIR) {
231
+ const root = resolve(String(extensionDir));
232
+ const rootStat = await stat(root).catch(() => null);
233
+ if (!rootStat?.isDirectory()) throw new Error('Browser extension directory is missing.');
234
+
235
+ const discovered = await listFiles(root);
236
+ const records = [];
237
+ for (const file of discovered) {
238
+ if (isForbiddenPrivateFile(file.relativePath)) throw new Error(`Forbidden private extension file: ${file.relativePath}.`);
239
+ const buffer = await readFile(file.path);
240
+ const text = buffer.toString('utf8');
241
+ const reason = sensitiveContentReason(text);
242
+ if (reason) throw new Error(`Sensitive ${reason} found in ${file.relativePath}.`);
243
+ records.push({
244
+ path: file.relativePath,
245
+ absolutePath: file.path,
246
+ buffer,
247
+ text,
248
+ size: buffer.byteLength,
249
+ sha256: publicFileHash(buffer),
250
+ });
251
+ }
252
+ records.sort((a, b) => a.path.localeCompare(b.path));
253
+ return records;
254
+ }
255
+
256
+ export async function validateBrowserExtensionPackage(extensionDir = DEFAULT_EXTENSION_DIR) {
257
+ const allFiles = await collectBrowserExtensionPackageFiles(extensionDir);
258
+ const availableFiles = new Map(allFiles.map((file) => [file.path, file]));
259
+ const manifestRecord = availableFiles.get('manifest.json');
260
+ if (!manifestRecord) throw new Error('Extension manifest.json is required.');
261
+
262
+ let manifest;
263
+ try {
264
+ manifest = JSON.parse(manifestRecord.text);
265
+ } catch {
266
+ throw new Error('Extension manifest.json must be valid JSON.');
267
+ }
268
+
269
+ const manifestSummary = validateManifest(manifest, availableFiles);
270
+ const packageFiles = manifestSummary.referenced_files.map((path) => availableFiles.get(path));
271
+ assertNoSyncStorage(packageFiles);
272
+ return { manifest: manifestSummary, files: packageFiles };
273
+ }
274
+
275
+ function crc32(buffer) {
276
+ if (!crcTable) {
277
+ crcTable = new Uint32Array(256);
278
+ for (let n = 0; n < 256; n += 1) {
279
+ let c = n;
280
+ for (let k = 0; k < 8; k += 1) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
281
+ crcTable[n] = c >>> 0;
282
+ }
283
+ }
284
+ let c = 0xffffffff;
285
+ for (const byte of buffer) c = crcTable[(c ^ byte) & 0xff] ^ (c >>> 8);
286
+ return (c ^ 0xffffffff) >>> 0;
287
+ }
288
+
289
+ function checkedUInt32(value, label) {
290
+ if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) throw new Error(`${label} exceeds ZIP32 limits.`);
291
+ return value;
292
+ }
293
+
294
+ function localFileHeader(fileNameBuffer, file) {
295
+ const header = Buffer.alloc(30);
296
+ header.writeUInt32LE(0x04034b50, 0);
297
+ header.writeUInt16LE(20, 4);
298
+ header.writeUInt16LE(UTF8_FLAG, 6);
299
+ header.writeUInt16LE(0, 8);
300
+ header.writeUInt16LE(FIXED_DOS_TIME, 10);
301
+ header.writeUInt16LE(FIXED_DOS_DATE, 12);
302
+ header.writeUInt32LE(file.crc32, 14);
303
+ header.writeUInt32LE(checkedUInt32(file.buffer.byteLength, 'ZIP file size'), 18);
304
+ header.writeUInt32LE(checkedUInt32(file.buffer.byteLength, 'ZIP file size'), 22);
305
+ header.writeUInt16LE(fileNameBuffer.byteLength, 26);
306
+ header.writeUInt16LE(0, 28);
307
+ return header;
308
+ }
309
+
310
+ function centralDirectoryHeader(fileNameBuffer, file, offset) {
311
+ const header = Buffer.alloc(46);
312
+ header.writeUInt32LE(0x02014b50, 0);
313
+ header.writeUInt16LE(20, 4);
314
+ header.writeUInt16LE(20, 6);
315
+ header.writeUInt16LE(UTF8_FLAG, 8);
316
+ header.writeUInt16LE(0, 10);
317
+ header.writeUInt16LE(FIXED_DOS_TIME, 12);
318
+ header.writeUInt16LE(FIXED_DOS_DATE, 14);
319
+ header.writeUInt32LE(file.crc32, 16);
320
+ header.writeUInt32LE(checkedUInt32(file.buffer.byteLength, 'ZIP file size'), 20);
321
+ header.writeUInt32LE(checkedUInt32(file.buffer.byteLength, 'ZIP file size'), 24);
322
+ header.writeUInt16LE(fileNameBuffer.byteLength, 28);
323
+ header.writeUInt16LE(0, 30);
324
+ header.writeUInt16LE(0, 32);
325
+ header.writeUInt16LE(0, 34);
326
+ header.writeUInt16LE(0, 36);
327
+ header.writeUInt32LE(0, 38);
328
+ header.writeUInt32LE(checkedUInt32(offset, 'ZIP file offset'), 42);
329
+ return header;
330
+ }
331
+
332
+ function endOfCentralDirectory(fileCount, centralSize, centralOffset) {
333
+ const footer = Buffer.alloc(22);
334
+ footer.writeUInt32LE(0x06054b50, 0);
335
+ footer.writeUInt16LE(0, 4);
336
+ footer.writeUInt16LE(0, 6);
337
+ footer.writeUInt16LE(fileCount, 8);
338
+ footer.writeUInt16LE(fileCount, 10);
339
+ footer.writeUInt32LE(checkedUInt32(centralSize, 'ZIP central directory size'), 12);
340
+ footer.writeUInt32LE(checkedUInt32(centralOffset, 'ZIP central directory offset'), 16);
341
+ footer.writeUInt16LE(0, 20);
342
+ return footer;
343
+ }
344
+
345
+ export function createDeterministicZip(files) {
346
+ if (!Array.isArray(files) || files.length === 0) throw new Error('ZIP requires at least one extension file.');
347
+ if (files.length > 0xffff) throw new Error('ZIP file count exceeds ZIP32 limits.');
348
+
349
+ const localParts = [];
350
+ const centralParts = [];
351
+ let offset = 0;
352
+ for (const entry of files) {
353
+ const fileNameBuffer = Buffer.from(entry.path, 'utf8');
354
+ const file = { ...entry, crc32: crc32(entry.buffer) };
355
+ const localHeader = localFileHeader(fileNameBuffer, file);
356
+ localParts.push(localHeader, fileNameBuffer, entry.buffer);
357
+ centralParts.push(centralDirectoryHeader(fileNameBuffer, file, offset), fileNameBuffer);
358
+ offset += localHeader.byteLength + fileNameBuffer.byteLength + entry.buffer.byteLength;
359
+ }
360
+ const centralOffset = offset;
361
+ const centralSize = centralParts.reduce((sum, part) => sum + part.byteLength, 0);
362
+ return Buffer.concat([...localParts, ...centralParts, endOfCentralDirectory(files.length, centralSize, centralOffset)]);
363
+ }
364
+
365
+ function publicFileRecords(files) {
366
+ return files.map((file) => ({ path: file.path, size: file.size, sha256: file.sha256 }));
367
+ }
368
+
369
+ function buildPublicReport({ manifest, files, zipBuffer, zipRequested, zipWritten }) {
370
+ const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
371
+ return {
372
+ schema: BROWSER_EXTENSION_PACKAGE_SCHEMA,
373
+ ok: true,
374
+ public_safe: true,
375
+ extension: manifest,
376
+ package: {
377
+ root: PACKAGE_ROOT_LABEL,
378
+ file_count: files.length,
379
+ total_bytes: totalBytes,
380
+ deterministic_order: files.map((file) => file.path),
381
+ zip_compatible_manifest: true,
382
+ zip_requested: zipRequested,
383
+ zip_written: zipWritten,
384
+ zip_path: zipRequested ? '<zip-output>' : null,
385
+ zip_sha256: zipBuffer ? publicFileHash(zipBuffer) : null,
386
+ blocker: null,
387
+ },
388
+ files: publicFileRecords(files),
389
+ checksums: {
390
+ manifest_sha256: files.find((file) => file.path === 'manifest.json')?.sha256 ?? null,
391
+ zip_sha256: zipBuffer ? publicFileHash(zipBuffer) : null,
392
+ },
393
+ safety: {
394
+ source_maps_denied: true,
395
+ private_files_denied: true,
396
+ token_patterns_denied: true,
397
+ local_paths_denied: true,
398
+ sync_storage_denied: true,
399
+ auto_injection_claimed: false,
400
+ store_submission_performed: false,
401
+ external_network_performed: false,
402
+ },
403
+ };
404
+ }
405
+
406
+ export async function buildBrowserExtensionPackage(options = {}) {
407
+ const extensionDir = options.extensionDir ?? DEFAULT_EXTENSION_DIR;
408
+ const zipPath = options.zipPath ? resolve(String(options.zipPath)) : null;
409
+ const { manifest, files } = await validateBrowserExtensionPackage(extensionDir);
410
+ const zipBuffer = zipPath ? createDeterministicZip(files) : null;
411
+ if (zipPath) {
412
+ await mkdir(dirname(zipPath), { recursive: true });
413
+ await writeFile(zipPath, zipBuffer);
414
+ }
415
+ return buildPublicReport({ manifest, files, zipBuffer, zipRequested: Boolean(zipPath), zipWritten: Boolean(zipPath) });
416
+ }
417
+
418
+ function safePackageErrorMessage(error) {
419
+ const message = String(error?.message ?? error);
420
+ if (/^(Missing value|Unknown argument|Browser extension directory is missing|Extension manifest|Referenced extension file|Forbidden private extension file|Sensitive .* found in|ZIP |content_scripts|web_accessible_resources)/u.test(message)) return message;
421
+ return 'Browser extension package validation failed.';
422
+ }
423
+
424
+ export async function runPackageBrowserExtension(argv = process.argv.slice(2)) {
425
+ try {
426
+ const options = Array.isArray(argv) ? parseBrowserExtensionPackageArgs(argv) : { ...argv };
427
+ if (options.help) return { help: usage() };
428
+ const report = await buildBrowserExtensionPackage(options);
429
+ if (options.outPath) {
430
+ const outPath = resolve(String(options.outPath));
431
+ await mkdir(dirname(outPath), { recursive: true });
432
+ await writeFile(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
433
+ }
434
+ return report;
435
+ } catch (error) {
436
+ return {
437
+ schema: BROWSER_EXTENSION_PACKAGE_SCHEMA,
438
+ ok: false,
439
+ public_safe: true,
440
+ error: {
441
+ code: 'BROWSER_EXTENSION_PACKAGE_BLOCKED',
442
+ message: safePackageErrorMessage(error),
443
+ },
444
+ package: {
445
+ root: PACKAGE_ROOT_LABEL,
446
+ zip_written: false,
447
+ blocker: 'Fix extension validation errors before writing a browser-extension ZIP.',
448
+ },
449
+ files: [],
450
+ safety: {
451
+ store_submission_performed: false,
452
+ external_network_performed: false,
453
+ },
454
+ };
455
+ }
456
+ }
457
+
458
+ async function main() {
459
+ const result = await runPackageBrowserExtension();
460
+ if (result.help) {
461
+ process.stdout.write(result.help);
462
+ return;
463
+ }
464
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
465
+ if (result.ok === false) process.exitCode = 1;
466
+ }
467
+
468
+ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
469
+ main().catch(() => {
470
+ process.stdout.write(`${JSON.stringify({ schema: BROWSER_EXTENSION_PACKAGE_SCHEMA, ok: false, public_safe: true, error: { code: 'BROWSER_EXTENSION_PACKAGE_FAILED', message: 'Browser extension package validation failed.' } }, null, 2)}\n`);
471
+ process.exitCode = 1;
472
+ });
473
+ }