browser-debug-mcp-bridge 1.10.0 → 1.11.0

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.
Files changed (45) hide show
  1. package/README.md +267 -195
  2. package/apps/mcp-server/dist/db/events-repository.js +61 -9
  3. package/apps/mcp-server/dist/db/events-repository.js.map +1 -1
  4. package/apps/mcp-server/dist/db/migrations.js +470 -70
  5. package/apps/mcp-server/dist/db/migrations.js.map +1 -1
  6. package/apps/mcp-server/dist/db/schema.js +134 -1
  7. package/apps/mcp-server/dist/db/schema.js.map +1 -1
  8. package/apps/mcp-server/dist/document-response-rewriter.js +196 -0
  9. package/apps/mcp-server/dist/document-response-rewriter.js.map +1 -0
  10. package/apps/mcp-server/dist/json-rewrite.js +189 -0
  11. package/apps/mcp-server/dist/json-rewrite.js.map +1 -0
  12. package/apps/mcp-server/dist/main.js +339 -2
  13. package/apps/mcp-server/dist/main.js.map +1 -1
  14. package/apps/mcp-server/dist/mcp/server.js +2146 -176
  15. package/apps/mcp-server/dist/mcp/server.js.map +1 -1
  16. package/apps/mcp-server/dist/next-asset-mapper.js +701 -0
  17. package/apps/mcp-server/dist/next-asset-mapper.js.map +1 -0
  18. package/apps/mcp-server/dist/next-source-override-planner.js +601 -0
  19. package/apps/mcp-server/dist/next-source-override-planner.js.map +1 -0
  20. package/apps/mcp-server/dist/override-audit-contract.js +51 -0
  21. package/apps/mcp-server/dist/override-audit-contract.js.map +1 -0
  22. package/apps/mcp-server/dist/override-audit.js +740 -0
  23. package/apps/mcp-server/dist/override-audit.js.map +1 -0
  24. package/apps/mcp-server/dist/override-capabilities.js +136 -0
  25. package/apps/mcp-server/dist/override-capabilities.js.map +1 -0
  26. package/apps/mcp-server/dist/override-observed-assets.js +179 -0
  27. package/apps/mcp-server/dist/override-observed-assets.js.map +1 -0
  28. package/apps/mcp-server/dist/override-poc.js +336 -0
  29. package/apps/mcp-server/dist/override-poc.js.map +1 -0
  30. package/apps/mcp-server/dist/override-profile-generator.js +403 -0
  31. package/apps/mcp-server/dist/override-profile-generator.js.map +1 -0
  32. package/apps/mcp-server/dist/override-response-planner.js +557 -0
  33. package/apps/mcp-server/dist/override-response-planner.js.map +1 -0
  34. package/apps/mcp-server/dist/override-rule-types.js +32 -0
  35. package/apps/mcp-server/dist/override-rule-types.js.map +1 -0
  36. package/apps/mcp-server/dist/retention.js +4 -3
  37. package/apps/mcp-server/dist/retention.js.map +1 -1
  38. package/apps/mcp-server/dist/rsc-flight-patch-safety.js +269 -0
  39. package/apps/mcp-server/dist/rsc-flight-patch-safety.js.map +1 -0
  40. package/apps/mcp-server/dist/websocket/messages.js +5 -0
  41. package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
  42. package/apps/mcp-server/dist/websocket/websocket-server.js +10 -0
  43. package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -1
  44. package/apps/mcp-server/package.json +1 -0
  45. package/package.json +12 -1
