@wenathlan/devthink 2.0.15 → 2.0.19
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/CHANGELOG.md +45 -0
- package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
- package/bridge.d.ts.map +1 -0
- package/bridge.js +1877 -0
- package/bun.d.ts.map +1 -0
- package/bun.js +52329 -0
- package/caps/background.json +712 -0
- package/caps/cli.json +360 -0
- package/caps/library.json +3611 -0
- package/caps/mcp.json +126 -0
- package/caps/pagebridge.json +377 -0
- package/caps/popup.json +51 -0
- package/caps/sidepanel.json +522 -0
- package/checksums.txt +142 -0
- package/cli.d.ts.map +1 -0
- package/{dist/cli.js → cli.js} +2 -2
- package/companion.d.ts.map +1 -0
- package/companion.js +389 -0
- package/crossbrowser.d.ts.map +1 -0
- package/crossbrowser.js +1191 -0
- package/dashdone.d.ts.map +1 -0
- package/dashdone.js +368 -0
- package/deno.d.ts.map +1 -0
- package/deno.js +52325 -0
- package/devthink.umd.js +52346 -0
- package/fixtures/bun.ts +7 -0
- package/fixtures/cjs.js +6 -0
- package/fixtures/deno.ts +6 -0
- package/fixtures/example-org-pagestate.json +22 -0
- package/fixtures/importers/automa-workflow.json +18 -0
- package/fixtures/importers/selenium-side.json +20 -0
- package/fixtures/importers/tabular-plan.csv +7 -0
- package/fixtures/importers/uivision-macro.json +10 -0
- package/fixtures/importers/v1-plan.json +13 -0
- package/fixtures/mcp-client.mjs +14 -0
- package/fixtures/pages/checkout.html +192 -0
- package/fixtures/pages/dashboard.html +110 -0
- package/fixtures/pages/feed.html +113 -0
- package/fixtures/pages/product-grid.html +150 -0
- package/fixtures/plans/form-inventory-plan.json +11 -0
- package/fixtures/plans/release-notes-plan.json +11 -0
- package/fixtures/recipes/agentscompete.json +11 -0
- package/fixtures/recipes/agentsforms.json +11 -0
- package/fixtures/recipes/agentsmonitor.json +11 -0
- package/fixtures/recipes/agentsreview.json +29 -0
- package/fixtures/recipes/agentswarm.json +11 -0
- package/fixtures/recipes/fillcalendar.json +12 -0
- package/fixtures/recipes/fillcheckout.json +13 -0
- package/fixtures/recipes/filldependentdropdowns.json +12 -0
- package/fixtures/recipes/fillfromcsv.json +13 -0
- package/fixtures/recipes/fillloginform.json +12 -0
- package/fixtures/recipes/fillmultistepwizard.json +13 -0
- package/fixtures/recipes/fillregistration.json +12 -0
- package/fixtures/recipes/fillsearchfilters.json +12 -0
- package/fixtures/recipes/filltypeahead.json +11 -0
- package/fixtures/recipes/monitoralerts.json +12 -0
- package/fixtures/recipes/monitoravailability.json +11 -0
- package/fixtures/recipes/monitornetworkquiet.json +12 -0
- package/fixtures/recipes/monitorpagechanges.json +11 -0
- package/fixtures/recipes/monitorpricechange.json +11 -0
- package/fixtures/recipes/monitorschedule.json +12 -0
- package/fixtures/recipes/scrapedynamicprices.json +13 -0
- package/fixtures/recipes/scrapeimagegallery.json +12 -0
- package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
- package/fixtures/recipes/scrapenestedlists.json +12 -0
- package/fixtures/recipes/scrapeproductgrid.json +12 -0
- package/fixtures/recipes/scrapereviews.json +12 -0
- package/fixtures/recipes/scrapesearchresults.json +12 -0
- package/fixtures/recipes/scrapetablepagination.json +12 -0
- package/fixtures/recipes/testaccessibility.json +34 -0
- package/fixtures/recipes/testconsoleerrors.json +12 -0
- package/fixtures/recipes/testdeeplinks.json +12 -0
- package/fixtures/recipes/testformvalidation.json +13 -0
- package/fixtures/recipes/testlayoutshift.json +12 -0
- package/fixtures/recipes/testlinkcheck.json +12 -0
- package/fixtures/recipes/testloadtiming.json +12 -0
- package/fixtures/recipes/testvisualdiff.json +12 -0
- package/fixtures/workflows/changelog-digest-workflow.json +13 -0
- package/fixtures/workflows/form-survey-workflow.json +13 -0
- package/gallery.json +706 -0
- package/hardening.d.ts.map +1 -0
- package/hardening.js +188 -0
- package/headless.d.ts.map +1 -0
- package/headless.js +11522 -0
- package/http.d.ts.map +1 -0
- package/http.js +2165 -0
- package/index.cjs +54682 -0
- package/index.d.ts.map +1 -0
- package/index.js +52310 -0
- package/index.neutral.js +46343 -0
- package/{web/manifest.json → manifest.json} +1 -1
- package/mcp.d.ts.map +1 -0
- package/mcp.js +20515 -0
- package/memory.d.ts.map +1 -0
- package/{memory.ts → memory.js} +2310 -3834
- package/nativehost.template.json +10 -0
- package/node.cjs +54686 -0
- package/node.d.ts.map +1 -0
- package/pack.d.ts.map +1 -0
- package/pack.js +805 -0
- package/package.json +83 -176
- package/policy.d.ts.map +1 -0
- package/{policy.ts → policy.js} +4679 -5073
- package/progress.d.ts.map +1 -0
- package/progress.js +645 -0
- package/protocol.d.ts.map +1 -0
- package/protocol.js +8334 -0
- package/schemas/audit.schema.json +226 -0
- package/schemas/capability.schema.json +113 -0
- package/schemas/envelope.schema.json +162 -0
- package/schemas/memory.schema.json +51 -0
- package/schemas/observation.schema.json +113 -0
- package/schemas/plan.schema.json +208 -0
- package/schemas/progress.schema.json +126 -0
- package/schemas/proposal.schema.json +68 -0
- package/schemas/session.schema.json +82 -0
- package/schemas/tool.schema.json +94 -0
- package/server.d.ts.map +1 -0
- package/server.js +28877 -0
- package/umd-example.html +18 -0
- package/umd.d.ts.map +1 -0
- package/accounts.ts +0 -2087
- package/agent.ts +0 -2331
- package/antigravity.ts +0 -2139
- package/apifreeze.ts +0 -4146
- package/auth.ts +0 -642
- package/background.ts +0 -16837
- package/bridge.ts +0 -2080
- package/build.ts +0 -54
- package/bun.ts +0 -26
- package/capture.ts +0 -1614
- package/checksecrets.ts +0 -72
- package/cjs.ts +0 -9
- package/cli.ts +0 -3500
- package/commands.ts +0 -1101
- package/companion.ts +0 -450
- package/compatibility.ts +0 -37
- package/config.ts +0 -3137
- package/constants.ts +0 -943
- package/core.ts +0 -1473
- package/crossbrowser.ts +0 -1359
- package/dashdone.ts +0 -605
- package/data.ts +0 -943
- package/database.ts +0 -133
- package/debug.ts +0 -850
- package/deno.ts +0 -27
- package/devthink.ts +0 -707
- package/dist/accounts.d.ts +0 -409
- package/dist/agent.d.ts +0 -837
- package/dist/antigravity.d.ts +0 -380
- package/dist/apifreeze.d.ts +0 -113
- package/dist/auth.d.ts +0 -224
- package/dist/capture.d.ts +0 -595
- package/dist/cjs.d.ts +0 -6
- package/dist/commands.d.ts +0 -310
- package/dist/compatibility.d.ts +0 -17
- package/dist/config.d.ts +0 -1853
- package/dist/constants.d.ts +0 -637
- package/dist/core.d.ts +0 -361
- package/dist/data.d.ts +0 -330
- package/dist/database.d.ts +0 -24
- package/dist/debug.d.ts +0 -270
- package/dist/engine.d.ts +0 -74
- package/dist/environments.d.ts +0 -244
- package/dist/evidence.d.ts +0 -277
- package/dist/export.d.ts +0 -311
- package/dist/fingerprint.d.ts +0 -594
- package/dist/flow.d.ts +0 -300
- package/dist/gates.d.ts +0 -103
- package/dist/identity.d.ts +0 -51
- package/dist/ids.d.ts +0 -7
- package/dist/llm.d.ts +0 -652
- package/dist/models.d.ts +0 -1807
- package/dist/net.d.ts +0 -325
- package/dist/neutral.d.ts +0 -41
- package/dist/oauth.d.ts +0 -703
- package/dist/page.d.ts +0 -1166
- package/dist/perf.d.ts +0 -481
- package/dist/plan.d.ts +0 -134
- package/dist/project.d.ts +0 -395
- package/dist/providers.d.ts +0 -37
- package/dist/quota.d.ts +0 -453
- package/dist/recovery.d.ts +0 -621
- package/dist/request.d.ts +0 -1492
- package/dist/run.d.ts +0 -779
- package/dist/runtime.d.ts +0 -212
- package/dist/search.d.ts +0 -375
- package/dist/security.d.ts +0 -534
- package/dist/serve.d.ts +0 -136
- package/dist/session.d.ts +0 -296
- package/dist/storage.d.ts +0 -53
- package/dist/streaming.d.ts +0 -1119
- package/dist/swarm.d.ts +0 -328
- package/dist/tools.d.ts +0 -170
- package/dist/types.d.ts +0 -8357
- package/dist/utils.d.ts +0 -82
- package/dist/version.d.ts +0 -7
- package/dist/versionregistry.d.ts +0 -406
- package/dist/views.d.ts +0 -579
- package/dist/workbenchsession.d.ts +0 -69
- package/dist/workflow.d.ts +0 -649
- package/docs/readme.md +0 -92
- package/docs/schemas/antigravity.json +0 -192
- package/engine.ts +0 -1524
- package/environments.ts +0 -1005
- package/evidence.ts +0 -643
- package/export.ts +0 -889
- package/fingerprint.ts +0 -1458
- package/flow.ts +0 -1190
- package/gates.ts +0 -307
- package/gateway.ts +0 -34
- package/hardening.ts +0 -191
- package/headless.ts +0 -369
- package/http.ts +0 -2110
- package/identity.ts +0 -246
- package/ids.ts +0 -26
- package/index.ts +0 -369
- package/llm.ts +0 -2682
- package/mcp.ts +0 -1835
- package/models.ts +0 -3805
- package/modes.ts +0 -43
- package/net.ts +0 -1214
- package/neutral.ts +0 -362
- package/node.ts +0 -26
- package/oauth.ts +0 -2216
- package/offscreen.ts +0 -170
- package/pack.ts +0 -956
- package/page.ts +0 -7824
- package/pagebridge.ts +0 -1116
- package/perf.ts +0 -957
- package/plan.ts +0 -672
- package/plugin.ts +0 -76
- package/prisma.config.ts +0 -27
- package/progress.ts +0 -1308
- package/project.ts +0 -1291
- package/protocol.ts +0 -4187
- package/providers.ts +0 -246
- package/quota.ts +0 -1887
- package/recovery.ts +0 -2433
- package/request.ts +0 -4482
- package/run.ts +0 -2135
- package/runtime.ts +0 -526
- package/search.ts +0 -1760
- package/security.ts +0 -1673
- package/serve.ts +0 -419
- package/server.ts +0 -5538
- package/session.ts +0 -1162
- package/storage.ts +0 -182
- package/streaming.ts +0 -4101
- package/swarm.ts +0 -884
- package/sync.ts +0 -56
- package/tests/agentcert.test.ts +0 -128
- package/tests/agentfleet.test.ts +0 -214
- package/tests/agentreview.test.ts +0 -241
- package/tests/agentstream.test.ts +0 -335
- package/tests/agentwork.test.ts +0 -356
- package/tests/apifreeze.test.ts +0 -290
- package/tests/apimap.test.ts +0 -112
- package/tests/approvalgate.test.ts +0 -175
- package/tests/artifactmanifest.test.ts +0 -204
- package/tests/attentionfeed.test.ts +0 -163
- package/tests/backgroundruns.test.ts +0 -100
- package/tests/batchscheduling.test.ts +0 -131
- package/tests/blackboard.test.ts +0 -238
- package/tests/bridge.test.ts +0 -1088
- package/tests/browsercoverage.test.ts +0 -134
- package/tests/browserpolyfills.test.ts +0 -124
- package/tests/build.mjs +0 -1260
- package/tests/clientauth.test.ts +0 -223
- package/tests/clitools.test.ts +0 -578
- package/tests/confirmgates.test.ts +0 -190
- package/tests/containerpack.test.ts +0 -143
- package/tests/controlflow.test.ts +0 -2120
- package/tests/coordination.test.ts +0 -345
- package/tests/copyscreen.test.ts +0 -139
- package/tests/core.test.ts +0 -417
- package/tests/costcert.test.ts +0 -95
- package/tests/crossbrowser.test.ts +0 -115
- package/tests/cspaudit.test.ts +0 -79
- package/tests/dashdone.test.ts +0 -615
- package/tests/datagrid.test.ts +0 -137
- package/tests/doccheck.test.ts +0 -107
- package/tests/dombatch.test.ts +0 -109
- package/tests/emulation.test.ts +0 -282
- package/tests/environments.test.ts +0 -259
- package/tests/evidenceviews.test.ts +0 -88
- package/tests/exporttools.test.ts +0 -185
- package/tests/firefoxprep.test.ts +0 -134
- package/tests/flowlibrary.test.ts +0 -242
- package/tests/flowrun.test.ts +0 -303
- package/tests/forensics.test.ts +0 -439
- package/tests/gateway.test.ts +0 -939
- package/tests/headlesslib.test.ts +0 -171
- package/tests/httpstream.test.ts +0 -277
- package/tests/immutablelog.test.ts +0 -157
- package/tests/inboundguard.test.ts +0 -177
- package/tests/lazyload.test.ts +0 -72
- package/tests/librarymodes.test.ts +0 -623
- package/tests/llm.test.ts +0 -696
- package/tests/manifest.test.ts +0 -226
- package/tests/maskinputs.test.ts +0 -150
- package/tests/matrixverify.test.ts +0 -111
- package/tests/mavenpack.test.ts +0 -75
- package/tests/mcpserver.test.ts +0 -704
- package/tests/memory.test.ts +0 -4155
- package/tests/memorycare.test.ts +0 -230
- package/tests/migrateplan.test.ts +0 -349
- package/tests/minimization.test.ts +0 -414
- package/tests/modelroute.test.ts +0 -112
- package/tests/multiagent.test.ts +0 -440
- package/tests/nativebridge.test.ts +0 -807
- package/tests/navigation.test.ts +0 -284
- package/tests/nugetpack.test.ts +0 -83
- package/tests/orchestration.test.ts +0 -449
- package/tests/originpolicy.test.ts +0 -405
- package/tests/outputcompare.test.ts +0 -97
- package/tests/package.test.ts +0 -91
- package/tests/pagebridge.test.ts +0 -5909
- package/tests/pentest.test.ts +0 -75
- package/tests/perfrecords.test.ts +0 -257
- package/tests/permdiff.test.ts +0 -85
- package/tests/phishguard.test.ts +0 -128
- package/tests/pickerviews.test.ts +0 -155
- package/tests/pipelines.test.ts +0 -381
- package/tests/planlint.test.ts +0 -240
- package/tests/planreview.test.ts +0 -383
- package/tests/platformtargets.test.ts +0 -56
- package/tests/policy.test.ts +0 -8284
- package/tests/portability.test.ts +0 -96
- package/tests/progress.test.ts +0 -1036
- package/tests/promptlibrary.test.ts +0 -116
- package/tests/protocol.test.ts +0 -4793
- package/tests/publishingpipeline.test.ts +0 -314
- package/tests/quickactions.test.ts +0 -164
- package/tests/readiness.test.ts +0 -78
- package/tests/recipes.test.ts +0 -163
- package/tests/redactshots.test.ts +0 -216
- package/tests/registernvidiakeys.ts +0 -51
- package/tests/relayserve.test.ts +0 -304
- package/tests/releasenotes.test.ts +0 -276
- package/tests/resilience.test.ts +0 -467
- package/tests/resourceaware.test.ts +0 -187
- package/tests/runbudget.test.ts +0 -125
- package/tests/runhistory.test.ts +0 -299
- package/tests/runreplay.test.ts +0 -113
- package/tests/runresume.test.ts +0 -184
- package/tests/runstate.test.ts +0 -288
- package/tests/runtimeadapters.test.ts +0 -251
- package/tests/safariskeleton.test.ts +0 -115
- package/tests/sandboxframe.test.ts +0 -196
- package/tests/sbom.test.ts +0 -105
- package/tests/secretvault.test.ts +0 -188
- package/tests/selcache.test.ts +0 -111
- package/tests/sessioninterface.test.ts +0 -791
- package/tests/sessions.test.ts +0 -591
- package/tests/siteprefs.test.ts +0 -228
- package/tests/snapshotdelta.test.ts +0 -165
- package/tests/statusviews.test.ts +0 -237
- package/tests/stepmeter.test.ts +0 -200
- package/tests/streamparse.test.ts +0 -163
- package/tests/stylepruning.test.ts +0 -90
- package/tests/surfaces.test.ts +0 -282
- package/tests/sweep.test.ts +0 -78
- package/tests/syncbridge.test.ts +0 -164
- package/tests/taskqueue.test.ts +0 -206
- package/tests/toolcalls.test.ts +0 -336
- package/tests/toolcatalog.test.ts +0 -201
- package/tests/torture-agent.test.ts +0 -706
- package/tests/torture-auth.test.ts +0 -461
- package/tests/torture-commands.test.ts +0 -895
- package/tests/torture-data.test.ts +0 -923
- package/tests/torture-environments.test.ts +0 -285
- package/tests/torture-export.test.ts +0 -263
- package/tests/torture-http.test.ts +0 -1379
- package/tests/torture-llm.test.ts +0 -808
- package/tests/torture-memory.test.ts +0 -1084
- package/tests/torture-plan.test.ts +0 -829
- package/tests/torture-run.test.ts +0 -626
- package/tests/torture-session.test.ts +0 -928
- package/tests/torture-swarm.test.ts +0 -1245
- package/tests/torture-tools.test.ts +0 -964
- package/tests/torture-workflow.test.ts +0 -2354
- package/tests/tourviews.test.ts +0 -52
- package/tests/transparency.test.ts +0 -147
- package/tests/trigger.test.ts +0 -941
- package/tests/v1sunset.test.ts +0 -428
- package/tests/virtlist.test.ts +0 -94
- package/tests/vision.test.ts +0 -540
- package/tests/vsixpack.test.ts +0 -145
- package/tests/webapi.test.ts +0 -523
- package/tests/workflow.test.ts +0 -1785
- package/tests/workfloweditor.test.ts +0 -759
- package/tests/xpipack.test.ts +0 -128
- package/tools.ts +0 -734
- package/types.ts +0 -9908
- package/umd.ts +0 -9
- package/utils.ts +0 -249
- package/version.ts +0 -8
- package/versionregistry.ts +0 -1310
- package/views.ts +0 -2153
- package/vitest.config.ts +0 -12
- package/web/App.tsx +0 -78
- package/web/README.md +0 -47
- package/web/capacitor.config.ts +0 -46
- package/web/const.ts +0 -19
- package/web/control.shell.tsx +0 -66
- package/web/dashboardpage.ts +0 -554
- package/web/db.ts +0 -493
- package/web/design.html +0 -541
- package/web/gatewayview/Gateway.tsx +0 -345
- package/web/gatewayview/config.ts +0 -1042
- package/web/gatewayview/definition.ts +0 -174
- package/web/icons.ts +0 -15
- package/web/optionspage.ts +0 -691
- package/web/popup.ts +0 -1651
- package/web/schema.prisma +0 -176
- package/web/sidepanel.ts +0 -7941
- package/web/sitemanifest.json +0 -13
- package/web/terminal-ui.tsx +0 -622
- package/web/transparencypage.ts +0 -541
- package/web/vite.config.ts +0 -39
- package/web/workspace.ts +0 -23
- package/workbenchmemory.ts +0 -104
- package/workbenchsession.ts +0 -304
- package/workflow.ts +0 -4495
- /package/{dist/bun.d.ts → bun.d.ts} +0 -0
- /package/{dist/cli.d.ts → cli.d.ts} +0 -0
- /package/{dist/companion.d.ts → companion.d.ts} +0 -0
- /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
- /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
- /package/{dist/deno.d.ts → deno.d.ts} +0 -0
- /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
- /package/{dist/headless.d.ts → headless.d.ts} +0 -0
- /package/{dist/http.d.ts → http.d.ts} +0 -0
- /package/{dist/index.d.ts → index.d.ts} +0 -0
- /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
- /package/{dist/memory.d.ts → memory.d.ts} +0 -0
- /package/{dist/node.d.ts → node.d.ts} +0 -0
- /package/{dist/pack.d.ts → pack.d.ts} +0 -0
- /package/{dist/policy.d.ts → policy.d.ts} +0 -0
- /package/{dist/progress.d.ts → progress.d.ts} +0 -0
- /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
- /package/{dist/server.d.ts → server.d.ts} +0 -0
- /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/pagebridge.ts
DELETED
|
@@ -1,1116 +0,0 @@
|
|
|
1
|
-
import type { observation, regionrect, resolvedtarget, toolstep } from "./types.js";
|
|
2
|
-
import { maskformstate, maskobservation, masktypedvalues } from "./security.js";
|
|
3
|
-
import { runpageaction, type stepresult } from "./page.js";
|
|
4
|
-
import { runpageread } from "./page.js";
|
|
5
|
-
import { runpagecontrol } from "./page.js";
|
|
6
|
-
import { runinteractstep } from "./page.js";
|
|
7
|
-
import { runpointerstep } from "./page.js";
|
|
8
|
-
import { runpagenav } from "./page.js";
|
|
9
|
-
import { harvestdialoglog, type observeddialog } from "./page.js";
|
|
10
|
-
import { builda11ytree, buildpagetree, buildreader, runpageobservation } from "./page.js";
|
|
11
|
-
import { collecttables, detectlistpatterns, collectsiblings, normalizetable, runpagedetection } from "./page.js";
|
|
12
|
-
import { runpagewatch } from "./page.js";
|
|
13
|
-
import { runcdpstep, rundebugwatch, flushconsole } from "./page.js";
|
|
14
|
-
import { runprofilestep } from "./page.js";
|
|
15
|
-
import { runemulationstep, revertemulationlayer } from "./page.js";
|
|
16
|
-
import { runpageform } from "./page.js";
|
|
17
|
-
import { runpagewizard } from "./page.js";
|
|
18
|
-
import { runpagedata } from "./page.js";
|
|
19
|
-
import { checksum } from "./data.js";
|
|
20
|
-
import { clean, elementlabel as label, elementselector as selector, resolvestep } from "./page.js";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Page bridge for reviewed steps.
|
|
24
|
-
* Correlated rules for the dispatch seam, semantic snapshots, target preview with resolution, dialog log reads and the original action set live in this file.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
function stepoptions(step: toolstep): Record<string, unknown> {
|
|
28
|
-
if (!step.options) return {};
|
|
29
|
-
try {
|
|
30
|
-
const parsed = JSON.parse(step.options);
|
|
31
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {};
|
|
32
|
-
} catch {
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Routes one reviewed step to the execution environment the policy names: dom actions that need page events keep this pagecontext bridge because page events only fire in the live page, evaluate steps run inside the isolated world the background injects through the scripting api, parse heavy read kinds may offload into the offscreen worker pool behind the granted capability and a step carrying untrusted markup renders inside the sandboxframe — this bridge itself only ever executes the pagecontext steps. */
|
|
38
|
-
export function routesenvironment(step: toolstep): "pagecontext" | "elsewhere" {
|
|
39
|
-
if (step.environment !== undefined && step.environment !== "pagecontext") return "elsewhere";
|
|
40
|
-
if (step.kind === "evaluate") return "elsewhere";
|
|
41
|
-
const options = stepoptions(step);
|
|
42
|
-
if (typeof options.markup === "string" && options.markup.trim() !== "") return "elsewhere";
|
|
43
|
-
return "pagecontext";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const previewid = "devthinktargetpreview";
|
|
47
|
-
|
|
48
|
-
function clearpreview(): void {
|
|
49
|
-
document.getElementById(previewid)?.remove();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Shows an ephemeral outline only; it neither mutates page data nor dispatches page events. */
|
|
53
|
-
export function previewtarget(
|
|
54
|
-
step: toolstep,
|
|
55
|
-
expectedorigin: string,
|
|
56
|
-
): { ok: boolean; summary: string; resolvedtarget?: resolvedtarget; candidates?: string[] } {
|
|
57
|
-
if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before preview." };
|
|
58
|
-
clearpreview();
|
|
59
|
-
const resolution = resolvestep(step, document);
|
|
60
|
-
if (resolution.status === "ambiguous")
|
|
61
|
-
return {
|
|
62
|
-
ok: false,
|
|
63
|
-
summary: `The reviewed ${resolution.mode} reference matched ${resolution.candidates.length} elements: ${resolution.candidates.join("; ")}.`,
|
|
64
|
-
candidates: resolution.candidates,
|
|
65
|
-
};
|
|
66
|
-
if (resolution.status !== "resolved") return { ok: false, summary: "Reviewed target is no longer available." };
|
|
67
|
-
const target = resolution.element;
|
|
68
|
-
const rect = target.getBoundingClientRect();
|
|
69
|
-
if (rect.width <= 0 || rect.height <= 0) return { ok: false, summary: "Reviewed target is not currently visible." };
|
|
70
|
-
const overlay = document.createElement("div");
|
|
71
|
-
overlay.id = previewid;
|
|
72
|
-
overlay.setAttribute("aria-hidden", "true");
|
|
73
|
-
Object.assign(overlay.style, {
|
|
74
|
-
position: "fixed",
|
|
75
|
-
left: `${Math.max(0, rect.left - 3)}px`,
|
|
76
|
-
top: `${Math.max(0, rect.top - 3)}px`,
|
|
77
|
-
width: `${rect.width + 6}px`,
|
|
78
|
-
height: `${rect.height + 6}px`,
|
|
79
|
-
border: "3px solid #2f80ed",
|
|
80
|
-
borderRadius: "6px",
|
|
81
|
-
boxShadow: "0 0 0 3px rgba(47,128,237,.28)",
|
|
82
|
-
pointerEvents: "none",
|
|
83
|
-
zIndex: "2147483647",
|
|
84
|
-
boxSizing: "border-box",
|
|
85
|
-
});
|
|
86
|
-
document.documentElement.append(overlay);
|
|
87
|
-
window.setTimeout(clearpreview, 5000);
|
|
88
|
-
return {
|
|
89
|
-
ok: true,
|
|
90
|
-
summary: `Previewing ${label(target) || target.tagName.toLowerCase()} for five seconds.`,
|
|
91
|
-
resolvedtarget: resolution.target,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Captures the complete semantic page context for a user-approved active tab, including the a11y, reader, listpattern and tableshape observation sections. */
|
|
96
|
-
export function capturesnapshot(): observation {
|
|
97
|
-
const candidates = [
|
|
98
|
-
...document.querySelectorAll(
|
|
99
|
-
"a[href], button, input, textarea, select, [role=button], [role=link], [role=combobox], [role=option], [role=checkbox], [role=radio], [role=switch], [role=tab], details, summary",
|
|
100
|
-
),
|
|
101
|
-
];
|
|
102
|
-
const interactive = candidates
|
|
103
|
-
.map((element) => ({
|
|
104
|
-
selector: selector(element),
|
|
105
|
-
role: element.getAttribute("role") || element.tagName.toLowerCase(),
|
|
106
|
-
label: label(element),
|
|
107
|
-
}))
|
|
108
|
-
.filter((item) => item.label || item.role);
|
|
109
|
-
const forms = [...document.querySelectorAll("input, textarea, select")].map((element) => ({
|
|
110
|
-
label: label(element),
|
|
111
|
-
type: element.getAttribute("type") || element.tagName.toLowerCase(),
|
|
112
|
-
name: element.getAttribute("name") || "",
|
|
113
|
-
...(element instanceof HTMLSelectElement
|
|
114
|
-
? { options: [...element.options].map((option) => clean(option.textContent || option.value)) }
|
|
115
|
-
: {}),
|
|
116
|
-
}));
|
|
117
|
-
const text = clean(document.body?.innerText || "");
|
|
118
|
-
const tree = buildpagetree(document);
|
|
119
|
-
const tables = collecttables(document).map((entry) => {
|
|
120
|
-
const shape = normalizetable(entry.rows, entry.caption);
|
|
121
|
-
return {
|
|
122
|
-
selector: entry.selector,
|
|
123
|
-
headers: shape.headers,
|
|
124
|
-
columns: shape.columns,
|
|
125
|
-
rows: shape.rows,
|
|
126
|
-
caption: shape.caption,
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
return {
|
|
130
|
-
schemaversion: 3,
|
|
131
|
-
url: location.href,
|
|
132
|
-
title: clean(document.title),
|
|
133
|
-
textpreview: text,
|
|
134
|
-
textlength: document.body?.innerText.length ?? 0,
|
|
135
|
-
forms,
|
|
136
|
-
interactive,
|
|
137
|
-
capturedat: Date.now(),
|
|
138
|
-
mode: "passive",
|
|
139
|
-
digest: pagedigest(),
|
|
140
|
-
a11y: builda11ytree(tree),
|
|
141
|
-
reader: buildreader(tree, clean(document.title)),
|
|
142
|
-
listpattern: detectlistpatterns(collectsiblings(document)),
|
|
143
|
-
tableshape: tables,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Reads and clears the dialog log the main world handler recorded on the shared document. */
|
|
148
|
-
export function readdialogs(): observeddialog[] {
|
|
149
|
-
return harvestdialoglog(document);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Extracts full, read-only structured content for a reviewed extraction step. */
|
|
153
|
-
function extractcontent(targetselector: string | undefined, root: Document): stepresult {
|
|
154
|
-
if (!targetselector) {
|
|
155
|
-
const links = [...root.querySelectorAll("a[href]")].map((element) => {
|
|
156
|
-
const href = element instanceof HTMLAnchorElement ? (element.getAttribute("href") ?? "") : "";
|
|
157
|
-
return { text: element.textContent?.trim() ?? "", href };
|
|
158
|
-
});
|
|
159
|
-
return { ok: true, summary: `Extracted ${links.length} link entries.`, details: { links } };
|
|
160
|
-
}
|
|
161
|
-
const target = root.querySelector(targetselector);
|
|
162
|
-
if (!target) return { ok: false, summary: "Extraction target is no longer available." };
|
|
163
|
-
const text = target.textContent ?? "";
|
|
164
|
-
return { ok: true, summary: `Extracted ${text.length} characters of content.`, details: { text } };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Scrolls one reviewed target into view without reading or changing other page state. */
|
|
168
|
-
function scrolltarget(target: HTMLElement): stepresult {
|
|
169
|
-
target.scrollIntoView({ block: "center", inline: "nearest", behavior: "auto" });
|
|
170
|
-
return { ok: true, summary: `Scrolled ${label(target) || target.tagName.toLowerCase()} into view.` };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Dispatches hover events on one reviewed target. */
|
|
174
|
-
function hovertarget(target: HTMLElement): stepresult {
|
|
175
|
-
for (const type of ["pointerover", "mouseover", "pointerenter"] as const) {
|
|
176
|
-
target.dispatchEvent(
|
|
177
|
-
new PointerEvent(type, { bubbles: type !== "pointerenter", cancelable: true, composed: true }),
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
target.dispatchEvent(new MouseEvent("mouseenter", { bubbles: false, cancelable: true }));
|
|
181
|
-
return { ok: true, summary: `Hover events delivered to ${label(target) || target.tagName.toLowerCase()}.` };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** Selects one reviewed existing option; values outside the declared options are refused. */
|
|
185
|
-
function selectoption(target: HTMLElement, value: string): stepresult {
|
|
186
|
-
if (!(target instanceof HTMLSelectElement)) return { ok: false, summary: "Target is not a select element." };
|
|
187
|
-
const option = [...target.options].find(
|
|
188
|
-
(candidate) => candidate.value === value || candidate.textContent?.trim() === value,
|
|
189
|
-
);
|
|
190
|
-
if (!option) return { ok: false, summary: "Reviewed option is not part of the select element." };
|
|
191
|
-
target.value = option.value;
|
|
192
|
-
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
193
|
-
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
194
|
-
return { ok: true, summary: `Selected ${clean(option.textContent || option.value)}.` };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const readkinds: ReadonlySet<string> = new Set([
|
|
198
|
-
"readattribute",
|
|
199
|
-
"readstyle",
|
|
200
|
-
"readgeometry",
|
|
201
|
-
"readvalue",
|
|
202
|
-
"readtext",
|
|
203
|
-
"readhtml",
|
|
204
|
-
"countelements",
|
|
205
|
-
"readtable",
|
|
206
|
-
"readlinks",
|
|
207
|
-
"readimages",
|
|
208
|
-
"readmeta",
|
|
209
|
-
"readforms",
|
|
210
|
-
"readstorage",
|
|
211
|
-
"waitfor",
|
|
212
|
-
"waittext",
|
|
213
|
-
"highlight",
|
|
214
|
-
"mapclicks",
|
|
215
|
-
"verifyvisible",
|
|
216
|
-
"verifyenabled",
|
|
217
|
-
"resolvexpath",
|
|
218
|
-
]);
|
|
219
|
-
const mutatingkinds: ReadonlySet<string> = new Set([
|
|
220
|
-
"presskey",
|
|
221
|
-
"clickdeep",
|
|
222
|
-
"rightclick",
|
|
223
|
-
"doubleclick",
|
|
224
|
-
"drag",
|
|
225
|
-
"drop",
|
|
226
|
-
"upload",
|
|
227
|
-
"clear",
|
|
228
|
-
"check",
|
|
229
|
-
"uncheck",
|
|
230
|
-
"toggle",
|
|
231
|
-
"submit",
|
|
232
|
-
"setattribute",
|
|
233
|
-
"removeattribute",
|
|
234
|
-
"writestorage",
|
|
235
|
-
"evaluate",
|
|
236
|
-
"fullscreen",
|
|
237
|
-
]);
|
|
238
|
-
const controlkinds: ReadonlySet<string> = new Set([
|
|
239
|
-
"typetime",
|
|
240
|
-
"appendtext",
|
|
241
|
-
"setvalue",
|
|
242
|
-
"typeedit",
|
|
243
|
-
"keyhold",
|
|
244
|
-
"keyrelease",
|
|
245
|
-
"submitsearch",
|
|
246
|
-
"selectmulti",
|
|
247
|
-
"chooseradio",
|
|
248
|
-
"setslider",
|
|
249
|
-
"setdate",
|
|
250
|
-
"setcolor",
|
|
251
|
-
"expanddetails",
|
|
252
|
-
]);
|
|
253
|
-
const interactkinds: ReadonlySet<string> = new Set([
|
|
254
|
-
"clicktext",
|
|
255
|
-
"clickaria",
|
|
256
|
-
"clickname",
|
|
257
|
-
"pierceshadow",
|
|
258
|
-
"enterframe",
|
|
259
|
-
]);
|
|
260
|
-
const pointerkinds: ReadonlySet<string> = new Set(["movepointer", "clickpoint", "shiftclick"]);
|
|
261
|
-
const observationkinds: ReadonlySet<string> = new Set([
|
|
262
|
-
"a11ytree",
|
|
263
|
-
"readvisible",
|
|
264
|
-
"readertree",
|
|
265
|
-
"readoutline",
|
|
266
|
-
"readselection",
|
|
267
|
-
"readopengraph",
|
|
268
|
-
"readlang",
|
|
269
|
-
"detectlanguage",
|
|
270
|
-
"listshadow",
|
|
271
|
-
"listframes",
|
|
272
|
-
]);
|
|
273
|
-
const detectionkinds: ReadonlySet<string> = new Set([
|
|
274
|
-
"detectlists",
|
|
275
|
-
"detecttables",
|
|
276
|
-
"detectinfinitescroll",
|
|
277
|
-
"detectvirtual",
|
|
278
|
-
"detectlazy",
|
|
279
|
-
"detectsticky",
|
|
280
|
-
"detectscrolllock",
|
|
281
|
-
"countpages",
|
|
282
|
-
"classifypage",
|
|
283
|
-
"fingerprintsection",
|
|
284
|
-
"readscrollpos",
|
|
285
|
-
]);
|
|
286
|
-
const watchstepkinds: ReadonlySet<string> = new Set([
|
|
287
|
-
"watchmutate",
|
|
288
|
-
"watchbanner",
|
|
289
|
-
"watchfocus",
|
|
290
|
-
"waitquiet",
|
|
291
|
-
"readjson",
|
|
292
|
-
"diffsnapshots",
|
|
293
|
-
"deriveselector",
|
|
294
|
-
]);
|
|
295
|
-
const debugstepkinds: ReadonlySet<string> = new Set(["watchconsole", "watcherrors", "watchtasks"]);
|
|
296
|
-
const cdpstepkinds: ReadonlySet<string> = new Set([
|
|
297
|
-
"attachcdp",
|
|
298
|
-
"detachcdp",
|
|
299
|
-
"cdpcmd",
|
|
300
|
-
"watchcdp",
|
|
301
|
-
"setbreakpoint",
|
|
302
|
-
"stepcode",
|
|
303
|
-
"watchexpr",
|
|
304
|
-
"overridescript",
|
|
305
|
-
]);
|
|
306
|
-
const profilestepkinds: ReadonlySet<string> = new Set([
|
|
307
|
-
"measureflow",
|
|
308
|
-
"heapshot",
|
|
309
|
-
"trackmemory",
|
|
310
|
-
"profilecpu",
|
|
311
|
-
"watchshifts",
|
|
312
|
-
"traceload",
|
|
313
|
-
"capturesourcemaps",
|
|
314
|
-
]);
|
|
315
|
-
const emulationstepkinds: ReadonlySet<string> = new Set([
|
|
316
|
-
"emulatedevice",
|
|
317
|
-
"emulatenetwork",
|
|
318
|
-
"emulatelocate",
|
|
319
|
-
"setuseragent",
|
|
320
|
-
"overridepermission",
|
|
321
|
-
"blackboxscripts",
|
|
322
|
-
]);
|
|
323
|
-
const navstepkinds: ReadonlySet<string> = new Set([
|
|
324
|
-
"waitload",
|
|
325
|
-
"waiturl",
|
|
326
|
-
"followlink",
|
|
327
|
-
"spanav",
|
|
328
|
-
"spawait",
|
|
329
|
-
"rewritequery",
|
|
330
|
-
"setfragment",
|
|
331
|
-
"stopnav",
|
|
332
|
-
"prefetch",
|
|
333
|
-
"preconnect",
|
|
334
|
-
"printpdf",
|
|
335
|
-
]);
|
|
336
|
-
const formkinds: ReadonlySet<string> = new Set([
|
|
337
|
-
"fillform",
|
|
338
|
-
"filllabel",
|
|
339
|
-
"fillplaceholder",
|
|
340
|
-
"detectfields",
|
|
341
|
-
"generatevalues",
|
|
342
|
-
"readerrors",
|
|
343
|
-
"skiphoneypot",
|
|
344
|
-
"detectlogin",
|
|
345
|
-
"detecttemplate",
|
|
346
|
-
"handoffcaptcha",
|
|
347
|
-
"asksubmit",
|
|
348
|
-
"submitform",
|
|
349
|
-
"consentpassword",
|
|
350
|
-
"attachfile",
|
|
351
|
-
]);
|
|
352
|
-
const wizardkinds: ReadonlySet<string> = new Set([
|
|
353
|
-
"runwizard",
|
|
354
|
-
"selectchain",
|
|
355
|
-
"picktypeahead",
|
|
356
|
-
"pickdate",
|
|
357
|
-
"fillcard",
|
|
358
|
-
"fillcode",
|
|
359
|
-
]);
|
|
360
|
-
const datastepkinds: ReadonlySet<string> = new Set(["scrapetable", "paginateextract"]);
|
|
361
|
-
|
|
362
|
-
/** Derives the deterministic page digest of one page state from its url, title and text length; checkpoints compare their digest against it before a resume continues. */
|
|
363
|
-
export function digestof(input: { url: string; title: string; textlength: number }): string {
|
|
364
|
-
return `page:${checksum(`${input.url}|${input.title}|${input.textlength}`)}`;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/** Reports the final url of the live page after its redirect chain so the urlhistory records the url the run landed on. */
|
|
368
|
-
export function pagefinalurl(): string {
|
|
369
|
-
return location.href;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/** Reads the digest of the live page so a checkpoint revalidates the page it captured. */
|
|
373
|
-
export function pagedigest(): string {
|
|
374
|
-
return digestof({
|
|
375
|
-
url: location.href,
|
|
376
|
-
title: clean(document.title),
|
|
377
|
-
textlength: document.body?.innerText.length ?? 0,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/** The idempotencykeys this page context already executed; a replay of the same key skips the duplicate. */
|
|
382
|
-
const executedstepkeys = new Set<string>();
|
|
383
|
-
|
|
384
|
-
/** Reads the idempotency verdict of one step: a key the page context already executed skips while an unknown or absent key runs. */
|
|
385
|
-
export function idempotencyverdict(
|
|
386
|
-
step: toolstep,
|
|
387
|
-
executedkeys: ReadonlySet<string>,
|
|
388
|
-
): { skip: boolean; summary: string } {
|
|
389
|
-
if (step.idempotencykey === undefined || step.idempotencykey.trim() === "")
|
|
390
|
-
return { skip: false, summary: "The step carries no idempotencykey; it runs exactly as reviewed." };
|
|
391
|
-
if (executedkeys.has(step.idempotencykey))
|
|
392
|
-
return {
|
|
393
|
-
skip: true,
|
|
394
|
-
summary: `The idempotencykey ${step.idempotencykey} already executed in this page context; the replay skips the duplicate so the side effect never repeats.`,
|
|
395
|
-
};
|
|
396
|
-
return {
|
|
397
|
-
skip: false,
|
|
398
|
-
summary: `The idempotencykey ${step.idempotencykey} is unknown to this page context; the step runs exactly once.`,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/** Clears the executed idempotencykeys of the page context; a fresh run starts with an empty key set. */
|
|
403
|
-
export function resetidempotency(): void {
|
|
404
|
-
executedstepkeys.clear();
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/** Builds the checkpoint payload after one successfully executed sensitive step: the run, the step boundary, the completed steps and the digest of the page it captured. */
|
|
408
|
-
export function checkpointafterstep(input: {
|
|
409
|
-
step: toolstep;
|
|
410
|
-
runid: string;
|
|
411
|
-
completed: string[];
|
|
412
|
-
}): { runid: string; stepid: string; completed: string[]; digest: string } | undefined {
|
|
413
|
-
if (input.step.risk !== "sensitive") return undefined;
|
|
414
|
-
return { runid: input.runid, stepid: input.step.id, completed: [...input.completed], digest: pagedigest() };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/** Performs one local action after the background policy gate and a fresh target resolution; the 1.1.70 family executes steps keyed by their idempotencykey so replays stay deduplicated. */
|
|
418
|
-
export async function performstep(
|
|
419
|
-
step: toolstep,
|
|
420
|
-
expectedorigin: string,
|
|
421
|
-
rootdocument: Document = document,
|
|
422
|
-
): Promise<stepresult> {
|
|
423
|
-
if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before action." };
|
|
424
|
-
const idempotency = idempotencyverdict(step, executedstepkeys);
|
|
425
|
-
if (idempotency.skip)
|
|
426
|
-
return { ok: true, summary: idempotency.summary, details: { replay: true, idempotencykey: step.idempotencykey } };
|
|
427
|
-
const output = await performstepinner(step, expectedorigin, rootdocument);
|
|
428
|
-
if (output.ok && step.idempotencykey !== undefined && step.idempotencykey.trim() !== "")
|
|
429
|
-
executedstepkeys.add(step.idempotencykey);
|
|
430
|
-
return output;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/** Runs the reviewed dispatch of one step inside the page after the idempotency gate cleared it. */
|
|
434
|
-
async function performstepinner(step: toolstep, expectedorigin: string, rootdocument: Document): Promise<stepresult> {
|
|
435
|
-
if (step.kind === "observe") return { ok: true, summary: "Observation completed." };
|
|
436
|
-
if (step.kind === "wait") {
|
|
437
|
-
const requested = step.value ? Number.parseInt(step.value, 10) : 250;
|
|
438
|
-
const duration = Number.isFinite(requested) && requested > 0 ? requested : 0;
|
|
439
|
-
return new Promise((resolve) =>
|
|
440
|
-
window.setTimeout(
|
|
441
|
-
() => resolve({ ok: true, summary: `Reviewed wait of ${duration} milliseconds completed.` }),
|
|
442
|
-
duration,
|
|
443
|
-
),
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
if (step.kind === "extract") return extractcontent(step.target, rootdocument);
|
|
447
|
-
if (step.kind === "navigate") {
|
|
448
|
-
if (!step.value || new URL(step.value).origin !== expectedorigin)
|
|
449
|
-
return { ok: false, summary: "Navigation target is outside the approved origin." };
|
|
450
|
-
location.assign(step.value);
|
|
451
|
-
return { ok: true, summary: "Navigation request sent." };
|
|
452
|
-
}
|
|
453
|
-
if (step.kind === "reload") {
|
|
454
|
-
location.reload();
|
|
455
|
-
return { ok: true, summary: "Page reload requested." };
|
|
456
|
-
}
|
|
457
|
-
if (step.kind === "back") {
|
|
458
|
-
history.back();
|
|
459
|
-
return { ok: true, summary: "History back requested." };
|
|
460
|
-
}
|
|
461
|
-
if (step.kind === "forward") {
|
|
462
|
-
history.forward();
|
|
463
|
-
return { ok: true, summary: "History forward requested." };
|
|
464
|
-
}
|
|
465
|
-
if (navstepkinds.has(step.kind)) return await runpagenav(step, rootdocument);
|
|
466
|
-
if (step.kind === "writeclipboard") {
|
|
467
|
-
const text = step.value ?? "";
|
|
468
|
-
await navigator.clipboard.writeText(text);
|
|
469
|
-
return {
|
|
470
|
-
ok: true,
|
|
471
|
-
summary: `Wrote ${text.length} reviewed character${text.length === 1 ? "" : "s"} to the clipboard with payload hash ${checksum(text)}.`,
|
|
472
|
-
details: { length: text.length, hash: checksum(text), destination: "clipboard" },
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
if (step.kind === "scrollpage") {
|
|
476
|
-
const options = stepoptions(step);
|
|
477
|
-
window.scrollBy({
|
|
478
|
-
left: typeof options.x === "number" ? options.x : 0,
|
|
479
|
-
top: typeof options.y === "number" ? options.y : 600,
|
|
480
|
-
behavior: "auto",
|
|
481
|
-
});
|
|
482
|
-
return { ok: true, summary: "Window scrolled by the reviewed amounts." };
|
|
483
|
-
}
|
|
484
|
-
if (step.kind === "scrollend") {
|
|
485
|
-
window.scrollTo(0, document.documentElement.scrollHeight);
|
|
486
|
-
return { ok: true, summary: "Window scrolled to the page end." };
|
|
487
|
-
}
|
|
488
|
-
if (step.kind === "scrolltop") {
|
|
489
|
-
window.scrollTo(0, 0);
|
|
490
|
-
return { ok: true, summary: "Window scrolled to the page top." };
|
|
491
|
-
}
|
|
492
|
-
const resolution = resolvestep(step, rootdocument);
|
|
493
|
-
if (resolution.status === "ambiguous") {
|
|
494
|
-
return {
|
|
495
|
-
ok: false,
|
|
496
|
-
summary: `The reviewed ${resolution.mode} reference matched ${resolution.candidates.length} elements: ${resolution.candidates.join("; ")}.`,
|
|
497
|
-
details: { mode: resolution.mode, candidates: resolution.candidates },
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
const element = resolution.status === "resolved" ? resolution.element : null;
|
|
501
|
-
let result: stepresult | Promise<stepresult>;
|
|
502
|
-
if (formkinds.has(step.kind)) result = runpageform(step, element, rootdocument);
|
|
503
|
-
else if (wizardkinds.has(step.kind)) result = runpagewizard(step, element, rootdocument);
|
|
504
|
-
else if (datastepkinds.has(step.kind)) result = runpagedata(step, element, rootdocument);
|
|
505
|
-
else if (readkinds.has(step.kind)) result = runpageread(step, element, rootdocument);
|
|
506
|
-
else if (controlkinds.has(step.kind)) result = runpagecontrol(step, element, rootdocument);
|
|
507
|
-
else if (interactkinds.has(step.kind)) return await runinteractstep(step, expectedorigin, performstep);
|
|
508
|
-
else if (pointerkinds.has(step.kind)) result = runpointerstep(step, resolution);
|
|
509
|
-
else if (observationkinds.has(step.kind)) result = runpageobservation(step, element, rootdocument);
|
|
510
|
-
else if (detectionkinds.has(step.kind)) result = runpagedetection(step, element, rootdocument);
|
|
511
|
-
else if (watchstepkinds.has(step.kind)) result = runpagewatch(step, element, rootdocument);
|
|
512
|
-
else if (debugstepkinds.has(step.kind)) return await rundebugwatch(step);
|
|
513
|
-
else if (cdpstepkinds.has(step.kind)) return await runcdpstep(step);
|
|
514
|
-
else if (profilestepkinds.has(step.kind)) return await runprofilestep(step);
|
|
515
|
-
else if (emulationstepkinds.has(step.kind)) return await runemulationstep(step);
|
|
516
|
-
else if (mutatingkinds.has(step.kind)) result = runpageaction(step, element);
|
|
517
|
-
else {
|
|
518
|
-
if (!element) return { ok: false, summary: "Action target is no longer available." };
|
|
519
|
-
if (step.kind === "scrollby") {
|
|
520
|
-
const options = stepoptions(step);
|
|
521
|
-
element.scrollBy({
|
|
522
|
-
left: typeof options.x === "number" ? options.x : 0,
|
|
523
|
-
top: typeof options.y === "number" ? options.y : 600,
|
|
524
|
-
behavior: "auto",
|
|
525
|
-
});
|
|
526
|
-
result = { ok: true, summary: "Container scrolled by the reviewed amounts." };
|
|
527
|
-
} else if (step.kind === "focus") {
|
|
528
|
-
element.focus();
|
|
529
|
-
result = { ok: true, summary: "Target focused." };
|
|
530
|
-
} else if (step.kind === "inspect")
|
|
531
|
-
result = { ok: true, summary: `Target: ${label(element) || element.tagName.toLowerCase()}.` };
|
|
532
|
-
else if (step.kind === "click") {
|
|
533
|
-
element.click();
|
|
534
|
-
result = { ok: true, summary: "Reviewed click completed." };
|
|
535
|
-
} else if (step.kind === "scroll") result = scrolltarget(element);
|
|
536
|
-
else if (step.kind === "hover") result = hovertarget(element);
|
|
537
|
-
else if (step.kind === "select") result = selectoption(element, step.value ?? "");
|
|
538
|
-
else if (step.kind === "type") {
|
|
539
|
-
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement))
|
|
540
|
-
return { ok: false, summary: "Target cannot receive text." };
|
|
541
|
-
if (typeof step.value !== "string") return { ok: false, summary: "Approved text is absent." };
|
|
542
|
-
element.focus();
|
|
543
|
-
element.value = step.value;
|
|
544
|
-
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
545
|
-
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
546
|
-
result = { ok: true, summary: "Approved text entered." };
|
|
547
|
-
} else return { ok: false, summary: "Unsupported action." };
|
|
548
|
-
}
|
|
549
|
-
const output = await result;
|
|
550
|
-
if (resolution.status === "resolved") {
|
|
551
|
-
return {
|
|
552
|
-
...output,
|
|
553
|
-
details: { ...(output.details ?? {}), mode: resolution.target.mode, resolvedtarget: resolution.target },
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
return output;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/** Measures the full scroll width and height before any stitching begins, beside the viewport geometry, the device pixel ratio and the current scroll position. */
|
|
560
|
-
export function measurepage(): {
|
|
561
|
-
scrollwidth: number;
|
|
562
|
-
scrollheight: number;
|
|
563
|
-
viewportwidth: number;
|
|
564
|
-
viewportheight: number;
|
|
565
|
-
pixelratio: number;
|
|
566
|
-
scrollx: number;
|
|
567
|
-
scrolly: number;
|
|
568
|
-
} {
|
|
569
|
-
const root = document.documentElement;
|
|
570
|
-
return {
|
|
571
|
-
scrollwidth: Math.max(root.scrollWidth, document.body?.scrollWidth ?? 0),
|
|
572
|
-
scrollheight: Math.max(root.scrollHeight, document.body?.scrollHeight ?? 0),
|
|
573
|
-
viewportwidth: window.innerWidth,
|
|
574
|
-
viewportheight: window.innerHeight,
|
|
575
|
-
pixelratio: window.devicePixelRatio || 1,
|
|
576
|
-
scrollx: window.scrollX,
|
|
577
|
-
scrolly: window.scrollY,
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/** Returns the pixel ratio scaled rect of one target element with the viewport crossing flag for the tiled fallback. */
|
|
582
|
-
export function elementrect(selector: string): {
|
|
583
|
-
ok: boolean;
|
|
584
|
-
rect?: regionrect;
|
|
585
|
-
pixelratio?: number;
|
|
586
|
-
crossesviewport?: boolean;
|
|
587
|
-
summary: string;
|
|
588
|
-
} {
|
|
589
|
-
const target = document.querySelector(selector);
|
|
590
|
-
if (!target) return { ok: false, summary: "The reviewed capture element is no longer available." };
|
|
591
|
-
const bounds = target.getBoundingClientRect();
|
|
592
|
-
if (bounds.width <= 0 || bounds.height <= 0)
|
|
593
|
-
return { ok: false, summary: "The reviewed capture element is not currently visible." };
|
|
594
|
-
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
595
|
-
const rect: regionrect = {
|
|
596
|
-
x: Math.round(bounds.left + window.scrollX),
|
|
597
|
-
y: Math.round(bounds.top + window.scrollY),
|
|
598
|
-
width: Math.round(bounds.width),
|
|
599
|
-
height: Math.round(bounds.height),
|
|
600
|
-
};
|
|
601
|
-
const viewrect: regionrect = { x: bounds.left, y: bounds.top, width: bounds.width, height: bounds.height };
|
|
602
|
-
return {
|
|
603
|
-
ok: true,
|
|
604
|
-
rect,
|
|
605
|
-
pixelratio: window.devicePixelRatio || 1,
|
|
606
|
-
crossesviewport:
|
|
607
|
-
viewrect.x < 0 ||
|
|
608
|
-
viewrect.y < 0 ||
|
|
609
|
-
viewrect.x + viewrect.width > viewport.width ||
|
|
610
|
-
viewrect.y + viewrect.height > viewport.height,
|
|
611
|
-
summary: `Measured the capture element at ${rect.width} by ${rect.height} css pixels.`,
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/** Resolves the unique element references of one selector for the foreach executor of the workflow control engine: every matched element reports its stable selector without mutating the page. */
|
|
616
|
-
export function queryelements(query: string): { ok: boolean; selectors: string[]; summary: string } {
|
|
617
|
-
const matched = [...document.querySelectorAll(query)];
|
|
618
|
-
if (matched.length === 0)
|
|
619
|
-
return { ok: true, selectors: [], summary: `The reviewed selector ${query} matched no element.` };
|
|
620
|
-
return {
|
|
621
|
-
ok: true,
|
|
622
|
-
selectors: matched.map((element) => selector(element)),
|
|
623
|
-
summary: `Resolved ${matched.length} element reference${matched.length === 1 ? "" : "s"} of the reviewed selector ${query}.`,
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
const scrollbarstyleid = "devthinkcapturehider";
|
|
628
|
-
|
|
629
|
-
/** Prepares the page for capture: hides the capture scrollbars through a scoped style rule and returns the original scroll position. */
|
|
630
|
-
export function preparecapture(): { scrollx: number; scrolly: number } {
|
|
631
|
-
if (!document.getElementById(scrollbarstyleid)) {
|
|
632
|
-
const style = document.createElement("style");
|
|
633
|
-
style.id = scrollbarstyleid;
|
|
634
|
-
style.setAttribute("aria-hidden", "true");
|
|
635
|
-
style.textContent =
|
|
636
|
-
"html::-webkit-scrollbar,body::-webkit-scrollbar{display:none!important}html{scrollbar-width:none!important}";
|
|
637
|
-
document.documentElement.append(style);
|
|
638
|
-
}
|
|
639
|
-
return { scrollx: window.scrollX, scrolly: window.scrollY };
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/** Scrolls the page to one reviewed tile offset and reports the settled position. */
|
|
643
|
-
export function scrollcapture(x: number, y: number): { x: number; y: number } {
|
|
644
|
-
window.scrollTo(x, y);
|
|
645
|
-
return { x: window.scrollX, y: window.scrollY };
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/** Restores the original scroll position after the last tile and removes the capture scrollbar hider. */
|
|
649
|
-
export function restorecapture(state: { scrollx: number; scrolly: number }): void {
|
|
650
|
-
document.getElementById(scrollbarstyleid)?.remove();
|
|
651
|
-
window.scrollTo(state.scrollx, state.scrolly);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/** Scrolls one scrollable container to a reviewed step top and reports its geometry. */
|
|
655
|
-
export function scrollcontainercapture(
|
|
656
|
-
selector: string,
|
|
657
|
-
top: number,
|
|
658
|
-
): { ok: boolean; top?: number; height?: number; viewportheight?: number; summary: string } {
|
|
659
|
-
const container = document.querySelector(selector);
|
|
660
|
-
if (!container) return { ok: false, summary: "The reviewed scrollable container is no longer available." };
|
|
661
|
-
if (!(container instanceof HTMLElement))
|
|
662
|
-
return { ok: false, summary: "The reviewed scrollable container cannot scroll." };
|
|
663
|
-
container.scrollTo({ top, behavior: "auto" });
|
|
664
|
-
return {
|
|
665
|
-
ok: true,
|
|
666
|
-
top: container.scrollTop,
|
|
667
|
-
height: container.scrollHeight,
|
|
668
|
-
viewportheight: container.clientHeight,
|
|
669
|
-
summary: `Scrolled the container to ${container.scrollTop} of ${container.scrollHeight} pixels.`,
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/** Waits the reviewed settle time between capture tiles. */
|
|
674
|
-
export function waitsettle(milliseconds: number): Promise<void> {
|
|
675
|
-
return new Promise((resolve) => window.setTimeout(resolve, Math.max(0, milliseconds)));
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/** Collects the visible text of one pdf report segment: every block element whose bounds intersect the reviewed vertical range contributes its text, so paginated reports split at reviewed break points. */
|
|
679
|
-
export function pdfsegment(top: number, height: number): { ok: boolean; text: string; summary: string } {
|
|
680
|
-
const blocks = [
|
|
681
|
-
...document.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li,td,th,blockquote,pre,figcaption,section,article > div"),
|
|
682
|
-
];
|
|
683
|
-
const lines: string[] = [];
|
|
684
|
-
for (const block of blocks) {
|
|
685
|
-
const bounds = block.getBoundingClientRect();
|
|
686
|
-
const blocktop = bounds.top + window.scrollY;
|
|
687
|
-
const blockbottom = blocktop + bounds.height;
|
|
688
|
-
if (blockbottom <= top || blocktop >= top + height) continue;
|
|
689
|
-
const text = clean(block.textContent ?? "");
|
|
690
|
-
if (text) lines.push(text);
|
|
691
|
-
}
|
|
692
|
-
const text = lines.join("\n");
|
|
693
|
-
return {
|
|
694
|
-
ok: true,
|
|
695
|
-
text,
|
|
696
|
-
summary: `Collected ${text.length} characters of the report segment at ${Math.round(top)} to ${Math.round(top + height)} pixels.`,
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/** Resolves the scroll tops of the reviewed pdf break point selectors for paginated reports. */
|
|
701
|
-
export function pdfbreaks(selectors: string[]): Array<{ selector: string; top: number }> {
|
|
702
|
-
const resolved: Array<{ selector: string; top: number }> = [];
|
|
703
|
-
for (const selector of selectors) {
|
|
704
|
-
const target = document.querySelector(selector);
|
|
705
|
-
if (!target) continue;
|
|
706
|
-
resolved.push({ selector, top: Math.round(target.getBoundingClientRect().top + window.scrollY) });
|
|
707
|
-
}
|
|
708
|
-
return resolved;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/** Grabs one still frame of a video element at the reviewed timestamp: the video seeks, pauses and draws to a canvas that encodes as an image; cross origin videos without cors refuse the draw honestly. */
|
|
712
|
-
export async function videoframe(
|
|
713
|
-
selector: string,
|
|
714
|
-
timestamp: number | undefined,
|
|
715
|
-
poster: boolean,
|
|
716
|
-
): Promise<{ ok: boolean; dataurl?: string; width?: number; height?: number; summary: string }> {
|
|
717
|
-
const target = document.querySelector(selector);
|
|
718
|
-
if (!(target instanceof HTMLVideoElement))
|
|
719
|
-
return { ok: false, summary: "The reviewed frame source is not a video element." };
|
|
720
|
-
target.pause();
|
|
721
|
-
if (
|
|
722
|
-
typeof timestamp === "number" &&
|
|
723
|
-
Number.isFinite(timestamp) &&
|
|
724
|
-
timestamp >= 0 &&
|
|
725
|
-
timestamp <= (target.duration || 0)
|
|
726
|
-
) {
|
|
727
|
-
await new Promise<void>((resolve) => {
|
|
728
|
-
const done = (): void => resolve();
|
|
729
|
-
target.addEventListener("seeked", done, { once: true });
|
|
730
|
-
target.currentTime = timestamp;
|
|
731
|
-
window.setTimeout(done, 1500);
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
const width = target.videoWidth || target.clientWidth || 1;
|
|
735
|
-
const height = target.videoHeight || target.clientHeight || 1;
|
|
736
|
-
try {
|
|
737
|
-
const canvas = document.createElement("canvas");
|
|
738
|
-
canvas.width = width;
|
|
739
|
-
canvas.height = height;
|
|
740
|
-
const context = canvas.getContext("2d");
|
|
741
|
-
if (!context) return { ok: false, summary: "The frame grab could not create a canvas context." };
|
|
742
|
-
context.drawImage(target, 0, 0, width, height);
|
|
743
|
-
const dataurl = canvas.toDataURL("image/png");
|
|
744
|
-
return {
|
|
745
|
-
ok: dataurl.length > 100,
|
|
746
|
-
dataurl,
|
|
747
|
-
width,
|
|
748
|
-
height,
|
|
749
|
-
summary: `Grabbed the video frame at ${target.currentTime.toFixed(2)} seconds of ${width} by ${height} pixels${poster ? " as the poster frame" : ""}.`,
|
|
750
|
-
};
|
|
751
|
-
} catch {
|
|
752
|
-
return {
|
|
753
|
-
ok: false,
|
|
754
|
-
summary: "The video frame draw was refused; cross origin videos need cors headers before frames can be read.",
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
/** Reads the playback state of one video element of the 1.1.77 family: the paused flag with the playback position and duration in milliseconds, so the frameocr pass refuses a playing video before any frame leaves the device; the read never pauses or seeks anything. */
|
|
760
|
-
export function videostate(selector: string): {
|
|
761
|
-
ok: boolean;
|
|
762
|
-
paused: boolean;
|
|
763
|
-
positionms: number;
|
|
764
|
-
durationms: number;
|
|
765
|
-
summary: string;
|
|
766
|
-
} {
|
|
767
|
-
const target = document.querySelector(selector);
|
|
768
|
-
if (!(target instanceof HTMLVideoElement))
|
|
769
|
-
return {
|
|
770
|
-
ok: false,
|
|
771
|
-
paused: false,
|
|
772
|
-
positionms: 0,
|
|
773
|
-
durationms: 0,
|
|
774
|
-
summary: "The reviewed frame source is not a video element.",
|
|
775
|
-
};
|
|
776
|
-
return {
|
|
777
|
-
ok: true,
|
|
778
|
-
paused: target.paused,
|
|
779
|
-
positionms: Math.round(target.currentTime * 1000),
|
|
780
|
-
durationms: Number.isFinite(target.duration) ? Math.round(target.duration * 1000) : 0,
|
|
781
|
-
summary: `The video ${selector} sits ${target.paused ? "paused" : "playing"} at ${(target.currentTime * 1000).toFixed(0)} milliseconds of ${(Number.isFinite(target.duration) ? target.duration * 1000 : 0).toFixed(0)} milliseconds.`,
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
/** Reads the content of one canvas element: plain 2d canvases return their buffer directly and webgl canvases request the buffer through a preserved read; tainted canvases refuse honestly. */
|
|
786
|
-
export function canvasdata(selector: string): {
|
|
787
|
-
ok: boolean;
|
|
788
|
-
context: "2d" | "webgl";
|
|
789
|
-
dataurl?: string;
|
|
790
|
-
width?: number;
|
|
791
|
-
height?: number;
|
|
792
|
-
preserved?: boolean;
|
|
793
|
-
summary: string;
|
|
794
|
-
} {
|
|
795
|
-
const target = document.querySelector(selector);
|
|
796
|
-
if (!(target instanceof HTMLCanvasElement))
|
|
797
|
-
return { ok: false, context: "2d", summary: "The reviewed canvas element is no longer available." };
|
|
798
|
-
const width = target.width || 1;
|
|
799
|
-
const height = target.height || 1;
|
|
800
|
-
const kind: "2d" | "webgl" = target.getContext("2d") ? "2d" : "webgl";
|
|
801
|
-
try {
|
|
802
|
-
let dataurl = "";
|
|
803
|
-
let preserved = false;
|
|
804
|
-
if (kind === "2d") {
|
|
805
|
-
dataurl = target.toDataURL("image/png");
|
|
806
|
-
} else {
|
|
807
|
-
dataurl = target.toDataURL("image/png");
|
|
808
|
-
if (dataurl.length <= 100) {
|
|
809
|
-
const gl = (target.getContext("webgl") ??
|
|
810
|
-
target.getContext("experimental-webgl")) as WebGLRenderingContext | null;
|
|
811
|
-
if (gl) {
|
|
812
|
-
const pixels = new Uint8Array(width * height * 4);
|
|
813
|
-
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
814
|
-
const canvas = document.createElement("canvas");
|
|
815
|
-
canvas.width = width;
|
|
816
|
-
canvas.height = height;
|
|
817
|
-
const context = canvas.getContext("2d");
|
|
818
|
-
if (context) {
|
|
819
|
-
const image = context.createImageData(width, height);
|
|
820
|
-
for (let row = 0; row < height; row += 1) {
|
|
821
|
-
const source = (height - 1 - row) * width * 4;
|
|
822
|
-
const destination = row * width * 4;
|
|
823
|
-
image.data.set(pixels.subarray(source, source + width * 4), destination);
|
|
824
|
-
}
|
|
825
|
-
context.putImageData(image, 0, 0);
|
|
826
|
-
dataurl = canvas.toDataURL("image/png");
|
|
827
|
-
preserved = true;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
return {
|
|
833
|
-
ok: dataurl.length > 100,
|
|
834
|
-
context: kind,
|
|
835
|
-
dataurl,
|
|
836
|
-
width,
|
|
837
|
-
height,
|
|
838
|
-
preserved,
|
|
839
|
-
summary: `Read the ${kind} canvas buffer of ${width} by ${height} pixels${kind === "webgl" ? (preserved ? " through a preserved readPixels pass" : " through the preserved drawing buffer") : ""}.`,
|
|
840
|
-
};
|
|
841
|
-
} catch {
|
|
842
|
-
return {
|
|
843
|
-
ok: false,
|
|
844
|
-
context: kind,
|
|
845
|
-
summary: "The canvas read was refused; tainted canvas content needs cross origin resources with cors headers.",
|
|
846
|
-
};
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
/** Probes the media streams of the page: every video and audio element with a srcObject reports its track kinds, labels, settings and live states; peer connection statistics stay outside the isolated world bridge. */
|
|
851
|
-
export function streamelements(selector?: string): Array<Record<string, unknown>> {
|
|
852
|
-
const root = selector ? document.querySelector(selector) : document;
|
|
853
|
-
if (!root) return [];
|
|
854
|
-
const entries: Array<Record<string, unknown>> = [];
|
|
855
|
-
for (const element of [...root.querySelectorAll("video, audio")]) {
|
|
856
|
-
const media = element instanceof HTMLMediaElement ? element : null;
|
|
857
|
-
if (!media) continue;
|
|
858
|
-
const stream = media.srcObject;
|
|
859
|
-
if (!(stream instanceof MediaStream)) continue;
|
|
860
|
-
entries.push({
|
|
861
|
-
kind: "webrtc",
|
|
862
|
-
label: stream.id,
|
|
863
|
-
live: stream.active,
|
|
864
|
-
tracks: stream.getTracks().map((track) => ({
|
|
865
|
-
kind: track.kind,
|
|
866
|
-
label: track.label,
|
|
867
|
-
state: track.readyState,
|
|
868
|
-
...(track.kind === "video"
|
|
869
|
-
? {
|
|
870
|
-
width: track.getSettings().width,
|
|
871
|
-
height: track.getSettings().height,
|
|
872
|
-
framerate: track.getSettings().frameRate,
|
|
873
|
-
}
|
|
874
|
-
: {}),
|
|
875
|
-
})),
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
return entries;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/** Reads the embedded video and audio sources of the page with their formats, durations, dimensions, codecs and track lists. */
|
|
882
|
-
export function mediaelements(): Array<Record<string, unknown>> {
|
|
883
|
-
const entries: Array<Record<string, unknown>> = [];
|
|
884
|
-
for (const element of [...document.querySelectorAll("video, audio")]) {
|
|
885
|
-
if (!(element instanceof HTMLMediaElement)) continue;
|
|
886
|
-
const source = element.querySelector("source");
|
|
887
|
-
const url = element.currentSrc || element.src || (source instanceof HTMLSourceElement ? source.src : "") || "";
|
|
888
|
-
if (!url) continue;
|
|
889
|
-
const type = (source instanceof HTMLSourceElement ? source.type : "") || "";
|
|
890
|
-
const codecs = type.includes("codecs=")
|
|
891
|
-
? type.slice(type.indexOf("codecs=") + "codecs=".length).replace(/["']/g, "")
|
|
892
|
-
: "";
|
|
893
|
-
entries.push({
|
|
894
|
-
url,
|
|
895
|
-
mime: type.split(";")[0] || "",
|
|
896
|
-
duration: Number.isFinite(element.duration) ? element.duration : 0,
|
|
897
|
-
width: element instanceof HTMLVideoElement ? element.videoWidth : 0,
|
|
898
|
-
height: element instanceof HTMLVideoElement ? element.videoHeight : 0,
|
|
899
|
-
codecs,
|
|
900
|
-
tracks: [...element.textTracks].map((track) => track.label || track.kind).filter(Boolean),
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
return entries;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
/** Collects the page assets: declared favicons and apple touch icons with their sizes, manifest declared icons and logo candidates from meta images and header imagery. */
|
|
907
|
-
export async function pageassets(): Promise<Array<Record<string, unknown>>> {
|
|
908
|
-
const entries: Array<Record<string, unknown>> = [];
|
|
909
|
-
for (const link of [...document.querySelectorAll("link[rel]")]) {
|
|
910
|
-
const rel = (link.getAttribute("rel") ?? "").toLowerCase();
|
|
911
|
-
const href = link.getAttribute("href");
|
|
912
|
-
if (!href || !(rel.includes("icon") || rel.includes("apple-touch"))) continue;
|
|
913
|
-
const resolved = new URL(href, location.href).toString();
|
|
914
|
-
entries.push({ kind: "favicon", url: resolved, bytes: 0, sizes: link.getAttribute("sizes") ?? "any" });
|
|
915
|
-
}
|
|
916
|
-
const og = document.querySelector('meta[property="og:image"]');
|
|
917
|
-
if (og instanceof HTMLMetaElement && og.content)
|
|
918
|
-
entries.push({ kind: "logo", url: new URL(og.content, location.href).toString(), bytes: 0, sizes: "og" });
|
|
919
|
-
for (const image of [...document.querySelectorAll("header img, nav img, img[alt*=logo i], img[src*=logo i]")]) {
|
|
920
|
-
const url = image instanceof HTMLImageElement ? image.currentSrc || image.src : "";
|
|
921
|
-
if (!url) continue;
|
|
922
|
-
entries.push({
|
|
923
|
-
kind: "logo",
|
|
924
|
-
url: new URL(url, location.href).toString(),
|
|
925
|
-
bytes: 0,
|
|
926
|
-
sizes: image instanceof HTMLImageElement ? `${image.naturalWidth}x${image.naturalHeight}` : "",
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
const manifestlink = document.querySelector('link[rel="manifest"]');
|
|
930
|
-
if (manifestlink instanceof HTMLLinkElement && manifestlink.href) {
|
|
931
|
-
try {
|
|
932
|
-
const response = await fetch(manifestlink.href);
|
|
933
|
-
const manifest = (await response.json()) as { icons?: Array<{ src?: string; sizes?: string }> };
|
|
934
|
-
for (const icon of manifest.icons ?? []) {
|
|
935
|
-
if (!icon.src) continue;
|
|
936
|
-
entries.push({
|
|
937
|
-
kind: "favicon",
|
|
938
|
-
url: new URL(icon.src, location.href).toString(),
|
|
939
|
-
bytes: 0,
|
|
940
|
-
sizes: icon.sizes ?? "any",
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
} catch {
|
|
944
|
-
/* a refused manifest fetch leaves the declared icons unreported */
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
return entries;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/** Detects every image of the page inside an optional selector scope: urls, alt text, natural dimensions, transfer sizes from the performance entries and mime types. */
|
|
951
|
-
export function pageimages(selector?: string): Array<Record<string, unknown>> {
|
|
952
|
-
const root = selector ? document.querySelector(selector) : document;
|
|
953
|
-
if (!root) return [];
|
|
954
|
-
const transfers = new Map<string, number>();
|
|
955
|
-
for (const entry of performance.getEntriesByType("resource")) {
|
|
956
|
-
const resource = entry as PerformanceResourceTiming;
|
|
957
|
-
if (resource.transferSize > 0) transfers.set(resource.name, resource.transferSize);
|
|
958
|
-
}
|
|
959
|
-
const images: Array<Record<string, unknown>> = [];
|
|
960
|
-
for (const element of [...root.querySelectorAll("img")]) {
|
|
961
|
-
if (!(element instanceof HTMLImageElement)) continue;
|
|
962
|
-
const url = element.currentSrc || element.src;
|
|
963
|
-
if (!url) continue;
|
|
964
|
-
const resolved = new URL(url, location.href).toString();
|
|
965
|
-
const type = element.getAttribute("type") ?? "";
|
|
966
|
-
images.push({
|
|
967
|
-
url: resolved,
|
|
968
|
-
alt: element.alt ?? "",
|
|
969
|
-
width: element.naturalWidth || element.width,
|
|
970
|
-
height: element.naturalHeight || element.height,
|
|
971
|
-
bytes: transfers.get(resolved) ?? 0,
|
|
972
|
-
mime: type || (element.src.startsWith("data:") ? element.src.slice(5, element.src.indexOf(";")) : "image/*"),
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
return images;
|
|
976
|
-
}
|
|
977
|
-
/** Parses fetched markup through the page domparser and runs the reviewed html queries: attribute values, text and element counts per query. */
|
|
978
|
-
export function parsehtmlmarkup(
|
|
979
|
-
body: string,
|
|
980
|
-
queries: Array<{ selector: string; attribute?: string; multi?: boolean }>,
|
|
981
|
-
): Array<{ selector: string; attribute?: string; multi: boolean; count: number; values: string[] }> {
|
|
982
|
-
const parsed = new DOMParser().parseFromString(body, "text/html");
|
|
983
|
-
return queries.map((query) => {
|
|
984
|
-
const matches = [...parsed.querySelectorAll(query.selector)];
|
|
985
|
-
const chosen = query.multi === true ? matches : matches.slice(0, 1);
|
|
986
|
-
const values = chosen.map((element) =>
|
|
987
|
-
query.attribute !== undefined ? (element.getAttribute(query.attribute) ?? "") : (element.textContent ?? ""),
|
|
988
|
-
);
|
|
989
|
-
return {
|
|
990
|
-
selector: query.selector,
|
|
991
|
-
...(query.attribute !== undefined ? { attribute: query.attribute } : {}),
|
|
992
|
-
multi: query.multi === true,
|
|
993
|
-
count: matches.length,
|
|
994
|
-
values,
|
|
995
|
-
};
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
/** Reads the request lifecycle facts the page timing buffers expose: every resource and navigation entry with its url, initiator, timing, transfer size, protocol and the response status a navigation entry reports; the buffers expose no header names, body bytes or subresource status codes. */
|
|
1000
|
-
export function resourcerecords(): Array<Record<string, unknown>> {
|
|
1001
|
-
const entries = [...performance.getEntriesByType("resource"), ...performance.getEntriesByType("navigation")];
|
|
1002
|
-
return entries.map((entry) => {
|
|
1003
|
-
const resource = entry as PerformanceResourceTiming & { responseStatus?: number };
|
|
1004
|
-
return {
|
|
1005
|
-
name: resource.name,
|
|
1006
|
-
initiatorType: resource.initiatorType ?? "",
|
|
1007
|
-
entryType: resource.entryType,
|
|
1008
|
-
startTime: resource.startTime,
|
|
1009
|
-
duration: resource.duration,
|
|
1010
|
-
transferSize: resource.transferSize ?? 0,
|
|
1011
|
-
nextHopProtocol: resource.nextHopProtocol ?? "",
|
|
1012
|
-
...(typeof resource.responseStatus === "number" ? { responseStatus: resource.responseStatus } : {}),
|
|
1013
|
-
};
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
/** Writes reviewed cookies for the granted origin of the page through the page document cookie jar: every record writes its name, value and path with an optional expiry; the write happens on the page the user granted, never through a browser cookies permission. */
|
|
1018
|
-
export function writecookies(records: Array<{ name: string; value: string; path: string; expiresat?: number }>): {
|
|
1019
|
-
written: number;
|
|
1020
|
-
summary: string;
|
|
1021
|
-
} {
|
|
1022
|
-
let written = 0;
|
|
1023
|
-
for (const record of records) {
|
|
1024
|
-
const expiry = record.expiresat !== undefined ? `; expires=${new Date(record.expiresat).toUTCString()}` : "";
|
|
1025
|
-
document.cookie = `${record.name}=${record.value}; path=${record.path}${expiry}; samesite=lax`;
|
|
1026
|
-
written += 1;
|
|
1027
|
-
}
|
|
1028
|
-
return {
|
|
1029
|
-
written,
|
|
1030
|
-
summary: `Wrote ${written} reviewed cookie${written === 1 ? "" : "s"} through the page cookie jar of ${location.origin}.`,
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
/** Reads the cookies of the granted origin through the page document cookie jar: document.cookie exposes the name and value pairs of the origin only, with no domain, path or expiry metadata. */
|
|
1035
|
-
export function readcookies(): Array<{ name: string; value: string }> {
|
|
1036
|
-
return document.cookie
|
|
1037
|
-
.split(";")
|
|
1038
|
-
.map((pair) => pair.trim())
|
|
1039
|
-
.filter((pair) => pair.length > 0)
|
|
1040
|
-
.map((pair) => {
|
|
1041
|
-
const separator = pair.indexOf("=");
|
|
1042
|
-
return separator === -1
|
|
1043
|
-
? { name: pair, value: "" }
|
|
1044
|
-
: { name: pair.slice(0, separator), value: pair.slice(separator + 1) };
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
/** Clears the cookies of the granted origin through the page document cookie jar: every matched name is expired on the root path; an absent name list clears every cookie the origin jar exposes. */
|
|
1049
|
-
export function clearcookies(names?: string[]): { cleared: number; summary: string } {
|
|
1050
|
-
const jar = readcookies();
|
|
1051
|
-
const targets = names !== undefined && names.length > 0 ? jar.filter((cookie) => names.includes(cookie.name)) : jar;
|
|
1052
|
-
for (const cookie of targets) document.cookie = `${cookie.name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
1053
|
-
return {
|
|
1054
|
-
cleared: targets.length,
|
|
1055
|
-
summary: `Cleared ${targets.length} cookie${targets.length === 1 ? "" : "s"} through the page cookie jar of ${location.origin}.`,
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
/** Masks the typed values of one step before they reach the log writer: a step whose target or kind names a masked field shape keeps its shape while the typed value and the matching option values carry the redaction marker, so typed secrets never land in any record. */
|
|
1060
|
-
export function maskstepvalues(step: toolstep, shapes: string[]): toolstep {
|
|
1061
|
-
const masked = masktypedvalues({ step, shapes });
|
|
1062
|
-
return {
|
|
1063
|
-
...step,
|
|
1064
|
-
...(masked.value !== undefined ? { value: masked.value } : {}),
|
|
1065
|
-
...(masked.options !== undefined ? { options: masked.options } : {}),
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
/** Masks the form state of one observation payload before logging: the observation schema keeps its field shapes while the values behind masked field shapes carry the redaction marker. */
|
|
1070
|
-
export function maskobservationstate(
|
|
1071
|
-
fields: Array<{ name: string; value: string }>,
|
|
1072
|
-
shapes: string[],
|
|
1073
|
-
): Array<{ name: string; value: string }> {
|
|
1074
|
-
return maskformstate(fields, shapes);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
/** Masks one full observation payload before it reaches the log writer: the field shapes stay while the option lists of masked field names carry the redaction marker. */
|
|
1078
|
-
export function maskobservationsnapshot(shot: observation, shapes: string[]): observation {
|
|
1079
|
-
return maskobservation(shot, shapes);
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
Object.assign(globalThis, {
|
|
1083
|
-
devthinkbridge: {
|
|
1084
|
-
maskstepvalues,
|
|
1085
|
-
maskobservationstate,
|
|
1086
|
-
maskobservationsnapshot,
|
|
1087
|
-
capturesnapshot,
|
|
1088
|
-
previewtarget,
|
|
1089
|
-
performstep,
|
|
1090
|
-
readdialogs,
|
|
1091
|
-
measurepage,
|
|
1092
|
-
elementrect,
|
|
1093
|
-
queryelements,
|
|
1094
|
-
preparecapture,
|
|
1095
|
-
scrollcapture,
|
|
1096
|
-
restorecapture,
|
|
1097
|
-
scrollcontainercapture,
|
|
1098
|
-
waitsettle,
|
|
1099
|
-
pdfsegment,
|
|
1100
|
-
pdfbreaks,
|
|
1101
|
-
videoframe,
|
|
1102
|
-
videostate,
|
|
1103
|
-
canvasdata,
|
|
1104
|
-
streamelements,
|
|
1105
|
-
mediaelements,
|
|
1106
|
-
pageassets,
|
|
1107
|
-
pageimages,
|
|
1108
|
-
parsehtmlmarkup,
|
|
1109
|
-
resourcerecords,
|
|
1110
|
-
writecookies,
|
|
1111
|
-
readcookies,
|
|
1112
|
-
clearcookies,
|
|
1113
|
-
revertemulationlayer,
|
|
1114
|
-
flushconsole,
|
|
1115
|
-
},
|
|
1116
|
-
});
|