browser-debug-mcp-bridge 1.10.0 → 1.11.1
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.
- package/README.md +267 -195
- package/apps/mcp-server/dist/db/events-repository.js +61 -9
- package/apps/mcp-server/dist/db/events-repository.js.map +1 -1
- package/apps/mcp-server/dist/db/migrations.js +470 -70
- package/apps/mcp-server/dist/db/migrations.js.map +1 -1
- package/apps/mcp-server/dist/db/schema.js +134 -1
- package/apps/mcp-server/dist/db/schema.js.map +1 -1
- package/apps/mcp-server/dist/document-response-rewriter.js +196 -0
- package/apps/mcp-server/dist/document-response-rewriter.js.map +1 -0
- package/apps/mcp-server/dist/json-rewrite.js +189 -0
- package/apps/mcp-server/dist/json-rewrite.js.map +1 -0
- package/apps/mcp-server/dist/main.js +339 -2
- package/apps/mcp-server/dist/main.js.map +1 -1
- package/apps/mcp-server/dist/mcp/server.js +2712 -246
- package/apps/mcp-server/dist/mcp/server.js.map +1 -1
- package/apps/mcp-server/dist/next-asset-mapper.js +701 -0
- package/apps/mcp-server/dist/next-asset-mapper.js.map +1 -0
- package/apps/mcp-server/dist/next-source-override-planner.js +601 -0
- package/apps/mcp-server/dist/next-source-override-planner.js.map +1 -0
- package/apps/mcp-server/dist/override-audit-contract.js +51 -0
- package/apps/mcp-server/dist/override-audit-contract.js.map +1 -0
- package/apps/mcp-server/dist/override-audit.js +740 -0
- package/apps/mcp-server/dist/override-audit.js.map +1 -0
- package/apps/mcp-server/dist/override-capabilities.js +157 -0
- package/apps/mcp-server/dist/override-capabilities.js.map +1 -0
- package/apps/mcp-server/dist/override-observed-assets.js +179 -0
- package/apps/mcp-server/dist/override-observed-assets.js.map +1 -0
- package/apps/mcp-server/dist/override-poc.js +336 -0
- package/apps/mcp-server/dist/override-poc.js.map +1 -0
- package/apps/mcp-server/dist/override-profile-generator.js +403 -0
- package/apps/mcp-server/dist/override-profile-generator.js.map +1 -0
- package/apps/mcp-server/dist/override-response-planner.js +559 -0
- package/apps/mcp-server/dist/override-response-planner.js.map +1 -0
- package/apps/mcp-server/dist/override-rule-types.js +32 -0
- package/apps/mcp-server/dist/override-rule-types.js.map +1 -0
- package/apps/mcp-server/dist/retention.js +4 -3
- package/apps/mcp-server/dist/retention.js.map +1 -1
- package/apps/mcp-server/dist/rsc-flight-patch-safety.js +269 -0
- package/apps/mcp-server/dist/rsc-flight-patch-safety.js.map +1 -0
- package/apps/mcp-server/dist/websocket/messages.js +5 -0
- package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
- package/apps/mcp-server/dist/websocket/websocket-server.js +10 -0
- package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -1
- package/apps/mcp-server/package.json +1 -0
- package/package.json +12 -1
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
-
import {
|
|
4
|
+
import { createHash, randomUUID } from 'crypto';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
5
6
|
import { dirname, resolve } from 'path';
|
|
6
7
|
import { z } from 'zod';
|
|
7
8
|
import { getConnection } from '../db/connection.js';
|
|
9
|
+
import { diagnoseOverridePoc, insertOverridePlanAudit, listOverridePlanAudits, listOverridePocRequests, listOverridePocRuns, } from '../override-audit.js';
|
|
10
|
+
import { createOverrideProfileConfig, OVERRIDE_PROFILE_ADAPTERS, } from '../override-profile-generator.js';
|
|
11
|
+
import { assertOverrideResponseRequestCaptureSafe, classifyOverrideResponseRequestCapability, } from '../override-capabilities.js';
|
|
12
|
+
import { getOverridePocConfigSummary } from '../override-poc.js';
|
|
13
|
+
import { normalizeOverrideRequestMethod } from '../override-rule-types.js';
|
|
14
|
+
import { mapNextOverrideAssetsWithDrift } from '../next-asset-mapper.js';
|
|
15
|
+
import { planNextSourceOverride } from '../next-source-override-planner.js';
|
|
16
|
+
import { listObservedOverrideAssets, persistObservedOverrideAssets } from '../override-observed-assets.js';
|
|
17
|
+
import { planOverrideResponsePatch } from '../override-response-planner.js';
|
|
8
18
|
function createDefaultMcpLogger() {
|
|
9
19
|
const write = (level, message, payload) => {
|
|
10
20
|
process.stderr.write(`${message} ${JSON.stringify({ level, ...payload })}\n`);
|
|
@@ -270,6 +280,13 @@ const TOOL_SCHEMAS = {
|
|
|
270
280
|
sessionId: { type: 'string' },
|
|
271
281
|
},
|
|
272
282
|
},
|
|
283
|
+
get_live_session_health: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
required: ['sessionId'],
|
|
286
|
+
properties: {
|
|
287
|
+
sessionId: { type: 'string' },
|
|
288
|
+
},
|
|
289
|
+
},
|
|
273
290
|
get_recent_events: {
|
|
274
291
|
type: 'object',
|
|
275
292
|
properties: {
|
|
@@ -470,13 +487,6 @@ const TOOL_SCHEMAS = {
|
|
|
470
487
|
maxTextLength: { type: 'number' },
|
|
471
488
|
},
|
|
472
489
|
},
|
|
473
|
-
get_live_session_health: {
|
|
474
|
-
type: 'object',
|
|
475
|
-
required: ['sessionId'],
|
|
476
|
-
properties: {
|
|
477
|
-
sessionId: { type: 'string' },
|
|
478
|
-
},
|
|
479
|
-
},
|
|
480
490
|
set_viewport: {
|
|
481
491
|
type: 'object',
|
|
482
492
|
required: ['sessionId', 'width', 'height'],
|
|
@@ -577,6 +587,224 @@ const TOOL_SCHEMAS = {
|
|
|
577
587
|
maxResponseBytes: { type: 'number' },
|
|
578
588
|
},
|
|
579
589
|
},
|
|
590
|
+
list_override_profiles: {
|
|
591
|
+
type: 'object',
|
|
592
|
+
properties: {},
|
|
593
|
+
},
|
|
594
|
+
create_override_profile: {
|
|
595
|
+
type: 'object',
|
|
596
|
+
required: ['targetBaseUrl'],
|
|
597
|
+
properties: {
|
|
598
|
+
adapter: { type: 'string' },
|
|
599
|
+
mode: { type: 'string' },
|
|
600
|
+
targetBaseUrl: { type: 'string' },
|
|
601
|
+
projectRoot: { type: 'string' },
|
|
602
|
+
assetRoot: { type: 'string' },
|
|
603
|
+
nextDir: { type: 'string' },
|
|
604
|
+
configPath: { type: 'string' },
|
|
605
|
+
profileId: { type: 'string' },
|
|
606
|
+
profileName: { type: 'string' },
|
|
607
|
+
enabled: { type: 'boolean' },
|
|
608
|
+
profileEnabled: { type: 'boolean' },
|
|
609
|
+
autoReload: { type: 'boolean' },
|
|
610
|
+
includeManifestFiles: { type: 'boolean' },
|
|
611
|
+
includeStaticFiles: { type: 'boolean' },
|
|
612
|
+
extensions: { type: 'array', items: { type: 'string' } },
|
|
613
|
+
maxRules: { type: 'number' },
|
|
614
|
+
writeConfig: { type: 'boolean' },
|
|
615
|
+
overwrite: { type: 'boolean' },
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
validate_override_profile: {
|
|
619
|
+
type: 'object',
|
|
620
|
+
properties: {
|
|
621
|
+
profileId: { type: 'string' },
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
preflight_overrides: {
|
|
625
|
+
type: 'object',
|
|
626
|
+
required: ['sessionId'],
|
|
627
|
+
properties: {
|
|
628
|
+
sessionId: { type: 'string' },
|
|
629
|
+
profileId: { type: 'string' },
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
observe_override_assets: {
|
|
633
|
+
type: 'object',
|
|
634
|
+
required: ['sessionId'],
|
|
635
|
+
properties: {
|
|
636
|
+
sessionId: { type: 'string' },
|
|
637
|
+
tabId: { type: 'number' },
|
|
638
|
+
includePerformance: { type: 'boolean' },
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
capture_override_response_body: {
|
|
642
|
+
type: 'object',
|
|
643
|
+
required: ['sessionId'],
|
|
644
|
+
properties: {
|
|
645
|
+
sessionId: { type: 'string' },
|
|
646
|
+
tabId: { type: 'number' },
|
|
647
|
+
targetUrl: { type: 'string' },
|
|
648
|
+
targetAssetUrl: { type: 'string' },
|
|
649
|
+
captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
|
|
650
|
+
triggerReload: { type: 'boolean' },
|
|
651
|
+
matchMode: { type: 'string', enum: ['exact', 'prefix'] },
|
|
652
|
+
ruleType: { type: 'string' },
|
|
653
|
+
requestMethod: { type: 'string' },
|
|
654
|
+
requestHeaders: { type: 'object' },
|
|
655
|
+
timeoutMs: { type: 'number' },
|
|
656
|
+
maxBodyBytes: { type: 'number' },
|
|
657
|
+
includeBody: { type: 'boolean' },
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
list_observed_override_assets: {
|
|
661
|
+
type: 'object',
|
|
662
|
+
required: ['sessionId'],
|
|
663
|
+
properties: {
|
|
664
|
+
sessionId: { type: 'string' },
|
|
665
|
+
limit: { type: 'number' },
|
|
666
|
+
sinceTimestamp: { type: 'number' },
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
map_next_override_assets: {
|
|
670
|
+
type: 'object',
|
|
671
|
+
required: ['projectRoot'],
|
|
672
|
+
properties: {
|
|
673
|
+
sessionId: { type: 'string' },
|
|
674
|
+
tabId: { type: 'number' },
|
|
675
|
+
projectRoot: { type: 'string' },
|
|
676
|
+
nextDir: { type: 'string' },
|
|
677
|
+
route: { type: 'string' },
|
|
678
|
+
sourcePaths: { type: 'array', items: { type: 'string' } },
|
|
679
|
+
observedAssets: { type: 'array', items: { type: 'object' } },
|
|
680
|
+
maxResults: { type: 'number' },
|
|
681
|
+
fetchProductionAssets: { type: 'boolean' },
|
|
682
|
+
productionFetchTimeoutMs: { type: 'number' },
|
|
683
|
+
maxProductionAssetBytes: { type: 'number' },
|
|
684
|
+
maxDriftCandidates: { type: 'number' },
|
|
685
|
+
productionFetchConcurrency: { type: 'number' },
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
plan_override_response_patch: {
|
|
689
|
+
type: 'object',
|
|
690
|
+
properties: {
|
|
691
|
+
sessionId: { type: 'string' },
|
|
692
|
+
tabId: { type: 'number' },
|
|
693
|
+
targetUrl: { type: 'string' },
|
|
694
|
+
targetAssetUrl: { type: 'string' },
|
|
695
|
+
captureMode: { type: 'string', enum: ['extension-fetch', 'cdp-response'] },
|
|
696
|
+
triggerReload: { type: 'boolean' },
|
|
697
|
+
ruleType: { type: 'string' },
|
|
698
|
+
requestMethod: { type: 'string' },
|
|
699
|
+
matchMode: { type: 'string', enum: ['exact', 'prefix'] },
|
|
700
|
+
requestHeaders: { type: 'object' },
|
|
701
|
+
timeoutMs: { type: 'number' },
|
|
702
|
+
contentType: { type: 'string' },
|
|
703
|
+
responseBodyText: { type: 'string' },
|
|
704
|
+
bodyText: { type: 'string' },
|
|
705
|
+
responseBodyBase64: { type: 'string' },
|
|
706
|
+
bodyBase64: { type: 'string' },
|
|
707
|
+
textPatches: { type: 'array', items: { type: 'object' } },
|
|
708
|
+
jsonPatches: { type: 'array', items: { type: 'object' } },
|
|
709
|
+
documentPatches: { type: 'array', items: { type: 'object' } },
|
|
710
|
+
maxBodyBytes: { type: 'number' },
|
|
711
|
+
outputRoot: { type: 'string' },
|
|
712
|
+
configPath: { type: 'string' },
|
|
713
|
+
writeBody: { type: 'boolean' },
|
|
714
|
+
writeConfig: { type: 'boolean' },
|
|
715
|
+
overwrite: { type: 'boolean' },
|
|
716
|
+
enabled: { type: 'boolean' },
|
|
717
|
+
profileEnabled: { type: 'boolean' },
|
|
718
|
+
autoReload: { type: 'boolean' },
|
|
719
|
+
profileId: { type: 'string' },
|
|
720
|
+
profileName: { type: 'string' },
|
|
721
|
+
ruleId: { type: 'string' },
|
|
722
|
+
includePreview: { type: 'boolean' },
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
plan_next_source_override: {
|
|
726
|
+
type: 'object',
|
|
727
|
+
required: ['projectRoot', 'sourceEdits'],
|
|
728
|
+
properties: {
|
|
729
|
+
sessionId: { type: 'string' },
|
|
730
|
+
tabId: { type: 'number' },
|
|
731
|
+
projectRoot: { type: 'string' },
|
|
732
|
+
nextDir: { type: 'string' },
|
|
733
|
+
route: { type: 'string' },
|
|
734
|
+
sourcePaths: { type: 'array', items: { type: 'string' } },
|
|
735
|
+
sourceEdits: { type: 'array', items: { type: 'object' } },
|
|
736
|
+
observedAssets: { type: 'array', items: { type: 'object' } },
|
|
737
|
+
configPath: { type: 'string' },
|
|
738
|
+
writeConfig: { type: 'boolean' },
|
|
739
|
+
overwrite: { type: 'boolean' },
|
|
740
|
+
enabled: { type: 'boolean' },
|
|
741
|
+
profileEnabled: { type: 'boolean' },
|
|
742
|
+
autoReload: { type: 'boolean' },
|
|
743
|
+
profileId: { type: 'string' },
|
|
744
|
+
profileName: { type: 'string' },
|
|
745
|
+
buildTimeoutMs: { type: 'number' },
|
|
746
|
+
maxRules: { type: 'number' },
|
|
747
|
+
fetchProductionAssets: { type: 'boolean' },
|
|
748
|
+
productionFetchTimeoutMs: { type: 'number' },
|
|
749
|
+
maxProductionAssetBytes: { type: 'number' },
|
|
750
|
+
maxDriftCandidates: { type: 'number' },
|
|
751
|
+
productionFetchConcurrency: { type: 'number' },
|
|
752
|
+
overlayTtlMs: { type: 'number' },
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
enable_overrides: {
|
|
756
|
+
type: 'object',
|
|
757
|
+
required: ['sessionId'],
|
|
758
|
+
properties: {
|
|
759
|
+
sessionId: { type: 'string' },
|
|
760
|
+
tabId: { type: 'number' },
|
|
761
|
+
profileId: { type: 'string' },
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
disable_overrides: {
|
|
765
|
+
type: 'object',
|
|
766
|
+
required: ['sessionId'],
|
|
767
|
+
properties: {
|
|
768
|
+
sessionId: { type: 'string' },
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
get_override_status: {
|
|
772
|
+
type: 'object',
|
|
773
|
+
properties: {
|
|
774
|
+
sessionId: { type: 'string' },
|
|
775
|
+
profileId: { type: 'string' },
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
get_override_request_log: {
|
|
779
|
+
type: 'object',
|
|
780
|
+
required: ['sessionId'],
|
|
781
|
+
properties: {
|
|
782
|
+
sessionId: { type: 'string' },
|
|
783
|
+
runId: { type: 'string' },
|
|
784
|
+
limit: { type: 'number' },
|
|
785
|
+
offset: { type: 'number' },
|
|
786
|
+
maxResponseBytes: { type: 'number' },
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
get_override_plan_log: {
|
|
790
|
+
type: 'object',
|
|
791
|
+
required: ['sessionId'],
|
|
792
|
+
properties: {
|
|
793
|
+
sessionId: { type: 'string' },
|
|
794
|
+
planId: { type: 'string' },
|
|
795
|
+
limit: { type: 'number' },
|
|
796
|
+
offset: { type: 'number' },
|
|
797
|
+
maxResponseBytes: { type: 'number' },
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
diagnose_overrides: {
|
|
801
|
+
type: 'object',
|
|
802
|
+
required: ['sessionId'],
|
|
803
|
+
properties: {
|
|
804
|
+
sessionId: { type: 'string' },
|
|
805
|
+
runId: { type: 'string' },
|
|
806
|
+
},
|
|
807
|
+
},
|
|
580
808
|
explain_last_failure: {
|
|
581
809
|
type: 'object',
|
|
582
810
|
required: ['sessionId'],
|
|
@@ -841,6 +1069,22 @@ const TOOL_DESCRIPTIONS = {
|
|
|
841
1069
|
wait_for_page_state: 'Poll compact page state until a structured assertion becomes true',
|
|
842
1070
|
capture_ui_snapshot: 'Capture redacted UI snapshot (DOM/styles/optional PNG) and persist it',
|
|
843
1071
|
get_live_console_logs: 'Read in-memory live console logs for a connected session',
|
|
1072
|
+
list_override_profiles: 'List configured browser override profiles',
|
|
1073
|
+
create_override_profile: 'Generate a candidate browser override profile from local build assets',
|
|
1074
|
+
validate_override_profile: 'Validate the current browser override profile and local asset readiness',
|
|
1075
|
+
preflight_overrides: 'Run production-safety checks before enabling browser overrides for a live session',
|
|
1076
|
+
observe_override_assets: 'Observe production render artifacts from a live extension tab',
|
|
1077
|
+
capture_override_response_body: 'Capture a bounded text response body from a live extension session for override planning, using extension fetch or explicit CDP response-stage capture',
|
|
1078
|
+
list_observed_override_assets: 'List persisted production render artifacts observed for a session',
|
|
1079
|
+
map_next_override_assets: 'Map observed production Next.js assets to local build chunks and source paths',
|
|
1080
|
+
plan_override_response_patch: 'Patch a supplied or live-captured text response body with literal textPatches or JSON Pointer jsonPatches and write an exact or prefix override rule for supported response types',
|
|
1081
|
+
plan_next_source_override: 'Apply source edits in a temp Next.js overlay build and plan exact browser override rules',
|
|
1082
|
+
enable_overrides: 'Enable browser overrides for a live extension session',
|
|
1083
|
+
disable_overrides: 'Disable browser overrides for a live extension session',
|
|
1084
|
+
get_override_status: 'Read live or persisted browser override status for a session',
|
|
1085
|
+
get_override_request_log: 'Read persisted browser override request audit rows',
|
|
1086
|
+
get_override_plan_log: 'Read persisted generated override plan audit rows with previews, hashes, and rollback metadata',
|
|
1087
|
+
diagnose_overrides: 'Diagnose persisted browser override runs and failure indicators',
|
|
844
1088
|
explain_last_failure: 'Explain the latest failure timeline',
|
|
845
1089
|
get_event_correlation: 'Correlate related events by window',
|
|
846
1090
|
list_snapshots: 'List snapshot metadata by session/time/trigger',
|
|
@@ -870,6 +1114,18 @@ const DEFAULT_NETWORK_POLL_TIMEOUT_MS = 15_000;
|
|
|
870
1114
|
const MAX_NETWORK_POLL_TIMEOUT_MS = 120_000;
|
|
871
1115
|
const DEFAULT_NETWORK_POLL_INTERVAL_MS = 250;
|
|
872
1116
|
const LIVE_SESSION_DISCONNECTED_CODE = 'LIVE_SESSION_DISCONNECTED';
|
|
1117
|
+
const OVERRIDE_LIVE_COMMAND_TIMEOUT_CODE = 'OVERRIDE_LIVE_COMMAND_TIMEOUT';
|
|
1118
|
+
const OVERRIDE_LIVE_COMMAND_FAILED_CODE = 'OVERRIDE_LIVE_COMMAND_FAILED';
|
|
1119
|
+
const STALE_LIVE_CONNECTION_GRACE_WINDOW_MS = 30 * 60 * 1000;
|
|
1120
|
+
const NOISE_SESSION_HOST_PATTERNS = [
|
|
1121
|
+
/(^|\.)adtrafficquality\.google$/i,
|
|
1122
|
+
/(^|\.)doubleclick\.net$/i,
|
|
1123
|
+
/(^|\.)googlesyndication\.com$/i,
|
|
1124
|
+
/(^|\.)googleadservices\.com$/i,
|
|
1125
|
+
/(^|\.)recaptcha\.net$/i,
|
|
1126
|
+
/(^|\.)gstatic\.com$/i,
|
|
1127
|
+
];
|
|
1128
|
+
const NOISE_SESSION_PATH_PATTERNS = [/\/sodar/i, /\/recaptcha/i, /runner\.html$/i];
|
|
873
1129
|
const NETWORK_CALL_SELECT_COLUMNS = `
|
|
874
1130
|
request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class, response_size_est,
|
|
875
1131
|
request_content_type, request_body_text, request_body_json, request_body_bytes, request_body_truncated, request_body_chunk_ref,
|
|
@@ -1076,128 +1332,1058 @@ function resolveLastUrl(payload) {
|
|
|
1076
1332
|
}
|
|
1077
1333
|
return undefined;
|
|
1078
1334
|
}
|
|
1079
|
-
function
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
sessionId: row.session_id,
|
|
1085
|
-
timestamp: row.ts,
|
|
1086
|
-
type: row.type,
|
|
1087
|
-
summary: describeEvent(row.type, payload),
|
|
1335
|
+
function classifySessionUrl(urlValue) {
|
|
1336
|
+
if (!urlValue) {
|
|
1337
|
+
return {
|
|
1338
|
+
kind: 'unknown',
|
|
1339
|
+
note: 'No session URL is available yet.',
|
|
1088
1340
|
};
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
const parsed = new URL(urlValue);
|
|
1344
|
+
const host = parsed.hostname.toLowerCase();
|
|
1345
|
+
const pathname = parsed.pathname.toLowerCase();
|
|
1346
|
+
const origin = parsed.origin;
|
|
1347
|
+
const isLocalhost = host === 'localhost' || host === '127.0.0.1';
|
|
1348
|
+
if (NOISE_SESSION_HOST_PATTERNS.some((pattern) => pattern.test(host))
|
|
1349
|
+
|| NOISE_SESSION_PATH_PATTERNS.some((pattern) => pattern.test(pathname))) {
|
|
1350
|
+
return {
|
|
1351
|
+
kind: 'likely_iframe_noise',
|
|
1352
|
+
note: 'Last URL looks like third-party iframe/ad traffic rather than the app surface.',
|
|
1353
|
+
origin,
|
|
1354
|
+
host,
|
|
1355
|
+
isLocalhost,
|
|
1356
|
+
};
|
|
1095
1357
|
}
|
|
1096
|
-
if (
|
|
1097
|
-
|
|
1358
|
+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
1359
|
+
return {
|
|
1360
|
+
kind: 'top_level_page',
|
|
1361
|
+
note: isLocalhost
|
|
1362
|
+
? 'Last URL looks like a local top-level app page.'
|
|
1363
|
+
: 'Last URL looks like a top-level app page.',
|
|
1364
|
+
origin,
|
|
1365
|
+
host,
|
|
1366
|
+
isLocalhost,
|
|
1367
|
+
};
|
|
1098
1368
|
}
|
|
1099
|
-
|
|
1369
|
+
}
|
|
1370
|
+
catch {
|
|
1371
|
+
return {
|
|
1372
|
+
kind: 'unknown',
|
|
1373
|
+
note: 'Session URL could not be parsed.',
|
|
1374
|
+
};
|
|
1100
1375
|
}
|
|
1101
1376
|
return {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
timestamp: row.ts,
|
|
1105
|
-
type: row.type,
|
|
1106
|
-
tabId: row.tab_id ?? (typeof payload.tabId === 'number' ? payload.tabId : undefined),
|
|
1107
|
-
origin: row.origin
|
|
1108
|
-
?? (typeof payload.origin === 'string' ? payload.origin : undefined)
|
|
1109
|
-
?? undefined,
|
|
1110
|
-
payload,
|
|
1377
|
+
kind: 'unknown',
|
|
1378
|
+
note: 'Session URL does not use an http(s) page origin.',
|
|
1111
1379
|
};
|
|
1112
1380
|
}
|
|
1113
|
-
function
|
|
1114
|
-
if (
|
|
1115
|
-
return
|
|
1381
|
+
function getSessionStatus(row) {
|
|
1382
|
+
if (row.ended_at) {
|
|
1383
|
+
return 'ended';
|
|
1116
1384
|
}
|
|
1117
|
-
if (
|
|
1118
|
-
return '
|
|
1385
|
+
if (row.paused_at) {
|
|
1386
|
+
return 'paused';
|
|
1119
1387
|
}
|
|
1120
|
-
return '
|
|
1388
|
+
return 'active';
|
|
1121
1389
|
}
|
|
1122
|
-
function
|
|
1123
|
-
|
|
1124
|
-
|
|
1390
|
+
function buildOverrideProfileRecords() {
|
|
1391
|
+
const summary = getOverridePocConfigSummary();
|
|
1392
|
+
return summary.profiles.map((profile) => ({
|
|
1393
|
+
profileId: profile.profileId,
|
|
1394
|
+
name: profile.name,
|
|
1395
|
+
active: profile.profileId === summary.activeProfileId,
|
|
1396
|
+
configEnabled: summary.configEnabled,
|
|
1397
|
+
enabled: profile.enabled,
|
|
1398
|
+
effectiveEnabled: summary.configEnabled && profile.enabled && profile.enabledRuleCount > 0,
|
|
1399
|
+
autoReload: profile.autoReload,
|
|
1400
|
+
configPath: summary.configPath,
|
|
1401
|
+
fileExists: profile.fileExists,
|
|
1402
|
+
ruleCount: profile.ruleCount,
|
|
1403
|
+
enabledRuleCount: profile.enabledRuleCount,
|
|
1404
|
+
rules: profile.rules,
|
|
1405
|
+
}));
|
|
1406
|
+
}
|
|
1407
|
+
function resolveOverrideProfileRecord(value) {
|
|
1408
|
+
const profiles = buildOverrideProfileRecords();
|
|
1409
|
+
const fallbackProfileId = typeof profiles[0]?.profileId === 'string' ? profiles[0].profileId : 'poc';
|
|
1410
|
+
const requestedProfileId = typeof value === 'string' && value.trim().length > 0 ? value.trim() : fallbackProfileId;
|
|
1411
|
+
const profile = profiles.find((candidate) => candidate.profileId === requestedProfileId);
|
|
1412
|
+
if (!profile) {
|
|
1413
|
+
throw new Error(`Unknown override profile: ${requestedProfileId}`);
|
|
1414
|
+
}
|
|
1415
|
+
return profile;
|
|
1416
|
+
}
|
|
1417
|
+
const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
1418
|
+
function sha256Text(value) {
|
|
1419
|
+
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
1420
|
+
}
|
|
1421
|
+
function isRecordWithRscFlightMetadata(value) {
|
|
1422
|
+
return isRecord(value)
|
|
1423
|
+
&& (value.productionMode === 'structured-flight-v1' && value.patchKind === 'string-value-text'
|
|
1424
|
+
|| value.productionMode === 'literal-response-v1' && value.patchKind === 'literal-text')
|
|
1425
|
+
&& value.source !== undefined
|
|
1426
|
+
&& value.patchKind !== undefined;
|
|
1427
|
+
}
|
|
1428
|
+
function normalizeRuleStringHeaders(value) {
|
|
1429
|
+
if (!isRecord(value)) {
|
|
1430
|
+
return undefined;
|
|
1125
1431
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1432
|
+
const headers = {};
|
|
1433
|
+
for (const [name, rawValue] of Object.entries(value)) {
|
|
1434
|
+
if (typeof rawValue !== 'string') {
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
const normalizedName = name.trim().toLowerCase();
|
|
1438
|
+
const normalizedValue = rawValue.trim();
|
|
1439
|
+
if (normalizedName.length > 0 && normalizedValue.length > 0) {
|
|
1440
|
+
headers[normalizedName] = normalizedValue;
|
|
1441
|
+
}
|
|
1128
1442
|
}
|
|
1129
|
-
return
|
|
1443
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
1130
1444
|
}
|
|
1131
|
-
function
|
|
1132
|
-
|
|
1133
|
-
|
|
1445
|
+
function getRscFlightRuleRequestHeaders(rule) {
|
|
1446
|
+
return isRecord(rule.rscFlight) ? normalizeRuleStringHeaders(rule.rscFlight.requestHeaders) : undefined;
|
|
1447
|
+
}
|
|
1448
|
+
function getOverrideRuleRequestHeaders(rule) {
|
|
1449
|
+
return normalizeRuleStringHeaders(rule.requestHeaders) ?? getRscFlightRuleRequestHeaders(rule);
|
|
1450
|
+
}
|
|
1451
|
+
function buildRscFlightRuleIssues(rule) {
|
|
1452
|
+
const ruleId = String(rule.ruleId ?? 'unknown');
|
|
1453
|
+
const issues = [];
|
|
1454
|
+
const rscFlight = rule.rscFlight;
|
|
1455
|
+
if (!isRecordWithRscFlightMetadata(rscFlight)) {
|
|
1456
|
+
return [{
|
|
1457
|
+
code: 'UNSUPPORTED_RSC_FLIGHT_RULE',
|
|
1458
|
+
severity: 'error',
|
|
1459
|
+
message: `Rule ${ruleId} targets a Next.js RSC flight response without production RSC metadata generated by the response planner.`,
|
|
1460
|
+
}];
|
|
1461
|
+
}
|
|
1462
|
+
const source = rscFlight.source;
|
|
1463
|
+
if (source !== 'cdp-response' && source !== 'extension-fetch') {
|
|
1464
|
+
issues.push({
|
|
1465
|
+
code: 'RSC_FLIGHT_METADATA_INVALID',
|
|
1466
|
+
severity: 'error',
|
|
1467
|
+
message: `Rule ${ruleId} RSC metadata source must be cdp-response or extension-fetch.`,
|
|
1468
|
+
});
|
|
1134
1469
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1470
|
+
if (!Array.isArray(rscFlight.textPatches) || rscFlight.textPatches.length === 0) {
|
|
1471
|
+
issues.push({
|
|
1472
|
+
code: 'RSC_FLIGHT_PATCHES_INVALID',
|
|
1473
|
+
severity: 'error',
|
|
1474
|
+
message: `Rule ${ruleId} RSC flight metadata must include string-value text patches.`,
|
|
1475
|
+
});
|
|
1138
1476
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1477
|
+
else {
|
|
1478
|
+
for (const [index, patch] of rscFlight.textPatches.entries()) {
|
|
1479
|
+
if (!isRecord(patch)
|
|
1480
|
+
|| typeof patch.search !== 'string'
|
|
1481
|
+
|| patch.search.length === 0
|
|
1482
|
+
|| typeof patch.replacement !== 'string'
|
|
1483
|
+
|| typeof patch.expectedCount !== 'number'
|
|
1484
|
+
|| !Number.isFinite(patch.expectedCount)
|
|
1485
|
+
|| patch.expectedCount < 0) {
|
|
1486
|
+
issues.push({
|
|
1487
|
+
code: 'RSC_FLIGHT_PATCHES_INVALID',
|
|
1488
|
+
severity: 'error',
|
|
1489
|
+
message: `Rule ${ruleId} RSC flight textPatches[${index}] is invalid.`,
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1144
1493
|
}
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1494
|
+
const requestMethod = normalizeOverrideRequestMethod(rule.requestMethod);
|
|
1495
|
+
const requestHeaders = getRscFlightRuleRequestHeaders(rule);
|
|
1496
|
+
const isCapturedPostRscFlight = requestMethod === 'POST' && requestHeaders?.rsc === '1';
|
|
1497
|
+
if (requestMethod !== 'GET' && !isCapturedPostRscFlight) {
|
|
1498
|
+
issues.push({
|
|
1499
|
+
code: 'RSC_FLIGHT_METHOD_UNSUPPORTED',
|
|
1500
|
+
severity: 'error',
|
|
1501
|
+
message: `Rule ${ruleId} RSC flight overrides only support GET requests or captured POST RSC response-stage patches.`,
|
|
1502
|
+
});
|
|
1151
1503
|
}
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
1504
|
+
const targetAssetUrl = typeof rule.targetAssetUrl === 'string' ? rule.targetAssetUrl : '';
|
|
1505
|
+
try {
|
|
1506
|
+
const parsed = new URL(targetAssetUrl);
|
|
1507
|
+
if (requestMethod === 'GET' && !parsed.searchParams.has('_rsc')) {
|
|
1508
|
+
issues.push({
|
|
1509
|
+
code: 'RSC_FLIGHT_TARGET_INVALID',
|
|
1510
|
+
severity: 'error',
|
|
1511
|
+
message: `Rule ${ruleId} RSC flight targetAssetUrl must include the _rsc search parameter.`,
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1155
1514
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1515
|
+
catch {
|
|
1516
|
+
issues.push({
|
|
1517
|
+
code: 'RSC_FLIGHT_TARGET_INVALID',
|
|
1518
|
+
severity: 'error',
|
|
1519
|
+
message: `Rule ${ruleId} RSC flight targetAssetUrl must be an absolute http(s) URL.`,
|
|
1520
|
+
});
|
|
1161
1521
|
}
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1522
|
+
const contentType = typeof rule.contentType === 'string' ? rule.contentType : '';
|
|
1523
|
+
const metadataContentType = typeof rscFlight.contentType === 'string' ? rscFlight.contentType : '';
|
|
1524
|
+
if (!contentType.toLowerCase().includes('text/x-component') || !metadataContentType.toLowerCase().includes('text/x-component')) {
|
|
1525
|
+
issues.push({
|
|
1526
|
+
code: 'RSC_FLIGHT_CONTENT_TYPE_INVALID',
|
|
1527
|
+
severity: 'error',
|
|
1528
|
+
message: `Rule ${ruleId} RSC flight overrides require text/x-component content types.`,
|
|
1529
|
+
});
|
|
1165
1530
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1531
|
+
const originalSha256 = typeof rscFlight.originalSha256 === 'string' ? rscFlight.originalSha256 : '';
|
|
1532
|
+
const patchedSha256 = typeof rscFlight.patchedSha256 === 'string' ? rscFlight.patchedSha256 : '';
|
|
1533
|
+
if (!SHA256_HEX_PATTERN.test(originalSha256) || !SHA256_HEX_PATTERN.test(patchedSha256) || originalSha256 === patchedSha256) {
|
|
1534
|
+
issues.push({
|
|
1535
|
+
code: 'RSC_FLIGHT_HASH_INVALID',
|
|
1536
|
+
severity: 'error',
|
|
1537
|
+
message: `Rule ${ruleId} RSC flight metadata must include distinct original and patched sha256 hashes.`,
|
|
1538
|
+
});
|
|
1171
1539
|
}
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1540
|
+
const patchedBytes = typeof rscFlight.patchedBytes === 'number' && Number.isFinite(rscFlight.patchedBytes)
|
|
1541
|
+
? Math.floor(rscFlight.patchedBytes)
|
|
1542
|
+
: null;
|
|
1543
|
+
if (patchedBytes === null || patchedBytes < 1) {
|
|
1544
|
+
issues.push({
|
|
1545
|
+
code: 'RSC_FLIGHT_BYTES_INVALID',
|
|
1546
|
+
severity: 'error',
|
|
1547
|
+
message: `Rule ${ruleId} RSC flight metadata must include a positive patchedBytes value.`,
|
|
1548
|
+
});
|
|
1175
1549
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
if (
|
|
1180
|
-
|
|
1550
|
+
const fileSizeBytes = typeof rule.fileSizeBytes === 'number' && Number.isFinite(rule.fileSizeBytes)
|
|
1551
|
+
? Math.floor(rule.fileSizeBytes)
|
|
1552
|
+
: null;
|
|
1553
|
+
if (patchedBytes !== null && fileSizeBytes !== null && patchedBytes !== fileSizeBytes) {
|
|
1554
|
+
issues.push({
|
|
1555
|
+
code: 'RSC_FLIGHT_LOCAL_FILE_MISMATCH',
|
|
1556
|
+
severity: 'error',
|
|
1557
|
+
message: `Rule ${ruleId} local RSC file size does not match patchedBytes metadata.`,
|
|
1558
|
+
});
|
|
1181
1559
|
}
|
|
1182
|
-
const
|
|
1183
|
-
if (
|
|
1184
|
-
|
|
1560
|
+
const resolvedLocalFilePath = typeof rule.resolvedLocalFilePath === 'string' ? rule.resolvedLocalFilePath : '';
|
|
1561
|
+
if (resolvedLocalFilePath && existsSync(resolvedLocalFilePath) && SHA256_HEX_PATTERN.test(patchedSha256)) {
|
|
1562
|
+
const body = readFileSync(resolvedLocalFilePath, 'utf8');
|
|
1563
|
+
if (!/(^|\n)\d+:/u.test(body)) {
|
|
1564
|
+
issues.push({
|
|
1565
|
+
code: 'RSC_FLIGHT_BODY_INVALID',
|
|
1566
|
+
severity: 'error',
|
|
1567
|
+
message: `Rule ${ruleId} local RSC file does not match the supported Flight payload shape.`,
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
if (sha256Text(body) !== patchedSha256) {
|
|
1571
|
+
issues.push({
|
|
1572
|
+
code: 'RSC_FLIGHT_LOCAL_FILE_MISMATCH',
|
|
1573
|
+
severity: 'error',
|
|
1574
|
+
message: `Rule ${ruleId} local RSC file hash does not match patchedSha256 metadata.`,
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1185
1577
|
}
|
|
1186
|
-
return
|
|
1578
|
+
return issues;
|
|
1187
1579
|
}
|
|
1188
|
-
function
|
|
1189
|
-
|
|
1190
|
-
|
|
1580
|
+
function buildOverrideProfileIssues(profile) {
|
|
1581
|
+
const issues = [];
|
|
1582
|
+
const rules = Array.isArray(profile.rules)
|
|
1583
|
+
? profile.rules.filter((rule) => isRecord(rule))
|
|
1584
|
+
: [];
|
|
1585
|
+
if (profile.configEnabled !== true) {
|
|
1586
|
+
issues.push({
|
|
1587
|
+
code: 'CONFIG_DISABLED',
|
|
1588
|
+
severity: 'warning',
|
|
1589
|
+
message: 'The override config is disabled and cannot replace requests until enabled.',
|
|
1590
|
+
});
|
|
1191
1591
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1592
|
+
if (profile.enabled !== true) {
|
|
1593
|
+
issues.push({
|
|
1594
|
+
code: 'PROFILE_DISABLED',
|
|
1595
|
+
severity: 'warning',
|
|
1596
|
+
message: 'The override profile is disabled and cannot replace requests until enabled.',
|
|
1597
|
+
});
|
|
1198
1598
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1599
|
+
if (rules.length === 0 || !rules.some((rule) => rule.enabled === true)) {
|
|
1600
|
+
issues.push({
|
|
1601
|
+
code: 'NO_ENABLED_RULES',
|
|
1602
|
+
severity: 'error',
|
|
1603
|
+
message: 'The override profile has no enabled rules.',
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
for (const rule of rules) {
|
|
1607
|
+
if (rule.enabled !== true) {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (typeof rule.targetAssetUrl !== 'string' || !rule.targetAssetUrl.startsWith('http')) {
|
|
1611
|
+
issues.push({
|
|
1612
|
+
code: 'TARGET_URL_INVALID',
|
|
1613
|
+
severity: 'error',
|
|
1614
|
+
message: `Rule ${String(rule.ruleId ?? 'unknown')} targetAssetUrl must be an absolute http(s) URL.`,
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
if (rule.fileExists !== true) {
|
|
1618
|
+
issues.push({
|
|
1619
|
+
code: 'LOCAL_FILE_MISSING',
|
|
1620
|
+
severity: 'error',
|
|
1621
|
+
message: `Rule ${String(rule.ruleId ?? 'unknown')} local override file does not exist.`,
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
issues.push(...classifyOverrideResponseRequestCapability({
|
|
1625
|
+
ruleId: rule.ruleId,
|
|
1626
|
+
requestMethod: rule.requestMethod,
|
|
1627
|
+
requestHeaders: getOverrideRuleRequestHeaders(rule),
|
|
1628
|
+
ruleType: rule.ruleType,
|
|
1629
|
+
}).issues.map((issue) => ({ ...issue })));
|
|
1630
|
+
if (rule.ruleType === 'rsc-flight') {
|
|
1631
|
+
issues.push(...buildRscFlightRuleIssues(rule));
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return issues;
|
|
1635
|
+
}
|
|
1636
|
+
function buildOverrideProfileNextActions(profile, issues) {
|
|
1637
|
+
if (issues.some((issue) => issue.code === 'SERVER_ACTION_UNSUPPORTED')) {
|
|
1638
|
+
return [{
|
|
1639
|
+
code: 'REPLAN_SERVER_ACTION_OVERRIDE',
|
|
1640
|
+
message: 'Server actions stay unsupported in production override mode; replace the flow with a GET document/data/API response path instead.',
|
|
1641
|
+
}];
|
|
1642
|
+
}
|
|
1643
|
+
if (issues.some((issue) => issue.code === 'MUTATION_REPLAY_UNSUPPORTED')) {
|
|
1644
|
+
return [{
|
|
1645
|
+
code: 'REPLAN_MUTATION_OVERRIDE',
|
|
1646
|
+
message: 'Mutation responses are not replay-safe; move the override to a GET document/data/API response or remove the non-GET rule.',
|
|
1647
|
+
}];
|
|
1648
|
+
}
|
|
1649
|
+
if (issues.some((issue) => issue.code === 'UNSAFE_REQUEST_METHOD')) {
|
|
1650
|
+
return [{
|
|
1651
|
+
code: 'REPLAN_GET_ONLY_OVERRIDE',
|
|
1652
|
+
message: 'Response override rules are production-safe only for GET requests; regenerate or remove non-GET rules.',
|
|
1653
|
+
}];
|
|
1654
|
+
}
|
|
1655
|
+
if (issues.some((issue) => issue.code === 'LOCAL_FILE_MISSING')) {
|
|
1656
|
+
return [{
|
|
1657
|
+
code: 'REBUILD_OR_FIX_LOCAL_PATHS',
|
|
1658
|
+
message: 'Rebuild the local app or fix localFilePath values before enabling overrides.',
|
|
1659
|
+
}];
|
|
1660
|
+
}
|
|
1661
|
+
if (issues.some((issue) => issue.code === 'NO_ENABLED_RULES')) {
|
|
1662
|
+
return [{
|
|
1663
|
+
code: 'ENABLE_RULES',
|
|
1664
|
+
message: 'Enable at least one rule in the selected override profile.',
|
|
1665
|
+
}];
|
|
1666
|
+
}
|
|
1667
|
+
if (issues.some((issue) => issue.code === 'TARGET_URL_INVALID')) {
|
|
1668
|
+
return [{
|
|
1669
|
+
code: 'FIX_TARGET_URLS',
|
|
1670
|
+
message: 'Use absolute http(s) production URLs for every targetAssetUrl.',
|
|
1671
|
+
}];
|
|
1672
|
+
}
|
|
1673
|
+
if (issues.some((issue) => typeof issue.code === 'string' && issue.code.startsWith('RSC_FLIGHT_'))
|
|
1674
|
+
|| issues.some((issue) => issue.code === 'UNSUPPORTED_RSC_FLIGHT_RULE')) {
|
|
1675
|
+
return [{
|
|
1676
|
+
code: 'REPLAN_RSC_RESPONSE_OVERRIDE',
|
|
1677
|
+
message: 'Regenerate the RSC rule with plan_override_response_patch from a captured text/x-component response body.',
|
|
1678
|
+
}];
|
|
1679
|
+
}
|
|
1680
|
+
if (profile.configEnabled !== true) {
|
|
1681
|
+
return [{
|
|
1682
|
+
code: 'ENABLE_CONFIG',
|
|
1683
|
+
message: 'Set the root override config enabled=true after reviewing the profile.',
|
|
1684
|
+
}];
|
|
1685
|
+
}
|
|
1686
|
+
if (profile.enabled !== true) {
|
|
1687
|
+
return [{
|
|
1688
|
+
code: 'ENABLE_PROFILE',
|
|
1689
|
+
message: 'Set the selected override profile enabled=true after reviewing its rules.',
|
|
1690
|
+
}];
|
|
1691
|
+
}
|
|
1692
|
+
return [{
|
|
1693
|
+
code: 'ENABLE_OVERRIDES',
|
|
1694
|
+
message: 'Enable overrides on a connected session, then reload the target tab if needed.',
|
|
1695
|
+
}];
|
|
1696
|
+
}
|
|
1697
|
+
function hasEnabledExperimentalRscFlightRule(profile) {
|
|
1698
|
+
const rules = Array.isArray(profile.rules)
|
|
1699
|
+
? profile.rules.filter((rule) => isRecord(rule))
|
|
1700
|
+
: [];
|
|
1701
|
+
return rules.some((rule) => {
|
|
1702
|
+
return rule.enabled === true
|
|
1703
|
+
&& rule.ruleType === 'rsc-flight'
|
|
1704
|
+
&& rule.allowExperimentalRscFlightFulfillment === true;
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
function canBypassPreflightForExperimentalRsc(profile, blockingCodes) {
|
|
1708
|
+
const allowedExperimentalBlockers = new Set([
|
|
1709
|
+
'UNSUPPORTED_RSC_FLIGHT_RULE',
|
|
1710
|
+
'NO_OBSERVED_ASSETS',
|
|
1711
|
+
'TARGET_ASSET_NOT_OBSERVED',
|
|
1712
|
+
]);
|
|
1713
|
+
return blockingCodes.length > 0
|
|
1714
|
+
&& blockingCodes.includes('UNSUPPORTED_RSC_FLIGHT_RULE')
|
|
1715
|
+
&& blockingCodes.every((code) => allowedExperimentalBlockers.has(code))
|
|
1716
|
+
&& hasEnabledExperimentalRscFlightRule(profile);
|
|
1717
|
+
}
|
|
1718
|
+
const OVERRIDE_VARIANT_HEADER_ALLOWLIST = new Set([
|
|
1719
|
+
'accept',
|
|
1720
|
+
'content-type',
|
|
1721
|
+
'next-router-prefetch',
|
|
1722
|
+
'next-router-state-tree',
|
|
1723
|
+
'purpose',
|
|
1724
|
+
'rsc',
|
|
1725
|
+
'x-nextjs-data',
|
|
1726
|
+
]);
|
|
1727
|
+
function normalizeOverrideVariantHeaders(value) {
|
|
1728
|
+
if (!isRecord(value)) {
|
|
1729
|
+
return {};
|
|
1730
|
+
}
|
|
1731
|
+
const normalized = {};
|
|
1732
|
+
for (const [rawName, rawValue] of Object.entries(value)) {
|
|
1733
|
+
const name = rawName.trim().toLowerCase();
|
|
1734
|
+
if (!OVERRIDE_VARIANT_HEADER_ALLOWLIST.has(name)) {
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
if (typeof rawValue === 'string' && rawValue.trim().length > 0) {
|
|
1738
|
+
normalized[name] = rawValue.trim();
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
if (typeof rawValue === 'number' || typeof rawValue === 'boolean') {
|
|
1742
|
+
normalized[name] = String(rawValue);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return normalized;
|
|
1746
|
+
}
|
|
1747
|
+
function buildOverrideVariantContext(options) {
|
|
1748
|
+
const targetUrl = normalizeOptionalString(options.targetUrl);
|
|
1749
|
+
if (!targetUrl) {
|
|
1750
|
+
return null;
|
|
1751
|
+
}
|
|
1752
|
+
const requestMethod = normalizeOverrideRequestMethod(options.requestMethod);
|
|
1753
|
+
const matchMode = normalizeOptionalString(options.matchMode) ?? 'exact';
|
|
1754
|
+
const ruleType = normalizeOptionalString(options.ruleType) ?? 'document';
|
|
1755
|
+
const captureMode = normalizeOptionalString(options.captureMode);
|
|
1756
|
+
const source = normalizeOptionalString(options.source);
|
|
1757
|
+
const headers = normalizeOverrideVariantHeaders(options.requestHeaders);
|
|
1758
|
+
const isPrefetchVariant = headers['next-router-prefetch'] === '1'
|
|
1759
|
+
|| headers.purpose?.toLowerCase() === 'prefetch';
|
|
1760
|
+
const isRscRequest = ruleType === 'rsc-flight' || headers.rsc === '1';
|
|
1761
|
+
let isNextDataRequest = ruleType === 'next-data' || headers['x-nextjs-data'] === '1';
|
|
1762
|
+
let origin;
|
|
1763
|
+
let pathname;
|
|
1764
|
+
let searchParams = [];
|
|
1765
|
+
try {
|
|
1766
|
+
const parsed = new URL(targetUrl);
|
|
1767
|
+
origin = parsed.origin;
|
|
1768
|
+
pathname = parsed.pathname;
|
|
1769
|
+
searchParams = Array.from(parsed.searchParams.entries()).map(([name, value]) => ({ name, value }));
|
|
1770
|
+
if (pathname.startsWith('/_next/data/')) {
|
|
1771
|
+
isNextDataRequest = true;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
catch {
|
|
1775
|
+
pathname = undefined;
|
|
1776
|
+
}
|
|
1777
|
+
const searchParamKeys = [...new Set(searchParams.map((entry) => entry.name))].sort();
|
|
1778
|
+
const variantBasis = {
|
|
1779
|
+
targetUrl,
|
|
1780
|
+
origin: origin ?? null,
|
|
1781
|
+
pathname: pathname ?? null,
|
|
1782
|
+
searchParams,
|
|
1783
|
+
requestMethod,
|
|
1784
|
+
matchMode,
|
|
1785
|
+
ruleType,
|
|
1786
|
+
captureMode: captureMode ?? null,
|
|
1787
|
+
source: source ?? null,
|
|
1788
|
+
triggerReload: options.triggerReload === true,
|
|
1789
|
+
headers,
|
|
1790
|
+
isPrefetchVariant,
|
|
1791
|
+
isRscRequest,
|
|
1792
|
+
isNextDataRequest,
|
|
1793
|
+
};
|
|
1794
|
+
return {
|
|
1795
|
+
...variantBasis,
|
|
1796
|
+
searchParamKeys,
|
|
1797
|
+
variantKey: sha256Text(JSON.stringify(variantBasis)),
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
function extractPlanVariantContext(plan) {
|
|
1801
|
+
if (isRecord(plan.patchSummary) && isRecord(plan.patchSummary.variantContext)) {
|
|
1802
|
+
return plan.patchSummary.variantContext;
|
|
1803
|
+
}
|
|
1804
|
+
if (isRecord(plan.capturedFromLiveSession)) {
|
|
1805
|
+
if (isRecord(plan.capturedFromLiveSession.variantContext)) {
|
|
1806
|
+
return plan.capturedFromLiveSession.variantContext;
|
|
1807
|
+
}
|
|
1808
|
+
return buildOverrideVariantContext({
|
|
1809
|
+
targetUrl: plan.capturedFromLiveSession.targetUrl ?? plan.targetAssetUrl,
|
|
1810
|
+
requestMethod: plan.capturedFromLiveSession.requestMethod ?? plan.requestMethod,
|
|
1811
|
+
matchMode: plan.capturedFromLiveSession.matchMode ?? plan.matchMode,
|
|
1812
|
+
ruleType: plan.capturedFromLiveSession.ruleType ?? plan.ruleType,
|
|
1813
|
+
captureMode: plan.capturedFromLiveSession.captureMode,
|
|
1814
|
+
source: plan.capturedFromLiveSession.source,
|
|
1815
|
+
triggerReload: plan.capturedFromLiveSession.triggerReload,
|
|
1816
|
+
requestHeaders: plan.capturedFromLiveSession.requestHeaders,
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
return buildOverrideVariantContext({
|
|
1820
|
+
targetUrl: plan.targetAssetUrl,
|
|
1821
|
+
requestMethod: plan.requestMethod,
|
|
1822
|
+
matchMode: plan.matchMode,
|
|
1823
|
+
ruleType: plan.ruleType,
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
function pushOverridePreflightIssue(issues, issue) {
|
|
1827
|
+
const code = typeof issue.code === 'string' ? issue.code : '';
|
|
1828
|
+
const source = typeof issue.source === 'string' ? issue.source : '';
|
|
1829
|
+
const message = typeof issue.message === 'string' ? issue.message : '';
|
|
1830
|
+
if (issues.some((existing) => existing.code === code && existing.source === source && existing.message === message)) {
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
issues.push(issue);
|
|
1834
|
+
}
|
|
1835
|
+
function getPreflightIssues(preflight) {
|
|
1836
|
+
return Array.isArray(preflight?.issues)
|
|
1837
|
+
? preflight.issues.filter((issue) => isRecord(issue))
|
|
1838
|
+
: [];
|
|
1839
|
+
}
|
|
1840
|
+
function getBlockingPreflightCodes(preflight) {
|
|
1841
|
+
return getPreflightIssues(preflight)
|
|
1842
|
+
.filter((issue) => issue.severity === 'error')
|
|
1843
|
+
.map((issue) => String(issue.code ?? 'UNKNOWN'));
|
|
1844
|
+
}
|
|
1845
|
+
function hasPreflightIssue(preflight, codes) {
|
|
1846
|
+
const expected = new Set(codes);
|
|
1847
|
+
return getPreflightIssues(preflight).some((issue) => expected.has(String(issue.code ?? '')));
|
|
1848
|
+
}
|
|
1849
|
+
function shouldRefreshObservedAssetsForEnable(preflight) {
|
|
1850
|
+
const assetReadinessCodes = new Set([
|
|
1851
|
+
'NO_OBSERVED_ASSETS',
|
|
1852
|
+
'TARGET_ASSET_NOT_OBSERVED',
|
|
1853
|
+
'SESSION_SCOPE_DRIFT',
|
|
1854
|
+
]);
|
|
1855
|
+
const blockingCodes = getBlockingPreflightCodes(preflight);
|
|
1856
|
+
if (blockingCodes.length === 0 || !blockingCodes.every((code) => assetReadinessCodes.has(code))) {
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
return hasPreflightIssue(preflight, [
|
|
1860
|
+
'NO_OBSERVED_ASSETS',
|
|
1861
|
+
'TARGET_ASSET_NOT_OBSERVED',
|
|
1862
|
+
'SESSION_SCOPE_DRIFT',
|
|
1863
|
+
]);
|
|
1864
|
+
}
|
|
1865
|
+
function buildOverridePreflight(options) {
|
|
1866
|
+
const session = options.db
|
|
1867
|
+
.prepare(`
|
|
1868
|
+
SELECT
|
|
1869
|
+
session_id,
|
|
1870
|
+
created_at,
|
|
1871
|
+
last_seen_at,
|
|
1872
|
+
paused_at,
|
|
1873
|
+
ended_at,
|
|
1874
|
+
tab_id,
|
|
1875
|
+
window_id,
|
|
1876
|
+
url_start,
|
|
1877
|
+
url_last,
|
|
1878
|
+
user_agent,
|
|
1879
|
+
viewport_w,
|
|
1880
|
+
viewport_h,
|
|
1881
|
+
dpr,
|
|
1882
|
+
safe_mode,
|
|
1883
|
+
pinned
|
|
1884
|
+
FROM sessions
|
|
1885
|
+
WHERE session_id = ?
|
|
1886
|
+
LIMIT 1
|
|
1887
|
+
`)
|
|
1888
|
+
.get(options.sessionId);
|
|
1889
|
+
const profile = resolveOverrideProfileRecord(options.profileId);
|
|
1890
|
+
const issues = [];
|
|
1891
|
+
const observedAssets = session
|
|
1892
|
+
? listObservedOverrideAssets(options.db, { sessionId: options.sessionId, limit: 200 })
|
|
1893
|
+
: [];
|
|
1894
|
+
const latestRun = session ? listOverridePocRuns(options.db, options.sessionId, 1, 0).runs[0] ?? null : null;
|
|
1895
|
+
const recentPlans = session
|
|
1896
|
+
? listOverridePlanAudits(options.db, { sessionId: options.sessionId, limit: 5, offset: 0 }).plans
|
|
1897
|
+
: [];
|
|
1898
|
+
const variantContexts = [...new Map(recentPlans
|
|
1899
|
+
.map((plan) => extractPlanVariantContext(plan))
|
|
1900
|
+
.filter((context) => context !== null)
|
|
1901
|
+
.map((context) => [String(context.variantKey ?? JSON.stringify(context)), context])).values()];
|
|
1902
|
+
const sessionState = options.getSessionConnectionState?.(options.sessionId);
|
|
1903
|
+
const hasLiveConnectionLookup = typeof options.getSessionConnectionState === 'function';
|
|
1904
|
+
const diagnosis = session ? diagnoseOverridePoc(options.db, options.sessionId, latestRun?.runId) : null;
|
|
1905
|
+
const observedAssetTabs = [...new Set(observedAssets
|
|
1906
|
+
.map((asset) => asset.tabId)
|
|
1907
|
+
.filter((tabId) => typeof tabId === 'number' && Number.isFinite(tabId)))].sort((a, b) => a - b);
|
|
1908
|
+
const observedAssetPageUrls = [...new Set(observedAssets
|
|
1909
|
+
.map((asset) => asset.pageUrl)
|
|
1910
|
+
.filter((pageUrl) => typeof pageUrl === 'string' && pageUrl.trim().length > 0))].slice(0, 5);
|
|
1911
|
+
const sessionTabId = typeof session?.tab_id === 'number' ? session.tab_id : undefined;
|
|
1912
|
+
const observedAssetsWithKnownTabs = observedAssets.filter((asset) => typeof asset.tabId === 'number');
|
|
1913
|
+
const topLevelScopeLikely = sessionTabId === undefined
|
|
1914
|
+
|| observedAssets.length === 0
|
|
1915
|
+
|| observedAssetsWithKnownTabs.length === 0
|
|
1916
|
+
|| observedAssetsWithKnownTabs.some((asset) => asset.tabId === sessionTabId);
|
|
1917
|
+
for (const issue of buildOverrideProfileIssues(profile)) {
|
|
1918
|
+
pushOverridePreflightIssue(issues, { ...issue, source: 'profile' });
|
|
1919
|
+
}
|
|
1920
|
+
if (!session) {
|
|
1921
|
+
pushOverridePreflightIssue(issues, {
|
|
1922
|
+
code: 'SESSION_NOT_FOUND',
|
|
1923
|
+
severity: 'error',
|
|
1924
|
+
source: 'session',
|
|
1925
|
+
message: `Session not found: ${options.sessionId}`,
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
else {
|
|
1929
|
+
const sessionStatus = getSessionStatus(session);
|
|
1930
|
+
if (sessionStatus === 'paused') {
|
|
1931
|
+
pushOverridePreflightIssue(issues, {
|
|
1932
|
+
code: 'SESSION_PAUSED',
|
|
1933
|
+
severity: 'error',
|
|
1934
|
+
source: 'session',
|
|
1935
|
+
message: `Session ${options.sessionId} is paused and cannot enable overrides until it resumes.`,
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
if (sessionStatus === 'ended') {
|
|
1939
|
+
pushOverridePreflightIssue(issues, {
|
|
1940
|
+
code: 'SESSION_ENDED',
|
|
1941
|
+
severity: 'error',
|
|
1942
|
+
source: 'session',
|
|
1943
|
+
message: `Session ${options.sessionId} has ended and cannot enable overrides.`,
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
if (hasLiveConnectionLookup && (!sessionState || sessionState.connected !== true)) {
|
|
1947
|
+
pushOverridePreflightIssue(issues, {
|
|
1948
|
+
code: LIVE_SESSION_DISCONNECTED_CODE,
|
|
1949
|
+
severity: 'error',
|
|
1950
|
+
source: 'connection',
|
|
1951
|
+
message: sessionState
|
|
1952
|
+
? `Session ${options.sessionId} is not currently connected to the live extension bridge. Last disconnect reason: ${sessionState.disconnectReason ?? 'unknown'}.`
|
|
1953
|
+
: `Session ${options.sessionId} has no current live extension connection state.`,
|
|
1954
|
+
disconnectedAt: sessionState?.disconnectedAt,
|
|
1955
|
+
disconnectReason: sessionState?.disconnectReason,
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
const enabledRules = Array.isArray(profile.rules)
|
|
1960
|
+
? profile.rules.filter((rule) => isRecord(rule) && rule.enabled === true)
|
|
1961
|
+
: [];
|
|
1962
|
+
const enabledRuleAssetReadiness = enabledRules
|
|
1963
|
+
.map((rule) => {
|
|
1964
|
+
const targetAssetUrl = normalizeOptionalString(rule.targetAssetUrl);
|
|
1965
|
+
if (!targetAssetUrl) {
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
const requestMethod = normalizeOverrideRequestMethod(rule.requestMethod);
|
|
1969
|
+
const matchMode = String(rule.matchMode ?? 'exact');
|
|
1970
|
+
const matchingAssets = observedAssets.filter((asset) => {
|
|
1971
|
+
const methodMatches = normalizeOverrideRequestMethod(asset.requestMethod) === requestMethod;
|
|
1972
|
+
if (!methodMatches) {
|
|
1973
|
+
return false;
|
|
1974
|
+
}
|
|
1975
|
+
return matchMode === 'prefix'
|
|
1976
|
+
? asset.url.startsWith(targetAssetUrl)
|
|
1977
|
+
: asset.url === targetAssetUrl;
|
|
1978
|
+
});
|
|
1979
|
+
return {
|
|
1980
|
+
ruleId: String(rule.ruleId ?? 'unknown'),
|
|
1981
|
+
targetAssetUrl,
|
|
1982
|
+
requestMethod,
|
|
1983
|
+
matchMode,
|
|
1984
|
+
captureProven: rule.ruleType === 'rsc-flight' && isRecordWithRscFlightMetadata(rule.rscFlight),
|
|
1985
|
+
matchingAssets,
|
|
1986
|
+
};
|
|
1987
|
+
})
|
|
1988
|
+
.filter((readiness) => readiness !== null);
|
|
1989
|
+
const matchedTargetAssetCount = enabledRuleAssetReadiness.filter((readiness) => readiness.matchingAssets.length > 0).length;
|
|
1990
|
+
const capturedTargetAssetCount = enabledRuleAssetReadiness
|
|
1991
|
+
.filter((readiness) => readiness.matchingAssets.length === 0 && readiness.captureProven)
|
|
1992
|
+
.length;
|
|
1993
|
+
const unobservedTargetAssetCount = enabledRuleAssetReadiness.length - matchedTargetAssetCount;
|
|
1994
|
+
const unsatisfiedTargetAssetCount = enabledRuleAssetReadiness.length - matchedTargetAssetCount - capturedTargetAssetCount;
|
|
1995
|
+
const targetAssetObserved = observedAssets.length > 0 && matchedTargetAssetCount > 0;
|
|
1996
|
+
const targetAssetReadinessSatisfied = observedAssets.length > 0
|
|
1997
|
+
&& (enabledRuleAssetReadiness.length === 0 || matchedTargetAssetCount > 0 || capturedTargetAssetCount > 0);
|
|
1998
|
+
const anyServiceWorkerControlled = observedAssets.some((asset) => asset.serviceWorkerControlled);
|
|
1999
|
+
const cspMetaTags = [...new Set(observedAssets.flatMap((asset) => asset.cspMetaTags))];
|
|
2000
|
+
if (observedAssets.length === 0) {
|
|
2001
|
+
pushOverridePreflightIssue(issues, {
|
|
2002
|
+
code: 'NO_OBSERVED_ASSETS',
|
|
2003
|
+
severity: 'error',
|
|
2004
|
+
source: 'observed-assets',
|
|
2005
|
+
message: 'No observed production assets are stored for this session yet; the target route is not capture-ready for override enablement.',
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
else if (!topLevelScopeLikely) {
|
|
2009
|
+
pushOverridePreflightIssue(issues, {
|
|
2010
|
+
code: 'SESSION_SCOPE_DRIFT',
|
|
2011
|
+
severity: 'error',
|
|
2012
|
+
source: 'observed-assets',
|
|
2013
|
+
message: `Observed override assets were recorded only for tab(s) ${observedAssetTabs.join(', ')}, but the session top-level tab is ${sessionTabId}.`,
|
|
2014
|
+
observedAssetTabs,
|
|
2015
|
+
sessionTabId,
|
|
2016
|
+
observedPageUrls: observedAssetPageUrls,
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
if (observedAssets.length > 0 && enabledRuleAssetReadiness.length > 0 && matchedTargetAssetCount === 0 && capturedTargetAssetCount === 0) {
|
|
2020
|
+
const sampleTargets = enabledRuleAssetReadiness.slice(0, 5).map((readiness) => ({
|
|
2021
|
+
ruleId: readiness.ruleId,
|
|
2022
|
+
requestMethod: readiness.requestMethod,
|
|
2023
|
+
matchMode: readiness.matchMode,
|
|
2024
|
+
targetAssetUrl: readiness.targetAssetUrl,
|
|
2025
|
+
}));
|
|
2026
|
+
pushOverridePreflightIssue(issues, {
|
|
2027
|
+
code: 'TARGET_ASSET_NOT_OBSERVED',
|
|
2028
|
+
severity: 'error',
|
|
2029
|
+
source: 'observed-assets',
|
|
2030
|
+
message: enabledRuleAssetReadiness.length === 1
|
|
2031
|
+
? `Rule ${enabledRuleAssetReadiness[0].ruleId} target asset was not observed for ${enabledRuleAssetReadiness[0].requestMethod} ${enabledRuleAssetReadiness[0].targetAssetUrl}.`
|
|
2032
|
+
: `None of the ${enabledRuleAssetReadiness.length} enabled override targets were observed for this session.`,
|
|
2033
|
+
checkedTargetAssetCount: enabledRuleAssetReadiness.length,
|
|
2034
|
+
sampleTargets,
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
for (const readiness of enabledRuleAssetReadiness) {
|
|
2038
|
+
if (readiness.matchingAssets.length === 0) {
|
|
2039
|
+
if (readiness.captureProven) {
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
pushOverridePreflightIssue(issues, {
|
|
2043
|
+
code: 'TARGET_ASSET_NOT_OBSERVED_FOR_RULE',
|
|
2044
|
+
severity: 'warning',
|
|
2045
|
+
source: 'observed-assets',
|
|
2046
|
+
message: `Rule ${readiness.ruleId} target asset was not observed for ${readiness.requestMethod} ${readiness.targetAssetUrl}.`,
|
|
2047
|
+
});
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
for (const asset of readiness.matchingAssets) {
|
|
2051
|
+
if (typeof asset.integrity === 'string' && asset.integrity.length > 0) {
|
|
2052
|
+
pushOverridePreflightIssue(issues, {
|
|
2053
|
+
code: 'TARGET_ASSET_SRI_PRESENT',
|
|
2054
|
+
severity: 'error',
|
|
2055
|
+
source: 'observed-assets',
|
|
2056
|
+
message: `Rule ${readiness.ruleId} target asset ${asset.url} includes integrity="${asset.integrity}" and cannot be overridden safely.`,
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
if (anyServiceWorkerControlled) {
|
|
2062
|
+
pushOverridePreflightIssue(issues, {
|
|
2063
|
+
code: 'SERVICE_WORKER_CONTROLLED',
|
|
2064
|
+
severity: 'warning',
|
|
2065
|
+
source: 'observed-assets',
|
|
2066
|
+
message: 'The observed page is service-worker controlled; verify the target requests still reach the network path that the debugger can fulfill.',
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
if (cspMetaTags.length > 0) {
|
|
2070
|
+
pushOverridePreflightIssue(issues, {
|
|
2071
|
+
code: 'CSP_META_PRESENT',
|
|
2072
|
+
severity: 'warning',
|
|
2073
|
+
source: 'observed-assets',
|
|
2074
|
+
message: `The observed page emitted ${cspMetaTags.length} CSP meta tag(s); document or bootstrap rewrites may still be constrained by page policy.`,
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
const ready = !issues.some((issue) => issue.severity === 'error');
|
|
2078
|
+
const nextActions = !ready
|
|
2079
|
+
? issues.some((issue) => issue.code === 'SESSION_NOT_FOUND' || issue.code === 'SESSION_PAUSED' || issue.code === 'SESSION_ENDED' || issue.code === LIVE_SESSION_DISCONNECTED_CODE)
|
|
2080
|
+
? [{ code: 'RECONNECT_SESSION', message: 'Reconnect or resume the target session before enabling overrides.' }]
|
|
2081
|
+
: issues.some((issue) => issue.code === 'SESSION_SCOPE_DRIFT')
|
|
2082
|
+
? [{ code: 'FOCUS_BOUND_TAB', message: 'Focus or reselect the bound top-level tab, then observe override assets again.' }]
|
|
2083
|
+
: issues.some((issue) => issue.code === 'SERVER_ACTION_UNSUPPORTED')
|
|
2084
|
+
? [{
|
|
2085
|
+
code: 'REPLAN_SERVER_ACTION_OVERRIDE',
|
|
2086
|
+
message: 'Server actions stay unsupported in production override mode; move the override to a GET document/data/API response.',
|
|
2087
|
+
}]
|
|
2088
|
+
: issues.some((issue) => issue.code === 'MUTATION_REPLAY_UNSUPPORTED')
|
|
2089
|
+
? [{
|
|
2090
|
+
code: 'REPLAN_MUTATION_OVERRIDE',
|
|
2091
|
+
message: 'Mutation responses are not replay-safe; use a GET document/data/API response path instead.',
|
|
2092
|
+
}]
|
|
2093
|
+
: issues.some((issue) => issue.code === 'UNSAFE_REQUEST_METHOD')
|
|
2094
|
+
? [{ code: 'REPLAN_GET_ONLY_OVERRIDE', message: 'Remove or regenerate non-GET rules before enabling overrides.' }]
|
|
2095
|
+
: issues.some((issue) => issue.code === 'TARGET_ASSET_SRI_PRESENT')
|
|
2096
|
+
? [{ code: 'CHOOSE_ANOTHER_OVERRIDE_PATH', message: 'Choose a document/data response path or remove SRI on the production asset before enabling overrides.' }]
|
|
2097
|
+
: issues.some((issue) => issue.code === 'NO_OBSERVED_ASSETS')
|
|
2098
|
+
? [{ code: 'OBSERVE_OVERRIDE_ASSETS', message: 'Observe the bound target route before enabling overrides.' }]
|
|
2099
|
+
: issues.some((issue) => issue.code === 'TARGET_ASSET_NOT_OBSERVED')
|
|
2100
|
+
? [{ code: 'OBSERVE_TARGET_ROUTE', message: 'Load the route that requests the configured target and observe assets again.' }]
|
|
2101
|
+
: buildOverrideProfileNextActions(profile, issues)
|
|
2102
|
+
: observedAssets.length === 0
|
|
2103
|
+
? [{ code: 'OBSERVE_OVERRIDE_ASSETS', message: 'Run observe_override_assets on the target route before enabling overrides in production workflows.' }]
|
|
2104
|
+
: [{ code: 'ENABLE_OVERRIDES', message: 'Preflight checks passed; the selected profile can be enabled on the live session.' }];
|
|
2105
|
+
return {
|
|
2106
|
+
ready,
|
|
2107
|
+
profileId: profile.profileId,
|
|
2108
|
+
profile,
|
|
2109
|
+
session: session
|
|
2110
|
+
? {
|
|
2111
|
+
sessionId: session.session_id,
|
|
2112
|
+
status: getSessionStatus(session),
|
|
2113
|
+
lastSeenAt: resolveSessionLastSeenAt(session, sessionState),
|
|
2114
|
+
connected: sessionState?.connected === true,
|
|
2115
|
+
disconnectedAt: sessionState?.disconnectedAt,
|
|
2116
|
+
disconnectReason: sessionState?.disconnectReason,
|
|
2117
|
+
urlLast: session.url_last ?? undefined,
|
|
2118
|
+
tabId: session.tab_id ?? undefined,
|
|
2119
|
+
}
|
|
2120
|
+
: null,
|
|
2121
|
+
issues,
|
|
2122
|
+
checks: {
|
|
2123
|
+
sessionFound: session !== undefined,
|
|
2124
|
+
connected: sessionState?.connected === true,
|
|
2125
|
+
captureReady: session !== undefined
|
|
2126
|
+
&& getSessionStatus(session) === 'active'
|
|
2127
|
+
&& (!hasLiveConnectionLookup || sessionState?.connected === true)
|
|
2128
|
+
&& observedAssets.length > 0
|
|
2129
|
+
&& topLevelScopeLikely
|
|
2130
|
+
&& !issues.some((issue) => issue.severity === 'error'),
|
|
2131
|
+
topLevelScopeLikely,
|
|
2132
|
+
observedAssetTabs,
|
|
2133
|
+
observedAssetPageUrls,
|
|
2134
|
+
observedAssetCount: observedAssets.length,
|
|
2135
|
+
targetAssetObserved,
|
|
2136
|
+
targetAssetReadinessSatisfied,
|
|
2137
|
+
matchedTargetAssetCount,
|
|
2138
|
+
capturedTargetAssetCount,
|
|
2139
|
+
unobservedTargetAssetCount,
|
|
2140
|
+
unsatisfiedTargetAssetCount,
|
|
2141
|
+
serviceWorkerControlled: anyServiceWorkerControlled,
|
|
2142
|
+
cspMetaTagCount: cspMetaTags.length,
|
|
2143
|
+
recentPlanCount: recentPlans.length,
|
|
2144
|
+
variantContextCount: variantContexts.length,
|
|
2145
|
+
},
|
|
2146
|
+
observedAssets: {
|
|
2147
|
+
count: observedAssets.length,
|
|
2148
|
+
tabIds: observedAssetTabs,
|
|
2149
|
+
pageUrls: observedAssetPageUrls,
|
|
2150
|
+
targetAssetObserved,
|
|
2151
|
+
targetAssetReadinessSatisfied,
|
|
2152
|
+
matchedTargetAssetCount,
|
|
2153
|
+
capturedTargetAssetCount,
|
|
2154
|
+
unobservedTargetAssetCount,
|
|
2155
|
+
unsatisfiedTargetAssetCount,
|
|
2156
|
+
serviceWorkerControlled: anyServiceWorkerControlled,
|
|
2157
|
+
cspMetaTags,
|
|
2158
|
+
},
|
|
2159
|
+
latestRun,
|
|
2160
|
+
recentPlans,
|
|
2161
|
+
variantContexts,
|
|
2162
|
+
diagnosis,
|
|
2163
|
+
nextActions,
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
function normalizeOptionalBooleanInput(value, fieldName) {
|
|
2167
|
+
if (value === undefined) {
|
|
2168
|
+
return undefined;
|
|
2169
|
+
}
|
|
2170
|
+
if (typeof value !== 'boolean') {
|
|
2171
|
+
throw new Error(`${fieldName} must be a boolean when provided`);
|
|
2172
|
+
}
|
|
2173
|
+
return value;
|
|
2174
|
+
}
|
|
2175
|
+
function normalizeOptionalNumberInput(value, fieldName) {
|
|
2176
|
+
if (value === undefined) {
|
|
2177
|
+
return undefined;
|
|
2178
|
+
}
|
|
2179
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2180
|
+
throw new Error(`${fieldName} must be a finite number when provided`);
|
|
2181
|
+
}
|
|
2182
|
+
return value;
|
|
2183
|
+
}
|
|
2184
|
+
function normalizeOptionalStringArrayInput(value, fieldName) {
|
|
2185
|
+
if (value === undefined) {
|
|
2186
|
+
return undefined;
|
|
2187
|
+
}
|
|
2188
|
+
if (!Array.isArray(value)) {
|
|
2189
|
+
throw new Error(`${fieldName} must be an array of strings when provided`);
|
|
2190
|
+
}
|
|
2191
|
+
return value.map((entry, index) => {
|
|
2192
|
+
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
2193
|
+
throw new Error(`${fieldName}[${index}] must be a non-empty string`);
|
|
2194
|
+
}
|
|
2195
|
+
return entry.trim();
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
function resolveSessionLastSeenAt(row, state) {
|
|
2199
|
+
return Math.max(row.created_at, row.last_seen_at ?? 0, row.paused_at ?? 0, row.ended_at ?? 0, state?.lastHeartbeatAt ?? 0);
|
|
2200
|
+
}
|
|
2201
|
+
function buildLiveConnectionRecord(row, scope, state) {
|
|
2202
|
+
const status = getSessionStatus(row);
|
|
2203
|
+
const lastSeenAt = resolveSessionLastSeenAt(row, state);
|
|
2204
|
+
const heartbeatAt = state?.lastHeartbeatAt;
|
|
2205
|
+
const heartbeatAgeMs = typeof heartbeatAt === 'number' ? Math.max(0, Date.now() - heartbeatAt) : undefined;
|
|
2206
|
+
const likelyStale = Boolean(!state?.connected
|
|
2207
|
+
&& status === 'active'
|
|
2208
|
+
&& scope.kind !== 'likely_iframe_noise'
|
|
2209
|
+
&& typeof heartbeatAt === 'number'
|
|
2210
|
+
&& Date.now() - heartbeatAt <= STALE_LIVE_CONNECTION_GRACE_WINDOW_MS);
|
|
2211
|
+
return {
|
|
2212
|
+
connected: state?.connected === true,
|
|
2213
|
+
connectedAt: state?.connectedAt,
|
|
2214
|
+
lastHeartbeatAt: heartbeatAt,
|
|
2215
|
+
heartbeatAgeMs,
|
|
2216
|
+
disconnectedAt: state?.disconnectedAt,
|
|
2217
|
+
disconnectReason: state?.disconnectReason ?? (status === 'ended' ? 'manual_stop' : undefined),
|
|
2218
|
+
status: status === 'ended'
|
|
2219
|
+
? 'ended'
|
|
2220
|
+
: status === 'paused'
|
|
2221
|
+
? 'paused'
|
|
2222
|
+
: state?.connected
|
|
2223
|
+
? 'connected'
|
|
2224
|
+
: likelyStale
|
|
2225
|
+
? 'likely_stale'
|
|
2226
|
+
: 'disconnected',
|
|
2227
|
+
captureReady: state?.connected === true && status === 'active',
|
|
2228
|
+
recommendedForLiveCapture: state?.connected === true && status === 'active' && scope.kind !== 'likely_iframe_noise',
|
|
2229
|
+
lastSeenAt,
|
|
2230
|
+
activityAgeMs: Math.max(0, Date.now() - lastSeenAt),
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
function buildLiveSessionNextAction(liveConnection, scope) {
|
|
2234
|
+
const liveStatus = typeof liveConnection.status === 'string' ? liveConnection.status : 'disconnected';
|
|
2235
|
+
if (liveStatus === 'connected' && scope.kind !== 'likely_iframe_noise') {
|
|
2236
|
+
return 'Use this session for live capture tools.';
|
|
2237
|
+
}
|
|
2238
|
+
if (liveStatus === 'connected' && scope.kind === 'likely_iframe_noise') {
|
|
2239
|
+
return 'Reconnect on a top-level app tab before relying on live navigation or performance captures.';
|
|
2240
|
+
}
|
|
2241
|
+
if (liveStatus === 'likely_stale') {
|
|
2242
|
+
return 'Retry list_sessions after a fresh app interaction or restart the session if live capture still fails.';
|
|
2243
|
+
}
|
|
2244
|
+
if (liveStatus === 'paused') {
|
|
2245
|
+
return 'Resume the session from the extension popup before using live capture tools.';
|
|
2246
|
+
}
|
|
2247
|
+
if (liveStatus === 'ended') {
|
|
2248
|
+
return 'Start a new extension session before using live capture tools.';
|
|
2249
|
+
}
|
|
2250
|
+
return 'Reconnect or restart the extension session before using live capture tools.';
|
|
2251
|
+
}
|
|
2252
|
+
function buildLiveSessionRecommendedAction(liveConnection, scope) {
|
|
2253
|
+
const liveStatus = typeof liveConnection.status === 'string' ? liveConnection.status : 'disconnected';
|
|
2254
|
+
if (liveStatus === 'connected' && scope.kind !== 'likely_iframe_noise') {
|
|
2255
|
+
return 'ready';
|
|
2256
|
+
}
|
|
2257
|
+
if (liveStatus === 'ended') {
|
|
2258
|
+
return 'start_new_session';
|
|
2259
|
+
}
|
|
2260
|
+
if (liveStatus === 'paused') {
|
|
2261
|
+
return 'resume_session';
|
|
2262
|
+
}
|
|
2263
|
+
return 'reconnect_extension';
|
|
2264
|
+
}
|
|
2265
|
+
function mapEventRecord(row, profile = 'legacy', options = {}) {
|
|
2266
|
+
const payload = readJsonPayload(row.payload_json);
|
|
2267
|
+
if (profile === 'compact') {
|
|
2268
|
+
const compact = {
|
|
2269
|
+
eventId: row.event_id,
|
|
2270
|
+
sessionId: row.session_id,
|
|
2271
|
+
timestamp: row.ts,
|
|
2272
|
+
type: row.type,
|
|
2273
|
+
summary: describeEvent(row.type, payload),
|
|
2274
|
+
};
|
|
2275
|
+
if (row.type === 'console') {
|
|
2276
|
+
compact.level = typeof payload.level === 'string' ? payload.level : undefined;
|
|
2277
|
+
compact.message = typeof payload.message === 'string' ? payload.message : undefined;
|
|
2278
|
+
}
|
|
2279
|
+
if (row.type === 'nav') {
|
|
2280
|
+
compact.url = resolveLastUrl(payload);
|
|
2281
|
+
}
|
|
2282
|
+
if (options.includePayload === true) {
|
|
2283
|
+
compact.payload = payload;
|
|
2284
|
+
}
|
|
2285
|
+
return compact;
|
|
2286
|
+
}
|
|
2287
|
+
return {
|
|
2288
|
+
eventId: row.event_id,
|
|
2289
|
+
sessionId: row.session_id,
|
|
2290
|
+
timestamp: row.ts,
|
|
2291
|
+
type: row.type,
|
|
2292
|
+
tabId: row.tab_id ?? (typeof payload.tabId === 'number' ? payload.tabId : undefined),
|
|
2293
|
+
origin: row.origin
|
|
2294
|
+
?? (typeof payload.origin === 'string' ? payload.origin : undefined)
|
|
2295
|
+
?? undefined,
|
|
2296
|
+
payload,
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
function classifyNetworkFailure(status, errorClass) {
|
|
2300
|
+
if (errorClass && errorClass.length > 0) {
|
|
2301
|
+
return errorClass;
|
|
2302
|
+
}
|
|
2303
|
+
if (typeof status === 'number' && status >= 400) {
|
|
2304
|
+
return 'http_error';
|
|
2305
|
+
}
|
|
2306
|
+
return 'unknown';
|
|
2307
|
+
}
|
|
2308
|
+
function buildNetworkFailureFilter(errorType) {
|
|
2309
|
+
if (typeof errorType !== 'string' || errorType.length === 0) {
|
|
2310
|
+
return '(error_class IS NOT NULL OR COALESCE(status, 0) >= 400)';
|
|
2311
|
+
}
|
|
2312
|
+
if (errorType === 'http_error') {
|
|
2313
|
+
return "(error_class = 'http_error' OR (error_class IS NULL AND COALESCE(status, 0) >= 400))";
|
|
2314
|
+
}
|
|
2315
|
+
return 'error_class = ?';
|
|
2316
|
+
}
|
|
2317
|
+
function resolveWindowSeconds(value, fallback, maxValue) {
|
|
2318
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2319
|
+
return fallback;
|
|
2320
|
+
}
|
|
2321
|
+
const floored = Math.floor(value);
|
|
2322
|
+
if (floored < 1) {
|
|
2323
|
+
return fallback;
|
|
2324
|
+
}
|
|
2325
|
+
return Math.min(floored, maxValue);
|
|
2326
|
+
}
|
|
2327
|
+
function resolveOptionalTimestamp(value) {
|
|
2328
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2329
|
+
return undefined;
|
|
2330
|
+
}
|
|
2331
|
+
const floored = Math.floor(value);
|
|
2332
|
+
return floored < 0 ? undefined : floored;
|
|
2333
|
+
}
|
|
2334
|
+
function resolveChunkBytes(value, fallback) {
|
|
2335
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2336
|
+
return fallback;
|
|
2337
|
+
}
|
|
2338
|
+
const floored = Math.floor(value);
|
|
2339
|
+
if (floored < 1) {
|
|
2340
|
+
return fallback;
|
|
2341
|
+
}
|
|
2342
|
+
return Math.min(floored, MAX_SNAPSHOT_ASSET_CHUNK_BYTES);
|
|
2343
|
+
}
|
|
2344
|
+
function resolveDurationMs(value, fallback, maxValue) {
|
|
2345
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2346
|
+
return fallback;
|
|
2347
|
+
}
|
|
2348
|
+
const floored = Math.floor(value);
|
|
2349
|
+
if (floored < 1) {
|
|
2350
|
+
return fallback;
|
|
2351
|
+
}
|
|
2352
|
+
return Math.min(floored, maxValue);
|
|
2353
|
+
}
|
|
2354
|
+
function resolveBodyChunkBytes(value) {
|
|
2355
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2356
|
+
return DEFAULT_BODY_CHUNK_BYTES;
|
|
2357
|
+
}
|
|
2358
|
+
const floored = Math.floor(value);
|
|
2359
|
+
if (floored < 1) {
|
|
2360
|
+
return DEFAULT_BODY_CHUNK_BYTES;
|
|
2361
|
+
}
|
|
2362
|
+
return Math.min(floored, MAX_BODY_CHUNK_BYTES);
|
|
2363
|
+
}
|
|
2364
|
+
function resolveTimeoutMs(value, fallback, maxValue) {
|
|
2365
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2366
|
+
return fallback;
|
|
2367
|
+
}
|
|
2368
|
+
const floored = Math.floor(value);
|
|
2369
|
+
if (floored < 100) {
|
|
2370
|
+
return fallback;
|
|
2371
|
+
}
|
|
2372
|
+
return Math.min(floored, maxValue);
|
|
2373
|
+
}
|
|
2374
|
+
function normalizeHttpMethod(value) {
|
|
2375
|
+
if (typeof value !== 'string') {
|
|
2376
|
+
return undefined;
|
|
2377
|
+
}
|
|
2378
|
+
const normalized = value.trim().toUpperCase();
|
|
2379
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
2380
|
+
}
|
|
2381
|
+
function normalizeOptionalString(value) {
|
|
2382
|
+
if (typeof value !== 'string') {
|
|
2383
|
+
return undefined;
|
|
2384
|
+
}
|
|
2385
|
+
const trimmed = value.trim();
|
|
2386
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
1201
2387
|
}
|
|
1202
2388
|
function normalizeStatusIn(value) {
|
|
1203
2389
|
if (!Array.isArray(value)) {
|
|
@@ -2138,6 +3324,66 @@ function normalizeCaptureError(sessionId, error) {
|
|
|
2138
3324
|
}
|
|
2139
3325
|
return fallback;
|
|
2140
3326
|
}
|
|
3327
|
+
function isCaptureTimeoutMessage(message) {
|
|
3328
|
+
const normalized = message.toLowerCase();
|
|
3329
|
+
return normalized.includes('timed out') || normalized.includes('timeout');
|
|
3330
|
+
}
|
|
3331
|
+
function isRecoverableOverrideLiveCommandError(error) {
|
|
3332
|
+
if (isLiveSessionDisconnectedError(error)) {
|
|
3333
|
+
return true;
|
|
3334
|
+
}
|
|
3335
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3336
|
+
return isCaptureTimeoutMessage(message);
|
|
3337
|
+
}
|
|
3338
|
+
function extractTimeoutMsFromMessage(message, fallback) {
|
|
3339
|
+
const match = message.match(/(?:after|waiting)\s+(\d+)ms/i);
|
|
3340
|
+
if (!match) {
|
|
3341
|
+
return fallback;
|
|
3342
|
+
}
|
|
3343
|
+
const parsed = Number.parseInt(match[1] ?? '', 10);
|
|
3344
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
3345
|
+
}
|
|
3346
|
+
function buildOverrideLiveCommandFailure(options) {
|
|
3347
|
+
const originalMessage = options.error instanceof Error ? options.error.message : String(options.error);
|
|
3348
|
+
const timeout = extractTimeoutMsFromMessage(originalMessage, options.timeoutMs);
|
|
3349
|
+
const timedOut = isCaptureTimeoutMessage(originalMessage);
|
|
3350
|
+
const disconnected = isLiveSessionDisconnectedError(options.error);
|
|
3351
|
+
const sessionState = options.getSessionConnectionState?.(options.sessionId);
|
|
3352
|
+
const code = disconnected
|
|
3353
|
+
? LIVE_SESSION_DISCONNECTED_CODE
|
|
3354
|
+
: timedOut
|
|
3355
|
+
? OVERRIDE_LIVE_COMMAND_TIMEOUT_CODE
|
|
3356
|
+
: OVERRIDE_LIVE_COMMAND_FAILED_CODE;
|
|
3357
|
+
const message = timedOut
|
|
3358
|
+
? `${options.command} for session ${options.sessionId} timed out after ${timeout}ms before the live extension returned an override command result.`
|
|
3359
|
+
: disconnected
|
|
3360
|
+
? `${options.command} for session ${options.sessionId} could not reach a connected live extension target.`
|
|
3361
|
+
: `${options.command} for session ${options.sessionId} failed before returning an override command result.`;
|
|
3362
|
+
return {
|
|
3363
|
+
ok: false,
|
|
3364
|
+
available: false,
|
|
3365
|
+
code,
|
|
3366
|
+
command: options.command,
|
|
3367
|
+
timeoutMs: timeout,
|
|
3368
|
+
timedOut,
|
|
3369
|
+
disconnected,
|
|
3370
|
+
message,
|
|
3371
|
+
originalMessage,
|
|
3372
|
+
sessionConnected: sessionState?.connected,
|
|
3373
|
+
disconnectedAt: sessionState?.disconnectedAt,
|
|
3374
|
+
disconnectReason: sessionState?.disconnectReason,
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
function createOverrideLiveCommandError(options) {
|
|
3378
|
+
const failure = buildOverrideLiveCommandFailure(options);
|
|
3379
|
+
const code = String(failure.code ?? OVERRIDE_LIVE_COMMAND_FAILED_CODE);
|
|
3380
|
+
const message = `${code}: ${options.command} for session ${options.sessionId} ${failure.timedOut === true
|
|
3381
|
+
? `timed out after ${String(failure.timeoutMs)}ms`
|
|
3382
|
+
: `failed`}. ${String(failure.message ?? '')} Original error: ${String(failure.originalMessage ?? 'unknown')}`;
|
|
3383
|
+
const error = new Error(message);
|
|
3384
|
+
Object.assign(error, { code, details: failure });
|
|
3385
|
+
return error;
|
|
3386
|
+
}
|
|
2141
3387
|
function isLiveSessionDisconnectedError(error) {
|
|
2142
3388
|
return error instanceof LiveSessionDisconnectedError;
|
|
2143
3389
|
}
|
|
@@ -2149,15 +3395,240 @@ async function executeLiveCapture(captureClient, sessionId, command, payload, ti
|
|
|
2149
3395
|
throw normalizeCaptureError(sessionId, error);
|
|
2150
3396
|
}
|
|
2151
3397
|
}
|
|
3398
|
+
async function executeOverrideLiveCaptureWithDiagnostics(options) {
|
|
3399
|
+
try {
|
|
3400
|
+
const capture = await executeLiveCapture(options.captureClient, options.sessionId, options.command, options.payload, options.timeoutMs);
|
|
3401
|
+
return { capture, payload: ensureCaptureSuccess(capture, options.sessionId) };
|
|
3402
|
+
}
|
|
3403
|
+
catch (error) {
|
|
3404
|
+
throw createOverrideLiveCommandError({
|
|
3405
|
+
sessionId: options.sessionId,
|
|
3406
|
+
command: options.command,
|
|
3407
|
+
timeoutMs: options.timeoutMs,
|
|
3408
|
+
error,
|
|
3409
|
+
getSessionConnectionState: options.getSessionConnectionState,
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
2152
3413
|
function ensureCaptureSuccess(result, sessionId) {
|
|
2153
3414
|
if (!result.ok) {
|
|
2154
3415
|
throw normalizeCaptureError(sessionId, new Error(result.error ?? 'Capture command failed'));
|
|
2155
3416
|
}
|
|
2156
3417
|
return result.payload ?? {};
|
|
2157
3418
|
}
|
|
2158
|
-
function
|
|
2159
|
-
const
|
|
2160
|
-
|
|
3419
|
+
async function refreshObservedAssetsForOverrideEnable(options) {
|
|
3420
|
+
const { payload } = await executeOverrideLiveCaptureWithDiagnostics({
|
|
3421
|
+
captureClient: options.captureClient,
|
|
3422
|
+
sessionId: options.sessionId,
|
|
3423
|
+
command: 'CAPTURE_OVERRIDE_OBSERVE_ASSETS',
|
|
3424
|
+
payload: { tabId: options.tabId, includePerformance: true },
|
|
3425
|
+
timeoutMs: 5_000,
|
|
3426
|
+
getSessionConnectionState: options.getSessionConnectionState,
|
|
3427
|
+
});
|
|
3428
|
+
persistObservedOverrideAssets(options.db, {
|
|
3429
|
+
...payload,
|
|
3430
|
+
sessionId: options.sessionId,
|
|
3431
|
+
tabId: payload.tabId ?? options.tabId,
|
|
3432
|
+
});
|
|
3433
|
+
return {
|
|
3434
|
+
tabId: typeof payload.tabId === 'number' ? payload.tabId : options.tabId,
|
|
3435
|
+
pageUrl: typeof payload.pageUrl === 'string' ? payload.pageUrl : undefined,
|
|
3436
|
+
assetCount: Array.isArray(payload.assets) ? payload.assets.length : 0,
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
function buildPersistedOverrideStatus(options) {
|
|
3440
|
+
let profile = null;
|
|
3441
|
+
let profileError;
|
|
3442
|
+
try {
|
|
3443
|
+
profile = resolveOverrideProfileRecord(options.profileId);
|
|
3444
|
+
}
|
|
3445
|
+
catch (error) {
|
|
3446
|
+
profileError = error instanceof Error ? error.message : String(error);
|
|
3447
|
+
}
|
|
3448
|
+
const latestRun = listOverridePocRuns(options.db, options.sessionId, 1, 0).runs[0] ?? null;
|
|
3449
|
+
const recentRequests = listOverridePocRequests(options.db, options.sessionId, 5, 0, latestRun?.runId).requests;
|
|
3450
|
+
const recentPlans = listOverridePlanAudits(options.db, { sessionId: options.sessionId, limit: 5, offset: 0 }).plans;
|
|
3451
|
+
let preflight;
|
|
3452
|
+
if (profileError) {
|
|
3453
|
+
preflight = {
|
|
3454
|
+
ready: false,
|
|
3455
|
+
profileId: null,
|
|
3456
|
+
profile: null,
|
|
3457
|
+
issues: [{
|
|
3458
|
+
code: 'OVERRIDE_CONFIG_UNAVAILABLE',
|
|
3459
|
+
severity: 'error',
|
|
3460
|
+
source: 'profile',
|
|
3461
|
+
message: profileError,
|
|
3462
|
+
}],
|
|
3463
|
+
checks: {
|
|
3464
|
+
sessionFound: auditSessionExists(options.db, options.sessionId),
|
|
3465
|
+
connected: options.getSessionConnectionState?.(options.sessionId)?.connected === true,
|
|
3466
|
+
},
|
|
3467
|
+
nextActions: [{
|
|
3468
|
+
code: 'FIX_OVERRIDE_CONFIG_PATH',
|
|
3469
|
+
message: 'Create a readable override-poc config or point OVERRIDE_POC_CONFIG_PATH at the intended config, then retry override status.',
|
|
3470
|
+
}],
|
|
3471
|
+
};
|
|
3472
|
+
}
|
|
3473
|
+
else {
|
|
3474
|
+
preflight = buildOverridePreflight({
|
|
3475
|
+
db: options.db,
|
|
3476
|
+
sessionId: options.sessionId,
|
|
3477
|
+
profileId: options.profileId,
|
|
3478
|
+
getSessionConnectionState: options.getSessionConnectionState,
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
return {
|
|
3482
|
+
profile,
|
|
3483
|
+
profileError,
|
|
3484
|
+
latestRun,
|
|
3485
|
+
recentRequests,
|
|
3486
|
+
recentPlans,
|
|
3487
|
+
preflight,
|
|
3488
|
+
diagnosis: diagnoseOverridePoc(options.db, options.sessionId, latestRun?.runId),
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
function auditSessionExists(db, sessionId) {
|
|
3492
|
+
const row = db.prepare('SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1').get(sessionId);
|
|
3493
|
+
return row !== undefined;
|
|
3494
|
+
}
|
|
3495
|
+
function hashLocalFileIfPresent(filePath) {
|
|
3496
|
+
if (!filePath || !existsSync(filePath)) {
|
|
3497
|
+
return { sha256: null, bytes: null };
|
|
3498
|
+
}
|
|
3499
|
+
const stat = statSync(filePath);
|
|
3500
|
+
if (!stat.isFile()) {
|
|
3501
|
+
return { sha256: null, bytes: null };
|
|
3502
|
+
}
|
|
3503
|
+
return {
|
|
3504
|
+
sha256: createHash('sha256').update(readFileSync(filePath)).digest('hex'),
|
|
3505
|
+
bytes: stat.size,
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
function resolveAuditProfileId(input) {
|
|
3509
|
+
return normalizeOptionalString(input.profileId) ?? null;
|
|
3510
|
+
}
|
|
3511
|
+
function buildOverrideRollbackMetadata(options) {
|
|
3512
|
+
return {
|
|
3513
|
+
disableTool: 'disable_overrides',
|
|
3514
|
+
validateTool: 'validate_override_profile',
|
|
3515
|
+
sessionId: options.sessionId,
|
|
3516
|
+
profileId: options.profileId,
|
|
3517
|
+
configPath: options.configPath ?? null,
|
|
3518
|
+
generatedFiles: Array.from(new Set(options.generatedFiles.filter((entry) => entry.trim().length > 0))),
|
|
3519
|
+
generatedDirectories: Array.from(new Set((options.generatedDirectories ?? []).filter((entry) => entry.trim().length > 0))),
|
|
3520
|
+
notes: [
|
|
3521
|
+
'Disable overrides for this session before deleting generated files or config entries.',
|
|
3522
|
+
'Re-run validate_override_profile after editing or removing generated config rules.',
|
|
3523
|
+
options.note,
|
|
3524
|
+
].filter((entry) => typeof entry === 'string' && entry.length > 0),
|
|
3525
|
+
};
|
|
3526
|
+
}
|
|
3527
|
+
function persistResponsePlanAudit(options) {
|
|
3528
|
+
if (!options.sessionId || !options.plan.rule || !auditSessionExists(options.db, options.sessionId)) {
|
|
3529
|
+
return undefined;
|
|
3530
|
+
}
|
|
3531
|
+
const profileId = resolveAuditProfileId(options.input);
|
|
3532
|
+
const record = {
|
|
3533
|
+
planId: randomUUID(),
|
|
3534
|
+
sessionId: options.sessionId,
|
|
3535
|
+
createdAt: Date.now(),
|
|
3536
|
+
plannerKind: 'response-patch',
|
|
3537
|
+
toolName: 'plan_override_response_patch',
|
|
3538
|
+
profileId,
|
|
3539
|
+
ruleId: options.plan.rule.ruleId,
|
|
3540
|
+
ruleType: options.plan.rule.ruleType,
|
|
3541
|
+
requestMethod: options.plan.requestMethod,
|
|
3542
|
+
matchMode: options.plan.matchMode,
|
|
3543
|
+
targetAssetUrl: options.plan.targetUrl,
|
|
3544
|
+
localFilePath: options.plan.localFilePath ?? options.plan.rule.localFilePath,
|
|
3545
|
+
configPath: options.plan.configPath ?? null,
|
|
3546
|
+
contentType: options.plan.contentType,
|
|
3547
|
+
originalSha256: options.plan.originalSha256,
|
|
3548
|
+
patchedSha256: options.plan.patchedSha256,
|
|
3549
|
+
originalBytes: options.plan.originalBytes,
|
|
3550
|
+
patchedBytes: options.plan.patchedBytes,
|
|
3551
|
+
patchSummary: {
|
|
3552
|
+
textPatches: options.plan.patches,
|
|
3553
|
+
jsonPatches: options.plan.jsonPatches,
|
|
3554
|
+
documentPatches: options.plan.documentPatches,
|
|
3555
|
+
ruleType: options.plan.ruleType,
|
|
3556
|
+
configWritten: options.plan.configWritten,
|
|
3557
|
+
rscFlight: options.plan.rule.rscFlight ?? null,
|
|
3558
|
+
variantContext: options.variantContext ?? null,
|
|
3559
|
+
},
|
|
3560
|
+
preview: options.plan.preview ?? null,
|
|
3561
|
+
warnings: options.plan.warnings,
|
|
3562
|
+
blockers: options.plan.blockers,
|
|
3563
|
+
capturedFromLiveSession: options.capturedFromLiveSession ?? null,
|
|
3564
|
+
rollback: buildOverrideRollbackMetadata({
|
|
3565
|
+
sessionId: options.sessionId,
|
|
3566
|
+
profileId,
|
|
3567
|
+
configPath: options.plan.configPath ?? null,
|
|
3568
|
+
generatedFiles: options.plan.localFilePath ? [options.plan.localFilePath] : [],
|
|
3569
|
+
note: 'Generated response override bodies are disposable once the override has been disabled.',
|
|
3570
|
+
}),
|
|
3571
|
+
};
|
|
3572
|
+
return insertOverridePlanAudit(options.db, record);
|
|
3573
|
+
}
|
|
3574
|
+
function persistNextSourcePlanAudits(options) {
|
|
3575
|
+
if (!options.sessionId || !auditSessionExists(options.db, options.sessionId)) {
|
|
3576
|
+
return [];
|
|
3577
|
+
}
|
|
3578
|
+
const sessionId = options.sessionId;
|
|
3579
|
+
const profileId = resolveAuditProfileId(options.input);
|
|
3580
|
+
const generatedFiles = options.plan.rules.map((rule) => rule.localFilePath);
|
|
3581
|
+
return options.plan.rules.map((rule) => {
|
|
3582
|
+
const localFile = hashLocalFileIfPresent(rule.localFilePath);
|
|
3583
|
+
const record = {
|
|
3584
|
+
planId: randomUUID(),
|
|
3585
|
+
sessionId: options.sessionId,
|
|
3586
|
+
createdAt: Date.now(),
|
|
3587
|
+
plannerKind: 'next-source-overlay',
|
|
3588
|
+
toolName: 'plan_next_source_override',
|
|
3589
|
+
profileId,
|
|
3590
|
+
ruleId: rule.ruleId,
|
|
3591
|
+
ruleType: rule.ruleType,
|
|
3592
|
+
requestMethod: rule.requestMethod,
|
|
3593
|
+
matchMode: rule.matchMode,
|
|
3594
|
+
targetAssetUrl: rule.targetAssetUrl,
|
|
3595
|
+
localFilePath: rule.localFilePath,
|
|
3596
|
+
configPath: options.plan.configPath ?? null,
|
|
3597
|
+
contentType: rule.contentType,
|
|
3598
|
+
originalSha256: null,
|
|
3599
|
+
patchedSha256: localFile.sha256,
|
|
3600
|
+
originalBytes: null,
|
|
3601
|
+
patchedBytes: localFile.bytes,
|
|
3602
|
+
patchSummary: {
|
|
3603
|
+
sourcePaths: options.plan.sourcePaths,
|
|
3604
|
+
editsApplied: options.plan.editsApplied,
|
|
3605
|
+
ruleReason: rule.reason,
|
|
3606
|
+
confidence: rule.confidence,
|
|
3607
|
+
score: rule.score,
|
|
3608
|
+
matchedSourcePaths: rule.matchedSourcePaths,
|
|
3609
|
+
originalAssetPath: rule.originalAssetPath ?? null,
|
|
3610
|
+
build: options.plan.build,
|
|
3611
|
+
configWritten: options.plan.configWritten,
|
|
3612
|
+
},
|
|
3613
|
+
preview: null,
|
|
3614
|
+
warnings: [...options.plan.warnings, ...rule.blockers.map((blocker) => `rule ${rule.ruleId}: ${blocker}`)],
|
|
3615
|
+
blockers: options.plan.blockers,
|
|
3616
|
+
capturedFromLiveSession: null,
|
|
3617
|
+
rollback: buildOverrideRollbackMetadata({
|
|
3618
|
+
sessionId,
|
|
3619
|
+
profileId,
|
|
3620
|
+
configPath: options.plan.configPath ?? null,
|
|
3621
|
+
generatedFiles,
|
|
3622
|
+
generatedDirectories: [options.plan.overlayRoot],
|
|
3623
|
+
note: 'Generated Next.js overlay folders are disposable once the override has been disabled.',
|
|
3624
|
+
}),
|
|
3625
|
+
};
|
|
3626
|
+
return insertOverridePlanAudit(options.db, record);
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3629
|
+
function normalizeSnapshotResponsePayload(payload, options) {
|
|
3630
|
+
const snapshotRecord = structuredClone(payload);
|
|
3631
|
+
const snapshotRoot = snapshotRecord.snapshot;
|
|
2161
3632
|
if (typeof snapshotRoot === 'object' && snapshotRoot !== null) {
|
|
2162
3633
|
const snapshotObject = snapshotRoot;
|
|
2163
3634
|
if (!options.includeDom) {
|
|
@@ -2265,7 +3736,12 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
2265
3736
|
const where = [];
|
|
2266
3737
|
const params = [];
|
|
2267
3738
|
if (sinceMinutes !== undefined && Number.isFinite(sinceMinutes) && sinceMinutes > 0) {
|
|
2268
|
-
where.push(
|
|
3739
|
+
where.push(`
|
|
3740
|
+
CASE
|
|
3741
|
+
WHEN COALESCE(last_seen_at, 0) > created_at THEN COALESCE(last_seen_at, 0)
|
|
3742
|
+
ELSE created_at
|
|
3743
|
+
END >= ?
|
|
3744
|
+
`);
|
|
2269
3745
|
params.push(Date.now() - Math.floor(sinceMinutes * 60_000));
|
|
2270
3746
|
}
|
|
2271
3747
|
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
@@ -2273,6 +3749,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
2273
3749
|
SELECT
|
|
2274
3750
|
session_id,
|
|
2275
3751
|
created_at,
|
|
3752
|
+
last_seen_at,
|
|
2276
3753
|
paused_at,
|
|
2277
3754
|
ended_at,
|
|
2278
3755
|
tab_id,
|
|
@@ -2287,49 +3764,48 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
2287
3764
|
pinned
|
|
2288
3765
|
FROM sessions
|
|
2289
3766
|
${whereClause}
|
|
2290
|
-
ORDER BY
|
|
3767
|
+
ORDER BY
|
|
3768
|
+
CASE
|
|
3769
|
+
WHEN COALESCE(last_seen_at, 0) > created_at THEN COALESCE(last_seen_at, 0)
|
|
3770
|
+
ELSE created_at
|
|
3771
|
+
END DESC,
|
|
3772
|
+
created_at DESC
|
|
2291
3773
|
LIMIT ? OFFSET ?
|
|
2292
3774
|
`;
|
|
2293
3775
|
const rows = db.prepare(sql).all(...params, limit + 1, offset);
|
|
2294
3776
|
const truncatedByLimit = rows.length > limit;
|
|
2295
|
-
const sessions = rows.slice(0, limit).map((row) =>
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
:
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
lastHeartbeatAt: state.lastHeartbeatAt,
|
|
2328
|
-
disconnectedAt: state.disconnectedAt,
|
|
2329
|
-
disconnectReason: state.disconnectReason,
|
|
2330
|
-
};
|
|
2331
|
-
})(),
|
|
2332
|
-
}));
|
|
3777
|
+
const sessions = rows.slice(0, limit).map((row) => {
|
|
3778
|
+
const status = getSessionStatus(row);
|
|
3779
|
+
const state = getSessionConnectionState?.(row.session_id);
|
|
3780
|
+
const lastUrl = row.url_last ?? undefined;
|
|
3781
|
+
const scope = classifySessionUrl(lastUrl);
|
|
3782
|
+
const liveConnection = buildLiveConnectionRecord(row, scope, state);
|
|
3783
|
+
return {
|
|
3784
|
+
sessionId: row.session_id,
|
|
3785
|
+
createdAt: row.created_at,
|
|
3786
|
+
lastSeenAt: resolveSessionLastSeenAt(row, state),
|
|
3787
|
+
pausedAt: row.paused_at ?? undefined,
|
|
3788
|
+
endedAt: row.ended_at ?? undefined,
|
|
3789
|
+
status,
|
|
3790
|
+
tabId: row.tab_id ?? undefined,
|
|
3791
|
+
windowId: row.window_id ?? undefined,
|
|
3792
|
+
urlStart: row.url_start ?? undefined,
|
|
3793
|
+
urlLast: lastUrl,
|
|
3794
|
+
lastUrl,
|
|
3795
|
+
userAgent: row.user_agent ?? undefined,
|
|
3796
|
+
viewport: row.viewport_w !== null && row.viewport_h !== null
|
|
3797
|
+
? {
|
|
3798
|
+
width: row.viewport_w,
|
|
3799
|
+
height: row.viewport_h,
|
|
3800
|
+
}
|
|
3801
|
+
: undefined,
|
|
3802
|
+
dpr: row.dpr ?? undefined,
|
|
3803
|
+
safeMode: row.safe_mode === 1,
|
|
3804
|
+
pinned: row.pinned === 1,
|
|
3805
|
+
scope,
|
|
3806
|
+
liveConnection,
|
|
3807
|
+
};
|
|
3808
|
+
});
|
|
2333
3809
|
const bytePage = applyByteBudget(sessions, maxResponseBytes);
|
|
2334
3810
|
const truncated = truncatedByLimit || bytePage.truncatedByBytes;
|
|
2335
3811
|
return {
|
|
@@ -2343,6 +3819,68 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
2343
3819
|
sessions: bytePage.items,
|
|
2344
3820
|
};
|
|
2345
3821
|
},
|
|
3822
|
+
get_session_summary: async (input) => {
|
|
3823
|
+
const db = getDb();
|
|
3824
|
+
const sessionId = getSessionId(input);
|
|
3825
|
+
if (!sessionId) {
|
|
3826
|
+
throw new Error('sessionId is required');
|
|
3827
|
+
}
|
|
3828
|
+
const session = db
|
|
3829
|
+
.prepare('SELECT session_id, created_at, ended_at, url_last, pinned FROM sessions WHERE session_id = ?')
|
|
3830
|
+
.get(sessionId);
|
|
3831
|
+
if (!session) {
|
|
3832
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
3833
|
+
}
|
|
3834
|
+
const counters = db
|
|
3835
|
+
.prepare(`
|
|
3836
|
+
SELECT
|
|
3837
|
+
SUM(CASE WHEN type = 'error' THEN 1 ELSE 0 END) AS errors,
|
|
3838
|
+
SUM(CASE WHEN type = 'console' AND json_extract(payload_json, '$.level') = 'warn' THEN 1 ELSE 0 END) AS warnings
|
|
3839
|
+
FROM events
|
|
3840
|
+
WHERE session_id = ?
|
|
3841
|
+
`)
|
|
3842
|
+
.get(sessionId);
|
|
3843
|
+
const networkFails = db
|
|
3844
|
+
.prepare(`
|
|
3845
|
+
SELECT COUNT(*) AS count
|
|
3846
|
+
FROM network
|
|
3847
|
+
WHERE session_id = ?
|
|
3848
|
+
AND (error_class IS NOT NULL OR COALESCE(status, 0) >= 400)
|
|
3849
|
+
`)
|
|
3850
|
+
.get(sessionId);
|
|
3851
|
+
const latestNav = db
|
|
3852
|
+
.prepare(`
|
|
3853
|
+
SELECT payload_json
|
|
3854
|
+
FROM events
|
|
3855
|
+
WHERE session_id = ? AND type = 'nav'
|
|
3856
|
+
ORDER BY ts DESC
|
|
3857
|
+
LIMIT 1
|
|
3858
|
+
`)
|
|
3859
|
+
.get(sessionId);
|
|
3860
|
+
const eventRange = db
|
|
3861
|
+
.prepare(`
|
|
3862
|
+
SELECT MIN(ts) AS start_ts, MAX(ts) AS end_ts
|
|
3863
|
+
FROM events
|
|
3864
|
+
WHERE session_id = ?
|
|
3865
|
+
`)
|
|
3866
|
+
.get(sessionId);
|
|
3867
|
+
const navPayload = latestNav ? readJsonPayload(latestNav.payload_json) : {};
|
|
3868
|
+
const lastUrl = resolveLastUrl(navPayload) ?? session.url_last ?? undefined;
|
|
3869
|
+
return {
|
|
3870
|
+
...createBaseResponse(sessionId),
|
|
3871
|
+
counts: {
|
|
3872
|
+
errors: counters.errors ?? 0,
|
|
3873
|
+
warnings: counters.warnings ?? 0,
|
|
3874
|
+
networkFails: networkFails.count,
|
|
3875
|
+
},
|
|
3876
|
+
lastUrl,
|
|
3877
|
+
timeRange: {
|
|
3878
|
+
start: eventRange.start_ts ?? session.created_at,
|
|
3879
|
+
end: eventRange.end_ts ?? session.ended_at ?? session.created_at,
|
|
3880
|
+
},
|
|
3881
|
+
pinned: session.pinned === 1,
|
|
3882
|
+
};
|
|
3883
|
+
},
|
|
2346
3884
|
get_live_session_health: async (input) => {
|
|
2347
3885
|
const db = getDb();
|
|
2348
3886
|
const sessionId = getSessionId(input);
|
|
@@ -2354,6 +3892,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
2354
3892
|
SELECT
|
|
2355
3893
|
session_id,
|
|
2356
3894
|
created_at,
|
|
3895
|
+
last_seen_at,
|
|
2357
3896
|
paused_at,
|
|
2358
3897
|
ended_at,
|
|
2359
3898
|
tab_id,
|
|
@@ -2367,124 +3906,454 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
2367
3906
|
pinned
|
|
2368
3907
|
FROM sessions
|
|
2369
3908
|
WHERE session_id = ?
|
|
2370
|
-
LIMIT 1
|
|
2371
3909
|
`)
|
|
2372
3910
|
.get(sessionId);
|
|
2373
3911
|
if (!session) {
|
|
2374
3912
|
throw new Error(`Session not found: ${sessionId}`);
|
|
2375
3913
|
}
|
|
2376
|
-
const
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
3914
|
+
const latestNav = db
|
|
3915
|
+
.prepare(`
|
|
3916
|
+
SELECT payload_json
|
|
3917
|
+
FROM events
|
|
3918
|
+
WHERE session_id = ? AND type = 'nav'
|
|
3919
|
+
ORDER BY ts DESC
|
|
3920
|
+
LIMIT 1
|
|
3921
|
+
`)
|
|
3922
|
+
.get(sessionId);
|
|
3923
|
+
const navPayload = latestNav ? readJsonPayload(latestNav.payload_json) : {};
|
|
3924
|
+
const lastUrl = resolveLastUrl(navPayload) ?? session.url_last ?? undefined;
|
|
3925
|
+
const scope = classifySessionUrl(lastUrl);
|
|
3926
|
+
const connectionState = getSessionConnectionState?.(sessionId);
|
|
3927
|
+
const liveConnection = buildLiveConnectionRecord(session, scope, connectionState);
|
|
3928
|
+
const status = getSessionStatus(session);
|
|
3929
|
+
const lastSeenAt = resolveSessionLastSeenAt(session, connectionState);
|
|
3930
|
+
const nextAction = buildLiveSessionNextAction(liveConnection, scope);
|
|
3931
|
+
const recommendedAction = buildLiveSessionRecommendedAction(liveConnection, scope);
|
|
3932
|
+
const sessionRecord = {
|
|
3933
|
+
sessionId: session.session_id,
|
|
3934
|
+
createdAt: session.created_at,
|
|
3935
|
+
lastSeenAt,
|
|
3936
|
+
pausedAt: session.paused_at ?? undefined,
|
|
3937
|
+
endedAt: session.ended_at ?? undefined,
|
|
3938
|
+
status,
|
|
3939
|
+
tabId: session.tab_id ?? undefined,
|
|
3940
|
+
windowId: session.window_id ?? undefined,
|
|
3941
|
+
urlStart: session.url_start ?? undefined,
|
|
3942
|
+
urlLast: session.url_last ?? undefined,
|
|
3943
|
+
lastUrl,
|
|
3944
|
+
viewport: session.viewport_w !== null && session.viewport_h !== null
|
|
3945
|
+
? {
|
|
3946
|
+
width: session.viewport_w,
|
|
3947
|
+
height: session.viewport_h,
|
|
3948
|
+
}
|
|
3949
|
+
: undefined,
|
|
3950
|
+
dpr: session.dpr ?? undefined,
|
|
3951
|
+
safeMode: session.safe_mode === 1,
|
|
3952
|
+
pinned: session.pinned === 1,
|
|
3953
|
+
};
|
|
3954
|
+
return {
|
|
3955
|
+
...createBaseResponse(sessionId),
|
|
3956
|
+
status,
|
|
3957
|
+
createdAt: session.created_at,
|
|
3958
|
+
lastSeenAt,
|
|
3959
|
+
pausedAt: session.paused_at ?? undefined,
|
|
3960
|
+
endedAt: session.ended_at ?? undefined,
|
|
3961
|
+
tabId: session.tab_id ?? undefined,
|
|
3962
|
+
windowId: session.window_id ?? undefined,
|
|
3963
|
+
lastUrl,
|
|
3964
|
+
safeMode: session.safe_mode === 1,
|
|
3965
|
+
pinned: session.pinned === 1,
|
|
3966
|
+
session: sessionRecord,
|
|
3967
|
+
scope,
|
|
3968
|
+
liveConnection,
|
|
3969
|
+
nextAction,
|
|
3970
|
+
recommendedAction,
|
|
3971
|
+
};
|
|
3972
|
+
},
|
|
3973
|
+
list_override_profiles: async () => {
|
|
3974
|
+
const profiles = buildOverrideProfileRecords();
|
|
3975
|
+
return {
|
|
3976
|
+
...createBaseResponse(),
|
|
3977
|
+
limitsApplied: {
|
|
3978
|
+
maxResults: profiles.length,
|
|
3979
|
+
truncated: false,
|
|
3980
|
+
},
|
|
3981
|
+
profiles,
|
|
3982
|
+
nextActions: profiles.length > 0
|
|
3983
|
+
? [{ code: 'VALIDATE_PROFILE', message: 'Run validate_override_profile before enabling overrides.' }]
|
|
3984
|
+
: [{ code: 'CREATE_PROFILE', message: 'Run create_override_profile to generate a candidate profile.' }],
|
|
3985
|
+
};
|
|
3986
|
+
},
|
|
3987
|
+
create_override_profile: async (input) => {
|
|
3988
|
+
const adapterInput = normalizeOptionalString(input.adapter) ?? normalizeOptionalString(input.mode);
|
|
3989
|
+
let adapter;
|
|
3990
|
+
if (adapterInput !== undefined) {
|
|
3991
|
+
if (!OVERRIDE_PROFILE_ADAPTERS.includes(adapterInput)) {
|
|
3992
|
+
throw new Error(`adapter must be one of: ${OVERRIDE_PROFILE_ADAPTERS.join(', ')}`);
|
|
3993
|
+
}
|
|
3994
|
+
adapter = adapterInput;
|
|
3995
|
+
}
|
|
3996
|
+
const targetBaseUrl = normalizeOptionalString(input.targetBaseUrl);
|
|
3997
|
+
if (!targetBaseUrl) {
|
|
3998
|
+
throw new Error('targetBaseUrl is required, for example https://example.com/_next/ or https://example.com/assets/');
|
|
3999
|
+
}
|
|
4000
|
+
const generated = createOverrideProfileConfig({
|
|
4001
|
+
adapter,
|
|
4002
|
+
targetBaseUrl,
|
|
4003
|
+
projectRoot: normalizeOptionalString(input.projectRoot),
|
|
4004
|
+
assetRoot: normalizeOptionalString(input.assetRoot),
|
|
4005
|
+
nextDir: normalizeOptionalString(input.nextDir),
|
|
4006
|
+
configPath: normalizeOptionalString(input.configPath),
|
|
4007
|
+
profileId: normalizeOptionalString(input.profileId),
|
|
4008
|
+
profileName: normalizeOptionalString(input.profileName),
|
|
4009
|
+
enabled: normalizeOptionalBooleanInput(input.enabled, 'enabled'),
|
|
4010
|
+
profileEnabled: normalizeOptionalBooleanInput(input.profileEnabled, 'profileEnabled'),
|
|
4011
|
+
autoReload: normalizeOptionalBooleanInput(input.autoReload, 'autoReload'),
|
|
4012
|
+
includeManifestFiles: normalizeOptionalBooleanInput(input.includeManifestFiles, 'includeManifestFiles'),
|
|
4013
|
+
includeStaticFiles: normalizeOptionalBooleanInput(input.includeStaticFiles, 'includeStaticFiles'),
|
|
4014
|
+
extensions: normalizeOptionalStringArrayInput(input.extensions, 'extensions'),
|
|
4015
|
+
maxRules: normalizeOptionalNumberInput(input.maxRules, 'maxRules'),
|
|
4016
|
+
});
|
|
4017
|
+
const writeConfig = normalizeOptionalBooleanInput(input.writeConfig, 'writeConfig') ?? false;
|
|
4018
|
+
const overwrite = normalizeOptionalBooleanInput(input.overwrite, 'overwrite') ?? false;
|
|
4019
|
+
const write = {
|
|
4020
|
+
written: false,
|
|
4021
|
+
path: generated.suggestedConfigPath,
|
|
4022
|
+
};
|
|
4023
|
+
let nextActions = generated.nextActions;
|
|
4024
|
+
if (writeConfig && generated.ruleCount === 0) {
|
|
4025
|
+
write.failureCode = 'NO_RULES';
|
|
4026
|
+
write.message = 'Generated profile has no rules; config was not written.';
|
|
4027
|
+
nextActions = [{
|
|
4028
|
+
code: 'BUILD_APP',
|
|
4029
|
+
message: 'Build the app so local assets exist, then generate the profile again.',
|
|
4030
|
+
}];
|
|
4031
|
+
}
|
|
4032
|
+
else if (writeConfig && existsSync(generated.suggestedConfigPath) && !overwrite) {
|
|
4033
|
+
write.failureCode = 'CONFIG_EXISTS';
|
|
4034
|
+
write.message = 'Config file already exists; pass overwrite=true or choose another configPath.';
|
|
4035
|
+
nextActions = [{
|
|
4036
|
+
code: 'OVERWRITE_OR_CHOOSE_CONFIG_PATH',
|
|
4037
|
+
message: 'Pass overwrite=true to replace the config file, or choose a different configPath.',
|
|
4038
|
+
}, ...generated.nextActions];
|
|
4039
|
+
}
|
|
4040
|
+
else if (writeConfig) {
|
|
4041
|
+
mkdirSync(dirname(generated.suggestedConfigPath), { recursive: true });
|
|
4042
|
+
writeFileSync(generated.suggestedConfigPath, generated.configJson, 'utf8');
|
|
4043
|
+
write.written = true;
|
|
4044
|
+
write.bytes = Buffer.byteLength(generated.configJson, 'utf8');
|
|
4045
|
+
nextActions = generated.nextActions.filter((action) => action.code !== 'SAVE_LOCAL_CONFIG');
|
|
4046
|
+
}
|
|
4047
|
+
return {
|
|
4048
|
+
...createBaseResponse(),
|
|
4049
|
+
limitsApplied: {
|
|
4050
|
+
maxResults: generated.ruleCount,
|
|
4051
|
+
truncated: generated.warnings.some((warning) => warning.startsWith('Rule generation was limited')),
|
|
4052
|
+
},
|
|
4053
|
+
adapter: generated.adapter,
|
|
4054
|
+
mode: generated.mode,
|
|
4055
|
+
projectRoot: generated.projectRoot,
|
|
4056
|
+
assetRoot: generated.assetRoot,
|
|
4057
|
+
nextDir: generated.nextDir,
|
|
4058
|
+
targetBaseUrl: generated.targetBaseUrl,
|
|
4059
|
+
suggestedConfigPath: generated.suggestedConfigPath,
|
|
4060
|
+
ruleCount: generated.ruleCount,
|
|
4061
|
+
manifestFiles: generated.manifestFiles,
|
|
4062
|
+
staticFileCount: generated.staticFileCount,
|
|
4063
|
+
missingManifestAssetCount: generated.missingManifestAssetCount,
|
|
4064
|
+
warnings: generated.warnings,
|
|
4065
|
+
nextActions,
|
|
4066
|
+
write,
|
|
4067
|
+
profile: generated.profile,
|
|
4068
|
+
config: generated.config,
|
|
4069
|
+
configJson: generated.configJson,
|
|
4070
|
+
};
|
|
4071
|
+
},
|
|
4072
|
+
validate_override_profile: async (input) => {
|
|
4073
|
+
const profile = resolveOverrideProfileRecord(input.profileId);
|
|
4074
|
+
const issues = buildOverrideProfileIssues(profile);
|
|
4075
|
+
return {
|
|
4076
|
+
...createBaseResponse(),
|
|
4077
|
+
profileId: profile.profileId,
|
|
4078
|
+
valid: !issues.some((issue) => issue.severity === 'error'),
|
|
4079
|
+
issues,
|
|
4080
|
+
nextActions: buildOverrideProfileNextActions(profile, issues),
|
|
4081
|
+
profile,
|
|
4082
|
+
};
|
|
4083
|
+
},
|
|
4084
|
+
preflight_overrides: async (input) => {
|
|
4085
|
+
const db = getDb();
|
|
4086
|
+
const sessionId = getSessionId(input);
|
|
4087
|
+
if (!sessionId) {
|
|
4088
|
+
throw new Error('sessionId is required');
|
|
4089
|
+
}
|
|
4090
|
+
const preflight = buildOverridePreflight({
|
|
4091
|
+
db,
|
|
4092
|
+
sessionId,
|
|
4093
|
+
profileId: input.profileId,
|
|
4094
|
+
getSessionConnectionState,
|
|
4095
|
+
});
|
|
4096
|
+
return {
|
|
4097
|
+
...createBaseResponse(sessionId),
|
|
4098
|
+
...preflight,
|
|
4099
|
+
};
|
|
4100
|
+
},
|
|
4101
|
+
list_observed_override_assets: async (input) => {
|
|
4102
|
+
const sessionId = getSessionId(input);
|
|
4103
|
+
if (!sessionId) {
|
|
4104
|
+
throw new Error('sessionId is required');
|
|
4105
|
+
}
|
|
4106
|
+
const assets = listObservedOverrideAssets(getDb(), {
|
|
4107
|
+
sessionId,
|
|
4108
|
+
limit: typeof input.limit === 'number' ? input.limit : undefined,
|
|
4109
|
+
sinceTimestamp: typeof input.sinceTimestamp === 'number' ? input.sinceTimestamp : undefined,
|
|
4110
|
+
});
|
|
2382
4111
|
return {
|
|
2383
4112
|
...createBaseResponse(sessionId),
|
|
2384
4113
|
limitsApplied: {
|
|
2385
|
-
maxResults:
|
|
4114
|
+
maxResults: assets.length,
|
|
2386
4115
|
truncated: false,
|
|
2387
4116
|
},
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
4117
|
+
assets,
|
|
4118
|
+
};
|
|
4119
|
+
},
|
|
4120
|
+
plan_override_response_patch: async (input) => {
|
|
4121
|
+
const sessionId = getSessionId(input);
|
|
4122
|
+
const plan = planOverrideResponsePatch(input);
|
|
4123
|
+
const variantContext = buildOverrideVariantContext({
|
|
4124
|
+
targetUrl: plan.targetUrl,
|
|
4125
|
+
requestMethod: plan.requestMethod,
|
|
4126
|
+
matchMode: plan.matchMode,
|
|
4127
|
+
ruleType: plan.ruleType,
|
|
4128
|
+
captureMode: input.captureMode,
|
|
4129
|
+
source: input.source,
|
|
4130
|
+
triggerReload: input.triggerReload,
|
|
4131
|
+
requestHeaders: input.requestHeaders,
|
|
4132
|
+
});
|
|
4133
|
+
const auditPlan = persistResponsePlanAudit({
|
|
4134
|
+
db: getDb(),
|
|
4135
|
+
sessionId,
|
|
4136
|
+
input,
|
|
4137
|
+
plan,
|
|
4138
|
+
variantContext,
|
|
4139
|
+
});
|
|
4140
|
+
return {
|
|
4141
|
+
...createBaseResponse(sessionId),
|
|
4142
|
+
limitsApplied: {
|
|
4143
|
+
maxResults: plan.rule ? 1 : 0,
|
|
4144
|
+
truncated: false,
|
|
2407
4145
|
},
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
disconnectReason: connection.disconnectReason,
|
|
2415
|
-
staleForMs,
|
|
2416
|
-
}
|
|
2417
|
-
: {
|
|
2418
|
-
connected: false,
|
|
2419
|
-
staleForMs,
|
|
2420
|
-
},
|
|
2421
|
-
recommendedAction: connection?.connected
|
|
2422
|
-
? 'ready'
|
|
2423
|
-
: session.ended_at
|
|
2424
|
-
? 'start_new_session'
|
|
2425
|
-
: 'reconnect_extension',
|
|
4146
|
+
variantContext,
|
|
4147
|
+
audit: {
|
|
4148
|
+
persisted: auditPlan !== undefined,
|
|
4149
|
+
plans: auditPlan ? [auditPlan] : [],
|
|
4150
|
+
},
|
|
4151
|
+
...plan,
|
|
2426
4152
|
};
|
|
2427
4153
|
},
|
|
2428
|
-
|
|
4154
|
+
map_next_override_assets: async (input) => {
|
|
4155
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
4156
|
+
if (!projectRoot) {
|
|
4157
|
+
throw new Error('projectRoot is required');
|
|
4158
|
+
}
|
|
4159
|
+
const sessionId = getSessionId(input);
|
|
4160
|
+
const observedAssets = Array.isArray(input.observedAssets)
|
|
4161
|
+
? input.observedAssets
|
|
4162
|
+
: sessionId
|
|
4163
|
+
? listObservedOverrideAssets(getDb(), { sessionId })
|
|
4164
|
+
: input.observedAssets;
|
|
4165
|
+
const mapping = await mapNextOverrideAssetsWithDrift({
|
|
4166
|
+
projectRoot,
|
|
4167
|
+
nextDir: normalizeOptionalString(input.nextDir),
|
|
4168
|
+
observedAssets,
|
|
4169
|
+
sourcePaths: input.sourcePaths,
|
|
4170
|
+
route: input.route,
|
|
4171
|
+
maxResults: input.maxResults,
|
|
4172
|
+
fetchProductionAssets: input.fetchProductionAssets,
|
|
4173
|
+
productionFetchTimeoutMs: input.productionFetchTimeoutMs,
|
|
4174
|
+
maxProductionAssetBytes: input.maxProductionAssetBytes,
|
|
4175
|
+
maxDriftCandidates: input.maxDriftCandidates,
|
|
4176
|
+
productionFetchConcurrency: input.productionFetchConcurrency,
|
|
4177
|
+
});
|
|
4178
|
+
return {
|
|
4179
|
+
...createBaseResponse(sessionId),
|
|
4180
|
+
limitsApplied: {
|
|
4181
|
+
maxResults: mapping.candidates.length,
|
|
4182
|
+
truncated: false,
|
|
4183
|
+
},
|
|
4184
|
+
observedFromPersisted: !Array.isArray(input.observedAssets) && sessionId
|
|
4185
|
+
? { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 }
|
|
4186
|
+
: undefined,
|
|
4187
|
+
...mapping,
|
|
4188
|
+
};
|
|
4189
|
+
},
|
|
4190
|
+
plan_next_source_override: async (input) => {
|
|
4191
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
4192
|
+
if (!projectRoot) {
|
|
4193
|
+
throw new Error('projectRoot is required');
|
|
4194
|
+
}
|
|
4195
|
+
const sessionId = getSessionId(input);
|
|
4196
|
+
const observedAssets = Array.isArray(input.observedAssets)
|
|
4197
|
+
? input.observedAssets
|
|
4198
|
+
: sessionId
|
|
4199
|
+
? listObservedOverrideAssets(getDb(), { sessionId })
|
|
4200
|
+
: input.observedAssets;
|
|
4201
|
+
const plan = await planNextSourceOverride({
|
|
4202
|
+
projectRoot,
|
|
4203
|
+
nextDir: normalizeOptionalString(input.nextDir),
|
|
4204
|
+
observedAssets,
|
|
4205
|
+
sourceEdits: input.sourceEdits,
|
|
4206
|
+
sourcePaths: input.sourcePaths,
|
|
4207
|
+
route: input.route,
|
|
4208
|
+
configPath: input.configPath,
|
|
4209
|
+
writeConfig: input.writeConfig,
|
|
4210
|
+
overwrite: input.overwrite,
|
|
4211
|
+
enabled: input.enabled,
|
|
4212
|
+
profileEnabled: input.profileEnabled,
|
|
4213
|
+
autoReload: input.autoReload,
|
|
4214
|
+
profileId: input.profileId,
|
|
4215
|
+
profileName: input.profileName,
|
|
4216
|
+
buildTimeoutMs: input.buildTimeoutMs,
|
|
4217
|
+
maxRules: input.maxRules,
|
|
4218
|
+
fetchProductionAssets: input.fetchProductionAssets,
|
|
4219
|
+
productionFetchTimeoutMs: input.productionFetchTimeoutMs,
|
|
4220
|
+
maxProductionAssetBytes: input.maxProductionAssetBytes,
|
|
4221
|
+
maxDriftCandidates: input.maxDriftCandidates,
|
|
4222
|
+
productionFetchConcurrency: input.productionFetchConcurrency,
|
|
4223
|
+
overlayTtlMs: input.overlayTtlMs,
|
|
4224
|
+
});
|
|
4225
|
+
const auditPlans = persistNextSourcePlanAudits({
|
|
4226
|
+
db: getDb(),
|
|
4227
|
+
sessionId,
|
|
4228
|
+
input,
|
|
4229
|
+
plan,
|
|
4230
|
+
});
|
|
4231
|
+
return {
|
|
4232
|
+
...createBaseResponse(sessionId),
|
|
4233
|
+
limitsApplied: {
|
|
4234
|
+
maxResults: plan.rules.length,
|
|
4235
|
+
truncated: false,
|
|
4236
|
+
},
|
|
4237
|
+
observedFromPersisted: !Array.isArray(input.observedAssets) && sessionId
|
|
4238
|
+
? { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 }
|
|
4239
|
+
: undefined,
|
|
4240
|
+
audit: {
|
|
4241
|
+
persisted: auditPlans.length > 0,
|
|
4242
|
+
plans: auditPlans,
|
|
4243
|
+
},
|
|
4244
|
+
...plan,
|
|
4245
|
+
};
|
|
4246
|
+
},
|
|
4247
|
+
get_override_status: async (input) => {
|
|
4248
|
+
const db = getDb();
|
|
4249
|
+
const sessionId = getSessionId(input);
|
|
4250
|
+
const profile = resolveOverrideProfileRecord(input.profileId);
|
|
4251
|
+
const latestRun = sessionId ? listOverridePocRuns(db, sessionId, 1, 0).runs[0] ?? null : null;
|
|
4252
|
+
const recentRequests = sessionId
|
|
4253
|
+
? listOverridePocRequests(db, sessionId, 5, 0, latestRun?.runId).requests
|
|
4254
|
+
: [];
|
|
4255
|
+
const recentPlans = sessionId
|
|
4256
|
+
? listOverridePlanAudits(db, { sessionId, limit: 5, offset: 0 }).plans
|
|
4257
|
+
: [];
|
|
4258
|
+
return {
|
|
4259
|
+
...createBaseResponse(sessionId),
|
|
4260
|
+
profile,
|
|
4261
|
+
latestRun,
|
|
4262
|
+
recentRequests,
|
|
4263
|
+
recentPlans,
|
|
4264
|
+
preflight: sessionId
|
|
4265
|
+
? buildOverridePreflight({
|
|
4266
|
+
db,
|
|
4267
|
+
sessionId,
|
|
4268
|
+
profileId: input.profileId,
|
|
4269
|
+
getSessionConnectionState,
|
|
4270
|
+
})
|
|
4271
|
+
: null,
|
|
4272
|
+
diagnosis: sessionId ? diagnoseOverridePoc(db, sessionId, latestRun?.runId) : null,
|
|
4273
|
+
nextActions: latestRun?.lastErrorCode
|
|
4274
|
+
? [{ code: 'DIAGNOSE_OVERRIDES', message: 'Run diagnose_overrides for the latest failed override run.' }]
|
|
4275
|
+
: latestRun
|
|
4276
|
+
? [{ code: 'GET_OVERRIDE_REQUEST_LOG', message: 'Inspect get_override_request_log for matched and fulfilled requests.' }]
|
|
4277
|
+
: [{ code: 'ENABLE_OVERRIDES', message: 'Enable overrides on a connected session after profile validation succeeds.' }],
|
|
4278
|
+
};
|
|
4279
|
+
},
|
|
4280
|
+
get_override_request_log: async (input) => {
|
|
4281
|
+
const db = getDb();
|
|
4282
|
+
const sessionId = getSessionId(input);
|
|
4283
|
+
if (!sessionId) {
|
|
4284
|
+
throw new Error('sessionId is required');
|
|
4285
|
+
}
|
|
4286
|
+
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
4287
|
+
const offset = resolveOffset(input.offset);
|
|
4288
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
4289
|
+
const runId = typeof input.runId === 'string' && input.runId.trim().length > 0
|
|
4290
|
+
? input.runId.trim()
|
|
4291
|
+
: undefined;
|
|
4292
|
+
const result = listOverridePocRequests(db, sessionId, limit, offset, runId);
|
|
4293
|
+
const bytePage = applyByteBudget(result.requests, maxResponseBytes);
|
|
4294
|
+
const truncated = result.hasMore || bytePage.truncatedByBytes;
|
|
4295
|
+
return {
|
|
4296
|
+
...createBaseResponse(sessionId),
|
|
4297
|
+
limitsApplied: {
|
|
4298
|
+
maxResults: limit,
|
|
4299
|
+
truncated,
|
|
4300
|
+
},
|
|
4301
|
+
runId: runId ?? null,
|
|
4302
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
4303
|
+
responseBytes: bytePage.responseBytes,
|
|
4304
|
+
requests: bytePage.items,
|
|
4305
|
+
nextActions: bytePage.items.length === 0
|
|
4306
|
+
? [{ code: 'RELOAD_TAB', message: 'Reload the selected tab after enabling overrides so matching requests are observed.' }]
|
|
4307
|
+
: [{ code: 'DIAGNOSE_OVERRIDES', message: 'Run diagnose_overrides if any matched request failed or did not fulfill.' }],
|
|
4308
|
+
};
|
|
4309
|
+
},
|
|
4310
|
+
get_override_plan_log: async (input) => {
|
|
4311
|
+
const db = getDb();
|
|
4312
|
+
const sessionId = getSessionId(input);
|
|
4313
|
+
if (!sessionId) {
|
|
4314
|
+
throw new Error('sessionId is required');
|
|
4315
|
+
}
|
|
4316
|
+
const limit = resolveLimit(input.limit, DEFAULT_EVENT_LIMIT);
|
|
4317
|
+
const offset = resolveOffset(input.offset);
|
|
4318
|
+
const maxResponseBytes = resolveMaxResponseBytes(input.maxResponseBytes);
|
|
4319
|
+
const planId = typeof input.planId === 'string' && input.planId.trim().length > 0
|
|
4320
|
+
? input.planId.trim()
|
|
4321
|
+
: undefined;
|
|
4322
|
+
const result = listOverridePlanAudits(db, { sessionId, limit, offset, planId });
|
|
4323
|
+
const bytePage = applyByteBudget(result.plans, maxResponseBytes);
|
|
4324
|
+
const truncated = result.hasMore || bytePage.truncatedByBytes;
|
|
4325
|
+
return {
|
|
4326
|
+
...createBaseResponse(sessionId),
|
|
4327
|
+
limitsApplied: {
|
|
4328
|
+
maxResults: limit,
|
|
4329
|
+
truncated,
|
|
4330
|
+
},
|
|
4331
|
+
planId: planId ?? null,
|
|
4332
|
+
pagination: buildOffsetPagination(offset, bytePage.items.length, truncated, maxResponseBytes),
|
|
4333
|
+
responseBytes: bytePage.responseBytes,
|
|
4334
|
+
plans: bytePage.items,
|
|
4335
|
+
nextActions: bytePage.items.length === 0
|
|
4336
|
+
? [{ code: 'PLAN_OVERRIDE', message: 'Run plan_override_response_patch or plan_next_source_override with sessionId to persist generated rule metadata.' }]
|
|
4337
|
+
: [{ code: 'REVIEW_ROLLBACK', message: 'Review rollback metadata before enabling or deleting generated override files.' }],
|
|
4338
|
+
};
|
|
4339
|
+
},
|
|
4340
|
+
diagnose_overrides: async (input) => {
|
|
2429
4341
|
const db = getDb();
|
|
2430
4342
|
const sessionId = getSessionId(input);
|
|
2431
4343
|
if (!sessionId) {
|
|
2432
4344
|
throw new Error('sessionId is required');
|
|
2433
4345
|
}
|
|
2434
|
-
const
|
|
2435
|
-
.
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
}
|
|
2440
|
-
const counters = db
|
|
2441
|
-
.prepare(`
|
|
2442
|
-
SELECT
|
|
2443
|
-
SUM(CASE WHEN type = 'error' THEN 1 ELSE 0 END) AS errors,
|
|
2444
|
-
SUM(CASE WHEN type = 'console' AND json_extract(payload_json, '$.level') = 'warn' THEN 1 ELSE 0 END) AS warnings
|
|
2445
|
-
FROM events
|
|
2446
|
-
WHERE session_id = ?
|
|
2447
|
-
`)
|
|
2448
|
-
.get(sessionId);
|
|
2449
|
-
const networkFails = db
|
|
2450
|
-
.prepare(`
|
|
2451
|
-
SELECT COUNT(*) AS count
|
|
2452
|
-
FROM network
|
|
2453
|
-
WHERE session_id = ?
|
|
2454
|
-
AND (error_class IS NOT NULL OR COALESCE(status, 0) >= 400)
|
|
2455
|
-
`)
|
|
2456
|
-
.get(sessionId);
|
|
2457
|
-
const latestNav = db
|
|
2458
|
-
.prepare(`
|
|
2459
|
-
SELECT payload_json
|
|
2460
|
-
FROM events
|
|
2461
|
-
WHERE session_id = ? AND type = 'nav'
|
|
2462
|
-
ORDER BY ts DESC
|
|
2463
|
-
LIMIT 1
|
|
2464
|
-
`)
|
|
2465
|
-
.get(sessionId);
|
|
2466
|
-
const eventRange = db
|
|
2467
|
-
.prepare(`
|
|
2468
|
-
SELECT MIN(ts) AS start_ts, MAX(ts) AS end_ts
|
|
2469
|
-
FROM events
|
|
2470
|
-
WHERE session_id = ?
|
|
2471
|
-
`)
|
|
2472
|
-
.get(sessionId);
|
|
2473
|
-
const navPayload = latestNav ? readJsonPayload(latestNav.payload_json) : {};
|
|
2474
|
-
const lastUrl = resolveLastUrl(navPayload) ?? session.url_last ?? undefined;
|
|
4346
|
+
const runId = typeof input.runId === 'string' && input.runId.trim().length > 0
|
|
4347
|
+
? input.runId.trim()
|
|
4348
|
+
: undefined;
|
|
4349
|
+
const diagnosis = diagnoseOverridePoc(db, sessionId, runId);
|
|
4350
|
+
const firstIssue = diagnosis.issues[0];
|
|
2475
4351
|
return {
|
|
2476
4352
|
...createBaseResponse(sessionId),
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
},
|
|
2482
|
-
lastUrl,
|
|
2483
|
-
timeRange: {
|
|
2484
|
-
start: eventRange.start_ts ?? session.created_at,
|
|
2485
|
-
end: eventRange.end_ts ?? session.ended_at ?? session.created_at,
|
|
2486
|
-
},
|
|
2487
|
-
pinned: session.pinned === 1,
|
|
4353
|
+
diagnosis,
|
|
4354
|
+
nextActions: firstIssue?.suggestedActions[0]
|
|
4355
|
+
? [{ code: firstIssue.code, message: firstIssue.suggestedActions[0] }]
|
|
4356
|
+
: [{ code: 'NO_DIAGNOSIS_ISSUES', message: 'No diagnosis issues were found for the selected override run.' }],
|
|
2488
4357
|
};
|
|
2489
4358
|
},
|
|
2490
4359
|
get_recent_events: async (input) => {
|
|
@@ -3822,7 +5691,7 @@ export function createV1ToolHandlers(getDb, getSessionConnectionState) {
|
|
|
3822
5691
|
},
|
|
3823
5692
|
};
|
|
3824
5693
|
}
|
|
3825
|
-
export function createV2ToolHandlers(captureClient) {
|
|
5694
|
+
export function createV2ToolHandlers(captureClient, getDb, getSessionConnectionState) {
|
|
3826
5695
|
const capturePageState = async (sessionId, input) => {
|
|
3827
5696
|
const maxItems = resolveStructuredMaxItems(input.maxItems, 40);
|
|
3828
5697
|
const maxTextLength = resolveStructuredTextLength(input.maxTextLength, 80);
|
|
@@ -3845,6 +5714,601 @@ export function createV2ToolHandlers(captureClient) {
|
|
|
3845
5714
|
};
|
|
3846
5715
|
};
|
|
3847
5716
|
return {
|
|
5717
|
+
observe_override_assets: async (input) => {
|
|
5718
|
+
const sessionId = getSessionId(input);
|
|
5719
|
+
if (!sessionId) {
|
|
5720
|
+
throw new Error('sessionId is required');
|
|
5721
|
+
}
|
|
5722
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
5723
|
+
const { capture, payload } = await executeOverrideLiveCaptureWithDiagnostics({
|
|
5724
|
+
captureClient,
|
|
5725
|
+
sessionId,
|
|
5726
|
+
command: 'CAPTURE_OVERRIDE_OBSERVE_ASSETS',
|
|
5727
|
+
payload: { tabId, includePerformance: input.includePerformance !== false },
|
|
5728
|
+
timeoutMs: 5_000,
|
|
5729
|
+
getSessionConnectionState,
|
|
5730
|
+
});
|
|
5731
|
+
const assetCount = Array.isArray(payload.assets) ? payload.assets.length : 0;
|
|
5732
|
+
const persisted = getDb
|
|
5733
|
+
? persistObservedOverrideAssets(getDb(), { ...payload, sessionId, tabId: payload.tabId ?? tabId })
|
|
5734
|
+
: undefined;
|
|
5735
|
+
return {
|
|
5736
|
+
...createBaseResponse(sessionId),
|
|
5737
|
+
limitsApplied: {
|
|
5738
|
+
maxResults: assetCount,
|
|
5739
|
+
truncated: capture.truncated ?? false,
|
|
5740
|
+
},
|
|
5741
|
+
persisted,
|
|
5742
|
+
...payload,
|
|
5743
|
+
nextActions: assetCount > 0
|
|
5744
|
+
? [{ code: 'MAP_NEXT_ASSETS', message: 'Run map_next_override_assets with projectRoot and sourcePaths to score override candidates.' }]
|
|
5745
|
+
: [{ code: 'LOAD_ROUTE', message: 'Load or interact with the target route so document, asset, and fetch resources are requested, then observe again.' }],
|
|
5746
|
+
};
|
|
5747
|
+
},
|
|
5748
|
+
capture_override_response_body: async (input) => {
|
|
5749
|
+
const sessionId = getSessionId(input);
|
|
5750
|
+
if (!sessionId) {
|
|
5751
|
+
throw new Error('sessionId is required');
|
|
5752
|
+
}
|
|
5753
|
+
const targetUrl = normalizeOptionalString(input.targetUrl) ?? normalizeOptionalString(input.targetAssetUrl);
|
|
5754
|
+
if (!targetUrl) {
|
|
5755
|
+
throw new Error('targetUrl is required');
|
|
5756
|
+
}
|
|
5757
|
+
assertOverrideResponseRequestCaptureSafe({
|
|
5758
|
+
requestMethod: input.requestMethod,
|
|
5759
|
+
requestHeaders: input.requestHeaders,
|
|
5760
|
+
ruleType: input.ruleType,
|
|
5761
|
+
subject: 'Response body capture request',
|
|
5762
|
+
});
|
|
5763
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
5764
|
+
const timeoutMs = resolveTimeoutMs(input.timeoutMs, 10_000, 60_000);
|
|
5765
|
+
const { capture, payload } = await executeOverrideLiveCaptureWithDiagnostics({
|
|
5766
|
+
captureClient,
|
|
5767
|
+
sessionId,
|
|
5768
|
+
command: 'CAPTURE_OVERRIDE_RESPONSE_BODY',
|
|
5769
|
+
payload: {
|
|
5770
|
+
targetUrl,
|
|
5771
|
+
tabId,
|
|
5772
|
+
captureMode: normalizeOptionalString(input.captureMode),
|
|
5773
|
+
triggerReload: typeof input.triggerReload === 'boolean' ? input.triggerReload : undefined,
|
|
5774
|
+
matchMode: normalizeOptionalString(input.matchMode),
|
|
5775
|
+
ruleType: normalizeOptionalString(input.ruleType),
|
|
5776
|
+
requestMethod: input.requestMethod,
|
|
5777
|
+
requestHeaders: isRecord(input.requestHeaders) ? input.requestHeaders : undefined,
|
|
5778
|
+
timeoutMs,
|
|
5779
|
+
maxBodyBytes: input.maxBodyBytes,
|
|
5780
|
+
includeBody: input.includeBody === true,
|
|
5781
|
+
},
|
|
5782
|
+
timeoutMs: timeoutMs + 2_000,
|
|
5783
|
+
getSessionConnectionState,
|
|
5784
|
+
});
|
|
5785
|
+
return {
|
|
5786
|
+
...createBaseResponse(sessionId),
|
|
5787
|
+
limitsApplied: {
|
|
5788
|
+
maxResults: 1,
|
|
5789
|
+
truncated: capture.truncated ?? payload.truncated === true,
|
|
5790
|
+
},
|
|
5791
|
+
...payload,
|
|
5792
|
+
nextActions: payload.bodyCaptured === true
|
|
5793
|
+
? [{ code: 'PLAN_RESPONSE_PATCH', message: 'Run plan_override_response_patch with textPatches or jsonPatches to generate an exact response override.' }]
|
|
5794
|
+
: [{ code: 'UNSUPPORTED_RESPONSE_BODY', message: 'Only bounded text-like response bodies can be patched safely.' }],
|
|
5795
|
+
};
|
|
5796
|
+
},
|
|
5797
|
+
plan_override_response_patch: async (input) => {
|
|
5798
|
+
const sessionId = getSessionId(input);
|
|
5799
|
+
let plannerInput = input;
|
|
5800
|
+
let capturedFromLiveSession;
|
|
5801
|
+
const hasProvidedBody = typeof input.responseBodyText === 'string'
|
|
5802
|
+
|| typeof input.bodyText === 'string'
|
|
5803
|
+
|| typeof input.responseBodyBase64 === 'string'
|
|
5804
|
+
|| typeof input.bodyBase64 === 'string';
|
|
5805
|
+
if (!hasProvidedBody && sessionId) {
|
|
5806
|
+
const targetUrl = normalizeOptionalString(input.targetUrl) ?? normalizeOptionalString(input.targetAssetUrl);
|
|
5807
|
+
if (!targetUrl) {
|
|
5808
|
+
throw new Error('targetUrl is required');
|
|
5809
|
+
}
|
|
5810
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
5811
|
+
const timeoutMs = resolveTimeoutMs(input.timeoutMs, 10_000, 60_000);
|
|
5812
|
+
const { payload } = await executeOverrideLiveCaptureWithDiagnostics({
|
|
5813
|
+
captureClient,
|
|
5814
|
+
sessionId,
|
|
5815
|
+
command: 'CAPTURE_OVERRIDE_RESPONSE_BODY',
|
|
5816
|
+
payload: {
|
|
5817
|
+
targetUrl,
|
|
5818
|
+
tabId,
|
|
5819
|
+
captureMode: normalizeOptionalString(input.captureMode),
|
|
5820
|
+
triggerReload: typeof input.triggerReload === 'boolean' ? input.triggerReload : undefined,
|
|
5821
|
+
matchMode: normalizeOptionalString(input.matchMode),
|
|
5822
|
+
ruleType: normalizeOptionalString(input.ruleType),
|
|
5823
|
+
requestMethod: input.requestMethod,
|
|
5824
|
+
requestHeaders: isRecord(input.requestHeaders) ? input.requestHeaders : undefined,
|
|
5825
|
+
timeoutMs,
|
|
5826
|
+
maxBodyBytes: input.maxBodyBytes,
|
|
5827
|
+
includeBody: true,
|
|
5828
|
+
},
|
|
5829
|
+
timeoutMs: timeoutMs + 2_000,
|
|
5830
|
+
getSessionConnectionState,
|
|
5831
|
+
});
|
|
5832
|
+
if (payload.truncated === true) {
|
|
5833
|
+
throw new Error('Captured response body was truncated; increase maxBodyBytes before planning a patch.');
|
|
5834
|
+
}
|
|
5835
|
+
if (typeof payload.bodyText !== 'string') {
|
|
5836
|
+
throw new Error('Captured response did not include a text body that can be patched.');
|
|
5837
|
+
}
|
|
5838
|
+
plannerInput = {
|
|
5839
|
+
...input,
|
|
5840
|
+
responseBodyText: payload.bodyText,
|
|
5841
|
+
contentType: input.contentType ?? payload.contentType,
|
|
5842
|
+
ruleType: input.ruleType ?? payload.ruleType,
|
|
5843
|
+
requestMethod: input.requestMethod ?? payload.requestMethod,
|
|
5844
|
+
captureMode: input.captureMode ?? payload.captureMode,
|
|
5845
|
+
source: payload.source,
|
|
5846
|
+
requestHeaders: payload.requestHeaders,
|
|
5847
|
+
};
|
|
5848
|
+
const variantContext = buildOverrideVariantContext({
|
|
5849
|
+
targetUrl: payload.targetUrl,
|
|
5850
|
+
requestMethod: input.requestMethod ?? payload.requestMethod,
|
|
5851
|
+
matchMode: payload.matchMode,
|
|
5852
|
+
ruleType: input.ruleType ?? payload.ruleType,
|
|
5853
|
+
captureMode: payload.captureMode,
|
|
5854
|
+
source: payload.source,
|
|
5855
|
+
triggerReload: payload.triggerReload,
|
|
5856
|
+
requestHeaders: payload.requestHeaders,
|
|
5857
|
+
});
|
|
5858
|
+
capturedFromLiveSession = {
|
|
5859
|
+
sessionId,
|
|
5860
|
+
targetUrl: payload.targetUrl,
|
|
5861
|
+
requestMethod: input.requestMethod ?? payload.requestMethod,
|
|
5862
|
+
statusCode: payload.statusCode,
|
|
5863
|
+
contentType: payload.contentType,
|
|
5864
|
+
bodyBytes: payload.bodyBytes,
|
|
5865
|
+
capturedBytes: payload.capturedBytes,
|
|
5866
|
+
truncated: payload.truncated === true,
|
|
5867
|
+
ruleType: payload.ruleType,
|
|
5868
|
+
matchMode: payload.matchMode,
|
|
5869
|
+
captureMode: payload.captureMode,
|
|
5870
|
+
source: payload.source,
|
|
5871
|
+
tabId: payload.tabId,
|
|
5872
|
+
triggerReload: payload.triggerReload,
|
|
5873
|
+
requestHeaders: payload.requestHeaders,
|
|
5874
|
+
variantContext,
|
|
5875
|
+
};
|
|
5876
|
+
}
|
|
5877
|
+
const plan = planOverrideResponsePatch(plannerInput);
|
|
5878
|
+
const variantContext = buildOverrideVariantContext({
|
|
5879
|
+
targetUrl: plan.targetUrl,
|
|
5880
|
+
requestMethod: plan.requestMethod,
|
|
5881
|
+
matchMode: plan.matchMode,
|
|
5882
|
+
ruleType: plan.ruleType,
|
|
5883
|
+
captureMode: plannerInput.captureMode,
|
|
5884
|
+
source: plannerInput.source,
|
|
5885
|
+
triggerReload: plannerInput.triggerReload,
|
|
5886
|
+
requestHeaders: plannerInput.requestHeaders,
|
|
5887
|
+
});
|
|
5888
|
+
const auditPlan = getDb
|
|
5889
|
+
? persistResponsePlanAudit({
|
|
5890
|
+
db: getDb(),
|
|
5891
|
+
sessionId,
|
|
5892
|
+
input,
|
|
5893
|
+
plan,
|
|
5894
|
+
capturedFromLiveSession,
|
|
5895
|
+
variantContext,
|
|
5896
|
+
})
|
|
5897
|
+
: undefined;
|
|
5898
|
+
return {
|
|
5899
|
+
...createBaseResponse(sessionId),
|
|
5900
|
+
limitsApplied: {
|
|
5901
|
+
maxResults: plan.rule ? 1 : 0,
|
|
5902
|
+
truncated: false,
|
|
5903
|
+
},
|
|
5904
|
+
capturedFromLiveSession,
|
|
5905
|
+
variantContext,
|
|
5906
|
+
audit: {
|
|
5907
|
+
persisted: auditPlan !== undefined,
|
|
5908
|
+
plans: auditPlan ? [auditPlan] : [],
|
|
5909
|
+
},
|
|
5910
|
+
...plan,
|
|
5911
|
+
};
|
|
5912
|
+
},
|
|
5913
|
+
map_next_override_assets: async (input) => {
|
|
5914
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
5915
|
+
if (!projectRoot) {
|
|
5916
|
+
throw new Error('projectRoot is required');
|
|
5917
|
+
}
|
|
5918
|
+
const sessionId = getSessionId(input);
|
|
5919
|
+
let observedAssets = input.observedAssets;
|
|
5920
|
+
let observedFromLiveTab;
|
|
5921
|
+
let observedFromPersisted;
|
|
5922
|
+
if (!Array.isArray(observedAssets) && sessionId) {
|
|
5923
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
5924
|
+
const command = 'CAPTURE_OVERRIDE_OBSERVE_ASSETS';
|
|
5925
|
+
const timeoutMs = 5_000;
|
|
5926
|
+
try {
|
|
5927
|
+
const capture = await executeLiveCapture(captureClient, sessionId, command, { tabId, includePerformance: true }, timeoutMs);
|
|
5928
|
+
observedFromLiveTab = ensureCaptureSuccess(capture, sessionId);
|
|
5929
|
+
observedAssets = observedFromLiveTab.assets;
|
|
5930
|
+
if (getDb) {
|
|
5931
|
+
persistObservedOverrideAssets(getDb(), { ...observedFromLiveTab, sessionId, tabId: observedFromLiveTab.tabId ?? tabId });
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
catch (error) {
|
|
5935
|
+
if (getDb && isRecoverableOverrideLiveCommandError(error)) {
|
|
5936
|
+
observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
|
|
5937
|
+
observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
|
|
5938
|
+
}
|
|
5939
|
+
else if (isRecoverableOverrideLiveCommandError(error)) {
|
|
5940
|
+
throw createOverrideLiveCommandError({
|
|
5941
|
+
sessionId,
|
|
5942
|
+
command,
|
|
5943
|
+
timeoutMs,
|
|
5944
|
+
error,
|
|
5945
|
+
getSessionConnectionState,
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5948
|
+
else {
|
|
5949
|
+
throw error;
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
const mapping = await mapNextOverrideAssetsWithDrift({
|
|
5954
|
+
projectRoot,
|
|
5955
|
+
nextDir: normalizeOptionalString(input.nextDir),
|
|
5956
|
+
observedAssets,
|
|
5957
|
+
sourcePaths: input.sourcePaths,
|
|
5958
|
+
route: input.route,
|
|
5959
|
+
maxResults: input.maxResults,
|
|
5960
|
+
fetchProductionAssets: input.fetchProductionAssets,
|
|
5961
|
+
productionFetchTimeoutMs: input.productionFetchTimeoutMs,
|
|
5962
|
+
maxProductionAssetBytes: input.maxProductionAssetBytes,
|
|
5963
|
+
maxDriftCandidates: input.maxDriftCandidates,
|
|
5964
|
+
productionFetchConcurrency: input.productionFetchConcurrency,
|
|
5965
|
+
});
|
|
5966
|
+
return {
|
|
5967
|
+
...createBaseResponse(sessionId),
|
|
5968
|
+
limitsApplied: {
|
|
5969
|
+
maxResults: mapping.candidates.length,
|
|
5970
|
+
truncated: false,
|
|
5971
|
+
},
|
|
5972
|
+
observedFromLiveTab: observedFromLiveTab
|
|
5973
|
+
? {
|
|
5974
|
+
pageUrl: observedFromLiveTab.pageUrl,
|
|
5975
|
+
tabId: observedFromLiveTab.tabId,
|
|
5976
|
+
assetCount: Array.isArray(observedFromLiveTab.assets) ? observedFromLiveTab.assets.length : 0,
|
|
5977
|
+
}
|
|
5978
|
+
: undefined,
|
|
5979
|
+
observedFromPersisted,
|
|
5980
|
+
...mapping,
|
|
5981
|
+
};
|
|
5982
|
+
},
|
|
5983
|
+
plan_next_source_override: async (input) => {
|
|
5984
|
+
const projectRoot = normalizeOptionalString(input.projectRoot);
|
|
5985
|
+
if (!projectRoot) {
|
|
5986
|
+
throw new Error('projectRoot is required');
|
|
5987
|
+
}
|
|
5988
|
+
const sessionId = getSessionId(input);
|
|
5989
|
+
let observedAssets = input.observedAssets;
|
|
5990
|
+
let observedFromLiveTab;
|
|
5991
|
+
let observedFromPersisted;
|
|
5992
|
+
if (!Array.isArray(observedAssets) && sessionId) {
|
|
5993
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
5994
|
+
const command = 'CAPTURE_OVERRIDE_OBSERVE_ASSETS';
|
|
5995
|
+
const timeoutMs = 5_000;
|
|
5996
|
+
try {
|
|
5997
|
+
const capture = await executeLiveCapture(captureClient, sessionId, command, { tabId, includePerformance: true }, timeoutMs);
|
|
5998
|
+
observedFromLiveTab = ensureCaptureSuccess(capture, sessionId);
|
|
5999
|
+
observedAssets = observedFromLiveTab.assets;
|
|
6000
|
+
if (getDb) {
|
|
6001
|
+
persistObservedOverrideAssets(getDb(), { ...observedFromLiveTab, sessionId, tabId: observedFromLiveTab.tabId ?? tabId });
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
catch (error) {
|
|
6005
|
+
if (getDb && isRecoverableOverrideLiveCommandError(error)) {
|
|
6006
|
+
observedAssets = listObservedOverrideAssets(getDb(), { sessionId });
|
|
6007
|
+
observedFromPersisted = { sessionId, assetCount: Array.isArray(observedAssets) ? observedAssets.length : 0 };
|
|
6008
|
+
}
|
|
6009
|
+
else if (isRecoverableOverrideLiveCommandError(error)) {
|
|
6010
|
+
throw createOverrideLiveCommandError({
|
|
6011
|
+
sessionId,
|
|
6012
|
+
command,
|
|
6013
|
+
timeoutMs,
|
|
6014
|
+
error,
|
|
6015
|
+
getSessionConnectionState,
|
|
6016
|
+
});
|
|
6017
|
+
}
|
|
6018
|
+
else {
|
|
6019
|
+
throw error;
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
}
|
|
6023
|
+
const plan = await planNextSourceOverride({
|
|
6024
|
+
projectRoot,
|
|
6025
|
+
nextDir: normalizeOptionalString(input.nextDir),
|
|
6026
|
+
observedAssets,
|
|
6027
|
+
sourceEdits: input.sourceEdits,
|
|
6028
|
+
sourcePaths: input.sourcePaths,
|
|
6029
|
+
route: input.route,
|
|
6030
|
+
configPath: input.configPath,
|
|
6031
|
+
writeConfig: input.writeConfig,
|
|
6032
|
+
overwrite: input.overwrite,
|
|
6033
|
+
enabled: input.enabled,
|
|
6034
|
+
profileEnabled: input.profileEnabled,
|
|
6035
|
+
autoReload: input.autoReload,
|
|
6036
|
+
profileId: input.profileId,
|
|
6037
|
+
profileName: input.profileName,
|
|
6038
|
+
buildTimeoutMs: input.buildTimeoutMs,
|
|
6039
|
+
maxRules: input.maxRules,
|
|
6040
|
+
fetchProductionAssets: input.fetchProductionAssets,
|
|
6041
|
+
productionFetchTimeoutMs: input.productionFetchTimeoutMs,
|
|
6042
|
+
maxProductionAssetBytes: input.maxProductionAssetBytes,
|
|
6043
|
+
maxDriftCandidates: input.maxDriftCandidates,
|
|
6044
|
+
productionFetchConcurrency: input.productionFetchConcurrency,
|
|
6045
|
+
overlayTtlMs: input.overlayTtlMs,
|
|
6046
|
+
});
|
|
6047
|
+
const auditPlans = getDb
|
|
6048
|
+
? persistNextSourcePlanAudits({
|
|
6049
|
+
db: getDb(),
|
|
6050
|
+
sessionId,
|
|
6051
|
+
input,
|
|
6052
|
+
plan,
|
|
6053
|
+
})
|
|
6054
|
+
: [];
|
|
6055
|
+
return {
|
|
6056
|
+
...createBaseResponse(sessionId),
|
|
6057
|
+
limitsApplied: {
|
|
6058
|
+
maxResults: plan.rules.length,
|
|
6059
|
+
truncated: false,
|
|
6060
|
+
},
|
|
6061
|
+
observedFromLiveTab: observedFromLiveTab
|
|
6062
|
+
? {
|
|
6063
|
+
pageUrl: observedFromLiveTab.pageUrl,
|
|
6064
|
+
tabId: observedFromLiveTab.tabId,
|
|
6065
|
+
assetCount: Array.isArray(observedFromLiveTab.assets) ? observedFromLiveTab.assets.length : 0,
|
|
6066
|
+
}
|
|
6067
|
+
: undefined,
|
|
6068
|
+
observedFromPersisted,
|
|
6069
|
+
audit: {
|
|
6070
|
+
persisted: auditPlans.length > 0,
|
|
6071
|
+
plans: auditPlans,
|
|
6072
|
+
},
|
|
6073
|
+
...plan,
|
|
6074
|
+
};
|
|
6075
|
+
},
|
|
6076
|
+
get_override_status: async (input) => {
|
|
6077
|
+
const sessionId = getSessionId(input);
|
|
6078
|
+
if (!sessionId) {
|
|
6079
|
+
throw new Error('sessionId is required');
|
|
6080
|
+
}
|
|
6081
|
+
const command = 'CAPTURE_OVERRIDE_POC_GET_STATUS';
|
|
6082
|
+
const timeoutMs = 3_000;
|
|
6083
|
+
let capture;
|
|
6084
|
+
let payload;
|
|
6085
|
+
try {
|
|
6086
|
+
capture = await executeLiveCapture(captureClient, sessionId, command, {}, timeoutMs);
|
|
6087
|
+
payload = ensureCaptureSuccess(capture, sessionId);
|
|
6088
|
+
}
|
|
6089
|
+
catch (error) {
|
|
6090
|
+
if (!getDb || !isRecoverableOverrideLiveCommandError(error)) {
|
|
6091
|
+
throw error;
|
|
6092
|
+
}
|
|
6093
|
+
const persisted = buildPersistedOverrideStatus({
|
|
6094
|
+
db: getDb(),
|
|
6095
|
+
sessionId,
|
|
6096
|
+
profileId: input.profileId,
|
|
6097
|
+
getSessionConnectionState,
|
|
6098
|
+
});
|
|
6099
|
+
const liveStatus = buildOverrideLiveCommandFailure({
|
|
6100
|
+
sessionId,
|
|
6101
|
+
command,
|
|
6102
|
+
timeoutMs,
|
|
6103
|
+
error,
|
|
6104
|
+
getSessionConnectionState,
|
|
6105
|
+
});
|
|
6106
|
+
return {
|
|
6107
|
+
...createBaseResponse(sessionId),
|
|
6108
|
+
limitsApplied: {
|
|
6109
|
+
maxResults: 1,
|
|
6110
|
+
truncated: false,
|
|
6111
|
+
},
|
|
6112
|
+
statusSource: 'persisted-audit',
|
|
6113
|
+
liveStatus,
|
|
6114
|
+
...persisted,
|
|
6115
|
+
nextActions: [
|
|
6116
|
+
{ code: 'RECONNECT_OR_RETRY_OVERRIDE_STATUS', message: 'Reconnect or rebind the top-level session, then retry get_override_status for live debugger state.' },
|
|
6117
|
+
{ code: 'DIAGNOSE_OVERRIDES', message: 'Run diagnose_overrides to inspect persisted run and readiness signals while live status is unavailable.' },
|
|
6118
|
+
],
|
|
6119
|
+
};
|
|
6120
|
+
}
|
|
6121
|
+
return {
|
|
6122
|
+
...createBaseResponse(sessionId),
|
|
6123
|
+
limitsApplied: {
|
|
6124
|
+
maxResults: 1,
|
|
6125
|
+
truncated: capture.truncated ?? false,
|
|
6126
|
+
},
|
|
6127
|
+
statusSource: 'live',
|
|
6128
|
+
liveStatus: {
|
|
6129
|
+
available: true,
|
|
6130
|
+
command,
|
|
6131
|
+
timeoutMs,
|
|
6132
|
+
},
|
|
6133
|
+
preflight: getDb
|
|
6134
|
+
? buildOverridePreflight({
|
|
6135
|
+
db: getDb(),
|
|
6136
|
+
sessionId,
|
|
6137
|
+
profileId: input.profileId,
|
|
6138
|
+
getSessionConnectionState,
|
|
6139
|
+
})
|
|
6140
|
+
: null,
|
|
6141
|
+
...payload,
|
|
6142
|
+
nextActions: payload.lastErrorCode
|
|
6143
|
+
? [{ code: 'DIAGNOSE_OVERRIDES', message: 'Run diagnose_overrides for the latest override failure.' }]
|
|
6144
|
+
: payload.active === true
|
|
6145
|
+
? [{ code: 'GET_OVERRIDE_REQUEST_LOG', message: 'Inspect get_override_request_log after the target tab loads matching assets.' }]
|
|
6146
|
+
: [{ code: 'ENABLE_OVERRIDES', message: 'Enable overrides after validating the selected profile.' }],
|
|
6147
|
+
};
|
|
6148
|
+
},
|
|
6149
|
+
preflight_overrides: async (input) => {
|
|
6150
|
+
const sessionId = getSessionId(input);
|
|
6151
|
+
if (!sessionId) {
|
|
6152
|
+
throw new Error('sessionId is required');
|
|
6153
|
+
}
|
|
6154
|
+
if (!getDb) {
|
|
6155
|
+
throw new Error('preflight_overrides requires database-backed override state');
|
|
6156
|
+
}
|
|
6157
|
+
return {
|
|
6158
|
+
...createBaseResponse(sessionId),
|
|
6159
|
+
...buildOverridePreflight({
|
|
6160
|
+
db: getDb(),
|
|
6161
|
+
sessionId,
|
|
6162
|
+
profileId: input.profileId,
|
|
6163
|
+
getSessionConnectionState,
|
|
6164
|
+
}),
|
|
6165
|
+
};
|
|
6166
|
+
},
|
|
6167
|
+
enable_overrides: async (input) => {
|
|
6168
|
+
const sessionId = getSessionId(input);
|
|
6169
|
+
if (!sessionId) {
|
|
6170
|
+
throw new Error('sessionId is required');
|
|
6171
|
+
}
|
|
6172
|
+
const tabId = resolveOptionalTabId(input.tabId);
|
|
6173
|
+
let preflight = getDb
|
|
6174
|
+
? buildOverridePreflight({
|
|
6175
|
+
db: getDb(),
|
|
6176
|
+
sessionId,
|
|
6177
|
+
profileId: input.profileId,
|
|
6178
|
+
getSessionConnectionState,
|
|
6179
|
+
})
|
|
6180
|
+
: null;
|
|
6181
|
+
let observedBeforeEnable;
|
|
6182
|
+
let observedAssetRefreshError;
|
|
6183
|
+
const initialBlockingCodes = getBlockingPreflightCodes(preflight);
|
|
6184
|
+
const initialProfile = isRecord(preflight?.profile) ? preflight.profile : {};
|
|
6185
|
+
const initialExperimentalBypass = canBypassPreflightForExperimentalRsc(initialProfile, initialBlockingCodes);
|
|
6186
|
+
if (preflight && getDb && !initialExperimentalBypass && shouldRefreshObservedAssetsForEnable(preflight)) {
|
|
6187
|
+
try {
|
|
6188
|
+
observedBeforeEnable = await refreshObservedAssetsForOverrideEnable({
|
|
6189
|
+
captureClient,
|
|
6190
|
+
db: getDb(),
|
|
6191
|
+
sessionId,
|
|
6192
|
+
tabId,
|
|
6193
|
+
getSessionConnectionState,
|
|
6194
|
+
});
|
|
6195
|
+
preflight = buildOverridePreflight({
|
|
6196
|
+
db: getDb(),
|
|
6197
|
+
sessionId,
|
|
6198
|
+
profileId: input.profileId,
|
|
6199
|
+
getSessionConnectionState,
|
|
6200
|
+
});
|
|
6201
|
+
}
|
|
6202
|
+
catch (error) {
|
|
6203
|
+
observedAssetRefreshError = error instanceof Error ? error.message : String(error);
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
if (preflight && preflight.ready !== true) {
|
|
6207
|
+
const blockingCodes = getBlockingPreflightCodes(preflight);
|
|
6208
|
+
const profile = isRecord(preflight.profile) ? preflight.profile : {};
|
|
6209
|
+
if (!canBypassPreflightForExperimentalRsc(profile, blockingCodes)) {
|
|
6210
|
+
const refreshSuffix = observedAssetRefreshError
|
|
6211
|
+
? `; observed asset refresh failed: ${observedAssetRefreshError}`
|
|
6212
|
+
: '';
|
|
6213
|
+
throw new Error(`Override preflight failed: ${blockingCodes.join(', ') || 'UNKNOWN'}${refreshSuffix}`);
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
const command = 'CAPTURE_OVERRIDE_POC_ENABLE';
|
|
6217
|
+
const timeoutMs = 8_000;
|
|
6218
|
+
let capture;
|
|
6219
|
+
let payload;
|
|
6220
|
+
try {
|
|
6221
|
+
capture = await executeLiveCapture(captureClient, sessionId, command, { tabId }, timeoutMs);
|
|
6222
|
+
payload = ensureCaptureSuccess(capture, sessionId);
|
|
6223
|
+
}
|
|
6224
|
+
catch (error) {
|
|
6225
|
+
throw createOverrideLiveCommandError({
|
|
6226
|
+
sessionId,
|
|
6227
|
+
command,
|
|
6228
|
+
timeoutMs,
|
|
6229
|
+
error,
|
|
6230
|
+
getSessionConnectionState,
|
|
6231
|
+
});
|
|
6232
|
+
}
|
|
6233
|
+
return {
|
|
6234
|
+
...createBaseResponse(sessionId),
|
|
6235
|
+
limitsApplied: {
|
|
6236
|
+
maxResults: 1,
|
|
6237
|
+
truncated: capture.truncated ?? false,
|
|
6238
|
+
},
|
|
6239
|
+
preflight,
|
|
6240
|
+
observedBeforeEnable,
|
|
6241
|
+
...payload,
|
|
6242
|
+
nextActions: [{ code: 'RELOAD_OR_INTERACT', message: 'Reload or interact with the tab so configured asset requests occur under the active override.' }],
|
|
6243
|
+
};
|
|
6244
|
+
},
|
|
6245
|
+
disable_overrides: async (input) => {
|
|
6246
|
+
const sessionId = getSessionId(input);
|
|
6247
|
+
if (!sessionId) {
|
|
6248
|
+
throw new Error('sessionId is required');
|
|
6249
|
+
}
|
|
6250
|
+
const command = 'CAPTURE_OVERRIDE_POC_DISABLE';
|
|
6251
|
+
const timeoutMs = 5_000;
|
|
6252
|
+
let capture;
|
|
6253
|
+
let payload;
|
|
6254
|
+
try {
|
|
6255
|
+
capture = await executeLiveCapture(captureClient, sessionId, command, {}, timeoutMs);
|
|
6256
|
+
payload = ensureCaptureSuccess(capture, sessionId);
|
|
6257
|
+
}
|
|
6258
|
+
catch (error) {
|
|
6259
|
+
if (!getDb || !isRecoverableOverrideLiveCommandError(error)) {
|
|
6260
|
+
throw createOverrideLiveCommandError({
|
|
6261
|
+
sessionId,
|
|
6262
|
+
command,
|
|
6263
|
+
timeoutMs,
|
|
6264
|
+
error,
|
|
6265
|
+
getSessionConnectionState,
|
|
6266
|
+
});
|
|
6267
|
+
}
|
|
6268
|
+
const persisted = buildPersistedOverrideStatus({
|
|
6269
|
+
db: getDb(),
|
|
6270
|
+
sessionId,
|
|
6271
|
+
profileId: input.profileId,
|
|
6272
|
+
getSessionConnectionState,
|
|
6273
|
+
});
|
|
6274
|
+
const disableAttempt = buildOverrideLiveCommandFailure({
|
|
6275
|
+
sessionId,
|
|
6276
|
+
command,
|
|
6277
|
+
timeoutMs,
|
|
6278
|
+
error,
|
|
6279
|
+
getSessionConnectionState,
|
|
6280
|
+
});
|
|
6281
|
+
return {
|
|
6282
|
+
...createBaseResponse(sessionId),
|
|
6283
|
+
limitsApplied: {
|
|
6284
|
+
maxResults: 1,
|
|
6285
|
+
truncated: false,
|
|
6286
|
+
},
|
|
6287
|
+
statusSource: 'persisted-audit',
|
|
6288
|
+
disableAttempt,
|
|
6289
|
+
...persisted,
|
|
6290
|
+
nextActions: [
|
|
6291
|
+
{ code: 'RECONNECT_OR_RETRY_DISABLE', message: 'Reconnect or rebind the top-level session, then retry disable_overrides to confirm debugger detachment.' },
|
|
6292
|
+
{ code: 'GET_OVERRIDE_STATUS', message: 'Run get_override_status after reconnecting to verify whether the override is still active.' },
|
|
6293
|
+
],
|
|
6294
|
+
};
|
|
6295
|
+
}
|
|
6296
|
+
return {
|
|
6297
|
+
...createBaseResponse(sessionId),
|
|
6298
|
+
limitsApplied: {
|
|
6299
|
+
maxResults: 1,
|
|
6300
|
+
truncated: capture.truncated ?? false,
|
|
6301
|
+
},
|
|
6302
|
+
statusSource: 'live',
|
|
6303
|
+
disableAttempt: {
|
|
6304
|
+
ok: true,
|
|
6305
|
+
command,
|
|
6306
|
+
timeoutMs,
|
|
6307
|
+
},
|
|
6308
|
+
...payload,
|
|
6309
|
+
nextActions: [{ code: 'VERIFY_DISABLED', message: 'Run get_override_status if you need to confirm the debugger override is inactive.' }],
|
|
6310
|
+
};
|
|
6311
|
+
},
|
|
3848
6312
|
get_dom_subtree: async (input) => {
|
|
3849
6313
|
const sessionId = getSessionId(input);
|
|
3850
6314
|
if (!sessionId) {
|
|
@@ -4522,7 +6986,9 @@ export async function routeToolCall(tools, toolName, input) {
|
|
|
4522
6986
|
}
|
|
4523
6987
|
export function createMCPServer(overrides = {}, options = {}) {
|
|
4524
6988
|
const logger = options.logger ?? createDefaultMcpLogger();
|
|
4525
|
-
const v2Handlers = options.captureClient
|
|
6989
|
+
const v2Handlers = options.captureClient
|
|
6990
|
+
? createV2ToolHandlers(options.captureClient, () => getConnection().db, options.getSessionConnectionState)
|
|
6991
|
+
: {};
|
|
4526
6992
|
const tools = createToolRegistry({
|
|
4527
6993
|
...createV1ToolHandlers(() => getConnection().db, options.getSessionConnectionState),
|
|
4528
6994
|
...v2Handlers,
|