@aryanduntley/pwa-debug 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2247 @@
1
+ const probeTabScripting = async (tabId) => {
2
+ try {
3
+ await chrome.scripting.executeScript({
4
+ target: { tabId },
5
+ world: 'ISOLATED',
6
+ func: () => '__pwa_debug_probe__',
7
+ });
8
+ return 'scripts_run';
9
+ }
10
+ catch {
11
+ return 'scripts_blocked';
12
+ }
13
+ };
14
+ const RESTRICTED_PROTOCOLS = [
15
+ 'chrome:',
16
+ 'chrome-extension:',
17
+ 'about:',
18
+ 'devtools:',
19
+ 'edge:',
20
+ 'brave:',
21
+ 'view-source:',
22
+ 'file:',
23
+ ];
24
+ const RESTRICTED_HOST_SUFFIXES = [
25
+ 'chromewebstore.google.com',
26
+ 'chrome.google.com',
27
+ ];
28
+ const classifyRestrictedUrl = (url) => {
29
+ if (!url)
30
+ return null;
31
+ let parsed;
32
+ try {
33
+ parsed = new URL(url);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ if (RESTRICTED_PROTOCOLS.includes(parsed.protocol))
39
+ return 'restricted_url';
40
+ if (RESTRICTED_HOST_SUFFIXES.includes(parsed.hostname)) {
41
+ return 'restricted_url';
42
+ }
43
+ return null;
44
+ };
45
+ const classifyDispatchFailure = async (input) => {
46
+ const restricted = classifyRestrictedUrl(input.url);
47
+ if (restricted !== null) {
48
+ return { code: 'restricted_url', message: input.lastErrorMessage };
49
+ }
50
+ const probe = await probeTabScripting(input.tabId);
51
+ if (probe === 'scripts_blocked') {
52
+ return { code: 'page_blocks_scripts', message: input.lastErrorMessage };
53
+ }
54
+ return {
55
+ code: 'cs_not_attached_refresh_tab',
56
+ message: input.lastErrorMessage,
57
+ };
58
+ };
59
+ const CS_BUNDLE_PATH = 'content-script.js';
60
+ const PAGE_WORLD_BUNDLE_PATH = 'page-world.js';
61
+ const selfHealCsAttachment = async (tabId) => {
62
+ try {
63
+ await chrome.scripting.executeScript({
64
+ target: { tabId },
65
+ world: 'ISOLATED',
66
+ files: [CS_BUNDLE_PATH],
67
+ });
68
+ await chrome.scripting.executeScript({
69
+ target: { tabId },
70
+ world: 'MAIN',
71
+ files: [PAGE_WORLD_BUNDLE_PATH],
72
+ });
73
+ return { ok: true };
74
+ }
75
+ catch (err) {
76
+ return { ok: false, reason: err.message };
77
+ }
78
+ };
79
+
80
+ const DEFAULT_TIMEOUT_MS = 4500;
81
+ const SELF_HEAL_SETTLE_MS = 100;
82
+ const dispatchToTab = async (tabId, req, opts = {}) => {
83
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
84
+ let timeoutHandle;
85
+ const timeoutPromise = new Promise((_, reject) => {
86
+ timeoutHandle = setTimeout(() => {
87
+ reject(new Error(`sw-tab-dispatch timeout after ${timeoutMs}ms (tabId=${tabId})`));
88
+ }, timeoutMs);
89
+ });
90
+ try {
91
+ const response = await Promise.race([
92
+ chrome.tabs.sendMessage(tabId, req),
93
+ timeoutPromise,
94
+ ]);
95
+ return response;
96
+ }
97
+ finally {
98
+ if (timeoutHandle !== undefined)
99
+ clearTimeout(timeoutHandle);
100
+ }
101
+ };
102
+ const dispatchToActiveTab = async (req, opts = {}) => {
103
+ const tabs = await chrome.tabs.query({
104
+ active: true,
105
+ lastFocusedWindow: true,
106
+ });
107
+ const tabId = tabs[0]?.id;
108
+ if (tabId === undefined) {
109
+ throw new Error('no active tab');
110
+ }
111
+ return dispatchToTab(tabId, req, opts);
112
+ };
113
+ const getTabUrl = async (tabId) => {
114
+ try {
115
+ const tab = await chrome.tabs.get(tabId);
116
+ return tab?.url;
117
+ }
118
+ catch {
119
+ return undefined;
120
+ }
121
+ };
122
+ const dispatchToTabClassified = async (tabId, req, opts = {}) => {
123
+ let response;
124
+ try {
125
+ response = await dispatchToTab(tabId, req, opts);
126
+ }
127
+ catch (err) {
128
+ const lastErrorMessage = err.message;
129
+ const url = await getTabUrl(tabId);
130
+ const failure = await classifyDispatchFailure({
131
+ tabId,
132
+ url,
133
+ lastErrorMessage,
134
+ });
135
+ if (failure.code !== 'cs_not_attached_refresh_tab') {
136
+ return { ok: false, code: failure.code, message: failure.message };
137
+ }
138
+ const heal = await selfHealCsAttachment(tabId);
139
+ if (!heal.ok) {
140
+ return {
141
+ ok: false,
142
+ code: 'cs_inject_failed',
143
+ message: heal.reason,
144
+ selfHealed: true,
145
+ };
146
+ }
147
+ await new Promise((resolve) => setTimeout(resolve, SELF_HEAL_SETTLE_MS));
148
+ try {
149
+ const retryResponse = await dispatchToTab(tabId, req, opts);
150
+ if (retryResponse.error) {
151
+ return {
152
+ ok: false,
153
+ code: 'page_world_blocked',
154
+ message: retryResponse.error.message,
155
+ selfHealed: true,
156
+ };
157
+ }
158
+ return { ok: true, response: retryResponse, selfHealed: true };
159
+ }
160
+ catch (retryErr) {
161
+ return {
162
+ ok: false,
163
+ code: 'cs_not_attached_refresh_tab',
164
+ message: retryErr.message,
165
+ selfHealed: true,
166
+ };
167
+ }
168
+ }
169
+ if (response.error) {
170
+ return {
171
+ ok: false,
172
+ code: 'page_world_blocked',
173
+ message: response.error.message,
174
+ };
175
+ }
176
+ return { ok: true, response };
177
+ };
178
+
179
+ const compilePatternList = (sources, fieldPathPrefix) => {
180
+ if (sources === undefined || sources.length === 0) {
181
+ return { ok: true, value: [] };
182
+ }
183
+ const out = [];
184
+ let i = 0;
185
+ for (const src of sources) {
186
+ try {
187
+ out.push(new RegExp(src));
188
+ }
189
+ catch (e) {
190
+ return {
191
+ ok: false,
192
+ error: {
193
+ kind: 'pattern_invalid',
194
+ fieldPath: `${fieldPathPrefix}[${i}]`,
195
+ error: e instanceof Error ? e.message : String(e),
196
+ },
197
+ };
198
+ }
199
+ i++;
200
+ }
201
+ return { ok: true, value: out };
202
+ };
203
+ const eventTextForPattern = (event) => {
204
+ try {
205
+ return JSON.stringify(event) ?? '';
206
+ }
207
+ catch {
208
+ return '';
209
+ }
210
+ };
211
+ const compileSourceFilter = (spec) => {
212
+ const includeResult = compilePatternList(spec?.pattern?.include, 'pattern.include');
213
+ if (!includeResult.ok)
214
+ return { ok: false, error: includeResult.error };
215
+ const excludeResult = compilePatternList(spec?.pattern?.exclude, 'pattern.exclude');
216
+ if (!excludeResult.ok)
217
+ return { ok: false, error: excludeResult.error };
218
+ const include = includeResult.value;
219
+ const exclude = excludeResult.value;
220
+ const levelSet = spec?.level !== undefined && spec.level.length > 0
221
+ ? new Set(spec.level)
222
+ : null;
223
+ const predicate = (event) => {
224
+ if (levelSet !== null) {
225
+ const lvl = event.level;
226
+ if (lvl === undefined || !levelSet.has(lvl))
227
+ return false;
228
+ }
229
+ if (include.length === 0 && exclude.length === 0)
230
+ return true;
231
+ const text = eventTextForPattern(event);
232
+ for (const re of exclude) {
233
+ if (re.test(text))
234
+ return false;
235
+ }
236
+ if (include.length > 0) {
237
+ let any = false;
238
+ for (const re of include) {
239
+ if (re.test(text)) {
240
+ any = true;
241
+ break;
242
+ }
243
+ }
244
+ if (!any)
245
+ return false;
246
+ }
247
+ return true;
248
+ };
249
+ return { ok: true, predicate };
250
+ };
251
+
252
+ /**
253
+ * Cross-package settings vocabulary — the single source of truth for every
254
+ * user-tunable setting in pwa-debug.
255
+ *
256
+ * Plug-ability invariant (M7): adding a new setting is exactly ONE line in
257
+ * {@link SettingTypeMap} + ONE entry in {@link SETTINGS_SCHEMA}. The host
258
+ * settings store, the settings.* MCP tools, and the extension settings cache
259
+ * all iterate {@link settingKeys} / {@link getSettingEntry} — no key is ever
260
+ * hardcoded — so a new key needs zero changes to any consumer's shape.
261
+ *
262
+ * Lives in @pwa-debug/shared so the host store and the (T3) extension cache
263
+ * enforce identical key/value shapes at compile time via getSetting<K>.
264
+ */
265
+ /** Runtime tuple of every {@link CaptureKind}, for validation and introspection. */
266
+ const CAPTURE_KINDS = [
267
+ 'console',
268
+ 'network',
269
+ 'dom_mutations',
270
+ 'lifecycle',
271
+ 'store_change',
272
+ 'replay',
273
+ 'library_popup',
274
+ 'page_error',
275
+ 'sw_state',
276
+ ];
277
+ // --- internal primitive guards (not exported; not part of the public surface) ---
278
+ const isNonNegInt = (v) => typeof v === 'number' &&
279
+ Number.isFinite(v) &&
280
+ Number.isInteger(v) &&
281
+ v >= 0;
282
+ const isBoolean = (v) => typeof v === 'boolean';
283
+ /** A valid TCP port for remote debugging: integer in [1, 65535]. */
284
+ const isPort = (v) => typeof v === 'number' &&
285
+ Number.isInteger(v) &&
286
+ v >= 1 &&
287
+ v <= 65535;
288
+ const isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === 'string');
289
+ const isCaptureKindSubset = (v) => Array.isArray(v) &&
290
+ new Set(v).size === v.length &&
291
+ v.every((x) => CAPTURE_KINDS.includes(x));
292
+ const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
293
+ const isReadControlValue = (v) => {
294
+ if (!isPlainObject(v))
295
+ return false;
296
+ const allowed = CAPTURE_KINDS;
297
+ for (const [k, flag] of Object.entries(v)) {
298
+ if (!allowed.includes(k))
299
+ return false;
300
+ if (typeof flag !== 'boolean')
301
+ return false;
302
+ }
303
+ return true;
304
+ };
305
+ const isReadControlsRecord = (v) => {
306
+ if (!isPlainObject(v))
307
+ return false;
308
+ for (const value of Object.values(v)) {
309
+ if (!isReadControlValue(value))
310
+ return false;
311
+ }
312
+ return true;
313
+ };
314
+ const CONSOLE_LEVELS = [
315
+ 'log',
316
+ 'info',
317
+ 'warn',
318
+ 'error',
319
+ 'debug',
320
+ 'trace',
321
+ ];
322
+ const isFilterPattern = (v) => {
323
+ if (!isPlainObject(v))
324
+ return false;
325
+ for (const [k, val] of Object.entries(v)) {
326
+ if (k !== 'include' && k !== 'exclude')
327
+ return false;
328
+ if (val === undefined)
329
+ continue;
330
+ if (!Array.isArray(val))
331
+ return false;
332
+ if (!val.every((x) => typeof x === 'string'))
333
+ return false;
334
+ }
335
+ return true;
336
+ };
337
+ const FILTER_SPEC_SOURCE_KEYS = ['level', 'pattern'];
338
+ const isSourceFilterSpec = (v) => {
339
+ if (!isPlainObject(v))
340
+ return false;
341
+ for (const [k, val] of Object.entries(v)) {
342
+ if (!FILTER_SPEC_SOURCE_KEYS.includes(k))
343
+ return false;
344
+ if (val === undefined)
345
+ continue;
346
+ if (k === 'level') {
347
+ if (!Array.isArray(val))
348
+ return false;
349
+ if (!val.every((x) => typeof x === 'string' && CONSOLE_LEVELS.includes(x)))
350
+ return false;
351
+ }
352
+ else if (k === 'pattern') {
353
+ if (!isFilterPattern(val))
354
+ return false;
355
+ }
356
+ }
357
+ return true;
358
+ };
359
+ const isCaptureFiltersRecord = (v) => {
360
+ if (!isPlainObject(v))
361
+ return false;
362
+ const allowed = CAPTURE_KINDS;
363
+ for (const [k, val] of Object.entries(v)) {
364
+ if (!allowed.includes(k))
365
+ return false;
366
+ if (val === undefined)
367
+ continue;
368
+ if (!isSourceFilterSpec(val))
369
+ return false;
370
+ }
371
+ return true;
372
+ };
373
+ /**
374
+ * The schema as data. Frozen. THIS is the single const instance; every
375
+ * consumer reaches it through {@link settingKeys} / {@link getSettingEntry}.
376
+ */
377
+ const SETTINGS_SCHEMA = Object.freeze({
378
+ 'capture.memoryCutoffPerKind': {
379
+ key: 'capture.memoryCutoffPerKind',
380
+ type: 'number',
381
+ default: 5000,
382
+ scope: 'host',
383
+ description: 'Max events retained in memory per capture kind before eviction (overflow goes to disk when capture.diskSpill.enabled).',
384
+ validate: isNonNegInt,
385
+ },
386
+ 'capture.diskSpill.enabled': {
387
+ key: 'capture.diskSpill.enabled',
388
+ type: 'boolean',
389
+ default: false,
390
+ scope: 'host',
391
+ description: 'When true, events evicted from the in-memory ring buffer are written to on-disk jsonl archives instead of dropped.',
392
+ validate: isBoolean,
393
+ },
394
+ 'capture.diskSpill.archiveLongevityDays': {
395
+ key: 'capture.diskSpill.archiveLongevityDays',
396
+ type: 'number',
397
+ default: 7,
398
+ scope: 'host',
399
+ description: 'Age in days after which a disk archive file is pruned on the next pruner tick.',
400
+ validate: isNonNegInt,
401
+ },
402
+ 'capture.diskSpill.maxBytes': {
403
+ key: 'capture.diskSpill.maxBytes',
404
+ type: 'number',
405
+ default: 100_000_000,
406
+ scope: 'host',
407
+ description: 'Total disk-archive byte cap; oldest archive files are evicted first when exceeded.',
408
+ validate: isNonNegInt,
409
+ },
410
+ 'sites.allowlist': {
411
+ key: 'sites.allowlist',
412
+ type: 'string[]',
413
+ default: ['*'],
414
+ scope: 'both',
415
+ description: 'Glob patterns of origins/URLs the capture pipeline is permitted to record. Default ["*"] = all sites.',
416
+ validate: isStringArray,
417
+ },
418
+ 'sites.blocklist': {
419
+ key: 'sites.blocklist',
420
+ type: 'string[]',
421
+ default: [],
422
+ scope: 'both',
423
+ description: 'Glob patterns of origins/URLs never captured; takes precedence over sites.allowlist.',
424
+ validate: isStringArray,
425
+ },
426
+ 'capture.enabledKinds': {
427
+ key: 'capture.enabledKinds',
428
+ type: 'enum[]',
429
+ default: [
430
+ 'console',
431
+ 'network',
432
+ 'dom_mutations',
433
+ 'lifecycle',
434
+ 'store_change',
435
+ 'replay',
436
+ 'library_popup',
437
+ 'sw_state',
438
+ ],
439
+ scope: 'both',
440
+ description: 'Subset of capture kinds actively recorded. Empty = capture nothing.',
441
+ validate: isCaptureKindSubset,
442
+ enumValues: CAPTURE_KINDS,
443
+ },
444
+ 'sites.readControls': {
445
+ key: 'sites.readControls',
446
+ type: 'record',
447
+ default: Object.freeze({}),
448
+ scope: 'both',
449
+ description: 'Per-site, per-kind read-permission overrides. Keys are glob patterns (same matcher as sites.allowlist); values are objects of CaptureKind→boolean flags. Missing flag = allowed; false denies that kind for matching URLs. Most-specific (longest) pattern wins per URL; ties broken lexicographically. Only DENIES events otherwise allowed by sites.allowlist + capture.enabledKinds — cannot re-enable what allowlist already rejected.',
450
+ validate: isReadControlsRecord,
451
+ },
452
+ 'capture.filters': {
453
+ key: 'capture.filters',
454
+ type: 'record',
455
+ default: Object.freeze({}),
456
+ scope: 'both',
457
+ description: 'Per-kind source-side capture filters. Keys are CaptureKinds; values are wire FilterSpecs (level + pattern only — cursors and limit are seq-based, meaningful only on the host). When set, the capture chokepoint applies the compiled predicate BEFORE the event reaches the host buffer; rejected events are dropped at the source. Validation tightens the wire shape to source-applicable fields only.',
458
+ validate: isCaptureFiltersRecord,
459
+ },
460
+ 'capture.stores.allowDispatch': {
461
+ key: 'capture.stores.allowDispatch',
462
+ type: 'boolean',
463
+ default: false,
464
+ scope: 'both',
465
+ description: 'When true, redux_dispatch (and forthcoming store-system dispatch tools) may write to the page-world store; when false (default), the dispatch tool rejects with an actionable next_steps[] hint. Gates the only write surface in the store-introspection family — reads (redux_get_state, redux_subscribe, redux_tail) are unaffected.',
466
+ validate: isBoolean,
467
+ },
468
+ 'capture.sourceMap.enabled': {
469
+ key: 'capture.sourceMap.enabled',
470
+ type: 'boolean',
471
+ default: true,
472
+ scope: 'both',
473
+ description: 'When true (default), source_map_resolve fetches and resolves source maps to translate generated stack frames into original-source coordinates. When false, the tool returns errorResponse with a hint. M13 ships query-time resolution only; capture-time auto-annotation is deferred to M13.5.',
474
+ validate: isBoolean,
475
+ },
476
+ 'launch.defaultPort': {
477
+ key: 'launch.defaultPort',
478
+ type: 'number',
479
+ default: 9222,
480
+ scope: 'host',
481
+ description: 'Default remote-debugging port used by pdl_launch_browser when no explicit `port` arg is given. 9222 is the chrome-devtools-mcp convention. Change it if 9222 is already in use on your machine.',
482
+ validate: isPort,
483
+ },
484
+ });
485
+ /**
486
+ * All setting keys in stable schema-declaration order — the canonical
487
+ * iteration order for defaults-merge and settings.list_schema.
488
+ */
489
+ const settingKeys = () => Object.keys(SETTINGS_SCHEMA);
490
+ /**
491
+ * Typed accessor for a single schema entry — the one DRY lookup point so a
492
+ * future key change is a single schema edit, never a consumer change.
493
+ */
494
+ const getSettingEntry = (key) => SETTINGS_SCHEMA[key];
495
+ /**
496
+ * Central pure type-guard: validate an unknown value against a key's schema
497
+ * validator. Single validation path shared by the host_settings store and the
498
+ * settings.set MCP tool. Narrows `value` to SettingTypeMap[K] on true.
499
+ */
500
+ const validateSettingValue = (key, value) => getSettingEntry(key).validate(value);
501
+ /**
502
+ * Factory producing a fresh fully-materialized {@link SettingsRecord} of every
503
+ * key's default. Array and plain-object defaults are cloned so the result
504
+ * never aliases the frozen SETTINGS_SCHEMA. The base the host_settings store
505
+ * merges over.
506
+ */
507
+ const defaultSettings = () => Object.fromEntries(settingKeys().map((k) => {
508
+ const d = getSettingEntry(k).default;
509
+ if (Array.isArray(d))
510
+ return [k, [...d]];
511
+ if (isPlainObject(d))
512
+ return [k, { ...d }];
513
+ return [k, d];
514
+ }));
515
+
516
+ // Single source of truth for the Path 7 pdl_* interaction action tools.
517
+ //
518
+ // Both the host (builds a ToolDef + Zod schema per entry) and the extension
519
+ // (SW request routing + page-world dispatch) import this table, so tool names,
520
+ // their dom_actions action kind, and their parameters stay in sync across
521
+ // packages. Adding a tool = one entry here + (for new param shapes) the typed
522
+ // param model below; the three generic layers need no per-tool code.
523
+ const S = (key, required = false, description) => ({
524
+ key,
525
+ type: 'string',
526
+ ...(required ? { required } : {}),
527
+ ...(description !== undefined ? { description } : {}),
528
+ });
529
+ const N = (key, required = false, description) => ({
530
+ key,
531
+ type: 'number',
532
+ ...(required ? { required } : {}),
533
+ ...(description !== undefined ? { description } : {}),
534
+ });
535
+ const B = (key, description) => ({
536
+ key,
537
+ type: 'boolean',
538
+ ...(description !== undefined ? { description } : {}),
539
+ });
540
+ const ACTION_TOOL_SPECS = Object.freeze([
541
+ // --- discrete (pointer/keyboard) ---
542
+ { tool: 'pdl_click', action: 'click', params: [], summary: 'Click an element (full pointer/mouse event chain so React/Vue delegated onClick fires).' },
543
+ { tool: 'pdl_dblclick', action: 'dblclick', params: [], summary: 'Double-click an element.' },
544
+ { tool: 'pdl_fill', action: 'fill', params: [S('value', true, 'text to set')], summary: "Set an input/textarea/select value via the native setter + input/change (works with React controlled inputs)." },
545
+ { tool: 'pdl_submit', action: 'submit', params: [], summary: 'Submit the form owning the located element (requestSubmit).' },
546
+ { tool: 'pdl_hover', action: 'hover', params: [], summary: 'Hover an element (pointer/mouse over/enter/move).' },
547
+ { tool: 'pdl_focus', action: 'focus', params: [], summary: 'Focus an element.' },
548
+ { tool: 'pdl_blur', action: 'blur', params: [], summary: 'Blur an element.' },
549
+ { tool: 'pdl_check', action: 'check', params: [], summary: 'Check a checkbox/radio (idempotent; native click path so onChange fires).' },
550
+ { tool: 'pdl_uncheck', action: 'uncheck', params: [], summary: 'Uncheck a checkbox (idempotent).' },
551
+ { tool: 'pdl_select_option', action: 'selectOption', params: [S('value', false, 'option value'), S('label', false, 'visible option label')], summary: 'Select a <select> option by value or visible label (one required).' },
552
+ { tool: 'pdl_key_press', action: 'keyPress', params: [S('key', true, "a character or named key e.g. 'Enter','Tab','ArrowDown'")], summary: 'Press a single key on an element.' },
553
+ { tool: 'pdl_type_sequence', action: 'typeSequence', params: [S('value', true, 'string to type char-by-char')], summary: 'Type a string into an editable element char-by-char.' },
554
+ // --- gestures (pointer/touch) ---
555
+ { tool: 'pdl_drag', action: 'drag', params: [N('toX', false, 'destination viewport X'), N('toY', false, 'destination viewport Y'), S('targetSelector', false, 'CSS selector for the drop target (alternative to toX/toY)'), N('steps', false, 'pointermove steps (default 10)'), B('html5', 'also fire the native HTML5 drag/drop sequence with a DataTransfer')], summary: 'Drag the located element to a point (toX/toY) or onto targetSelector; pointer drag + optional HTML5 DnD.' },
556
+ { tool: 'pdl_scroll', action: 'scroll', params: [N('deltaX', false, 'horizontal scroll delta'), N('deltaY', false, 'vertical scroll delta'), B('intoView', 'scrollIntoView (centered) instead of by-delta')], summary: 'Scroll the located element by delta (dispatches wheel + scrollBy) or scrollIntoView.' },
557
+ { tool: 'pdl_swipe', action: 'swipe', params: [{ key: 'direction', type: 'enum', required: true, enum: ['up', 'down', 'left', 'right'], description: 'swipe direction' }, N('distance', false, 'px distance (default 100)'), N('steps', false, 'touchmove steps (default 10)')], summary: 'Swipe a touch across the located element in a direction.' },
558
+ { tool: 'pdl_tap', action: 'tap', params: [], summary: 'Tap (touchstart/touchend) the located element.' },
559
+ { tool: 'pdl_double_tap', action: 'doubleTap', params: [], summary: 'Double-tap the located element.' },
560
+ { tool: 'pdl_long_press', action: 'longPress', params: [N('duration', false, 'hold ms before release (default 500)')], summary: 'Long-press the located element (holds, then releases + contextmenu).' },
561
+ { tool: 'pdl_pinch', action: 'pinch', params: [N('scale', true, 'target scale: >1 zoom in, <1 zoom out'), N('steps', false, 'touchmove steps (default 10)')], summary: 'Pinch-zoom on the located element with two touches.' },
562
+ ]);
563
+
564
+ const isSwRequestEnvelope = (m) => {
565
+ if (m === null || typeof m !== 'object')
566
+ return false;
567
+ const r = m;
568
+ return (r['type'] === 'request' &&
569
+ typeof r['requestId'] === 'string' &&
570
+ typeof r['tool'] === 'string');
571
+ };
572
+ const fetchPageWorld = async (tabId) => {
573
+ const result = await dispatchToTabClassified(tabId, { tool: 'session_ping' });
574
+ if (result.ok) {
575
+ const payload = result.response.payload;
576
+ return {
577
+ pageWorld: payload ?? null,
578
+ ...(result.selfHealed ? { pageWorldSelfHealed: true } : {}),
579
+ };
580
+ }
581
+ return {
582
+ pageWorld: null,
583
+ pageWorldError: result.code,
584
+ pageWorldErrorMessage: result.message,
585
+ ...(result.selfHealed ? { pageWorldSelfHealed: true } : {}),
586
+ };
587
+ };
588
+ const handleSessionPing = async () => {
589
+ const tabs = await chrome.tabs.query({
590
+ active: true,
591
+ lastFocusedWindow: true,
592
+ });
593
+ const attachedTabId = tabs[0]?.id ?? null;
594
+ const extensionVersion = chrome.runtime.getManifest().version;
595
+ const pageWorldResult = attachedTabId !== null
596
+ ? await fetchPageWorld(attachedTabId)
597
+ : {
598
+ pageWorld: null,
599
+ pageWorldError: 'no_active_tab',
600
+ pageWorldErrorMessage: 'no active tab',
601
+ };
602
+ const result = {
603
+ extensionVersion,
604
+ attachedTabId,
605
+ pageWorld: pageWorldResult.pageWorld,
606
+ ...(pageWorldResult.pageWorldError !== undefined
607
+ ? { pageWorldError: pageWorldResult.pageWorldError }
608
+ : {}),
609
+ ...(pageWorldResult.pageWorldErrorMessage !== undefined
610
+ ? { pageWorldErrorMessage: pageWorldResult.pageWorldErrorMessage }
611
+ : {}),
612
+ ...(pageWorldResult.pageWorldSelfHealed
613
+ ? { pageWorldSelfHealed: true }
614
+ : {}),
615
+ };
616
+ return result;
617
+ };
618
+ const sanitizeRecentFilter = (raw) => {
619
+ if (raw === null || typeof raw !== 'object')
620
+ return {};
621
+ const r = raw;
622
+ const kinds = Array.isArray(r['kinds'])
623
+ ? r['kinds'].filter((k) => typeof k === 'string')
624
+ : undefined;
625
+ const sinceMs = typeof r['sinceMs'] === 'number' ? r['sinceMs'] : undefined;
626
+ const limit = typeof r['limit'] === 'number' ? r['limit'] : undefined;
627
+ return {
628
+ ...(kinds !== undefined ? { kinds } : {}),
629
+ ...(sinceMs !== undefined ? { sinceMs } : {}),
630
+ ...(limit !== undefined ? { limit } : {}),
631
+ };
632
+ };
633
+ const handleRecentEvents = async (env, ctx) => {
634
+ const filter = sanitizeRecentFilter(env.payload);
635
+ const result = ctx.sink.getRecent(filter);
636
+ return result;
637
+ };
638
+ const sanitizeEvaluateInput = (raw) => {
639
+ if (raw === null || typeof raw !== 'object')
640
+ return null;
641
+ const r = raw;
642
+ const expression = r['expression'];
643
+ if (typeof expression !== 'string' || expression.length === 0)
644
+ return null;
645
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
646
+ ? r['tab_id']
647
+ : undefined;
648
+ return {
649
+ tabId,
650
+ payload: {
651
+ expression,
652
+ ...(typeof r['timeout_ms'] === 'number' && r['timeout_ms'] > 0
653
+ ? { timeout_ms: r['timeout_ms'] }
654
+ : {}),
655
+ ...(typeof r['await_promise'] === 'boolean'
656
+ ? { await_promise: r['await_promise'] }
657
+ : {}),
658
+ },
659
+ };
660
+ };
661
+ const handleEvaluate = async (env) => {
662
+ const sanitized = sanitizeEvaluateInput(env.payload);
663
+ if (sanitized === null) {
664
+ throw new Error('evaluate: payload must be { expression: non-empty string, tab_id?, timeout_ms?, await_promise? }');
665
+ }
666
+ const csReq = { tool: 'evaluate', payload: sanitized.payload };
667
+ const response = sanitized.tabId !== undefined
668
+ ? await dispatchToTab(sanitized.tabId, csReq)
669
+ : await dispatchToActiveTab(csReq);
670
+ if (response.error) {
671
+ throw new Error(response.error.message);
672
+ }
673
+ return response.payload;
674
+ };
675
+ const sanitizeReactTreeInput = (raw) => {
676
+ if (raw === undefined || raw === null) {
677
+ return { tabId: undefined, payload: {} };
678
+ }
679
+ if (typeof raw !== 'object')
680
+ return null;
681
+ const r = raw;
682
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
683
+ ? r['tab_id']
684
+ : undefined;
685
+ const payload = {};
686
+ if (typeof r['root_index'] === 'number' &&
687
+ Number.isInteger(r['root_index']) &&
688
+ r['root_index'] >= 0) {
689
+ payload['root_index'] = r['root_index'];
690
+ }
691
+ if (typeof r['depth_limit'] === 'number' &&
692
+ Number.isInteger(r['depth_limit']) &&
693
+ r['depth_limit'] > 0) {
694
+ payload['depth_limit'] = r['depth_limit'];
695
+ }
696
+ if (typeof r['max_nodes'] === 'number' &&
697
+ Number.isInteger(r['max_nodes']) &&
698
+ r['max_nodes'] > 0) {
699
+ payload['max_nodes'] = r['max_nodes'];
700
+ }
701
+ return { tabId, payload };
702
+ };
703
+ const handleReactTree = async (env) => {
704
+ const sanitized = sanitizeReactTreeInput(env.payload);
705
+ if (sanitized === null) {
706
+ throw new Error('react_tree: payload must be an object with optional { tab_id?, root_index?, depth_limit?, max_nodes? }');
707
+ }
708
+ const csReq = { tool: 'react_tree', payload: sanitized.payload };
709
+ const response = sanitized.tabId !== undefined
710
+ ? await dispatchToTab(sanitized.tabId, csReq)
711
+ : await dispatchToActiveTab(csReq);
712
+ if (response.error) {
713
+ throw new Error(response.error.message);
714
+ }
715
+ return response.payload;
716
+ };
717
+ const sanitizeReactGetStateInput = (raw) => {
718
+ if (raw === null || typeof raw !== 'object')
719
+ return null;
720
+ const r = raw;
721
+ const stableId = r['stable_id'];
722
+ if (typeof stableId !== 'string' || stableId.length === 0)
723
+ return null;
724
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
725
+ ? r['tab_id']
726
+ : undefined;
727
+ const payload = { stable_id: stableId };
728
+ if (typeof r['root_index'] === 'number' &&
729
+ Number.isInteger(r['root_index']) &&
730
+ r['root_index'] >= 0) {
731
+ payload['root_index'] = r['root_index'];
732
+ }
733
+ if (typeof r['include_props'] === 'boolean')
734
+ payload['include_props'] = r['include_props'];
735
+ if (typeof r['include_hooks'] === 'boolean')
736
+ payload['include_hooks'] = r['include_hooks'];
737
+ return { tabId, payload };
738
+ };
739
+ const handleReactGetState = async (env) => {
740
+ const sanitized = sanitizeReactGetStateInput(env.payload);
741
+ if (sanitized === null) {
742
+ throw new Error('react_get_state: payload must be { stable_id: non-empty string, tab_id?, root_index?, include_props?, include_hooks? }');
743
+ }
744
+ const csReq = { tool: 'react_get_state', payload: sanitized.payload };
745
+ const response = sanitized.tabId !== undefined
746
+ ? await dispatchToTab(sanitized.tabId, csReq)
747
+ : await dispatchToActiveTab(csReq);
748
+ if (response.error) {
749
+ throw new Error(response.error.message);
750
+ }
751
+ return response.payload;
752
+ };
753
+ const sanitizeReactFindByTextInput = (raw) => {
754
+ if (raw === null || typeof raw !== 'object')
755
+ return null;
756
+ const r = raw;
757
+ const pattern = r['pattern'];
758
+ if (typeof pattern !== 'string' || pattern.length === 0)
759
+ return null;
760
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
761
+ ? r['tab_id']
762
+ : undefined;
763
+ const payload = { pattern };
764
+ if (typeof r['exact'] === 'boolean')
765
+ payload['exact'] = r['exact'];
766
+ if (typeof r['root_index'] === 'number' &&
767
+ Number.isInteger(r['root_index']) &&
768
+ r['root_index'] >= 0) {
769
+ payload['root_index'] = r['root_index'];
770
+ }
771
+ if (typeof r['max_matches'] === 'number' &&
772
+ Number.isInteger(r['max_matches']) &&
773
+ r['max_matches'] > 0) {
774
+ payload['max_matches'] = r['max_matches'];
775
+ }
776
+ return { tabId, payload };
777
+ };
778
+ const handleReactFindByText = async (env) => {
779
+ const sanitized = sanitizeReactFindByTextInput(env.payload);
780
+ if (sanitized === null) {
781
+ throw new Error('react_find_by_text: payload must be { pattern: non-empty string, tab_id?, exact?, root_index?, max_matches? }');
782
+ }
783
+ const csReq = { tool: 'react_find_by_text', payload: sanitized.payload };
784
+ const response = sanitized.tabId !== undefined
785
+ ? await dispatchToTab(sanitized.tabId, csReq)
786
+ : await dispatchToActiveTab(csReq);
787
+ if (response.error) {
788
+ throw new Error(response.error.message);
789
+ }
790
+ return response.payload;
791
+ };
792
+ const sanitizeReactFindByRoleInput = (raw) => {
793
+ if (raw === null || typeof raw !== 'object')
794
+ return null;
795
+ const r = raw;
796
+ const role = r['role'];
797
+ if (typeof role !== 'string' || role.length === 0)
798
+ return null;
799
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
800
+ ? r['tab_id']
801
+ : undefined;
802
+ const payload = { role };
803
+ if (typeof r['name'] === 'string' && r['name'].length > 0) {
804
+ payload['name'] = r['name'];
805
+ }
806
+ if (typeof r['root_index'] === 'number' &&
807
+ Number.isInteger(r['root_index']) &&
808
+ r['root_index'] >= 0) {
809
+ payload['root_index'] = r['root_index'];
810
+ }
811
+ if (typeof r['max_matches'] === 'number' &&
812
+ Number.isInteger(r['max_matches']) &&
813
+ r['max_matches'] > 0) {
814
+ payload['max_matches'] = r['max_matches'];
815
+ }
816
+ return { tabId, payload };
817
+ };
818
+ const handleReactFindByRole = async (env) => {
819
+ const sanitized = sanitizeReactFindByRoleInput(env.payload);
820
+ if (sanitized === null) {
821
+ throw new Error('react_find_by_role: payload must be { role: non-empty string, tab_id?, name?, root_index?, max_matches? }');
822
+ }
823
+ const csReq = { tool: 'react_find_by_role', payload: sanitized.payload };
824
+ const response = sanitized.tabId !== undefined
825
+ ? await dispatchToTab(sanitized.tabId, csReq)
826
+ : await dispatchToActiveTab(csReq);
827
+ if (response.error) {
828
+ throw new Error(response.error.message);
829
+ }
830
+ return response.payload;
831
+ };
832
+ const sanitizeVueTreeInput = (raw) => {
833
+ if (raw === undefined || raw === null) {
834
+ return { tabId: undefined, payload: {} };
835
+ }
836
+ if (typeof raw !== 'object')
837
+ return null;
838
+ const r = raw;
839
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
840
+ ? r['tab_id']
841
+ : undefined;
842
+ const payload = {};
843
+ if (typeof r['root_index'] === 'number' &&
844
+ Number.isInteger(r['root_index']) &&
845
+ r['root_index'] >= 0) {
846
+ payload['root_index'] = r['root_index'];
847
+ }
848
+ if (typeof r['depth_limit'] === 'number' &&
849
+ Number.isInteger(r['depth_limit']) &&
850
+ r['depth_limit'] > 0) {
851
+ payload['depth_limit'] = r['depth_limit'];
852
+ }
853
+ if (typeof r['max_nodes'] === 'number' &&
854
+ Number.isInteger(r['max_nodes']) &&
855
+ r['max_nodes'] > 0) {
856
+ payload['max_nodes'] = r['max_nodes'];
857
+ }
858
+ return { tabId, payload };
859
+ };
860
+ const handleVueTree = async (env) => {
861
+ const sanitized = sanitizeVueTreeInput(env.payload);
862
+ if (sanitized === null) {
863
+ throw new Error('vue_tree: payload must be an object with optional { tab_id?, root_index?, depth_limit?, max_nodes? }');
864
+ }
865
+ const csReq = { tool: 'vue_tree', payload: sanitized.payload };
866
+ const response = sanitized.tabId !== undefined
867
+ ? await dispatchToTab(sanitized.tabId, csReq)
868
+ : await dispatchToActiveTab(csReq);
869
+ if (response.error) {
870
+ throw new Error(response.error.message);
871
+ }
872
+ return response.payload;
873
+ };
874
+ const sanitizeVueGetStateInput = (raw) => {
875
+ if (raw === null || typeof raw !== 'object')
876
+ return null;
877
+ const r = raw;
878
+ const stableId = r['stable_id'];
879
+ if (typeof stableId !== 'string' || stableId.length === 0)
880
+ return null;
881
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
882
+ ? r['tab_id']
883
+ : undefined;
884
+ const payload = { stable_id: stableId };
885
+ if (typeof r['include_props'] === 'boolean')
886
+ payload['include_props'] = r['include_props'];
887
+ if (typeof r['include_state'] === 'boolean')
888
+ payload['include_state'] = r['include_state'];
889
+ return { tabId, payload };
890
+ };
891
+ const handleVueGetState = async (env) => {
892
+ const sanitized = sanitizeVueGetStateInput(env.payload);
893
+ if (sanitized === null) {
894
+ throw new Error('vue_get_state: payload must be { stable_id: non-empty string, tab_id?, include_props?, include_state? }');
895
+ }
896
+ const csReq = { tool: 'vue_get_state', payload: sanitized.payload };
897
+ const response = sanitized.tabId !== undefined
898
+ ? await dispatchToTab(sanitized.tabId, csReq)
899
+ : await dispatchToActiveTab(csReq);
900
+ if (response.error) {
901
+ throw new Error(response.error.message);
902
+ }
903
+ return response.payload;
904
+ };
905
+ const sanitizeVueFindByTextInput = (raw) => {
906
+ if (raw === null || typeof raw !== 'object')
907
+ return null;
908
+ const r = raw;
909
+ const pattern = r['pattern'];
910
+ if (typeof pattern !== 'string' || pattern.length === 0)
911
+ return null;
912
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
913
+ ? r['tab_id']
914
+ : undefined;
915
+ const payload = { pattern };
916
+ if (typeof r['exact'] === 'boolean')
917
+ payload['exact'] = r['exact'];
918
+ if (typeof r['root_index'] === 'number' &&
919
+ Number.isInteger(r['root_index']) &&
920
+ r['root_index'] >= 0) {
921
+ payload['root_index'] = r['root_index'];
922
+ }
923
+ if (typeof r['max_matches'] === 'number' &&
924
+ Number.isInteger(r['max_matches']) &&
925
+ r['max_matches'] > 0) {
926
+ payload['max_matches'] = r['max_matches'];
927
+ }
928
+ return { tabId, payload };
929
+ };
930
+ const handleVueFindByText = async (env) => {
931
+ const sanitized = sanitizeVueFindByTextInput(env.payload);
932
+ if (sanitized === null) {
933
+ throw new Error('vue_find_by_text: payload must be { pattern: non-empty string, tab_id?, exact?, root_index?, max_matches? }');
934
+ }
935
+ const csReq = { tool: 'vue_find_by_text', payload: sanitized.payload };
936
+ const response = sanitized.tabId !== undefined
937
+ ? await dispatchToTab(sanitized.tabId, csReq)
938
+ : await dispatchToActiveTab(csReq);
939
+ if (response.error) {
940
+ throw new Error(response.error.message);
941
+ }
942
+ return response.payload;
943
+ };
944
+ const sanitizeVueFindByRoleInput = (raw) => {
945
+ if (raw === null || typeof raw !== 'object')
946
+ return null;
947
+ const r = raw;
948
+ const role = r['role'];
949
+ if (typeof role !== 'string' || role.length === 0)
950
+ return null;
951
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
952
+ ? r['tab_id']
953
+ : undefined;
954
+ const payload = { role };
955
+ if (typeof r['name'] === 'string' && r['name'].length > 0) {
956
+ payload['name'] = r['name'];
957
+ }
958
+ if (typeof r['root_index'] === 'number' &&
959
+ Number.isInteger(r['root_index']) &&
960
+ r['root_index'] >= 0) {
961
+ payload['root_index'] = r['root_index'];
962
+ }
963
+ if (typeof r['max_matches'] === 'number' &&
964
+ Number.isInteger(r['max_matches']) &&
965
+ r['max_matches'] > 0) {
966
+ payload['max_matches'] = r['max_matches'];
967
+ }
968
+ return { tabId, payload };
969
+ };
970
+ const handleVueFindByRole = async (env) => {
971
+ const sanitized = sanitizeVueFindByRoleInput(env.payload);
972
+ if (sanitized === null) {
973
+ throw new Error('vue_find_by_role: payload must be { role: non-empty string, tab_id?, name?, root_index?, max_matches? }');
974
+ }
975
+ const csReq = { tool: 'vue_find_by_role', payload: sanitized.payload };
976
+ const response = sanitized.tabId !== undefined
977
+ ? await dispatchToTab(sanitized.tabId, csReq)
978
+ : await dispatchToActiveTab(csReq);
979
+ if (response.error) {
980
+ throw new Error(response.error.message);
981
+ }
982
+ return response.payload;
983
+ };
984
+ // ── Svelte introspection (Path 5 M42) ───────────────────────────────────────
985
+ const handleSvelteComponents = async (env) => {
986
+ const raw = env.payload;
987
+ const tabId = raw !== null &&
988
+ typeof raw === 'object' &&
989
+ typeof raw['tab_id'] === 'number' &&
990
+ Number.isFinite(raw['tab_id'])
991
+ ? raw['tab_id']
992
+ : undefined;
993
+ const csReq = { tool: 'svelte_components', payload: {} };
994
+ const response = tabId !== undefined
995
+ ? await dispatchToTab(tabId, csReq)
996
+ : await dispatchToActiveTab(csReq);
997
+ if (response.error) {
998
+ throw new Error(response.error.message);
999
+ }
1000
+ return response.payload;
1001
+ };
1002
+ // sw_status (PWA Runtime Diagnostics): forward to the active (or given) tab's
1003
+ // page-world, which reads navigator.serviceWorker. No params beyond tab_id —
1004
+ // same shape as svelte_components.
1005
+ const handleSwStatus = async (env) => {
1006
+ const raw = env.payload;
1007
+ const tabId = raw !== null &&
1008
+ typeof raw === 'object' &&
1009
+ typeof raw['tab_id'] === 'number' &&
1010
+ Number.isFinite(raw['tab_id'])
1011
+ ? raw['tab_id']
1012
+ : undefined;
1013
+ const csReq = { tool: 'sw_status', payload: {} };
1014
+ const response = tabId !== undefined
1015
+ ? await dispatchToTab(tabId, csReq)
1016
+ : await dispatchToActiveTab(csReq);
1017
+ if (response.error) {
1018
+ throw new Error(response.error.message);
1019
+ }
1020
+ return response.payload;
1021
+ };
1022
+ // cache_* (PWA Runtime Diagnostics): forward to the active/given tab's page-world.
1023
+ const readTabId = (raw) => raw !== null &&
1024
+ typeof raw === 'object' &&
1025
+ typeof raw['tab_id'] === 'number' &&
1026
+ Number.isFinite(raw['tab_id'])
1027
+ ? raw['tab_id']
1028
+ : undefined;
1029
+ const handleCacheList = async (env) => {
1030
+ const tabId = readTabId(env.payload);
1031
+ const csReq = { tool: 'cache_list', payload: {} };
1032
+ const response = tabId !== undefined
1033
+ ? await dispatchToTab(tabId, csReq)
1034
+ : await dispatchToActiveTab(csReq);
1035
+ if (response.error)
1036
+ throw new Error(response.error.message);
1037
+ return response.payload;
1038
+ };
1039
+ const handlePwaStatus = async (env) => {
1040
+ const tabId = readTabId(env.payload);
1041
+ const csReq = { tool: 'pwa_status', payload: {} };
1042
+ const response = tabId !== undefined
1043
+ ? await dispatchToTab(tabId, csReq)
1044
+ : await dispatchToActiveTab(csReq);
1045
+ if (response.error)
1046
+ throw new Error(response.error.message);
1047
+ return response.payload;
1048
+ };
1049
+ const handlePwaInstallability = async (env) => {
1050
+ const tabId = readTabId(env.payload);
1051
+ const csReq = { tool: 'pwa_installability', payload: {} };
1052
+ const response = tabId !== undefined
1053
+ ? await dispatchToTab(tabId, csReq)
1054
+ : await dispatchToActiveTab(csReq);
1055
+ if (response.error)
1056
+ throw new Error(response.error.message);
1057
+ return response.payload;
1058
+ };
1059
+ // storage_get (PWA Runtime Diagnostics T2): forward area + limit to the page-world.
1060
+ const handleStorageGet = async (env) => {
1061
+ const r = env.payload !== null && typeof env.payload === 'object'
1062
+ ? env.payload
1063
+ : {};
1064
+ const payload = {};
1065
+ if (r['area'] === 'session' || r['area'] === 'local')
1066
+ payload['area'] = r['area'];
1067
+ if (typeof r['limit'] === 'number' &&
1068
+ Number.isInteger(r['limit']) &&
1069
+ r['limit'] > 0) {
1070
+ payload['limit'] = r['limit'];
1071
+ }
1072
+ const tabId = readTabId(env.payload);
1073
+ const csReq = { tool: 'storage_get', payload };
1074
+ const response = tabId !== undefined
1075
+ ? await dispatchToTab(tabId, csReq)
1076
+ : await dispatchToActiveTab(csReq);
1077
+ if (response.error)
1078
+ throw new Error(response.error.message);
1079
+ return response.payload;
1080
+ };
1081
+ // idb_list (PWA Runtime Diagnostics T2): forward to the active/given tab's
1082
+ // page-world, which reads indexedDB. No params beyond tab_id.
1083
+ const handleIdbList = async (env) => {
1084
+ const tabId = readTabId(env.payload);
1085
+ const csReq = { tool: 'idb_list', payload: {} };
1086
+ const response = tabId !== undefined
1087
+ ? await dispatchToTab(tabId, csReq)
1088
+ : await dispatchToActiveTab(csReq);
1089
+ if (response.error)
1090
+ throw new Error(response.error.message);
1091
+ return response.payload;
1092
+ };
1093
+ // idb_query (PWA Runtime Diagnostics T2): forward db + store + limit to the
1094
+ // page-world over a read-only IndexedDB transaction.
1095
+ const handleIdbQuery = async (env) => {
1096
+ const r = env.payload !== null && typeof env.payload === 'object'
1097
+ ? env.payload
1098
+ : {};
1099
+ const db = r['db'];
1100
+ if (typeof db !== 'string' || db.length === 0) {
1101
+ throw new Error('idb_query: payload must include { db: non-empty string }');
1102
+ }
1103
+ const store = r['store'];
1104
+ if (typeof store !== 'string' || store.length === 0) {
1105
+ throw new Error('idb_query: payload must include { store: non-empty string }');
1106
+ }
1107
+ const payload = { db, store };
1108
+ if (typeof r['limit'] === 'number' &&
1109
+ Number.isInteger(r['limit']) &&
1110
+ r['limit'] > 0) {
1111
+ payload['limit'] = r['limit'];
1112
+ }
1113
+ const tabId = readTabId(env.payload);
1114
+ const csReq = { tool: 'idb_query', payload };
1115
+ const response = tabId !== undefined
1116
+ ? await dispatchToTab(tabId, csReq)
1117
+ : await dispatchToActiveTab(csReq);
1118
+ if (response.error)
1119
+ throw new Error(response.error.message);
1120
+ return response.payload;
1121
+ };
1122
+ // pwa_update_gather (PWA Runtime Diagnostics T3): forward to the active/given
1123
+ // tab's page-world, which gathers the SW snapshot + cache entries. Optional
1124
+ // per_cache_limit beyond tab_id.
1125
+ const handlePwaUpdateGather = async (env) => {
1126
+ const r = env.payload !== null && typeof env.payload === 'object'
1127
+ ? env.payload
1128
+ : {};
1129
+ const payload = {};
1130
+ if (typeof r['per_cache_limit'] === 'number' &&
1131
+ Number.isInteger(r['per_cache_limit']) &&
1132
+ r['per_cache_limit'] > 0) {
1133
+ payload['per_cache_limit'] = r['per_cache_limit'];
1134
+ }
1135
+ const tabId = readTabId(env.payload);
1136
+ const csReq = { tool: 'pwa_update_gather', payload };
1137
+ const response = tabId !== undefined
1138
+ ? await dispatchToTab(tabId, csReq)
1139
+ : await dispatchToActiveTab(csReq);
1140
+ if (response.error)
1141
+ throw new Error(response.error.message);
1142
+ return response.payload;
1143
+ };
1144
+ // pwa_snapshot (PWA Runtime Diagnostics T3): forward to the active/given tab's
1145
+ // page-world, which composes the runtime-state blob. No params beyond tab_id.
1146
+ const handlePwaSnapshotGather = async (env) => {
1147
+ const tabId = readTabId(env.payload);
1148
+ const csReq = { tool: 'pwa_snapshot_gather', payload: {} };
1149
+ const response = tabId !== undefined
1150
+ ? await dispatchToTab(tabId, csReq)
1151
+ : await dispatchToActiveTab(csReq);
1152
+ if (response.error)
1153
+ throw new Error(response.error.message);
1154
+ return response.payload;
1155
+ };
1156
+ const handleCacheInspect = async (env) => {
1157
+ const r = env.payload !== null && typeof env.payload === 'object'
1158
+ ? env.payload
1159
+ : {};
1160
+ const name = r['cache_name'];
1161
+ if (typeof name !== 'string' || name.length === 0) {
1162
+ throw new Error('cache_inspect: payload must include { cache_name: non-empty string }');
1163
+ }
1164
+ const payload = { cache_name: name };
1165
+ if (typeof r['limit'] === 'number' && Number.isInteger(r['limit']) && r['limit'] > 0) {
1166
+ payload['limit'] = r['limit'];
1167
+ }
1168
+ const tabId = readTabId(env.payload);
1169
+ const csReq = { tool: 'cache_inspect', payload };
1170
+ const response = tabId !== undefined
1171
+ ? await dispatchToTab(tabId, csReq)
1172
+ : await dispatchToActiveTab(csReq);
1173
+ if (response.error)
1174
+ throw new Error(response.error.message);
1175
+ return response.payload;
1176
+ };
1177
+ const handleCacheMatch = async (env) => {
1178
+ const r = env.payload !== null && typeof env.payload === 'object'
1179
+ ? env.payload
1180
+ : {};
1181
+ const url = r['url'];
1182
+ if (typeof url !== 'string' || url.length === 0) {
1183
+ throw new Error('cache_match: payload must include { url: non-empty string }');
1184
+ }
1185
+ const tabId = readTabId(env.payload);
1186
+ const csReq = { tool: 'cache_match', payload: { url } };
1187
+ const response = tabId !== undefined
1188
+ ? await dispatchToTab(tabId, csReq)
1189
+ : await dispatchToActiveTab(csReq);
1190
+ if (response.error)
1191
+ throw new Error(response.error.message);
1192
+ return response.payload;
1193
+ };
1194
+ const sanitizeSvelteFindByTextInput = (raw) => {
1195
+ if (raw === null || typeof raw !== 'object')
1196
+ return null;
1197
+ const r = raw;
1198
+ const pattern = r['pattern'];
1199
+ if (typeof pattern !== 'string' || pattern.length === 0)
1200
+ return null;
1201
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
1202
+ ? r['tab_id']
1203
+ : undefined;
1204
+ const payload = { pattern };
1205
+ if (typeof r['exact'] === 'boolean')
1206
+ payload['exact'] = r['exact'];
1207
+ if (typeof r['max_matches'] === 'number' &&
1208
+ Number.isInteger(r['max_matches']) &&
1209
+ r['max_matches'] > 0) {
1210
+ payload['max_matches'] = r['max_matches'];
1211
+ }
1212
+ return { tabId, payload };
1213
+ };
1214
+ const handleSvelteFindByText = async (env) => {
1215
+ const sanitized = sanitizeSvelteFindByTextInput(env.payload);
1216
+ if (sanitized === null) {
1217
+ throw new Error('svelte_find_by_text: payload must be { pattern: non-empty string, tab_id?, exact?, max_matches? }');
1218
+ }
1219
+ const csReq = { tool: 'svelte_find_by_text', payload: sanitized.payload };
1220
+ const response = sanitized.tabId !== undefined
1221
+ ? await dispatchToTab(sanitized.tabId, csReq)
1222
+ : await dispatchToActiveTab(csReq);
1223
+ if (response.error) {
1224
+ throw new Error(response.error.message);
1225
+ }
1226
+ return response.payload;
1227
+ };
1228
+ const sanitizeSvelteFindByRoleInput = (raw) => {
1229
+ if (raw === null || typeof raw !== 'object')
1230
+ return null;
1231
+ const r = raw;
1232
+ const role = r['role'];
1233
+ if (typeof role !== 'string' || role.length === 0)
1234
+ return null;
1235
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
1236
+ ? r['tab_id']
1237
+ : undefined;
1238
+ const payload = { role };
1239
+ if (typeof r['name'] === 'string' && r['name'].length > 0) {
1240
+ payload['name'] = r['name'];
1241
+ }
1242
+ if (typeof r['max_matches'] === 'number' &&
1243
+ Number.isInteger(r['max_matches']) &&
1244
+ r['max_matches'] > 0) {
1245
+ payload['max_matches'] = r['max_matches'];
1246
+ }
1247
+ return { tabId, payload };
1248
+ };
1249
+ const handleSvelteFindByRole = async (env) => {
1250
+ const sanitized = sanitizeSvelteFindByRoleInput(env.payload);
1251
+ if (sanitized === null) {
1252
+ throw new Error('svelte_find_by_role: payload must be { role: non-empty string, tab_id?, name?, max_matches? }');
1253
+ }
1254
+ const csReq = { tool: 'svelte_find_by_role', payload: sanitized.payload };
1255
+ const response = sanitized.tabId !== undefined
1256
+ ? await dispatchToTab(sanitized.tabId, csReq)
1257
+ : await dispatchToActiveTab(csReq);
1258
+ if (response.error) {
1259
+ throw new Error(response.error.message);
1260
+ }
1261
+ return response.payload;
1262
+ };
1263
+ // ── Solid introspection (Path 5 M43) ────────────────────────────────────────
1264
+ const handleSolidDetect = async (env) => {
1265
+ const raw = env.payload;
1266
+ const tabId = raw !== null &&
1267
+ typeof raw === 'object' &&
1268
+ typeof raw['tab_id'] === 'number' &&
1269
+ Number.isFinite(raw['tab_id'])
1270
+ ? raw['tab_id']
1271
+ : undefined;
1272
+ const csReq = { tool: 'solid_detect', payload: {} };
1273
+ const response = tabId !== undefined
1274
+ ? await dispatchToTab(tabId, csReq)
1275
+ : await dispatchToActiveTab(csReq);
1276
+ if (response.error) {
1277
+ throw new Error(response.error.message);
1278
+ }
1279
+ return response.payload;
1280
+ };
1281
+ const sanitizeSolidFindByTextInput = (raw) => {
1282
+ if (raw === null || typeof raw !== 'object')
1283
+ return null;
1284
+ const r = raw;
1285
+ const pattern = r['pattern'];
1286
+ if (typeof pattern !== 'string' || pattern.length === 0)
1287
+ return null;
1288
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
1289
+ ? r['tab_id']
1290
+ : undefined;
1291
+ const payload = { pattern };
1292
+ if (typeof r['exact'] === 'boolean')
1293
+ payload['exact'] = r['exact'];
1294
+ if (typeof r['max_matches'] === 'number' &&
1295
+ Number.isInteger(r['max_matches']) &&
1296
+ r['max_matches'] > 0) {
1297
+ payload['max_matches'] = r['max_matches'];
1298
+ }
1299
+ return { tabId, payload };
1300
+ };
1301
+ const handleSolidFindByText = async (env) => {
1302
+ const sanitized = sanitizeSolidFindByTextInput(env.payload);
1303
+ if (sanitized === null) {
1304
+ throw new Error('solid_find_by_text: payload must be { pattern: non-empty string, tab_id?, exact?, max_matches? }');
1305
+ }
1306
+ const csReq = { tool: 'solid_find_by_text', payload: sanitized.payload };
1307
+ const response = sanitized.tabId !== undefined
1308
+ ? await dispatchToTab(sanitized.tabId, csReq)
1309
+ : await dispatchToActiveTab(csReq);
1310
+ if (response.error) {
1311
+ throw new Error(response.error.message);
1312
+ }
1313
+ return response.payload;
1314
+ };
1315
+ const sanitizeSolidFindByRoleInput = (raw) => {
1316
+ if (raw === null || typeof raw !== 'object')
1317
+ return null;
1318
+ const r = raw;
1319
+ const role = r['role'];
1320
+ if (typeof role !== 'string' || role.length === 0)
1321
+ return null;
1322
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
1323
+ ? r['tab_id']
1324
+ : undefined;
1325
+ const payload = { role };
1326
+ if (typeof r['name'] === 'string' && r['name'].length > 0) {
1327
+ payload['name'] = r['name'];
1328
+ }
1329
+ if (typeof r['max_matches'] === 'number' &&
1330
+ Number.isInteger(r['max_matches']) &&
1331
+ r['max_matches'] > 0) {
1332
+ payload['max_matches'] = r['max_matches'];
1333
+ }
1334
+ return { tabId, payload };
1335
+ };
1336
+ const handleSolidFindByRole = async (env) => {
1337
+ const sanitized = sanitizeSolidFindByRoleInput(env.payload);
1338
+ if (sanitized === null) {
1339
+ throw new Error('solid_find_by_role: payload must be { role: non-empty string, tab_id?, name?, max_matches? }');
1340
+ }
1341
+ const csReq = { tool: 'solid_find_by_role', payload: sanitized.payload };
1342
+ const response = sanitized.tabId !== undefined
1343
+ ? await dispatchToTab(sanitized.tabId, csReq)
1344
+ : await dispatchToActiveTab(csReq);
1345
+ if (response.error) {
1346
+ throw new Error(response.error.message);
1347
+ }
1348
+ return response.payload;
1349
+ };
1350
+ const sanitizeReduxGetStateInput = (raw) => {
1351
+ if (raw === undefined || raw === null) {
1352
+ return { tabId: undefined, payload: {} };
1353
+ }
1354
+ if (typeof raw !== 'object')
1355
+ return null;
1356
+ const r = raw;
1357
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
1358
+ ? r['tab_id']
1359
+ : undefined;
1360
+ const payload = {};
1361
+ if (typeof r['path'] === 'string' && r['path'].length > 0) {
1362
+ payload['path'] = r['path'];
1363
+ }
1364
+ return { tabId, payload };
1365
+ };
1366
+ const handleReduxGetState = async (env) => {
1367
+ const sanitized = sanitizeReduxGetStateInput(env.payload);
1368
+ if (sanitized === null) {
1369
+ throw new Error('redux_get_state: payload must be an object with optional { tab_id?, path? }');
1370
+ }
1371
+ const csReq = { tool: 'redux_get_state', payload: sanitized.payload };
1372
+ const response = sanitized.tabId !== undefined
1373
+ ? await dispatchToTab(sanitized.tabId, csReq)
1374
+ : await dispatchToActiveTab(csReq);
1375
+ if (response.error) {
1376
+ throw new Error(response.error.message);
1377
+ }
1378
+ return response.payload;
1379
+ };
1380
+ const sanitizeReduxSubscribeInput = (raw) => {
1381
+ if (raw === null || typeof raw !== 'object')
1382
+ return null;
1383
+ const r = raw;
1384
+ const action = r['action'];
1385
+ if (action !== 'start' && action !== 'stop')
1386
+ return null;
1387
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
1388
+ ? r['tab_id']
1389
+ : undefined;
1390
+ const payload = { action };
1391
+ if (typeof r['path'] === 'string' && r['path'].length > 0) {
1392
+ payload['path'] = r['path'];
1393
+ }
1394
+ return { tabId, payload };
1395
+ };
1396
+ const handleReduxSubscribe = async (env) => {
1397
+ const sanitized = sanitizeReduxSubscribeInput(env.payload);
1398
+ if (sanitized === null) {
1399
+ throw new Error("redux_subscribe: payload must be { action: 'start' | 'stop', tab_id?, path? }");
1400
+ }
1401
+ const csReq = { tool: 'redux_subscribe', payload: sanitized.payload };
1402
+ const response = sanitized.tabId !== undefined
1403
+ ? await dispatchToTab(sanitized.tabId, csReq)
1404
+ : await dispatchToActiveTab(csReq);
1405
+ if (response.error) {
1406
+ throw new Error(response.error.message);
1407
+ }
1408
+ return response.payload;
1409
+ };
1410
+ const sanitizeReduxDispatchInput = (raw) => {
1411
+ if (raw === null || typeof raw !== 'object')
1412
+ return null;
1413
+ const r = raw;
1414
+ const action = r['action'];
1415
+ if (action === null || typeof action !== 'object')
1416
+ return null;
1417
+ const a = action;
1418
+ if (typeof a['type'] !== 'string' || a['type'].length === 0) {
1419
+ return null;
1420
+ }
1421
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
1422
+ ? r['tab_id']
1423
+ : undefined;
1424
+ return { tabId, payload: { action } };
1425
+ };
1426
+ const handleReduxDispatch = async (env) => {
1427
+ const sanitized = sanitizeReduxDispatchInput(env.payload);
1428
+ if (sanitized === null) {
1429
+ throw new Error('redux_dispatch: payload must be { action: { type: non-empty string; payload? }, tab_id? }');
1430
+ }
1431
+ const csReq = { tool: 'redux_dispatch', payload: sanitized.payload };
1432
+ const response = sanitized.tabId !== undefined
1433
+ ? await dispatchToTab(sanitized.tabId, csReq)
1434
+ : await dispatchToActiveTab(csReq);
1435
+ if (response.error) {
1436
+ throw new Error(response.error.message);
1437
+ }
1438
+ return response.payload;
1439
+ };
1440
+ // ── Unified store_* family (Path 4 M2) ──────────────────────────────────────
1441
+ // Framework-aware variants of the redux_* sanitizers: they additionally pass
1442
+ // an optional `framework` selector through to the page-world store handlers
1443
+ // (which auto-detect when it is absent) and forward to the store_* page keys.
1444
+ // The redux_* handlers above are kept untouched as deprecated aliases.
1445
+ const extractFramework = (r) => typeof r['framework'] === 'string' && r['framework'].length > 0
1446
+ ? r['framework']
1447
+ : undefined;
1448
+ const routedTabId = (r) => typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
1449
+ ? r['tab_id']
1450
+ : undefined;
1451
+ const sanitizeStoreGetStateInput = (raw) => {
1452
+ if (raw === undefined || raw === null) {
1453
+ return { tabId: undefined, payload: {} };
1454
+ }
1455
+ if (typeof raw !== 'object')
1456
+ return null;
1457
+ const r = raw;
1458
+ const payload = {};
1459
+ if (typeof r['path'] === 'string' && r['path'].length > 0) {
1460
+ payload['path'] = r['path'];
1461
+ }
1462
+ const framework = extractFramework(r);
1463
+ if (framework !== undefined)
1464
+ payload['framework'] = framework;
1465
+ return { tabId: routedTabId(r), payload };
1466
+ };
1467
+ const handleStoreGetState = async (env) => {
1468
+ const sanitized = sanitizeStoreGetStateInput(env.payload);
1469
+ if (sanitized === null) {
1470
+ throw new Error('store_get_state: payload must be an object with optional { tab_id?, path?, framework? }');
1471
+ }
1472
+ const csReq = { tool: 'store_get_state', payload: sanitized.payload };
1473
+ const response = sanitized.tabId !== undefined
1474
+ ? await dispatchToTab(sanitized.tabId, csReq)
1475
+ : await dispatchToActiveTab(csReq);
1476
+ if (response.error) {
1477
+ throw new Error(response.error.message);
1478
+ }
1479
+ return response.payload;
1480
+ };
1481
+ const sanitizeStoreSubscribeInput = (raw) => {
1482
+ if (raw === null || typeof raw !== 'object')
1483
+ return null;
1484
+ const r = raw;
1485
+ const action = r['action'];
1486
+ if (action !== 'start' && action !== 'stop')
1487
+ return null;
1488
+ const payload = { action };
1489
+ if (typeof r['path'] === 'string' && r['path'].length > 0) {
1490
+ payload['path'] = r['path'];
1491
+ }
1492
+ const framework = extractFramework(r);
1493
+ if (framework !== undefined)
1494
+ payload['framework'] = framework;
1495
+ return { tabId: routedTabId(r), payload };
1496
+ };
1497
+ const handleStoreSubscribe = async (env) => {
1498
+ const sanitized = sanitizeStoreSubscribeInput(env.payload);
1499
+ if (sanitized === null) {
1500
+ throw new Error("store_subscribe: payload must be { action: 'start' | 'stop', tab_id?, path?, framework? }");
1501
+ }
1502
+ const csReq = { tool: 'store_subscribe', payload: sanitized.payload };
1503
+ const response = sanitized.tabId !== undefined
1504
+ ? await dispatchToTab(sanitized.tabId, csReq)
1505
+ : await dispatchToActiveTab(csReq);
1506
+ if (response.error) {
1507
+ throw new Error(response.error.message);
1508
+ }
1509
+ return response.payload;
1510
+ };
1511
+ const sanitizeStoreDispatchInput = (raw) => {
1512
+ if (raw === null || typeof raw !== 'object')
1513
+ return null;
1514
+ const r = raw;
1515
+ const action = r['action'];
1516
+ if (action === null || typeof action !== 'object')
1517
+ return null;
1518
+ const a = action;
1519
+ if (typeof a['type'] !== 'string' || a['type'].length === 0) {
1520
+ return null;
1521
+ }
1522
+ const payload = { action };
1523
+ const framework = extractFramework(r);
1524
+ if (framework !== undefined)
1525
+ payload['framework'] = framework;
1526
+ return { tabId: routedTabId(r), payload };
1527
+ };
1528
+ const handleStoreDispatch = async (env) => {
1529
+ const sanitized = sanitizeStoreDispatchInput(env.payload);
1530
+ if (sanitized === null) {
1531
+ throw new Error('store_dispatch: payload must be { action: { type: non-empty string; payload? }, tab_id?, framework? }');
1532
+ }
1533
+ const csReq = { tool: 'store_dispatch', payload: sanitized.payload };
1534
+ const response = sanitized.tabId !== undefined
1535
+ ? await dispatchToTab(sanitized.tabId, csReq)
1536
+ : await dispatchToActiveTab(csReq);
1537
+ if (response.error) {
1538
+ throw new Error(response.error.message);
1539
+ }
1540
+ return response.payload;
1541
+ };
1542
+ const sanitizeSourceMapResolveInput = (raw) => {
1543
+ if (raw === null || typeof raw !== 'object')
1544
+ return null;
1545
+ const r = raw;
1546
+ const scriptUrl = r['script_url'];
1547
+ if (typeof scriptUrl !== 'string' || scriptUrl.length === 0)
1548
+ return null;
1549
+ const line = r['line'];
1550
+ if (typeof line !== 'number' || !Number.isInteger(line) || line < 1)
1551
+ return null;
1552
+ const column = r['column'];
1553
+ if (typeof column !== 'number' || !Number.isInteger(column) || column < 0) {
1554
+ return null;
1555
+ }
1556
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
1557
+ ? r['tab_id']
1558
+ : undefined;
1559
+ return {
1560
+ tabId,
1561
+ payload: { script_url: scriptUrl, line, column },
1562
+ };
1563
+ };
1564
+ const handleSourceMapResolve = async (env) => {
1565
+ const sanitized = sanitizeSourceMapResolveInput(env.payload);
1566
+ if (sanitized === null) {
1567
+ throw new Error('source_map_resolve: payload must be { script_url, line: int>=1, column: int>=0, tab_id? }');
1568
+ }
1569
+ const csReq = { tool: 'source_map_resolve', payload: sanitized.payload };
1570
+ const response = sanitized.tabId !== undefined
1571
+ ? await dispatchToTab(sanitized.tabId, csReq)
1572
+ : await dispatchToActiveTab(csReq);
1573
+ if (response.error) {
1574
+ throw new Error(response.error.message);
1575
+ }
1576
+ return response.payload;
1577
+ };
1578
+ const sanitizeSessionRecordInput = (raw) => {
1579
+ if (raw === null || typeof raw !== 'object')
1580
+ return null;
1581
+ const r = raw;
1582
+ const action = r['action'];
1583
+ if (action !== 'start' && action !== 'stop')
1584
+ return null;
1585
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
1586
+ ? r['tab_id']
1587
+ : undefined;
1588
+ const payload = { action };
1589
+ if (typeof r['session_id'] === 'string' && r['session_id'].length > 0) {
1590
+ payload['session_id'] = r['session_id'];
1591
+ }
1592
+ if (typeof r['duration_cap_ms'] === 'number' &&
1593
+ Number.isInteger(r['duration_cap_ms']) &&
1594
+ r['duration_cap_ms'] > 0) {
1595
+ payload['duration_cap_ms'] = r['duration_cap_ms'];
1596
+ }
1597
+ return { tabId, payload };
1598
+ };
1599
+ const handleSessionRecord = async (env) => {
1600
+ const sanitized = sanitizeSessionRecordInput(env.payload);
1601
+ if (sanitized === null) {
1602
+ throw new Error("session_record: payload must be { action: 'start' | 'stop', tab_id?, session_id?, duration_cap_ms? }");
1603
+ }
1604
+ const csReq = { tool: 'session_record', payload: sanitized.payload };
1605
+ const response = sanitized.tabId !== undefined
1606
+ ? await dispatchToTab(sanitized.tabId, csReq)
1607
+ : await dispatchToActiveTab(csReq);
1608
+ if (response.error) {
1609
+ throw new Error(response.error.message);
1610
+ }
1611
+ return response.payload;
1612
+ };
1613
+ // --- Path 7 interaction action tools (pdl_*) ---------------------------------
1614
+ // One generic handler per ACTION_TOOL_SPECS entry: extract tab_id for routing,
1615
+ // forward the locator + params payload to the page-world unchanged.
1616
+ const sanitizeActionInput = (raw) => {
1617
+ if (raw === null || typeof raw !== 'object')
1618
+ return null;
1619
+ const r = raw;
1620
+ const tabId = typeof r['tab_id'] === 'number' && Number.isFinite(r['tab_id'])
1621
+ ? r['tab_id']
1622
+ : undefined;
1623
+ const payload = {};
1624
+ for (const [k, v] of Object.entries(r)) {
1625
+ if (k !== 'tab_id' && k !== 'extension_id' && v !== undefined)
1626
+ payload[k] = v;
1627
+ }
1628
+ return tabId !== undefined ? { tabId, payload } : { payload };
1629
+ };
1630
+ const makeActionRequestHandler = (tool) => async (env) => {
1631
+ const sanitized = sanitizeActionInput(env.payload);
1632
+ if (sanitized === null) {
1633
+ throw new Error(`${tool}: payload must be an object carrying a locator`);
1634
+ }
1635
+ const csReq = { tool, payload: sanitized.payload };
1636
+ const response = sanitized.tabId !== undefined
1637
+ ? await dispatchToTab(sanitized.tabId, csReq)
1638
+ : await dispatchToActiveTab(csReq);
1639
+ if (response.error)
1640
+ throw new Error(response.error.message);
1641
+ return response.payload;
1642
+ };
1643
+ const actionRequestHandlers = Object.freeze(Object.fromEntries(ACTION_TOOL_SPECS.map((s) => [s.tool, makeActionRequestHandler(s.tool)])));
1644
+ const HANDLERS = Object.freeze({
1645
+ ...actionRequestHandlers,
1646
+ session_ping: handleSessionPing,
1647
+ recent_events: handleRecentEvents,
1648
+ evaluate: handleEvaluate,
1649
+ react_tree: handleReactTree,
1650
+ react_get_state: handleReactGetState,
1651
+ react_find_by_text: handleReactFindByText,
1652
+ react_find_by_role: handleReactFindByRole,
1653
+ vue_tree: handleVueTree,
1654
+ vue_get_state: handleVueGetState,
1655
+ vue_find_by_text: handleVueFindByText,
1656
+ vue_find_by_role: handleVueFindByRole,
1657
+ svelte_components: handleSvelteComponents,
1658
+ svelte_find_by_text: handleSvelteFindByText,
1659
+ svelte_find_by_role: handleSvelteFindByRole,
1660
+ solid_detect: handleSolidDetect,
1661
+ solid_find_by_text: handleSolidFindByText,
1662
+ solid_find_by_role: handleSolidFindByRole,
1663
+ redux_get_state: handleReduxGetState,
1664
+ redux_subscribe: handleReduxSubscribe,
1665
+ redux_dispatch: handleReduxDispatch,
1666
+ store_get_state: handleStoreGetState,
1667
+ store_subscribe: handleStoreSubscribe,
1668
+ store_dispatch: handleStoreDispatch,
1669
+ source_map_resolve: handleSourceMapResolve,
1670
+ session_record: handleSessionRecord,
1671
+ sw_status: handleSwStatus,
1672
+ cache_list: handleCacheList,
1673
+ cache_inspect: handleCacheInspect,
1674
+ cache_match: handleCacheMatch,
1675
+ pwa_status: handlePwaStatus,
1676
+ pwa_installability: handlePwaInstallability,
1677
+ storage_get: handleStorageGet,
1678
+ idb_list: handleIdbList,
1679
+ idb_query: handleIdbQuery,
1680
+ pwa_update_gather: handlePwaUpdateGather,
1681
+ pwa_snapshot_gather: handlePwaSnapshotGather,
1682
+ });
1683
+ const errorResponse = (requestId, message) => Object.freeze({
1684
+ type: 'response',
1685
+ requestId,
1686
+ error: Object.freeze({ message }),
1687
+ });
1688
+ const okResponse = (requestId, payload) => Object.freeze({
1689
+ type: 'response',
1690
+ requestId,
1691
+ payload,
1692
+ });
1693
+ const routeRequest = async (env, ctx) => {
1694
+ const handler = HANDLERS[env.tool];
1695
+ if (!handler) {
1696
+ return errorResponse(env.requestId, `unknown tool: ${env.tool}`);
1697
+ }
1698
+ try {
1699
+ const payload = await handler(env, ctx);
1700
+ return okResponse(env.requestId, payload);
1701
+ }
1702
+ catch (err) {
1703
+ return errorResponse(env.requestId, err.message);
1704
+ }
1705
+ };
1706
+
1707
+ const PAGE_EVENT_SW_TAG = 'pwa-debug-page-event';
1708
+
1709
+ const createBatchAccumulator = (opts) => {
1710
+ if (!Number.isFinite(opts.maxSize) || opts.maxSize <= 0) {
1711
+ throw new Error(`createBatchAccumulator: maxSize must be > 0, got ${String(opts.maxSize)}`);
1712
+ }
1713
+ if (!Number.isFinite(opts.maxMs) || opts.maxMs <= 0) {
1714
+ throw new Error(`createBatchAccumulator: maxMs must be > 0, got ${String(opts.maxMs)}`);
1715
+ }
1716
+ const maxSize = opts.maxSize;
1717
+ const maxMs = opts.maxMs;
1718
+ const flush = opts.flush;
1719
+ const pending = [];
1720
+ let timerHandle;
1721
+ const clearTimer = () => {
1722
+ if (timerHandle !== undefined) {
1723
+ clearTimeout(timerHandle);
1724
+ timerHandle = undefined;
1725
+ }
1726
+ };
1727
+ const flushNow = () => {
1728
+ clearTimer();
1729
+ if (pending.length === 0)
1730
+ return;
1731
+ const snapshot = pending.slice();
1732
+ pending.length = 0;
1733
+ flush(snapshot);
1734
+ };
1735
+ const push = (item) => {
1736
+ pending.push(item);
1737
+ if (pending.length >= maxSize) {
1738
+ flushNow();
1739
+ return;
1740
+ }
1741
+ if (timerHandle === undefined) {
1742
+ timerHandle = setTimeout(flushNow, maxMs);
1743
+ }
1744
+ };
1745
+ const dispose = () => {
1746
+ clearTimer();
1747
+ pending.length = 0;
1748
+ };
1749
+ return Object.freeze({ push, flushNow, dispose });
1750
+ };
1751
+
1752
+ const DEFAULT_BUFFER_SIZE = 200;
1753
+ const DEFAULT_LIMIT = 50;
1754
+ const DEFAULT_FORWARD_MAX_SIZE = 50;
1755
+ const DEFAULT_FORWARD_MAX_MS = 100;
1756
+ const createEventSink = (input = {}) => {
1757
+ const logger = input.logger;
1758
+ const shouldRecord = input.shouldRecord;
1759
+ const bufferSize = input.bufferSize !== undefined && input.bufferSize > 0
1760
+ ? Math.floor(input.bufferSize)
1761
+ : DEFAULT_BUFFER_SIZE;
1762
+ const buffer = [];
1763
+ let writeIndex = 0;
1764
+ const perKind = {};
1765
+ let totalReceived = 0;
1766
+ const forwardAcc = input.forwardEvents !== undefined
1767
+ ? createBatchAccumulator({
1768
+ maxSize: input.forwardMaxSize ?? DEFAULT_FORWARD_MAX_SIZE,
1769
+ maxMs: input.forwardMaxMs ?? DEFAULT_FORWARD_MAX_MS,
1770
+ flush: input.forwardEvents,
1771
+ })
1772
+ : undefined;
1773
+ const snapshotStats = () => Object.freeze({
1774
+ totalReceived,
1775
+ perKind: Object.freeze({ ...perKind }),
1776
+ bufferSize,
1777
+ });
1778
+ const handle = (event) => {
1779
+ if (shouldRecord !== undefined && !shouldRecord(event)) {
1780
+ // Gated out: no stats, no buffer, no logger, no forward.
1781
+ return;
1782
+ }
1783
+ perKind[event.kind] = (perKind[event.kind] ?? 0) + 1;
1784
+ totalReceived += 1;
1785
+ if (buffer.length < bufferSize) {
1786
+ buffer.push(event);
1787
+ }
1788
+ else {
1789
+ buffer[writeIndex] = event;
1790
+ }
1791
+ writeIndex = (writeIndex + 1) % bufferSize;
1792
+ if (logger !== undefined) {
1793
+ try {
1794
+ logger(event);
1795
+ }
1796
+ catch {
1797
+ // Logger failures must not interrupt event ingestion.
1798
+ }
1799
+ }
1800
+ if (forwardAcc !== undefined) {
1801
+ forwardAcc.push(event);
1802
+ }
1803
+ };
1804
+ const getStats = () => snapshotStats();
1805
+ const getRecent = (filter = {}) => {
1806
+ const ordered = [];
1807
+ if (buffer.length < bufferSize) {
1808
+ for (let i = 0; i < buffer.length; i++) {
1809
+ ordered.push(buffer[i]);
1810
+ }
1811
+ }
1812
+ else {
1813
+ for (let i = 0; i < bufferSize; i++) {
1814
+ ordered.push(buffer[(writeIndex + i) % bufferSize]);
1815
+ }
1816
+ }
1817
+ const kindsSet = filter.kinds !== undefined && filter.kinds.length > 0
1818
+ ? new Set(filter.kinds)
1819
+ : undefined;
1820
+ const afterKinds = kindsSet === undefined
1821
+ ? ordered
1822
+ : ordered.filter((e) => kindsSet.has(e.kind));
1823
+ const sinceMs = filter.sinceMs;
1824
+ const afterSince = sinceMs === undefined
1825
+ ? afterKinds
1826
+ : afterKinds.filter((e) => e.ts > sinceMs);
1827
+ const requested = filter.limit !== undefined ? Math.floor(filter.limit) : DEFAULT_LIMIT;
1828
+ const cap = Math.max(0, Math.min(requested, bufferSize));
1829
+ const events = afterSince.length > cap
1830
+ ? afterSince.slice(afterSince.length - cap)
1831
+ : afterSince;
1832
+ return Object.freeze({
1833
+ events: Object.freeze([...events]),
1834
+ stats: snapshotStats(),
1835
+ });
1836
+ };
1837
+ const flushNow = () => {
1838
+ forwardAcc?.flushNow();
1839
+ };
1840
+ const dispose = () => {
1841
+ forwardAcc?.dispose();
1842
+ };
1843
+ return Object.freeze({ handle, getStats, getRecent, flushNow, dispose });
1844
+ };
1845
+ const isPageEventSwMessage = (msg) => {
1846
+ if (msg === null || typeof msg !== 'object')
1847
+ return false;
1848
+ const r = msg;
1849
+ return r['tag'] === PAGE_EVENT_SW_TAG && 'event' in r;
1850
+ };
1851
+
1852
+ const isEnabled = (opts, subkind) => opts?.enabled?.[subkind] !== false;
1853
+ const frameKeyFor = (frameId) => frameId === undefined || frameId === 0 ? 'top' : `frame-${frameId}`;
1854
+ const createSwLifecycleProducer = (deps) => {
1855
+ if (typeof chrome === 'undefined' ||
1856
+ !chrome.tabs ||
1857
+ !chrome.webNavigation) {
1858
+ return () => { };
1859
+ }
1860
+ const { sink, opts, getTabUrl } = deps;
1861
+ const now = () => Date.now();
1862
+ let disposed = false;
1863
+ const cleanups = [];
1864
+ const tryEmit = (event) => {
1865
+ if (disposed)
1866
+ return;
1867
+ try {
1868
+ sink.handle(event);
1869
+ }
1870
+ catch {
1871
+ // Capture failure must never break the SW.
1872
+ }
1873
+ };
1874
+ const buildEvent = (payload, frameUrl, frameKey) => Object.freeze({
1875
+ kind: 'lifecycle',
1876
+ source: 'sw',
1877
+ ts: now(),
1878
+ frameUrl,
1879
+ frameKey,
1880
+ ...payload,
1881
+ });
1882
+ if (isEnabled(opts, 'navigation_committed')) {
1883
+ const onCommitted = (d) => {
1884
+ const payload = {
1885
+ subkind: 'navigation_committed',
1886
+ tabId: d.tabId,
1887
+ frameId: d.frameId,
1888
+ url: d.url,
1889
+ ...(d.transitionType ? { transitionType: d.transitionType } : {}),
1890
+ ...(d.transitionQualifiers
1891
+ ? { transitionQualifiers: d.transitionQualifiers }
1892
+ : {}),
1893
+ };
1894
+ tryEmit(buildEvent(payload, d.url, frameKeyFor(d.frameId)));
1895
+ };
1896
+ chrome.webNavigation.onCommitted.addListener(onCommitted);
1897
+ cleanups.push(() => chrome.webNavigation.onCommitted.removeListener(onCommitted));
1898
+ }
1899
+ if (isEnabled(opts, 'history_state_updated')) {
1900
+ const onHistory = (d) => {
1901
+ const payload = {
1902
+ subkind: 'history_state_updated',
1903
+ tabId: d.tabId,
1904
+ frameId: d.frameId,
1905
+ url: d.url,
1906
+ ...(d.transitionType ? { transitionType: d.transitionType } : {}),
1907
+ ...(d.transitionQualifiers
1908
+ ? { transitionQualifiers: d.transitionQualifiers }
1909
+ : {}),
1910
+ };
1911
+ tryEmit(buildEvent(payload, d.url, frameKeyFor(d.frameId)));
1912
+ };
1913
+ chrome.webNavigation.onHistoryStateUpdated.addListener(onHistory);
1914
+ cleanups.push(() => chrome.webNavigation.onHistoryStateUpdated.removeListener(onHistory));
1915
+ }
1916
+ if (isEnabled(opts, 'tab_status')) {
1917
+ const onUpdated = (tabId, changeInfo, tab) => {
1918
+ if (changeInfo.status !== 'loading' && changeInfo.status !== 'complete') {
1919
+ return;
1920
+ }
1921
+ const payload = {
1922
+ subkind: 'tab_status',
1923
+ tabId,
1924
+ status: changeInfo.status,
1925
+ };
1926
+ const url = tab.url ?? getTabUrl?.(tabId) ?? '';
1927
+ tryEmit(buildEvent(payload, url, 'top'));
1928
+ };
1929
+ chrome.tabs.onUpdated.addListener(onUpdated);
1930
+ cleanups.push(() => chrome.tabs.onUpdated.removeListener(onUpdated));
1931
+ }
1932
+ if (isEnabled(opts, 'tab_removed')) {
1933
+ const onRemoved = (tabId, removeInfo) => {
1934
+ const payload = {
1935
+ subkind: 'tab_removed',
1936
+ tabId,
1937
+ isWindowClosing: removeInfo.isWindowClosing,
1938
+ };
1939
+ const url = getTabUrl?.(tabId) ?? '';
1940
+ tryEmit(buildEvent(payload, url, 'top'));
1941
+ };
1942
+ chrome.tabs.onRemoved.addListener(onRemoved);
1943
+ cleanups.push(() => chrome.tabs.onRemoved.removeListener(onRemoved));
1944
+ }
1945
+ return () => {
1946
+ if (disposed)
1947
+ return;
1948
+ disposed = true;
1949
+ for (const cleanup of cleanups) {
1950
+ try {
1951
+ cleanup();
1952
+ }
1953
+ catch {
1954
+ // Ignore cleanup failures.
1955
+ }
1956
+ }
1957
+ };
1958
+ };
1959
+
1960
+ const attachFrameId = (event, frameId) => {
1961
+ if (frameId === undefined)
1962
+ return event;
1963
+ return { ...event, frameId };
1964
+ };
1965
+
1966
+ /**
1967
+ * Extension-side typed settings cache — SW-side mirror of host_settings.
1968
+ *
1969
+ * The host sends:
1970
+ * • One IpcEventEnvelope{ tool:'settings_snapshot', payload:{values: SettingsRecord} }
1971
+ * on extension register/handshake.
1972
+ * • One IpcEventEnvelope{ tool:'settings_changed', payload: SettingChange }
1973
+ * on each host store change.
1974
+ *
1975
+ * The SW orchestrator routes the envelope's payload to applySnapshot or
1976
+ * applyChange. This module owns the cache state — nothing here knows about
1977
+ * chrome.*, sockets, or IPC framing; it accepts already-decoded payloads as
1978
+ * `unknown` and defensively validates everything at the boundary.
1979
+ *
1980
+ * Pre-snapshot, getSetting returns the schema default for every key so
1981
+ * consumers (capture pipeline at T4, future UI) tolerate boot ordering.
1982
+ */
1983
+ const isKnownKey = (k) => typeof k === 'string' &&
1984
+ settingKeys().includes(k);
1985
+ const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
1986
+ const createSettingsCache = () => {
1987
+ let current = defaultSettings();
1988
+ const getSetting = (key) => current[key];
1989
+ const getAll = () => current;
1990
+ const applySnapshot = (payload) => {
1991
+ if (!isRecord(payload))
1992
+ return { applied: 0 };
1993
+ const values = payload['values'];
1994
+ if (!isRecord(values))
1995
+ return { applied: 0 };
1996
+ const next = {
1997
+ ...defaultSettings(),
1998
+ };
1999
+ let applied = 0;
2000
+ for (const k of settingKeys()) {
2001
+ const v = values[k];
2002
+ if (v !== undefined && validateSettingValue(k, v)) {
2003
+ next[k] = v;
2004
+ applied += 1;
2005
+ }
2006
+ }
2007
+ current = next;
2008
+ return { applied };
2009
+ };
2010
+ const applyChange = (payload) => {
2011
+ if (!isRecord(payload))
2012
+ return { applied: false };
2013
+ const key = payload['key'];
2014
+ if (!isKnownKey(key))
2015
+ return { applied: false };
2016
+ const value = payload['value'];
2017
+ if (!validateSettingValue(key, value))
2018
+ return { applied: false };
2019
+ // Re-narrowing: getSettingEntry confirms the key exists in schema (defensive).
2020
+ if (!getSettingEntry(key))
2021
+ return { applied: false };
2022
+ current = { ...current, [key]: value };
2023
+ return { applied: true };
2024
+ };
2025
+ return { getSetting, getAll, applySnapshot, applyChange };
2026
+ };
2027
+
2028
+ const REGEX_SPECIALS = /[.+?^${}()|[\]\\]/g;
2029
+ const globToRegex = (pattern) => {
2030
+ const escaped = pattern.replace(REGEX_SPECIALS, '\\$&').replace(/\*/g, '.*');
2031
+ return new RegExp(`^${escaped}$`);
2032
+ };
2033
+ const matchesAnyGlob = (value, patterns) => patterns.some((p) => globToRegex(p).test(value));
2034
+ /**
2035
+ * Raw captured-event kinds (6) -> M7 capture categories (4).
2036
+ * The union side reflects the captured_event.ts kind discriminants.
2037
+ */
2038
+ const KIND_MAP = Object.freeze({
2039
+ console: 'console',
2040
+ fetch: 'network',
2041
+ xhr: 'network',
2042
+ websocket: 'network',
2043
+ dom_mutation: 'dom_mutations',
2044
+ lifecycle: 'lifecycle',
2045
+ // App service-worker lifecycle: its own capture category + host buffer.
2046
+ sw_state: 'sw_state',
2047
+ store_change: 'store_change',
2048
+ replay: 'replay',
2049
+ library_popup: 'library_popup',
2050
+ });
2051
+ const eventKindToCaptureKind = (rawKind) => KIND_MAP[rawKind] ?? null;
2052
+ const isKindEnabled = (rawKind, enabledKinds) => {
2053
+ const ck = eventKindToCaptureKind(rawKind);
2054
+ if (ck === null)
2055
+ return true; // unknown raw kinds default-allow (future-compat)
2056
+ return enabledKinds.includes(ck);
2057
+ };
2058
+ const isUrlAllowed = (url, allowlist, blocklist) => {
2059
+ if (matchesAnyGlob(url, blocklist))
2060
+ return false; // blocklist wins
2061
+ if (allowlist.length === 0)
2062
+ return false; // explicit opt-in model
2063
+ return matchesAnyGlob(url, allowlist);
2064
+ };
2065
+ /**
2066
+ * Pick the most-specific readControls entry whose glob pattern matches `url`.
2067
+ * Specificity = longest pattern string; ties broken by lexicographic order of
2068
+ * the pattern so the choice is deterministic. Returns undefined when no
2069
+ * pattern matches (caller treats as no restriction).
2070
+ */
2071
+ const pickMostSpecificReadControl = (url, controls) => {
2072
+ let winnerPattern;
2073
+ let winnerValue;
2074
+ for (const [pattern, value] of Object.entries(controls)) {
2075
+ if (!matchesAnyGlob(url, [pattern]))
2076
+ continue;
2077
+ if (winnerPattern === undefined) {
2078
+ winnerPattern = pattern;
2079
+ winnerValue = value;
2080
+ continue;
2081
+ }
2082
+ if (pattern.length > winnerPattern.length ||
2083
+ (pattern.length === winnerPattern.length && pattern < winnerPattern)) {
2084
+ winnerPattern = pattern;
2085
+ winnerValue = value;
2086
+ }
2087
+ }
2088
+ return winnerValue;
2089
+ };
2090
+ /**
2091
+ * True iff the resolved readControls entry permits the given CaptureKind.
2092
+ * Missing flag = allowed (no restriction); explicit `false` denies. Undefined
2093
+ * control (no matching readControls entry) = allowed.
2094
+ */
2095
+ const isKindAllowedByReadControls = (kind, control) => control?.[kind] !== false;
2096
+ /**
2097
+ * True iff the per-kind capture.filters predicate (if configured) accepts the
2098
+ * event. No filter for the resolved kind => allow. compileSourceFilter ok:false
2099
+ * (malformed regex despite the schema validator) => fail-open (allow) so a
2100
+ * single bad filter cannot suppress all events of a kind.
2101
+ */
2102
+ const passesCaptureFilter = (event, captureKind, filters) => {
2103
+ const spec = filters[captureKind];
2104
+ if (spec === undefined)
2105
+ return true;
2106
+ const compiled = compileSourceFilter(spec);
2107
+ if (!compiled.ok)
2108
+ return true;
2109
+ return compiled.predicate(event);
2110
+ };
2111
+ const shouldCaptureEvent = (event, settings) => {
2112
+ if (!isKindEnabled(event.kind, settings['capture.enabledKinds'])) {
2113
+ return false;
2114
+ }
2115
+ if (!isUrlAllowed(event.frameUrl, settings['sites.allowlist'], settings['sites.blocklist'])) {
2116
+ return false;
2117
+ }
2118
+ const captureKind = eventKindToCaptureKind(event.kind);
2119
+ if (captureKind === null)
2120
+ return true; // unknown raw kinds default-allow
2121
+ const control = pickMostSpecificReadControl(event.frameUrl, settings['sites.readControls']);
2122
+ if (!isKindAllowedByReadControls(captureKind, control))
2123
+ return false;
2124
+ return passesCaptureFilter(event, captureKind, settings['capture.filters']);
2125
+ };
2126
+
2127
+ const isSettingsEventMessage = (msg) => {
2128
+ if (msg === null || typeof msg !== 'object')
2129
+ return false;
2130
+ const m = msg;
2131
+ return (m['type'] === 'event' &&
2132
+ (m['tool'] === 'settings_snapshot' || m['tool'] === 'settings_changed'));
2133
+ };
2134
+ const HOST_NAME = 'com.pwa_debug.host';
2135
+ const CAPTURES_EVENT_TOOL = 'captures';
2136
+ const installEventSinkListener = (sink) => {
2137
+ chrome.runtime.onMessage.addListener((msg, sender) => {
2138
+ if (!isPageEventSwMessage(msg))
2139
+ return;
2140
+ sink.handle(attachFrameId(msg.event, sender.frameId));
2141
+ });
2142
+ };
2143
+ const logSetupHint = (extId, errorMessage) => {
2144
+ const reason = errorMessage ? `: ${errorMessage}` : '';
2145
+ console.warn(`[pwa-debug/sw] native host not registered for this extension${reason}\n` +
2146
+ `[pwa-debug/sw] To register, ask Claude (or any MCP client) to call:\n` +
2147
+ `[pwa-debug/sw] mcp__pwa_debug__host_register_extension { extension_id: "${extId}" }\n` +
2148
+ `[pwa-debug/sw] Then reload this extension at chrome://extensions and the connect will retry.`);
2149
+ };
2150
+ const connectNativeHost = (sink, portRef, settingsCache) => {
2151
+ const extId = chrome.runtime.id;
2152
+ console.log(`[pwa-debug/sw] connecting to native host: ${HOST_NAME}`);
2153
+ let port;
2154
+ try {
2155
+ port = chrome.runtime.connectNative(HOST_NAME);
2156
+ }
2157
+ catch (e) {
2158
+ logSetupHint(extId, e.message);
2159
+ return;
2160
+ }
2161
+ portRef.current = port;
2162
+ port.onMessage.addListener((msg) => {
2163
+ if (isSwRequestEnvelope(msg)) {
2164
+ routeRequest(msg, { sink }).then((response) => {
2165
+ try {
2166
+ port.postMessage(response);
2167
+ }
2168
+ catch (err) {
2169
+ console.warn('[pwa-debug/sw] postMessage failed:', err.message);
2170
+ }
2171
+ }, (err) => {
2172
+ console.warn('[pwa-debug/sw] routeRequest rejected (should not happen):', err.message);
2173
+ });
2174
+ return;
2175
+ }
2176
+ if (isSettingsEventMessage(msg)) {
2177
+ if (msg.tool === 'settings_snapshot') {
2178
+ const r = settingsCache.applySnapshot(msg.payload);
2179
+ console.log(`[pwa-debug/sw] settings_snapshot applied (${r.applied} keys)`);
2180
+ }
2181
+ else {
2182
+ const r = settingsCache.applyChange(msg.payload);
2183
+ if (!r.applied) {
2184
+ console.warn('[pwa-debug/sw] settings_changed dropped (invalid payload):', msg.payload);
2185
+ }
2186
+ }
2187
+ return;
2188
+ }
2189
+ console.log('[pwa-debug/sw] from host:', msg);
2190
+ });
2191
+ port.onDisconnect.addListener(() => {
2192
+ portRef.current = null;
2193
+ const err = chrome.runtime.lastError;
2194
+ const msg = err?.message ?? '';
2195
+ if (/not found|forbidden|access/i.test(msg)) {
2196
+ logSetupHint(extId, msg);
2197
+ }
2198
+ else if (msg.length > 0) {
2199
+ console.log('[pwa-debug/sw] native port disconnected:', msg);
2200
+ }
2201
+ else {
2202
+ console.log('[pwa-debug/sw] native port disconnected (clean)');
2203
+ }
2204
+ });
2205
+ };
2206
+ const bootstrap = () => {
2207
+ chrome.runtime.onInstalled.addListener((details) => {
2208
+ console.log('[pwa-debug/sw] installed:', details.reason);
2209
+ });
2210
+ console.log(`[pwa-debug/sw] id=${chrome.runtime.id}`);
2211
+ console.log('[pwa-debug/sw] up');
2212
+ const portRef = { current: null };
2213
+ const extensionId = chrome.runtime.id;
2214
+ const sendEventEnvelope = (events) => {
2215
+ const port = portRef.current;
2216
+ if (port === null)
2217
+ return;
2218
+ try {
2219
+ port.postMessage({
2220
+ type: 'event',
2221
+ tool: CAPTURES_EVENT_TOOL,
2222
+ extensionId,
2223
+ payload: { events },
2224
+ });
2225
+ }
2226
+ catch (err) {
2227
+ console.warn('[pwa-debug/sw] event flush postMessage failed:', err.message);
2228
+ }
2229
+ };
2230
+ const settingsCache = createSettingsCache();
2231
+ const sink = createEventSink({
2232
+ logger: (event) => {
2233
+ console.log('[pwa-debug/sw] event', event.kind, event);
2234
+ },
2235
+ forwardEvents: sendEventEnvelope,
2236
+ // Re-reads settings on every event so live host pushes (T3) take effect
2237
+ // without an extension reload (M7 acceptance).
2238
+ shouldRecord: (event) => shouldCaptureEvent(event, settingsCache.getAll()),
2239
+ });
2240
+ installEventSinkListener(sink);
2241
+ createSwLifecycleProducer({ sink });
2242
+ connectNativeHost(sink, portRef, settingsCache);
2243
+ };
2244
+ bootstrap();
2245
+
2246
+ export { bootstrap };
2247
+ //# sourceMappingURL=service-worker.js.map