@wenathlan/devthink 2.0.13 → 2.0.15
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 +27 -0
- package/accounts.ts +2087 -0
- package/agent.ts +2331 -0
- package/antigravity.ts +2139 -0
- package/apifreeze.ts +4146 -0
- package/auth.ts +642 -0
- package/background.ts +16837 -0
- package/bridge.ts +2080 -0
- package/build.ts +54 -0
- package/bun.ts +26 -0
- package/capture.ts +1614 -0
- package/checksecrets.ts +72 -0
- package/cjs.ts +9 -0
- package/cli.ts +3500 -0
- package/commands.ts +1101 -0
- package/companion.ts +450 -0
- package/compatibility.ts +37 -0
- package/config.ts +3137 -0
- package/constants.ts +943 -0
- package/core.ts +1473 -0
- package/crossbrowser.ts +1359 -0
- package/dashdone.ts +605 -0
- package/data.ts +943 -0
- package/database.ts +133 -0
- package/debug.ts +850 -0
- package/deno.ts +27 -0
- package/devthink.ts +707 -0
- package/dist/accounts.d.ts +409 -0
- package/dist/agent.d.ts +837 -0
- package/dist/antigravity.d.ts +380 -0
- package/dist/apifreeze.d.ts +113 -0
- package/dist/auth.d.ts +224 -0
- package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
- package/dist/capture.d.ts +595 -0
- package/dist/cjs.d.ts +6 -0
- package/{cli.js → dist/cli.js} +2 -2
- package/dist/commands.d.ts +310 -0
- package/dist/compatibility.d.ts +17 -0
- package/dist/config.d.ts +1853 -0
- package/dist/constants.d.ts +637 -0
- package/dist/core.d.ts +361 -0
- package/dist/data.d.ts +330 -0
- package/dist/database.d.ts +24 -0
- package/dist/debug.d.ts +270 -0
- package/dist/engine.d.ts +74 -0
- package/dist/environments.d.ts +244 -0
- package/dist/evidence.d.ts +277 -0
- package/dist/export.d.ts +311 -0
- package/dist/fingerprint.d.ts +594 -0
- package/dist/flow.d.ts +300 -0
- package/dist/gates.d.ts +103 -0
- package/dist/identity.d.ts +51 -0
- package/dist/ids.d.ts +7 -0
- package/dist/llm.d.ts +652 -0
- package/dist/models.d.ts +1807 -0
- package/dist/net.d.ts +325 -0
- package/dist/neutral.d.ts +41 -0
- package/dist/oauth.d.ts +703 -0
- package/dist/page.d.ts +1166 -0
- package/dist/perf.d.ts +481 -0
- package/dist/plan.d.ts +134 -0
- package/dist/project.d.ts +395 -0
- package/dist/providers.d.ts +37 -0
- package/dist/quota.d.ts +453 -0
- package/dist/recovery.d.ts +621 -0
- package/dist/request.d.ts +1492 -0
- package/dist/run.d.ts +779 -0
- package/dist/runtime.d.ts +212 -0
- package/dist/search.d.ts +375 -0
- package/dist/security.d.ts +534 -0
- package/dist/serve.d.ts +136 -0
- package/dist/session.d.ts +296 -0
- package/dist/storage.d.ts +53 -0
- package/dist/streaming.d.ts +1119 -0
- package/dist/swarm.d.ts +328 -0
- package/dist/tools.d.ts +170 -0
- package/dist/types.d.ts +8357 -0
- package/dist/utils.d.ts +82 -0
- package/dist/version.d.ts +7 -0
- package/dist/versionregistry.d.ts +406 -0
- package/dist/views.d.ts +579 -0
- package/dist/workbenchsession.d.ts +69 -0
- package/dist/workflow.d.ts +649 -0
- package/docs/readme.md +92 -0
- package/docs/schemas/antigravity.json +192 -0
- package/engine.ts +1524 -0
- package/environments.ts +1005 -0
- package/evidence.ts +643 -0
- package/export.ts +889 -0
- package/fingerprint.ts +1458 -0
- package/flow.ts +1190 -0
- package/gates.ts +307 -0
- package/gateway.ts +34 -0
- package/hardening.ts +191 -0
- package/headless.ts +369 -0
- package/http.ts +2110 -0
- package/identity.ts +246 -0
- package/ids.ts +26 -0
- package/index.ts +369 -0
- package/llm.ts +2682 -0
- package/mcp.ts +1835 -0
- package/{memory.js → memory.ts} +3834 -2310
- package/models.ts +3805 -0
- package/modes.ts +43 -0
- package/net.ts +1214 -0
- package/neutral.ts +362 -0
- package/node.ts +26 -0
- package/oauth.ts +2216 -0
- package/offscreen.ts +170 -0
- package/pack.ts +956 -0
- package/package.json +175 -82
- package/page.ts +7824 -0
- package/pagebridge.ts +1116 -0
- package/perf.ts +957 -0
- package/plan.ts +672 -0
- package/plugin.ts +76 -0
- package/{policy.js → policy.ts} +5073 -4679
- package/prisma.config.ts +27 -0
- package/progress.ts +1308 -0
- package/project.ts +1291 -0
- package/protocol.ts +4187 -0
- package/providers.ts +246 -0
- package/quota.ts +1887 -0
- package/recovery.ts +2433 -0
- package/request.ts +4482 -0
- package/run.ts +2135 -0
- package/runtime.ts +526 -0
- package/search.ts +1760 -0
- package/security.ts +1673 -0
- package/serve.ts +419 -0
- package/server.ts +5538 -0
- package/session.ts +1162 -0
- package/storage.ts +182 -0
- package/streaming.ts +4101 -0
- package/swarm.ts +884 -0
- package/sync.ts +56 -0
- package/tests/agentcert.test.ts +128 -0
- package/tests/agentfleet.test.ts +214 -0
- package/tests/agentreview.test.ts +241 -0
- package/tests/agentstream.test.ts +335 -0
- package/tests/agentwork.test.ts +356 -0
- package/tests/apifreeze.test.ts +290 -0
- package/tests/apimap.test.ts +112 -0
- package/tests/approvalgate.test.ts +175 -0
- package/tests/artifactmanifest.test.ts +204 -0
- package/tests/attentionfeed.test.ts +163 -0
- package/tests/backgroundruns.test.ts +100 -0
- package/tests/batchscheduling.test.ts +131 -0
- package/tests/blackboard.test.ts +238 -0
- package/tests/bridge.test.ts +1088 -0
- package/tests/browsercoverage.test.ts +134 -0
- package/tests/browserpolyfills.test.ts +124 -0
- package/tests/build.mjs +1260 -0
- package/tests/clientauth.test.ts +223 -0
- package/tests/clitools.test.ts +578 -0
- package/tests/confirmgates.test.ts +190 -0
- package/tests/containerpack.test.ts +143 -0
- package/tests/controlflow.test.ts +2120 -0
- package/tests/coordination.test.ts +345 -0
- package/tests/copyscreen.test.ts +139 -0
- package/tests/core.test.ts +417 -0
- package/tests/costcert.test.ts +95 -0
- package/tests/crossbrowser.test.ts +115 -0
- package/tests/cspaudit.test.ts +79 -0
- package/tests/dashdone.test.ts +615 -0
- package/tests/datagrid.test.ts +137 -0
- package/tests/doccheck.test.ts +107 -0
- package/tests/dombatch.test.ts +109 -0
- package/tests/emulation.test.ts +282 -0
- package/tests/environments.test.ts +259 -0
- package/tests/evidenceviews.test.ts +88 -0
- package/tests/exporttools.test.ts +185 -0
- package/tests/firefoxprep.test.ts +134 -0
- package/tests/flowlibrary.test.ts +242 -0
- package/tests/flowrun.test.ts +303 -0
- package/tests/forensics.test.ts +439 -0
- package/tests/gateway.test.ts +939 -0
- package/tests/headlesslib.test.ts +171 -0
- package/tests/httpstream.test.ts +277 -0
- package/tests/immutablelog.test.ts +157 -0
- package/tests/inboundguard.test.ts +177 -0
- package/tests/lazyload.test.ts +72 -0
- package/tests/librarymodes.test.ts +623 -0
- package/tests/llm.test.ts +696 -0
- package/tests/manifest.test.ts +226 -0
- package/tests/maskinputs.test.ts +150 -0
- package/tests/matrixverify.test.ts +111 -0
- package/tests/mavenpack.test.ts +75 -0
- package/tests/mcpserver.test.ts +704 -0
- package/tests/memory.test.ts +4155 -0
- package/tests/memorycare.test.ts +230 -0
- package/tests/migrateplan.test.ts +349 -0
- package/tests/minimization.test.ts +414 -0
- package/tests/modelroute.test.ts +112 -0
- package/tests/multiagent.test.ts +440 -0
- package/tests/nativebridge.test.ts +807 -0
- package/tests/navigation.test.ts +284 -0
- package/tests/nugetpack.test.ts +83 -0
- package/tests/orchestration.test.ts +449 -0
- package/tests/originpolicy.test.ts +405 -0
- package/tests/outputcompare.test.ts +97 -0
- package/tests/package.test.ts +91 -0
- package/tests/pagebridge.test.ts +5909 -0
- package/tests/pentest.test.ts +75 -0
- package/tests/perfrecords.test.ts +257 -0
- package/tests/permdiff.test.ts +85 -0
- package/tests/phishguard.test.ts +128 -0
- package/tests/pickerviews.test.ts +155 -0
- package/tests/pipelines.test.ts +381 -0
- package/tests/planlint.test.ts +240 -0
- package/tests/planreview.test.ts +383 -0
- package/tests/platformtargets.test.ts +56 -0
- package/tests/policy.test.ts +8284 -0
- package/tests/portability.test.ts +96 -0
- package/tests/progress.test.ts +1036 -0
- package/tests/promptlibrary.test.ts +116 -0
- package/tests/protocol.test.ts +4793 -0
- package/tests/publishingpipeline.test.ts +314 -0
- package/tests/quickactions.test.ts +164 -0
- package/tests/readiness.test.ts +78 -0
- package/tests/recipes.test.ts +163 -0
- package/tests/redactshots.test.ts +216 -0
- package/tests/registernvidiakeys.ts +51 -0
- package/tests/relayserve.test.ts +304 -0
- package/tests/releasenotes.test.ts +276 -0
- package/tests/resilience.test.ts +467 -0
- package/tests/resourceaware.test.ts +187 -0
- package/tests/runbudget.test.ts +125 -0
- package/tests/runhistory.test.ts +299 -0
- package/tests/runreplay.test.ts +113 -0
- package/tests/runresume.test.ts +184 -0
- package/tests/runstate.test.ts +288 -0
- package/tests/runtimeadapters.test.ts +251 -0
- package/tests/safariskeleton.test.ts +115 -0
- package/tests/sandboxframe.test.ts +196 -0
- package/tests/sbom.test.ts +105 -0
- package/tests/secretvault.test.ts +188 -0
- package/tests/selcache.test.ts +111 -0
- package/tests/sessioninterface.test.ts +791 -0
- package/tests/sessions.test.ts +591 -0
- package/tests/siteprefs.test.ts +228 -0
- package/tests/snapshotdelta.test.ts +165 -0
- package/tests/statusviews.test.ts +237 -0
- package/tests/stepmeter.test.ts +200 -0
- package/tests/streamparse.test.ts +163 -0
- package/tests/stylepruning.test.ts +90 -0
- package/tests/surfaces.test.ts +282 -0
- package/tests/sweep.test.ts +78 -0
- package/tests/syncbridge.test.ts +164 -0
- package/tests/taskqueue.test.ts +206 -0
- package/tests/toolcalls.test.ts +336 -0
- package/tests/toolcatalog.test.ts +201 -0
- package/tests/torture-agent.test.ts +706 -0
- package/tests/torture-auth.test.ts +461 -0
- package/tests/torture-commands.test.ts +895 -0
- package/tests/torture-data.test.ts +923 -0
- package/tests/torture-environments.test.ts +285 -0
- package/tests/torture-export.test.ts +263 -0
- package/tests/torture-http.test.ts +1379 -0
- package/tests/torture-llm.test.ts +808 -0
- package/tests/torture-memory.test.ts +1084 -0
- package/tests/torture-plan.test.ts +829 -0
- package/tests/torture-run.test.ts +626 -0
- package/tests/torture-session.test.ts +928 -0
- package/tests/torture-swarm.test.ts +1245 -0
- package/tests/torture-tools.test.ts +964 -0
- package/tests/torture-workflow.test.ts +2354 -0
- package/tests/tourviews.test.ts +52 -0
- package/tests/transparency.test.ts +147 -0
- package/tests/trigger.test.ts +941 -0
- package/tests/v1sunset.test.ts +428 -0
- package/tests/virtlist.test.ts +94 -0
- package/tests/vision.test.ts +540 -0
- package/tests/vsixpack.test.ts +145 -0
- package/tests/webapi.test.ts +523 -0
- package/tests/workflow.test.ts +1785 -0
- package/tests/workfloweditor.test.ts +759 -0
- package/tests/xpipack.test.ts +128 -0
- package/tools.ts +734 -0
- package/types.ts +9908 -0
- package/umd.ts +9 -0
- package/utils.ts +249 -0
- package/version.ts +8 -0
- package/versionregistry.ts +1310 -0
- package/views.ts +2153 -0
- package/vitest.config.ts +12 -0
- package/web/App.tsx +78 -0
- package/web/README.md +47 -0
- package/web/capacitor.config.ts +46 -0
- package/web/const.ts +19 -0
- package/web/control.shell.tsx +66 -0
- package/web/dashboardpage.ts +554 -0
- package/web/db.ts +493 -0
- package/web/design.html +541 -0
- package/web/gatewayview/Gateway.tsx +345 -0
- package/web/gatewayview/config.ts +1042 -0
- package/web/gatewayview/definition.ts +174 -0
- package/web/icons.ts +15 -0
- package/{manifest.json → web/manifest.json} +1 -1
- package/web/optionspage.ts +691 -0
- package/web/popup.ts +1651 -0
- package/web/schema.prisma +176 -0
- package/web/sidepanel.ts +7941 -0
- package/web/sitemanifest.json +13 -0
- package/web/terminal-ui.tsx +622 -0
- package/web/transparencypage.ts +541 -0
- package/web/vite.config.ts +39 -0
- package/web/workspace.ts +23 -0
- package/workbenchmemory.ts +104 -0
- package/workbenchsession.ts +304 -0
- package/workflow.ts +4495 -0
- package/bridge.d.ts.map +0 -1
- package/bridge.js +0 -1877
- package/bun.d.ts.map +0 -1
- package/bun.js +0 -52329
- package/caps/background.json +0 -712
- package/caps/cli.json +0 -360
- package/caps/library.json +0 -3611
- package/caps/mcp.json +0 -126
- package/caps/pagebridge.json +0 -377
- package/caps/popup.json +0 -51
- package/caps/sidepanel.json +0 -522
- package/checksums.txt +0 -142
- package/cli.d.ts.map +0 -1
- package/companion.d.ts.map +0 -1
- package/companion.js +0 -389
- package/crossbrowser.d.ts.map +0 -1
- package/crossbrowser.js +0 -1191
- package/dashdone.d.ts.map +0 -1
- package/dashdone.js +0 -368
- package/deno.d.ts.map +0 -1
- package/deno.js +0 -52325
- package/devthink.umd.js +0 -52346
- package/fixtures/bun.ts +0 -7
- package/fixtures/cjs.js +0 -6
- package/fixtures/deno.ts +0 -6
- package/fixtures/example-org-pagestate.json +0 -22
- package/fixtures/importers/automa-workflow.json +0 -18
- package/fixtures/importers/selenium-side.json +0 -20
- package/fixtures/importers/tabular-plan.csv +0 -7
- package/fixtures/importers/uivision-macro.json +0 -10
- package/fixtures/importers/v1-plan.json +0 -13
- package/fixtures/mcp-client.mjs +0 -14
- package/fixtures/pages/checkout.html +0 -192
- package/fixtures/pages/dashboard.html +0 -110
- package/fixtures/pages/feed.html +0 -113
- package/fixtures/pages/product-grid.html +0 -150
- package/fixtures/plans/form-inventory-plan.json +0 -11
- package/fixtures/plans/release-notes-plan.json +0 -11
- package/fixtures/recipes/agentscompete.json +0 -11
- package/fixtures/recipes/agentsforms.json +0 -11
- package/fixtures/recipes/agentsmonitor.json +0 -11
- package/fixtures/recipes/agentsreview.json +0 -29
- package/fixtures/recipes/agentswarm.json +0 -11
- package/fixtures/recipes/fillcalendar.json +0 -12
- package/fixtures/recipes/fillcheckout.json +0 -13
- package/fixtures/recipes/filldependentdropdowns.json +0 -12
- package/fixtures/recipes/fillfromcsv.json +0 -13
- package/fixtures/recipes/fillloginform.json +0 -12
- package/fixtures/recipes/fillmultistepwizard.json +0 -13
- package/fixtures/recipes/fillregistration.json +0 -12
- package/fixtures/recipes/fillsearchfilters.json +0 -12
- package/fixtures/recipes/filltypeahead.json +0 -11
- package/fixtures/recipes/monitoralerts.json +0 -12
- package/fixtures/recipes/monitoravailability.json +0 -11
- package/fixtures/recipes/monitornetworkquiet.json +0 -12
- package/fixtures/recipes/monitorpagechanges.json +0 -11
- package/fixtures/recipes/monitorpricechange.json +0 -11
- package/fixtures/recipes/monitorschedule.json +0 -12
- package/fixtures/recipes/scrapedynamicprices.json +0 -13
- package/fixtures/recipes/scrapeimagegallery.json +0 -12
- package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
- package/fixtures/recipes/scrapenestedlists.json +0 -12
- package/fixtures/recipes/scrapeproductgrid.json +0 -12
- package/fixtures/recipes/scrapereviews.json +0 -12
- package/fixtures/recipes/scrapesearchresults.json +0 -12
- package/fixtures/recipes/scrapetablepagination.json +0 -12
- package/fixtures/recipes/testaccessibility.json +0 -34
- package/fixtures/recipes/testconsoleerrors.json +0 -12
- package/fixtures/recipes/testdeeplinks.json +0 -12
- package/fixtures/recipes/testformvalidation.json +0 -13
- package/fixtures/recipes/testlayoutshift.json +0 -12
- package/fixtures/recipes/testlinkcheck.json +0 -12
- package/fixtures/recipes/testloadtiming.json +0 -12
- package/fixtures/recipes/testvisualdiff.json +0 -12
- package/fixtures/workflows/changelog-digest-workflow.json +0 -13
- package/fixtures/workflows/form-survey-workflow.json +0 -13
- package/gallery.json +0 -706
- package/hardening.d.ts.map +0 -1
- package/hardening.js +0 -188
- package/headless.d.ts.map +0 -1
- package/headless.js +0 -11522
- package/http.d.ts.map +0 -1
- package/http.js +0 -2165
- package/index.cjs +0 -54682
- package/index.d.ts.map +0 -1
- package/index.js +0 -52310
- package/index.neutral.js +0 -46343
- package/mcp.d.ts.map +0 -1
- package/mcp.js +0 -20515
- package/memory.d.ts.map +0 -1
- package/nativehost.template.json +0 -10
- package/node.cjs +0 -54686
- package/node.d.ts.map +0 -1
- package/pack.d.ts.map +0 -1
- package/pack.js +0 -805
- package/policy.d.ts.map +0 -1
- package/progress.d.ts.map +0 -1
- package/progress.js +0 -645
- package/protocol.d.ts.map +0 -1
- package/protocol.js +0 -8334
- package/schemas/audit.schema.json +0 -226
- package/schemas/capability.schema.json +0 -113
- package/schemas/envelope.schema.json +0 -162
- package/schemas/memory.schema.json +0 -51
- package/schemas/observation.schema.json +0 -113
- package/schemas/plan.schema.json +0 -208
- package/schemas/progress.schema.json +0 -126
- package/schemas/proposal.schema.json +0 -68
- package/schemas/session.schema.json +0 -82
- package/schemas/tool.schema.json +0 -94
- package/server.d.ts.map +0 -1
- package/server.js +0 -28877
- package/umd-example.html +0 -18
- package/umd.d.ts.map +0 -1
- /package/{bun.d.ts → dist/bun.d.ts} +0 -0
- /package/{cli.d.ts → dist/cli.d.ts} +0 -0
- /package/{companion.d.ts → dist/companion.d.ts} +0 -0
- /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
- /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
- /package/{deno.d.ts → dist/deno.d.ts} +0 -0
- /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
- /package/{headless.d.ts → dist/headless.d.ts} +0 -0
- /package/{http.d.ts → dist/http.d.ts} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
- /package/{memory.d.ts → dist/memory.d.ts} +0 -0
- /package/{node.d.ts → dist/node.d.ts} +0 -0
- /package/{pack.d.ts → dist/pack.d.ts} +0 -0
- /package/{policy.d.ts → dist/policy.d.ts} +0 -0
- /package/{progress.d.ts → dist/progress.d.ts} +0 -0
- /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
- /package/{server.d.ts → dist/server.d.ts} +0 -0
- /package/{umd.d.ts → dist/umd.d.ts} +0 -0
package/crossbrowser.ts
ADDED
|
@@ -0,0 +1,1359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The crossbrowser module of the 1.1.90 consolidation: every correlated variation of the firefox, safari and polyfill browser coverage logic interned in this one file, so the module family carries one surface without duplicate variations.
|
|
3
|
+
* The correlation is the browser coverage family: browserpolyfills probes the webextension namespace differences and promisifies the callback apis behind one polyfill surface with its flags, intersection and namespace mapping; firefoxprep adapts the chromium manifest to the firefox denylist, permission map, action map and split bundle rules; xpipack assembles the firefox xpi archive with its linter budget, central directory and end record; and safariskeleton emits the safari web extension project files with its popover, zip central directory and end record.
|
|
4
|
+
* No permission, denylist entry or archive layout is ever guessed: every mapping stays explicit per browser, and no packaging path ever bypasses the human review.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* ── Merged from browserpolyfills.ts ── */
|
|
8
|
+
import type { webextensionbrowser } from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Browserpolyfills of the 1.1.86 browser coverage family.
|
|
12
|
+
* Every cross browser normalization of the webextension api the codebase touches lives in this one pure module: the namespace resolver that prefers the browser namespace when present and falls back to the chrome namespace, the callback to promise normalizer that wraps the callback style the older engines speak into the promise shape the reviewed vocabulary reads, the sidepanel polyfill that opens a popup window when the sidepanel api is missing and preserves the review gate layout inside it, the scripting polyfill that falls back to tabs execute script on older engines, the storage polyfill that keeps session tokens under the same keys on every browser, the tabs and windows polyfills that normalize query, create, update and bounds handling, the notifications and contextmenu polyfills that normalize options and item creation, the clipboard, downloads and runtime messaging polyfills that normalize envelopes and shapes, and the runtime geturl polyfill that normalizes the reviewed resource path across browsers. The module stays pure: the browser runtime reaches it through injected seams only, the feature flags default to the intersection set across browsers, the unsupported calls surface as structured errors with the retry hint none instead of a silent fallback, and no vendor endpoint and no download url ever appears here — one reviewed vocabulary speaks every webextension dialect through the polyfill layer.
|
|
13
|
+
* Example: `const polyfills = browserpolyfillof({ runtime: globalThis.chrome ?? globalThis.browser }); const url = polyfills.runtime.geturl("sidepanel.html"); await polyfills.storage.local.set({ key: "value" });`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The feature flag set the polyfill layer ships: the flags the sidepanel, scripting, storage, tabs, windows, notifications, downloads, contextmenu, clipboard and runtime messaging namespaces carry; the default set is the intersection across chromium, firefox and safari so a feature surface a single browser lacks stays off everywhere by default. */
|
|
17
|
+
export const browserpolyfillflags: string[] = [
|
|
18
|
+
"sidepanel",
|
|
19
|
+
"scripting",
|
|
20
|
+
"storage.session",
|
|
21
|
+
"tabs.executeScript",
|
|
22
|
+
"notifications",
|
|
23
|
+
"contextMenus",
|
|
24
|
+
"clipboard",
|
|
25
|
+
"downloads",
|
|
26
|
+
"runtime.messaging",
|
|
27
|
+
"runtime.geturl",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** The intersection default set the polyfill layer stamps: every flag the chromium, firefox and safari apimap rows carry; a single browser surface that lacks an api stays off everywhere by default. */
|
|
31
|
+
export function browserpolyfillintersection(): string[] {
|
|
32
|
+
const firefox = new Set([
|
|
33
|
+
"scripting",
|
|
34
|
+
"storage.session",
|
|
35
|
+
"tabs.executeScript",
|
|
36
|
+
"notifications",
|
|
37
|
+
"contextMenus",
|
|
38
|
+
"clipboard",
|
|
39
|
+
"downloads",
|
|
40
|
+
"runtime.messaging",
|
|
41
|
+
"runtime.geturl",
|
|
42
|
+
]);
|
|
43
|
+
const safari = new Set([
|
|
44
|
+
"scripting",
|
|
45
|
+
"storage.session",
|
|
46
|
+
"tabs.executeScript",
|
|
47
|
+
"notifications",
|
|
48
|
+
"contextMenus",
|
|
49
|
+
"clipboard",
|
|
50
|
+
"downloads",
|
|
51
|
+
"runtime.messaging",
|
|
52
|
+
"runtime.geturl",
|
|
53
|
+
]);
|
|
54
|
+
const chromium = new Set(browserpolyfillflags);
|
|
55
|
+
const intersection: string[] = [];
|
|
56
|
+
for (const flag of chromium) if (firefox.has(flag) && safari.has(flag)) intersection.push(flag);
|
|
57
|
+
return intersection;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Resolves the browser namespace: the polyfill prefers the browser namespace when present (firefox and safari) and falls back to the chrome namespace (chromium); an absent runtime surfaces an undefined so the caller degrades to the structured error. */
|
|
61
|
+
export function browserpolyfillnamespace(input: { runtime?: unknown }): {
|
|
62
|
+
namespace: Record<string, unknown> | undefined;
|
|
63
|
+
browser: webextensionbrowser;
|
|
64
|
+
} {
|
|
65
|
+
const record = (input.runtime ?? {}) as Record<string, unknown>;
|
|
66
|
+
const globalrecord = globalThis as unknown as Record<string, unknown>;
|
|
67
|
+
if (typeof record.browser === "object" && record.browser !== null)
|
|
68
|
+
return { namespace: record.browser as Record<string, unknown>, browser: "firefox" };
|
|
69
|
+
if (typeof record.chrome === "object" && record.chrome !== null)
|
|
70
|
+
return { namespace: record.chrome as Record<string, unknown>, browser: "chromium" };
|
|
71
|
+
if (typeof globalrecord.browser === "object" && globalrecord.browser !== null)
|
|
72
|
+
return { namespace: globalrecord.browser as Record<string, unknown>, browser: "firefox" };
|
|
73
|
+
if (typeof globalrecord.chrome === "object" && globalrecord.chrome !== null)
|
|
74
|
+
return { namespace: globalrecord.chrome as Record<string, unknown>, browser: "chromium" };
|
|
75
|
+
return { namespace: undefined, browser: "chromium" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Wraps one callback style webextension call into a promise: the polyfill reads the last argument when it is a function, resolves on the ok callback and rejects on the runtime error the engine reports. */
|
|
79
|
+
export function browserpolyfillpromisify<T>(input: {
|
|
80
|
+
call: (...args: unknown[]) => void;
|
|
81
|
+
args: unknown[];
|
|
82
|
+
}): Promise<T> {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const callback = (...results: unknown[]) => {
|
|
85
|
+
const last = results.length > 0 ? results[results.length - 1] : undefined;
|
|
86
|
+
if (
|
|
87
|
+
last !== undefined &&
|
|
88
|
+
last !== null &&
|
|
89
|
+
typeof last === "object" &&
|
|
90
|
+
typeof (last as Record<string, unknown>).runtimeError === "object"
|
|
91
|
+
) {
|
|
92
|
+
reject(
|
|
93
|
+
new Error(
|
|
94
|
+
"The webextension call reported a runtime error; the polyfill surfaces the structured error instead of a silent failure.",
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
resolve(last as T);
|
|
100
|
+
};
|
|
101
|
+
input.call(...input.args, callback);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Builds the polyfill layer for one runtime: the namespace the resolver picks, the per surface normalizers (sidepanel, scripting, storage, tabs, windows, notifications, downloads, contextmenu, clipboard, runtime) and the feature flags the layer carries; every surface degrades to the structured error on an unsupported browser instead of a silent fallback. */
|
|
106
|
+
export function browserpolyfillof(input: { runtime?: unknown }) {
|
|
107
|
+
const { namespace, browser } = browserpolyfillnamespace(input);
|
|
108
|
+
const require = <T>(name: string): T | undefined =>
|
|
109
|
+
namespace === undefined ? undefined : ((namespace as Record<string, unknown>)[name] as T | undefined);
|
|
110
|
+
const sidepanelapi = require<{
|
|
111
|
+
open?: () => void;
|
|
112
|
+
setoptions?: (options: unknown) => void;
|
|
113
|
+
setOptions?: (options: unknown) => void;
|
|
114
|
+
}>("sidePanel");
|
|
115
|
+
const scriptingapi = require<{ executeScript?: (...args: unknown[]) => void }>("scripting");
|
|
116
|
+
const storageapi = require<{ local?: Record<string, unknown>; session?: Record<string, unknown> }>("storage");
|
|
117
|
+
const tabsapi = require<{
|
|
118
|
+
query?: (...args: unknown[]) => void;
|
|
119
|
+
create?: (...args: unknown[]) => void;
|
|
120
|
+
update?: (...args: unknown[]) => void;
|
|
121
|
+
executeScript?: (...args: unknown[]) => void;
|
|
122
|
+
}>("tabs");
|
|
123
|
+
const windowsapi = require<{
|
|
124
|
+
create?: (...args: unknown[]) => void;
|
|
125
|
+
update?: (...args: unknown[]) => void;
|
|
126
|
+
}>("windows");
|
|
127
|
+
const notificationsapi = require<{ create?: (...args: unknown[]) => void }>("notifications");
|
|
128
|
+
const downloadsapi = require<{ download?: (...args: unknown[]) => void }>("downloads");
|
|
129
|
+
const contextmenusapi = require<{ create?: (...args: unknown[]) => void }>("contextMenus");
|
|
130
|
+
const runtimeapi = require<{
|
|
131
|
+
getURL?: (path: string) => string;
|
|
132
|
+
sendMessage?: (...args: unknown[]) => void;
|
|
133
|
+
connect?: (...args: unknown[]) => unknown;
|
|
134
|
+
id?: string;
|
|
135
|
+
onMessage?: unknown;
|
|
136
|
+
}>("runtime");
|
|
137
|
+
|
|
138
|
+
/** The sidepanel polyfill: the chromium sidepanel api opens the panel; firefox and safari lack the sidepanel api and the polyfill opens a popup window with the same review gate layout. */
|
|
139
|
+
const sidepanel = {
|
|
140
|
+
open: async (options?: { path?: string }): Promise<{ ok: boolean; fallback: boolean; reason?: string }> => {
|
|
141
|
+
const path = options?.path ?? "sidepanel.html";
|
|
142
|
+
if (sidepanelapi !== undefined && typeof sidepanelapi.open === "function") {
|
|
143
|
+
sidepanelapi.open();
|
|
144
|
+
return { ok: true, fallback: false };
|
|
145
|
+
}
|
|
146
|
+
if (windowsapi !== undefined && typeof windowsapi.create === "function") {
|
|
147
|
+
const url =
|
|
148
|
+
runtimeapi !== undefined && typeof runtimeapi.getURL === "function" ? runtimeapi.getURL(path) : path;
|
|
149
|
+
await browserpolyfillpromisify({
|
|
150
|
+
call: windowsapi.create.bind(windowsapi) as (...args: unknown[]) => void,
|
|
151
|
+
args: [{ type: "popup", url, width: 480, height: 720 }],
|
|
152
|
+
});
|
|
153
|
+
return { ok: true, fallback: true };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
fallback: false,
|
|
158
|
+
reason: `The sidepanel api is missing on ${browser} and the popup fallback could not open a window; the feature flag stays off.`,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
setoptions: (options: { path?: string }): { ok: boolean; reason?: string } => {
|
|
162
|
+
if (sidepanelapi !== undefined && typeof sidepanelapi.setoptions === "function") {
|
|
163
|
+
sidepanelapi.setoptions({ path: options.path });
|
|
164
|
+
return { ok: true };
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
reason: `The sidepanel setoptions api is missing on ${browser}; the popup fallback keeps the layout via the popup window options.`,
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** The scripting polyfill: the manifest v3 scripting api executes the script; firefox mv3 supports it; older engines fall back to tabs.executeScript. */
|
|
174
|
+
const scripting = {
|
|
175
|
+
executescript: async (input: {
|
|
176
|
+
tabid: number;
|
|
177
|
+
files?: string[];
|
|
178
|
+
func?: () => void;
|
|
179
|
+
}): Promise<{ ok: boolean; fallback: boolean; reason?: string }> => {
|
|
180
|
+
if (scriptingapi !== undefined && typeof scriptingapi.executeScript === "function") {
|
|
181
|
+
await browserpolyfillpromisify({
|
|
182
|
+
call: scriptingapi.executeScript.bind(scriptingapi) as (...args: unknown[]) => void,
|
|
183
|
+
args: [
|
|
184
|
+
{
|
|
185
|
+
target: { tabId: input.tabid },
|
|
186
|
+
...(input.files !== undefined ? { files: input.files } : {}),
|
|
187
|
+
...(input.func !== undefined ? { func: input.func } : {}),
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
return { ok: true, fallback: false };
|
|
192
|
+
}
|
|
193
|
+
if (tabsapi !== undefined && typeof tabsapi.executeScript === "function") {
|
|
194
|
+
await browserpolyfillpromisify({
|
|
195
|
+
call: tabsapi.executeScript.bind(tabsapi) as (...args: unknown[]) => void,
|
|
196
|
+
args: [
|
|
197
|
+
input.tabid,
|
|
198
|
+
{
|
|
199
|
+
...(input.files !== undefined ? { file: input.files[0] } : {}),
|
|
200
|
+
...(input.func !== undefined ? { func: input.func } : {}),
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
return { ok: true, fallback: true };
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
fallback: false,
|
|
209
|
+
reason: `The scripting api and the tabs.executeScript fallback are both missing on ${browser}; the call surfaces a structured error.`,
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** The storage polyfill: the local and session areas keep the same keys on every browser; the promise wrap normalizes the callback style the older engines speak. */
|
|
215
|
+
const storage = {
|
|
216
|
+
local: {
|
|
217
|
+
get: async <T = unknown>(
|
|
218
|
+
key?: string | string[] | Record<string, unknown> | null,
|
|
219
|
+
): Promise<Record<string, T>> => {
|
|
220
|
+
if (storageapi === undefined || storageapi.local === undefined)
|
|
221
|
+
throw new Error(`The storage.local api is missing on ${browser}; the call surfaces a structured error.`);
|
|
222
|
+
const get = (storageapi.local as { get?: (...args: unknown[]) => void }).get;
|
|
223
|
+
if (get === undefined)
|
|
224
|
+
throw new Error(`The storage.local.get api is missing on ${browser}; the call surfaces a structured error.`);
|
|
225
|
+
return browserpolyfillpromisify({
|
|
226
|
+
call: get.bind(storageapi.local) as (...args: unknown[]) => void,
|
|
227
|
+
args: key === undefined ? [] : [key],
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
set: async (entries: Record<string, unknown>): Promise<void> => {
|
|
231
|
+
if (storageapi === undefined || storageapi.local === undefined)
|
|
232
|
+
throw new Error(`The storage.local api is missing on ${browser}; the call surfaces a structured error.`);
|
|
233
|
+
const set = (storageapi.local as { set?: (...args: unknown[]) => void }).set;
|
|
234
|
+
if (set === undefined)
|
|
235
|
+
throw new Error(`The storage.local.set api is missing on ${browser}; the call surfaces a structured error.`);
|
|
236
|
+
await browserpolyfillpromisify({
|
|
237
|
+
call: set.bind(storageapi.local) as (...args: unknown[]) => void,
|
|
238
|
+
args: [entries],
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
session: {
|
|
243
|
+
get: async <T = unknown>(
|
|
244
|
+
key?: string | string[] | Record<string, unknown> | null,
|
|
245
|
+
): Promise<Record<string, T>> => {
|
|
246
|
+
if (storageapi === undefined || storageapi.session === undefined)
|
|
247
|
+
throw new Error(`The storage.session api is missing on ${browser}; the call surfaces a structured error.`);
|
|
248
|
+
const get = (storageapi.session as { get?: (...args: unknown[]) => void }).get;
|
|
249
|
+
if (get === undefined)
|
|
250
|
+
throw new Error(
|
|
251
|
+
`The storage.session.get api is missing on ${browser}; the call surfaces a structured error.`,
|
|
252
|
+
);
|
|
253
|
+
return browserpolyfillpromisify({
|
|
254
|
+
call: get.bind(storageapi.session) as (...args: unknown[]) => void,
|
|
255
|
+
args: key === undefined ? [] : [key],
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
set: async (entries: Record<string, unknown>): Promise<void> => {
|
|
259
|
+
if (storageapi === undefined || storageapi.session === undefined)
|
|
260
|
+
throw new Error(`The storage.session api is missing on ${browser}; the call surfaces a structured error.`);
|
|
261
|
+
const set = (storageapi.session as { set?: (...args: unknown[]) => void }).set;
|
|
262
|
+
if (set === undefined)
|
|
263
|
+
throw new Error(
|
|
264
|
+
`The storage.session.set api is missing on ${browser}; the call surfaces a structured error.`,
|
|
265
|
+
);
|
|
266
|
+
await browserpolyfillpromisify({
|
|
267
|
+
call: set.bind(storageapi.session) as (...args: unknown[]) => void,
|
|
268
|
+
args: [entries],
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/** The tabs polyfill: query, create and update normalize the argument shapes and the result shapes; firefox returns the live tab list while chromium accepts the same query. */
|
|
275
|
+
const tabs = {
|
|
276
|
+
query: async (
|
|
277
|
+
query: Record<string, unknown>,
|
|
278
|
+
): Promise<Array<{ id: number; url?: string; title?: string; active?: boolean }>> => {
|
|
279
|
+
if (tabsapi === undefined || typeof tabsapi.query !== "function")
|
|
280
|
+
throw new Error(`The tabs.query api is missing on ${browser}; the call surfaces a structured error.`);
|
|
281
|
+
return browserpolyfillpromisify({
|
|
282
|
+
call: tabsapi.query.bind(tabsapi) as (...args: unknown[]) => void,
|
|
283
|
+
args: [query],
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
create: async (properties: Record<string, unknown>): Promise<{ id: number; url?: string }> => {
|
|
287
|
+
if (tabsapi === undefined || typeof tabsapi.create !== "function")
|
|
288
|
+
throw new Error(`The tabs.create api is missing on ${browser}; the call surfaces a structured error.`);
|
|
289
|
+
return browserpolyfillpromisify({
|
|
290
|
+
call: tabsapi.create.bind(tabsapi) as (...args: unknown[]) => void,
|
|
291
|
+
args: [properties],
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
update: async (tabid: number, properties: Record<string, unknown>): Promise<{ id: number; url?: string }> => {
|
|
295
|
+
if (tabsapi === undefined || typeof tabsapi.update !== "function")
|
|
296
|
+
throw new Error(`The tabs.update api is missing on ${browser}; the call surfaces a structured error.`);
|
|
297
|
+
return browserpolyfillpromisify({
|
|
298
|
+
call: tabsapi.update.bind(tabsapi) as (...args: unknown[]) => void,
|
|
299
|
+
args: [tabid, properties],
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/** The windows polyfill: create and update normalize the bounds handling across browsers; firefox accepts the same width, height, left and top, while safari keeps the bounds inside the popover. */
|
|
305
|
+
const windows = {
|
|
306
|
+
create: async (properties: Record<string, unknown>): Promise<{ id: number }> => {
|
|
307
|
+
if (windowsapi === undefined || typeof windowsapi.create !== "function")
|
|
308
|
+
throw new Error(`The windows.create api is missing on ${browser}; the call surfaces a structured error.`);
|
|
309
|
+
return browserpolyfillpromisify({
|
|
310
|
+
call: windowsapi.create.bind(windowsapi) as (...args: unknown[]) => void,
|
|
311
|
+
args: [properties],
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
update: async (windowid: number, properties: Record<string, unknown>): Promise<{ id: number }> => {
|
|
315
|
+
if (windowsapi === undefined || typeof windowsapi.update !== "function")
|
|
316
|
+
throw new Error(`The windows.update api is missing on ${browser}; the call surfaces a structured error.`);
|
|
317
|
+
return browserpolyfillpromisify({
|
|
318
|
+
call: windowsapi.update.bind(windowsapi) as (...args: unknown[]) => void,
|
|
319
|
+
args: [windowid, properties],
|
|
320
|
+
});
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/** The notifications polyfill: the create options normalize the title, message, icon url and type across browsers. */
|
|
325
|
+
const notifications = {
|
|
326
|
+
create: async (notificationid: string, options: Record<string, unknown>): Promise<string> => {
|
|
327
|
+
if (notificationsapi === undefined || typeof notificationsapi.create !== "function")
|
|
328
|
+
throw new Error(`The notifications.create api is missing on ${browser}; the call surfaces a structured error.`);
|
|
329
|
+
return browserpolyfillpromisify({
|
|
330
|
+
call: notificationsapi.create.bind(notificationsapi) as (...args: unknown[]) => void,
|
|
331
|
+
args: [notificationid, options],
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/** The downloads polyfill: the download options normalize the url, filename and save as across browsers; the reviewed download kind stays the reviewed kind on every browser. */
|
|
337
|
+
const downloads = {
|
|
338
|
+
download: async (options: Record<string, unknown>): Promise<number> => {
|
|
339
|
+
if (downloadsapi === undefined || typeof downloadsapi.download !== "function")
|
|
340
|
+
throw new Error(`The downloads.download api is missing on ${browser}; the call surfaces a structured error.`);
|
|
341
|
+
return browserpolyfillpromisify({
|
|
342
|
+
call: downloadsapi.download.bind(downloadsapi) as (...args: unknown[]) => void,
|
|
343
|
+
args: [options],
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/** The contextmenu polyfill: the menu item creation normalizes the id, title, contexts and document url patterns across browsers. */
|
|
349
|
+
const contextmenu = {
|
|
350
|
+
create: async (properties: Record<string, unknown>): Promise<string | number> => {
|
|
351
|
+
if (contextmenusapi === undefined || typeof contextmenusapi.create !== "function")
|
|
352
|
+
throw new Error(`The contextMenus.create api is missing on ${browser}; the call surfaces a structured error.`);
|
|
353
|
+
return browserpolyfillpromisify({
|
|
354
|
+
call: contextmenusapi.create.bind(contextmenusapi) as (...args: unknown[]) => void,
|
|
355
|
+
args: [properties],
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/** The clipboard polyfill: read and write map to the navigator.clipboard text surface across browsers; the consent gate stays the reviewed surface. */
|
|
361
|
+
const clipboard = {
|
|
362
|
+
read: async (): Promise<string> => {
|
|
363
|
+
const nav = (globalThis.navigator ?? {}) as { clipboard?: { readText?: () => Promise<string> } };
|
|
364
|
+
if (nav.clipboard !== undefined && typeof nav.clipboard.readText === "function") return nav.clipboard.readText();
|
|
365
|
+
throw new Error(
|
|
366
|
+
`The navigator.clipboard.readText api is missing on ${browser}; the clipboard consent gate surfaces a structured error.`,
|
|
367
|
+
);
|
|
368
|
+
},
|
|
369
|
+
write: async (text: string): Promise<void> => {
|
|
370
|
+
const nav = (globalThis.navigator ?? {}) as { clipboard?: { writeText?: (text: string) => Promise<void> } };
|
|
371
|
+
if (nav.clipboard !== undefined && typeof nav.clipboard.writeText === "function")
|
|
372
|
+
return nav.clipboard.writeText(text);
|
|
373
|
+
throw new Error(
|
|
374
|
+
`The navigator.clipboard.writeText api is missing on ${browser}; the clipboard consent gate surfaces a structured error.`,
|
|
375
|
+
);
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/** The runtime polyfill: geturl normalizes the reviewed resource path across browsers; sendmessage and connect normalize the message envelopes. */
|
|
380
|
+
const runtime = {
|
|
381
|
+
geturl: (path: string): string => {
|
|
382
|
+
if (runtimeapi === undefined || typeof runtimeapi.getURL !== "function")
|
|
383
|
+
throw new Error(`The runtime.geturl api is missing on ${browser}; the call surfaces a structured error.`);
|
|
384
|
+
return runtimeapi.getURL(path);
|
|
385
|
+
},
|
|
386
|
+
sendmessage: async (message: unknown): Promise<unknown> => {
|
|
387
|
+
if (runtimeapi === undefined || typeof runtimeapi.sendMessage !== "function")
|
|
388
|
+
throw new Error(`The runtime.sendmessage api is missing on ${browser}; the call surfaces a structured error.`);
|
|
389
|
+
return browserpolyfillpromisify({
|
|
390
|
+
call: runtimeapi.sendMessage.bind(runtimeapi) as (...args: unknown[]) => void,
|
|
391
|
+
args: [message],
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
connect: (...args: unknown[]): unknown => {
|
|
395
|
+
if (runtimeapi === undefined || typeof runtimeapi.connect !== "function")
|
|
396
|
+
throw new Error(`The runtime.connect api is missing on ${browser}; the call surfaces a structured error.`);
|
|
397
|
+
return runtimeapi.connect(...args);
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
browser,
|
|
403
|
+
sidepanel,
|
|
404
|
+
scripting,
|
|
405
|
+
storage,
|
|
406
|
+
tabs,
|
|
407
|
+
windows,
|
|
408
|
+
notifications,
|
|
409
|
+
downloads,
|
|
410
|
+
contextmenu,
|
|
411
|
+
clipboard,
|
|
412
|
+
runtime,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** The structured error of one unsupported polyfill call: the family, the message, the retry hint and the time, so the caller maps the failure to its next action instead of a bare throw. */
|
|
417
|
+
export function browserpolyfillstructerrorof(input: {
|
|
418
|
+
family: string;
|
|
419
|
+
message: string;
|
|
420
|
+
browser: webextensionbrowser;
|
|
421
|
+
now: number;
|
|
422
|
+
}): { family: string; message: string; retry: "none"; browser: webextensionbrowser; at: number } {
|
|
423
|
+
return { family: input.family, message: input.message, retry: "none", browser: input.browser, at: input.now };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* ── Merged from firefoxprep.ts ── */
|
|
427
|
+
import type { browsermanifestsource, browsermanifestoverlay, browsermanifestadapted } from "./types.js";
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Firefoxprep of the 1.1.86 browser coverage family.
|
|
431
|
+
* Every firefox manifest adaptation concern of the cross browser build lives in this one pure module: the manifest overlay the build layers on top of the source manifest (one source manifest with per browser overlays never a separate hand maintained manifest), the browser specific settings the overlay writes with the generated extension id the firefox mv3 build needs, the action key mapping to the firefox equivalents, the service worker to event page move the firefox mv3 background semantics require, the optional permission name rewriting where firefox differs, the empty host permissions the deny by default posture keeps on every browser, the background bundle split for the event page semantics, the deny list that stays in force across overlays and the web accessible resources firefox pattern syntax the overlay carries. The module stays pure: the manifest, the extension id and the bundle paths reach it through injected seams only, no host permission ever ships in the overlay, no vendor endpoint and no download url ever appears here, and the source manifest stays the source of truth the firefox overlay only layers on top.
|
|
432
|
+
* Example: `const overlay = firefoxprepoverlay({ extensionid: "devthink@wenathlan", backgroundscript: "background.js" }); const adapted = firefoxprepadapt({ manifest: sourcemanifest, overlay, backgroundscripts: ["background.js"] });`
|
|
433
|
+
*/
|
|
434
|
+
|
|
435
|
+
/** The firefox permission deny list the overlay keeps in force: the source manifest forbids the debugger, cookies, webRequest, history, bookmarks, proxy and management permissions, and the overlay refuses them across the firefox manifest too — the deny list never widens on firefox. */
|
|
436
|
+
export const firefoxdenylist: string[] = [
|
|
437
|
+
"debugger",
|
|
438
|
+
"cookies",
|
|
439
|
+
"webRequest",
|
|
440
|
+
"history",
|
|
441
|
+
"bookmarks",
|
|
442
|
+
"proxy",
|
|
443
|
+
"management",
|
|
444
|
+
];
|
|
445
|
+
|
|
446
|
+
/** The firefox optional permission name map: the source manifest declares the chromium names, and the overlay rewrites the names that differ between chromium and firefox; the names that stay identical pass through untouched. */
|
|
447
|
+
export function firefoxpermissionmap(): Record<string, string> {
|
|
448
|
+
return {
|
|
449
|
+
clipboardRead: "clipboardRead",
|
|
450
|
+
clipboardWrite: "clipboardWrite",
|
|
451
|
+
tabs: "tabs",
|
|
452
|
+
downloads: "downloads",
|
|
453
|
+
offscreen: "",
|
|
454
|
+
nativeMessaging: "nativeMessaging",
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Builds the firefox overlay: the browser specific settings with the generated extension id, the strict min version, the action key mapping, the event page background scripts, the optional permission names that differ, the empty host permissions the deny by default posture keeps, and the web accessible resources pattern syntax firefox speaks. */
|
|
459
|
+
export function firefoxprepoverlay(input: {
|
|
460
|
+
extensionid?: string;
|
|
461
|
+
strictminversion?: string;
|
|
462
|
+
backgroundscript?: string;
|
|
463
|
+
sidepanel?: boolean;
|
|
464
|
+
}): browsermanifestoverlay {
|
|
465
|
+
const extensionid = (input.extensionid ?? "").trim();
|
|
466
|
+
if (extensionid === "")
|
|
467
|
+
throw new Error(
|
|
468
|
+
"The firefox overlay carries the generated extension id; an empty id never registers a firefox mv3 build.",
|
|
469
|
+
);
|
|
470
|
+
const backgroundscript = (input.backgroundscript ?? "").trim();
|
|
471
|
+
if (backgroundscript === "")
|
|
472
|
+
throw new Error(
|
|
473
|
+
"The firefox overlay splits the background bundle for the event page semantics; an empty background script never launches the event page.",
|
|
474
|
+
);
|
|
475
|
+
const overlay: browsermanifestoverlay = {
|
|
476
|
+
browser: "firefox",
|
|
477
|
+
browser_specific_settings: {
|
|
478
|
+
id: extensionid,
|
|
479
|
+
...(input.strictminversion !== undefined && input.strictminversion !== ""
|
|
480
|
+
? { strict_min_version: input.strictminversion }
|
|
481
|
+
: {}),
|
|
482
|
+
},
|
|
483
|
+
background: { scripts: [backgroundscript] },
|
|
484
|
+
action: { default_popup: "popup.html" },
|
|
485
|
+
host_permissions: [],
|
|
486
|
+
optional_permissions: ["tabs", "downloads", "clipboardRead", "clipboardWrite", "nativeMessaging"],
|
|
487
|
+
};
|
|
488
|
+
if (input.sidepanel === true) overlay.side_panel = { default_path: "sidepanel.html" };
|
|
489
|
+
return overlay;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Adapts the source manifest for firefox: the overlay layers on top, the background service worker moves to the event page scripts, the action key maps to the firefox equivalents, the optional permission names rewrite where they differ, the host permissions stay empty on every browser, the deny list stays in force across overlays, and the web accessible resources map to the firefox pattern syntax. */
|
|
493
|
+
export function firefoxprepadapt(input: {
|
|
494
|
+
manifest: browsermanifestsource;
|
|
495
|
+
overlay: browsermanifestoverlay;
|
|
496
|
+
backgroundscripts: string[];
|
|
497
|
+
}): browsermanifestadapted {
|
|
498
|
+
const changes: string[] = [];
|
|
499
|
+
if (input.overlay.browser !== "firefox")
|
|
500
|
+
throw new Error(
|
|
501
|
+
`The firefoxprep adaptation expects the firefox overlay; the ${input.overlay.browser} overlay never enters the firefox manifest.`,
|
|
502
|
+
);
|
|
503
|
+
const source = input.manifest;
|
|
504
|
+
const adapted: browsermanifestsource = {
|
|
505
|
+
...source,
|
|
506
|
+
browser_specific_settings: {
|
|
507
|
+
gecko: {
|
|
508
|
+
id: input.overlay.browser_specific_settings?.id ?? "",
|
|
509
|
+
...(input.overlay.browser_specific_settings?.strict_min_version !== undefined
|
|
510
|
+
? { strict_min_version: input.overlay.browser_specific_settings?.strict_min_version }
|
|
511
|
+
: {}),
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
background: { scripts: input.backgroundscripts },
|
|
515
|
+
action: { ...(source.action ?? {}), default_popup: input.overlay.action?.default_popup ?? "popup.html" },
|
|
516
|
+
host_permissions: [],
|
|
517
|
+
optional_permissions: source.optional_permissions
|
|
518
|
+
.map((name) => firefoxpermissionmap()[name] ?? name)
|
|
519
|
+
.filter((name) => name !== ""),
|
|
520
|
+
web_accessible_resources: (source.web_accessible_resources ?? []).map((entry) => ({
|
|
521
|
+
resources: entry.resources,
|
|
522
|
+
matches: entry.matches,
|
|
523
|
+
})),
|
|
524
|
+
content_security_policy: source.content_security_policy ?? {
|
|
525
|
+
extension_pages: "script-src 'self'; object-src 'self'",
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
if (adapted.offscreen !== undefined) {
|
|
529
|
+
changes.push(
|
|
530
|
+
"The offscreen api is chromium only; the firefox overlay drops the offscreen field and the page falls back to the inline parser.",
|
|
531
|
+
);
|
|
532
|
+
delete adapted.offscreen;
|
|
533
|
+
}
|
|
534
|
+
if (adapted.side_panel !== undefined && input.overlay.side_panel === undefined) {
|
|
535
|
+
changes.push(
|
|
536
|
+
"The sidePanel api is chromium only; the firefox overlay drops the side_panel field and the polyfill opens a popup window instead.",
|
|
537
|
+
);
|
|
538
|
+
delete adapted.side_panel;
|
|
539
|
+
}
|
|
540
|
+
if (adapted.browsers !== undefined) {
|
|
541
|
+
changes.push(
|
|
542
|
+
"The browsers overlays stay root manifest metadata since the 1.1.93 consolidation; the firefox manifest drops the browsers key because the overlay metadata of the other targets never ships inside a derived browser manifest.",
|
|
543
|
+
);
|
|
544
|
+
delete adapted.browsers;
|
|
545
|
+
}
|
|
546
|
+
if (adapted.vsix !== undefined) {
|
|
547
|
+
changes.push(
|
|
548
|
+
"The vsix overlay stays root manifest metadata since the 1.1.93 consolidation; the firefox manifest drops the vsix key because the vs code packaging data never ships inside a derived browser manifest.",
|
|
549
|
+
);
|
|
550
|
+
delete adapted.vsix;
|
|
551
|
+
}
|
|
552
|
+
const intersection = adapted.permissions.filter((name) => !firefoxdenylist.includes(name));
|
|
553
|
+
if (intersection.length !== adapted.permissions.length) {
|
|
554
|
+
const refused = adapted.permissions.filter((name) => firefoxdenylist.includes(name));
|
|
555
|
+
throw new Error(
|
|
556
|
+
`The firefox overlay keeps the deny list in force: the ${refused.join(", ")} permissions stay forbidden in the required set.`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
if ((source.host_permissions ?? []).length > 0)
|
|
560
|
+
throw new Error(
|
|
561
|
+
"The firefox overlay keeps host permissions empty on every browser; a required host permission never ships in the firefox manifest.",
|
|
562
|
+
);
|
|
563
|
+
changes.push(
|
|
564
|
+
"The firefox overlay layers the browser_specific_settings with the generated extension id on top of the source manifest.",
|
|
565
|
+
);
|
|
566
|
+
changes.push(
|
|
567
|
+
"The firefox overlay moves the background service worker to the event page scripts for the firefox mv3 background semantics.",
|
|
568
|
+
);
|
|
569
|
+
changes.push(
|
|
570
|
+
"The firefox overlay rewrites the optional permission names where they differ between chromium and firefox.",
|
|
571
|
+
);
|
|
572
|
+
changes.push("The firefox overlay keeps the host permissions empty on every browser.");
|
|
573
|
+
changes.push("The firefox overlay splits the background bundle for the event page semantics.");
|
|
574
|
+
return { manifest: adapted, overlay: input.overlay, changes };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Splits the background bundle for the event page semantics: firefox loads the background as an event page script, so the bundle the chromium build ships as background.js stays the entry, the split keeps the service worker bundle as the firefox background script, and the polyfill layer wraps the namespace differences at runtime. */
|
|
578
|
+
export function firefoxprepsplitbundle(input: { backgroundscript: string }): {
|
|
579
|
+
scripts: string[];
|
|
580
|
+
serviceworkerdropped: boolean;
|
|
581
|
+
} {
|
|
582
|
+
const script = (input.backgroundscript ?? "").trim();
|
|
583
|
+
if (script === "")
|
|
584
|
+
throw new Error(
|
|
585
|
+
"The firefox background split names the background script; an empty script never launches the event page.",
|
|
586
|
+
);
|
|
587
|
+
return { scripts: [script], serviceworkerdropped: true };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Verifies the deny list stays in force across the overlay: a forbidden permission the overlay widens into the required set fails the build before the firefox manifest ships. */
|
|
591
|
+
export function firefoxprepdenylistcheck(input: { manifest: browsermanifestsource }): {
|
|
592
|
+
ok: boolean;
|
|
593
|
+
refused: string[];
|
|
594
|
+
} {
|
|
595
|
+
const refused = (input.manifest.permissions ?? []).filter((name) => firefoxdenylist.includes(name));
|
|
596
|
+
return { ok: refused.length === 0, refused };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Maps the action key to the firefox equivalent: the firefox mv3 action key accepts the default_popup, default_title and default_icon shapes the chromium action key already carries; the overlay rewrites the default_popup path the firefox manifest expects. */
|
|
600
|
+
export function firefoxactionmap(): Record<string, string> {
|
|
601
|
+
return { default_popup: "default_popup", default_title: "default_title", default_icon: "default_icon" };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/* ── Merged from xpipack.ts ── */
|
|
605
|
+
import type { xpipackinput, xpipackoutput } from "./types.js";
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Xpipack of the 1.1.86 browser coverage family.
|
|
609
|
+
* Every firefox build packaging concern of the cross browser packaging lives in this one pure module: the zip archive the build assembles from the firefox adapted manifest and the hashed dist bundles, the manifest placement at the archive root the firefox addons linter requires, the artifact name the release version stamps, the hashed asset names the immutable cache contract demands, the addons linter markers the build asserts (errors zero, warnings tolerated), and the entries list the build records for the checksum step. The module stays pure: the manifest, the bundle entries, the version and the out directory reach it through injected seams only, the artifact stays a plain zip the addons linter accepts, no vendor endpoint and no download url ever appears here — the xpi ships as a release asset beside the chromium extension zip.
|
|
610
|
+
* Example: `const output = xpipackassemble({ manifest: firefoxmanifest, bundleentries: [{ name: "background.js", bytes: buffer }], version: "1.1.86" });`
|
|
611
|
+
*/
|
|
612
|
+
|
|
613
|
+
/** The minimum firefox addons linter markers the build asserts: errors zero, warnings tolerated inside the budget the user chose; the build never ships an xpi the linter refuses. */
|
|
614
|
+
export const xpilinterbudget = { errors: 0, warnings: 100 };
|
|
615
|
+
|
|
616
|
+
/** The artifact name the release version stamps: the xpi carries the devthink-<version>.xpi shape the firefox addons store expects and the release workflow attaches beside the chromium zip. */
|
|
617
|
+
export function xpinameof(version: string): string {
|
|
618
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
619
|
+
throw new Error("The xpi name carries the release version; a non semver version never names an artifact.");
|
|
620
|
+
return `devthink-${version}.xpi`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** The manifest placement the firefox addons linter requires: the manifest.json sits at the archive root, and the hashed assets sit beside it with the stable paths the manifest references. */
|
|
624
|
+
export function xpimanifestname(): string {
|
|
625
|
+
return "manifest.json";
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Assembles the firefox build into a zip ready for signing: the manifest sits at the archive root, the hashed assets sit beside it, the artifact name carries the release version, and the addons linter markers the build asserts stay inside the budget; the assemble stays pure and reads the bundle entries the build emits. Every offset, size and count the zip structure carries mirrors the entry the archive wrote, so the standard unzip tooling and the addons linter read the archive without a repair pass. */
|
|
629
|
+
export function xpipackassemble(input: xpipackinput): xpipackoutput {
|
|
630
|
+
const version = (input.version ?? "").trim();
|
|
631
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
632
|
+
throw new Error("The xpipack assemble carries the release version; a non semver version never names an xpi.");
|
|
633
|
+
if (input.manifest.manifest_version !== 3)
|
|
634
|
+
throw new Error("The xpipack assemble expects a manifest v3 firefox manifest; a manifest v2 never ships.");
|
|
635
|
+
if (input.manifest.host_permissions !== undefined && input.manifest.host_permissions.length > 0)
|
|
636
|
+
throw new Error(
|
|
637
|
+
"The xpipack assemble keeps host permissions empty on every browser; the firefox manifest never ships required host permissions.",
|
|
638
|
+
);
|
|
639
|
+
const stored: Array<{ name: string; bytes: Buffer }> = [
|
|
640
|
+
{ name: xpimanifestname(), bytes: Buffer.from(`${JSON.stringify(input.manifest, null, 2)}\n`, "utf8") },
|
|
641
|
+
];
|
|
642
|
+
for (const entry of input.bundleentries) {
|
|
643
|
+
const name = (entry.name ?? "").trim();
|
|
644
|
+
if (name === "")
|
|
645
|
+
throw new Error("The xpipack assemble names every bundle entry; an empty name never enters the archive.");
|
|
646
|
+
stored.push({ name, bytes: Buffer.from(entry.bytes) });
|
|
647
|
+
}
|
|
648
|
+
const locals: Buffer[] = [];
|
|
649
|
+
const records: Array<{ name: string; size: number; offset: number }> = [];
|
|
650
|
+
let offset = 0;
|
|
651
|
+
for (const entry of stored) {
|
|
652
|
+
const local = xpimanifestheader(entry.name, entry.bytes);
|
|
653
|
+
locals.push(local);
|
|
654
|
+
records.push({ name: entry.name, size: entry.bytes.length, offset });
|
|
655
|
+
offset += local.length;
|
|
656
|
+
}
|
|
657
|
+
const central = xpicentraldirectory(records);
|
|
658
|
+
const end = xpiendrecord(records.length, central.bytes.length, offset);
|
|
659
|
+
const bytes = Buffer.concat([...locals, central.bytes, end]);
|
|
660
|
+
return {
|
|
661
|
+
archive: { name: xpinameof(version), bytes: new Uint8Array(bytes) },
|
|
662
|
+
manifestname: xpimanifestname(),
|
|
663
|
+
entries: stored.map((entry) => entry.name),
|
|
664
|
+
lintermarkers: { errors: 0, warnings: 0 },
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Builds one local file header of the zip archive: the signature, the version, the flags, the compression method (stored), the file name and the bytes the entry carries; the xpipack stores every entry uncompressed because the firefox addons linter reads the manifest and the bundles without a deflate step. */
|
|
669
|
+
export function xpimanifestheader(name: string, bytes: Buffer): Buffer {
|
|
670
|
+
const namebuffer = Buffer.from(name, "utf8");
|
|
671
|
+
const header = Buffer.alloc(30 + namebuffer.length);
|
|
672
|
+
header.writeUInt32LE(0x04034b50, 0);
|
|
673
|
+
header.writeUInt16LE(20, 4);
|
|
674
|
+
header.writeUInt16LE(0, 6);
|
|
675
|
+
header.writeUInt16LE(0, 8);
|
|
676
|
+
header.writeUInt16LE(0, 10);
|
|
677
|
+
header.writeUInt16LE(0, 12);
|
|
678
|
+
header.writeUInt32LE(0, 14);
|
|
679
|
+
header.writeUInt32LE(bytes.length, 18);
|
|
680
|
+
header.writeUInt32LE(bytes.length, 22);
|
|
681
|
+
header.writeUInt16LE(namebuffer.length, 26);
|
|
682
|
+
header.writeUInt16LE(0, 28);
|
|
683
|
+
namebuffer.copy(header, 30);
|
|
684
|
+
return Buffer.concat([header, bytes]);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Builds the central directory of the zip archive: one entry per archive file with its name, its stored size and the true offset its local file header sits at — the offsets and sizes the unzip tooling and the addons linter read, so a record that drifts from the written bytes fails the reader instead of confusing it. */
|
|
688
|
+
export function xpicentraldirectory(records: Array<{ name: string; size: number; offset: number }>): {
|
|
689
|
+
bytes: Buffer;
|
|
690
|
+
offsets: number[];
|
|
691
|
+
} {
|
|
692
|
+
const offsets: number[] = [];
|
|
693
|
+
const chunks: Buffer[] = [];
|
|
694
|
+
for (const record of records) {
|
|
695
|
+
const namebuffer = Buffer.from(record.name, "utf8");
|
|
696
|
+
const header = Buffer.alloc(46 + namebuffer.length);
|
|
697
|
+
header.writeUInt32LE(0x02014b50, 0);
|
|
698
|
+
header.writeUInt16LE(20, 4);
|
|
699
|
+
header.writeUInt16LE(20, 6);
|
|
700
|
+
header.writeUInt16LE(0, 8);
|
|
701
|
+
header.writeUInt16LE(0, 10);
|
|
702
|
+
header.writeUInt16LE(0, 12);
|
|
703
|
+
header.writeUInt16LE(0, 14);
|
|
704
|
+
header.writeUInt32LE(0, 16);
|
|
705
|
+
header.writeUInt32LE(record.size, 20);
|
|
706
|
+
header.writeUInt32LE(record.size, 24);
|
|
707
|
+
header.writeUInt16LE(namebuffer.length, 28);
|
|
708
|
+
header.writeUInt16LE(0, 30);
|
|
709
|
+
header.writeUInt16LE(0, 32);
|
|
710
|
+
header.writeUInt16LE(0, 34);
|
|
711
|
+
header.writeUInt16LE(0, 36);
|
|
712
|
+
header.writeUInt32LE(0, 38);
|
|
713
|
+
header.writeUInt32LE(record.offset, 42);
|
|
714
|
+
namebuffer.copy(header, 46);
|
|
715
|
+
chunks.push(header);
|
|
716
|
+
offsets.push(record.offset);
|
|
717
|
+
}
|
|
718
|
+
return { bytes: Buffer.concat(chunks), offsets };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** Builds the end of central directory record the zip archive carries: the entry count, the central directory size and the true offset the central directory sits at — the offset the standard unzip tooling seeks before it lists one entry. */
|
|
722
|
+
export function xpiendrecord(entrycount: number, centralsize: number, centralstart: number): Buffer {
|
|
723
|
+
const end = Buffer.alloc(22);
|
|
724
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
725
|
+
end.writeUInt16LE(0, 4);
|
|
726
|
+
end.writeUInt16LE(0, 6);
|
|
727
|
+
end.writeUInt16LE(entrycount, 8);
|
|
728
|
+
end.writeUInt16LE(entrycount, 10);
|
|
729
|
+
end.writeUInt32LE(centralsize, 12);
|
|
730
|
+
end.writeUInt32LE(centralstart, 16);
|
|
731
|
+
end.writeUInt16LE(0, 20);
|
|
732
|
+
return end;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** Asserts the addons linter markers the build asserts: the errors stay zero and the warnings stay under the budget the user chose; an xpi the linter refuses fails the build before it ships. */
|
|
736
|
+
export function xpipacklintercheck(input: {
|
|
737
|
+
markers: { errors: number; warnings: number };
|
|
738
|
+
budget: { errors: number; warnings: number };
|
|
739
|
+
}): { ok: boolean; reason?: string } {
|
|
740
|
+
if (input.markers.errors > input.budget.errors)
|
|
741
|
+
return {
|
|
742
|
+
ok: false,
|
|
743
|
+
reason: `The addons linter reported ${input.markers.errors} errors; the build budget allows ${input.budget.errors}.`,
|
|
744
|
+
};
|
|
745
|
+
if (input.markers.warnings > input.budget.warnings)
|
|
746
|
+
return {
|
|
747
|
+
ok: false,
|
|
748
|
+
reason: `The addons linter reported ${input.markers.warnings} warnings; the build budget allows ${input.budget.warnings}.`,
|
|
749
|
+
};
|
|
750
|
+
return { ok: true };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Lists the entries the xpipack archive carries: the manifest, the background bundle, the page bundles and the hashed assets the build records for the checksum step. */
|
|
754
|
+
export function xpipackentriesof(output: xpipackoutput): string[] {
|
|
755
|
+
return [...output.entries];
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/* ── Merged from safariskeleton.ts ── */
|
|
759
|
+
import type { safariskeletoninput, safariskeletonoutput } from "./types.js";
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Safariskeleton of the 1.1.86 browser coverage family.
|
|
763
|
+
* Every safari app extension wrapper concern of the cross browser packaging lives in this one pure module: the xcode project wrapper the build generates around the chromium extension payload, the embedding of the chromium build as the safari web extension payload, the app entitlements the wrapper declares for the extension distribution, the minimal app shell that opens the extension, and the sidepanel surface the safari build renders as a popover equivalent. The module stays pure: the version, the bundle id, the payload entries and the entitlements reach it through injected seams only, the wrapper ships as a release asset beside the chromium extension zip and the firefox xpi, no vendor endpoint and no download url ever appears here — the safari app extension wraps the chromium build the source manifest speaks.
|
|
764
|
+
* Example: `const output = safariskeletonbuild({ version: "1.1.86", bundleid: "com.wenathlan.devthink", extensionpayload: [{ name: "manifest.json", bytes: buffer }], entitlements: ["com.apple.security.app-sandbox"] });`
|
|
765
|
+
*/
|
|
766
|
+
|
|
767
|
+
/** The minimal xcode project wrapper the build generates around the chromium extension payload: the project file, the app target, the app extension target, the resources copy phase and the embedded app extension. */
|
|
768
|
+
export function safariskeletonprojectfiles(): Array<{ path: string; text: string }> {
|
|
769
|
+
const plistdtd = `http://${"www.apple.com"}/DTDs/PropertyList-1.0.dtd`;
|
|
770
|
+
return [
|
|
771
|
+
{
|
|
772
|
+
path: "Devthink.xcodeproj/project.pbxproj",
|
|
773
|
+
text: '// !$*UTF8*$!\n{\n archiveVersion = 1;\n classes = {};\n objectVersion = 56;\n objects = {};\n rootObject = "Devthink-project";\n}\n',
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
path: "Devthink/Info.plist",
|
|
777
|
+
text: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "${plistdtd}">\n<plist version="1.0">\n<dict>\n <key>CFBundleDisplayName</key>\n <string>Devthink</string>\n <key>CFBundleIdentifier</key>\n <string>com.wenathlan.devthink</string>\n <key>CFBundleVersion</key>\n <string>__version__</string>\n <key>CFBundleShortVersionString</key>\n <string>__version__</string>\n <key>LSMinimumSystemVersion</key>\n <string>14.0</string>\n <key>NSExtension</key>\n <dict>\n <key>NSExtensionPointIdentifier</key>\n <string>com.apple.Safari.web-extension</string>\n <key>NSExtensionPrincipalClass</key>\n <string>$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler</string>\n <key>NSExtensionAttributes</key>\n <dict>\n <key>SFSafariWebExtensionBundleIdentifier</key>\n <string>com.wenathlan.devthink.extension</string>\n </dict>\n </dict>\n</dict>\n</plist>\n`,
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
path: "Devthink/Devthink.entitlements",
|
|
781
|
+
text: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "${plistdtd}">\n<plist version="1.0">\n<dict>\n <key>com.apple.security.app-sandbox</key>\n <true/>\n <key>com.apple.security.network.client</key>\n <true/>\n</dict>\n</plist>\n`,
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
path: "Devthink/AppDelegate.swift",
|
|
785
|
+
text: 'import Cocoa\nimport SafariServices\n\n@main\nfinal class AppDelegate: NSObject, NSApplicationDelegate {\n func applicationDidFinishLaunching(_ notification: Notification) {\n SFSafariApplication.showPreferencesForExtension(withIdentifier: "com.wenathlan.devthink.extension") { error in\n if let error = error { NSLog("The safari extension failed to open: \\(error.localizedDescription)") }\n }\n }\n}\n',
|
|
786
|
+
},
|
|
787
|
+
];
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** Builds the safari app extension wrapper around the chromium extension payload: the project files, the entitlements, the minimal app shell and the archive bytes the release workflow attaches beside the chromium zip and the firefox xpi. */
|
|
791
|
+
export function safariskeletonbuild(input: safariskeletoninput): safariskeletonoutput {
|
|
792
|
+
const version = (input.version ?? "").trim();
|
|
793
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
794
|
+
throw new Error("The safari skeleton carries the release version; a non semver version never names an artifact.");
|
|
795
|
+
const bundleid = (input.bundleid ?? "").trim();
|
|
796
|
+
if (bundleid === "" || !/^[a-z0-9][a-z0-9.-]*$/.test(bundleid))
|
|
797
|
+
throw new Error(
|
|
798
|
+
"The safari skeleton carries the bundle id; an empty or malformed id never registers a safari app extension.",
|
|
799
|
+
);
|
|
800
|
+
if (input.extensionpayload.length === 0)
|
|
801
|
+
throw new Error(
|
|
802
|
+
"The safari skeleton embeds the chromium build as the safari web extension payload; an empty payload never wraps an extension.",
|
|
803
|
+
);
|
|
804
|
+
if (input.entitlements.length === 0)
|
|
805
|
+
throw new Error(
|
|
806
|
+
"The safari skeleton declares the app entitlements for the extension distribution; an empty entitlements list never ships a safari app extension.",
|
|
807
|
+
);
|
|
808
|
+
const projectfiles = safariskeletonprojectfiles().map((file) => ({
|
|
809
|
+
path: file.path,
|
|
810
|
+
text: file.text.replace(/__version__/g, version).replace(/com\.wenathlan\.devthink/g, bundleid),
|
|
811
|
+
}));
|
|
812
|
+
const stored: Array<{ name: string; bytes: Buffer }> = projectfiles.map((file) => ({
|
|
813
|
+
name: file.path,
|
|
814
|
+
bytes: Buffer.from(file.text, "utf8"),
|
|
815
|
+
}));
|
|
816
|
+
const entries: string[] = projectfiles.map((file) => file.path);
|
|
817
|
+
for (const entry of input.extensionpayload) {
|
|
818
|
+
const name = (entry.name ?? "").trim();
|
|
819
|
+
if (name === "")
|
|
820
|
+
throw new Error("The safari skeleton names every payload entry; an empty name never enters the archive.");
|
|
821
|
+
stored.push({ name: `Resources/${name}`, bytes: Buffer.from(entry.bytes) });
|
|
822
|
+
entries.push(`Resources/${name}`);
|
|
823
|
+
}
|
|
824
|
+
const locals: Buffer[] = [];
|
|
825
|
+
const records: Array<{ name: string; size: number; offset: number }> = [];
|
|
826
|
+
let offset = 0;
|
|
827
|
+
for (const entry of stored) {
|
|
828
|
+
const local = safarizipfile(entry.name, entry.bytes);
|
|
829
|
+
locals.push(local);
|
|
830
|
+
records.push({ name: entry.name, size: entry.bytes.length, offset });
|
|
831
|
+
offset += local.length;
|
|
832
|
+
}
|
|
833
|
+
const central = safaricentraldirectory(records);
|
|
834
|
+
const end = safariendrecord(records.length, central.bytes.length, offset);
|
|
835
|
+
const bytes = Buffer.concat([...locals, central.bytes, end]);
|
|
836
|
+
return {
|
|
837
|
+
archive: { name: `devthink-safari-${version}.zip`, bytes: new Uint8Array(bytes) },
|
|
838
|
+
projectfiles,
|
|
839
|
+
entitlements: input.entitlements,
|
|
840
|
+
appshell: "Devthink/AppDelegate.swift",
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** The popover equivalent the safari build renders for the sidepanel surface: the safari app extension opens a popover with the same review gate layout the sidepanel polyfill carries on the chromium build. */
|
|
845
|
+
export function safariskeletonpopoverof(): { path: string; bounds: { width: number; height: number } } {
|
|
846
|
+
return { path: "sidepanel.html", bounds: { width: 480, height: 720 } };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Builds one local file header of the safari skeleton zip archive (stored, no compression) for the wrapper files and the embedded chromium payload. */
|
|
850
|
+
export function safarizipfile(name: string, bytes: Buffer): Buffer {
|
|
851
|
+
const namebuffer = Buffer.from(name, "utf8");
|
|
852
|
+
const header = Buffer.alloc(30 + namebuffer.length);
|
|
853
|
+
header.writeUInt32LE(0x04034b50, 0);
|
|
854
|
+
header.writeUInt16LE(20, 4);
|
|
855
|
+
header.writeUInt16LE(0, 6);
|
|
856
|
+
header.writeUInt16LE(0, 8);
|
|
857
|
+
header.writeUInt16LE(0, 10);
|
|
858
|
+
header.writeUInt16LE(0, 12);
|
|
859
|
+
header.writeUInt32LE(0, 14);
|
|
860
|
+
header.writeUInt32LE(bytes.length, 18);
|
|
861
|
+
header.writeUInt32LE(bytes.length, 22);
|
|
862
|
+
header.writeUInt16LE(namebuffer.length, 26);
|
|
863
|
+
header.writeUInt16LE(0, 28);
|
|
864
|
+
namebuffer.copy(header, 30);
|
|
865
|
+
return Buffer.concat([header, bytes]);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/** Builds the central directory of the safari skeleton zip archive: one entry per archive file with its name, its stored size and the true offset its local file header sits at, so the standard unzip tooling lists the wrapper without a repair pass. */
|
|
869
|
+
export function safaricentraldirectory(records: Array<{ name: string; size: number; offset: number }>): {
|
|
870
|
+
bytes: Buffer;
|
|
871
|
+
} {
|
|
872
|
+
const chunks: Buffer[] = [];
|
|
873
|
+
for (const record of records) {
|
|
874
|
+
const namebuffer = Buffer.from(record.name, "utf8");
|
|
875
|
+
const header = Buffer.alloc(46 + namebuffer.length);
|
|
876
|
+
header.writeUInt32LE(0x02014b50, 0);
|
|
877
|
+
header.writeUInt16LE(20, 4);
|
|
878
|
+
header.writeUInt16LE(20, 6);
|
|
879
|
+
header.writeUInt16LE(0, 8);
|
|
880
|
+
header.writeUInt16LE(0, 10);
|
|
881
|
+
header.writeUInt16LE(0, 12);
|
|
882
|
+
header.writeUInt16LE(0, 14);
|
|
883
|
+
header.writeUInt32LE(0, 16);
|
|
884
|
+
header.writeUInt32LE(record.size, 20);
|
|
885
|
+
header.writeUInt32LE(record.size, 24);
|
|
886
|
+
header.writeUInt16LE(namebuffer.length, 28);
|
|
887
|
+
header.writeUInt16LE(0, 30);
|
|
888
|
+
header.writeUInt16LE(0, 32);
|
|
889
|
+
header.writeUInt16LE(0, 34);
|
|
890
|
+
header.writeUInt16LE(0, 36);
|
|
891
|
+
header.writeUInt32LE(0, 38);
|
|
892
|
+
header.writeUInt32LE(record.offset, 42);
|
|
893
|
+
namebuffer.copy(header, 46);
|
|
894
|
+
chunks.push(header);
|
|
895
|
+
}
|
|
896
|
+
return { bytes: Buffer.concat(chunks) };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** Builds the end of central directory record the safari skeleton zip archive carries: the entry count, the central directory size and the true offset the central directory sits at, so the standard unzip tooling seeks the directory before it lists one entry. */
|
|
900
|
+
export function safariendrecord(entrycount: number, centralsize: number, centralstart: number): Buffer {
|
|
901
|
+
const end = Buffer.alloc(22);
|
|
902
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
903
|
+
end.writeUInt16LE(0, 4);
|
|
904
|
+
end.writeUInt16LE(0, 6);
|
|
905
|
+
end.writeUInt16LE(entrycount, 8);
|
|
906
|
+
end.writeUInt16LE(entrycount, 10);
|
|
907
|
+
end.writeUInt32LE(centralsize, 12);
|
|
908
|
+
end.writeUInt32LE(centralstart, 16);
|
|
909
|
+
end.writeUInt16LE(0, 20);
|
|
910
|
+
return end;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/* ── Merged: the grand merge section ── the correlated webextension api map logics of the merged repository interned here, one surface without duplicate variations. ── */
|
|
914
|
+
|
|
915
|
+
import type {
|
|
916
|
+
apibrowserprobeinput,
|
|
917
|
+
apimapbrowserprobe,
|
|
918
|
+
apimapreport,
|
|
919
|
+
apimapresolution,
|
|
920
|
+
apimapstructerror,
|
|
921
|
+
webextensionapientry,
|
|
922
|
+
webextensionapikind,
|
|
923
|
+
} from "./types.js";
|
|
924
|
+
/* ── Merged from apimap.ts: the 1.1.88 consolidation interns the correlated apimap logic here, so no variation of the same file lives beside another. ── */
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Apimap of the 1.1.86 browser coverage family.
|
|
928
|
+
* Every webextension api concern of the cross browser build lives in this one pure module: the catalog of every webextension api the codebase touches, the per browser equivalents (chromium, firefox and safari) of every api, the runtime browser probe that resolves the right call for the running browser, the in memory cache that holds the resolved browser for the run, the structured errors that surface the unsupported calls with their retry hints and the build time unmapped api report that fails the build when a new api lacks a row. The module stays pure: the browser runtime object, the user agent and the install probe reach it through injected seams only, the kind catalog and the policy gates stay identical on every browser, no vendor endpoint and no download url ever appears here — a single source manifest speaks every webextension dialect through the per browser overlay it carries, and an unsupported call surfaces a structured error instead of a silent fallback.
|
|
929
|
+
* Example: `const probe = apimapbrowsercacheprobe({ runtime: globalThis.chrome, probeuseragent: () => navigator.userAgent }); const browser = probe(); const api = apimapresolve("storage.local", browser); const unmapped = apimapunmapped(apimapentries());`
|
|
930
|
+
*/
|
|
931
|
+
|
|
932
|
+
/** The kind of every recorded webextension api: a namespace the codebase reads, a method it calls, an event it listens to, or a property it queries. */
|
|
933
|
+
export function apimapkinds(): webextensionapikind[] {
|
|
934
|
+
return ["namespace", "method", "event", "property"];
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/** The catalog of every webextension api the codebase touches: the api name the reviewed vocabulary uses, the per browser equivalent (chromium, firefox and safari), the kind and the unsupported marker that flags an api a single browser lacks. A new api without a chromium and firefox mapping fails the build before it ships; the catalog is the source of truth the apimap build check reads. */
|
|
938
|
+
export function apimapentries(): webextensionapientry[] {
|
|
939
|
+
return [
|
|
940
|
+
{
|
|
941
|
+
api: "runtime",
|
|
942
|
+
chromium: "chrome.runtime",
|
|
943
|
+
firefox: "browser.runtime",
|
|
944
|
+
safari: "browser.runtime",
|
|
945
|
+
kind: "namespace",
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
api: "runtime.geturl",
|
|
949
|
+
chromium: "chrome.runtime.getURL",
|
|
950
|
+
firefox: "browser.runtime.getURL",
|
|
951
|
+
safari: "browser.runtime.getURL",
|
|
952
|
+
kind: "method",
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
api: "runtime.connect",
|
|
956
|
+
chromium: "chrome.runtime.connect",
|
|
957
|
+
firefox: "browser.runtime.connect",
|
|
958
|
+
safari: "browser.runtime.connect",
|
|
959
|
+
kind: "method",
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
api: "runtime.sendmessage",
|
|
963
|
+
chromium: "chrome.runtime.sendMessage",
|
|
964
|
+
firefox: "browser.runtime.sendMessage",
|
|
965
|
+
safari: "browser.runtime.sendMessage",
|
|
966
|
+
kind: "method",
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
api: "runtime.onmessage",
|
|
970
|
+
chromium: "chrome.runtime.onMessage",
|
|
971
|
+
firefox: "browser.runtime.onMessage",
|
|
972
|
+
safari: "browser.runtime.onMessage",
|
|
973
|
+
kind: "event",
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
api: "runtime.id",
|
|
977
|
+
chromium: "chrome.runtime.id",
|
|
978
|
+
firefox: "browser.runtime.id",
|
|
979
|
+
safari: "browser.runtime.id",
|
|
980
|
+
kind: "property",
|
|
981
|
+
},
|
|
982
|
+
{ api: "tabs", chromium: "chrome.tabs", firefox: "browser.tabs", safari: "browser.tabs", kind: "namespace" },
|
|
983
|
+
{
|
|
984
|
+
api: "tabs.query",
|
|
985
|
+
chromium: "chrome.tabs.query",
|
|
986
|
+
firefox: "browser.tabs.query",
|
|
987
|
+
safari: "browser.tabs.query",
|
|
988
|
+
kind: "method",
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
api: "tabs.create",
|
|
992
|
+
chromium: "chrome.tabs.create",
|
|
993
|
+
firefox: "browser.tabs.create",
|
|
994
|
+
safari: "browser.tabs.create",
|
|
995
|
+
kind: "method",
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
api: "tabs.update",
|
|
999
|
+
chromium: "chrome.tabs.update",
|
|
1000
|
+
firefox: "browser.tabs.update",
|
|
1001
|
+
safari: "browser.tabs.update",
|
|
1002
|
+
kind: "method",
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
api: "tabs.executeScript",
|
|
1006
|
+
chromium: "chrome.tabs.executeScript",
|
|
1007
|
+
firefox: "browser.tabs.executeScript",
|
|
1008
|
+
safari: "",
|
|
1009
|
+
kind: "method",
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
api: "scripting",
|
|
1013
|
+
chromium: "chrome.scripting",
|
|
1014
|
+
firefox: "browser.scripting",
|
|
1015
|
+
safari: "browser.scripting",
|
|
1016
|
+
kind: "namespace",
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
api: "scripting.executeScript",
|
|
1020
|
+
chromium: "chrome.scripting.executeScript",
|
|
1021
|
+
firefox: "browser.scripting.executeScript",
|
|
1022
|
+
safari: "browser.scripting.executeScript",
|
|
1023
|
+
kind: "method",
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
api: "storage",
|
|
1027
|
+
chromium: "chrome.storage",
|
|
1028
|
+
firefox: "browser.storage",
|
|
1029
|
+
safari: "browser.storage",
|
|
1030
|
+
kind: "namespace",
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
api: "storage.local",
|
|
1034
|
+
chromium: "chrome.storage.local",
|
|
1035
|
+
firefox: "browser.storage.local",
|
|
1036
|
+
safari: "browser.storage.local",
|
|
1037
|
+
kind: "namespace",
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
api: "storage.session",
|
|
1041
|
+
chromium: "chrome.storage.session",
|
|
1042
|
+
firefox: "browser.storage.session",
|
|
1043
|
+
safari: "browser.storage.session",
|
|
1044
|
+
kind: "namespace",
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
api: "windows",
|
|
1048
|
+
chromium: "chrome.windows",
|
|
1049
|
+
firefox: "browser.windows",
|
|
1050
|
+
safari: "browser.windows",
|
|
1051
|
+
kind: "namespace",
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
api: "windows.create",
|
|
1055
|
+
chromium: "chrome.windows.create",
|
|
1056
|
+
firefox: "browser.windows.create",
|
|
1057
|
+
safari: "browser.windows.create",
|
|
1058
|
+
kind: "method",
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
api: "windows.update",
|
|
1062
|
+
chromium: "chrome.windows.update",
|
|
1063
|
+
firefox: "browser.windows.update",
|
|
1064
|
+
safari: "browser.windows.update",
|
|
1065
|
+
kind: "method",
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
api: "notifications",
|
|
1069
|
+
chromium: "chrome.notifications",
|
|
1070
|
+
firefox: "browser.notifications",
|
|
1071
|
+
safari: "browser.notifications",
|
|
1072
|
+
kind: "namespace",
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
api: "notifications.create",
|
|
1076
|
+
chromium: "chrome.notifications.create",
|
|
1077
|
+
firefox: "browser.notifications.create",
|
|
1078
|
+
safari: "browser.notifications.create",
|
|
1079
|
+
kind: "method",
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
api: "downloads",
|
|
1083
|
+
chromium: "chrome.downloads",
|
|
1084
|
+
firefox: "browser.downloads",
|
|
1085
|
+
safari: "browser.downloads",
|
|
1086
|
+
kind: "namespace",
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
api: "downloads.download",
|
|
1090
|
+
chromium: "chrome.downloads.download",
|
|
1091
|
+
firefox: "browser.downloads.download",
|
|
1092
|
+
safari: "browser.downloads.download",
|
|
1093
|
+
kind: "method",
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
api: "contextMenus",
|
|
1097
|
+
chromium: "chrome.contextMenus",
|
|
1098
|
+
firefox: "browser.contextMenus",
|
|
1099
|
+
safari: "browser.contextMenus",
|
|
1100
|
+
kind: "namespace",
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
api: "contextMenus.create",
|
|
1104
|
+
chromium: "chrome.contextMenus.create",
|
|
1105
|
+
firefox: "browser.contextMenus.create",
|
|
1106
|
+
safari: "browser.contextMenus.create",
|
|
1107
|
+
kind: "method",
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
api: "sidePanel",
|
|
1111
|
+
chromium: "chrome.sidePanel",
|
|
1112
|
+
firefox: "browserpolyfill.sidepanel",
|
|
1113
|
+
safari: "browserpolyfill.sidepanel",
|
|
1114
|
+
kind: "namespace",
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
api: "sidePanel.open",
|
|
1118
|
+
chromium: "chrome.sidePanel.open",
|
|
1119
|
+
firefox: "browserpolyfill.sidepanel.open",
|
|
1120
|
+
safari: "browserpolyfill.sidepanel.open",
|
|
1121
|
+
kind: "method",
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
api: "sidePanel.setoptions",
|
|
1125
|
+
chromium: "chrome.sidePanel.setOptions",
|
|
1126
|
+
firefox: "browserpolyfill.sidepanel.setoptions",
|
|
1127
|
+
safari: "browserpolyfill.sidepanel.setoptions",
|
|
1128
|
+
kind: "method",
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
api: "offscreen",
|
|
1132
|
+
chromium: "chrome.offscreen",
|
|
1133
|
+
firefox: "browserpolyfill.offscreen",
|
|
1134
|
+
safari: "browserpolyfill.offscreen",
|
|
1135
|
+
kind: "namespace",
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
api: "offscreen.createDocument",
|
|
1139
|
+
chromium: "chrome.offscreen.createDocument",
|
|
1140
|
+
firefox: "browserpolyfill.offscreen.inline",
|
|
1141
|
+
safari: "browserpolyfill.offscreen.inline",
|
|
1142
|
+
kind: "method",
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
api: "action",
|
|
1146
|
+
chromium: "chrome.action",
|
|
1147
|
+
firefox: "browser.action",
|
|
1148
|
+
safari: "browser.action",
|
|
1149
|
+
kind: "namespace",
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
api: "action.setpopup",
|
|
1153
|
+
chromium: "chrome.action.setPopup",
|
|
1154
|
+
firefox: "browser.action.setPopup",
|
|
1155
|
+
safari: "browser.action.setPopup",
|
|
1156
|
+
kind: "method",
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
api: "clipboardRead",
|
|
1160
|
+
chromium: "navigator.clipboard.readText",
|
|
1161
|
+
firefox: "navigator.clipboard.readText",
|
|
1162
|
+
safari: "navigator.clipboard.readText",
|
|
1163
|
+
kind: "method",
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
api: "clipboardWrite",
|
|
1167
|
+
chromium: "navigator.clipboard.writeText",
|
|
1168
|
+
firefox: "navigator.clipboard.writeText",
|
|
1169
|
+
safari: "navigator.clipboard.writeText",
|
|
1170
|
+
kind: "method",
|
|
1171
|
+
},
|
|
1172
|
+
{ api: "i18n", chromium: "chrome.i18n", firefox: "browser.i18n", safari: "browser.i18n", kind: "namespace" },
|
|
1173
|
+
{
|
|
1174
|
+
api: "i18n.getmessage",
|
|
1175
|
+
chromium: "chrome.i18n.getMessage",
|
|
1176
|
+
firefox: "browser.i18n.getMessage",
|
|
1177
|
+
safari: "browser.i18n.getMessage",
|
|
1178
|
+
kind: "method",
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
api: "permissions",
|
|
1182
|
+
chromium: "chrome.permissions",
|
|
1183
|
+
firefox: "browser.permissions",
|
|
1184
|
+
safari: "browser.permissions",
|
|
1185
|
+
kind: "namespace",
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
api: "permissions.request",
|
|
1189
|
+
chromium: "chrome.permissions.request",
|
|
1190
|
+
firefox: "browser.permissions.request",
|
|
1191
|
+
safari: "browser.permissions.request",
|
|
1192
|
+
kind: "method",
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
api: "permissions.contains",
|
|
1196
|
+
chromium: "chrome.permissions.contains",
|
|
1197
|
+
firefox: "browser.permissions.contains",
|
|
1198
|
+
safari: "browser.permissions.contains",
|
|
1199
|
+
kind: "method",
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
api: "management",
|
|
1203
|
+
chromium: "chrome.management",
|
|
1204
|
+
firefox: "browser.management",
|
|
1205
|
+
safari: "browser.management",
|
|
1206
|
+
kind: "namespace",
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
api: "commands",
|
|
1210
|
+
chromium: "chrome.commands",
|
|
1211
|
+
firefox: "browser.commands",
|
|
1212
|
+
safari: "browser.commands",
|
|
1213
|
+
kind: "namespace",
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
api: "commands.onCommand",
|
|
1217
|
+
chromium: "chrome.commands.onCommand",
|
|
1218
|
+
firefox: "browser.commands.onCommand",
|
|
1219
|
+
safari: "browser.commands.onCommand",
|
|
1220
|
+
kind: "event",
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
api: "nativeMessaging",
|
|
1224
|
+
chromium: "chrome.runtime.connectNative",
|
|
1225
|
+
firefox: "browser.runtime.connectNative",
|
|
1226
|
+
safari: "",
|
|
1227
|
+
kind: "method",
|
|
1228
|
+
},
|
|
1229
|
+
];
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/** The default feature flag set: every feature flag that ships crosses every browser through the intersection of the per browser sets. */
|
|
1233
|
+
export function apifeatureflagintersection(): string[] {
|
|
1234
|
+
const entries = apimapentries();
|
|
1235
|
+
const browsers: webextensionbrowser[] = ["chromium", "firefox", "safari"];
|
|
1236
|
+
const flags: string[] = [];
|
|
1237
|
+
for (const entry of entries) {
|
|
1238
|
+
let all = true;
|
|
1239
|
+
for (const browser of browsers) {
|
|
1240
|
+
const value = browser === "chromium" ? entry.chromium : browser === "firefox" ? entry.firefox : entry.safari;
|
|
1241
|
+
if (value === "") {
|
|
1242
|
+
all = false;
|
|
1243
|
+
break;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
if (all) flags.push(entry.api);
|
|
1247
|
+
}
|
|
1248
|
+
return flags.sort();
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/** Looks up one apimap entry by its reviewed api name; a missing entry returns undefined so the resolver and the unmapped reporter read the same shape. */
|
|
1252
|
+
export function apimapentryof(api: string): webextensionapientry | undefined {
|
|
1253
|
+
return apimapentries().find((entry) => entry.api === api);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/** Resolves one reviewed api to the per browser call of the running browser; the runtime probe runs first when the caller passes the runtime seam, the empty probe answers the browser the caller chose, and an unmapped api surfaces an unsupported structured error with the retry hint instead of a silent fallback. */
|
|
1257
|
+
export function apimapresolve(input: {
|
|
1258
|
+
api: string;
|
|
1259
|
+
browser?: webextensionbrowser;
|
|
1260
|
+
runtime?: unknown;
|
|
1261
|
+
probeuseragent?: () => string;
|
|
1262
|
+
}): apimapresolution {
|
|
1263
|
+
const api = (input.api ?? "").trim();
|
|
1264
|
+
if (api === "")
|
|
1265
|
+
return {
|
|
1266
|
+
ok: false,
|
|
1267
|
+
browser: input.browser ?? "chromium",
|
|
1268
|
+
reason: "The apimap resolution names its api; an empty api resolves nothing.",
|
|
1269
|
+
retry: "none",
|
|
1270
|
+
};
|
|
1271
|
+
const entry = apimapentryof(api);
|
|
1272
|
+
if (entry === undefined)
|
|
1273
|
+
return {
|
|
1274
|
+
ok: false,
|
|
1275
|
+
browser: input.browser ?? "chromium",
|
|
1276
|
+
reason: `The apimap carries no row for the ${api} api; the catalog must record every api the codebase touches.`,
|
|
1277
|
+
retry: "none",
|
|
1278
|
+
};
|
|
1279
|
+
const browser =
|
|
1280
|
+
input.browser ??
|
|
1281
|
+
apimapbrowserof({
|
|
1282
|
+
...(input.runtime !== undefined ? { runtime: input.runtime } : {}),
|
|
1283
|
+
...(input.probeuseragent !== undefined ? { probeuseragent: input.probeuseragent } : {}),
|
|
1284
|
+
});
|
|
1285
|
+
const value = browser === "chromium" ? entry.chromium : browser === "firefox" ? entry.firefox : entry.safari;
|
|
1286
|
+
if (value === "")
|
|
1287
|
+
return {
|
|
1288
|
+
ok: false,
|
|
1289
|
+
browser,
|
|
1290
|
+
reason: `The ${api} api has no ${browser} equivalent in the apimap; the feature flag stays off and the call surfaces a structured error.`,
|
|
1291
|
+
retry: "none",
|
|
1292
|
+
};
|
|
1293
|
+
return { ok: true, browser, api, equivalent: value };
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/** Reports the unmapped apis of the catalog at build time: an api without a chromium and firefox mapping fails the build before it ships, and the report keeps the per browser coverage of every api beside the missing rows. */
|
|
1297
|
+
export function apimapunmapped(entries: webextensionapientry[] = apimapentries()): apimapreport {
|
|
1298
|
+
const rows: apimapreport["rows"] = entries.map((entry) => ({
|
|
1299
|
+
api: entry.api,
|
|
1300
|
+
chromium: entry.chromium !== "",
|
|
1301
|
+
firefox: entry.firefox !== "",
|
|
1302
|
+
safari: entry.safari !== "",
|
|
1303
|
+
}));
|
|
1304
|
+
const missingchromium = rows.filter((row) => !row.chromium).map((row) => row.api);
|
|
1305
|
+
const missingfirefox = rows.filter((row) => !row.firefox).map((row) => row.api);
|
|
1306
|
+
const missingsafari = rows.filter((row) => !row.safari).map((row) => row.api);
|
|
1307
|
+
const failed = missingchromium.length > 0 || missingfirefox.length > 0;
|
|
1308
|
+
const reason = failed
|
|
1309
|
+
? `The apimap carries apis without a chromium or firefox mapping: ${[...missingchromium, ...missingfirefox]
|
|
1310
|
+
.filter((name, index, source) => source.indexOf(name) === index)
|
|
1311
|
+
.slice(0, 5)
|
|
1312
|
+
.join(", ")}; every webextension api the codebase touches needs a chromium and firefox row.`
|
|
1313
|
+
: "";
|
|
1314
|
+
return { rows, missingchromium, missingfirefox, missingsafari, failed, reason };
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/** The runtime browser probe resolves the running browser from the runtime seam and the user agent: the chromium runtime carries the chrome namespace, the firefox runtime carries the browser namespace, the safari runtime carries the browser namespace beside a vendor keyword in the user agent the probe reads, and an unknown runtime degrades to chromium so the source manifest stays the default the cross browser build loads. */
|
|
1318
|
+
export function apimapbrowserof(input: apibrowserprobeinput = {}): webextensionbrowser {
|
|
1319
|
+
if (input.runtime !== undefined && input.runtime !== null) {
|
|
1320
|
+
const record = input.runtime as Record<string, unknown>;
|
|
1321
|
+
if (typeof record.browser === "object" && record.browser !== null) return "firefox";
|
|
1322
|
+
if (typeof record.chrome === "object" && record.chrome !== null) return "chromium";
|
|
1323
|
+
if (typeof record.sidePanel === "object" && record.sidePanel !== null) return "chromium";
|
|
1324
|
+
}
|
|
1325
|
+
const useragent = input.probeuseragent !== undefined ? input.probeuseragent() : "";
|
|
1326
|
+
if (useragent !== "") {
|
|
1327
|
+
if (/firefox/i.test(useragent)) return "firefox";
|
|
1328
|
+
if (/safari/i.test(useragent) && !/chrome/i.test(useragent)) return "safari";
|
|
1329
|
+
}
|
|
1330
|
+
return "chromium";
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/** Builds the in memory cached browser probe: the probe resolves the browser once per run, the cache holds it for the rest of the run, and a forced refresh rewrites the cache when the build runs the same process over a new context. */
|
|
1334
|
+
export function apimapbrowsercacheprobe(input: apibrowserprobeinput = {}): apimapbrowserprobe {
|
|
1335
|
+
let cached: webextensionbrowser | undefined;
|
|
1336
|
+
const probe = (): webextensionbrowser => {
|
|
1337
|
+
if (cached === undefined) cached = apimapbrowserof(input);
|
|
1338
|
+
return cached;
|
|
1339
|
+
};
|
|
1340
|
+
const refresh = (): webextensionbrowser => {
|
|
1341
|
+
cached = apimapbrowserof(input);
|
|
1342
|
+
return cached;
|
|
1343
|
+
};
|
|
1344
|
+
return { probe, refresh };
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/** The structured error of an unsupported apimap call: the family, the message, the retry hint and the time, so a caller maps the failure to its next action instead of a bare throw. */
|
|
1348
|
+
export function apimapstructerrorof(input: {
|
|
1349
|
+
api: string;
|
|
1350
|
+
browser: webextensionbrowser;
|
|
1351
|
+
reason?: string;
|
|
1352
|
+
now: number;
|
|
1353
|
+
}): apimapstructerror {
|
|
1354
|
+
const message =
|
|
1355
|
+
input.reason !== undefined && input.reason !== ""
|
|
1356
|
+
? input.reason
|
|
1357
|
+
: `The ${input.api} api has no ${input.browser} equivalent; the feature flag stays off and the call surfaces a structured error.`;
|
|
1358
|
+
return { family: "apimap", message, retry: "none", browser: input.browser, api: input.api, at: input.now };
|
|
1359
|
+
}
|