@wenathlan/devthink 2.0.14 → 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 +60 -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/environments.ts
DELETED
|
@@ -1,1005 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The environments module of the 1.1.90 consolidation: every correlated variation of the execution environment, emulation and sandbox rendering logic interned in this one file, so the module family carries one surface without duplicate variations.
|
|
3
|
-
* The correlation is the execution environment family: environments holds the base grammar (the per kind requirement table, the executor registry, the offscreen document lifecycle with its worker pool plan, the executor traffic envelopes and the isolated world injection of reviewed arguments); emulation stacks the device, network, location, agent and permission presets as revertible layers with the blackbox rules that hide the stack frames of the host page; and sandboxframe renders the untrusted markup of a reviewed step inside the sandboxed page with scripts, schemes and handlers stripped before render and the result posted back through a per render nonce.
|
|
4
|
-
* No pool size, preset, retention or posture is ever hardcoded: every interval, ceiling and grant stays the user's choice, and no environment ever bypasses the human review.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
actionkind,
|
|
9
|
-
environmentkind,
|
|
10
|
-
environmentrequirement,
|
|
11
|
-
offscreenregistryentry,
|
|
12
|
-
toolstep,
|
|
13
|
-
workerrequest,
|
|
14
|
-
workerresponse,
|
|
15
|
-
} from "./types.js";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Execution environment logic of the 1.1.60 family.
|
|
19
|
-
* Correlated rules for the environment grammar live here: the per kind requirement table (the live page, the isolated world of the scripting api, the offscreen document worker pool and the sandboxed frame), the executor registry that maps every environmentkind to its runtime adapter, the offscreen document lifecycle with its spawn on first use, reuse across the steps of one run and close at completion, the worker pool plan that grows and shrinks with the pending parse queue, the executor traffic envelopes with their transferable plan and partial streaming, and the isolated world injection that carries reviewed arguments only.
|
|
20
|
-
* No pool size, interval, reason list or environment posture is ever hardcoded: the user choices drive every decision, the capability gate keeps offscreenworker behind the optional offscreen grant with an inline fallback, and no environment ever bypasses the human review.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/** The parse heavy read kinds the offscreen worker pool may take over: large html snapshots, network json payloads, table row reductions, accessibility tree shaping, complex selector evaluation and screenshot stitching. */
|
|
24
|
-
const offloadfamilies: ReadonlyArray<{ task: string; kinds: readonly string[] }> = [
|
|
25
|
-
{ task: "htmlsnapshot", kinds: ["readhtml", "parsehtml", "readertree", "readoutline", "classifypage"] },
|
|
26
|
-
{ task: "jsonpayload", kinds: ["readjson", "parsejson"] },
|
|
27
|
-
{ task: "tablerows", kinds: ["readtable", "scrapetable", "detecttables", "deduperows", "transformvalues"] },
|
|
28
|
-
{ task: "a11ytree", kinds: ["a11ytree"] },
|
|
29
|
-
{ task: "complexselector", kinds: ["resolvexpath", "deriveselector", "detectvirtual"] },
|
|
30
|
-
{ task: "stitchshots", kinds: ["contactsheet", "timelapse", "makethumbs"] },
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
/** Reads the parse family of one offload eligible kind; a kind outside the families returns undefined. */
|
|
34
|
-
export function offfamilyof(kind: actionkind): string | undefined {
|
|
35
|
-
return offloadfamilies.find((family) => family.kinds.includes(kind))?.task;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Reads the environments one action kind may run in: evaluate runs in the isolated world only, a step carrying untrusted markup renders inside the sandboxframe only, the parse heavy kinds may offload into the offscreen worker pool beside the live page and every other kind keeps the pagecontext of the page bridge. */
|
|
39
|
-
export function environmentsof(step: Pick<toolstep, "kind" | "options">): environmentkind[] {
|
|
40
|
-
if (markuprenderstep(step)) return ["sandboxframe"];
|
|
41
|
-
if (step.kind === "evaluate") return ["isolatedworld"];
|
|
42
|
-
if (offfamilyof(step.kind) !== undefined) return ["pagecontext", "offscreenworker"];
|
|
43
|
-
return ["pagecontext"];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Reads the default environment of one step: the absent environment field routes to the isolated world for evaluate, the sandboxframe for untrusted markup and the pagecontext for everything else including the parse offload candidates until the user turns the offload on. */
|
|
47
|
-
export function defaultenvironment(step: Pick<toolstep, "kind" | "options">): environmentkind {
|
|
48
|
-
if (markuprenderstep(step)) return "sandboxframe";
|
|
49
|
-
if (step.kind === "evaluate") return "isolatedworld";
|
|
50
|
-
return "pagecontext";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function offamilyeligible(kind: actionkind): boolean {
|
|
54
|
-
return offloadfamilies.some((family) => family.kinds.includes(kind));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Lists the parse heavy kinds the offscreen worker pool may take over, grouped by their parse family. */
|
|
58
|
-
export function offloadkinds(): Array<{ task: string; kinds: string[] }> {
|
|
59
|
-
return offloadfamilies.map((family) => ({ task: family.task, kinds: [...family.kinds] }));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Reads whether one step carries untrusted markup for rendering: a reviewed options object with a markup string renders inside the sandboxframe only, so the untrusted markup never reenters the page dom. */
|
|
63
|
-
export function markuprenderstep(step: Pick<toolstep, "kind" | "options">): boolean {
|
|
64
|
-
if (!step.options) return false;
|
|
65
|
-
try {
|
|
66
|
-
const parsed = JSON.parse(step.options);
|
|
67
|
-
return Boolean(
|
|
68
|
-
parsed &&
|
|
69
|
-
typeof parsed === "object" &&
|
|
70
|
-
!Array.isArray(parsed) &&
|
|
71
|
-
typeof (parsed as Record<string, unknown>).markup === "string" &&
|
|
72
|
-
((parsed as Record<string, unknown>).markup as string).trim() !== "",
|
|
73
|
-
);
|
|
74
|
-
} catch {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Builds the per kind environment requirement table the policy exposes: one profile per action kind with its allowed environments and its default. */
|
|
80
|
-
export function environmentrequirementsof(kinds: actionkind[]): environmentrequirement[] {
|
|
81
|
-
return kinds.map((kind) => {
|
|
82
|
-
const bare: Pick<toolstep, "kind" | "options"> = { kind };
|
|
83
|
-
const environments = environmentsof(bare);
|
|
84
|
-
return { kind, environments, defaultenvironment: defaultenvironment(bare) };
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Maps every environmentkind to its runtime adapter: pagecontext rides the page bridge in the live page, isolatedworld rides the scripting api world page scripts cannot touch, offscreenworker rides the offscreen document worker pool and sandboxframe rides the sandboxed page with no extension privileges. */
|
|
89
|
-
export function executorregistry(): Array<{ environment: environmentkind; adapter: string; description: string }> {
|
|
90
|
-
return [
|
|
91
|
-
{
|
|
92
|
-
environment: "pagecontext",
|
|
93
|
-
adapter: "pagebridge",
|
|
94
|
-
description: "The page bridge executes dom actions inside the live page because page events only fire there.",
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
environment: "isolatedworld",
|
|
98
|
-
adapter: "scriptingapi",
|
|
99
|
-
description:
|
|
100
|
-
"The scripting api injects step logic inside the isolated world where page globals stay unreachable from step code.",
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
environment: "offscreenworker",
|
|
104
|
-
adapter: "offscreendocument",
|
|
105
|
-
description:
|
|
106
|
-
"The offscreen document hosts the worker pool that parses heavy payloads away from the page; the capability gate keeps it behind the optional offscreen grant with an inline fallback.",
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
environment: "sandboxframe",
|
|
110
|
-
adapter: "sandboxpage",
|
|
111
|
-
description:
|
|
112
|
-
"The sandboxed page renders untrusted markup with scripts and handlers stripped before render and posts its result back through a per render nonce.",
|
|
113
|
-
},
|
|
114
|
-
];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Routes one step to its environment: the reviewed environment field wins when it sits inside the requirement table, the offload toggle moves parse heavy kinds into the offscreen worker pool, and a missing offscreen capability grant falls the step back to inline parsing inside the page instead of refusing the reviewed work. */
|
|
118
|
-
export function routeenvironment(
|
|
119
|
-
step: toolstep,
|
|
120
|
-
input: { offload: boolean; granted: boolean },
|
|
121
|
-
): { environment: environmentkind; fallback: boolean; reason: string } {
|
|
122
|
-
const allowed = environmentsof(step);
|
|
123
|
-
const named = step.environment;
|
|
124
|
-
if (named !== undefined) {
|
|
125
|
-
if (!allowed.includes(named))
|
|
126
|
-
return {
|
|
127
|
-
environment: defaultenvironment(step),
|
|
128
|
-
fallback: false,
|
|
129
|
-
reason: `The ${named} environment sits outside the ${allowed.join(", ")} the ${step.kind} kind permits, so the executor routes to the ${defaultenvironment(step)} default.`,
|
|
130
|
-
};
|
|
131
|
-
return {
|
|
132
|
-
environment: named,
|
|
133
|
-
fallback: false,
|
|
134
|
-
reason: `The reviewed step names its ${named} environment and the ${step.kind} kind permits it.`,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
if (markuprenderstep(step))
|
|
138
|
-
return {
|
|
139
|
-
environment: "sandboxframe",
|
|
140
|
-
fallback: false,
|
|
141
|
-
reason: `The ${step.kind} step carries untrusted markup, so it renders inside the sandboxframe only.`,
|
|
142
|
-
};
|
|
143
|
-
if (step.kind === "evaluate")
|
|
144
|
-
return {
|
|
145
|
-
environment: "isolatedworld",
|
|
146
|
-
fallback: false,
|
|
147
|
-
reason: "The evaluate kind runs inside the isolated world where page globals stay unreachable from step code.",
|
|
148
|
-
};
|
|
149
|
-
if (offamilyeligible(step.kind)) {
|
|
150
|
-
if (!input.offload)
|
|
151
|
-
return {
|
|
152
|
-
environment: "pagecontext",
|
|
153
|
-
fallback: false,
|
|
154
|
-
reason: `The ${step.kind} step stays inside the page because the user keeps the parse offload off.`,
|
|
155
|
-
};
|
|
156
|
-
if (!input.granted)
|
|
157
|
-
return {
|
|
158
|
-
environment: "pagecontext",
|
|
159
|
-
fallback: true,
|
|
160
|
-
reason: `The ${step.kind} step falls back to inline parsing inside the page because the offscreen capability grant stays absent.`,
|
|
161
|
-
};
|
|
162
|
-
return {
|
|
163
|
-
environment: "offscreenworker",
|
|
164
|
-
fallback: false,
|
|
165
|
-
reason: `The ${step.kind} step offloads into the offscreen worker pool under the granted capability.`,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
return {
|
|
169
|
-
environment: "pagecontext",
|
|
170
|
-
fallback: false,
|
|
171
|
-
reason: `The ${step.kind} step keeps the pagecontext because page events only fire inside the live page.`,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Builds one executor traffic envelope for the offscreen worker pool: the parse family of the kind, the payload reference and the transferable keys the payload carries. */
|
|
176
|
-
export function workerrequestof(input: {
|
|
177
|
-
id: string;
|
|
178
|
-
runid: string;
|
|
179
|
-
stepid: string;
|
|
180
|
-
kind: actionkind;
|
|
181
|
-
payload: string;
|
|
182
|
-
options?: Record<string, unknown>;
|
|
183
|
-
sentat: number;
|
|
184
|
-
}): workerrequest {
|
|
185
|
-
const task = offfamilyof(input.kind);
|
|
186
|
-
if (task === undefined) throw new Error(`The ${input.kind} kind stays outside the offscreen worker pool families.`);
|
|
187
|
-
if (input.payload.trim() === "") throw new Error("The worker request needs its payload reference.");
|
|
188
|
-
return {
|
|
189
|
-
id: input.id,
|
|
190
|
-
runid: input.runid,
|
|
191
|
-
stepid: input.stepid,
|
|
192
|
-
task,
|
|
193
|
-
payload: input.payload,
|
|
194
|
-
transferables: transferablekeys(input.options ?? {}),
|
|
195
|
-
sentat: input.sentat,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Reads the payload keys the executor moves as transferable buffers where possible: every ArrayBuffer value travels as a transferable so the worker owns its bytes without a copy. */
|
|
200
|
-
export function transferablekeys(options: Record<string, unknown>): string[] {
|
|
201
|
-
return Object.keys(options).filter((key) => options[key] instanceof ArrayBuffer);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** Builds one worker answer to the executor: the finished result or one streamed partial chunk with its sequence number. */
|
|
205
|
-
export function workerresponseof(input: {
|
|
206
|
-
id: string;
|
|
207
|
-
requestid: string;
|
|
208
|
-
ok: boolean;
|
|
209
|
-
result?: string;
|
|
210
|
-
partial?: number;
|
|
211
|
-
summary: string;
|
|
212
|
-
receivedat: number;
|
|
213
|
-
}): workerresponse {
|
|
214
|
-
if (input.summary.trim() === "") throw new Error("The worker answer needs its summary in plain language.");
|
|
215
|
-
return {
|
|
216
|
-
id: input.id,
|
|
217
|
-
requestid: input.requestid,
|
|
218
|
-
ok: input.ok,
|
|
219
|
-
...(input.result !== undefined ? { result: input.result } : {}),
|
|
220
|
-
...(input.partial !== undefined ? { partial: input.partial } : {}),
|
|
221
|
-
summary: input.summary,
|
|
222
|
-
receivedat: input.receivedat,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** Reads one worker answer: a partial chunk streams on while the final answer without a partial sequence completes the request, and a failed answer refuses without a result. */
|
|
227
|
-
export function acceptworkerresponse(response: workerresponse): {
|
|
228
|
-
done: boolean;
|
|
229
|
-
partial: boolean;
|
|
230
|
-
result?: string;
|
|
231
|
-
reason: string;
|
|
232
|
-
} {
|
|
233
|
-
if (!response.ok)
|
|
234
|
-
return {
|
|
235
|
-
done: true,
|
|
236
|
-
partial: false,
|
|
237
|
-
reason: `The worker refused the request ${response.requestid}: ${response.summary}`,
|
|
238
|
-
};
|
|
239
|
-
if (response.partial !== undefined)
|
|
240
|
-
return {
|
|
241
|
-
done: false,
|
|
242
|
-
partial: true,
|
|
243
|
-
reason: `The partial ${response.partial} of the request ${response.requestid} streams back to the executor.`,
|
|
244
|
-
};
|
|
245
|
-
return {
|
|
246
|
-
done: true,
|
|
247
|
-
partial: false,
|
|
248
|
-
...(response.result !== undefined ? { result: response.result } : {}),
|
|
249
|
-
reason: `The request ${response.requestid} completed inside the offscreen worker pool.`,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** Plans the worker pool against the pending parse queue: the pool grows with the pending parses and shrinks with the idle workers, a user configured size pins the pool with no engine cap, and an absent size lets the queue alone decide. */
|
|
254
|
-
export function poolplan(input: { pending: number; current: number; size?: number }): {
|
|
255
|
-
workers: number;
|
|
256
|
-
added: number;
|
|
257
|
-
retired: number;
|
|
258
|
-
reason: string;
|
|
259
|
-
} {
|
|
260
|
-
if (input.size !== undefined) {
|
|
261
|
-
if (!Number.isFinite(input.size) || input.size < 1 || !Number.isInteger(input.size))
|
|
262
|
-
return {
|
|
263
|
-
workers: input.current,
|
|
264
|
-
added: 0,
|
|
265
|
-
retired: 0,
|
|
266
|
-
reason:
|
|
267
|
-
"The configured pool size stays a positive whole number the user chose; the pool keeps its current workers.",
|
|
268
|
-
};
|
|
269
|
-
const target = input.size;
|
|
270
|
-
if (target > input.current)
|
|
271
|
-
return {
|
|
272
|
-
workers: target,
|
|
273
|
-
added: target - input.current,
|
|
274
|
-
retired: 0,
|
|
275
|
-
reason: `The user configured pool size ${target} adds ${target - input.current} worker${target - input.current === 1 ? "" : "s"} to the pool.`,
|
|
276
|
-
};
|
|
277
|
-
if (target < input.current)
|
|
278
|
-
return {
|
|
279
|
-
workers: target,
|
|
280
|
-
added: 0,
|
|
281
|
-
retired: input.current - target,
|
|
282
|
-
reason: `The user configured pool size ${target} retires ${input.current - target} worker${input.current - target === 1 ? "" : "s"} from the pool.`,
|
|
283
|
-
};
|
|
284
|
-
return {
|
|
285
|
-
workers: target,
|
|
286
|
-
added: 0,
|
|
287
|
-
retired: 0,
|
|
288
|
-
reason: `The pool holds the ${target} workers the user configured.`,
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
if (input.pending > input.current)
|
|
292
|
-
return {
|
|
293
|
-
workers: input.pending,
|
|
294
|
-
added: input.pending - input.current,
|
|
295
|
-
retired: 0,
|
|
296
|
-
reason: `The ${input.pending} pending parses grow the pool by ${input.pending - input.current} worker${input.pending - input.current === 1 ? "" : "s"}; no engine cap exists.`,
|
|
297
|
-
};
|
|
298
|
-
if (input.current > input.pending)
|
|
299
|
-
return {
|
|
300
|
-
workers: input.pending,
|
|
301
|
-
added: 0,
|
|
302
|
-
retired: input.current - input.pending,
|
|
303
|
-
reason: `The ${input.current - input.pending} idle worker${input.current - input.pending === 1 ? "" : "s"} retire down to the ${input.pending} pending parse${input.pending === 1 ? "" : "s"}.`,
|
|
304
|
-
};
|
|
305
|
-
return {
|
|
306
|
-
workers: input.current,
|
|
307
|
-
added: 0,
|
|
308
|
-
retired: 0,
|
|
309
|
-
reason: `The ${input.current} workers match the ${input.pending} pending parses; the pool stays unchanged.`,
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/** Spawns the offscreen document of one run on first use and reuses the same document across the steps of the run: a registry entry already open for the run carries on while the first need records its reasons and justification. */
|
|
314
|
-
export function openoffscreen(input: {
|
|
315
|
-
registry: offscreenregistryentry[];
|
|
316
|
-
document: string;
|
|
317
|
-
runid: string;
|
|
318
|
-
reasons: string[];
|
|
319
|
-
justification: string;
|
|
320
|
-
now: number;
|
|
321
|
-
}): { registry: offscreenregistryentry[]; entry: offscreenregistryentry; reused: boolean } {
|
|
322
|
-
if (input.document.trim() === "") throw new Error("The offscreen document needs its user configured path.");
|
|
323
|
-
if (input.reasons.length === 0) throw new Error("The offscreen document needs the reasons the user reviewed.");
|
|
324
|
-
if (input.justification.trim() === "")
|
|
325
|
-
throw new Error("The offscreen document needs its justification in plain language.");
|
|
326
|
-
const open = input.registry.find((entry) => entry.runid === input.runid && entry.closedat === undefined);
|
|
327
|
-
if (open) return { registry: input.registry, entry: open, reused: true };
|
|
328
|
-
const entry: offscreenregistryentry = {
|
|
329
|
-
document: input.document,
|
|
330
|
-
runid: input.runid,
|
|
331
|
-
reasons: [...input.reasons],
|
|
332
|
-
justification: input.justification,
|
|
333
|
-
createdat: input.now,
|
|
334
|
-
};
|
|
335
|
-
return { registry: [entry, ...input.registry], entry, reused: false };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** Closes the offscreen document of one run when the run completes: the registry entry records its close time so the audit reads every spawn with its teardown. */
|
|
339
|
-
export function closeoffscreen(
|
|
340
|
-
registry: offscreenregistryentry[],
|
|
341
|
-
runid: string,
|
|
342
|
-
now: number,
|
|
343
|
-
): { registry: offscreenregistryentry[]; closed: boolean } {
|
|
344
|
-
const open = registry.find((entry) => entry.runid === runid && entry.closedat === undefined);
|
|
345
|
-
if (!open) return { registry, closed: false };
|
|
346
|
-
return { registry: registry.map((entry) => (entry === open ? { ...entry, closedat: now } : entry)), closed: true };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/** Builds the isolated world injection of one evaluate step: the scripting api injects the reviewed code inside the isolated world where page globals stay unreachable from step code, and the injection carries reviewed arguments only. */
|
|
350
|
-
export function isolatedinjection(step: toolstep): { world: "ISOLATED"; code: string; args: string[] } {
|
|
351
|
-
if (step.kind !== "evaluate") throw new Error("The isolated world injection serves the evaluate kind only.");
|
|
352
|
-
if (!step.value || step.value.trim() === "") throw new Error("The evaluate step needs its reviewed expression.");
|
|
353
|
-
let args: string[] = [];
|
|
354
|
-
if (step.options) {
|
|
355
|
-
try {
|
|
356
|
-
const parsed = JSON.parse(step.options);
|
|
357
|
-
if (Array.isArray(parsed)) args = parsed.filter((item) => typeof item === "string") as string[];
|
|
358
|
-
} catch {
|
|
359
|
-
/* an options object outside the reviewed argument list stays empty */
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return { world: "ISOLATED", code: step.value, args };
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/** The runsummary task family of the 1.1.63 session interface: the distillation of a completed run runs as one offscreen worker task beside the parse families. */
|
|
366
|
-
export const runsummarytask = "runsummary";
|
|
367
|
-
|
|
368
|
-
/** Builds the offscreen worker request of one runsummary distillation: the task family name, the run and session provenance and the distilled payload reference; the step id slot carries the session id because the distillation belongs to the whole run. */
|
|
369
|
-
export function summaryrequestof(input: {
|
|
370
|
-
id: string;
|
|
371
|
-
runid: string;
|
|
372
|
-
sessionid: string;
|
|
373
|
-
payload: string;
|
|
374
|
-
sentat: number;
|
|
375
|
-
}): workerrequest {
|
|
376
|
-
if (input.payload.trim() === "") throw new Error("The runsummary request needs its payload reference.");
|
|
377
|
-
return {
|
|
378
|
-
id: input.id,
|
|
379
|
-
runid: input.runid,
|
|
380
|
-
stepid: input.sessionid,
|
|
381
|
-
task: runsummarytask,
|
|
382
|
-
payload: input.payload,
|
|
383
|
-
transferables: [],
|
|
384
|
-
sentat: input.sentat,
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/* ── Merged from emulation.ts ── */
|
|
389
|
-
import type {
|
|
390
|
-
agentpreset,
|
|
391
|
-
blackboxrule,
|
|
392
|
-
devicepreset,
|
|
393
|
-
emulationlayer,
|
|
394
|
-
emulationstate,
|
|
395
|
-
locationconsent,
|
|
396
|
-
locationpreset,
|
|
397
|
-
networkpreset,
|
|
398
|
-
permissiongrant,
|
|
399
|
-
permissionstate,
|
|
400
|
-
presetlibrary,
|
|
401
|
-
stackframe,
|
|
402
|
-
} from "./types.js";
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Emulation layer engine for the 1.1.48 family.
|
|
406
|
-
* Every correlated rule for the reviewed masks of the run lives in this file: the preset normalizers of the user curated device, network, location and agent libraries, the reviewed revert plan grammar, the layer apply and revert math with prior state capture and reverse order restore, the stacking conflict order where the last applied layer wins, the latitude, longitude and user agent grammars, the browser permission set, the blackbox pattern matching that hides third party frames from stack traces, the retention expiry of reverted layer states and the versioned preset library import and export.
|
|
407
|
-
* True device metric, network condition, geolocation and user agent override needs browser debugger or platform permissions that the manifest gate forbids, so every layer applies page-injected overrides through the scripting api and shapes only the traffic the extension itself initiates; the derivation is recorded on every layer instead of hidden.
|
|
408
|
-
*/
|
|
409
|
-
|
|
410
|
-
/** The emulation kinds of the 1.1.48 family, listed among the available capabilities of every proposal request. */
|
|
411
|
-
export const emulationkinds: string[] = [
|
|
412
|
-
"emulatedevice",
|
|
413
|
-
"emulatenetwork",
|
|
414
|
-
"emulatelocate",
|
|
415
|
-
"setuseragent",
|
|
416
|
-
"overridepermission",
|
|
417
|
-
"blackboxscripts",
|
|
418
|
-
];
|
|
419
|
-
|
|
420
|
-
/** The reviewed browser permission set of the override grammar; overrides outside this set are refused. */
|
|
421
|
-
export const browserpermissions: string[] = [
|
|
422
|
-
"geolocation",
|
|
423
|
-
"notifications",
|
|
424
|
-
"camera",
|
|
425
|
-
"microphone",
|
|
426
|
-
"clipboard-read",
|
|
427
|
-
"clipboard-write",
|
|
428
|
-
"midi",
|
|
429
|
-
"persistent-storage",
|
|
430
|
-
];
|
|
431
|
-
|
|
432
|
-
/** The reviewed permission states of an override: granted, denied or the browser default prompt. */
|
|
433
|
-
export const permissionstates: permissionstate[] = ["granted", "denied", "prompt"];
|
|
434
|
-
|
|
435
|
-
/** The emulation family of one emulation action kind. */
|
|
436
|
-
export function familyofkind(
|
|
437
|
-
kind: string,
|
|
438
|
-
): "device" | "network" | "location" | "agent" | "permission" | "blackbox" | undefined {
|
|
439
|
-
if (kind === "emulatedevice") return "device";
|
|
440
|
-
if (kind === "emulatenetwork") return "network";
|
|
441
|
-
if (kind === "emulatelocate") return "location";
|
|
442
|
-
if (kind === "setuseragent") return "agent";
|
|
443
|
-
if (kind === "overridepermission") return "permission";
|
|
444
|
-
if (kind === "blackboxscripts") return "blackbox";
|
|
445
|
-
return undefined;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/** Normalizes one user curated device preset: the name, width, height, pixel ratio and the mobile flag with every bound a user choice only. */
|
|
449
|
-
export function devicepresetof(value: unknown): devicepreset | undefined {
|
|
450
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
451
|
-
const entry = value as Record<string, unknown>;
|
|
452
|
-
const name = typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : undefined;
|
|
453
|
-
const width =
|
|
454
|
-
typeof entry.width === "number" && Number.isInteger(entry.width) && entry.width > 0 ? entry.width : undefined;
|
|
455
|
-
const height =
|
|
456
|
-
typeof entry.height === "number" && Number.isInteger(entry.height) && entry.height > 0 ? entry.height : undefined;
|
|
457
|
-
const pixelratio =
|
|
458
|
-
typeof entry.pixelratio === "number" && Number.isFinite(entry.pixelratio) && entry.pixelratio > 0
|
|
459
|
-
? entry.pixelratio
|
|
460
|
-
: undefined;
|
|
461
|
-
if (name === undefined || width === undefined || height === undefined || pixelratio === undefined) return undefined;
|
|
462
|
-
return { name, width, height, pixelratio, mobile: entry.mobile === true };
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/** Normalizes one user curated network preset: the name, latency, download and upload bounds and the offline flag. */
|
|
466
|
-
export function networkpresetof(value: unknown): networkpreset | undefined {
|
|
467
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
468
|
-
const entry = value as Record<string, unknown>;
|
|
469
|
-
const name = typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : undefined;
|
|
470
|
-
const latency =
|
|
471
|
-
typeof entry.latency === "number" && Number.isFinite(entry.latency) && entry.latency >= 0
|
|
472
|
-
? entry.latency
|
|
473
|
-
: undefined;
|
|
474
|
-
const download =
|
|
475
|
-
typeof entry.download === "number" && Number.isFinite(entry.download) && entry.download >= 0
|
|
476
|
-
? entry.download
|
|
477
|
-
: undefined;
|
|
478
|
-
const upload =
|
|
479
|
-
typeof entry.upload === "number" && Number.isFinite(entry.upload) && entry.upload >= 0 ? entry.upload : undefined;
|
|
480
|
-
if (name === undefined || latency === undefined || download === undefined || upload === undefined) return undefined;
|
|
481
|
-
return { name, latency, download, upload, offline: entry.offline === true };
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/** Normalizes one user curated location preset: the name, the latitude and longitude inside the reviewed ranges and the non-negative accuracy radius. */
|
|
485
|
-
export function locationpresetof(value: unknown): locationpreset | undefined {
|
|
486
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
487
|
-
const entry = value as Record<string, unknown>;
|
|
488
|
-
const name = typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : undefined;
|
|
489
|
-
const latitude = typeof entry.latitude === "number" && Number.isFinite(entry.latitude) ? entry.latitude : undefined;
|
|
490
|
-
const longitude =
|
|
491
|
-
typeof entry.longitude === "number" && Number.isFinite(entry.longitude) ? entry.longitude : undefined;
|
|
492
|
-
const accuracy =
|
|
493
|
-
typeof entry.accuracy === "number" && Number.isFinite(entry.accuracy) && entry.accuracy >= 0
|
|
494
|
-
? entry.accuracy
|
|
495
|
-
: undefined;
|
|
496
|
-
if (name === undefined || latitude === undefined || longitude === undefined || accuracy === undefined)
|
|
497
|
-
return undefined;
|
|
498
|
-
if (!locationrangevalid(latitude, longitude)) return undefined;
|
|
499
|
-
return { name, latitude, longitude, accuracy };
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/** Normalizes one user curated agent preset: the user agent string of the reviewed grammar, the platform and the non-empty brand list reported together. */
|
|
503
|
-
export function agentpresetof(value: unknown): agentpreset | undefined {
|
|
504
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
505
|
-
const entry = value as Record<string, unknown>;
|
|
506
|
-
const name = typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : undefined;
|
|
507
|
-
const useragent = typeof entry.useragent === "string" ? entry.useragent : undefined;
|
|
508
|
-
const platform = typeof entry.platform === "string" && entry.platform.trim() ? entry.platform.trim() : undefined;
|
|
509
|
-
const brands = Array.isArray(entry.brands)
|
|
510
|
-
? entry.brands.filter((brand): brand is string => typeof brand === "string" && brand.trim().length > 0)
|
|
511
|
-
: [];
|
|
512
|
-
if (name === undefined || useragent === undefined || platform === undefined || brands.length === 0) return undefined;
|
|
513
|
-
if (!agentgrammarvalid(useragent)) return undefined;
|
|
514
|
-
return { name, useragent, platform, brands: [...new Set(brands)] };
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
/** Normalizes one reviewed permission override: the name of the reviewed browser permission set, the state of the reviewed permission states and the run scope flag. */
|
|
518
|
-
export function permissiongrantof(value: unknown): permissiongrant | undefined {
|
|
519
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
520
|
-
const entry = value as Record<string, unknown>;
|
|
521
|
-
const name = typeof entry.name === "string" && browserpermissions.includes(entry.name) ? entry.name : undefined;
|
|
522
|
-
const state =
|
|
523
|
-
typeof entry.state === "string" && permissionstates.includes(entry.state as permissionstate)
|
|
524
|
-
? (entry.state as permissionstate)
|
|
525
|
-
: undefined;
|
|
526
|
-
if (name === undefined || state === undefined) return undefined;
|
|
527
|
-
return { name, state, runscope: entry.runscope !== false };
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/** Normalizes one reviewed blackbox rule: a non-empty url pattern list where every pattern names its origin explicitly and the trace scope of profiles, traces or both. */
|
|
531
|
-
export function blackboxruleof(value: unknown): blackboxrule | undefined {
|
|
532
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
533
|
-
const entry = value as Record<string, unknown>;
|
|
534
|
-
const urlpatterns = Array.isArray(entry.urlpatterns)
|
|
535
|
-
? entry.urlpatterns.filter(
|
|
536
|
-
(pattern): pattern is string => typeof pattern === "string" && /^https:\/\//.test(pattern),
|
|
537
|
-
)
|
|
538
|
-
: [];
|
|
539
|
-
const tracescope = entry.tracescope;
|
|
540
|
-
if (urlpatterns.length === 0) return undefined;
|
|
541
|
-
if (tracescope !== "profiles" && tracescope !== "traces" && tracescope !== "both") return undefined;
|
|
542
|
-
return { urlpatterns: [...new Set(urlpatterns)], tracescope };
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/** Normalizes one reviewed revert plan: a non-empty ordered list of revert steps kept beside its layer; plans without steps are refused. */
|
|
546
|
-
export function revertplanof(value: unknown): string[] | undefined {
|
|
547
|
-
const steps = Array.isArray(value)
|
|
548
|
-
? value.filter((step): step is string => typeof step === "string" && step.trim().length > 0)
|
|
549
|
-
: [];
|
|
550
|
-
return steps.length > 0 ? steps : undefined;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/** Builds one emulation layer record with its origin scope, apply time, captured prior state and reviewed revert plan. */
|
|
554
|
-
export function newlayer(input: {
|
|
555
|
-
id: string;
|
|
556
|
-
runid: string;
|
|
557
|
-
stepid: string;
|
|
558
|
-
family: emulationlayer["family"];
|
|
559
|
-
name: string;
|
|
560
|
-
originscope: string;
|
|
561
|
-
revertplan: string[];
|
|
562
|
-
prior?: Record<string, unknown>;
|
|
563
|
-
at: number;
|
|
564
|
-
}): emulationlayer {
|
|
565
|
-
return {
|
|
566
|
-
id: input.id,
|
|
567
|
-
runid: input.runid,
|
|
568
|
-
stepid: input.stepid,
|
|
569
|
-
family: input.family,
|
|
570
|
-
name: input.name,
|
|
571
|
-
originscope: input.originscope,
|
|
572
|
-
appliedat: input.at,
|
|
573
|
-
...(input.prior !== undefined ? { prior: input.prior } : {}),
|
|
574
|
-
revertplan: [...input.revertplan],
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/** Builds the initial emulation state of one run scoped to its tab and origin. */
|
|
579
|
-
export function emulationstateof(input: { runid: string; tabid: number; origin: string; now: number }): emulationstate {
|
|
580
|
-
return { runid: input.runid, tabid: input.tabid, origin: input.origin, layers: [], updatedat: input.now };
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/** Stacks one layer onto the emulation state in apply order: a second layer of the same family replaces the first in effect while both stay in the history, so the last applied layer wins conflicts. */
|
|
584
|
-
export function applylayer(state: emulationstate, layer: emulationlayer, at: number): emulationstate {
|
|
585
|
-
const layers = [...state.layers.filter((item) => item.id !== layer.id), layer];
|
|
586
|
-
return { ...state, layers, updatedat: at };
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/** Reverts one layer of the state by its id: the revert time stamps the record while the layer history survives for review. */
|
|
590
|
-
export function revertlayer(state: emulationstate, layerid: string, at: number): emulationstate {
|
|
591
|
-
const layers = state.layers.map((layer) =>
|
|
592
|
-
layer.id === layerid && layer.revertedat === undefined ? { ...layer, revertedat: at } : layer,
|
|
593
|
-
);
|
|
594
|
-
return { ...state, layers, updatedat: at };
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/** Reverts every active layer of the state in reverse apply order: the reversed list is the exact restore sequence and every layer keeps its revert stamp. */
|
|
598
|
-
export function revertalllayers(
|
|
599
|
-
state: emulationstate,
|
|
600
|
-
at: number,
|
|
601
|
-
): { state: emulationstate; reverted: emulationlayer[] } {
|
|
602
|
-
const reverted = [...state.layers].reverse().filter((layer) => layer.revertedat === undefined);
|
|
603
|
-
const layers = state.layers.map((layer) => (layer.revertedat === undefined ? { ...layer, revertedat: at } : layer));
|
|
604
|
-
return { state: { ...state, layers, updatedat: at }, reverted };
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/** Returns the active layers of the state in apply order. */
|
|
608
|
-
export function activelayers(state: emulationstate | undefined): emulationlayer[] {
|
|
609
|
-
return state ? state.layers.filter((layer) => layer.revertedat === undefined) : [];
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/** Returns the names of the active layers for the response envelope and the review panel. */
|
|
613
|
-
export function layernames(state: emulationstate | undefined): string[] {
|
|
614
|
-
return activelayers(state).map((layer) => layer.name);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/** Counts the active layers of one family so the panel can warn when layers stack on one tab. */
|
|
618
|
-
export function stackedcount(state: emulationstate | undefined): number {
|
|
619
|
-
return activelayers(state).length;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/** True when the reviewed latitude stays inside the -90 to 90 degree range and the longitude inside the -180 to 180 degree range. */
|
|
623
|
-
export function locationrangevalid(latitude: number, longitude: number): boolean {
|
|
624
|
-
return (
|
|
625
|
-
Number.isFinite(latitude) &&
|
|
626
|
-
Number.isFinite(longitude) &&
|
|
627
|
-
latitude >= -90 &&
|
|
628
|
-
latitude <= 90 &&
|
|
629
|
-
longitude >= -180 &&
|
|
630
|
-
longitude <= 180
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/** Validates one user agent string against the reviewed grammar: tokens of word characters, separators, slashes, spaces, numbers and version dots; the string must carry at least one token pair and refuse line breaks. */
|
|
635
|
-
export function agentgrammarvalid(useragent: string): boolean {
|
|
636
|
-
const text = useragent.trim();
|
|
637
|
-
if (text.length === 0 || text.length > 512) return false;
|
|
638
|
-
if (/[\r\n]/.test(text)) return false;
|
|
639
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._+\-()/:; ,]*$/.test(text)) return false;
|
|
640
|
-
return /\/\d/.test(text) || /\d+\.\d+/.test(text);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/** Grades one reviewed permission name by its power: location, camera, microphone and notification overrides carry the user's most sensitive signals and stay the highest grade. */
|
|
644
|
-
export function permissiongrade(name: string): "powerful" | "standard" {
|
|
645
|
-
return name === "geolocation" || name === "camera" || name === "microphone" || name === "notifications"
|
|
646
|
-
? "powerful"
|
|
647
|
-
: "standard";
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/** Matches one script url against a reviewed blackbox pattern of an explicit https origin with single star segments and double star subtrees. */
|
|
651
|
-
export function blackboxmatches(urlpattern: string, url: string): boolean {
|
|
652
|
-
const patternmatch = /^(https:\/\/[^/]+)(\/.*)?$/.exec(urlpattern);
|
|
653
|
-
const urlmatch = /^(https:\/\/[^/]+)(\/.*)?$/.exec(url);
|
|
654
|
-
if (!patternmatch || !urlmatch) return false;
|
|
655
|
-
if (patternmatch[1] !== urlmatch[1]) return false;
|
|
656
|
-
const patternpath = (patternmatch[2] ?? "/").split("/").filter((segment) => segment.length > 0);
|
|
657
|
-
const urlpath = (urlmatch[2] ?? "/").split("/").filter((segment) => segment.length > 0);
|
|
658
|
-
const walk = (patternindex: number, urlindex: number): boolean => {
|
|
659
|
-
if (patternindex >= patternpath.length) return urlindex >= urlpath.length;
|
|
660
|
-
const segment = patternpath[patternindex];
|
|
661
|
-
if (segment === undefined) return false;
|
|
662
|
-
if (segment === "**")
|
|
663
|
-
return walk(patternindex + 1, urlindex) || (urlindex < urlpath.length && walk(patternindex, urlindex + 1));
|
|
664
|
-
if (urlindex >= urlpath.length) return false;
|
|
665
|
-
if (segment !== "*" && segment !== urlpath[urlindex]) return false;
|
|
666
|
-
return walk(patternindex + 1, urlindex + 1);
|
|
667
|
-
};
|
|
668
|
-
return walk(0, 0);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/** Hides blackboxed frames from one stack trace: every frame whose url matches a rule pattern with a traces scope of traces or both is dropped so third party frames never enter the shaped trace. */
|
|
672
|
-
export function hideblackboxedframes(rules: blackboxrule[], frames: stackframe[]): stackframe[] {
|
|
673
|
-
const patterns = rules
|
|
674
|
-
.filter((rule) => rule.tracescope === "traces" || rule.tracescope === "both")
|
|
675
|
-
.flatMap((rule) => rule.urlpatterns);
|
|
676
|
-
if (patterns.length === 0) return frames;
|
|
677
|
-
return frames.filter((frame) => !patterns.some((pattern) => blackboxmatches(pattern, frame.url)));
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/** Marks one url as blackboxed when any rule with a profiles or both trace scope matches it, so the trace shaping lists the hidden third party urls. */
|
|
681
|
-
export function blackboxedurls(rules: blackboxrule[], urls: string[]): string[] {
|
|
682
|
-
const patterns = rules.flatMap((rule) => rule.urlpatterns);
|
|
683
|
-
return urls.filter((url) => patterns.some((pattern) => blackboxmatches(pattern, url)));
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/** Expires the prior states of reverted layers after the retention window while the layer history itself always survives; an absent window keeps every prior state. */
|
|
687
|
-
export function expirelayers(state: emulationstate, retention: number | undefined, now: number): emulationstate {
|
|
688
|
-
if (retention === undefined) return state;
|
|
689
|
-
const layers = state.layers.map((layer) => {
|
|
690
|
-
if (layer.revertedat === undefined || layer.prior === undefined || layer.priorexpired === true) return layer;
|
|
691
|
-
if (now - layer.revertedat <= retention) return layer;
|
|
692
|
-
const { prior, ...metadata } = layer;
|
|
693
|
-
void prior;
|
|
694
|
-
return { ...metadata, priorexpired: true };
|
|
695
|
-
});
|
|
696
|
-
return { ...state, layers, updatedat: now };
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/** Builds one shareable preset library file of the user curated presets; the version carries the library contract for imports through review. */
|
|
700
|
-
export function exportpresetlibrary(input: {
|
|
701
|
-
devices: devicepreset[];
|
|
702
|
-
networks: networkpreset[];
|
|
703
|
-
locations: locationpreset[];
|
|
704
|
-
agents: agentpreset[];
|
|
705
|
-
now: number;
|
|
706
|
-
}): presetlibrary {
|
|
707
|
-
return {
|
|
708
|
-
version: 1,
|
|
709
|
-
devices: [...input.devices],
|
|
710
|
-
networks: [...input.networks],
|
|
711
|
-
locations: [...input.locations],
|
|
712
|
-
agents: [...input.agents],
|
|
713
|
-
exportedat: input.now,
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/** Parses one reviewed preset library file: every preset entry must pass its normalizer and a file without any valid preset is refused; unknown fields stay ignored. */
|
|
718
|
-
export function importpresetlibrary(value: unknown): presetlibrary | undefined {
|
|
719
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
720
|
-
const entry = value as Record<string, unknown>;
|
|
721
|
-
const devices = (Array.isArray(entry.devices) ? entry.devices : []).flatMap((preset) => {
|
|
722
|
-
const parsed = devicepresetof(preset);
|
|
723
|
-
return parsed !== undefined ? [parsed] : [];
|
|
724
|
-
});
|
|
725
|
-
const networks = (Array.isArray(entry.networks) ? entry.networks : []).flatMap((preset) => {
|
|
726
|
-
const parsed = networkpresetof(preset);
|
|
727
|
-
return parsed !== undefined ? [parsed] : [];
|
|
728
|
-
});
|
|
729
|
-
const locations = (Array.isArray(entry.locations) ? entry.locations : []).flatMap((preset) => {
|
|
730
|
-
const parsed = locationpresetof(preset);
|
|
731
|
-
return parsed !== undefined ? [parsed] : [];
|
|
732
|
-
});
|
|
733
|
-
const agents = (Array.isArray(entry.agents) ? entry.agents : []).flatMap((preset) => {
|
|
734
|
-
const parsed = agentpresetof(preset);
|
|
735
|
-
return parsed !== undefined ? [parsed] : [];
|
|
736
|
-
});
|
|
737
|
-
if (devices.length + networks.length + locations.length + agents.length === 0) return undefined;
|
|
738
|
-
return {
|
|
739
|
-
version:
|
|
740
|
-
typeof entry.version === "number" && Number.isInteger(entry.version) && entry.version >= 1 ? entry.version : 1,
|
|
741
|
-
devices,
|
|
742
|
-
networks,
|
|
743
|
-
locations,
|
|
744
|
-
agents,
|
|
745
|
-
exportedat: typeof entry.exportedat === "number" ? entry.exportedat : Date.now(),
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/** True when one approved location consent of that origin covers the reviewed coordinates; the prompt shows the exact latitude and longitude before the override applies. */
|
|
750
|
-
export function locationconsentcovers(
|
|
751
|
-
origin: string,
|
|
752
|
-
latitude: number,
|
|
753
|
-
longitude: number,
|
|
754
|
-
consents: locationconsent[],
|
|
755
|
-
): boolean {
|
|
756
|
-
return consents.some(
|
|
757
|
-
(consent) =>
|
|
758
|
-
consent.origin === origin &&
|
|
759
|
-
consent.approved === true &&
|
|
760
|
-
consent.revokedat === undefined &&
|
|
761
|
-
consent.latitude === latitude &&
|
|
762
|
-
consent.longitude === longitude,
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
/* ── Merged from sandboxframe.ts ── */
|
|
767
|
-
import type { environmentprovenance, sandboxrender, sandboxrenderresult } from "./types.js";
|
|
768
|
-
|
|
769
|
-
/**
|
|
770
|
-
* Sandbox frame logic of the 1.1.60 family.
|
|
771
|
-
* Correlated rules for untrusted markup live here: the render descriptor with its per render nonce, the stripping of scripts and event handlers before any render, the postmessage envelope the sandboxed page answers through, the nonce checked acceptance of every render result and the provenance each render records with its source origin.
|
|
772
|
-
* The sandboxed page carries no extension privileges: a render result never reenters the dom outside the frame, and the untrusted markup never touches the page the session granted.
|
|
773
|
-
*/
|
|
774
|
-
|
|
775
|
-
/** Strips every script and event handler from untrusted markup before render through one linear character scan, because a replace chain over reassembled payloads (`<scr<script>ipt>` or `javajavascript:script:`) reads as incomplete sanitization to static analysis even inside a convergence loop. The scan rebuilds the markup instead of removing fragments from it: a well formed tag keeps its name and its safe attributes with the script url schemes stripped from the values, every `on` event handler attribute drops, a script element drops with its whole content through the closing tag, and a malformed tag whose body carries another open angle (the reassembly payloads live on) drops entirely so no `<script` or `on...=` sequence ever survives into the sanitized output. */
|
|
776
|
-
export function stripscripts(markup: string): string {
|
|
777
|
-
let out = "";
|
|
778
|
-
let index = 0;
|
|
779
|
-
while (index < markup.length) {
|
|
780
|
-
const open = markup.indexOf("<", index);
|
|
781
|
-
if (open === -1) {
|
|
782
|
-
out += markup.slice(index);
|
|
783
|
-
break;
|
|
784
|
-
}
|
|
785
|
-
out += markup.slice(index, open);
|
|
786
|
-
const close = markup.indexOf(">", open);
|
|
787
|
-
if (close === -1) {
|
|
788
|
-
index = markup.length;
|
|
789
|
-
break;
|
|
790
|
-
} /* an unterminated tag fragment never renders */
|
|
791
|
-
const body = markup.slice(open + 1, close);
|
|
792
|
-
if (body.includes("<")) {
|
|
793
|
-
index = close + 1;
|
|
794
|
-
continue;
|
|
795
|
-
} /* a reassembled payload drops as a whole */
|
|
796
|
-
const namematch = /^\/?\s*([a-zA-Z][a-zA-Z0-9:-]*)/.exec(body);
|
|
797
|
-
const name = (namematch?.[1] ?? "").toLowerCase();
|
|
798
|
-
if (name === "script") {
|
|
799
|
-
if (body.startsWith("/") || body.endsWith("/")) {
|
|
800
|
-
index = close + 1;
|
|
801
|
-
continue;
|
|
802
|
-
} /* a closing or self closing script tag drops alone */
|
|
803
|
-
const rest = markup.slice(close + 1).toLowerCase();
|
|
804
|
-
const end = rest.indexOf("</script");
|
|
805
|
-
if (end === -1) {
|
|
806
|
-
index = markup.length;
|
|
807
|
-
break;
|
|
808
|
-
} /* an unterminated script element swallows the rest */
|
|
809
|
-
const endclose = markup.indexOf(">", close + 1 + end);
|
|
810
|
-
index = endclose === -1 ? markup.length : endclose + 1;
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
out += rebuildtag(name, body);
|
|
814
|
-
index = close + 1;
|
|
815
|
-
}
|
|
816
|
-
return out.trim();
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/** Rebuilds one tag with its event handler attributes dropped and its script url schemes stripped: the name keeps its letter case, every kept attribute re-renders with double quoted values, and a non element tag (a comment or a doctype) passes through verbatim because its body already carries no open angle. */
|
|
820
|
-
function rebuildtag(name: string, body: string): string {
|
|
821
|
-
if (name === "") return `<${body}>`;
|
|
822
|
-
if (body.startsWith("/")) return `</${name}>`;
|
|
823
|
-
const kept: string[] = [];
|
|
824
|
-
let cursor = namelengthof(body);
|
|
825
|
-
while (cursor < body.length) {
|
|
826
|
-
while (cursor < body.length && /\s/.test(charof(body, cursor))) cursor += 1;
|
|
827
|
-
if (cursor >= body.length) break;
|
|
828
|
-
const attrstart = cursor;
|
|
829
|
-
while (cursor < body.length && !/\s/.test(charof(body, cursor)) && charof(body, cursor) !== "=") cursor += 1;
|
|
830
|
-
const attrname = body.slice(attrstart, cursor).toLowerCase();
|
|
831
|
-
if (attrname.startsWith("on") && attrname.length > 2) {
|
|
832
|
-
cursor = skipattribute(body, cursor);
|
|
833
|
-
continue;
|
|
834
|
-
}
|
|
835
|
-
if (charof(body, cursor) !== "=") {
|
|
836
|
-
kept.push(attrname);
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
cursor += 1;
|
|
840
|
-
const quote = charof(body, cursor);
|
|
841
|
-
if (quote === '"' || quote === "'") {
|
|
842
|
-
cursor += 1;
|
|
843
|
-
const valueend = body.indexOf(quote, cursor);
|
|
844
|
-
const value = stripschemes(body.slice(cursor, valueend === -1 ? body.length : valueend));
|
|
845
|
-
cursor = valueend === -1 ? body.length : valueend + 1;
|
|
846
|
-
kept.push(`${attrname}="${value}"`);
|
|
847
|
-
continue;
|
|
848
|
-
}
|
|
849
|
-
const valuestart = cursor;
|
|
850
|
-
while (cursor < body.length && !/\s/.test(charof(body, cursor))) cursor += 1;
|
|
851
|
-
kept.push(`${attrname}="${stripschemes(body.slice(valuestart, cursor))}"`);
|
|
852
|
-
}
|
|
853
|
-
return kept.length === 0 ? `<${name}>` : `<${name} ${kept.join(" ")}>`;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/** Reads one character of a body safely: an out of bounds position answers the empty string so the strict index access never surfaces undefined. */
|
|
857
|
-
function charof(text: string, at: number): string {
|
|
858
|
-
return at >= 0 && at < text.length ? text[at]! : "";
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
/** Reads the length of the tag name the body starts with: the attribute walk starts right after it. */
|
|
862
|
-
function namelengthof(body: string): number {
|
|
863
|
-
const match = /^[a-zA-Z][a-zA-Z0-9:-]*/.exec(body);
|
|
864
|
-
return match === null ? 0 : match[0].length;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/** Advances past one attribute value (a quoted or bare run) so a dropped event handler leaves nothing behind: the function answers the cursor after the value. */
|
|
868
|
-
function skipattribute(body: string, cursor: number): number {
|
|
869
|
-
if (charof(body, cursor) !== "=") return cursor;
|
|
870
|
-
const at = cursor + 1;
|
|
871
|
-
const quote = charof(body, at);
|
|
872
|
-
if (quote === '"' || quote === "'") {
|
|
873
|
-
const valueend = body.indexOf(quote, at + 1);
|
|
874
|
-
return valueend === -1 ? body.length : valueend + 1;
|
|
875
|
-
}
|
|
876
|
-
let bare = at;
|
|
877
|
-
while (bare < body.length && !/\s/.test(charof(body, bare))) bare += 1;
|
|
878
|
-
return bare;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/** Strips the script url schemes from one attribute value through plain index finds, so no regular expression runs over untrusted text anywhere in the sanitizer. */
|
|
882
|
-
function stripschemes(value: string): string {
|
|
883
|
-
let out = value;
|
|
884
|
-
for (const scheme of ["javascript:", "vbscript:", "data:"]) {
|
|
885
|
-
let at = out.toLowerCase().indexOf(scheme);
|
|
886
|
-
while (at >= 0) {
|
|
887
|
-
out = out.slice(0, at) + out.slice(at + scheme.length);
|
|
888
|
-
at = out.toLowerCase().indexOf(scheme);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
return out;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/** Strips every tag from a render result text through a single linear scan: an open angle starts a tag that runs to the next close angle or, when no close angle follows, to the end of the text, so an unclosed fragment never survives into the result. */
|
|
895
|
-
function striptags(text: string): string {
|
|
896
|
-
let out = "";
|
|
897
|
-
let index = 0;
|
|
898
|
-
while (index < text.length) {
|
|
899
|
-
const open = text.indexOf("<", index);
|
|
900
|
-
if (open === -1) {
|
|
901
|
-
out += text.slice(index);
|
|
902
|
-
break;
|
|
903
|
-
}
|
|
904
|
-
out += text.slice(index, open);
|
|
905
|
-
const close = text.indexOf(">", open);
|
|
906
|
-
if (close === -1) break;
|
|
907
|
-
index = close + 1;
|
|
908
|
-
}
|
|
909
|
-
return out;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/** Issues the per render nonce of one sandbox render: every message the sandboxed page answers carries the nonce of exactly one render so a stale or replayed message never passes. */
|
|
913
|
-
export function nonceof(seed: string): string {
|
|
914
|
-
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
915
|
-
let hash = 0;
|
|
916
|
-
for (let index = 0; index < seed.length; index += 1) hash = (hash * 31 + seed.charCodeAt(index)) >>> 0;
|
|
917
|
-
let nonce = "";
|
|
918
|
-
let state = hash === 0 ? 0x9e3779b9 : hash;
|
|
919
|
-
for (let index = 0; index < 16; index += 1) {
|
|
920
|
-
state = (state * 1664525 + 1013904223) >>> 0;
|
|
921
|
-
nonce += alphabet[state % alphabet.length];
|
|
922
|
-
}
|
|
923
|
-
return nonce;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
/** Builds one sandbox render descriptor: the sanitized markup, the per render nonce and the source origin of the untrusted markup, ready for the sandboxed page that holds no extension privileges. */
|
|
927
|
-
export function sandboxrenderof(input: {
|
|
928
|
-
id: string;
|
|
929
|
-
markup: string;
|
|
930
|
-
sourceorigin: string;
|
|
931
|
-
stepid: string;
|
|
932
|
-
now: number;
|
|
933
|
-
}): sandboxrender {
|
|
934
|
-
if (input.markup.trim() === "") throw new Error("The sandbox render needs its untrusted markup.");
|
|
935
|
-
if (input.sourceorigin.trim() === "")
|
|
936
|
-
throw new Error("The sandbox render needs the source origin of its untrusted markup.");
|
|
937
|
-
if (input.stepid.trim() === "") throw new Error("The sandbox render names the reviewed step it renders for.");
|
|
938
|
-
return {
|
|
939
|
-
id: input.id,
|
|
940
|
-
nonce: nonceof(`${input.id}:${input.now}`),
|
|
941
|
-
markup: stripscripts(input.markup),
|
|
942
|
-
sourceorigin: input.sourceorigin,
|
|
943
|
-
stepid: input.stepid,
|
|
944
|
-
renderedat: input.now,
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
/** Builds the postmessage envelope the executor posts into the sandboxed page: the render nonce and the sanitized markup travel together so the frame answers with the same nonce only. */
|
|
949
|
-
export function rendermessage(render: sandboxrender): { channel: string; type: string; nonce: string; markup: string } {
|
|
950
|
-
return { channel: "devthinksandbox", type: "render", nonce: render.nonce, markup: render.markup };
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
/** Accepts one sandbox render result posted back through postmessage: the nonce must answer exactly one known unanswered render, an unknown, replayed or already answered nonce refuses, and the accepted result stays plain text that never reenters the dom outside the frame. The returned renders carry the answeredat marker of the accepted render so the caller threads the closed render out of its pending set. */
|
|
954
|
-
export function acceptrenderresult(input: {
|
|
955
|
-
renders: sandboxrender[];
|
|
956
|
-
message: { channel?: string; type?: string; nonce?: string; ok?: boolean; text?: string; summary?: string };
|
|
957
|
-
now: number;
|
|
958
|
-
}): { accepted: boolean; result?: sandboxrenderresult; reason: string; renders: sandboxrender[] } {
|
|
959
|
-
if (input.message.channel !== "devthinksandbox")
|
|
960
|
-
return {
|
|
961
|
-
accepted: false,
|
|
962
|
-
reason: "The sandbox message travels the devthinksandbox channel only.",
|
|
963
|
-
renders: input.renders,
|
|
964
|
-
};
|
|
965
|
-
if (input.message.type !== "renderresult")
|
|
966
|
-
return {
|
|
967
|
-
accepted: false,
|
|
968
|
-
reason: "The sandbox message answers with the renderresult type only.",
|
|
969
|
-
renders: input.renders,
|
|
970
|
-
};
|
|
971
|
-
const render = input.renders.find(
|
|
972
|
-
(entry) => entry.nonce === input.message.nonce && entry.answeredat === undefined && entry.renderedat <= input.now,
|
|
973
|
-
);
|
|
974
|
-
if (!render)
|
|
975
|
-
return {
|
|
976
|
-
accepted: false,
|
|
977
|
-
reason:
|
|
978
|
-
"The sandbox message carries no nonce of a known unanswered render; a stale, replayed or already answered message never passes.",
|
|
979
|
-
renders: input.renders,
|
|
980
|
-
};
|
|
981
|
-
const text = striptags(input.message.text ?? "");
|
|
982
|
-
const result: sandboxrenderresult = {
|
|
983
|
-
nonce: render.nonce,
|
|
984
|
-
ok: input.message.ok !== false,
|
|
985
|
-
text,
|
|
986
|
-
summary:
|
|
987
|
-
input.message.summary?.trim() ||
|
|
988
|
-
`The sandbox frame rendered the markup of the step ${render.stepid} and returned its inert text.`,
|
|
989
|
-
at: input.now,
|
|
990
|
-
};
|
|
991
|
-
const renders = input.renders.map((entry) =>
|
|
992
|
-
entry.nonce === render.nonce ? { ...entry, answeredat: input.now } : entry,
|
|
993
|
-
);
|
|
994
|
-
return {
|
|
995
|
-
accepted: true,
|
|
996
|
-
result,
|
|
997
|
-
reason: `The render result of the step ${render.stepid} answers the nonce of its render; the text stays inside the frame.`,
|
|
998
|
-
renders,
|
|
999
|
-
};
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
/** Reads the provenance of one sandbox render: the source origin, the step and the sandboxframe environment land beside the stored render so the audit reads where every untrusted markup came from. */
|
|
1003
|
-
export function renderprovenance(render: sandboxrender): environmentprovenance {
|
|
1004
|
-
return { origin: render.sourceorigin, stepid: render.stepid, environment: "sandboxframe" };
|
|
1005
|
-
}
|