@wenathlan/devthink 2.0.15 → 2.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
- package/bridge.d.ts.map +1 -0
- package/bridge.js +1877 -0
- package/bun.d.ts.map +1 -0
- package/bun.js +52329 -0
- package/caps/background.json +712 -0
- package/caps/cli.json +360 -0
- package/caps/library.json +3611 -0
- package/caps/mcp.json +126 -0
- package/caps/pagebridge.json +377 -0
- package/caps/popup.json +51 -0
- package/caps/sidepanel.json +522 -0
- package/checksums.txt +142 -0
- package/cli.d.ts.map +1 -0
- package/{dist/cli.js → cli.js} +2 -2
- package/companion.d.ts.map +1 -0
- package/companion.js +389 -0
- package/crossbrowser.d.ts.map +1 -0
- package/crossbrowser.js +1191 -0
- package/dashdone.d.ts.map +1 -0
- package/dashdone.js +368 -0
- package/deno.d.ts.map +1 -0
- package/deno.js +52325 -0
- package/devthink.umd.js +52346 -0
- package/fixtures/bun.ts +7 -0
- package/fixtures/cjs.js +6 -0
- package/fixtures/deno.ts +6 -0
- package/fixtures/example-org-pagestate.json +22 -0
- package/fixtures/importers/automa-workflow.json +18 -0
- package/fixtures/importers/selenium-side.json +20 -0
- package/fixtures/importers/tabular-plan.csv +7 -0
- package/fixtures/importers/uivision-macro.json +10 -0
- package/fixtures/importers/v1-plan.json +13 -0
- package/fixtures/mcp-client.mjs +14 -0
- package/fixtures/pages/checkout.html +192 -0
- package/fixtures/pages/dashboard.html +110 -0
- package/fixtures/pages/feed.html +113 -0
- package/fixtures/pages/product-grid.html +150 -0
- package/fixtures/plans/form-inventory-plan.json +11 -0
- package/fixtures/plans/release-notes-plan.json +11 -0
- package/fixtures/recipes/agentscompete.json +11 -0
- package/fixtures/recipes/agentsforms.json +11 -0
- package/fixtures/recipes/agentsmonitor.json +11 -0
- package/fixtures/recipes/agentsreview.json +29 -0
- package/fixtures/recipes/agentswarm.json +11 -0
- package/fixtures/recipes/fillcalendar.json +12 -0
- package/fixtures/recipes/fillcheckout.json +13 -0
- package/fixtures/recipes/filldependentdropdowns.json +12 -0
- package/fixtures/recipes/fillfromcsv.json +13 -0
- package/fixtures/recipes/fillloginform.json +12 -0
- package/fixtures/recipes/fillmultistepwizard.json +13 -0
- package/fixtures/recipes/fillregistration.json +12 -0
- package/fixtures/recipes/fillsearchfilters.json +12 -0
- package/fixtures/recipes/filltypeahead.json +11 -0
- package/fixtures/recipes/monitoralerts.json +12 -0
- package/fixtures/recipes/monitoravailability.json +11 -0
- package/fixtures/recipes/monitornetworkquiet.json +12 -0
- package/fixtures/recipes/monitorpagechanges.json +11 -0
- package/fixtures/recipes/monitorpricechange.json +11 -0
- package/fixtures/recipes/monitorschedule.json +12 -0
- package/fixtures/recipes/scrapedynamicprices.json +13 -0
- package/fixtures/recipes/scrapeimagegallery.json +12 -0
- package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
- package/fixtures/recipes/scrapenestedlists.json +12 -0
- package/fixtures/recipes/scrapeproductgrid.json +12 -0
- package/fixtures/recipes/scrapereviews.json +12 -0
- package/fixtures/recipes/scrapesearchresults.json +12 -0
- package/fixtures/recipes/scrapetablepagination.json +12 -0
- package/fixtures/recipes/testaccessibility.json +34 -0
- package/fixtures/recipes/testconsoleerrors.json +12 -0
- package/fixtures/recipes/testdeeplinks.json +12 -0
- package/fixtures/recipes/testformvalidation.json +13 -0
- package/fixtures/recipes/testlayoutshift.json +12 -0
- package/fixtures/recipes/testlinkcheck.json +12 -0
- package/fixtures/recipes/testloadtiming.json +12 -0
- package/fixtures/recipes/testvisualdiff.json +12 -0
- package/fixtures/workflows/changelog-digest-workflow.json +13 -0
- package/fixtures/workflows/form-survey-workflow.json +13 -0
- package/gallery.json +706 -0
- package/hardening.d.ts.map +1 -0
- package/hardening.js +188 -0
- package/headless.d.ts.map +1 -0
- package/headless.js +11522 -0
- package/http.d.ts.map +1 -0
- package/http.js +2165 -0
- package/index.cjs +54682 -0
- package/index.d.ts.map +1 -0
- package/index.js +52310 -0
- package/index.neutral.js +46343 -0
- package/{web/manifest.json → manifest.json} +1 -1
- package/mcp.d.ts.map +1 -0
- package/mcp.js +20515 -0
- package/memory.d.ts.map +1 -0
- package/{memory.ts → memory.js} +2310 -3834
- package/nativehost.template.json +10 -0
- package/node.cjs +54686 -0
- package/node.d.ts.map +1 -0
- package/pack.d.ts.map +1 -0
- package/pack.js +805 -0
- package/package.json +83 -176
- package/policy.d.ts.map +1 -0
- package/{policy.ts → policy.js} +4679 -5073
- package/progress.d.ts.map +1 -0
- package/progress.js +645 -0
- package/protocol.d.ts.map +1 -0
- package/protocol.js +8334 -0
- package/schemas/audit.schema.json +226 -0
- package/schemas/capability.schema.json +113 -0
- package/schemas/envelope.schema.json +162 -0
- package/schemas/memory.schema.json +51 -0
- package/schemas/observation.schema.json +113 -0
- package/schemas/plan.schema.json +208 -0
- package/schemas/progress.schema.json +126 -0
- package/schemas/proposal.schema.json +68 -0
- package/schemas/session.schema.json +82 -0
- package/schemas/tool.schema.json +94 -0
- package/server.d.ts.map +1 -0
- package/server.js +28877 -0
- package/umd-example.html +18 -0
- package/umd.d.ts.map +1 -0
- package/accounts.ts +0 -2087
- package/agent.ts +0 -2331
- package/antigravity.ts +0 -2139
- package/apifreeze.ts +0 -4146
- package/auth.ts +0 -642
- package/background.ts +0 -16837
- package/bridge.ts +0 -2080
- package/build.ts +0 -54
- package/bun.ts +0 -26
- package/capture.ts +0 -1614
- package/checksecrets.ts +0 -72
- package/cjs.ts +0 -9
- package/cli.ts +0 -3500
- package/commands.ts +0 -1101
- package/companion.ts +0 -450
- package/compatibility.ts +0 -37
- package/config.ts +0 -3137
- package/constants.ts +0 -943
- package/core.ts +0 -1473
- package/crossbrowser.ts +0 -1359
- package/dashdone.ts +0 -605
- package/data.ts +0 -943
- package/database.ts +0 -133
- package/debug.ts +0 -850
- package/deno.ts +0 -27
- package/devthink.ts +0 -707
- package/dist/accounts.d.ts +0 -409
- package/dist/agent.d.ts +0 -837
- package/dist/antigravity.d.ts +0 -380
- package/dist/apifreeze.d.ts +0 -113
- package/dist/auth.d.ts +0 -224
- package/dist/capture.d.ts +0 -595
- package/dist/cjs.d.ts +0 -6
- package/dist/commands.d.ts +0 -310
- package/dist/compatibility.d.ts +0 -17
- package/dist/config.d.ts +0 -1853
- package/dist/constants.d.ts +0 -637
- package/dist/core.d.ts +0 -361
- package/dist/data.d.ts +0 -330
- package/dist/database.d.ts +0 -24
- package/dist/debug.d.ts +0 -270
- package/dist/engine.d.ts +0 -74
- package/dist/environments.d.ts +0 -244
- package/dist/evidence.d.ts +0 -277
- package/dist/export.d.ts +0 -311
- package/dist/fingerprint.d.ts +0 -594
- package/dist/flow.d.ts +0 -300
- package/dist/gates.d.ts +0 -103
- package/dist/identity.d.ts +0 -51
- package/dist/ids.d.ts +0 -7
- package/dist/llm.d.ts +0 -652
- package/dist/models.d.ts +0 -1807
- package/dist/net.d.ts +0 -325
- package/dist/neutral.d.ts +0 -41
- package/dist/oauth.d.ts +0 -703
- package/dist/page.d.ts +0 -1166
- package/dist/perf.d.ts +0 -481
- package/dist/plan.d.ts +0 -134
- package/dist/project.d.ts +0 -395
- package/dist/providers.d.ts +0 -37
- package/dist/quota.d.ts +0 -453
- package/dist/recovery.d.ts +0 -621
- package/dist/request.d.ts +0 -1492
- package/dist/run.d.ts +0 -779
- package/dist/runtime.d.ts +0 -212
- package/dist/search.d.ts +0 -375
- package/dist/security.d.ts +0 -534
- package/dist/serve.d.ts +0 -136
- package/dist/session.d.ts +0 -296
- package/dist/storage.d.ts +0 -53
- package/dist/streaming.d.ts +0 -1119
- package/dist/swarm.d.ts +0 -328
- package/dist/tools.d.ts +0 -170
- package/dist/types.d.ts +0 -8357
- package/dist/utils.d.ts +0 -82
- package/dist/version.d.ts +0 -7
- package/dist/versionregistry.d.ts +0 -406
- package/dist/views.d.ts +0 -579
- package/dist/workbenchsession.d.ts +0 -69
- package/dist/workflow.d.ts +0 -649
- package/docs/readme.md +0 -92
- package/docs/schemas/antigravity.json +0 -192
- package/engine.ts +0 -1524
- package/environments.ts +0 -1005
- package/evidence.ts +0 -643
- package/export.ts +0 -889
- package/fingerprint.ts +0 -1458
- package/flow.ts +0 -1190
- package/gates.ts +0 -307
- package/gateway.ts +0 -34
- package/hardening.ts +0 -191
- package/headless.ts +0 -369
- package/http.ts +0 -2110
- package/identity.ts +0 -246
- package/ids.ts +0 -26
- package/index.ts +0 -369
- package/llm.ts +0 -2682
- package/mcp.ts +0 -1835
- package/models.ts +0 -3805
- package/modes.ts +0 -43
- package/net.ts +0 -1214
- package/neutral.ts +0 -362
- package/node.ts +0 -26
- package/oauth.ts +0 -2216
- package/offscreen.ts +0 -170
- package/pack.ts +0 -956
- package/page.ts +0 -7824
- package/pagebridge.ts +0 -1116
- package/perf.ts +0 -957
- package/plan.ts +0 -672
- package/plugin.ts +0 -76
- package/prisma.config.ts +0 -27
- package/progress.ts +0 -1308
- package/project.ts +0 -1291
- package/protocol.ts +0 -4187
- package/providers.ts +0 -246
- package/quota.ts +0 -1887
- package/recovery.ts +0 -2433
- package/request.ts +0 -4482
- package/run.ts +0 -2135
- package/runtime.ts +0 -526
- package/search.ts +0 -1760
- package/security.ts +0 -1673
- package/serve.ts +0 -419
- package/server.ts +0 -5538
- package/session.ts +0 -1162
- package/storage.ts +0 -182
- package/streaming.ts +0 -4101
- package/swarm.ts +0 -884
- package/sync.ts +0 -56
- package/tests/agentcert.test.ts +0 -128
- package/tests/agentfleet.test.ts +0 -214
- package/tests/agentreview.test.ts +0 -241
- package/tests/agentstream.test.ts +0 -335
- package/tests/agentwork.test.ts +0 -356
- package/tests/apifreeze.test.ts +0 -290
- package/tests/apimap.test.ts +0 -112
- package/tests/approvalgate.test.ts +0 -175
- package/tests/artifactmanifest.test.ts +0 -204
- package/tests/attentionfeed.test.ts +0 -163
- package/tests/backgroundruns.test.ts +0 -100
- package/tests/batchscheduling.test.ts +0 -131
- package/tests/blackboard.test.ts +0 -238
- package/tests/bridge.test.ts +0 -1088
- package/tests/browsercoverage.test.ts +0 -134
- package/tests/browserpolyfills.test.ts +0 -124
- package/tests/build.mjs +0 -1260
- package/tests/clientauth.test.ts +0 -223
- package/tests/clitools.test.ts +0 -578
- package/tests/confirmgates.test.ts +0 -190
- package/tests/containerpack.test.ts +0 -143
- package/tests/controlflow.test.ts +0 -2120
- package/tests/coordination.test.ts +0 -345
- package/tests/copyscreen.test.ts +0 -139
- package/tests/core.test.ts +0 -417
- package/tests/costcert.test.ts +0 -95
- package/tests/crossbrowser.test.ts +0 -115
- package/tests/cspaudit.test.ts +0 -79
- package/tests/dashdone.test.ts +0 -615
- package/tests/datagrid.test.ts +0 -137
- package/tests/doccheck.test.ts +0 -107
- package/tests/dombatch.test.ts +0 -109
- package/tests/emulation.test.ts +0 -282
- package/tests/environments.test.ts +0 -259
- package/tests/evidenceviews.test.ts +0 -88
- package/tests/exporttools.test.ts +0 -185
- package/tests/firefoxprep.test.ts +0 -134
- package/tests/flowlibrary.test.ts +0 -242
- package/tests/flowrun.test.ts +0 -303
- package/tests/forensics.test.ts +0 -439
- package/tests/gateway.test.ts +0 -939
- package/tests/headlesslib.test.ts +0 -171
- package/tests/httpstream.test.ts +0 -277
- package/tests/immutablelog.test.ts +0 -157
- package/tests/inboundguard.test.ts +0 -177
- package/tests/lazyload.test.ts +0 -72
- package/tests/librarymodes.test.ts +0 -623
- package/tests/llm.test.ts +0 -696
- package/tests/manifest.test.ts +0 -226
- package/tests/maskinputs.test.ts +0 -150
- package/tests/matrixverify.test.ts +0 -111
- package/tests/mavenpack.test.ts +0 -75
- package/tests/mcpserver.test.ts +0 -704
- package/tests/memory.test.ts +0 -4155
- package/tests/memorycare.test.ts +0 -230
- package/tests/migrateplan.test.ts +0 -349
- package/tests/minimization.test.ts +0 -414
- package/tests/modelroute.test.ts +0 -112
- package/tests/multiagent.test.ts +0 -440
- package/tests/nativebridge.test.ts +0 -807
- package/tests/navigation.test.ts +0 -284
- package/tests/nugetpack.test.ts +0 -83
- package/tests/orchestration.test.ts +0 -449
- package/tests/originpolicy.test.ts +0 -405
- package/tests/outputcompare.test.ts +0 -97
- package/tests/package.test.ts +0 -91
- package/tests/pagebridge.test.ts +0 -5909
- package/tests/pentest.test.ts +0 -75
- package/tests/perfrecords.test.ts +0 -257
- package/tests/permdiff.test.ts +0 -85
- package/tests/phishguard.test.ts +0 -128
- package/tests/pickerviews.test.ts +0 -155
- package/tests/pipelines.test.ts +0 -381
- package/tests/planlint.test.ts +0 -240
- package/tests/planreview.test.ts +0 -383
- package/tests/platformtargets.test.ts +0 -56
- package/tests/policy.test.ts +0 -8284
- package/tests/portability.test.ts +0 -96
- package/tests/progress.test.ts +0 -1036
- package/tests/promptlibrary.test.ts +0 -116
- package/tests/protocol.test.ts +0 -4793
- package/tests/publishingpipeline.test.ts +0 -314
- package/tests/quickactions.test.ts +0 -164
- package/tests/readiness.test.ts +0 -78
- package/tests/recipes.test.ts +0 -163
- package/tests/redactshots.test.ts +0 -216
- package/tests/registernvidiakeys.ts +0 -51
- package/tests/relayserve.test.ts +0 -304
- package/tests/releasenotes.test.ts +0 -276
- package/tests/resilience.test.ts +0 -467
- package/tests/resourceaware.test.ts +0 -187
- package/tests/runbudget.test.ts +0 -125
- package/tests/runhistory.test.ts +0 -299
- package/tests/runreplay.test.ts +0 -113
- package/tests/runresume.test.ts +0 -184
- package/tests/runstate.test.ts +0 -288
- package/tests/runtimeadapters.test.ts +0 -251
- package/tests/safariskeleton.test.ts +0 -115
- package/tests/sandboxframe.test.ts +0 -196
- package/tests/sbom.test.ts +0 -105
- package/tests/secretvault.test.ts +0 -188
- package/tests/selcache.test.ts +0 -111
- package/tests/sessioninterface.test.ts +0 -791
- package/tests/sessions.test.ts +0 -591
- package/tests/siteprefs.test.ts +0 -228
- package/tests/snapshotdelta.test.ts +0 -165
- package/tests/statusviews.test.ts +0 -237
- package/tests/stepmeter.test.ts +0 -200
- package/tests/streamparse.test.ts +0 -163
- package/tests/stylepruning.test.ts +0 -90
- package/tests/surfaces.test.ts +0 -282
- package/tests/sweep.test.ts +0 -78
- package/tests/syncbridge.test.ts +0 -164
- package/tests/taskqueue.test.ts +0 -206
- package/tests/toolcalls.test.ts +0 -336
- package/tests/toolcatalog.test.ts +0 -201
- package/tests/torture-agent.test.ts +0 -706
- package/tests/torture-auth.test.ts +0 -461
- package/tests/torture-commands.test.ts +0 -895
- package/tests/torture-data.test.ts +0 -923
- package/tests/torture-environments.test.ts +0 -285
- package/tests/torture-export.test.ts +0 -263
- package/tests/torture-http.test.ts +0 -1379
- package/tests/torture-llm.test.ts +0 -808
- package/tests/torture-memory.test.ts +0 -1084
- package/tests/torture-plan.test.ts +0 -829
- package/tests/torture-run.test.ts +0 -626
- package/tests/torture-session.test.ts +0 -928
- package/tests/torture-swarm.test.ts +0 -1245
- package/tests/torture-tools.test.ts +0 -964
- package/tests/torture-workflow.test.ts +0 -2354
- package/tests/tourviews.test.ts +0 -52
- package/tests/transparency.test.ts +0 -147
- package/tests/trigger.test.ts +0 -941
- package/tests/v1sunset.test.ts +0 -428
- package/tests/virtlist.test.ts +0 -94
- package/tests/vision.test.ts +0 -540
- package/tests/vsixpack.test.ts +0 -145
- package/tests/webapi.test.ts +0 -523
- package/tests/workflow.test.ts +0 -1785
- package/tests/workfloweditor.test.ts +0 -759
- package/tests/xpipack.test.ts +0 -128
- package/tools.ts +0 -734
- package/types.ts +0 -9908
- package/umd.ts +0 -9
- package/utils.ts +0 -249
- package/version.ts +0 -8
- package/versionregistry.ts +0 -1310
- package/views.ts +0 -2153
- package/vitest.config.ts +0 -12
- package/web/App.tsx +0 -78
- package/web/README.md +0 -47
- package/web/capacitor.config.ts +0 -46
- package/web/const.ts +0 -19
- package/web/control.shell.tsx +0 -66
- package/web/dashboardpage.ts +0 -554
- package/web/db.ts +0 -493
- package/web/design.html +0 -541
- package/web/gatewayview/Gateway.tsx +0 -345
- package/web/gatewayview/config.ts +0 -1042
- package/web/gatewayview/definition.ts +0 -174
- package/web/icons.ts +0 -15
- package/web/optionspage.ts +0 -691
- package/web/popup.ts +0 -1651
- package/web/schema.prisma +0 -176
- package/web/sidepanel.ts +0 -7941
- package/web/sitemanifest.json +0 -13
- package/web/terminal-ui.tsx +0 -622
- package/web/transparencypage.ts +0 -541
- package/web/vite.config.ts +0 -39
- package/web/workspace.ts +0 -23
- package/workbenchmemory.ts +0 -104
- package/workbenchsession.ts +0 -304
- package/workflow.ts +0 -4495
- /package/{dist/bun.d.ts → bun.d.ts} +0 -0
- /package/{dist/cli.d.ts → cli.d.ts} +0 -0
- /package/{dist/companion.d.ts → companion.d.ts} +0 -0
- /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
- /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
- /package/{dist/deno.d.ts → deno.d.ts} +0 -0
- /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
- /package/{dist/headless.d.ts → headless.d.ts} +0 -0
- /package/{dist/http.d.ts → http.d.ts} +0 -0
- /package/{dist/index.d.ts → index.d.ts} +0 -0
- /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
- /package/{dist/memory.d.ts → memory.d.ts} +0 -0
- /package/{dist/node.d.ts → node.d.ts} +0 -0
- /package/{dist/pack.d.ts → pack.d.ts} +0 -0
- /package/{dist/policy.d.ts → policy.d.ts} +0 -0
- /package/{dist/progress.d.ts → progress.d.ts} +0 -0
- /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
- /package/{dist/server.d.ts → server.d.ts} +0 -0
- /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/auth.ts
DELETED
|
@@ -1,642 +0,0 @@
|
|
|
1
|
-
/** The auth module of the 1.1.88 consolidation: every correlated variation of the auth logic interned in this one file, so the module family carries one surface without duplicate variations. */
|
|
2
|
-
|
|
3
|
-
/* ── Merged from clientauth.ts: the 1.1.88 consolidation interns the correlated clientauth logic here, so no variation of the same file lives beside another. ── */
|
|
4
|
-
import type {
|
|
5
|
-
allowlistentry,
|
|
6
|
-
authchallenge,
|
|
7
|
-
authmethod,
|
|
8
|
-
clientidentity,
|
|
9
|
-
pairingcode,
|
|
10
|
-
sessiontoken,
|
|
11
|
-
tlsconfig,
|
|
12
|
-
tokenhash,
|
|
13
|
-
toolnamespace,
|
|
14
|
-
formpayload,
|
|
15
|
-
multipartpayload,
|
|
16
|
-
oauthflow,
|
|
17
|
-
revocationrule,
|
|
18
|
-
bridgepairingrecord,
|
|
19
|
-
relaytokenrecord,
|
|
20
|
-
} from "./types.js";
|
|
21
|
-
import { randomid } from "./memory.js";
|
|
22
|
-
import { toolnamespaces } from "./tools.js";
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Client auth of the 1.1.55 agent protocol part two.
|
|
26
|
-
* Every pairing, token, allowlist and handshake concern for remote clients lives in this file: the one time pairing codes with their single use state, the session tokens issued in exchange and stored only as sha-256 tokenhash values, the token verification on every frame with its scope limits and expiry, the client allowlist that refuses unknown fingerprints, the grant history of every scope change, the auth challenges with their single use nonces, the handshake completion that marks a client paired, the revocation that stays available to the user at any time, the token expiry that enforces the user configured lifetime and the auth failures that answer with one fixed json rpc message so the pairing state never leaks.
|
|
27
|
-
* The module stays pure: every value is a user choice with no code ceiling, and no endpoint, provider, certificate or key is ever hardcoded.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
/** The prefix every stored tokenhash carries so raw token values never persist. */
|
|
31
|
-
export const tokenhashprefix = "sha256:";
|
|
32
|
-
|
|
33
|
-
/** The documented default session token lifetime of one hour; any user configured window wins. */
|
|
34
|
-
export const defaulttokenlifetimems = 3_600_000;
|
|
35
|
-
|
|
36
|
-
/** The documented default pairing code window of five minutes; any user configured window wins. */
|
|
37
|
-
export const defaultpairinglifetimems = 300_000;
|
|
38
|
-
|
|
39
|
-
/** The documented default auth challenge window of two minutes; any user configured window wins. */
|
|
40
|
-
export const defaultchallengelifetimems = 120_000;
|
|
41
|
-
|
|
42
|
-
/** The fixed message every auth failure answers with; it names no client, token or pairing state so nothing leaks. */
|
|
43
|
-
export const authrefusedmessage = "The remote frame failed its authentication handshake.";
|
|
44
|
-
|
|
45
|
-
/** Digests one raw token into its stored tokenhash form: the sha-256 hex digest under the documented prefix, computed through the platform crypto seam so the raw value never persists. */
|
|
46
|
-
export async function tokenhashof(raw: string): Promise<tokenhash> {
|
|
47
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(raw));
|
|
48
|
-
return tokenhashprefix + [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Issues one pairing code for a one time client pairing: the human copied code, the granted namespaces, the issue time and the user configured expiry window. */
|
|
52
|
-
export function issuepairingcode(input: {
|
|
53
|
-
now: number;
|
|
54
|
-
scopes: toolnamespace[];
|
|
55
|
-
lifetime?: number;
|
|
56
|
-
code?: string;
|
|
57
|
-
}): pairingcode {
|
|
58
|
-
const scopes = input.scopes.filter((scope) => toolnamespaces.includes(scope));
|
|
59
|
-
return {
|
|
60
|
-
code: input.code ?? `DT-${randomid().replace(/-/g, "").slice(0, 8).toUpperCase()}`,
|
|
61
|
-
scopes,
|
|
62
|
-
issuedat: input.now,
|
|
63
|
-
expiresat: input.now + (input.lifetime ?? defaultpairinglifetimems),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Exchanges one pairing code for its granted scopes: the code must match, stay pending and sit inside its window while a used or expired code never pairs a second client. */
|
|
68
|
-
export function redeempairingcode(input: { codes: pairingcode[]; code: string; now: number }): {
|
|
69
|
-
code?: pairingcode;
|
|
70
|
-
reason?: string;
|
|
71
|
-
} {
|
|
72
|
-
const match = input.codes.find((candidate) => candidate.code === input.code);
|
|
73
|
-
if (match === undefined) return { reason: authrefusedmessage };
|
|
74
|
-
if (match.usedat !== undefined)
|
|
75
|
-
return { reason: "The pairing code was already used once and never pairs a second client." };
|
|
76
|
-
if (input.now >= match.expiresat) return { reason: "The pairing code expired before the exchange completed." };
|
|
77
|
-
return { code: { ...match, usedat: input.now } };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Issues one session token for a paired client: the raw token leaves exactly once in the answer while the record persists only its tokenhash form, the scopes, the issue time and the user configured expiry. */
|
|
81
|
-
export async function issuetoken(input: {
|
|
82
|
-
clientid: string;
|
|
83
|
-
scopes: toolnamespace[];
|
|
84
|
-
now: number;
|
|
85
|
-
lifetime?: number;
|
|
86
|
-
id?: string;
|
|
87
|
-
raw?: string;
|
|
88
|
-
}): Promise<{ token: sessiontoken; raw: string }> {
|
|
89
|
-
const raw = input.raw ?? `${randomid()}.${randomid()}`;
|
|
90
|
-
const token: sessiontoken = {
|
|
91
|
-
id: input.id ?? randomid(),
|
|
92
|
-
clientid: input.clientid,
|
|
93
|
-
hash: await tokenhashof(raw),
|
|
94
|
-
scopes: input.scopes.filter((scope) => toolnamespaces.includes(scope)),
|
|
95
|
-
issuedat: input.now,
|
|
96
|
-
expiresat: input.now + (input.lifetime ?? defaulttokenlifetimems),
|
|
97
|
-
};
|
|
98
|
-
return { token, raw };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Validates one session token on every frame: the digest of the presented raw token must match a stored, unrevoked, unexpired record; every failure answers with the fixed message so the pairing state never leaks. */
|
|
102
|
-
export async function verifytoken(input: {
|
|
103
|
-
tokens: sessiontoken[];
|
|
104
|
-
raw: string;
|
|
105
|
-
now: number;
|
|
106
|
-
}): Promise<{ token?: sessiontoken; reason?: string }> {
|
|
107
|
-
const hash = await tokenhashof(input.raw);
|
|
108
|
-
const match = input.tokens.find((candidate) => candidate.hash === hash);
|
|
109
|
-
if (match === undefined) return { reason: authrefusedmessage };
|
|
110
|
-
if (match.revokedat !== undefined) return { reason: authrefusedmessage };
|
|
111
|
-
if (input.now >= match.expiresat) return { reason: authrefusedmessage };
|
|
112
|
-
return { token: match };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Revokes every session token of one client on demand; the revocation stays available to the user at any time and the records stay for the audit trail. */
|
|
116
|
-
export function revokeclient(tokens: sessiontoken[], clientid: string, now: number): sessiontoken[] {
|
|
117
|
-
return tokens.map((token) =>
|
|
118
|
-
token.clientid === clientid && token.revokedat === undefined ? { ...token, revokedat: now } : token,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Enforces the configured token lifetime: tokens past their expiry leave the live set while their records stay for the audit trail. */
|
|
123
|
-
export function expiretokens(input: { tokens: sessiontoken[]; now: number }): {
|
|
124
|
-
live: sessiontoken[];
|
|
125
|
-
expired: sessiontoken[];
|
|
126
|
-
} {
|
|
127
|
-
const expired = input.tokens.filter((token) => token.revokedat === undefined && input.now >= token.expiresat);
|
|
128
|
-
return { live: input.tokens.filter((token) => !expired.includes(token)), expired };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Checks the client allowlist: a fingerprint must carry an entry and, when a namespace is asked, the entry must grant it. */
|
|
132
|
-
export function checkallowlist(input: { entries: allowlistentry[]; fingerprint: string; namespace?: toolnamespace }): {
|
|
133
|
-
allowed: boolean;
|
|
134
|
-
reason?: string;
|
|
135
|
-
} {
|
|
136
|
-
const entry = input.entries.find((candidate) => candidate.fingerprint === input.fingerprint);
|
|
137
|
-
if (entry === undefined)
|
|
138
|
-
return {
|
|
139
|
-
allowed: false,
|
|
140
|
-
reason: `The client fingerprint ${input.fingerprint} is not on the allowlist and is refused.`,
|
|
141
|
-
};
|
|
142
|
-
if (input.namespace !== undefined && !entry.namespaces.includes(input.namespace))
|
|
143
|
-
return { allowed: false, reason: `The allowlist entry ${entry.displayname} grants no ${input.namespace} tools.` };
|
|
144
|
-
return { allowed: true };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Grants or updates one allowlist entry with its grant history: every scope change lands in the history with its actor and plain language change. */
|
|
148
|
-
export function grantallowlistentry(input: {
|
|
149
|
-
entries: allowlistentry[];
|
|
150
|
-
identity: clientidentity;
|
|
151
|
-
namespaces: toolnamespace[];
|
|
152
|
-
actor: string;
|
|
153
|
-
now: number;
|
|
154
|
-
}): allowlistentry[] {
|
|
155
|
-
const scopes = input.namespaces.filter((scope) => toolnamespaces.includes(scope));
|
|
156
|
-
const existing = input.entries.find((entry) => entry.fingerprint === input.identity.fingerprint);
|
|
157
|
-
if (existing === undefined) {
|
|
158
|
-
return [
|
|
159
|
-
{
|
|
160
|
-
fingerprint: input.identity.fingerprint,
|
|
161
|
-
displayname: input.identity.displayname,
|
|
162
|
-
namespaces: scopes,
|
|
163
|
-
grantedat: input.now,
|
|
164
|
-
history: [
|
|
165
|
-
{
|
|
166
|
-
at: input.now,
|
|
167
|
-
actor: input.actor,
|
|
168
|
-
change: `Granted the ${scopes.length > 0 ? scopes.join(", ") : "no"} namespaces.`,
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
},
|
|
172
|
-
...input.entries,
|
|
173
|
-
];
|
|
174
|
-
}
|
|
175
|
-
return input.entries.map((entry) =>
|
|
176
|
-
entry.fingerprint !== input.identity.fingerprint
|
|
177
|
-
? entry
|
|
178
|
-
: {
|
|
179
|
-
...entry,
|
|
180
|
-
displayname: input.identity.displayname,
|
|
181
|
-
namespaces: scopes,
|
|
182
|
-
history: [
|
|
183
|
-
{
|
|
184
|
-
at: input.now,
|
|
185
|
-
actor: input.actor,
|
|
186
|
-
change: `Rescoped to ${scopes.length > 0 ? scopes.join(", ") : "no"} namespaces.`,
|
|
187
|
-
},
|
|
188
|
-
...entry.history,
|
|
189
|
-
],
|
|
190
|
-
},
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Issues one auth challenge to a new client: the single use nonce, the method it authenticates and the user configured expiry window. */
|
|
195
|
-
export function issuechallenge(input: {
|
|
196
|
-
method: authmethod;
|
|
197
|
-
now: number;
|
|
198
|
-
lifetime?: number;
|
|
199
|
-
nonce?: string;
|
|
200
|
-
}): authchallenge {
|
|
201
|
-
return {
|
|
202
|
-
nonce: input.nonce ?? randomid(),
|
|
203
|
-
method: input.method,
|
|
204
|
-
issuedat: input.now,
|
|
205
|
-
expiresat: input.now + (input.lifetime ?? defaultchallengelifetimems),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Completes the auth handshake: the echoed nonce must match the live challenge and the presented token must verify; success marks the client paired while every failure answers with the fixed message. */
|
|
210
|
-
export async function verifyauth(input: {
|
|
211
|
-
challenge: authchallenge;
|
|
212
|
-
nonce: string;
|
|
213
|
-
tokens: sessiontoken[];
|
|
214
|
-
rawtoken: string;
|
|
215
|
-
now: number;
|
|
216
|
-
}): Promise<{ verified: boolean; clientid?: string; reason?: string }> {
|
|
217
|
-
if (input.nonce !== input.challenge.nonce) return { verified: false, reason: authrefusedmessage };
|
|
218
|
-
if (input.now >= input.challenge.expiresat)
|
|
219
|
-
return { verified: false, reason: "The auth challenge expired before the handshake completed." };
|
|
220
|
-
const checked = await verifytoken({ tokens: input.tokens, raw: input.rawtoken, now: input.now });
|
|
221
|
-
if (checked.token === undefined) return { verified: false, reason: authrefusedmessage };
|
|
222
|
-
return { verified: true, clientid: checked.token.clientid };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Limits one tool call to the namespaces the token scopes grant: an unknown namespace fails fast while a known but ungranted namespace stays a consent refusal. */
|
|
226
|
-
export function scopecheck(
|
|
227
|
-
token: sessiontoken | undefined,
|
|
228
|
-
namespace: toolnamespace | undefined,
|
|
229
|
-
): { allowed: boolean; fast?: boolean; reason?: string } {
|
|
230
|
-
if (namespace === undefined)
|
|
231
|
-
return { allowed: false, fast: true, reason: "The tool call names no reviewed namespace." };
|
|
232
|
-
if (token === undefined) return { allowed: false, reason: "The tool call carries no verified session token." };
|
|
233
|
-
if (!token.scopes.includes(namespace))
|
|
234
|
-
return { allowed: false, reason: `The session token grants no ${namespace} tools.` };
|
|
235
|
-
return { allowed: true };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** Reports the tls termination state of one transport config for the remote status view: the mode, the certificate requirement and whether the peer was verified. */
|
|
239
|
-
export function tlsstateof(tls: tlsconfig): { mode: string; certificaterequired: boolean; verified: boolean } {
|
|
240
|
-
return {
|
|
241
|
-
mode: tls.mode,
|
|
242
|
-
certificaterequired: tls.mode === "required" || tls.certificatefingerprint !== undefined,
|
|
243
|
-
verified: tls.verifiedat !== undefined,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/* ── Merged from netauth.ts: the 1.1.88 consolidation interns the correlated netauth logic here, so no variation of the same file lives beside another. ── */
|
|
248
|
-
/** Correlated auth and upload logics of the 1.1.44 family: reviewed oauth flows with code capture, token exchange and refresh, token revocation, api key entries, urlencoded form payloads and streamed multipart uploads. */
|
|
249
|
-
|
|
250
|
-
/** Parses one reviewed oauth flow from step options: provider, authorize url, token url, scopes and the redirect origin the code capture watches. */
|
|
251
|
-
export function oauthflowof(value: unknown): oauthflow | undefined {
|
|
252
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
253
|
-
const options = value as Record<string, unknown>;
|
|
254
|
-
if (typeof options.provider !== "string" || !options.provider.trim()) return undefined;
|
|
255
|
-
if (typeof options.authorizeurl !== "string" || !options.authorizeurl.trim()) return undefined;
|
|
256
|
-
if (typeof options.tokenurl !== "string" || !options.tokenurl.trim()) return undefined;
|
|
257
|
-
if (
|
|
258
|
-
!Array.isArray(options.scopes) ||
|
|
259
|
-
options.scopes.length === 0 ||
|
|
260
|
-
!options.scopes.every((item): item is string => typeof item === "string" && item.trim().length > 0)
|
|
261
|
-
)
|
|
262
|
-
return undefined;
|
|
263
|
-
if (typeof options.redirectorigin !== "string" || !options.redirectorigin.trim()) return undefined;
|
|
264
|
-
return {
|
|
265
|
-
provider: options.provider.trim(),
|
|
266
|
-
authorizeurl: options.authorizeurl.trim(),
|
|
267
|
-
tokenurl: options.tokenurl.trim(),
|
|
268
|
-
scopes: options.scopes.map((item) => item.trim()),
|
|
269
|
-
redirectorigin: options.redirectorigin.trim(),
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** Builds the provider consent page url of one reviewed oauth flow with the response type code, the redirect origin, the scopes and the run scoped state token. */
|
|
274
|
-
export function authorizeurl(flow: oauthflow, state: string): string {
|
|
275
|
-
const url = new URL(flow.authorizeurl);
|
|
276
|
-
url.searchParams.set("response_type", "code");
|
|
277
|
-
url.searchParams.set("redirect_uri", flow.redirectorigin);
|
|
278
|
-
url.searchParams.set("scope", flow.scopes.join(" "));
|
|
279
|
-
url.searchParams.set("state", state);
|
|
280
|
-
return url.toString();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** Captures the oauth code from one redirect url: the url must land on the granted redirect origin with the matching state token and carry the code parameter; error redirects are refused with their reason. */
|
|
284
|
-
export function capturecode(
|
|
285
|
-
url: string,
|
|
286
|
-
redirectorigin: string,
|
|
287
|
-
state: string,
|
|
288
|
-
): { code: string; error?: undefined } | { error: string } {
|
|
289
|
-
let parsed: URL;
|
|
290
|
-
try {
|
|
291
|
-
parsed = new URL(url);
|
|
292
|
-
} catch {
|
|
293
|
-
return { error: "The redirect url does not parse for the code capture." };
|
|
294
|
-
}
|
|
295
|
-
const granted = redirectorigin.includes("/", redirectorigin.indexOf("://") + 3)
|
|
296
|
-
? `${parsed.origin}${parsed.pathname}`.startsWith(redirectorigin)
|
|
297
|
-
: parsed.origin === redirectorigin;
|
|
298
|
-
if (!granted)
|
|
299
|
-
return { error: `The redirect landed on ${parsed.origin} outside the granted redirect origin ${redirectorigin}.` };
|
|
300
|
-
const returned = parsed.searchParams.get("state");
|
|
301
|
-
if (returned !== state) return { error: "The redirect state token does not match the reviewed flow." };
|
|
302
|
-
const error = parsed.searchParams.get("error");
|
|
303
|
-
if (error) return { error: `The provider refused the flow: ${error}.` };
|
|
304
|
-
const code = parsed.searchParams.get("code");
|
|
305
|
-
if (!code) return { error: "The redirect carries no authorization code." };
|
|
306
|
-
return { code };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/** Parses one token endpoint response body: access token, refresh token, expiry seconds and granted scopes; a body without a token string parses to nothing. */
|
|
310
|
-
export function parsetokens(
|
|
311
|
-
body: string,
|
|
312
|
-
): { accesstoken?: string; refreshtoken?: string; expiresin?: number; scopes?: string[] } | undefined {
|
|
313
|
-
let parsed: unknown;
|
|
314
|
-
try {
|
|
315
|
-
parsed = JSON.parse(body);
|
|
316
|
-
} catch {
|
|
317
|
-
return undefined;
|
|
318
|
-
}
|
|
319
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
320
|
-
const record = parsed as Record<string, unknown>;
|
|
321
|
-
const tokens: { accesstoken?: string; refreshtoken?: string; expiresin?: number; scopes?: string[] } = {};
|
|
322
|
-
if (typeof record["access_token"] === "string" && record["access_token"]) tokens.accesstoken = record["access_token"];
|
|
323
|
-
if (typeof record["refresh_token"] === "string" && record["refresh_token"])
|
|
324
|
-
tokens.refreshtoken = record["refresh_token"];
|
|
325
|
-
if (typeof record["expires_in"] === "number" && Number.isFinite(record["expires_in"]) && record["expires_in"] >= 0)
|
|
326
|
-
tokens.expiresin = record["expires_in"];
|
|
327
|
-
if (typeof record.scope === "string" && record.scope.trim()) tokens.scopes = record.scope.trim().split(/\s+/);
|
|
328
|
-
if (tokens.accesstoken === undefined && tokens.refreshtoken === undefined) return undefined;
|
|
329
|
-
return tokens;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/** Builds the token endpoint request of one reviewed flow: the authorization code exchange with the redirect origin or the refresh token grant that stays scoped to the provider token origin. */
|
|
333
|
-
export function tokenrequest(
|
|
334
|
-
flow: oauthflow,
|
|
335
|
-
input: { code?: string; refreshtoken?: string },
|
|
336
|
-
): { url: string; body: string } {
|
|
337
|
-
if (input.refreshtoken !== undefined)
|
|
338
|
-
return {
|
|
339
|
-
url: flow.tokenurl,
|
|
340
|
-
body: urlencodeform([
|
|
341
|
-
{ name: "grant_type", value: "refresh_token" },
|
|
342
|
-
{ name: "refresh_token", value: input.refreshtoken },
|
|
343
|
-
]),
|
|
344
|
-
};
|
|
345
|
-
return {
|
|
346
|
-
url: flow.tokenurl,
|
|
347
|
-
body: urlencodeform([
|
|
348
|
-
{ name: "grant_type", value: "authorization_code" },
|
|
349
|
-
{ name: "code", value: input.code ?? "" },
|
|
350
|
-
{ name: "redirect_uri", value: flow.redirectorigin },
|
|
351
|
-
]),
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/** Parses one reviewed token revocation rule from step options: the token ids, the reason and the revoke time. */
|
|
356
|
-
export function revocationruleof(value: unknown): revocationrule | undefined {
|
|
357
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
358
|
-
const options = value as Record<string, unknown>;
|
|
359
|
-
if (
|
|
360
|
-
!Array.isArray(options.tokenids) ||
|
|
361
|
-
options.tokenids.length === 0 ||
|
|
362
|
-
!options.tokenids.every((item): item is string => typeof item === "string" && item.trim().length > 0)
|
|
363
|
-
)
|
|
364
|
-
return undefined;
|
|
365
|
-
if (typeof options.reason !== "string" || !options.reason.trim()) return undefined;
|
|
366
|
-
return {
|
|
367
|
-
tokenids: options.tokenids.map((item) => item.trim()),
|
|
368
|
-
reason: options.reason.trim(),
|
|
369
|
-
revokedat: Date.now(),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/** Parses one reviewed urlencoded form payload from step options: the target url, the field list and the optional encoding name of the 1.1.76 family; an absent encoding keeps the urlencoded grammar. */
|
|
374
|
-
export function formpayloadof(value: unknown): formpayload | undefined {
|
|
375
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
376
|
-
const options = value as Record<string, unknown>;
|
|
377
|
-
if (typeof options.url !== "string" || !options.url.trim()) return undefined;
|
|
378
|
-
if (!Array.isArray(options.fields) || options.fields.length === 0) return undefined;
|
|
379
|
-
const fields: Array<{ name: string; value: string }> = [];
|
|
380
|
-
for (const item of options.fields) {
|
|
381
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) return undefined;
|
|
382
|
-
const field = item as Record<string, unknown>;
|
|
383
|
-
if (typeof field.name !== "string" || !field.name.trim()) return undefined;
|
|
384
|
-
if (typeof field.value !== "string") return undefined;
|
|
385
|
-
fields.push({ name: field.name.trim(), value: field.value });
|
|
386
|
-
}
|
|
387
|
-
return {
|
|
388
|
-
url: options.url.trim(),
|
|
389
|
-
fields,
|
|
390
|
-
...(typeof options.encoding === "string" && options.encoding.trim() ? { encoding: options.encoding.trim() } : {}),
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/** Encodes one reviewed field list with the application/x-www-form-urlencoded grammar: names and values percent encode with the form encoding set and pairs join with ampersands. */
|
|
395
|
-
export function urlencodeform(fields: Array<{ name: string; value: string }>): string {
|
|
396
|
-
return fields.map((field) => `${formencode(field.name)}=${formencode(field.value)}`).join("&");
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/** Percent encodes one form component with the urlencoded grammar over its UTF-8 bytes. */
|
|
400
|
-
function formencode(value: string): string {
|
|
401
|
-
const bytes = [...new TextEncoder().encode(value)];
|
|
402
|
-
return bytes
|
|
403
|
-
.map((byte) =>
|
|
404
|
-
(byte >= 0x41 && byte <= 0x5a) ||
|
|
405
|
-
(byte >= 0x61 && byte <= 0x7a) ||
|
|
406
|
-
(byte >= 0x30 && byte <= 0x39) ||
|
|
407
|
-
byte === 0x2d ||
|
|
408
|
-
byte === 0x5f ||
|
|
409
|
-
byte === 0x2e ||
|
|
410
|
-
byte === 0x7e
|
|
411
|
-
? String.fromCharCode(byte)
|
|
412
|
-
: `%${byte.toString(16).toUpperCase().padStart(2, "0")}`,
|
|
413
|
-
)
|
|
414
|
-
.join("");
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/** Parses one reviewed multipart upload payload from step options: the target url, the field list and the reviewed files. */
|
|
418
|
-
export function multipartpayloadof(value: unknown): multipartpayload | undefined {
|
|
419
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
420
|
-
const options = value as Record<string, unknown>;
|
|
421
|
-
if (typeof options.url !== "string" || !options.url.trim()) return undefined;
|
|
422
|
-
if (!Array.isArray(options.files) || options.files.length === 0) return undefined;
|
|
423
|
-
const fields: Array<{ name: string; value: string }> = [];
|
|
424
|
-
for (const item of Array.isArray(options.fields) ? options.fields : []) {
|
|
425
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) return undefined;
|
|
426
|
-
const field = item as Record<string, unknown>;
|
|
427
|
-
if (typeof field.name !== "string" || !field.name.trim()) return undefined;
|
|
428
|
-
if (typeof field.value !== "string") return undefined;
|
|
429
|
-
fields.push({ name: field.name.trim(), value: field.value });
|
|
430
|
-
}
|
|
431
|
-
const files: multipartpayload["files"] = [];
|
|
432
|
-
for (const item of options.files) {
|
|
433
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) return undefined;
|
|
434
|
-
const file = item as Record<string, unknown>;
|
|
435
|
-
if (typeof file.name !== "string" || !file.name.trim()) return undefined;
|
|
436
|
-
if (typeof file.filename !== "string" || !file.filename.trim()) return undefined;
|
|
437
|
-
if (typeof file.mime !== "string" || !file.mime.trim()) return undefined;
|
|
438
|
-
if (typeof file.content !== "string") return undefined;
|
|
439
|
-
if (file.reviewed !== true) return undefined;
|
|
440
|
-
files.push({
|
|
441
|
-
name: file.name.trim(),
|
|
442
|
-
filename: file.filename.trim(),
|
|
443
|
-
mime: file.mime.trim(),
|
|
444
|
-
content: file.content,
|
|
445
|
-
reviewed: true,
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
const payload: multipartpayload = {
|
|
449
|
-
url: options.url.trim(),
|
|
450
|
-
fields,
|
|
451
|
-
files,
|
|
452
|
-
...(typeof options.boundary === "string" && options.boundary.trim() ? { boundary: options.boundary.trim() } : {}),
|
|
453
|
-
};
|
|
454
|
-
return payload;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/** Builds one locally unique multipart boundary token. */
|
|
458
|
-
function newboundary(): string {
|
|
459
|
-
return `----devthink${Math.random().toString(16).slice(2)}${Date.now().toString(16)}`;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/** Streams one reviewed multipart payload as ordered chunks: one chunk per field part, per file part and the closing boundary, so large uploads move chunk by chunk without buffering the whole payload. */
|
|
463
|
-
export function multipartchunks(payload: multipartpayload): { chunks: string[]; boundary: string; bytes: number } {
|
|
464
|
-
const boundary = payload.boundary ?? newboundary();
|
|
465
|
-
const chunks: string[] = [];
|
|
466
|
-
for (const field of payload.fields)
|
|
467
|
-
chunks.push(`--${boundary}\r\ncontent-disposition: form-data; name="${field.name}"\r\n\r\n${field.value}\r\n`);
|
|
468
|
-
for (const file of payload.files)
|
|
469
|
-
chunks.push(
|
|
470
|
-
`--${boundary}\r\ncontent-disposition: form-data; name="${file.name}"; filename="${file.filename}"\r\ncontent-type: ${file.mime}\r\n\r\n${file.content}\r\n`,
|
|
471
|
-
);
|
|
472
|
-
chunks.push(`--${boundary}--\r\n`);
|
|
473
|
-
return { chunks, boundary, bytes: chunks.reduce((total, chunk) => total + chunk.length, 0) };
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/* ── Merged from sharedauth.ts: the 1.1.88 consolidation interns the correlated sharedauth logic here, so no variation of the same file lives beside another. ── */
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Shared auth of the 1.1.82 site integration family.
|
|
480
|
-
* Every pairing and token concern of the site bridge lives in this one pure module, and it extends the reviewed clientauth family instead of duplicating it: the pairing code mint inside the extension options reuses the one time pairing code record of the clientauth family bound to the relay origin the user typed; the expiry countdown reads the code window for the options page display; the code exchange redeems the clientauth record once and answers the session token exactly once while the record persists only its sha-256 hash through the clientauth digest; the rotation on every reconnect issues a fresh token that keeps the id of the token it replaced and revokes the old one; the token storage stays scoped to the relay origin so one relay never reads the sessions of another; and the one click revocation stamps every record with its revocation time. The pairing scopes stay the browser namespace because the site never gains an executor: the token authenticates frames only.
|
|
481
|
-
* Example: `const pairing = mintbridgepairing({ origin, now }); const exchange = await exchangebridgepairing({ records: [pairing], origin, code: pairing.code.code, sessionid, now }); const rotated = await rotatebridgetoken({ tokens: [exchange.record], sessionid, origin, now });`
|
|
482
|
-
*/
|
|
483
|
-
|
|
484
|
-
/** The pairing scopes the bridge grants: the browser namespace only, because a paired site reads and answers frames while the extension stays the only executor. */
|
|
485
|
-
export const bridgepairingscopes: toolnamespace[] = ["browser"];
|
|
486
|
-
|
|
487
|
-
/** Mints one bridge pairing code inside the extension options: the clientauth pairing code record bound to the relay origin the user typed, with the lifetime staying the user's choice over the documented five minute window. */
|
|
488
|
-
export function mintbridgepairing(input: {
|
|
489
|
-
origin: string;
|
|
490
|
-
now: number;
|
|
491
|
-
lifetime?: number;
|
|
492
|
-
code?: string;
|
|
493
|
-
}): bridgepairingrecord {
|
|
494
|
-
const origin = input.origin.trim();
|
|
495
|
-
if (origin === "")
|
|
496
|
-
throw new Error(
|
|
497
|
-
"The bridge pairing names its relay origin; the origin is the user configured relay url the code binds to.",
|
|
498
|
-
);
|
|
499
|
-
return {
|
|
500
|
-
origin,
|
|
501
|
-
code: issuepairingcode({
|
|
502
|
-
now: input.now,
|
|
503
|
-
scopes: [...bridgepairingscopes],
|
|
504
|
-
...(input.lifetime !== undefined ? { lifetime: input.lifetime } : {}),
|
|
505
|
-
...(input.code !== undefined ? { code: input.code } : {}),
|
|
506
|
-
}),
|
|
507
|
-
at: input.now,
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/** Reads the expiry countdown of one bridge pairing code for the options page: the seconds left inside the window, the expired flag once the window passed and the human label the options page renders. */
|
|
512
|
-
export function pairingcountdown(
|
|
513
|
-
record: bridgepairingrecord,
|
|
514
|
-
now: number,
|
|
515
|
-
): { secondsleft: number; expired: boolean; label: string } {
|
|
516
|
-
const secondsleft = Math.max(0, Math.ceil((record.code.expiresat - now) / 1000));
|
|
517
|
-
const expired = now >= record.code.expiresat || record.code.usedat !== undefined;
|
|
518
|
-
const minutes = Math.floor(secondsleft / 60);
|
|
519
|
-
const seconds = secondsleft % 60;
|
|
520
|
-
const label = expired
|
|
521
|
-
? `The pairing code ${record.code.code} expired.`
|
|
522
|
-
: `The pairing code ${record.code.code} expires in ${minutes}m ${String(seconds).padStart(2, "0")}s.`;
|
|
523
|
-
return { secondsleft, expired, label };
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/** Exchanges one bridge pairing code for its relay session token: the code must match a record of the same relay origin, stay pending and sit inside its window (the clientauth redemption enforces the single use state), and the answer carries the raw token exactly once while the record persists only its sha-256 hash scoped to the origin and session. */
|
|
527
|
-
export async function exchangebridgepairing(input: {
|
|
528
|
-
records: bridgepairingrecord[];
|
|
529
|
-
origin: string;
|
|
530
|
-
code: string;
|
|
531
|
-
sessionid: string;
|
|
532
|
-
now: number;
|
|
533
|
-
lifetime?: number;
|
|
534
|
-
}): Promise<{ record?: relaytokenrecord; raw?: string; used?: pairingcode; reason?: string }> {
|
|
535
|
-
const origin = input.origin.trim();
|
|
536
|
-
if (origin === "") return { reason: "The pairing exchange names its relay origin." };
|
|
537
|
-
const sessionid = input.sessionid.trim();
|
|
538
|
-
if (sessionid === "") return { reason: "The pairing exchange names its session." };
|
|
539
|
-
const match = input.records.find((record) => record.origin === origin && record.code.code === input.code.trim());
|
|
540
|
-
if (match === undefined) return { reason: `No pending pairing code of the origin ${origin} matches the typed code.` };
|
|
541
|
-
const redemption = redeempairingcode({
|
|
542
|
-
codes: input.records.map((record) => record.code),
|
|
543
|
-
code: input.code.trim(),
|
|
544
|
-
now: input.now,
|
|
545
|
-
});
|
|
546
|
-
if (redemption.reason !== undefined) return { reason: redemption.reason };
|
|
547
|
-
if (redemption.code === undefined) return { reason: "The pairing code redeemed no session." };
|
|
548
|
-
const raw = `${randomid()}.${randomid()}`;
|
|
549
|
-
const record: relaytokenrecord = {
|
|
550
|
-
id: randomid(),
|
|
551
|
-
sessionid,
|
|
552
|
-
origin,
|
|
553
|
-
hash: await tokenhashof(raw),
|
|
554
|
-
issuedat: input.now,
|
|
555
|
-
...(input.lifetime !== undefined && Number.isFinite(input.lifetime) && input.lifetime > 0
|
|
556
|
-
? { expiresat: input.now + input.lifetime }
|
|
557
|
-
: {}),
|
|
558
|
-
};
|
|
559
|
-
return { record, raw, used: redemption.code };
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/** Rotates the relay session token on one reconnect: the fresh raw token answers exactly once, the new record keeps the id of the token it replaced through the rotation chain, and the replaced record stamps its revocation time while its hash history stays for the audit trail. */
|
|
563
|
-
export async function rotatebridgetoken(input: {
|
|
564
|
-
tokens: relaytokenrecord[];
|
|
565
|
-
sessionid: string;
|
|
566
|
-
origin: string;
|
|
567
|
-
now: number;
|
|
568
|
-
lifetime?: number;
|
|
569
|
-
raw?: string;
|
|
570
|
-
}): Promise<{ tokens: relaytokenrecord[]; raw: string; record: relaytokenrecord }> {
|
|
571
|
-
const sessionid = input.sessionid.trim();
|
|
572
|
-
if (sessionid === "") throw new Error("The token rotation names its session.");
|
|
573
|
-
const origin = input.origin.trim();
|
|
574
|
-
if (origin === "") throw new Error("The token rotation names its relay origin.");
|
|
575
|
-
const current = input.tokens
|
|
576
|
-
.filter((token) => token.sessionid === sessionid && token.origin === origin && token.revokedat === undefined)
|
|
577
|
-
.pop();
|
|
578
|
-
const raw = input.raw ?? `${randomid()}.${randomid()}`;
|
|
579
|
-
const record: relaytokenrecord = {
|
|
580
|
-
id: randomid(),
|
|
581
|
-
sessionid,
|
|
582
|
-
origin,
|
|
583
|
-
hash: await tokenhashof(raw),
|
|
584
|
-
issuedat: input.now,
|
|
585
|
-
...(current !== undefined ? { rotatedfrom: current.id } : {}),
|
|
586
|
-
...(input.lifetime !== undefined && Number.isFinite(input.lifetime) && input.lifetime > 0
|
|
587
|
-
? { expiresat: input.now + input.lifetime }
|
|
588
|
-
: {}),
|
|
589
|
-
};
|
|
590
|
-
const tokens = input.tokens.map((token) => (token.id === current?.id ? { ...token, revokedat: input.now } : token));
|
|
591
|
-
return { tokens: [...tokens, record], raw, record };
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/** Verifies one relay session token on an incoming frame: the sha-256 digest of the raw token must match a record of the same relay origin and session that stays unrevoked and inside its expiry, so a frame of another origin, another session, a revoked or an expired token never passes. */
|
|
595
|
-
export async function verifybridgetoken(input: {
|
|
596
|
-
tokens: relaytokenrecord[];
|
|
597
|
-
origin: string;
|
|
598
|
-
sessionid: string;
|
|
599
|
-
raw: string;
|
|
600
|
-
now: number;
|
|
601
|
-
}): Promise<{ ok: boolean; reason?: string; token?: relaytokenrecord }> {
|
|
602
|
-
const hash = await tokenhashof(input.raw);
|
|
603
|
-
const match = input.tokens.find((token) => token.hash === hash);
|
|
604
|
-
if (match === undefined) return { ok: false, reason: "The frame token matches no pairing of this bridge." };
|
|
605
|
-
if (match.origin !== input.origin)
|
|
606
|
-
return {
|
|
607
|
-
ok: false,
|
|
608
|
-
reason: "The frame token stays scoped to its relay origin and never crosses to another relay.",
|
|
609
|
-
};
|
|
610
|
-
if (match.sessionid !== input.sessionid)
|
|
611
|
-
return { ok: false, reason: "The frame token stays scoped to its session and never crosses to another session." };
|
|
612
|
-
if (match.revokedat !== undefined)
|
|
613
|
-
return {
|
|
614
|
-
ok: false,
|
|
615
|
-
reason: "The frame token carries its revocation; a rotated or revoked token never passes again.",
|
|
616
|
-
};
|
|
617
|
-
if (match.expiresat !== undefined && input.now >= match.expiresat)
|
|
618
|
-
return { ok: false, reason: "The frame token expired before the frame arrived." };
|
|
619
|
-
return { ok: true, token: match };
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/** Reads the origin scoped storage key of the relay token records: every origin keeps its own token family so one relay never reads the sessions of another. */
|
|
623
|
-
export function tokenscopedkey(origin: string): string {
|
|
624
|
-
const trimmed = origin.trim();
|
|
625
|
-
if (trimmed === "") throw new Error("The token storage key names its relay origin.");
|
|
626
|
-
return `bridgetokens:${trimmed}`;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/** Revokes every relay session token with one click: every live record stamps its revocation time while the records themselves stay for the audit trail, so the socket and the pairing die instantly and no frame passes afterwards. */
|
|
630
|
-
export function revokeallsessions(tokens: relaytokenrecord[], now: number): relaytokenrecord[] {
|
|
631
|
-
return tokens.map((token) => (token.revokedat === undefined ? { ...token, revokedat: now } : token));
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/** Reads the live token count of one origin: the unrevoked records the options page lists beside the revoke all button. */
|
|
635
|
-
export function livetokensof(tokens: relaytokenrecord[], origin: string, now: number): relaytokenrecord[] {
|
|
636
|
-
return tokens.filter(
|
|
637
|
-
(token) =>
|
|
638
|
-
token.origin === origin &&
|
|
639
|
-
token.revokedat === undefined &&
|
|
640
|
-
(token.expiresat === undefined || now < token.expiresat),
|
|
641
|
-
);
|
|
642
|
-
}
|