@@ -0,0 +1,701 @@
1
+ import { createHash } from 'crypto';
2
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
3
+ import { extname, relative, resolve } from 'path';
4
+ import { isOverridePocRuleType, normalizeOverrideRequestMethod, } from './override-rule-types.js';
5
+ const NEXT_MANIFEST_RELATIVE_PATHS = [
6
+ 'build-manifest.json',
7
+ 'app-build-manifest.json',
8
+ 'react-loadable-manifest.json',
9
+ ];
10
+ const SOURCE_MAP_EXTENSIONS = new Set(['.js', '.mjs']);
11
+ const DEFAULT_PRODUCTION_FETCH_TIMEOUT_MS = 5_000;
12
+ const DEFAULT_MAX_PRODUCTION_ASSET_BYTES = 2 * 1024 * 1024;
13
+ const DEFAULT_MAX_DRIFT_CANDIDATES = 20;
14
+ const DEFAULT_DRIFT_FETCH_CONCURRENCY = 4;
15
+ function isRecord(value) {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ }
18
+ function toPortablePath(value) {
19
+ return value.replace(/\\/g, '/');
20
+ }
21
+ function normalizeRoute(value) {
22
+ if (!value) {
23
+ return undefined;
24
+ }
25
+ const trimmed = value.trim();
26
+ if (!trimmed) {
27
+ return undefined;
28
+ }
29
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
30
+ }
31
+ function normalizeOptionalString(value) {
32
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
33
+ }
34
+ function normalizeOptionalInteger(value) {
35
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
36
+ return undefined;
37
+ }
38
+ return Math.floor(value);
39
+ }
40
+ function inferObservedRuleType(value, url, normalized) {
41
+ if (isOverridePocRuleType(value.ruleType)) {
42
+ return value.ruleType;
43
+ }
44
+ const kind = normalizeOptionalString(value.kind)?.toLowerCase();
45
+ const resourceType = normalizeOptionalString(value.resourceType)?.toLowerCase();
46
+ const initiatorType = normalizeOptionalString(value.initiatorType)?.toLowerCase();
47
+ const contentType = normalizeOptionalString(value.contentType)?.toLowerCase();
48
+ if (kind === 'document' || resourceType === 'document' || value.fromNavigation === true) {
49
+ return 'document';
50
+ }
51
+ if (contentType?.includes('text/x-component') || url.includes('_rsc=') || normalized.pathname.includes('/__flight__')) {
52
+ return 'rsc-flight';
53
+ }
54
+ if (normalized.pathname.includes('/_next/data/')) {
55
+ return 'next-data';
56
+ }
57
+ if (normalized.assetPath) {
58
+ return 'asset';
59
+ }
60
+ if (initiatorType === 'fetch' || initiatorType === 'xmlhttprequest' || kind === 'fetch' || kind === 'xmlhttprequest') {
61
+ return 'api-response';
62
+ }
63
+ return 'asset';
64
+ }
65
+ function normalizeAssetPathFromUrl(value) {
66
+ let pathname = value;
67
+ try {
68
+ pathname = new URL(value).pathname;
69
+ }
70
+ catch {
71
+ const queryIndex = pathname.search(/[?#]/);
72
+ if (queryIndex >= 0) {
73
+ pathname = pathname.slice(0, queryIndex);
74
+ }
75
+ }
76
+ pathname = toPortablePath(pathname);
77
+ const nextIndex = pathname.indexOf('/_next/');
78
+ if (nextIndex < 0) {
79
+ return { assetPath: null, pathname };
80
+ }
81
+ const assetPath = pathname.slice(nextIndex + '/_next/'.length).replace(/^\/+/, '');
82
+ return {
83
+ assetPath: assetPath.startsWith('static/') ? assetPath : null,
84
+ pathname,
85
+ };
86
+ }
87
+ function normalizeObservedAsset(value) {
88
+ if (!isRecord(value)) {
89
+ return null;
90
+ }
91
+ const url = normalizeOptionalString(value.url)
92
+ ?? normalizeOptionalString(value.href)
93
+ ?? normalizeOptionalString(value.requestUrl);
94
+ if (!url) {
95
+ return null;
96
+ }
97
+ const normalized = normalizeAssetPathFromUrl(url);
98
+ const ruleType = inferObservedRuleType(value, url, normalized);
99
+ return {
100
+ url,
101
+ assetPath: normalized.assetPath,
102
+ pathname: normalized.pathname,
103
+ ruleType,
104
+ requestMethod: normalizeOverrideRequestMethod(value.requestMethod),
105
+ resourceType: normalizeOptionalString(value.resourceType),
106
+ contentType: normalizeOptionalString(value.contentType),
107
+ statusCode: normalizeOptionalInteger(value.statusCode),
108
+ kind: normalizeOptionalString(value.kind),
109
+ initiatorType: normalizeOptionalString(value.initiatorType),
110
+ rel: normalizeOptionalString(value.rel),
111
+ as: normalizeOptionalString(value.as),
112
+ integrity: normalizeOptionalString(value.integrity),
113
+ fromDom: value.fromDom === true,
114
+ fromPerformance: value.fromPerformance === true,
115
+ fromNavigation: value.fromNavigation === true,
116
+ fromFetch: value.fromFetch === true,
117
+ };
118
+ }
119
+ export function normalizeObservedOverrideAssets(values) {
120
+ if (!Array.isArray(values)) {
121
+ return [];
122
+ }
123
+ const byUrl = new Map();
124
+ for (const value of values) {
125
+ const normalized = normalizeObservedAsset(value);
126
+ if (!normalized) {
127
+ continue;
128
+ }
129
+ const key = `${normalized.requestMethod}\0${normalized.url}`;
130
+ const existing = byUrl.get(key);
131
+ if (existing) {
132
+ existing.fromDom = existing.fromDom || normalized.fromDom;
133
+ existing.fromPerformance = existing.fromPerformance || normalized.fromPerformance;
134
+ existing.integrity = existing.integrity ?? normalized.integrity;
135
+ existing.kind = existing.kind ?? normalized.kind;
136
+ existing.initiatorType = existing.initiatorType ?? normalized.initiatorType;
137
+ existing.ruleType = existing.ruleType === 'asset' ? normalized.ruleType : existing.ruleType;
138
+ existing.resourceType = existing.resourceType ?? normalized.resourceType;
139
+ existing.contentType = existing.contentType ?? normalized.contentType;
140
+ existing.statusCode = existing.statusCode ?? normalized.statusCode;
141
+ existing.fromNavigation = existing.fromNavigation || normalized.fromNavigation;
142
+ existing.fromFetch = existing.fromFetch || normalized.fromFetch;
143
+ continue;
144
+ }
145
+ byUrl.set(key, normalized);
146
+ }
147
+ return Array.from(byUrl.values()).sort((first, second) => first.url.localeCompare(second.url));
148
+ }
149
+ function readJsonFile(filePath) {
150
+ try {
151
+ return JSON.parse(readFileSync(filePath, 'utf8'));
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function readJsonText(text) {
158
+ try {
159
+ return JSON.parse(text);
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ }
165
+ function collectManifestReferences(value, context, references) {
166
+ if (typeof value === 'string') {
167
+ const normalized = normalizeAssetPathFromUrl(value);
168
+ if (!normalized.assetPath) {
169
+ return;
170
+ }
171
+ const reference = references.get(normalized.assetPath) ?? { routes: new Set(), manifests: new Set(), sources: new Set() };
172
+ if (context.route) {
173
+ reference.routes.add(context.route);
174
+ }
175
+ reference.manifests.add(context.manifestFile);
176
+ references.set(normalized.assetPath, reference);
177
+ return;
178
+ }
179
+ if (Array.isArray(value)) {
180
+ for (const entry of value) {
181
+ collectManifestReferences(entry, context, references);
182
+ }
183
+ return;
184
+ }
185
+ if (!isRecord(value)) {
186
+ return;
187
+ }
188
+ for (const [key, entry] of Object.entries(value)) {
189
+ const route = key.startsWith('/') ? key : context.route;
190
+ collectManifestReferences(entry, { ...context, route }, references);
191
+ }
192
+ }
193
+ function walkFiles(currentDir, matcher, filePaths) {
194
+ if (!existsSync(currentDir)) {
195
+ return;
196
+ }
197
+ for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
198
+ const fullPath = resolve(currentDir, entry.name);
199
+ if (entry.isDirectory()) {
200
+ walkFiles(fullPath, matcher, filePaths);
201
+ continue;
202
+ }
203
+ if (entry.isFile() && matcher(fullPath)) {
204
+ filePaths.push(fullPath);
205
+ }
206
+ }
207
+ }
208
+ function collectClientReferenceManifestAssets(nextDir, references, warnings) {
209
+ const manifestFiles = [];
210
+ walkFiles(resolve(nextDir, 'server', 'app'), (filePath) => filePath.endsWith('client-reference-manifest.js'), manifestFiles);
211
+ for (const manifestFile of manifestFiles) {
212
+ const content = readFileSync(manifestFile, 'utf8');
213
+ const assignmentMatch = content.match(/__RSC_MANIFEST\["([^"]+)"\]\s*=\s*(\{[\s\S]*\})\s*;?\s*$/);
214
+ if (!assignmentMatch) {
215
+ continue;
216
+ }
217
+ const route = normalizeRoute(assignmentMatch[1]);
218
+ const manifest = readJsonText(assignmentMatch[2] ?? '');
219
+ if (!isRecord(manifest) || !isRecord(manifest.clientModules)) {
220
+ warnings.push(`Unable to parse client reference manifest: ${manifestFile}`);
221
+ continue;
222
+ }
223
+ for (const [sourceReference, moduleRecord] of Object.entries(manifest.clientModules)) {
224
+ if (!isRecord(moduleRecord) || !Array.isArray(moduleRecord.chunks)) {
225
+ continue;
226
+ }
227
+ for (const chunk of moduleRecord.chunks) {
228
+ if (typeof chunk !== 'string') {
229
+ continue;
230
+ }
231
+ const normalized = normalizeAssetPathFromUrl(chunk);
232
+ if (!normalized.assetPath) {
233
+ continue;
234
+ }
235
+ const reference = references.get(normalized.assetPath) ?? { routes: new Set(), manifests: new Set(), sources: new Set() };
236
+ if (route) {
237
+ reference.routes.add(route);
238
+ }
239
+ reference.manifests.add(manifestFile);
240
+ reference.sources.add(normalizeSourceReference(sourceReference));
241
+ references.set(normalized.assetPath, reference);
242
+ }
243
+ }
244
+ }
245
+ }
246
+ function walkAssets(root, currentDir, assetPaths) {
247
+ for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
248
+ const fullPath = resolve(currentDir, entry.name);
249
+ if (entry.isDirectory()) {
250
+ walkAssets(root, fullPath, assetPaths);
251
+ continue;
252
+ }
253
+ if (!entry.isFile()) {
254
+ continue;
255
+ }
256
+ const extension = extname(entry.name).toLowerCase();
257
+ if (extension !== '.js' && extension !== '.mjs' && extension !== '.css') {
258
+ continue;
259
+ }
260
+ assetPaths.push(toPortablePath(relative(root, fullPath)));
261
+ }
262
+ }
263
+ function readSourceMapSources(assetFilePath) {
264
+ if (!SOURCE_MAP_EXTENSIONS.has(extname(assetFilePath).toLowerCase())) {
265
+ return { sources: [] };
266
+ }
267
+ const sourceMapPath = `${assetFilePath}.map`;
268
+ if (!existsSync(sourceMapPath)) {
269
+ return { sources: [] };
270
+ }
271
+ const parsed = readJsonFile(sourceMapPath);
272
+ const sources = isRecord(parsed) && Array.isArray(parsed.sources)
273
+ ? parsed.sources.filter((entry) => typeof entry === 'string')
274
+ : [];
275
+ return {
276
+ sourceMapPath,
277
+ sources: sources.map(normalizeSourceReference),
278
+ };
279
+ }
280
+ function normalizeSourceReference(value) {
281
+ let normalized = toPortablePath(value.trim());
282
+ const queryIndex = normalized.search(/[?#]/);
283
+ if (queryIndex >= 0) {
284
+ normalized = normalized.slice(0, queryIndex);
285
+ }
286
+ normalized = normalized.replace(/^webpack:\/\/[^/]+\//, '');
287
+ normalized = normalized.replace(/^file:\/\//, '');
288
+ normalized = normalized.replace(/^\.\//, '');
289
+ normalized = normalized.replace(/^\/+/, '');
290
+ return normalized;
291
+ }
292
+ export function normalizeNextSourcePath(projectRoot, sourcePath) {
293
+ const portable = toPortablePath(sourcePath.trim());
294
+ if (!portable) {
295
+ return portable;
296
+ }
297
+ if (/^[a-zA-Z]:\//.test(portable) || portable.startsWith('/')) {
298
+ return toPortablePath(relative(projectRoot, resolve(portable))).replace(/^\.\//, '');
299
+ }
300
+ return portable.replace(/^\.\//, '');
301
+ }
302
+ export function nextAssetSourceMatches(candidateSource, requestedSource) {
303
+ const normalizedCandidate = candidateSource.toLowerCase();
304
+ const normalizedRequested = requestedSource.toLowerCase();
305
+ return normalizedCandidate === normalizedRequested
306
+ || normalizedCandidate.endsWith(`/${normalizedRequested}`)
307
+ || normalizedCandidate.endsWith(normalizedRequested);
308
+ }
309
+ function hashFile(filePath) {
310
+ return createHash('sha256').update(readFileSync(filePath)).digest('hex');
311
+ }
312
+ function hashBuffer(buffer) {
313
+ return createHash('sha256').update(buffer).digest('hex');
314
+ }
315
+ function normalizeAssetSignature(content) {
316
+ return content
317
+ .replace(/\/\/# sourceMappingURL=.*$/gm, '')
318
+ .replace(/\b[a-f0-9]{8,}\b/gi, '<hash>')
319
+ .replace(/\s+/g, '');
320
+ }
321
+ async function readBoundedResponseBytes(response, maxBytes) {
322
+ const contentLength = Number(response.headers.get('content-length') ?? Number.NaN);
323
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
324
+ return { status: 'too_large' };
325
+ }
326
+ if (!response.body) {
327
+ const buffer = Buffer.from(await response.arrayBuffer());
328
+ return buffer.byteLength > maxBytes ? { status: 'too_large' } : { status: 'ok', bytes: buffer };
329
+ }
330
+ const reader = response.body.getReader();
331
+ const chunks = [];
332
+ let totalBytes = 0;
333
+ while (true) {
334
+ const read = await reader.read();
335
+ if (read.done) {
336
+ break;
337
+ }
338
+ totalBytes += read.value.byteLength;
339
+ if (totalBytes > maxBytes) {
340
+ await reader.cancel();
341
+ return { status: 'too_large' };
342
+ }
343
+ chunks.push(read.value);
344
+ }
345
+ return { status: 'ok', bytes: Buffer.concat(chunks, totalBytes) };
346
+ }
347
+ async function fetchProductionAsset(url, options) {
348
+ const controller = new AbortController();
349
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
350
+ try {
351
+ const response = await fetch(url, { signal: controller.signal });
352
+ if (!response.ok) {
353
+ return { status: 'unavailable', error: `HTTP ${response.status}` };
354
+ }
355
+ return await readBoundedResponseBytes(response, options.maxBytes);
356
+ }
357
+ catch (error) {
358
+ return {
359
+ status: 'unavailable',
360
+ error: error instanceof Error ? error.message : String(error),
361
+ };
362
+ }
363
+ finally {
364
+ clearTimeout(timeout);
365
+ }
366
+ }
367
+ async function mapWithConcurrency(values, concurrency, mapper) {
368
+ let nextIndex = 0;
369
+ const workerCount = Math.min(concurrency, values.length);
370
+ await Promise.all(Array.from({ length: workerCount }, async () => {
371
+ while (nextIndex < values.length) {
372
+ const current = values[nextIndex];
373
+ nextIndex += 1;
374
+ if (current !== undefined) {
375
+ await mapper(current);
376
+ }
377
+ }
378
+ }));
379
+ }
380
+ export function createNextAssetIndex(projectRootInput, nextDirInput) {
381
+ const projectRoot = resolve(projectRootInput);
382
+ const nextDir = resolve(projectRoot, nextDirInput ?? '.next');
383
+ const warnings = [];
384
+ const manifestReferences = new Map();
385
+ if (!existsSync(nextDir)) {
386
+ warnings.push(`Next.js output directory not found at ${nextDir}. Build the app before mapping override assets.`);
387
+ return { projectRoot, nextDir, assets: [], byAssetPath: new Map(), warnings };
388
+ }
389
+ for (const manifestRelativePath of NEXT_MANIFEST_RELATIVE_PATHS) {
390
+ const manifestFile = resolve(nextDir, manifestRelativePath);
391
+ if (!existsSync(manifestFile)) {
392
+ continue;
393
+ }
394
+ const manifest = readJsonFile(manifestFile);
395
+ if (!manifest) {
396
+ warnings.push(`Unable to parse Next.js manifest: ${manifestFile}`);
397
+ continue;
398
+ }
399
+ collectManifestReferences(manifest, { manifestFile }, manifestReferences);
400
+ }
401
+ collectClientReferenceManifestAssets(nextDir, manifestReferences, warnings);
402
+ const staticRoot = resolve(nextDir, 'static');
403
+ const assetPaths = [];
404
+ if (existsSync(staticRoot)) {
405
+ walkAssets(nextDir, staticRoot, assetPaths);
406
+ }
407
+ else {
408
+ warnings.push(`Next.js static directory not found at ${staticRoot}.`);
409
+ }
410
+ const assets = assetPaths.sort((first, second) => first.localeCompare(second)).map((assetPath) => {
411
+ const localFilePath = resolve(nextDir, assetPath);
412
+ const stat = statSync(localFilePath);
413
+ const sourceMap = readSourceMapSources(localFilePath);
414
+ const manifestReference = manifestReferences.get(assetPath);
415
+ const sourceMapSources = sourceMap.sources;
416
+ const manifestSources = Array.from(manifestReference?.sources ?? []);
417
+ const sources = Array.from(new Set([
418
+ ...sourceMapSources,
419
+ ...manifestSources,
420
+ ])).sort();
421
+ return {
422
+ assetPath,
423
+ localFilePath,
424
+ extension: extname(assetPath).toLowerCase(),
425
+ sizeBytes: stat.size,
426
+ sha256: hashFile(localFilePath),
427
+ sourceMapPath: sourceMap.sourceMapPath,
428
+ sourceCount: sources.length,
429
+ sourceMapSources: sourceMapSources.sort(),
430
+ manifestSources: manifestSources.sort(),
431
+ sources,
432
+ manifestRoutes: Array.from(manifestReference?.routes ?? []).sort(),
433
+ manifestFiles: Array.from(manifestReference?.manifests ?? []).sort(),
434
+ };
435
+ });
436
+ return {
437
+ projectRoot,
438
+ nextDir,
439
+ assets,
440
+ byAssetPath: new Map(assets.map((asset) => [asset.assetPath, asset])),
441
+ warnings,
442
+ };
443
+ }
444
+ function confidenceFromScore(score) {
445
+ if (score >= 80) {
446
+ return 'high';
447
+ }
448
+ if (score >= 45) {
449
+ return 'medium';
450
+ }
451
+ return 'low';
452
+ }
453
+ function buildNextActions(candidates, warnings) {
454
+ if (warnings.length > 0 && candidates.length === 0) {
455
+ return [{ code: 'BUILD_OR_OBSERVE', message: 'Build the Next.js app and observe a live page before generating override rules.' }];
456
+ }
457
+ if (candidates.some((candidate) => candidate.blockers.includes('PRODUCTION_LOCAL_DRIFT'))) {
458
+ return [{ code: 'RESOLVE_PRODUCTION_LOCAL_DRIFT', message: 'Rebuild the local app from the same revision/config as production or disable drift checking before reviewing lower-confidence rules.' }];
459
+ }
460
+ if (candidates.some((candidate) => candidate.blockers.includes('SRI_PRESENT'))) {
461
+ return [{ code: 'HANDLE_SRI_BLOCKER', message: 'Choose a non-SRI target or add document/SRI mitigation before enabling this override.' }];
462
+ }
463
+ if (candidates.some((candidate) => candidate.confidence === 'high')) {
464
+ return [{ code: 'CREATE_OVERRIDE_PROFILE', message: 'Use high-confidence targetAssetUrl/localFilePath pairs to create or update an override profile.' }];
465
+ }
466
+ if (candidates.length > 0) {
467
+ return [{ code: 'REVIEW_MAPPING', message: 'Review medium/low-confidence mappings before enabling browser overrides.' }];
468
+ }
469
+ return [{ code: 'INTERACT_WITH_ROUTE', message: 'Load or interact with the target route so the browser requests the relevant Next.js chunks, then observe assets again.' }];
470
+ }
471
+ export function mapNextOverrideAssets(options) {
472
+ const index = createNextAssetIndex(options.projectRoot, normalizeOptionalString(options.nextDir));
473
+ const observedAssets = normalizeObservedOverrideAssets(options.observedAssets);
474
+ const observedNextAssets = observedAssets.filter((asset) => asset.assetPath !== null);
475
+ const sourcePaths = Array.isArray(options.sourcePaths)
476
+ ? options.sourcePaths
477
+ .filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
478
+ .map((entry) => normalizeNextSourcePath(index.projectRoot, entry))
479
+ : [];
480
+ const route = normalizeRoute(normalizeOptionalString(options.route));
481
+ const maxResults = typeof options.maxResults === 'number' && Number.isFinite(options.maxResults)
482
+ ? Math.max(1, Math.floor(options.maxResults))
483
+ : 50;
484
+ const candidates = [];
485
+ const unmatchedObservedAssets = [];
486
+ for (const observed of observedNextAssets) {
487
+ const assetPath = observed.assetPath;
488
+ const asset = assetPath ? index.byAssetPath.get(assetPath) : undefined;
489
+ if (!asset || !assetPath) {
490
+ unmatchedObservedAssets.push(observed);
491
+ continue;
492
+ }
493
+ const matchedSourcePaths = sourcePaths.filter((sourcePath) => {
494
+ return asset.sources.some((source) => nextAssetSourceMatches(source, sourcePath));
495
+ });
496
+ const routeMatches = route ? asset.manifestRoutes.includes(route) : false;
497
+ const reasons = ['exact_next_asset_path_match'];
498
+ const blockers = [];
499
+ let score = 65;
500
+ if (sourcePaths.length > 0) {
501
+ if (matchedSourcePaths.length > 0) {
502
+ score += 30;
503
+ reasons.push('source_map_source_match');
504
+ }
505
+ else if (asset.sourceMapPath) {
506
+ score -= 25;
507
+ reasons.push('source_map_present_without_requested_source');
508
+ }
509
+ else {
510
+ score -= 10;
511
+ reasons.push('source_map_missing');
512
+ }
513
+ }
514
+ if (route) {
515
+ if (routeMatches) {
516
+ score += 10;
517
+ reasons.push('route_manifest_match');
518
+ }
519
+ else if (asset.manifestRoutes.length > 0) {
520
+ score -= 10;
521
+ reasons.push('route_manifest_mismatch');
522
+ }
523
+ else {
524
+ reasons.push('route_manifest_unavailable');
525
+ }
526
+ }
527
+ if (observed.integrity) {
528
+ score -= 35;
529
+ blockers.push('SRI_PRESENT');
530
+ reasons.push('script_or_link_has_integrity_attribute');
531
+ }
532
+ candidates.push({
533
+ targetAssetUrl: observed.url,
534
+ assetPath,
535
+ localFilePath: asset.localFilePath,
536
+ confidence: confidenceFromScore(score),
537
+ score: Math.max(0, Math.min(100, score)),
538
+ reasons,
539
+ blockers,
540
+ matchedSourcePaths,
541
+ manifestRoutes: asset.manifestRoutes,
542
+ observed,
543
+ });
544
+ }
545
+ const sortedCandidates = candidates
546
+ .sort((first, second) => second.score - first.score || first.assetPath.localeCompare(second.assetPath))
547
+ .slice(0, maxResults);
548
+ return {
549
+ projectRoot: index.projectRoot,
550
+ nextDir: index.nextDir,
551
+ route,
552
+ sourcePaths,
553
+ observedAssetCount: observedAssets.length,
554
+ observedNextAssetCount: observedNextAssets.length,
555
+ indexedAssetCount: index.assets.length,
556
+ sourceMappedAssetCount: index.assets.filter((asset) => asset.sourceMapPath).length,
557
+ candidates: sortedCandidates,
558
+ unmatchedObservedAssets,
559
+ warnings: index.warnings,
560
+ nextActions: buildNextActions(sortedCandidates, index.warnings),
561
+ };
562
+ }
563
+ function createEmptyDriftSummary(maxChecked, concurrency) {
564
+ return {
565
+ checked: 0,
566
+ matched: 0,
567
+ signatureMatched: 0,
568
+ different: 0,
569
+ unavailable: 0,
570
+ tooLarge: 0,
571
+ skipped: 0,
572
+ maxChecked,
573
+ concurrency,
574
+ };
575
+ }
576
+ function applyDriftToCandidate(candidate, drift) {
577
+ candidate.drift = drift;
578
+ if (drift.status === 'matched') {
579
+ candidate.score = Math.min(100, candidate.score + 5);
580
+ candidate.reasons.push('production_local_hash_match');
581
+ }
582
+ else if (drift.status === 'signature_match') {
583
+ candidate.score = Math.max(0, candidate.score - 5);
584
+ candidate.reasons.push('production_local_normalized_signature_match');
585
+ }
586
+ else if (drift.status === 'different') {
587
+ candidate.score = Math.max(0, candidate.score - 35);
588
+ candidate.blockers.push('PRODUCTION_LOCAL_DRIFT');
589
+ candidate.reasons.push('production_local_hash_drift');
590
+ }
591
+ else if (drift.status === 'too_large') {
592
+ candidate.reasons.push('production_asset_too_large_for_drift_check');
593
+ }
594
+ else {
595
+ candidate.reasons.push('production_asset_unavailable_for_drift_check');
596
+ }
597
+ candidate.confidence = confidenceFromScore(candidate.score);
598
+ }
599
+ async function createDriftCheck(candidate, options, localCache) {
600
+ let local = localCache.get(candidate.localFilePath);
601
+ if (!local) {
602
+ const buffer = readFileSync(candidate.localFilePath);
603
+ local = {
604
+ buffer,
605
+ sha256: hashBuffer(buffer),
606
+ signature: normalizeAssetSignature(buffer.toString('utf8')),
607
+ };
608
+ localCache.set(candidate.localFilePath, local);
609
+ }
610
+ const fetched = await fetchProductionAsset(candidate.targetAssetUrl, options);
611
+ const checkedAt = Date.now();
612
+ if (fetched.status === 'too_large') {
613
+ return {
614
+ checkedAt,
615
+ status: 'too_large',
616
+ localSha256: local.sha256,
617
+ localBytes: local.buffer.byteLength,
618
+ };
619
+ }
620
+ if (fetched.status === 'unavailable') {
621
+ return {
622
+ checkedAt,
623
+ status: 'unavailable',
624
+ localSha256: local.sha256,
625
+ localBytes: local.buffer.byteLength,
626
+ error: fetched.error,
627
+ };
628
+ }
629
+ const productionSha256 = hashBuffer(fetched.bytes);
630
+ if (productionSha256 === local.sha256) {
631
+ return {
632
+ checkedAt,
633
+ status: 'matched',
634
+ productionSha256,
635
+ localSha256: local.sha256,
636
+ productionBytes: fetched.bytes.byteLength,
637
+ localBytes: local.buffer.byteLength,
638
+ normalizedSignatureMatch: true,
639
+ };
640
+ }
641
+ const normalizedSignatureMatch = normalizeAssetSignature(fetched.bytes.toString('utf8')) === local.signature;
642
+ return {
643
+ checkedAt,
644
+ status: normalizedSignatureMatch ? 'signature_match' : 'different',
645
+ productionSha256,
646
+ localSha256: local.sha256,
647
+ productionBytes: fetched.bytes.byteLength,
648
+ localBytes: local.buffer.byteLength,
649
+ normalizedSignatureMatch,
650
+ };
651
+ }
652
+ export async function mapNextOverrideAssetsWithDrift(options) {
653
+ const result = mapNextOverrideAssets(options);
654
+ if (options.fetchProductionAssets !== true || result.candidates.length === 0) {
655
+ return result;
656
+ }
657
+ const timeoutMs = typeof options.productionFetchTimeoutMs === 'number' && Number.isFinite(options.productionFetchTimeoutMs)
658
+ ? Math.max(500, Math.floor(options.productionFetchTimeoutMs))
659
+ : DEFAULT_PRODUCTION_FETCH_TIMEOUT_MS;
660
+ const maxBytes = typeof options.maxProductionAssetBytes === 'number' && Number.isFinite(options.maxProductionAssetBytes)
661
+ ? Math.max(1024, Math.floor(options.maxProductionAssetBytes))
662
+ : DEFAULT_MAX_PRODUCTION_ASSET_BYTES;
663
+ const maxDriftCandidates = typeof options.maxDriftCandidates === 'number' && Number.isFinite(options.maxDriftCandidates)
664
+ ? Math.max(1, Math.floor(options.maxDriftCandidates))
665
+ : DEFAULT_MAX_DRIFT_CANDIDATES;
666
+ const concurrency = typeof options.productionFetchConcurrency === 'number' && Number.isFinite(options.productionFetchConcurrency)
667
+ ? Math.max(1, Math.min(8, Math.floor(options.productionFetchConcurrency)))
668
+ : DEFAULT_DRIFT_FETCH_CONCURRENCY;
669
+ const driftSummary = createEmptyDriftSummary(maxDriftCandidates, concurrency);
670
+ const candidatesToCheck = result.candidates.slice(0, maxDriftCandidates);
671
+ const localCache = new Map();
672
+ driftSummary.skipped = Math.max(0, result.candidates.length - candidatesToCheck.length);
673
+ if (driftSummary.skipped > 0) {
674
+ result.warnings.push(`Skipped ${driftSummary.skipped} candidate(s) after maxDriftCandidates=${maxDriftCandidates} to keep drift checks bounded.`);
675
+ }
676
+ await mapWithConcurrency(candidatesToCheck, concurrency, async (candidate) => {
677
+ const drift = await createDriftCheck(candidate, { timeoutMs, maxBytes }, localCache);
678
+ driftSummary.checked += 1;
679
+ if (drift.status === 'matched') {
680
+ driftSummary.matched += 1;
681
+ }
682
+ else if (drift.status === 'signature_match') {
683
+ driftSummary.signatureMatched += 1;
684
+ }
685
+ else if (drift.status === 'different') {
686
+ driftSummary.different += 1;
687
+ }
688
+ else if (drift.status === 'too_large') {
689
+ driftSummary.tooLarge += 1;
690
+ }
691
+ else {
692
+ driftSummary.unavailable += 1;
693
+ }
694
+ applyDriftToCandidate(candidate, drift);
695
+ });
696
+ result.driftSummary = driftSummary;
697
+ result.candidates = result.candidates.sort((first, second) => second.score - first.score || first.assetPath.localeCompare(second.assetPath));
698
+ result.nextActions = buildNextActions(result.candidates, result.warnings);
699
+ return result;
700
+ }
701
+ //# sourceMappingURL=next-asset-mapper.js.map