@wenathlan/extension 1.1.47 → 1.1.49
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 +6 -4
- package/dist/emulation.d.ts +89 -0
- package/dist/emulation.d.ts.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +886 -4
- package/dist/index.js.map +4 -4
- package/dist/memory.d.ts +83 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +51 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +65 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/sessions.d.ts +79 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/types.d.ts +250 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +1413 -8
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +288 -5
- package/extension/dist/pagebridge.js.map +3 -3
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +24 -2
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +375 -2
- package/extension/dist/sidepanel.js.map +3 -3
- package/extension/dist/style.css +4 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -224,6 +224,362 @@ function expireprofilerecords(input) {
|
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// emulation.ts
|
|
228
|
+
var emulationkinds = ["emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission", "blackboxscripts"];
|
|
229
|
+
var browserpermissions = ["geolocation", "notifications", "camera", "microphone", "clipboard-read", "clipboard-write", "midi", "persistent-storage"];
|
|
230
|
+
var permissionstates = ["granted", "denied", "prompt"];
|
|
231
|
+
function familyofkind(kind) {
|
|
232
|
+
if (kind === "emulatedevice") return "device";
|
|
233
|
+
if (kind === "emulatenetwork") return "network";
|
|
234
|
+
if (kind === "emulatelocate") return "location";
|
|
235
|
+
if (kind === "setuseragent") return "agent";
|
|
236
|
+
if (kind === "overridepermission") return "permission";
|
|
237
|
+
if (kind === "blackboxscripts") return "blackbox";
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
function devicepresetof(value) {
|
|
241
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
242
|
+
const entry = value;
|
|
243
|
+
const name = typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : void 0;
|
|
244
|
+
const width = typeof entry.width === "number" && Number.isInteger(entry.width) && entry.width > 0 ? entry.width : void 0;
|
|
245
|
+
const height = typeof entry.height === "number" && Number.isInteger(entry.height) && entry.height > 0 ? entry.height : void 0;
|
|
246
|
+
const pixelratio = typeof entry.pixelratio === "number" && Number.isFinite(entry.pixelratio) && entry.pixelratio > 0 ? entry.pixelratio : void 0;
|
|
247
|
+
if (name === void 0 || width === void 0 || height === void 0 || pixelratio === void 0) return void 0;
|
|
248
|
+
return { name, width, height, pixelratio, mobile: entry.mobile === true };
|
|
249
|
+
}
|
|
250
|
+
function networkpresetof(value) {
|
|
251
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
252
|
+
const entry = value;
|
|
253
|
+
const name = typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : void 0;
|
|
254
|
+
const latency = typeof entry.latency === "number" && Number.isFinite(entry.latency) && entry.latency >= 0 ? entry.latency : void 0;
|
|
255
|
+
const download = typeof entry.download === "number" && Number.isFinite(entry.download) && entry.download >= 0 ? entry.download : void 0;
|
|
256
|
+
const upload = typeof entry.upload === "number" && Number.isFinite(entry.upload) && entry.upload >= 0 ? entry.upload : void 0;
|
|
257
|
+
if (name === void 0 || latency === void 0 || download === void 0 || upload === void 0) return void 0;
|
|
258
|
+
return { name, latency, download, upload, offline: entry.offline === true };
|
|
259
|
+
}
|
|
260
|
+
function locationpresetof(value) {
|
|
261
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
262
|
+
const entry = value;
|
|
263
|
+
const name = typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : void 0;
|
|
264
|
+
const latitude = typeof entry.latitude === "number" && Number.isFinite(entry.latitude) ? entry.latitude : void 0;
|
|
265
|
+
const longitude = typeof entry.longitude === "number" && Number.isFinite(entry.longitude) ? entry.longitude : void 0;
|
|
266
|
+
const accuracy = typeof entry.accuracy === "number" && Number.isFinite(entry.accuracy) && entry.accuracy >= 0 ? entry.accuracy : void 0;
|
|
267
|
+
if (name === void 0 || latitude === void 0 || longitude === void 0 || accuracy === void 0) return void 0;
|
|
268
|
+
if (!locationrangevalid(latitude, longitude)) return void 0;
|
|
269
|
+
return { name, latitude, longitude, accuracy };
|
|
270
|
+
}
|
|
271
|
+
function agentpresetof(value) {
|
|
272
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
273
|
+
const entry = value;
|
|
274
|
+
const name = typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : void 0;
|
|
275
|
+
const useragent = typeof entry.useragent === "string" ? entry.useragent : void 0;
|
|
276
|
+
const platform = typeof entry.platform === "string" && entry.platform.trim() ? entry.platform.trim() : void 0;
|
|
277
|
+
const brands = Array.isArray(entry.brands) ? entry.brands.filter((brand) => typeof brand === "string" && brand.trim().length > 0) : [];
|
|
278
|
+
if (name === void 0 || useragent === void 0 || platform === void 0 || brands.length === 0) return void 0;
|
|
279
|
+
if (!agentgrammarvalid(useragent)) return void 0;
|
|
280
|
+
return { name, useragent, platform, brands: [...new Set(brands)] };
|
|
281
|
+
}
|
|
282
|
+
function permissiongrantof(value) {
|
|
283
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
284
|
+
const entry = value;
|
|
285
|
+
const name = typeof entry.name === "string" && browserpermissions.includes(entry.name) ? entry.name : void 0;
|
|
286
|
+
const state = typeof entry.state === "string" && permissionstates.includes(entry.state) ? entry.state : void 0;
|
|
287
|
+
if (name === void 0 || state === void 0) return void 0;
|
|
288
|
+
return { name, state, runscope: entry.runscope !== false };
|
|
289
|
+
}
|
|
290
|
+
function blackboxruleof(value) {
|
|
291
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
292
|
+
const entry = value;
|
|
293
|
+
const urlpatterns = Array.isArray(entry.urlpatterns) ? entry.urlpatterns.filter((pattern) => typeof pattern === "string" && /^https:\/\//.test(pattern)) : [];
|
|
294
|
+
const tracescope = entry.tracescope;
|
|
295
|
+
if (urlpatterns.length === 0) return void 0;
|
|
296
|
+
if (tracescope !== "profiles" && tracescope !== "traces" && tracescope !== "both") return void 0;
|
|
297
|
+
return { urlpatterns: [...new Set(urlpatterns)], tracescope };
|
|
298
|
+
}
|
|
299
|
+
function revertplanof(value) {
|
|
300
|
+
const steps = Array.isArray(value) ? value.filter((step) => typeof step === "string" && step.trim().length > 0) : [];
|
|
301
|
+
return steps.length > 0 ? steps : void 0;
|
|
302
|
+
}
|
|
303
|
+
function newlayer(input) {
|
|
304
|
+
return { id: input.id, runid: input.runid, stepid: input.stepid, family: input.family, name: input.name, originscope: input.originscope, appliedat: input.at, ...input.prior !== void 0 ? { prior: input.prior } : {}, revertplan: [...input.revertplan] };
|
|
305
|
+
}
|
|
306
|
+
function emulationstateof(input) {
|
|
307
|
+
return { runid: input.runid, tabid: input.tabid, origin: input.origin, layers: [], updatedat: input.now };
|
|
308
|
+
}
|
|
309
|
+
function applylayer(state, layer, at) {
|
|
310
|
+
const layers = [...state.layers.filter((item) => item.id !== layer.id), layer];
|
|
311
|
+
return { ...state, layers, updatedat: at };
|
|
312
|
+
}
|
|
313
|
+
function revertalllayers(state, at) {
|
|
314
|
+
const reverted = [...state.layers].reverse().filter((layer) => layer.revertedat === void 0);
|
|
315
|
+
const layers = state.layers.map((layer) => layer.revertedat === void 0 ? { ...layer, revertedat: at } : layer);
|
|
316
|
+
return { state: { ...state, layers, updatedat: at }, reverted };
|
|
317
|
+
}
|
|
318
|
+
function activelayers(state) {
|
|
319
|
+
return state ? state.layers.filter((layer) => layer.revertedat === void 0) : [];
|
|
320
|
+
}
|
|
321
|
+
function layernames(state) {
|
|
322
|
+
return activelayers(state).map((layer) => layer.name);
|
|
323
|
+
}
|
|
324
|
+
function locationrangevalid(latitude, longitude) {
|
|
325
|
+
return Number.isFinite(latitude) && Number.isFinite(longitude) && latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180;
|
|
326
|
+
}
|
|
327
|
+
function agentgrammarvalid(useragent) {
|
|
328
|
+
const text2 = useragent.trim();
|
|
329
|
+
if (text2.length === 0 || text2.length > 512) return false;
|
|
330
|
+
if (/[\r\n]/.test(text2)) return false;
|
|
331
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._+\-()/:; ,]*$/.test(text2)) return false;
|
|
332
|
+
return /\/\d/.test(text2) || /\d+\.\d+/.test(text2);
|
|
333
|
+
}
|
|
334
|
+
function permissiongrade(name) {
|
|
335
|
+
return name === "geolocation" || name === "camera" || name === "microphone" || name === "notifications" ? "powerful" : "standard";
|
|
336
|
+
}
|
|
337
|
+
function expirelayers(state, retention, now) {
|
|
338
|
+
if (retention === void 0) return state;
|
|
339
|
+
const layers = state.layers.map((layer) => {
|
|
340
|
+
if (layer.revertedat === void 0 || layer.prior === void 0 || layer.priorexpired === true) return layer;
|
|
341
|
+
if (now - layer.revertedat <= retention) return layer;
|
|
342
|
+
const { prior, ...metadata } = layer;
|
|
343
|
+
void prior;
|
|
344
|
+
return { ...metadata, priorexpired: true };
|
|
345
|
+
});
|
|
346
|
+
return { ...state, layers, updatedat: now };
|
|
347
|
+
}
|
|
348
|
+
function exportpresetlibrary(input) {
|
|
349
|
+
return { version: 1, devices: [...input.devices], networks: [...input.networks], locations: [...input.locations], agents: [...input.agents], exportedat: input.now };
|
|
350
|
+
}
|
|
351
|
+
function importpresetlibrary(value) {
|
|
352
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
353
|
+
const entry = value;
|
|
354
|
+
const devices = (Array.isArray(entry.devices) ? entry.devices : []).flatMap((preset) => {
|
|
355
|
+
const parsed = devicepresetof(preset);
|
|
356
|
+
return parsed !== void 0 ? [parsed] : [];
|
|
357
|
+
});
|
|
358
|
+
const networks = (Array.isArray(entry.networks) ? entry.networks : []).flatMap((preset) => {
|
|
359
|
+
const parsed = networkpresetof(preset);
|
|
360
|
+
return parsed !== void 0 ? [parsed] : [];
|
|
361
|
+
});
|
|
362
|
+
const locations = (Array.isArray(entry.locations) ? entry.locations : []).flatMap((preset) => {
|
|
363
|
+
const parsed = locationpresetof(preset);
|
|
364
|
+
return parsed !== void 0 ? [parsed] : [];
|
|
365
|
+
});
|
|
366
|
+
const agents = (Array.isArray(entry.agents) ? entry.agents : []).flatMap((preset) => {
|
|
367
|
+
const parsed = agentpresetof(preset);
|
|
368
|
+
return parsed !== void 0 ? [parsed] : [];
|
|
369
|
+
});
|
|
370
|
+
if (devices.length + networks.length + locations.length + agents.length === 0) return void 0;
|
|
371
|
+
return { version: typeof entry.version === "number" && Number.isInteger(entry.version) && entry.version >= 1 ? entry.version : 1, devices, networks, locations, agents, exportedat: typeof entry.exportedat === "number" ? entry.exportedat : Date.now() };
|
|
372
|
+
}
|
|
373
|
+
function locationconsentcovers(origin, latitude, longitude, consents) {
|
|
374
|
+
return consents.some((consent) => consent.origin === origin && consent.approved === true && consent.revokedat === void 0 && consent.latitude === latitude && consent.longitude === longitude);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// sessions.ts
|
|
378
|
+
var sessionkinds = ["persiststate", "capturesession", "restoresession", "namedsessions", "diffsessions", "searchsessions", "exportsessions", "importsessions"];
|
|
379
|
+
var sessionfileversion = 1;
|
|
380
|
+
var snapshotsections = ["tabs", "scroll", "forms", "storage", "cookies"];
|
|
381
|
+
var searchfields = ["urls", "titles", "names", "text"];
|
|
382
|
+
function checksumtext(payload) {
|
|
383
|
+
let hash = 2166136261;
|
|
384
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
385
|
+
hash ^= payload.charCodeAt(index);
|
|
386
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
387
|
+
}
|
|
388
|
+
return hash.toString(16).padStart(8, "0");
|
|
389
|
+
}
|
|
390
|
+
function taskstatechecksum(runid, stepcursor, outputs) {
|
|
391
|
+
return checksumtext(`${runid}:${stepcursor}:${outputs.length}:${outputs.map((output) => `${output.stepid}:${output.ok}:${output.summary.length}`).join("|")}`);
|
|
392
|
+
}
|
|
393
|
+
function taskstateof(input) {
|
|
394
|
+
return { runid: input.runid, stepcursor: input.stepcursor, outputs: input.outputs, checkpointat: input.checkpointat, checksum: taskstatechecksum(input.runid, input.stepcursor, input.outputs) };
|
|
395
|
+
}
|
|
396
|
+
function taskstatevalid(state) {
|
|
397
|
+
if (!state || typeof state.runid !== "string" || !state.runid.trim()) return false;
|
|
398
|
+
if (typeof state.stepcursor !== "number" || !Number.isInteger(state.stepcursor) || state.stepcursor < 0) return false;
|
|
399
|
+
if (typeof state.checkpointat !== "number" || !Number.isFinite(state.checkpointat)) return false;
|
|
400
|
+
if (!Array.isArray(state.outputs)) return false;
|
|
401
|
+
return state.checksum === taskstatechecksum(state.runid, state.stepcursor, state.outputs);
|
|
402
|
+
}
|
|
403
|
+
function sessiontabof(value) {
|
|
404
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
405
|
+
const candidate = value;
|
|
406
|
+
if (typeof candidate.url !== "string" || !candidate.url.trim()) return void 0;
|
|
407
|
+
if (typeof candidate.title !== "string") return void 0;
|
|
408
|
+
if (typeof candidate.index !== "number" || !Number.isInteger(candidate.index) || candidate.index < 0) return void 0;
|
|
409
|
+
const scrollx = typeof candidate.scrollx === "number" && Number.isFinite(candidate.scrollx) ? candidate.scrollx : 0;
|
|
410
|
+
const scrolly = typeof candidate.scrolly === "number" && Number.isFinite(candidate.scrolly) ? candidate.scrolly : 0;
|
|
411
|
+
const forms = Array.isArray(candidate.forms) ? candidate.forms.flatMap((entry) => {
|
|
412
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
|
|
413
|
+
const form = entry;
|
|
414
|
+
if (typeof form.selector !== "string" || !form.selector.trim()) return [];
|
|
415
|
+
return [{ selector: form.selector, value: typeof form.value === "string" ? form.value : "" }];
|
|
416
|
+
}) : [];
|
|
417
|
+
return { url: candidate.url, title: candidate.title, index: candidate.index, scrollx, scrolly, forms };
|
|
418
|
+
}
|
|
419
|
+
function autointervalof(value) {
|
|
420
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
421
|
+
const candidate = value;
|
|
422
|
+
if (typeof candidate.period !== "number" || !Number.isFinite(candidate.period) || candidate.period <= 0) return void 0;
|
|
423
|
+
if (typeof candidate.maxsnapshots !== "number" || !Number.isInteger(candidate.maxsnapshots) || candidate.maxsnapshots < 1) return void 0;
|
|
424
|
+
if (typeof candidate.expiry !== "number" || !Number.isFinite(candidate.expiry) || candidate.expiry < 0) return void 0;
|
|
425
|
+
return { period: candidate.period, maxsnapshots: candidate.maxsnapshots, expiry: candidate.expiry };
|
|
426
|
+
}
|
|
427
|
+
function snapshotplanof(value) {
|
|
428
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
429
|
+
const candidate = value;
|
|
430
|
+
if (candidate.scope !== "tab" && candidate.scope !== "run" && candidate.scope !== "all") return void 0;
|
|
431
|
+
const sections = Array.isArray(candidate.sections) ? candidate.sections.flatMap((section) => typeof section === "string" && snapshotsections.includes(section) ? [section] : []) : [];
|
|
432
|
+
if (sections.length === 0) return void 0;
|
|
433
|
+
if (typeof candidate.captures !== "boolean") return void 0;
|
|
434
|
+
const auto = candidate.auto === void 0 ? void 0 : autointervalof(candidate.auto);
|
|
435
|
+
if (candidate.auto !== void 0 && auto === void 0) return void 0;
|
|
436
|
+
return { scope: candidate.scope, sections, captures: candidate.captures, ...auto !== void 0 ? { auto } : {} };
|
|
437
|
+
}
|
|
438
|
+
function restoreplanof(value) {
|
|
439
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
440
|
+
const candidate = value;
|
|
441
|
+
if (candidate.tabpolicy !== "reopen" && candidate.tabpolicy !== "skip") return void 0;
|
|
442
|
+
if (candidate.formpolicy !== "restore" && candidate.formpolicy !== "skip") return void 0;
|
|
443
|
+
if (candidate.capturepolicy !== "link" && candidate.capturepolicy !== "skip") return void 0;
|
|
444
|
+
return { tabpolicy: candidate.tabpolicy, formpolicy: candidate.formpolicy, capturepolicy: candidate.capturepolicy };
|
|
445
|
+
}
|
|
446
|
+
function searchqueryof(value) {
|
|
447
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
448
|
+
const candidate = value;
|
|
449
|
+
const terms = Array.isArray(candidate.terms) ? candidate.terms.flatMap((term) => typeof term === "string" && term.trim() ? [term.trim()] : []) : [];
|
|
450
|
+
if (terms.length === 0) return void 0;
|
|
451
|
+
const fields = Array.isArray(candidate.fields) ? candidate.fields.flatMap((field) => typeof field === "string" && searchfields.includes(field) ? [field] : []) : [...searchfields];
|
|
452
|
+
if (fields.length === 0) return void 0;
|
|
453
|
+
const from = typeof candidate.from === "number" && Number.isFinite(candidate.from) ? candidate.from : void 0;
|
|
454
|
+
const to = typeof candidate.to === "number" && Number.isFinite(candidate.to) ? candidate.to : void 0;
|
|
455
|
+
if (from !== void 0 && to !== void 0 && from > to) return void 0;
|
|
456
|
+
return { terms, fields, ...from !== void 0 ? { from } : {}, ...to !== void 0 ? { to } : {} };
|
|
457
|
+
}
|
|
458
|
+
function sessionfolderof(value) {
|
|
459
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
460
|
+
const candidate = value;
|
|
461
|
+
if (typeof candidate.name !== "string" || !candidate.name.trim()) return void 0;
|
|
462
|
+
const parent = typeof candidate.parent === "string" && candidate.parent.trim() ? candidate.parent : void 0;
|
|
463
|
+
const tags = Array.isArray(candidate.tags) ? candidate.tags.flatMap((tag) => typeof tag === "string" && tag.trim() ? [tag] : []) : [];
|
|
464
|
+
return { name: candidate.name, ...parent !== void 0 ? { parent } : {}, tags };
|
|
465
|
+
}
|
|
466
|
+
function newsessionrecord(input) {
|
|
467
|
+
return { id: input.id, name: input.name, createdat: input.createdat, tabs: input.tabs, captures: input.captures, storage: input.storage, cookies: input.cookies, ...input.folder !== void 0 ? { folder: input.folder } : {}, tags: input.tags ?? [], ...input.auto === true ? { auto: true } : {} };
|
|
468
|
+
}
|
|
469
|
+
function diffsessionrecords(left, right) {
|
|
470
|
+
const changes = [];
|
|
471
|
+
const leftbyindex = new Map(left.tabs.map((tab) => [tab.index, tab]));
|
|
472
|
+
const rightbyindex = new Map(right.tabs.map((tab) => [tab.index, tab]));
|
|
473
|
+
for (const tab of right.tabs) {
|
|
474
|
+
const prior = leftbyindex.get(tab.index);
|
|
475
|
+
if (!prior) {
|
|
476
|
+
changes.push({ class: "added", subject: "tab", detail: `Tab ${tab.index} added: ${tab.url}` });
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (prior.url !== tab.url) changes.push({ class: "changed", subject: "url", detail: `Tab ${tab.index} moved from ${prior.url} to ${tab.url}` });
|
|
480
|
+
if (prior.title !== tab.title) changes.push({ class: "changed", subject: "tab", detail: `Tab ${tab.index} title changed from "${prior.title}" to "${tab.title}"` });
|
|
481
|
+
const priorforms = new Map(prior.forms.map((form) => [form.selector, form.value]));
|
|
482
|
+
for (const form of tab.forms) {
|
|
483
|
+
const before = priorforms.get(form.selector);
|
|
484
|
+
if (before === void 0) {
|
|
485
|
+
changes.push({ class: "added", subject: "form", detail: `Form field ${form.selector} of tab ${tab.index} added with a value` });
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (before !== form.value) changes.push({ class: "changed", subject: "form", detail: `Form field ${form.selector} of tab ${tab.index} changed its captured value` });
|
|
489
|
+
}
|
|
490
|
+
for (const form of prior.forms) if (!tab.forms.some((entry) => entry.selector === form.selector)) changes.push({ class: "removed", subject: "form", detail: `Form field ${form.selector} of tab ${tab.index} removed` });
|
|
491
|
+
}
|
|
492
|
+
for (const tab of left.tabs) if (!rightbyindex.has(tab.index)) changes.push({ class: "removed", subject: "tab", detail: `Tab ${tab.index} removed: ${tab.url}` });
|
|
493
|
+
const leftstorage = new Map(left.storage.map((entry) => [entry.origin, entry]));
|
|
494
|
+
for (const entry of right.storage) {
|
|
495
|
+
const prior = leftstorage.get(entry.origin);
|
|
496
|
+
if (!prior) {
|
|
497
|
+
changes.push({ class: "added", subject: "storage", detail: `Local storage of ${entry.origin} captured with ${entry.keys.length} keys` });
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (prior.keys.join("|") !== entry.keys.join("|") || prior.values.join("|") !== entry.values.join("|")) changes.push({ class: "changed", subject: "storage", detail: `Local storage of ${entry.origin} changed its captured keys or values` });
|
|
501
|
+
}
|
|
502
|
+
for (const entry of left.storage) if (!right.storage.some((candidate) => candidate.origin === entry.origin)) changes.push({ class: "removed", subject: "storage", detail: `Local storage of ${entry.origin} left the capture` });
|
|
503
|
+
return changes;
|
|
504
|
+
}
|
|
505
|
+
function newsessiondiff(input) {
|
|
506
|
+
return { id: input.id, leftid: input.left.id, rightid: input.right.id, changes: diffsessionrecords(input.left, input.right), at: input.at };
|
|
507
|
+
}
|
|
508
|
+
function searchsessionrecords(query, records) {
|
|
509
|
+
const matches = [];
|
|
510
|
+
for (const record2 of records) {
|
|
511
|
+
if (query.from !== void 0 && record2.createdat < query.from) continue;
|
|
512
|
+
if (query.to !== void 0 && record2.createdat > query.to) continue;
|
|
513
|
+
const haystacks = [
|
|
514
|
+
{ field: "urls", text: record2.tabs.map((tab) => tab.url).join(" ") },
|
|
515
|
+
{ field: "titles", text: record2.tabs.map((tab) => tab.title).join(" ") },
|
|
516
|
+
{ field: "names", text: [record2.name, record2.folder ?? "", ...record2.tags].join(" ") },
|
|
517
|
+
{ field: "text", text: record2.tabs.flatMap((tab) => tab.forms.map((form) => form.value)).join(" ") }
|
|
518
|
+
];
|
|
519
|
+
for (const haystack of haystacks) {
|
|
520
|
+
if (!query.fields.includes(haystack.field)) continue;
|
|
521
|
+
const lower = haystack.text.toLowerCase();
|
|
522
|
+
for (const term of query.terms) {
|
|
523
|
+
const at = lower.indexOf(term.toLowerCase());
|
|
524
|
+
if (at < 0) continue;
|
|
525
|
+
const start = Math.max(0, at - 30);
|
|
526
|
+
matches.push({ sessionid: record2.id, field: haystack.field, term, at: record2.createdat, excerpt: haystack.text.slice(start, start + 80).trim() });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return matches;
|
|
531
|
+
}
|
|
532
|
+
function exportsessionfile(records, now) {
|
|
533
|
+
const recordids = records.map((record2) => record2.id);
|
|
534
|
+
const payload = JSON.stringify(records);
|
|
535
|
+
return { formatversion: sessionfileversion, records, recordids, bytesize: payload.length, checksum: checksumtext(`${sessionfileversion}:${recordids.join(",")}:${payload.length}`), exportedat: now };
|
|
536
|
+
}
|
|
537
|
+
function importsessionfile(value) {
|
|
538
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
539
|
+
const candidate = value;
|
|
540
|
+
if (candidate.formatversion !== sessionfileversion) return void 0;
|
|
541
|
+
const records = Array.isArray(candidate.records) ? candidate.records.flatMap((record2) => sessionrecordvalid(record2) ? [record2] : []) : [];
|
|
542
|
+
if (records.length === 0) return void 0;
|
|
543
|
+
if (!Array.isArray(candidate.recordids) || candidate.recordids.length !== records.length || !candidate.recordids.every((id, index) => id === records[index]?.id)) return void 0;
|
|
544
|
+
const bytesize = typeof candidate.bytesize === "number" && Number.isFinite(candidate.bytesize) ? candidate.bytesize : -1;
|
|
545
|
+
if (bytesize < 0) return void 0;
|
|
546
|
+
const checksum2 = typeof candidate.checksum === "string" ? candidate.checksum : "";
|
|
547
|
+
if (checksum2 !== checksumtext(`${sessionfileversion}:${candidate.recordids.join(",")}:${bytesize}`)) return void 0;
|
|
548
|
+
return { formatversion: sessionfileversion, records, recordids: candidate.recordids, bytesize, checksum: checksum2, exportedat: typeof candidate.exportedat === "number" && Number.isFinite(candidate.exportedat) ? candidate.exportedat : 0 };
|
|
549
|
+
}
|
|
550
|
+
function sessionrecordvalid(value) {
|
|
551
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
552
|
+
const candidate = value;
|
|
553
|
+
if (typeof candidate.id !== "string" || !candidate.id.trim()) return false;
|
|
554
|
+
if (typeof candidate.name !== "string" || !candidate.name.trim()) return false;
|
|
555
|
+
if (typeof candidate.createdat !== "number" || !Number.isFinite(candidate.createdat)) return false;
|
|
556
|
+
if (!Array.isArray(candidate.tabs) || !candidate.tabs.every((tab) => sessiontabof(tab) !== void 0)) return false;
|
|
557
|
+
if (!Array.isArray(candidate.captures) || !candidate.captures.every((id) => typeof id === "string")) return false;
|
|
558
|
+
if (!Array.isArray(candidate.tags) || !candidate.tags.every((tag) => typeof tag === "string")) return false;
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
function expiresessions(records, retention, now) {
|
|
562
|
+
if (retention === void 0 || !Number.isFinite(retention)) return records;
|
|
563
|
+
return records.map((record2) => {
|
|
564
|
+
if (record2.sectionsexpired || now - record2.createdat < retention) return record2;
|
|
565
|
+
return { id: record2.id, name: record2.name, createdat: record2.createdat, tabs: [], captures: record2.captures, storage: [], cookies: [], ...record2.folder !== void 0 ? { folder: record2.folder } : {}, tags: record2.tags, ...record2.auto === true ? { auto: true } : {}, ...record2.restoredat !== void 0 ? { restoredat: record2.restoredat } : {}, sectionsexpired: true };
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
function filteredsessions(records, filter) {
|
|
569
|
+
return records.filter((record2) => {
|
|
570
|
+
if (filter.name !== void 0 && !record2.name.toLowerCase().includes(filter.name.toLowerCase())) return false;
|
|
571
|
+
if (filter.folder !== void 0 && record2.folder !== filter.folder) return false;
|
|
572
|
+
if (filter.from !== void 0 && record2.createdat < filter.from) return false;
|
|
573
|
+
if (filter.to !== void 0 && record2.createdat > filter.to) return false;
|
|
574
|
+
return true;
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function crashinterrupted(state, plansteps, now) {
|
|
578
|
+
if (!state) return void 0;
|
|
579
|
+
if (state.stepcursor >= plansteps) return state;
|
|
580
|
+
return { ...state, interrupted: true, crashat: state.crashat ?? now };
|
|
581
|
+
}
|
|
582
|
+
|
|
227
583
|
// memory.ts
|
|
228
584
|
var sessionmemory = class {
|
|
229
585
|
constructor(adapter) {
|
|
@@ -1594,6 +1950,169 @@ var sessionmemory = class {
|
|
|
1594
1950
|
await this.adapter.set("sourcemapconsents", updated);
|
|
1595
1951
|
return revoked;
|
|
1596
1952
|
}
|
|
1953
|
+
/** Stores the emulation state of one run keyed by its run id; the reverted layer prior states expire after the user configured retention window while the layer history always survives. */
|
|
1954
|
+
async setemulationstate(state) {
|
|
1955
|
+
const retention = (await this.getsettings())?.emulationretention;
|
|
1956
|
+
await this.adapter.set(`emulationstate${state.runid}`, expirelayers(state, retention, Date.now()));
|
|
1957
|
+
}
|
|
1958
|
+
/** Returns the persisted emulation state of one run so the layers survive service worker restarts. */
|
|
1959
|
+
async getemulationstate(runid) {
|
|
1960
|
+
return this.adapter.get(`emulationstate${runid}`);
|
|
1961
|
+
}
|
|
1962
|
+
/** Returns the active and past layers of one run, newest last in apply order; the listlayers accessor of the emulation memory. */
|
|
1963
|
+
async listlayers(runid) {
|
|
1964
|
+
const state = await this.getemulationstate(runid);
|
|
1965
|
+
return state?.layers ?? [];
|
|
1966
|
+
}
|
|
1967
|
+
/** Stores one user curated device preset by its name so the preset library stays user data instead of a hardcoded list. */
|
|
1968
|
+
async setdevicepreset(preset) {
|
|
1969
|
+
const records = (await this.adapter.get("devicepresets") ?? []).filter((item) => item.name !== preset.name);
|
|
1970
|
+
await this.adapter.set("devicepresets", [...records, preset]);
|
|
1971
|
+
}
|
|
1972
|
+
/** Returns every user curated device preset. */
|
|
1973
|
+
async getdevicepresets() {
|
|
1974
|
+
return await this.adapter.get("devicepresets") ?? [];
|
|
1975
|
+
}
|
|
1976
|
+
/** Stores one user curated network preset by its name with editable values. */
|
|
1977
|
+
async setnetworkpreset(preset) {
|
|
1978
|
+
const records = (await this.adapter.get("networkpresets") ?? []).filter((item) => item.name !== preset.name);
|
|
1979
|
+
await this.adapter.set("networkpresets", [...records, preset]);
|
|
1980
|
+
}
|
|
1981
|
+
/** Returns every user curated network preset. */
|
|
1982
|
+
async getnetworkpresets() {
|
|
1983
|
+
return await this.adapter.get("networkpresets") ?? [];
|
|
1984
|
+
}
|
|
1985
|
+
/** Stores one user curated location preset by its name. */
|
|
1986
|
+
async setlocationpreset(preset) {
|
|
1987
|
+
const records = (await this.adapter.get("locationpresets") ?? []).filter((item) => item.name !== preset.name);
|
|
1988
|
+
await this.adapter.set("locationpresets", [...records, preset]);
|
|
1989
|
+
}
|
|
1990
|
+
/** Returns every user curated location preset. */
|
|
1991
|
+
async getlocationpresets() {
|
|
1992
|
+
return await this.adapter.get("locationpresets") ?? [];
|
|
1993
|
+
}
|
|
1994
|
+
/** Stores one user curated agent preset by its name. */
|
|
1995
|
+
async setagentpreset(preset) {
|
|
1996
|
+
const records = (await this.adapter.get("agentpresets") ?? []).filter((item) => item.name !== preset.name);
|
|
1997
|
+
await this.adapter.set("agentpresets", [...records, preset]);
|
|
1998
|
+
}
|
|
1999
|
+
/** Returns every user curated agent preset. */
|
|
2000
|
+
async getagentpresets() {
|
|
2001
|
+
return await this.adapter.get("agentpresets") ?? [];
|
|
2002
|
+
}
|
|
2003
|
+
/** Replaces the blackbox rule set of one origin so third party script blackboxing stays scoped per origin. */
|
|
2004
|
+
async setblackboxrules(origin, rules) {
|
|
2005
|
+
const records = (await this.adapter.get("blackboxrules") ?? []).filter((item) => item.origin !== origin);
|
|
2006
|
+
await this.adapter.set("blackboxrules", [...records, { origin, rules }]);
|
|
2007
|
+
}
|
|
2008
|
+
/** Returns every stored blackbox rule set with its origin. */
|
|
2009
|
+
async getblackboxrules() {
|
|
2010
|
+
return await this.adapter.get("blackboxrules") ?? [];
|
|
2011
|
+
}
|
|
2012
|
+
/** Records one permission override of a run with its prior state captured for the exact restore. */
|
|
2013
|
+
async addpermissionoverride(record2) {
|
|
2014
|
+
const records = (await this.adapter.get("permissionoverrides") ?? []).filter((item) => item.id !== record2.id);
|
|
2015
|
+
await this.adapter.set("permissionoverrides", [record2, ...records]);
|
|
2016
|
+
}
|
|
2017
|
+
/** Returns the permission override history with restore states, newest first. */
|
|
2018
|
+
async getpermissionoverrides() {
|
|
2019
|
+
return await this.adapter.get("permissionoverrides") ?? [];
|
|
2020
|
+
}
|
|
2021
|
+
/** Stores one location consent decision per origin, replacing the previous decision of its id. */
|
|
2022
|
+
async setlocationconsent(consent) {
|
|
2023
|
+
const records = (await this.adapter.get("locationconsents") ?? []).filter((item) => item.id !== consent.id);
|
|
2024
|
+
await this.adapter.set("locationconsents", [consent, ...records]);
|
|
2025
|
+
}
|
|
2026
|
+
/** Returns every location consent decision, newest first. */
|
|
2027
|
+
async getlocationconsents() {
|
|
2028
|
+
return await this.adapter.get("locationconsents") ?? [];
|
|
2029
|
+
}
|
|
2030
|
+
/** Returns the persisted task state checkpoint of one run so the run resumes after a service worker restart. */
|
|
2031
|
+
async gettaskstate(runid) {
|
|
2032
|
+
return this.adapter.get(`taskstate${runid}`);
|
|
2033
|
+
}
|
|
2034
|
+
/** Persists one task state checkpoint per run with its corruption checksum. */
|
|
2035
|
+
async settaskstate(state) {
|
|
2036
|
+
return this.adapter.set(`taskstate${state.runid}`, state);
|
|
2037
|
+
}
|
|
2038
|
+
/** Returns the session event history with timestamps, newest first. */
|
|
2039
|
+
async getsessionevents() {
|
|
2040
|
+
return await this.adapter.get("sessionevents") ?? [];
|
|
2041
|
+
}
|
|
2042
|
+
/** Records one session event of the run with its timestamp and detail. */
|
|
2043
|
+
async addsessionevent(event) {
|
|
2044
|
+
const records = await this.getsessionevents();
|
|
2045
|
+
await this.adapter.set("sessionevents", [event, ...records]);
|
|
2046
|
+
}
|
|
2047
|
+
/** Returns every saved session record with its sections, newest first. */
|
|
2048
|
+
async getsessionrecords() {
|
|
2049
|
+
return await this.adapter.get("sessionrecords") ?? [];
|
|
2050
|
+
}
|
|
2051
|
+
/** Adds one saved session record to the library. */
|
|
2052
|
+
async addsessionrecord(record2) {
|
|
2053
|
+
const records = await this.getsessionrecords();
|
|
2054
|
+
await this.adapter.set("sessionrecords", [record2, ...records]);
|
|
2055
|
+
}
|
|
2056
|
+
/** Replaces one saved session record by its id after a filing or restore touches it. */
|
|
2057
|
+
async updatesessionrecord(record2) {
|
|
2058
|
+
const records = await this.getsessionrecords();
|
|
2059
|
+
await this.adapter.set("sessionrecords", records.map((item) => item.id === record2.id ? record2 : item));
|
|
2060
|
+
}
|
|
2061
|
+
/** Lists saved sessions filtered by name substring, folder and time window; the filter stays a user choice with no result cap. */
|
|
2062
|
+
async listsessions(filter) {
|
|
2063
|
+
return filteredsessions(await this.getsessionrecords(), filter);
|
|
2064
|
+
}
|
|
2065
|
+
/** Returns one saved session with every section; an expired record carries its metadata only. */
|
|
2066
|
+
async getsessionrecord(id) {
|
|
2067
|
+
return (await this.getsessionrecords()).find((record2) => record2.id === id);
|
|
2068
|
+
}
|
|
2069
|
+
/** Runs the reviewed search query across every stored session and returns the matches with their session ids and time windows. */
|
|
2070
|
+
async searchmemory(query) {
|
|
2071
|
+
return searchsessionrecords(query, await this.getsessionrecords());
|
|
2072
|
+
}
|
|
2073
|
+
/** Returns the folder tree of the session library. */
|
|
2074
|
+
async getsessionfolders() {
|
|
2075
|
+
return await this.adapter.get("sessionfolders") ?? [];
|
|
2076
|
+
}
|
|
2077
|
+
/** Replaces the folder tree after a reviewed filing adds or moves one folder. */
|
|
2078
|
+
async setsessionfolders(folders) {
|
|
2079
|
+
return this.adapter.set("sessionfolders", folders);
|
|
2080
|
+
}
|
|
2081
|
+
/** Returns every stored session diff result, newest first. */
|
|
2082
|
+
async getsessiondiffs() {
|
|
2083
|
+
return await this.adapter.get("sessiondiffs") ?? [];
|
|
2084
|
+
}
|
|
2085
|
+
/** Stores one session diff result for later review. */
|
|
2086
|
+
async addsessiondiff(diff) {
|
|
2087
|
+
const records = await this.getsessiondiffs();
|
|
2088
|
+
await this.adapter.set("sessiondiffs", [diff, ...records]);
|
|
2089
|
+
}
|
|
2090
|
+
/** Returns the persisted auto snapshot state with the reviewed interval, the last snapshot time and the snapshot count. */
|
|
2091
|
+
async getautosnapshot() {
|
|
2092
|
+
return await this.adapter.get("autosnapshot") ?? void 0;
|
|
2093
|
+
}
|
|
2094
|
+
/** Stores the auto snapshot state of the reviewed interval. */
|
|
2095
|
+
async setautosnapshot(state) {
|
|
2096
|
+
return this.adapter.set("autosnapshot", state);
|
|
2097
|
+
}
|
|
2098
|
+
/** Clears the auto snapshot interval so on demand captures stay the only source of records. */
|
|
2099
|
+
async clearautosnapshot() {
|
|
2100
|
+
return this.adapter.set("autosnapshot", null);
|
|
2101
|
+
}
|
|
2102
|
+
/** Expires the heavy sections of saved sessions after the reviewed retention window while the record metadata survives. */
|
|
2103
|
+
async applysessionexpiry(retention, now) {
|
|
2104
|
+
const records = expiresessions(await this.getsessionrecords(), retention, now);
|
|
2105
|
+
await this.adapter.set("sessionrecords", records);
|
|
2106
|
+
return records;
|
|
2107
|
+
}
|
|
2108
|
+
/** Returns the crash marker of a run interrupted by a browser restart. */
|
|
2109
|
+
async getcrashflag() {
|
|
2110
|
+
return await this.adapter.get("crashed") ?? false;
|
|
2111
|
+
}
|
|
2112
|
+
/** Sets the crash marker so the sessions view offers the crash restore inside the consent model. */
|
|
2113
|
+
async setcrashflag(value) {
|
|
2114
|
+
return this.adapter.set("crashed", value);
|
|
2115
|
+
}
|
|
1597
2116
|
};
|
|
1598
2117
|
function mediakindof(record2) {
|
|
1599
2118
|
if ("pages" in record2) return "pdf";
|
|
@@ -2813,9 +3332,9 @@ function consolediff(input) {
|
|
|
2813
3332
|
}
|
|
2814
3333
|
|
|
2815
3334
|
// policy.ts
|
|
2816
|
-
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts", "recordscreen", "captureaudio", "downloadimages", "callrest", "callgraphql", "sendmessage", "blockrequest", "mockresponse", "rewriteheaders", "setcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles", "attachcdp", "detachcdp", "cdpcmd", "overridescript", "heapshot", "profilecpu", "capturesourcemaps"]);
|
|
3335
|
+
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts", "recordscreen", "captureaudio", "downloadimages", "callrest", "callgraphql", "sendmessage", "blockrequest", "mockresponse", "rewriteheaders", "setcookies", "clearcookies", "authflow", "saveapikey", "routeproxy", "postform", "postfiles", "attachcdp", "detachcdp", "cdpcmd", "overridescript", "heapshot", "profilecpu", "capturesourcemaps", "emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission", "restoresession", "exportsessions", "importsessions"]);
|
|
2817
3336
|
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction", "capturebodies", "setbreakpoint", "stepcode", "watchexpr"]);
|
|
2818
|
-
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet", "capturepdf", "captureframe", "readmedia", "readassets", "probestream", "timelapse", "shotcanvas", "convertimage", "makethumbs", "fetchurl", "parsejson", "parsehtml", "opensocket", "waitmessage", "watchrequests", "readheaders", "mapapi", "subscribesse", "longpoll", "extractapi", "readcookies", "watchconsole", "watcherrors", "watchtasks", "watchcdp", "measureflow", "trackmemory", "watchshifts", "traceload", "annotatetrace", "replaytrace"]);
|
|
3337
|
+
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures", "shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet", "capturepdf", "captureframe", "readmedia", "readassets", "probestream", "timelapse", "shotcanvas", "convertimage", "makethumbs", "fetchurl", "parsejson", "parsehtml", "opensocket", "waitmessage", "watchrequests", "readheaders", "mapapi", "subscribesse", "longpoll", "extractapi", "readcookies", "watchconsole", "watcherrors", "watchtasks", "watchcdp", "measureflow", "trackmemory", "watchshifts", "traceload", "annotatetrace", "replaytrace", "blackboxscripts", "persiststate", "capturesession", "namedsessions", "diffsessions", "searchsessions"]);
|
|
2819
3338
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
2820
3339
|
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
2821
3340
|
var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract", "shotelement", "captureframe", "shotcanvas"]);
|
|
@@ -2834,6 +3353,8 @@ var controlactions = /* @__PURE__ */ new Set(["blockrequest", "mockresponse", "r
|
|
|
2834
3353
|
var debugactions = /* @__PURE__ */ new Set(["watchconsole", "watcherrors", "watchtasks"]);
|
|
2835
3354
|
var cdpactions = /* @__PURE__ */ new Set(["attachcdp", "detachcdp", "cdpcmd", "watchcdp", "setbreakpoint", "stepcode", "watchexpr", "overridescript"]);
|
|
2836
3355
|
var profileractions = /* @__PURE__ */ new Set(["measureflow", "heapshot", "trackmemory", "profilecpu", "watchshifts", "traceload", "annotatetrace", "replaytrace", "capturesourcemaps"]);
|
|
3356
|
+
var emulationactions = /* @__PURE__ */ new Set(["emulatedevice", "emulatenetwork", "emulatelocate", "setuseragent", "overridepermission", "blackboxscripts"]);
|
|
3357
|
+
var sessionactions = /* @__PURE__ */ new Set(["persiststate", "capturesession", "restoresession", "namedsessions", "diffsessions", "searchsessions", "exportsessions", "importsessions"]);
|
|
2837
3358
|
var credentialheaders = /* @__PURE__ */ new Set(["authorization", "proxy-authorization", "cookie", "cookie2", "set-cookie", "api-key", "x-api-key", "x-auth-token", "x-session-token", "proxy-authorization"]);
|
|
2838
3359
|
var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
|
|
2839
3360
|
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
@@ -2849,6 +3370,9 @@ function hostpattern(origin) {
|
|
|
2849
3370
|
if (parsed.protocol !== "https:") throw new Error("Only HTTPS origins can be granted.");
|
|
2850
3371
|
return `${parsed.origin}/*`;
|
|
2851
3372
|
}
|
|
3373
|
+
function issessionkind(kind) {
|
|
3374
|
+
return sessionactions.has(kind);
|
|
3375
|
+
}
|
|
2852
3376
|
function isdebugkind(kind) {
|
|
2853
3377
|
return debugactions.has(kind);
|
|
2854
3378
|
}
|
|
@@ -2858,6 +3382,9 @@ function iscdpkind(kind) {
|
|
|
2858
3382
|
function isprofilekind(kind) {
|
|
2859
3383
|
return profileractions.has(kind);
|
|
2860
3384
|
}
|
|
3385
|
+
function isemulationkind(kind) {
|
|
3386
|
+
return emulationactions.has(kind);
|
|
3387
|
+
}
|
|
2861
3388
|
function actionrisk(kind) {
|
|
2862
3389
|
if (!allowedactions.has(kind)) throw new Error("Unsupported browser action.");
|
|
2863
3390
|
if (sensitiveactions.has(kind)) return "sensitive";
|
|
@@ -2884,6 +3411,8 @@ function requiredcapability(kind) {
|
|
|
2884
3411
|
if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
|
|
2885
3412
|
if (kind === "downloadimages") return "downloads";
|
|
2886
3413
|
if (kind === "authflow") return "tabs";
|
|
3414
|
+
if (kind === "capturesession" || kind === "restoresession") return "tabs";
|
|
3415
|
+
if (kind === "exportsessions") return "downloads";
|
|
2887
3416
|
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
2888
3417
|
if (tabscommandactions.has(kind)) return "tabs";
|
|
2889
3418
|
return void 0;
|
|
@@ -4221,6 +4750,165 @@ function breakpointbudgetallowed(active, ceiling) {
|
|
|
4221
4750
|
function breakpointceilingof(settings) {
|
|
4222
4751
|
return settings?.breakpointceiling;
|
|
4223
4752
|
}
|
|
4753
|
+
function emugate(input) {
|
|
4754
|
+
const gate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now: input.now, action: "emulate the run tab" });
|
|
4755
|
+
if (!gate.allowed) return gate;
|
|
4756
|
+
if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "Emulation layers need an approved plan before they apply." };
|
|
4757
|
+
let options = {};
|
|
4758
|
+
try {
|
|
4759
|
+
options = parseoptions(input.step);
|
|
4760
|
+
} catch {
|
|
4761
|
+
options = {};
|
|
4762
|
+
}
|
|
4763
|
+
if (options.reviewed !== true) return { allowed: false, reason: `The ${input.step.kind} layer needs the explicit reviewed flag before any mask applies.` };
|
|
4764
|
+
if (revertplanof(options.revertplan) === void 0) return { allowed: false, reason: `Every ${input.step.kind} layer needs a reviewed revert plan beside it before any mask applies.` };
|
|
4765
|
+
return { allowed: true };
|
|
4766
|
+
}
|
|
4767
|
+
function emulationstackallowed(plan, kind, active) {
|
|
4768
|
+
if (!plan) return { allowed: false, reason: "Layer stacking needs the reviewed plan first." };
|
|
4769
|
+
const listed = plan.steps.filter((step) => step.kind === kind).length;
|
|
4770
|
+
if (active >= listed) return { allowed: false, reason: `The plan lists ${listed} reviewed ${kind} step${listed === 1 ? "" : "s"} and ${active} layer${active === 1 ? "" : "s"} of that family are already active; stacking beyond the reviewed plan is refused.` };
|
|
4771
|
+
return { allowed: true };
|
|
4772
|
+
}
|
|
4773
|
+
function locationconsentgate(origin, latitude, longitude, consents) {
|
|
4774
|
+
if (consents.some((consent) => consent.origin === origin && consent.revokedat !== void 0)) return { allowed: false, reason: `The location consent on ${origin} was revoked; approve a new prompt before the location override runs again.` };
|
|
4775
|
+
if (locationconsentcovers(origin, latitude, longitude, consents)) return { allowed: true };
|
|
4776
|
+
return { allowed: false, reason: `The location override of ${latitude}, ${longitude} on ${origin} needs the reviewed location consent first; approve the prompt with the coordinates shown in the review panel.` };
|
|
4777
|
+
}
|
|
4778
|
+
function validateemulationgrammar(step, options) {
|
|
4779
|
+
const kind = step.kind;
|
|
4780
|
+
if (revertplanof(options.revertplan) === void 0) return { allowed: false, reason: `Every ${kind} layer needs a reviewed revert plan before any mask applies.` };
|
|
4781
|
+
if (kind === "emulatedevice") {
|
|
4782
|
+
const preset = devicepresetof(options.device);
|
|
4783
|
+
if (!preset) return { allowed: false, reason: "The device layer needs a reviewed preset with a name, positive integer width and height and a positive pixel ratio." };
|
|
4784
|
+
if (options.reload !== void 0 && typeof options.reload !== "boolean") return { allowed: false, reason: "The reviewed reload flag must be a boolean; the page reloads only when the reviewed plan asks." };
|
|
4785
|
+
return { allowed: true };
|
|
4786
|
+
}
|
|
4787
|
+
if (kind === "emulatenetwork") {
|
|
4788
|
+
const preset = networkpresetof(options.network);
|
|
4789
|
+
if (!preset) return { allowed: false, reason: "The network layer needs a reviewed preset with a name and zero or positive latency, download and upload bounds." };
|
|
4790
|
+
if (options.window !== void 0 && (typeof options.window !== "number" || !Number.isFinite(options.window) || options.window < 0)) return { allowed: false, reason: "The reviewed offline window must be zero or a positive number of milliseconds with no code ceiling." };
|
|
4791
|
+
return { allowed: true };
|
|
4792
|
+
}
|
|
4793
|
+
if (kind === "emulatelocate") {
|
|
4794
|
+
const preset = locationpresetof(options.location);
|
|
4795
|
+
if (!preset) return { allowed: false, reason: "The location layer needs a reviewed preset with a name, a latitude inside -90 and 90, a longitude inside -180 and 180 and a zero or positive accuracy radius." };
|
|
4796
|
+
if (!locationrangevalid(preset.latitude, preset.longitude)) return { allowed: false, reason: "The reviewed latitude must stay inside -90 and 90 degrees and the longitude inside -180 and 180 degrees." };
|
|
4797
|
+
return { allowed: true };
|
|
4798
|
+
}
|
|
4799
|
+
if (kind === "setuseragent") {
|
|
4800
|
+
const preset = agentpresetof(options.agent);
|
|
4801
|
+
if (!preset) return { allowed: false, reason: "The agent layer needs a reviewed preset with a user agent string of the reviewed grammar, a platform and a non-empty brand list." };
|
|
4802
|
+
if (!agentgrammarvalid(preset.useragent)) return { allowed: false, reason: "The reviewed user agent string must use the reviewed grammar of tokens, separators and version marks without line breaks." };
|
|
4803
|
+
return { allowed: true };
|
|
4804
|
+
}
|
|
4805
|
+
if (kind === "overridepermission") {
|
|
4806
|
+
const grant = permissiongrantof(options.permission);
|
|
4807
|
+
if (!grant) return { allowed: false, reason: `The permission override needs a reviewed name of the browser permission set (${browserpermissions.join(", ")}) and a state of ${permissionstates.join(", ")}.` };
|
|
4808
|
+
void permissiongrade(grant.name);
|
|
4809
|
+
return { allowed: true };
|
|
4810
|
+
}
|
|
4811
|
+
if (kind === "blackboxscripts") {
|
|
4812
|
+
const rules = Array.isArray(options.rules) ? options.rules.flatMap((rule) => {
|
|
4813
|
+
const parsed = blackboxruleof(rule);
|
|
4814
|
+
return parsed !== void 0 ? [parsed] : [];
|
|
4815
|
+
}) : [];
|
|
4816
|
+
if (rules.length === 0) return { allowed: false, reason: "The blackbox layer needs a reviewed non-empty rule list where every pattern names its origin explicitly and carries a trace scope." };
|
|
4817
|
+
return { allowed: true };
|
|
4818
|
+
}
|
|
4819
|
+
return { allowed: true };
|
|
4820
|
+
}
|
|
4821
|
+
function validatesessiongrammar(step, options) {
|
|
4822
|
+
const kind = step.kind;
|
|
4823
|
+
if (kind === "persiststate") {
|
|
4824
|
+
if (options.resume !== void 0 && typeof options.resume !== "boolean") return { allowed: false, reason: "The reviewed resume flag must be a boolean." };
|
|
4825
|
+
return { allowed: true };
|
|
4826
|
+
}
|
|
4827
|
+
if (kind === "capturesession") {
|
|
4828
|
+
const plan = snapshotplanof(options.snapshot);
|
|
4829
|
+
if (!plan) return { allowed: false, reason: "The session capture needs a reviewed snapshot plan with its scope, a non-empty section list of the reviewed grammar (tabs, scroll, forms, storage, cookies) and the capture link flag." };
|
|
4830
|
+
if (plan.auto !== void 0) {
|
|
4831
|
+
const interval = autointervalof(options.snapshot.auto);
|
|
4832
|
+
if (interval === void 0) return { allowed: false, reason: "The reviewed auto snapshot interval needs a positive period, a positive maximum snapshot count and a zero or positive expiry window with no code ceiling." };
|
|
4833
|
+
}
|
|
4834
|
+
return { allowed: true };
|
|
4835
|
+
}
|
|
4836
|
+
if (kind === "restoresession") {
|
|
4837
|
+
if (typeof options.sessionid !== "string" || !options.sessionid.trim()) return { allowed: false, reason: "The session restore needs the reviewed session id of the saved record." };
|
|
4838
|
+
if (restoreplanof(options.restore) === void 0) return { allowed: false, reason: "The session restore needs a reviewed restore plan with its tab, form and capture policies." };
|
|
4839
|
+
if (options.reviewed !== true) return { allowed: false, reason: "Every session restore needs the explicit restore review with its tabs, form state and captures listed before it reopens anything." };
|
|
4840
|
+
return { allowed: true };
|
|
4841
|
+
}
|
|
4842
|
+
if (kind === "namedsessions") {
|
|
4843
|
+
if (typeof options.sessionid !== "string" || !options.sessionid.trim()) return { allowed: false, reason: "The session filing needs the reviewed session id of the saved record." };
|
|
4844
|
+
if (typeof options.name !== "string" || !options.name.trim()) return { allowed: false, reason: "The session filing needs a reviewed non-empty session name." };
|
|
4845
|
+
if (options.folder !== void 0 && (typeof options.folder !== "string" || !options.folder.trim())) return { allowed: false, reason: "The reviewed folder name must be a non-empty string." };
|
|
4846
|
+
if (options.tags !== void 0 && (!Array.isArray(options.tags) || !options.tags.every((tag) => typeof tag === "string" && tag.trim()))) return { allowed: false, reason: "The reviewed tag list must be a list of non-empty strings." };
|
|
4847
|
+
return { allowed: true };
|
|
4848
|
+
}
|
|
4849
|
+
if (kind === "diffsessions") {
|
|
4850
|
+
if (typeof options.left !== "string" || !options.left.trim() || typeof options.right !== "string" || !options.right.trim()) return { allowed: false, reason: "The session diff needs the reviewed ids of both saved sessions." };
|
|
4851
|
+
return { allowed: true };
|
|
4852
|
+
}
|
|
4853
|
+
if (kind === "searchsessions") {
|
|
4854
|
+
if (searchqueryof(options.query) === void 0) return { allowed: false, reason: "The session search needs a reviewed query with a non-empty term list, fields of the reviewed grammar (urls, titles, names, text) and an optional time window." };
|
|
4855
|
+
return { allowed: true };
|
|
4856
|
+
}
|
|
4857
|
+
if (kind === "exportsessions") {
|
|
4858
|
+
if (options.reviewed !== true) return { allowed: false, reason: "Session exports need the explicit export review before any session file leaves the device." };
|
|
4859
|
+
if (options.ids !== void 0 && (!Array.isArray(options.ids) || options.ids.length === 0 || !options.ids.every((id) => typeof id === "string" && id.trim()))) return { allowed: false, reason: "The reviewed export id list must be a non-empty list of saved session ids." };
|
|
4860
|
+
return { allowed: true };
|
|
4861
|
+
}
|
|
4862
|
+
if (kind === "importsessions") {
|
|
4863
|
+
if (options.reviewed !== true) return { allowed: false, reason: "Session imports need the explicit full record review before any record joins the library." };
|
|
4864
|
+
if (importsessionfile(options.file) === void 0) return { allowed: false, reason: "The session import needs a reviewed file of the known format version with an intact checksum." };
|
|
4865
|
+
return { allowed: true };
|
|
4866
|
+
}
|
|
4867
|
+
return { allowed: true };
|
|
4868
|
+
}
|
|
4869
|
+
function restorereviewgranted(step) {
|
|
4870
|
+
let options = {};
|
|
4871
|
+
try {
|
|
4872
|
+
options = parseoptions(step);
|
|
4873
|
+
} catch {
|
|
4874
|
+
options = {};
|
|
4875
|
+
}
|
|
4876
|
+
if (restoreplanof(options.restore) === void 0) return { allowed: false, reason: "Every session restore needs a reviewed restore plan with its tab, form and capture policies." };
|
|
4877
|
+
if (options.reviewed !== true) return { allowed: false, reason: "The session restore needs the explicit restore review of its tabs, form state and captures before it reopens anything." };
|
|
4878
|
+
return { allowed: true };
|
|
4879
|
+
}
|
|
4880
|
+
function sessionrestoregate(input) {
|
|
4881
|
+
const gate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now: input.now, action: "run the session memory step" });
|
|
4882
|
+
if (!gate.allowed) return gate;
|
|
4883
|
+
if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "Session memory steps need an approved plan before they run." };
|
|
4884
|
+
if (input.step.kind === "restoresession") return restorereviewgranted(input.step);
|
|
4885
|
+
return { allowed: true };
|
|
4886
|
+
}
|
|
4887
|
+
function restoreoriginsgranted(urls, grants) {
|
|
4888
|
+
const covered = new Set(grants);
|
|
4889
|
+
const skippedorigins = [];
|
|
4890
|
+
for (const url of urls) {
|
|
4891
|
+
let origin = "";
|
|
4892
|
+
try {
|
|
4893
|
+
origin = new URL(url).origin;
|
|
4894
|
+
} catch {
|
|
4895
|
+
origin = "";
|
|
4896
|
+
}
|
|
4897
|
+
if (!origin || !covered.has(origin)) skippedorigins.push(origin || url);
|
|
4898
|
+
}
|
|
4899
|
+
return { allowed: skippedorigins.length === 0, skippedorigins: [...new Set(skippedorigins)] };
|
|
4900
|
+
}
|
|
4901
|
+
function sessionnameunique(name, records, recordid) {
|
|
4902
|
+
if (records.some((record2) => record2.name === name && record2.id !== recordid)) return { allowed: false, reason: `The session name ${name} already exists in the library; review a unique name.` };
|
|
4903
|
+
return { allowed: true };
|
|
4904
|
+
}
|
|
4905
|
+
function sessionfolderunique(name, folders) {
|
|
4906
|
+
if (folders.some((folder) => folder.name === name)) return { allowed: false, reason: `The folder name ${name} already exists in the library; review a unique folder name.` };
|
|
4907
|
+
return { allowed: true };
|
|
4908
|
+
}
|
|
4909
|
+
function snapshotretentionwindow(settings) {
|
|
4910
|
+
return settings?.sessionretention;
|
|
4911
|
+
}
|
|
4224
4912
|
function validatecdpgrammar(step, options) {
|
|
4225
4913
|
const kind = step.kind;
|
|
4226
4914
|
if (kind === "attachcdp") {
|
|
@@ -4782,6 +5470,14 @@ function validatestep(step, origin) {
|
|
|
4782
5470
|
const profilecheck = validateprofilegrammar(step, options);
|
|
4783
5471
|
if (!profilecheck.allowed) return profilecheck;
|
|
4784
5472
|
}
|
|
5473
|
+
if (isemulationkind(step.kind)) {
|
|
5474
|
+
const emulationcheck = validateemulationgrammar(step, options);
|
|
5475
|
+
if (!emulationcheck.allowed) return emulationcheck;
|
|
5476
|
+
}
|
|
5477
|
+
if (issessionkind(step.kind)) {
|
|
5478
|
+
const sessioncheck = validatesessiongrammar(step, options);
|
|
5479
|
+
if (!sessioncheck.allowed) return sessioncheck;
|
|
5480
|
+
}
|
|
4785
5481
|
if (step.kind === "tabcreate") {
|
|
4786
5482
|
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
4787
5483
|
if (options.window !== void 0 && (typeof options.window !== "number" || !Number.isInteger(options.window) || options.window < 0)) return { allowed: false, reason: "The reviewed target window id must be a non-negative integer." };
|
|
@@ -4951,6 +5647,27 @@ function canexecute(input) {
|
|
|
4951
5647
|
}
|
|
4952
5648
|
}
|
|
4953
5649
|
}
|
|
5650
|
+
if (isemulationkind(input.step.kind)) {
|
|
5651
|
+
const emugatecheck = emugate({ session: input.session, plan: input.plan, step: input.step, tabid: input.tabid, origin: input.origin, now });
|
|
5652
|
+
if (!emugatecheck.allowed) return emugatecheck;
|
|
5653
|
+
}
|
|
5654
|
+
if (issessionkind(input.step.kind)) {
|
|
5655
|
+
const sessiongatecheck = sessionrestoregate({ session: input.session, plan: input.plan, step: input.step, tabid: input.tabid, origin: input.origin, now });
|
|
5656
|
+
if (!sessiongatecheck.allowed) return sessiongatecheck;
|
|
5657
|
+
if (input.step.kind === "restoresession") {
|
|
5658
|
+
let restoreoptions = {};
|
|
5659
|
+
try {
|
|
5660
|
+
restoreoptions = parseoptions(input.step);
|
|
5661
|
+
} catch {
|
|
5662
|
+
restoreoptions = {};
|
|
5663
|
+
}
|
|
5664
|
+
for (const url of Array.isArray(restoreoptions.origins) ? restoreoptions.origins : []) {
|
|
5665
|
+
if (typeof url !== "string" || !url) continue;
|
|
5666
|
+
const origingate = origincheck(input.session, url);
|
|
5667
|
+
if (!origingate.allowed) return { allowed: false, reason: `The session restore reopens ${url} outside the session origin grants; review the restore record or grant the origin.` };
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
4954
5671
|
if (iscontrolkind(input.step.kind)) {
|
|
4955
5672
|
const controlgate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now, action: "control the network" });
|
|
4956
5673
|
if (!controlgate.allowed) return controlgate;
|
|
@@ -5196,9 +5913,20 @@ function recordprofile(progress, planid, stepid, entry, now) {
|
|
|
5196
5913
|
const outcome = { stepid, ok: true, summary: `The profiling ${entry.family} capture ran${counts.length > 0 ? ` with ${counts}` : ""}.`, details: { profile: entry }, at: now };
|
|
5197
5914
|
return recordoutcome(base, planid, outcome, now);
|
|
5198
5915
|
}
|
|
5916
|
+
function recordemulation(progress, planid, stepid, entry, now) {
|
|
5917
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
5918
|
+
const outcome = { stepid, ok: true, summary: `${entry.reason}: ${entry.applied.length} applied layer${entry.applied.length === 1 ? "" : "s"}${entry.applied.length > 0 ? ` (${entry.applied.join(", ")})` : ""} and ${entry.reverted.length} reverted layer${entry.reverted.length === 1 ? "" : "s"}${entry.reverted.length > 0 ? ` (${entry.reverted.join(", ")})` : ""}.`, details: { emulation: entry }, at: now };
|
|
5919
|
+
return recordoutcome(base, planid, outcome, now);
|
|
5920
|
+
}
|
|
5921
|
+
function recordsession(progress, planid, stepid, entry, now) {
|
|
5922
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
5923
|
+
const counts = `${entry.sections !== void 0 ? `${entry.sections} section${entry.sections === 1 ? "" : "s"}, ` : ""}${entry.matches !== void 0 ? `${entry.matches} match${entry.matches === 1 ? "" : "es"}, ` : ""}${entry.restored !== void 0 ? `${entry.restored} restored tab${entry.restored === 1 ? "" : "s"}, ` : ""}${entry.skipped !== void 0 ? `${entry.skipped} skipped origin${entry.skipped === 1 ? "" : "s"}, ` : ""}${entry.cursor !== void 0 ? `cursor ${entry.cursor}, ` : ""}${entry.bytes !== void 0 ? `${entry.bytes} byte${entry.bytes === 1 ? "" : "s"}, ` : ""}`.replace(/, $/, "");
|
|
5924
|
+
const outcome = { stepid, ok: true, summary: `${entry.detail}${counts.length > 0 ? ` with ${counts}` : ""}.`, details: { session: entry }, at: now };
|
|
5925
|
+
return recordoutcome(base, planid, outcome, now);
|
|
5926
|
+
}
|
|
5199
5927
|
|
|
5200
5928
|
// version.ts
|
|
5201
|
-
var packageversion = "1.1.
|
|
5929
|
+
var packageversion = "1.1.49";
|
|
5202
5930
|
|
|
5203
5931
|
// types.ts
|
|
5204
5932
|
var protocolversion = packageversion;
|
|
@@ -5370,6 +6098,50 @@ function parseproposal(value, origin, grants) {
|
|
|
5370
6098
|
}
|
|
5371
6099
|
if (step.kind === "annotatetrace" && (!Array.isArray(profileoptions.annotations) || profileoptions.annotations.length === 0 || !profileoptions.annotations.every((annotation) => annotationof(annotation) !== void 0))) throw new Error("Trace annotation steps without reviewed step annotations are refused.");
|
|
5372
6100
|
}
|
|
6101
|
+
if (isemulationkind(step.kind)) {
|
|
6102
|
+
const granted = covered.some((pattern) => {
|
|
6103
|
+
try {
|
|
6104
|
+
return new URL(origin).origin === new URL(pattern).origin;
|
|
6105
|
+
} catch {
|
|
6106
|
+
return false;
|
|
6107
|
+
}
|
|
6108
|
+
});
|
|
6109
|
+
if (!granted) throw new Error(`The ${step.kind} step of ${origin} targets an origin outside the grants.`);
|
|
6110
|
+
let emulationoptions = {};
|
|
6111
|
+
try {
|
|
6112
|
+
emulationoptions = parseoptions(step);
|
|
6113
|
+
} catch {
|
|
6114
|
+
emulationoptions = {};
|
|
6115
|
+
}
|
|
6116
|
+
if (revertplanof(emulationoptions.revertplan) === void 0) throw new Error("Emulation steps without a reviewed revert plan are refused.");
|
|
6117
|
+
if (step.kind === "emulatelocate") {
|
|
6118
|
+
const preset = locationpresetof(emulationoptions.location);
|
|
6119
|
+
if (preset === void 0) throw new Error("Location emulation needs a reviewed preset with coordinates inside the latitude and longitude ranges.");
|
|
6120
|
+
}
|
|
6121
|
+
if (step.kind === "overridepermission" && permissiongrantof(emulationoptions.permission) === void 0) throw new Error("Permission overrides of unknown permission names are refused.");
|
|
6122
|
+
}
|
|
6123
|
+
if (issessionkind(step.kind)) {
|
|
6124
|
+
let sessionoptions = {};
|
|
6125
|
+
try {
|
|
6126
|
+
sessionoptions = parseoptions(step);
|
|
6127
|
+
} catch {
|
|
6128
|
+
sessionoptions = {};
|
|
6129
|
+
}
|
|
6130
|
+
if (step.kind === "restoresession") {
|
|
6131
|
+
for (const url of Array.isArray(sessionoptions.origins) ? sessionoptions.origins : []) {
|
|
6132
|
+
if (typeof url !== "string" || !url) continue;
|
|
6133
|
+
const granted = covered.some((pattern) => {
|
|
6134
|
+
try {
|
|
6135
|
+
return new URL(url).origin === new URL(pattern).origin;
|
|
6136
|
+
} catch {
|
|
6137
|
+
return false;
|
|
6138
|
+
}
|
|
6139
|
+
});
|
|
6140
|
+
if (!granted) throw new Error(`The session restore reopens ${url} outside the grants.`);
|
|
6141
|
+
}
|
|
6142
|
+
}
|
|
6143
|
+
if (step.kind === "importsessions" && importsessionfile(sessionoptions.file) === void 0) throw new Error("Session import files of unknown format versions are refused.");
|
|
6144
|
+
}
|
|
5373
6145
|
const evaluation = validatestep(step, origin);
|
|
5374
6146
|
if (!evaluation.allowed) throw new Error(evaluation.reason);
|
|
5375
6147
|
const target = outboundtarget(step);
|
|
@@ -5444,7 +6216,7 @@ function requestbody(input) {
|
|
|
5444
6216
|
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
5445
6217
|
}
|
|
5446
6218
|
function outcomeresponse(input) {
|
|
5447
|
-
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {}, ...input.media ? { media: input.media } : {}, ...input.transport ? { transport: input.transport } : {}, ...input.network ? { network: input.network } : {}, ...input.control ? { control: input.control } : {}, ...input.timeline ? { timeline: input.timeline } : {}, ...input.cdp ? { cdp: input.cdp } : {}, ...input.profile ? { profile: input.profile } : {} });
|
|
6219
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {}, ...input.capture ? { capture: input.capture } : {}, ...input.media ? { media: input.media } : {}, ...input.transport ? { transport: input.transport } : {}, ...input.network ? { network: input.network } : {}, ...input.control ? { control: input.control } : {}, ...input.timeline ? { timeline: input.timeline } : {}, ...input.cdp ? { cdp: input.cdp } : {}, ...input.profile ? { profile: input.profile } : {}, ...input.emulation ? { emulation: input.emulation } : {}, ...input.session ? { session: input.session } : {} });
|
|
5448
6220
|
}
|
|
5449
6221
|
function mapresponse(input) {
|
|
5450
6222
|
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
|
|
@@ -5567,6 +6339,17 @@ function profilereport(input) {
|
|
|
5567
6339
|
});
|
|
5568
6340
|
return { version: protocolversion, flows: input.flows, heaps: input.heaps, samples: input.samples, trends: input.trends, profiles: input.profiles, shifts: input.shifts, traces: input.traces, sourcemaps: input.sourcemaps, consents };
|
|
5569
6341
|
}
|
|
6342
|
+
function emulationreport(input) {
|
|
6343
|
+
const consents = input.consents.map((consent) => {
|
|
6344
|
+
const { prompt, ...metadata } = consent;
|
|
6345
|
+
void prompt;
|
|
6346
|
+
return metadata;
|
|
6347
|
+
});
|
|
6348
|
+
return { version: protocolversion, ...input.state !== void 0 ? { state: input.state } : {}, layers: input.state?.layers ?? [], devices: input.devices, networks: input.networks, locations: input.locations, agents: input.agents, blackbox: input.blackbox, permissions: input.permissions, consents };
|
|
6349
|
+
}
|
|
6350
|
+
function sessionreport(input) {
|
|
6351
|
+
return { version: protocolversion, records: input.records, events: input.events, folders: input.folders, diffs: input.diffs, ...input.auto !== void 0 ? { auto: input.auto } : {}, ...input.crashed === true ? { crashed: true } : {} };
|
|
6352
|
+
}
|
|
5570
6353
|
|
|
5571
6354
|
// capture.ts
|
|
5572
6355
|
var capturekinds = ["shotview", "shotfullpage", "shotelement", "shotregion", "contactsheet"];
|
|
@@ -6143,6 +6926,46 @@ async function runbrowseraction(step, sessiontabid, windowid) {
|
|
|
6143
6926
|
}
|
|
6144
6927
|
}
|
|
6145
6928
|
|
|
6929
|
+
// extension/pagesession.ts
|
|
6930
|
+
function capturepagestate(sections) {
|
|
6931
|
+
const wants = (section) => sections.includes(section);
|
|
6932
|
+
const forms = [];
|
|
6933
|
+
if (wants("forms")) {
|
|
6934
|
+
const elements = Array.from(document.querySelectorAll("input, textarea, select"));
|
|
6935
|
+
elements.forEach((element, index) => {
|
|
6936
|
+
if (element.type === "password") return;
|
|
6937
|
+
const selector = element.id ? `#${element.id}` : element.name ? `[name="${element.name}"]` : `${element.tagName.toLowerCase()}:nth-of-type(${index + 1})`;
|
|
6938
|
+
forms.push({ selector, value: element.value });
|
|
6939
|
+
});
|
|
6940
|
+
}
|
|
6941
|
+
const storagekeys = [];
|
|
6942
|
+
const storagevalues = [];
|
|
6943
|
+
if (wants("storage")) {
|
|
6944
|
+
for (let index = 0; index < localStorage.length; index += 1) {
|
|
6945
|
+
const key = localStorage.key(index);
|
|
6946
|
+
if (key === null) continue;
|
|
6947
|
+
storagekeys.push(key);
|
|
6948
|
+
storagevalues.push(localStorage.getItem(key) ?? "");
|
|
6949
|
+
}
|
|
6950
|
+
}
|
|
6951
|
+
const cookienames = wants("cookies") ? document.cookie.split(";").map((part) => part.split("=")[0]?.trim() ?? "").filter((name) => name.length > 0) : [];
|
|
6952
|
+
return { scrollx: window.scrollX, scrolly: window.scrollY, forms, storagekeys, storagevalues, cookienames };
|
|
6953
|
+
}
|
|
6954
|
+
function restorepagestate(state) {
|
|
6955
|
+
window.scrollTo(state.scrollx, state.scrolly);
|
|
6956
|
+
let restored = 0;
|
|
6957
|
+
for (const form of Array.isArray(state.forms) ? state.forms : []) {
|
|
6958
|
+
const element = document.querySelector(form.selector);
|
|
6959
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
|
6960
|
+
element.value = form.value;
|
|
6961
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
6962
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
6963
|
+
restored += 1;
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
return { restored, summary: `Restored the scroll position and ${restored} form field${restored === 1 ? "" : "s"} of the reopened tab.` };
|
|
6967
|
+
}
|
|
6968
|
+
|
|
6146
6969
|
// extension/tabscommand.ts
|
|
6147
6970
|
function parsetabquery(step) {
|
|
6148
6971
|
let options = {};
|
|
@@ -7255,7 +8078,7 @@ function stepoptions2(step) {
|
|
|
7255
8078
|
}
|
|
7256
8079
|
async function refreshcapabilities() {
|
|
7257
8080
|
const report = await readcapabilities();
|
|
7258
|
-
const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds], control: [...controlkinds], debug: [...timelinekinds, ...cdpkinds], profile: [...profilerkinds] };
|
|
8081
|
+
const withmedia = { ...report, captures: [...capturekinds], media: [...mediakinds], http: [...httpkinds], netwatch: [...socketkinds, ...netwatchkinds], control: [...controlkinds], debug: [...timelinekinds, ...cdpkinds], profile: [...profilerkinds], emulation: [...emulationkinds], sessions: [...sessionkinds] };
|
|
7259
8082
|
await memory.setcapabilities(withmedia);
|
|
7260
8083
|
return withmedia;
|
|
7261
8084
|
}
|
|
@@ -7394,6 +8217,7 @@ function browserauditkind(step) {
|
|
|
7394
8217
|
return "tab";
|
|
7395
8218
|
}
|
|
7396
8219
|
function stepauditkind(step, ok) {
|
|
8220
|
+
if (issessionkind(step.kind)) return "session";
|
|
7397
8221
|
if (isbrowserkind(step.kind)) return browserauditkind(step);
|
|
7398
8222
|
if (istabscommandkind(step.kind)) {
|
|
7399
8223
|
if (step.kind === "grouptabs" || step.kind === "colorgroup" || step.kind === "collapsegroup") return "group";
|
|
@@ -7696,6 +8520,8 @@ async function tracktabupdate(tabid2, changeinfo) {
|
|
|
7696
8520
|
});
|
|
7697
8521
|
await stopprofileinstrumentsforrun(runid, `the run tab navigated to ${url}`).catch(() => {
|
|
7698
8522
|
});
|
|
8523
|
+
await revertemulationforrun(runid, `the run tab navigated to ${url}`).catch(() => {
|
|
8524
|
+
});
|
|
7699
8525
|
}
|
|
7700
8526
|
return;
|
|
7701
8527
|
}
|
|
@@ -7721,6 +8547,10 @@ chrome.tabs.onActivated.addListener((activeinfo) => {
|
|
|
7721
8547
|
void recordtabwatchevent("activated", activeinfo.tabId);
|
|
7722
8548
|
});
|
|
7723
8549
|
chrome.tabs.onRemoved.addListener((tabid2) => {
|
|
8550
|
+
for (const [runid, state] of [...activeemulation.entries()]) {
|
|
8551
|
+
if (state.tabid === tabid2) void revertemulationforrun(runid, `the run tab ${tabid2} dropped`, tabid2).catch(() => {
|
|
8552
|
+
});
|
|
8553
|
+
}
|
|
7724
8554
|
const url = lastknownurls.get(tabid2);
|
|
7725
8555
|
const title = lastknowntitles.get(tabid2) ?? "";
|
|
7726
8556
|
const windowid = 0;
|
|
@@ -11360,11 +12190,293 @@ async function refreshbadge() {
|
|
|
11360
12190
|
const observedrequests = (await memory.getexchanges()).length;
|
|
11361
12191
|
const livechannels = (await memory.getchannels()).filter((channel) => channel.state === "open" || channel.state === "connecting").length;
|
|
11362
12192
|
const activerulescount = [...activerules.values()].reduce((total2, ruleset) => total2 + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0);
|
|
12193
|
+
const locationprompts = (await memory.getlocationconsents()).filter((consent) => consent.approved === void 0).length;
|
|
12194
|
+
const emulatedlayers = [...activeemulation.values()].reduce((total2, state) => total2 + activelayers(state).length, 0);
|
|
11363
12195
|
const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
|
|
11364
|
-
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts + consoleprompts + debuggerprompts + observedrequests + livechannels + activerulescount;
|
|
12196
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets + captures + media + recordingprompts + fetchprompts + consoleprompts + debuggerprompts + locationprompts + emulatedlayers + observedrequests + livechannels + activerulescount;
|
|
11365
12197
|
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
11366
12198
|
});
|
|
11367
12199
|
}
|
|
12200
|
+
var activeemulation = /* @__PURE__ */ new Map();
|
|
12201
|
+
async function loademulationstate(runid) {
|
|
12202
|
+
const existing = activeemulation.get(runid);
|
|
12203
|
+
if (existing) return existing;
|
|
12204
|
+
const stored = await memory.getemulationstate(runid);
|
|
12205
|
+
if (stored) activeemulation.set(runid, stored);
|
|
12206
|
+
return stored;
|
|
12207
|
+
}
|
|
12208
|
+
async function revertemulationforrun(runid, reason, tabid2) {
|
|
12209
|
+
const state = await loademulationstate(runid);
|
|
12210
|
+
if (!state) return;
|
|
12211
|
+
const now = Date.now();
|
|
12212
|
+
const outcome = revertalllayers(state, now);
|
|
12213
|
+
const target = tabid2 ?? state.tabid;
|
|
12214
|
+
if (outcome.reverted.length > 0 && target !== void 0) {
|
|
12215
|
+
for (const layer of outcome.reverted) {
|
|
12216
|
+
await chrome.scripting.executeScript({ target: { tabId: target }, func: (family, prior) => {
|
|
12217
|
+
const bridge = globalThis.devthinkbridge;
|
|
12218
|
+
if (bridge) bridge.revertemulationlayer(family, prior);
|
|
12219
|
+
}, args: [layer.family, layer.prior] }).catch(() => {
|
|
12220
|
+
});
|
|
12221
|
+
}
|
|
12222
|
+
}
|
|
12223
|
+
activeemulation.set(runid, outcome.state);
|
|
12224
|
+
await memory.setemulationstate(outcome.state);
|
|
12225
|
+
const session = await memory.getsession();
|
|
12226
|
+
for (const layer of outcome.reverted) {
|
|
12227
|
+
await audit("emulation", `Reverted the ${layer.family} layer ${layer.name} of run ${runid} on ${reason}; the prior state${layer.prior !== void 0 ? " restored exactly" : " needed no page state"} through the revert plan of ${layer.revertplan.join(", ")}.`, { ...session ? { sessionid: session.id } : {}, planid: runid, stepid: layer.stepid });
|
|
12228
|
+
}
|
|
12229
|
+
if (outcome.reverted.length > 0) {
|
|
12230
|
+
const plan = await memory.getplan();
|
|
12231
|
+
if (plan) await memory.setprogress(recordemulation(await memory.getprogress(), plan.id, outcome.reverted[0]?.stepid ?? "", { applied: [], reverted: outcome.reverted.map((layer) => layer.name), reason: `Emulation reverted on ${reason}` }, now));
|
|
12232
|
+
await refreshbadge();
|
|
12233
|
+
}
|
|
12234
|
+
}
|
|
12235
|
+
async function executeemulationstep(step, session, plan, tabid2, origin) {
|
|
12236
|
+
const options = stepoptions2(step);
|
|
12237
|
+
const extra = { sessionid: session.id, planid: plan.id, stepid: step.id };
|
|
12238
|
+
const revertplan = revertplanof(options.revertplan) ?? [];
|
|
12239
|
+
const family = familyofkind(step.kind) ?? "device";
|
|
12240
|
+
const state = await loademulationstate(plan.id) ?? emulationstateof({ runid: plan.id, tabid: tabid2, origin, now: Date.now() });
|
|
12241
|
+
const stacked = activelayers(state).filter((layer2) => layer2.family === family).length;
|
|
12242
|
+
const stackgate2 = emulationstackallowed(plan, step.kind, stacked);
|
|
12243
|
+
if (!stackgate2.allowed) throw new Error(stackgate2.reason);
|
|
12244
|
+
if (step.kind === "emulatelocate") {
|
|
12245
|
+
const preset = locationpresetof(options.location);
|
|
12246
|
+
if (!preset) throw new Error("A reviewed location preset is required before the location override applies.");
|
|
12247
|
+
const consents = await memory.getlocationconsents();
|
|
12248
|
+
const consentgate = locationconsentgate(origin, preset.latitude, preset.longitude, consents);
|
|
12249
|
+
if (!consentgate.allowed) {
|
|
12250
|
+
const pending = consents.find((consent) => consent.origin === origin && consent.approved === void 0 && consent.latitude === preset.latitude && consent.longitude === preset.longitude);
|
|
12251
|
+
if (!pending) {
|
|
12252
|
+
await memory.setlocationconsent({ id: randomid(), prompt: `Location override of ${preset.latitude}, ${preset.longitude} on ${origin} for run ${plan.id} through a page-injected geolocation override; the true browser location stays untouched.`, origin, latitude: preset.latitude, longitude: preset.longitude, consentedat: Date.now() });
|
|
12253
|
+
await refreshbadge();
|
|
12254
|
+
}
|
|
12255
|
+
throw new Error(`${consentgate.reason} The prompt is open in the review panel with the coordinates shown; approve it and run the step again.`);
|
|
12256
|
+
}
|
|
12257
|
+
}
|
|
12258
|
+
if (step.kind === "blackboxscripts") {
|
|
12259
|
+
const rules = (Array.isArray(options.rules) ? options.rules : []).flatMap((rule) => {
|
|
12260
|
+
const parsed = blackboxruleof(rule);
|
|
12261
|
+
return parsed !== void 0 ? [parsed] : [];
|
|
12262
|
+
});
|
|
12263
|
+
if (rules.length === 0) throw new Error("A reviewed non-empty blackbox rule list is required.");
|
|
12264
|
+
const output2 = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The blackbox registration returned no result." };
|
|
12265
|
+
if (!output2.ok) return output2;
|
|
12266
|
+
await memory.setblackboxrules(origin, rules);
|
|
12267
|
+
const name2 = `${rules.length} blackbox rule${rules.length === 1 ? "" : "s"}`;
|
|
12268
|
+
const layer2 = newlayer({ id: randomid(), runid: plan.id, stepid: step.id, family: "blackbox", name: name2, originscope: origin, revertplan, at: Date.now() });
|
|
12269
|
+
const updated2 = applylayer(state, layer2, Date.now());
|
|
12270
|
+
activeemulation.set(plan.id, updated2);
|
|
12271
|
+
await memory.setemulationstate(updated2);
|
|
12272
|
+
await memory.setprogress(recordemulation(await memory.getprogress(), plan.id, step.id, { applied: [name2], reverted: [], reason: "Blackbox rules registered" }, Date.now()));
|
|
12273
|
+
await audit("emulation", `Marked ${rules.flatMap((rule) => rule.urlpatterns).length} third party url pattern${rules.flatMap((rule) => rule.urlpatterns).length === 1 ? "" : "s"} as blackboxed in the traces of ${origin} with the ${rules.map((rule) => rule.tracescope).join(", ")} scope${revertplan.length > 0 ? ` and the revert plan of ${revertplan.join(", ")}` : ""}; the rules stay read only trace shaping.`, extra);
|
|
12274
|
+
await refreshbadge();
|
|
12275
|
+
return { ok: true, summary: output2.summary, details: { ...output2.details ?? {}, emulation: { applied: [name2], reverted: [] } } };
|
|
12276
|
+
}
|
|
12277
|
+
const priorpermission = step.kind === "overridepermission" ? await chrome.scripting.executeScript({ target: { tabId: tabid2 }, func: (name2) => navigator.permissions?.query({ name: name2 }).then((status) => status.state).catch(() => "prompt"), args: [permissiongrantof(options.permission)?.name ?? ""] }).then((result) => result[0]?.result).catch(() => "prompt") : void 0;
|
|
12278
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The emulation step returned no result." };
|
|
12279
|
+
if (!output.ok) return output;
|
|
12280
|
+
const prior = output.details?.prior;
|
|
12281
|
+
let name = family;
|
|
12282
|
+
if (step.kind === "emulatedevice") name = devicepresetof(options.device)?.name ?? "device";
|
|
12283
|
+
if (step.kind === "emulatenetwork") name = networkpresetof(options.network)?.name ?? "network";
|
|
12284
|
+
if (step.kind === "emulatelocate") name = locationpresetof(options.location)?.name ?? "location";
|
|
12285
|
+
if (step.kind === "setuseragent") name = agentpresetof(options.agent)?.name ?? "agent";
|
|
12286
|
+
if (step.kind === "overridepermission") name = `${permissiongrantof(options.permission)?.name ?? "permission"} ${permissiongrantof(options.permission)?.state ?? ""}`.trim();
|
|
12287
|
+
const layer = newlayer({ id: randomid(), runid: plan.id, stepid: step.id, family, name, originscope: origin, revertplan, ...prior !== void 0 ? { prior } : {}, at: Date.now() });
|
|
12288
|
+
const updated = applylayer(state, layer, Date.now());
|
|
12289
|
+
activeemulation.set(plan.id, updated);
|
|
12290
|
+
await memory.setemulationstate(updated);
|
|
12291
|
+
if (step.kind === "overridepermission") {
|
|
12292
|
+
const grant = permissiongrantof(options.permission);
|
|
12293
|
+
if (grant) await memory.addpermissionoverride({ id: layer.id, runid: plan.id, stepid: step.id, origin, name: grant.name, state: grant.state, priorstate: priorpermission === "granted" || priorpermission === "denied" || priorpermission === "prompt" ? priorpermission : "prompt", appliedat: Date.now() });
|
|
12294
|
+
}
|
|
12295
|
+
if (step.kind === "emulatedevice" && options.reload === true) await chrome.tabs.reload(tabid2).catch(() => {
|
|
12296
|
+
});
|
|
12297
|
+
const grade = step.kind === "overridepermission" ? ` graded ${permissiongrade(permissiongrantof(options.permission)?.name ?? "")} by the reviewed permission name` : "";
|
|
12298
|
+
await memory.setprogress(recordemulation(await memory.getprogress(), plan.id, step.id, { applied: [name], reverted: [], reason: `Emulation layer applied${grade}` }, Date.now()));
|
|
12299
|
+
await audit("emulation", `Applied the ${family} layer ${name} of run ${plan.id} on ${origin}${grade}${prior !== void 0 ? " with the prior page state captured for the exact revert" : ""} and the revert plan of ${revertplan.join(", ")}; the mask is a page-injected override through the scripting api because no debugger or platform permission exists in the manifest.`, extra);
|
|
12300
|
+
await refreshbadge();
|
|
12301
|
+
return { ok: true, summary: output.summary, details: { ...output.details ?? {}, emulation: { applied: [name], reverted: [] } } };
|
|
12302
|
+
}
|
|
12303
|
+
var restoreloadwindow = 4e3;
|
|
12304
|
+
var restorepollstep = 200;
|
|
12305
|
+
async function waittabloaded(tabid2) {
|
|
12306
|
+
const started = Date.now();
|
|
12307
|
+
while (Date.now() - started < restoreloadwindow) {
|
|
12308
|
+
const tab = await chrome.tabs.get(tabid2).catch(() => void 0);
|
|
12309
|
+
if (!tab || tab.status === "complete") return;
|
|
12310
|
+
await new Promise((resolve) => setTimeout(resolve, restorepollstep));
|
|
12311
|
+
}
|
|
12312
|
+
}
|
|
12313
|
+
async function capturesessionrecord(plan, session, runid) {
|
|
12314
|
+
const grants = session.grants ?? [session.origin];
|
|
12315
|
+
const query = plan.scope === "all" ? {} : plan.scope === "run" ? { currentWindow: true } : { active: true, currentWindow: true };
|
|
12316
|
+
const tabs = await chrome.tabs.query(query).catch(() => []);
|
|
12317
|
+
const capturedtabs = [];
|
|
12318
|
+
const storage = [];
|
|
12319
|
+
const cookies = [];
|
|
12320
|
+
for (const tab of tabs) {
|
|
12321
|
+
const url = tab.url ?? "";
|
|
12322
|
+
if (!url.startsWith("http")) continue;
|
|
12323
|
+
let taborigin = "";
|
|
12324
|
+
try {
|
|
12325
|
+
taborigin = new URL(url).origin;
|
|
12326
|
+
} catch {
|
|
12327
|
+
taborigin = "";
|
|
12328
|
+
}
|
|
12329
|
+
let state;
|
|
12330
|
+
if (tab.id !== void 0 && (plan.sections.includes("scroll") || plan.sections.includes("forms") || plan.sections.includes("storage") || plan.sections.includes("cookies"))) {
|
|
12331
|
+
state = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: capturepagestate, args: [plan.sections] }).then((result) => result[0]?.result).catch(() => void 0);
|
|
12332
|
+
}
|
|
12333
|
+
capturedtabs.push({ url, title: tab.title ?? "", index: tab.index ?? 0, scrollx: state?.scrollx ?? 0, scrolly: state?.scrolly ?? 0, forms: plan.sections.includes("forms") ? state?.forms ?? [] : [] });
|
|
12334
|
+
if (state && taborigin && grants.includes(taborigin)) {
|
|
12335
|
+
if (plan.sections.includes("storage") && state.storagekeys.length > 0) storage.push({ origin: taborigin, keys: state.storagekeys, values: state.storagevalues });
|
|
12336
|
+
if (plan.sections.includes("cookies") && state.cookienames.length > 0) cookies.push({ origin: taborigin, names: state.cookienames });
|
|
12337
|
+
}
|
|
12338
|
+
}
|
|
12339
|
+
const captures = plan.captures ? (await memory.getcaptures()).filter((record2) => record2.runid === runid).map((record2) => record2.id) : [];
|
|
12340
|
+
return newsessionrecord({ id: randomid(), name: `session ${new Date(Date.now()).toISOString()}`, createdat: Date.now(), tabs: capturedtabs.sort((left, right) => left.index - right.index), captures, storage, cookies });
|
|
12341
|
+
}
|
|
12342
|
+
async function performrestore(record2, restore, session) {
|
|
12343
|
+
const grants = session.grants ?? [session.origin];
|
|
12344
|
+
const grantscheck = restoreoriginsgranted(record2.tabs.map((tab) => tab.url), grants);
|
|
12345
|
+
const restored = [];
|
|
12346
|
+
for (const tab of [...record2.tabs].sort((left, right) => left.index - right.index)) {
|
|
12347
|
+
if (restore.tabpolicy !== "reopen") break;
|
|
12348
|
+
let taborigin = "";
|
|
12349
|
+
try {
|
|
12350
|
+
taborigin = new URL(tab.url).origin;
|
|
12351
|
+
} catch {
|
|
12352
|
+
taborigin = "";
|
|
12353
|
+
}
|
|
12354
|
+
if (!taborigin || !grants.includes(taborigin)) continue;
|
|
12355
|
+
const created = await chrome.tabs.create({ url: tab.url, index: tab.index, active: false }).catch(() => void 0);
|
|
12356
|
+
if (!created?.id) continue;
|
|
12357
|
+
await waittabloaded(created.id);
|
|
12358
|
+
if (restore.formpolicy === "restore") {
|
|
12359
|
+
await chrome.scripting.executeScript({ target: { tabId: created.id }, func: restorepagestate, args: [{ scrollx: tab.scrollx, scrolly: tab.scrolly, forms: tab.forms }] }).catch(() => {
|
|
12360
|
+
});
|
|
12361
|
+
}
|
|
12362
|
+
restored.push(tab);
|
|
12363
|
+
}
|
|
12364
|
+
await memory.updatesessionrecord({ ...record2, restoredat: Date.now() });
|
|
12365
|
+
return { restored, skippedorigins: grantscheck.skippedorigins };
|
|
12366
|
+
}
|
|
12367
|
+
async function executesessionstep(step, session, plan, tabid2, origin) {
|
|
12368
|
+
const options = stepoptions2(step);
|
|
12369
|
+
const extra = { sessionid: session.id, planid: plan.id, stepid: step.id };
|
|
12370
|
+
if (step.kind === "persiststate") {
|
|
12371
|
+
const progress = await memory.getprogress();
|
|
12372
|
+
const tracked = progress && progress.planid === plan.id ? progress : void 0;
|
|
12373
|
+
const state = taskstateof({ runid: plan.id, stepcursor: tracked?.completedsteps.length ?? 0, outputs: tracked?.outcomes ?? [], checkpointat: Date.now() });
|
|
12374
|
+
await memory.settaskstate(state);
|
|
12375
|
+
await memory.addsessionevent({ id: randomid(), kind: "persist", at: Date.now(), tabid: tabid2, detail: `Checkpointed the run at step cursor ${state.stepcursor} of ${plan.steps.length}.` });
|
|
12376
|
+
await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "persist", detail: "Checkpointed the task state of the run", cursor: state.stepcursor }, Date.now()));
|
|
12377
|
+
await audit("session", `Persisted the task state checkpoint of run ${plan.id} at step cursor ${state.stepcursor} of ${plan.steps.length}; the checksum detects corruption before any resume and the run resumes from it after a service worker restart.`, extra);
|
|
12378
|
+
return { ok: true, summary: `Task state checkpointed at step cursor ${state.stepcursor}.`, details: { session: { family: "persist", detail: "Task state checkpoint", cursor: state.stepcursor } } };
|
|
12379
|
+
}
|
|
12380
|
+
if (step.kind === "capturesession") {
|
|
12381
|
+
const snapshot2 = snapshotplanof(options.snapshot);
|
|
12382
|
+
if (!snapshot2) throw new Error("A reviewed snapshot plan is required before the session capture runs.");
|
|
12383
|
+
const record2 = await capturesessionrecord(snapshot2, session, plan.id);
|
|
12384
|
+
await memory.addsessionrecord(record2);
|
|
12385
|
+
if (snapshot2.auto) {
|
|
12386
|
+
const current = await memory.getautosnapshot();
|
|
12387
|
+
await memory.setautosnapshot({ interval: snapshot2.auto, lastat: Date.now(), count: current?.count ?? 0 });
|
|
12388
|
+
}
|
|
12389
|
+
await memory.addsessionevent({ id: randomid(), kind: "capture", at: Date.now(), tabid: tabid2, detail: `Captured ${record2.tabs.length} tabs with the ${snapshot2.sections.join(", ")} sections into record ${record2.id}.` });
|
|
12390
|
+
await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "capture", detail: `Captured the browsing session record ${record2.id}`, recordid: record2.id, sections: snapshot2.sections.length }, Date.now()));
|
|
12391
|
+
await audit("session", `Captured the browsing session record ${record2.id} of ${record2.tabs.length} tab${record2.tabs.length === 1 ? "" : "s"} with the ${snapshot2.sections.join(", ")} sections${snapshot2.captures ? ` linking ${record2.captures.length} capture record${record2.captures.length === 1 ? "" : "s"}` : ""}${snapshot2.auto ? ` and the reviewed auto interval of ${snapshot2.auto.period} milliseconds with at most ${snapshot2.auto.maxsnapshots} snapshot${snapshot2.auto.maxsnapshots === 1 ? "" : "s"}` : ""}; the local storage and cookie names of ungranted origins stayed out of the capture.`, extra);
|
|
12392
|
+
return { ok: true, summary: `Captured ${record2.tabs.length} tabs into the session record ${record2.id}.`, details: { snapshotid: record2.id, session: { family: "capture", detail: `Session record ${record2.id}`, recordid: record2.id, sections: snapshot2.sections.length } } };
|
|
12393
|
+
}
|
|
12394
|
+
if (step.kind === "restoresession") {
|
|
12395
|
+
const restore = restoreplanof(options.restore);
|
|
12396
|
+
if (!restore) throw new Error("A reviewed restore plan is required before the session restore runs.");
|
|
12397
|
+
const record2 = await memory.getsessionrecord(String(options.sessionid ?? ""));
|
|
12398
|
+
if (!record2) throw new Error(`No saved session matches ${String(options.sessionid ?? "")}.`);
|
|
12399
|
+
if (record2.sectionsexpired) throw new Error("The saved session sections expired after the retention window; only the record metadata survives for review.");
|
|
12400
|
+
const outcome = await performrestore(record2, restore, session);
|
|
12401
|
+
await memory.addsessionevent({ id: randomid(), kind: "restore", at: Date.now(), tabid: tabid2, detail: `Restored ${outcome.restored.length} tabs of record ${record2.id}${outcome.skippedorigins.length > 0 ? ` and skipped ${outcome.skippedorigins.join(", ")}` : ""}.` });
|
|
12402
|
+
await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "restore", detail: `Restored the session record ${record2.id}`, recordid: record2.id, sections: record2.tabs.length, restored: outcome.restored.length, skipped: outcome.skippedorigins.length }, Date.now()));
|
|
12403
|
+
await audit("session", `Restored the saved session ${record2.id} on demand: ${outcome.restored.length} tab${outcome.restored.length === 1 ? "" : "s"} reopened in their recorded order with the ${restore.formpolicy} form policy and the ${restore.capturepolicy} capture policy${outcome.skippedorigins.length > 0 ? ` while ${outcome.skippedorigins.join(", ")} stayed skipped because their grants expired` : ""}.`, extra);
|
|
12404
|
+
return { ok: true, summary: `Restored ${outcome.restored.length} of ${record2.tabs.length} tabs${outcome.skippedorigins.length > 0 ? `; skipped ${outcome.skippedorigins.join(", ")} outside the grants` : ""}.`, details: { restoreid: record2.id, skippedorigins: outcome.skippedorigins, session: { family: "restore", detail: `Session restore of ${record2.id}`, recordid: record2.id, sections: record2.tabs.length, restored: outcome.restored.length, skipped: outcome.skippedorigins.length } } };
|
|
12405
|
+
}
|
|
12406
|
+
if (step.kind === "namedsessions") {
|
|
12407
|
+
const record2 = await memory.getsessionrecord(String(options.sessionid ?? ""));
|
|
12408
|
+
if (!record2) throw new Error(`No saved session matches ${String(options.sessionid ?? "")}.`);
|
|
12409
|
+
const name = String(options.name ?? "");
|
|
12410
|
+
const records = await memory.getsessionrecords();
|
|
12411
|
+
const unique = sessionnameunique(name, records, record2.id);
|
|
12412
|
+
if (!unique.allowed) throw new Error(unique.reason);
|
|
12413
|
+
const folder = typeof options.folder === "string" && options.folder.trim() ? options.folder : record2.folder;
|
|
12414
|
+
const tags = Array.isArray(options.tags) ? options.tags.filter((tag) => typeof tag === "string" && tag.trim()) : record2.tags;
|
|
12415
|
+
await memory.updatesessionrecord({ ...record2, name, ...folder !== void 0 ? { folder } : {}, tags });
|
|
12416
|
+
if (folder !== void 0) {
|
|
12417
|
+
const folders = await memory.getsessionfolders();
|
|
12418
|
+
if (sessionfolderunique(folder, folders).allowed) await memory.setsessionfolders([...folders, ...sessionfolderof({ name: folder, tags }) !== void 0 ? [sessionfolderof({ name: folder, tags })] : []]);
|
|
12419
|
+
}
|
|
12420
|
+
await memory.addsessionevent({ id: randomid(), kind: "name", at: Date.now(), tabid: tabid2, detail: `Filed the session record ${record2.id} as ${name}${folder !== void 0 ? ` under ${folder}` : ""}.` });
|
|
12421
|
+
await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "name", detail: `Filed the session record ${record2.id} as ${name}`, recordid: record2.id }, Date.now()));
|
|
12422
|
+
await audit("session", `Filed the saved session ${record2.id} under the reviewed name ${name}${folder !== void 0 ? ` inside the ${folder} folder` : ""}${tags.length > 0 ? ` with the tags ${tags.join(", ")}` : ""}; the filing stays read only organization.`, extra);
|
|
12423
|
+
return { ok: true, summary: `Filed the session record ${record2.id} as ${name}.`, details: { session: { family: "name", detail: `Session filed as ${name}`, recordid: record2.id } } };
|
|
12424
|
+
}
|
|
12425
|
+
if (step.kind === "diffsessions") {
|
|
12426
|
+
const left = await memory.getsessionrecord(String(options.left ?? ""));
|
|
12427
|
+
const right = await memory.getsessionrecord(String(options.right ?? ""));
|
|
12428
|
+
if (!left || !right) throw new Error("The session diff needs both saved sessions in the library.");
|
|
12429
|
+
const diff = newsessiondiff({ id: randomid(), left, right, at: Date.now() });
|
|
12430
|
+
await memory.addsessiondiff(diff);
|
|
12431
|
+
await memory.addsessionevent({ id: randomid(), kind: "diff", at: Date.now(), tabid: tabid2, detail: `Diffed ${left.id} and ${right.id} with ${diff.changes.length} changes.` });
|
|
12432
|
+
await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "diff", detail: `Diffed the sessions ${left.id} and ${right.id}`, recordid: diff.id, sections: diff.changes.length }, Date.now()));
|
|
12433
|
+
await audit("session", `Compared the saved sessions ${left.id} and ${right.id}: ${diff.changes.length} tab, url, form and storage change${diff.changes.length === 1 ? "" : "s"} classified as read only comparison evidence.`, extra);
|
|
12434
|
+
return { ok: true, summary: `Diffed the sessions: ${diff.changes.length} change${diff.changes.length === 1 ? "" : "s"}.`, details: { diffid: diff.id, changes: diff.changes, session: { family: "diff", detail: `Session diff ${diff.id}`, recordid: diff.id, sections: diff.changes.length } } };
|
|
12435
|
+
}
|
|
12436
|
+
if (step.kind === "searchsessions") {
|
|
12437
|
+
const query = searchqueryof(options.query);
|
|
12438
|
+
if (!query) throw new Error("A reviewed search query is required before the session search runs.");
|
|
12439
|
+
const matches = await memory.searchmemory(query);
|
|
12440
|
+
await memory.addsessionevent({ id: randomid(), kind: "search", at: Date.now(), tabid: tabid2, detail: `Searched ${query.terms.join(", ")} across the saved sessions with ${matches.length} matches.` });
|
|
12441
|
+
await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "search", detail: `Searched ${query.terms.join(", ")} across the saved sessions`, matches: matches.length }, Date.now()));
|
|
12442
|
+
await audit("session", `Searched the terms ${query.terms.join(", ")} across the saved sessions on the ${query.fields.join(", ")} fields${query.from !== void 0 || query.to !== void 0 ? ` inside the reviewed time window` : ""}: ${matches.length} match${matches.length === 1 ? "" : "es"} returned with their session ids.`, extra);
|
|
12443
|
+
return { ok: true, summary: `Found ${matches.length} match${matches.length === 1 ? "" : "es"} across the saved sessions.`, details: { matches, matchcount: matches.length, session: { family: "search", detail: `Session search of ${query.terms.join(", ")}`, matches: matches.length } } };
|
|
12444
|
+
}
|
|
12445
|
+
if (step.kind === "exportsessions") {
|
|
12446
|
+
if (options.reviewed !== true) throw new Error("Session exports need the explicit export review before any session file leaves the device.");
|
|
12447
|
+
const records = await memory.getsessionrecords();
|
|
12448
|
+
const selected = Array.isArray(options.ids) && options.ids.length > 0 ? records.filter((record2) => options.ids.includes(record2.id)) : records;
|
|
12449
|
+
if (selected.length === 0) throw new Error("No saved session matches the reviewed export ids.");
|
|
12450
|
+
const file = exportsessionfile(selected, Date.now());
|
|
12451
|
+
const payload = JSON.stringify(file);
|
|
12452
|
+
await chrome.downloads.download({ url: `data:application/json;charset=utf-8,${encodeURIComponent(payload)}`, filename: `devthink-sessions-${Date.now()}.json` }).catch(() => {
|
|
12453
|
+
throw new Error("The session file download was refused by the browser.");
|
|
12454
|
+
});
|
|
12455
|
+
await memory.addsessionevent({ id: randomid(), kind: "export", at: Date.now(), tabid: tabid2, detail: `Exported ${file.records.length} session records as a ${file.bytesize} byte file.` });
|
|
12456
|
+
await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "export", detail: `Exported ${file.records.length} session records`, bytes: file.bytesize }, Date.now()));
|
|
12457
|
+
await audit("export", `Exported ${file.records.length} saved session record${file.records.length === 1 ? "" : "s"} as one ${file.bytesize} byte session file of format version ${file.formatversion} with its checksum, through the reviewed download flow.`, extra);
|
|
12458
|
+
return { ok: true, summary: `Exported ${file.records.length} session record${file.records.length === 1 ? "" : "s"} as a ${file.bytesize} byte file.`, details: { session: { family: "export", detail: `Session export of ${file.records.length} records`, bytes: file.bytesize } } };
|
|
12459
|
+
}
|
|
12460
|
+
if (step.kind === "importsessions") {
|
|
12461
|
+
if (options.reviewed !== true) throw new Error("Session imports need the explicit full record review before any record joins the library.");
|
|
12462
|
+
const file = importsessionfile(options.file);
|
|
12463
|
+
if (!file) throw new Error(`The reviewed session file failed its format version or checksum validation; only format version ${sessionfileversion} imports.`);
|
|
12464
|
+
const records = await memory.getsessionrecords();
|
|
12465
|
+
let added = 0;
|
|
12466
|
+
for (const record2 of file.records) {
|
|
12467
|
+
if (records.some((existing) => existing.id === record2.id)) continue;
|
|
12468
|
+
const unique = sessionnameunique(record2.name, [...records, ...file.records.filter((candidate) => candidate.id !== record2.id).map((candidate) => ({ id: candidate.id, name: candidate.name }))]);
|
|
12469
|
+
const name = unique.allowed ? record2.name : `${record2.name} (${record2.id.slice(0, 6)})`;
|
|
12470
|
+
await memory.addsessionrecord({ ...record2, name });
|
|
12471
|
+
added += 1;
|
|
12472
|
+
}
|
|
12473
|
+
await memory.addsessionevent({ id: randomid(), kind: "import", at: Date.now(), tabid: tabid2, detail: `Imported ${added} session records from a reviewed file of format version ${file.formatversion}.` });
|
|
12474
|
+
await memory.setprogress(recordsession(await memory.getprogress(), plan.id, step.id, { family: "import", detail: `Imported ${added} session records`, sections: added }, Date.now()));
|
|
12475
|
+
await audit("session", `Imported ${added} of ${file.records.length} reviewed session record${file.records.length === 1 ? "" : "s"} from a session file of format version ${file.formatversion}; every record was listed in the full record review before it joined the library.`, extra);
|
|
12476
|
+
return { ok: true, summary: `Imported ${added} session record${added === 1 ? "" : "s"} after review.`, details: { session: { family: "import", detail: `Session import of ${added} records`, sections: added } } };
|
|
12477
|
+
}
|
|
12478
|
+
throw new Error(`The ${step.kind} step has no session memory executor.`);
|
|
12479
|
+
}
|
|
11368
12480
|
async function executestep(stepid) {
|
|
11369
12481
|
const session = await memory.getsession();
|
|
11370
12482
|
const plan = await memory.getplan();
|
|
@@ -11438,6 +12550,12 @@ async function executestep(stepid) {
|
|
|
11438
12550
|
} else if (isprofilekind(step.kind)) {
|
|
11439
12551
|
if (!session || !plan || plan.state !== "approved") throw new Error("Profiling kinds refuse to run outside an approved session plan.");
|
|
11440
12552
|
output = await executeprofilestep(step, session, plan, tab.id, origin);
|
|
12553
|
+
} else if (isemulationkind(step.kind)) {
|
|
12554
|
+
if (!session || !plan || plan.state !== "approved") throw new Error("Emulation kinds refuse to run outside an approved session plan.");
|
|
12555
|
+
output = await executeemulationstep(step, session, plan, tab.id, origin);
|
|
12556
|
+
} else if (issessionkind(step.kind)) {
|
|
12557
|
+
if (!session || !plan || plan.state !== "approved") throw new Error("Session memory kinds refuse to run outside an approved session plan.");
|
|
12558
|
+
output = await executesessionstep(step, session, plan, tab.id, origin);
|
|
11441
12559
|
} else {
|
|
11442
12560
|
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
11443
12561
|
const fresh = await snapshot(tab.id);
|
|
@@ -11484,6 +12602,7 @@ async function executestep(stepid) {
|
|
|
11484
12602
|
const completed = watchwindow ? recordwatchcompletion(base, plan.id, stepid, watchwindow.startedat, watchwindow.lifetime, Date.now()) : recordstep(base, plan.id, stepid, Date.now());
|
|
11485
12603
|
const tracked = recordoutcome(completed, plan.id, outcome, Date.now());
|
|
11486
12604
|
await memory.setprogress(tracked);
|
|
12605
|
+
await memory.settaskstate(taskstateof({ runid: plan.id, stepcursor: tracked.completedsteps.length, outputs: tracked.outcomes ?? [], checkpointat: Date.now() }));
|
|
11487
12606
|
const tracker = activememorytrackers.get(plan.id);
|
|
11488
12607
|
if (tracker) await sampleheapforstep(tracker, stepid, tab.id, origin, plan).catch(() => {
|
|
11489
12608
|
});
|
|
@@ -11502,6 +12621,8 @@ async function executestep(stepid) {
|
|
|
11502
12621
|
});
|
|
11503
12622
|
await stopprofileinstrumentsforrun(plan.id, "plan completion").catch(() => {
|
|
11504
12623
|
});
|
|
12624
|
+
await revertemulationforrun(plan.id, "plan completion").catch(() => {
|
|
12625
|
+
});
|
|
11505
12626
|
const done = { ...plan, state: "completed", completedat: Date.now() };
|
|
11506
12627
|
await memory.setplan(done);
|
|
11507
12628
|
await audit("complete", "Every reviewed step of the approved plan has executed.", { ...session ? { sessionid: session.id } : {}, planid: done.id });
|
|
@@ -11675,10 +12796,14 @@ async function handlerequest(message, sender) {
|
|
|
11675
12796
|
const clones = clonetabs(tabs);
|
|
11676
12797
|
const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
|
|
11677
12798
|
const report = await buildtabreport(tabs);
|
|
12799
|
+
const autosnapshot = await memory.getautosnapshot();
|
|
12800
|
+
const crashed = await memory.getcrashflag();
|
|
12801
|
+
const sessionrecords = await memory.applysessionexpiry(snapshotretentionwindow(runsettings), Date.now());
|
|
12802
|
+
const taskstate = plan ? await memory.gettaskstate(plan.id) : void 0;
|
|
11678
12803
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
11679
12804
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
11680
12805
|
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
11681
|
-
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", media: mediarecords, imagebatches, recordingconsents, recordingactive: [...activerecordings.values()].map((active) => ({ id: active.record.id, kind: active.record.kind, scope: active.record.scope, startedat: active.record.startedat, stopat: active.stopat })), recordingwindow: runsettings?.recordingwindow, calls, endpoints, fetchconsents, apikeys, callretention: runsettings?.callretention, fetchesactive: activefetches.size, exchanges, channels, subscriptions, apimap, messages: messagecount, webrequestgrant: runsettings?.webrequestgrant === true, bodyretention: runsettings?.bodyretention, timelineretention: runsettings?.timelineretention, timeline, consoleconsents: await memory.getconsoleconsents(), rotationtargets: await memory.getrotationtargets(), levelsummaries: await memory.getlevelsummaries(), cdpsessions: await memory.getcdpsessions(), cdpcommands: await memory.getcdpcommands(), cdpeventrules: await memory.getcdpeventrules(), breakpoints: await memory.getbreakpoints(), pauses: await memory.getpauses(), watchexpressions: await memory.getwatchexpressions(), scriptoverrides: await memory.getscriptoverrides(), debuggergrants: await memory.getdebuggergrants(), pauseretention: runsettings?.pauseretention, breakpointceiling: runsettings?.breakpointceiling, cdpattached: [...activecdpsessions.values()].filter((active) => active.session.detachedat === void 0).length, profileretention: runsettings?.profileretention, traceceiling: runsettings?.traceceiling, profile: profilereport({ flows: await memory.getflowmetrics(), heaps: await memory.getheaprecords(), samples: await memory.getgrowsamples(), trends: await memory.gettrends(), profiles: await memory.getcpuprofiles(), shifts: await memory.getshiftentries(), traces: await memory.gettracerecords(), sourcemaps: await memory.getsourcemaps(), consents: await memory.getsourcemapconsents() }), profileactive: activememorytrackers.size + activeprofiletargets.size, profiletargets: [...activeprofiletargets.values()].flatMap((entry) => entry.targets), socketsactive: activesockets.size, traffic, tokens, authflows, activerules: [...activerules.values()].reduce((total, ruleset) => total + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0), ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
|
|
12806
|
+
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks, captures: capturemetadata, capturepairs, capturepolicy: runsettings?.capturepolicy ?? "manual", media: mediarecords, imagebatches, recordingconsents, recordingactive: [...activerecordings.values()].map((active) => ({ id: active.record.id, kind: active.record.kind, scope: active.record.scope, startedat: active.record.startedat, stopat: active.stopat })), recordingwindow: runsettings?.recordingwindow, calls, endpoints, fetchconsents, apikeys, callretention: runsettings?.callretention, fetchesactive: activefetches.size, exchanges, channels, subscriptions, apimap, messages: messagecount, webrequestgrant: runsettings?.webrequestgrant === true, bodyretention: runsettings?.bodyretention, timelineretention: runsettings?.timelineretention, timeline, consoleconsents: await memory.getconsoleconsents(), rotationtargets: await memory.getrotationtargets(), levelsummaries: await memory.getlevelsummaries(), cdpsessions: await memory.getcdpsessions(), cdpcommands: await memory.getcdpcommands(), cdpeventrules: await memory.getcdpeventrules(), breakpoints: await memory.getbreakpoints(), pauses: await memory.getpauses(), watchexpressions: await memory.getwatchexpressions(), scriptoverrides: await memory.getscriptoverrides(), debuggergrants: await memory.getdebuggergrants(), pauseretention: runsettings?.pauseretention, breakpointceiling: runsettings?.breakpointceiling, cdpattached: [...activecdpsessions.values()].filter((active) => active.session.detachedat === void 0).length, profileretention: runsettings?.profileretention, traceceiling: runsettings?.traceceiling, profile: profilereport({ flows: await memory.getflowmetrics(), heaps: await memory.getheaprecords(), samples: await memory.getgrowsamples(), trends: await memory.gettrends(), profiles: await memory.getcpuprofiles(), shifts: await memory.getshiftentries(), traces: await memory.gettracerecords(), sourcemaps: await memory.getsourcemaps(), consents: await memory.getsourcemapconsents() }), profileactive: activememorytrackers.size + activeprofiletargets.size, profiletargets: [...activeprofiletargets.values()].flatMap((entry) => entry.targets), socketsactive: activesockets.size, emulation: emulationreport({ ...plan && await loademulationstate(plan.id) !== void 0 ? { state: await loademulationstate(plan.id) } : {}, devices: await memory.getdevicepresets(), networks: await memory.getnetworkpresets(), locations: await memory.getlocationpresets(), agents: await memory.getagentpresets(), blackbox: await memory.getblackboxrules(), permissions: await memory.getpermissionoverrides(), consents: await memory.getlocationconsents() }), emulatedlayers: plan ? layernames(await loademulationstate(plan.id)) : [], emulationretention: runsettings?.emulationretention, traffic, tokens, authflows, activerules: [...activerules.values()].reduce((total, ruleset) => total + ruleset.blocks.filter((rule) => rule.revertedat === void 0).length + ruleset.mocks.filter((rule) => rule.revertedat === void 0).length + ruleset.rewrites.filter((rule) => rule.revertedat === void 0).length + (ruleset.proxy !== void 0 && ruleset.proxy.revertedat === void 0 ? 1 : 0), 0), sessionmemory: sessionreport({ records: sessionrecords, events: await memory.getsessionevents(), folders: await memory.getsessionfolders(), diffs: await memory.getsessiondiffs(), ...autosnapshot !== void 0 ? { auto: autosnapshot.interval } : {}, ...crashed ? { crashed: true } : {} }), autosnapshotstate: autosnapshot, sessionretention: runsettings?.sessionretention, ...taskstate !== void 0 ? { taskstate } : {}, ...stitchprogress.size > 0 ? { stitchprogress: [...stitchprogress.values()] } : {} };
|
|
11682
12807
|
}
|
|
11683
12808
|
case "capabilities":
|
|
11684
12809
|
return refreshcapabilities();
|
|
@@ -11724,7 +12849,8 @@ async function handlerequest(message, sender) {
|
|
|
11724
12849
|
const media = outcome.details?.media;
|
|
11725
12850
|
const network = outcome.details?.network;
|
|
11726
12851
|
const timeline = outcome.details?.timeline;
|
|
11727
|
-
|
|
12852
|
+
const sessionblock = outcome.details?.session;
|
|
12853
|
+
return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {}, ...capture ? { capture } : {}, ...media ? { media } : {}, ...network ? { network } : {}, ...timeline ? { timeline } : {}, ...sessionblock !== void 0 ? { session: { recordid: sessionblock.recordid ?? "", sections: sessionblock.sections ?? 0, ...sessionblock.matches !== void 0 ? { matches: sessionblock.matches } : {}, ...sessionblock.restored !== void 0 ? { restored: sessionblock.restored } : {}, ...sessionblock.skipped !== void 0 ? { skipped: sessionblock.skipped } : {}, ...sessionblock.cursor !== void 0 ? { cursor: sessionblock.cursor } : {}, ...sessionblock.bytes !== void 0 ? { bytes: sessionblock.bytes } : {} } } : {} }));
|
|
11728
12854
|
}
|
|
11729
12855
|
case "map": {
|
|
11730
12856
|
const plan = await memory.getplan();
|
|
@@ -12396,6 +13522,103 @@ async function handlerequest(message, sender) {
|
|
|
12396
13522
|
await refreshbadge();
|
|
12397
13523
|
return { revoked, tokenids };
|
|
12398
13524
|
}
|
|
13525
|
+
case "emulationreport": {
|
|
13526
|
+
const plan = await memory.getplan();
|
|
13527
|
+
const storedemulation = plan ? await loademulationstate(plan.id) : void 0;
|
|
13528
|
+
return emulationreport({ ...storedemulation !== void 0 ? { state: storedemulation } : {}, devices: await memory.getdevicepresets(), networks: await memory.getnetworkpresets(), locations: await memory.getlocationpresets(), agents: await memory.getagentpresets(), blackbox: await memory.getblackboxrules(), permissions: await memory.getpermissionoverrides(), consents: await memory.getlocationconsents() });
|
|
13529
|
+
}
|
|
13530
|
+
case "revertemulation": {
|
|
13531
|
+
const plan = await memory.getplan();
|
|
13532
|
+
if (!plan) throw new Error("No plan is available for an emulation revert.");
|
|
13533
|
+
const state = await loademulationstate(plan.id);
|
|
13534
|
+
if (!state || activelayers(state).length === 0) throw new Error("No active emulation layer covers this run.");
|
|
13535
|
+
await revertemulationforrun(plan.id, "review panel demand");
|
|
13536
|
+
return { reverted: true, layers: await memory.listlayers(plan.id) };
|
|
13537
|
+
}
|
|
13538
|
+
case "restoreemulation": {
|
|
13539
|
+
const plan = await memory.getplan();
|
|
13540
|
+
if (!plan) throw new Error("No plan is available for an emulation restore.");
|
|
13541
|
+
const stored = await memory.getemulationstate(plan.id);
|
|
13542
|
+
if (!stored || stored.layers.length === 0) throw new Error("No stored emulation layer history covers this run.");
|
|
13543
|
+
activeemulation.set(plan.id, stored);
|
|
13544
|
+
await refreshbadge();
|
|
13545
|
+
await audit("emulation", `Restored the stored emulation state of run ${plan.id} with ${activelayers(stored).length} active layer${activelayers(stored).length === 1 ? "" : "s"} on user demand after the service worker restart; the layer history stayed persisted through the run record.`, { planid: plan.id });
|
|
13546
|
+
return { restored: true, layers: stored.layers };
|
|
13547
|
+
}
|
|
13548
|
+
case "approvelocationconsent": {
|
|
13549
|
+
const inputapprove = message;
|
|
13550
|
+
const records = await memory.getlocationconsents();
|
|
13551
|
+
const record2 = records.find((item) => item.id === inputapprove.id);
|
|
13552
|
+
if (!record2) throw new Error("No location consent prompt matches the id.");
|
|
13553
|
+
const decided = { ...record2, approved: true, usedat: Date.now() };
|
|
13554
|
+
await memory.setlocationconsent(decided);
|
|
13555
|
+
await audit("consent", `Location override consent on ${record2.origin} for ${record2.latitude}, ${record2.longitude} approved from the review panel; the decision persists for those coordinates of that origin.`, { planid: record2.id });
|
|
13556
|
+
await refreshbadge();
|
|
13557
|
+
return { approved: true, origin: record2.origin, latitude: record2.latitude, longitude: record2.longitude };
|
|
13558
|
+
}
|
|
13559
|
+
case "setdevicepreset": {
|
|
13560
|
+
const inputpreset = message;
|
|
13561
|
+
const preset = devicepresetof(inputpreset.device);
|
|
13562
|
+
if (!preset) throw new Error("A reviewed device preset needs a name, positive integer width and height and a positive pixel ratio.");
|
|
13563
|
+
await memory.setdevicepreset(preset);
|
|
13564
|
+
await audit("emulation", `Stored the device preset ${preset.name} of ${preset.width} by ${preset.height} css pixels with pixel ratio ${preset.pixelratio} and the ${preset.mobile ? "mobile" : "desktop"} hint in the user curated library.`, {});
|
|
13565
|
+
return preset;
|
|
13566
|
+
}
|
|
13567
|
+
case "setnetworkpreset": {
|
|
13568
|
+
const inputpreset = message;
|
|
13569
|
+
const preset = networkpresetof(inputpreset.network);
|
|
13570
|
+
if (!preset) throw new Error("A reviewed network preset needs a name and zero or positive latency, download and upload bounds.");
|
|
13571
|
+
await memory.setnetworkpreset(preset);
|
|
13572
|
+
await audit("emulation", `Stored the network preset ${preset.name} with ${preset.latency} milliseconds latency, ${preset.download} and ${preset.upload} kilobit per second bounds${preset.offline ? " and the offline flag" : ""} in the user curated library.`, {});
|
|
13573
|
+
return preset;
|
|
13574
|
+
}
|
|
13575
|
+
case "setlocationpreset": {
|
|
13576
|
+
const inputpreset = message;
|
|
13577
|
+
const preset = locationpresetof(inputpreset.location);
|
|
13578
|
+
if (!preset) throw new Error("A reviewed location preset needs a name, a latitude inside -90 and 90, a longitude inside -180 and 180 and a zero or positive accuracy radius.");
|
|
13579
|
+
await memory.setlocationpreset(preset);
|
|
13580
|
+
await audit("emulation", `Stored the location preset ${preset.name} of ${preset.latitude}, ${preset.longitude} with the ${preset.accuracy} meter accuracy radius in the user curated library.`, {});
|
|
13581
|
+
return preset;
|
|
13582
|
+
}
|
|
13583
|
+
case "setagentpreset": {
|
|
13584
|
+
const inputpreset = message;
|
|
13585
|
+
const preset = agentpresetof(inputpreset.agent);
|
|
13586
|
+
if (!preset) throw new Error("A reviewed agent preset needs a user agent string of the reviewed grammar, a platform and a non-empty brand list.");
|
|
13587
|
+
await memory.setagentpreset(preset);
|
|
13588
|
+
await audit("emulation", `Stored the agent preset ${preset.name} with platform ${preset.platform} and ${preset.brands.length} brand${preset.brands.length === 1 ? "" : "s"} in the user curated library.`, {});
|
|
13589
|
+
return preset;
|
|
13590
|
+
}
|
|
13591
|
+
case "exportpresets": {
|
|
13592
|
+
const session = await memory.getsession();
|
|
13593
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Preset exports stay behind the consent gate of an active session.");
|
|
13594
|
+
const granted = await chrome.permissions.contains({ permissions: ["downloads"] }).catch(() => false);
|
|
13595
|
+
if (!granted) throw new Error("The preset export needs the downloads capability; request it from the review panel.");
|
|
13596
|
+
const file = exportpresetlibrary({ devices: await memory.getdevicepresets(), networks: await memory.getnetworkpresets(), locations: await memory.getlocationpresets(), agents: await memory.getagentpresets(), now: Date.now() });
|
|
13597
|
+
const dataurl = `data:application/json;base64,${btoa(JSON.stringify(file, null, 2))}`;
|
|
13598
|
+
await chrome.downloads.download({ url: dataurl, filename: `devthink-presets-${Date.now()}.json` });
|
|
13599
|
+
await audit("emulation", `The review panel exported the versioned preset library of ${file.devices.length + file.networks.length + file.locations.length + file.agents.length} preset${file.devices.length + file.networks.length + file.locations.length + file.agents.length === 1 ? "" : "s"} through the reviewed download flow.`, { sessionid: session.id });
|
|
13600
|
+
return { exported: file.devices.length + file.networks.length + file.locations.length + file.agents.length, version: file.version };
|
|
13601
|
+
}
|
|
13602
|
+
case "importpresets": {
|
|
13603
|
+
const inputimport = message;
|
|
13604
|
+
const library = importpresetlibrary(inputimport.file);
|
|
13605
|
+
if (!library) throw new Error("The reviewed preset file carries no valid preset of any family; the import is refused.");
|
|
13606
|
+
for (const preset of library.devices) await memory.setdevicepreset(preset);
|
|
13607
|
+
for (const preset of library.networks) await memory.setnetworkpreset(preset);
|
|
13608
|
+
for (const preset of library.locations) await memory.setlocationpreset(preset);
|
|
13609
|
+
for (const preset of library.agents) await memory.setagentpreset(preset);
|
|
13610
|
+
const session = await memory.getsession();
|
|
13611
|
+
await audit("emulation", `Imported the reviewed preset library version ${library.version} with ${library.devices.length} device, ${library.networks.length} network, ${library.locations.length} location and ${library.agents.length} agent preset${library.devices.length + library.networks.length + library.locations.length + library.agents.length === 1 ? "" : "s"} through review.`, { ...session ? { sessionid: session.id } : {} });
|
|
13612
|
+
return { imported: library.devices.length + library.networks.length + library.locations.length + library.agents.length, version: library.version };
|
|
13613
|
+
}
|
|
13614
|
+
case "setemulationretention": {
|
|
13615
|
+
const inputretention = message;
|
|
13616
|
+
const settings = await memory.getsettings();
|
|
13617
|
+
const retention = typeof inputretention.retention === "number" && Number.isInteger(inputretention.retention) && inputretention.retention >= 0 ? inputretention.retention : void 0;
|
|
13618
|
+
await memory.setsettings({ ...settings, ...retention !== void 0 ? { emulationretention: retention } : {} });
|
|
13619
|
+
await audit("configure", `The user set the reverted emulation layer state retention to ${retention === void 0 ? "keep every prior state" : retention} layer${retention === 1 ? "" : "s"}; the layer history itself always survives.`);
|
|
13620
|
+
return { emulationretention: retention };
|
|
13621
|
+
}
|
|
12399
13622
|
case "revertproxyroute": {
|
|
12400
13623
|
const inputrevert = message;
|
|
12401
13624
|
const plan = await memory.getplan();
|
|
@@ -12607,11 +13830,15 @@ async function handlerequest(message, sender) {
|
|
|
12607
13830
|
});
|
|
12608
13831
|
await stopprofileinstrumentsforrun(stoppedplan.id, "run cancel").catch(() => {
|
|
12609
13832
|
});
|
|
13833
|
+
await revertemulationforrun(stoppedplan.id, "run cancel").catch(() => {
|
|
13834
|
+
});
|
|
12610
13835
|
} else {
|
|
12611
13836
|
await closechannelsforrun("none").catch(() => {
|
|
12612
13837
|
});
|
|
12613
13838
|
await revertcontrolsforrun("none", "run cancel").catch(() => {
|
|
12614
13839
|
});
|
|
13840
|
+
await revertemulationforrun("none", "run cancel").catch(() => {
|
|
13841
|
+
});
|
|
12615
13842
|
}
|
|
12616
13843
|
for (const [runid, active] of [...activecdpsessions.entries()]) {
|
|
12617
13844
|
active.cancelled = true;
|
|
@@ -12620,6 +13847,10 @@ async function handlerequest(message, sender) {
|
|
|
12620
13847
|
await stopprofileinstrumentsforrun(runid, "run cancel").catch(() => {
|
|
12621
13848
|
});
|
|
12622
13849
|
}
|
|
13850
|
+
for (const runid of [...activeemulation.keys()]) {
|
|
13851
|
+
await revertemulationforrun(runid, "run cancel").catch(() => {
|
|
13852
|
+
});
|
|
13853
|
+
}
|
|
12623
13854
|
for (const [id, active] of [...activerecordings.entries()]) {
|
|
12624
13855
|
const finished = finishrecording(active.record, Date.now());
|
|
12625
13856
|
await memory.addmedia(finished).catch(() => {
|
|
@@ -12632,6 +13863,113 @@ async function handlerequest(message, sender) {
|
|
|
12632
13863
|
await audit("stop", "The user stopped the browser session.", { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {} });
|
|
12633
13864
|
return { stopped: true };
|
|
12634
13865
|
}
|
|
13866
|
+
case "sessionreview": {
|
|
13867
|
+
const inputreview = message;
|
|
13868
|
+
const record2 = await memory.getsessionrecord(inputreview.sessionid?.trim() ?? "");
|
|
13869
|
+
if (!record2) throw new Error(`No saved session matches ${inputreview.sessionid ?? ""}.`);
|
|
13870
|
+
return { record: record2, tabs: record2.tabs.map((tab) => ({ url: tab.url, title: tab.title, index: tab.index, forms: tab.forms.length })), captures: record2.captures, storage: record2.storage.map((entry) => ({ origin: entry.origin, keys: entry.keys.length })), cookies: record2.cookies };
|
|
13871
|
+
}
|
|
13872
|
+
case "approverestore": {
|
|
13873
|
+
const inputrestore = message;
|
|
13874
|
+
const session = await memory.getsession();
|
|
13875
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("The crash restore needs an active browser session before it reopens anything.");
|
|
13876
|
+
const record2 = await memory.getsessionrecord(inputrestore.sessionid?.trim() ?? "");
|
|
13877
|
+
if (!record2) throw new Error(`No saved session matches ${inputrestore.sessionid ?? ""}.`);
|
|
13878
|
+
if (record2.sectionsexpired) throw new Error("The saved session sections expired after the retention window; only the record metadata survives for review.");
|
|
13879
|
+
const restore = { tabpolicy: "reopen", formpolicy: "restore", capturepolicy: "link" };
|
|
13880
|
+
const outcome = await performrestore(record2, restore, session);
|
|
13881
|
+
await memory.addsessionevent({ id: randomid(), kind: "restore", at: Date.now(), tabid: session.tabid, detail: `Crash restore reopened ${outcome.restored.length} tabs of record ${record2.id}${outcome.skippedorigins.length > 0 ? ` and skipped ${outcome.skippedorigins.join(", ")}` : ""}.` });
|
|
13882
|
+
await audit("session", `The user approved the crash restore of the saved session ${record2.id}: ${outcome.restored.length} tab${outcome.restored.length === 1 ? "" : "s"} reopened in their recorded order with the scroll and form state restored${outcome.skippedorigins.length > 0 ? ` while ${outcome.skippedorigins.join(", ")} stayed skipped because their grants expired` : ""}.`, { sessionid: session.id });
|
|
13883
|
+
await refreshbadge();
|
|
13884
|
+
return { restored: outcome.restored.length, skippedorigins: outcome.skippedorigins };
|
|
13885
|
+
}
|
|
13886
|
+
case "sessiondiff": {
|
|
13887
|
+
const inputdiff = message;
|
|
13888
|
+
const left = await memory.getsessionrecord(inputdiff.left?.trim() ?? "");
|
|
13889
|
+
const right = await memory.getsessionrecord(inputdiff.right?.trim() ?? "");
|
|
13890
|
+
if (!left || !right) throw new Error("The session diff needs both saved sessions in the library.");
|
|
13891
|
+
return { changes: diffsessionrecords(left, right), leftid: left.id, rightid: right.id };
|
|
13892
|
+
}
|
|
13893
|
+
case "loadsessionfile": {
|
|
13894
|
+
const inputfile = message;
|
|
13895
|
+
let parsed;
|
|
13896
|
+
try {
|
|
13897
|
+
parsed = JSON.parse(inputfile.content ?? "");
|
|
13898
|
+
} catch {
|
|
13899
|
+
throw new Error("The selected file is not a valid session file.");
|
|
13900
|
+
}
|
|
13901
|
+
const file = importsessionfile(parsed);
|
|
13902
|
+
if (!file) throw new Error(`The selected session file failed its format version or checksum validation; only format version ${sessionfileversion} imports.`);
|
|
13903
|
+
return { formatversion: file.formatversion, records: file.records.map((record2) => ({ id: record2.id, name: record2.name, tabs: record2.tabs.length, folder: record2.folder, tags: record2.tags })), bytesize: file.bytesize };
|
|
13904
|
+
}
|
|
13905
|
+
case "importsessionrecords": {
|
|
13906
|
+
const session = await memory.getsession();
|
|
13907
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Session imports need an active browser session behind the consent gates.");
|
|
13908
|
+
const inputimport = message;
|
|
13909
|
+
const file = importsessionfile(inputimport.file);
|
|
13910
|
+
if (!file) throw new Error(`The reviewed session file failed its format version or checksum validation; only format version ${sessionfileversion} imports.`);
|
|
13911
|
+
const records = await memory.getsessionrecords();
|
|
13912
|
+
let added = 0;
|
|
13913
|
+
for (const record2 of file.records) {
|
|
13914
|
+
if (records.some((existing) => existing.id === record2.id)) continue;
|
|
13915
|
+
const unique = sessionnameunique(record2.name, [...records, ...file.records.filter((candidate) => candidate.id !== record2.id).map((candidate) => ({ id: candidate.id, name: candidate.name }))]);
|
|
13916
|
+
const name = unique.allowed ? record2.name : `${record2.name} (${record2.id.slice(0, 6)})`;
|
|
13917
|
+
await memory.addsessionrecord({ ...record2, name });
|
|
13918
|
+
added += 1;
|
|
13919
|
+
}
|
|
13920
|
+
await memory.addsessionevent({ id: randomid(), kind: "import", at: Date.now(), tabid: session.tabid, detail: `Imported ${added} session records from a reviewed file of format version ${file.formatversion}.` });
|
|
13921
|
+
await audit("session", `The review panel imported ${added} of ${file.records.length} reviewed session record${file.records.length === 1 ? "" : "s"} from a session file of format version ${file.formatversion}; every record was listed before it joined the library.`, { sessionid: session.id });
|
|
13922
|
+
return { imported: added };
|
|
13923
|
+
}
|
|
13924
|
+
case "resumerun": {
|
|
13925
|
+
const plan = await memory.getplan();
|
|
13926
|
+
const session = await memory.getsession();
|
|
13927
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("The run resume needs an active browser session.");
|
|
13928
|
+
if (!plan || plan.state !== "approved") throw new Error("The run resume needs the approved plan of the interrupted run.");
|
|
13929
|
+
const state = await memory.gettaskstate(plan.id);
|
|
13930
|
+
if (!state || !taskstatevalid(state)) throw new Error("The persisted task state is missing or corrupted; the checksum refused the resume.");
|
|
13931
|
+
const remaining = plan.steps.slice(state.stepcursor);
|
|
13932
|
+
await memory.addsessionevent({ id: randomid(), kind: "resume", at: Date.now(), tabid: session.tabid, detail: `Resumed the run ${plan.id} from step cursor ${state.stepcursor} with ${remaining.length} remaining reviewed steps.` });
|
|
13933
|
+
await audit("resume", `Resumed the run ${plan.id} from the persisted task state checkpoint at step cursor ${state.stepcursor}; ${remaining.length} reviewed step${remaining.length === 1 ? "" : "s"} remain and every one still passes the consent gates.`, { sessionid: session.id, planid: plan.id });
|
|
13934
|
+
await memory.setcrashflag(false);
|
|
13935
|
+
let executed = 0;
|
|
13936
|
+
for (const step of remaining) {
|
|
13937
|
+
const output = await executestep(step.id).catch(() => void 0);
|
|
13938
|
+
if (output === void 0) break;
|
|
13939
|
+
executed += 1;
|
|
13940
|
+
}
|
|
13941
|
+
return { resumed: true, stepcursor: state.stepcursor, remaining: remaining.length, executed };
|
|
13942
|
+
}
|
|
13943
|
+
case "setsessionretention": {
|
|
13944
|
+
const inputretention = message;
|
|
13945
|
+
const settings = await memory.getsettings();
|
|
13946
|
+
const retention = typeof inputretention.retention === "number" && Number.isInteger(inputretention.retention) && inputretention.retention >= 0 ? inputretention.retention : void 0;
|
|
13947
|
+
await memory.setsettings({ ...settings, ...retention !== void 0 ? { sessionretention: retention } : {} });
|
|
13948
|
+
await memory.applysessionexpiry(retention, Date.now());
|
|
13949
|
+
await audit("configure", `The user set the session retention to ${retention === void 0 ? "keep every section" : `${retention} millisecond${retention === 1 ? "" : "s"}`}; the record metadata always survives and no code ceiling applies.`);
|
|
13950
|
+
return { sessionretention: retention };
|
|
13951
|
+
}
|
|
13952
|
+
case "exportsessionfile": {
|
|
13953
|
+
const session = await memory.getsession();
|
|
13954
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Session exports need an active browser session behind the consent gates.");
|
|
13955
|
+
const inputexport = message;
|
|
13956
|
+
const records = await memory.getsessionrecords();
|
|
13957
|
+
const selected = Array.isArray(inputexport.ids) && inputexport.ids.length > 0 ? records.filter((record2) => inputexport.ids.includes(record2.id)) : records;
|
|
13958
|
+
if (selected.length === 0) throw new Error("No saved session matches the reviewed export ids.");
|
|
13959
|
+
const file = exportsessionfile(selected, Date.now());
|
|
13960
|
+
const payload = JSON.stringify(file);
|
|
13961
|
+
await chrome.downloads.download({ url: `data:application/json;charset=utf-8,${encodeURIComponent(payload)}`, filename: `devthink-sessions-${Date.now()}.json` }).catch(() => {
|
|
13962
|
+
throw new Error("The session file download was refused by the browser.");
|
|
13963
|
+
});
|
|
13964
|
+
await memory.addsessionevent({ id: randomid(), kind: "export", at: Date.now(), tabid: session.tabid, detail: `The review panel exported ${file.records.length} session records as a ${file.bytesize} byte file.` });
|
|
13965
|
+
await audit("export", `The review panel exported ${file.records.length} saved session record${file.records.length === 1 ? "" : "s"} as one ${file.bytesize} byte session file of format version ${file.formatversion} after the explicit export review.`, { sessionid: session.id });
|
|
13966
|
+
return { exported: file.records.length, bytes: file.bytesize };
|
|
13967
|
+
}
|
|
13968
|
+
case "clearautosnapshot": {
|
|
13969
|
+
await memory.clearautosnapshot();
|
|
13970
|
+
await audit("session", "The user cleared the reviewed auto snapshot interval; on demand captures stay the only source of session records.");
|
|
13971
|
+
return { cleared: true };
|
|
13972
|
+
}
|
|
12635
13973
|
default:
|
|
12636
13974
|
throw new Error("Unknown Devthink request.");
|
|
12637
13975
|
}
|
|
@@ -12640,6 +13978,67 @@ chrome.runtime.onMessage.addListener((message, sender, sendresponse) => {
|
|
|
12640
13978
|
handlerequest(message, sender).then((value) => sendresponse({ ok: true, value })).catch((error) => sendresponse({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
12641
13979
|
return true;
|
|
12642
13980
|
});
|
|
13981
|
+
async function detectcrash() {
|
|
13982
|
+
const plan = await memory.getplan();
|
|
13983
|
+
if (!plan || plan.state !== "approved") return;
|
|
13984
|
+
const state = await memory.gettaskstate(plan.id);
|
|
13985
|
+
if (!state || !taskstatevalid(state)) return;
|
|
13986
|
+
const marked = crashinterrupted(state, plan.steps.length, Date.now());
|
|
13987
|
+
if (marked === state || marked === void 0) return;
|
|
13988
|
+
await memory.settaskstate(marked);
|
|
13989
|
+
await memory.setcrashflag(true);
|
|
13990
|
+
await memory.addsessionevent({ id: randomid(), kind: "crash", at: Date.now(), detail: `Run ${plan.id} was interrupted by a browser restart at step cursor ${state.stepcursor} of ${plan.steps.length}.` });
|
|
13991
|
+
await audit("session", `The crash detector marked the run ${plan.id} interrupted by a browser restart at step cursor ${state.stepcursor} of ${plan.steps.length}; the crash restore prompt stays inside the session consent model.`, { planid: plan.id });
|
|
13992
|
+
await refreshbadge().catch(() => {
|
|
13993
|
+
});
|
|
13994
|
+
}
|
|
13995
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
13996
|
+
void detectcrash();
|
|
13997
|
+
});
|
|
13998
|
+
async function maybeautosnapshot() {
|
|
13999
|
+
const state = await memory.getautosnapshot();
|
|
14000
|
+
if (!state || !Number.isFinite(state.interval.period)) return;
|
|
14001
|
+
const now = Date.now();
|
|
14002
|
+
if (now - state.lastat < state.interval.period) return;
|
|
14003
|
+
const session = await memory.getsession();
|
|
14004
|
+
const plan = await memory.getplan();
|
|
14005
|
+
if (!session || session.stoppedat || session.expiresat <= now || session.pausedat || !plan || plan.state !== "approved") return;
|
|
14006
|
+
const step = plan.steps.find((candidate) => candidate.kind === "capturesession");
|
|
14007
|
+
if (!step) return;
|
|
14008
|
+
const autorecords = (await memory.getsessionrecords()).filter((record3) => record3.auto);
|
|
14009
|
+
if (autorecords.length >= state.interval.maxsnapshots) {
|
|
14010
|
+
await memory.clearautosnapshot();
|
|
14011
|
+
await audit("session", `The reviewed auto snapshot interval stopped after ${state.interval.maxsnapshots} snapshot${state.interval.maxsnapshots === 1 ? "" : "s"}; the retention window of ${state.interval.expiry} millisecond${state.interval.expiry === 1 ? "" : "s"} expires them by user choice.`, { sessionid: session.id, planid: plan.id });
|
|
14012
|
+
return;
|
|
14013
|
+
}
|
|
14014
|
+
const options = stepoptions2(step);
|
|
14015
|
+
const snapshot2 = snapshotplanof(options.snapshot);
|
|
14016
|
+
if (!snapshot2) return;
|
|
14017
|
+
const record2 = await capturesessionrecord({ ...snapshot2, ...snapshot2.auto !== void 0 ? { auto: snapshot2.auto } : {} }, session, plan.id).catch(() => void 0);
|
|
14018
|
+
if (!record2) return;
|
|
14019
|
+
const auto = { ...record2, auto: true };
|
|
14020
|
+
await memory.addsessionrecord(auto);
|
|
14021
|
+
await memory.setautosnapshot({ interval: state.interval, lastat: now, count: state.count + 1 });
|
|
14022
|
+
await memory.addsessionevent({ id: randomid(), kind: "auto", at: now, tabid: session.tabid, detail: `Auto snapshot ${record2.id} captured ${record2.tabs.length} tabs on the reviewed interval.` });
|
|
14023
|
+
await memory.applysessionexpiry(state.interval.expiry > 0 ? state.interval.expiry : snapshotretentionwindow(await memory.getsettings()), now);
|
|
14024
|
+
await audit("session", `The reviewed auto snapshot interval of ${state.interval.period} milliseconds captured the session record ${record2.id} of ${record2.tabs.length} tab${record2.tabs.length === 1 ? "" : "s"}; snapshot ${state.count + 1} of the reviewed maximum of ${state.interval.maxsnapshots}.`, { sessionid: session.id, planid: plan.id });
|
|
14025
|
+
}
|
|
14026
|
+
setInterval(() => {
|
|
14027
|
+
void maybeautosnapshot().catch(() => {
|
|
14028
|
+
});
|
|
14029
|
+
}, 3e4);
|
|
14030
|
+
function schedulewakes() {
|
|
14031
|
+
const alarms = chrome.alarms;
|
|
14032
|
+
try {
|
|
14033
|
+
alarms?.create("devthinkautosnapshot", { periodInMinutes: 1 });
|
|
14034
|
+
alarms?.onAlarm?.addListener(() => {
|
|
14035
|
+
void maybeautosnapshot().catch(() => {
|
|
14036
|
+
});
|
|
14037
|
+
});
|
|
14038
|
+
} catch {
|
|
14039
|
+
}
|
|
14040
|
+
}
|
|
14041
|
+
schedulewakes();
|
|
12643
14042
|
async function reconcilewatches() {
|
|
12644
14043
|
for (const watch of await memory.getwatches()) {
|
|
12645
14044
|
if (watch.closedat !== void 0) continue;
|
|
@@ -12650,6 +14049,12 @@ async function reconcilewatches() {
|
|
|
12650
14049
|
}
|
|
12651
14050
|
reconcilewatches().catch(() => {
|
|
12652
14051
|
});
|
|
14052
|
+
async function restoreemulationstate() {
|
|
14053
|
+
const plan = await memory.getplan();
|
|
14054
|
+
if (plan) await loademulationstate(plan.id);
|
|
14055
|
+
}
|
|
14056
|
+
restoreemulationstate().catch(() => {
|
|
14057
|
+
});
|
|
12653
14058
|
chrome.runtime.onConnect.addListener((port) => {
|
|
12654
14059
|
if (port.name !== "devthinksidepanel" || port.sender?.id !== chrome.runtime.id || !port.sender.url?.startsWith(chrome.runtime.getURL(""))) return port.disconnect();
|
|
12655
14060
|
port.onMessage.addListener((message) => {
|