@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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
closeheadlesssession,
|
|
4
|
+
headlessauditread,
|
|
5
|
+
headlessgatewait,
|
|
6
|
+
headlessmemory,
|
|
7
|
+
headlessplan,
|
|
8
|
+
headlesspolicyvalidate,
|
|
9
|
+
headlessrun,
|
|
10
|
+
headlesssessionview,
|
|
11
|
+
headlesssubscribe,
|
|
12
|
+
headlesstelemetryposture,
|
|
13
|
+
openheadlesssession,
|
|
14
|
+
remoteattachframes,
|
|
15
|
+
} from "../headless.js";
|
|
16
|
+
import { appendlogentry, openrunlog, sealrunlog } from "../security.js";
|
|
17
|
+
import { protocolversion } from "../types.js";
|
|
18
|
+
import type { flowrungate, planfile } from "../types.js";
|
|
19
|
+
|
|
20
|
+
const now = 1_800_000_000_000;
|
|
21
|
+
|
|
22
|
+
function plan(): planfile {
|
|
23
|
+
return {
|
|
24
|
+
version: protocolversion,
|
|
25
|
+
goal: "digest the changelog",
|
|
26
|
+
origin: "https://example.org",
|
|
27
|
+
steps: [{ id: "observe", kind: "observe", label: "Observe the page" }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const gate: flowrungate = {
|
|
32
|
+
id: "gate:type",
|
|
33
|
+
kind: "type",
|
|
34
|
+
origin: "https://example.org",
|
|
35
|
+
reason: "The step is sensitive and waits at its consent gate.",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("headless sessions", () => {
|
|
39
|
+
it("opens one session on an HTTPS origin with telemetry off by default", () => {
|
|
40
|
+
const session = openheadlesssession({ origin: "https://example.org", now });
|
|
41
|
+
expect(session.state).toBe("active");
|
|
42
|
+
expect(session.telemetry).toBe(false);
|
|
43
|
+
expect(session.gatesresolved).toBe(0);
|
|
44
|
+
expect(() => openheadlesssession({ origin: "http://example.org", now })).toThrow(/HTTPS/);
|
|
45
|
+
expect(() => openheadlesssession({ origin: "", now })).toThrow(/HTTPS/);
|
|
46
|
+
const closed = closeheadlesssession(session);
|
|
47
|
+
expect(closed.state).toBe("closed");
|
|
48
|
+
expect(headlesssessionview(closed).gatesresolved).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("routes every gate wait to the consent provider callback the host implements", async () => {
|
|
52
|
+
const session = openheadlesssession({ origin: "https://example.org", now });
|
|
53
|
+
const approved = await headlessgatewait({ session, gate, provider: { resolvegate: async () => "approve" } });
|
|
54
|
+
expect(approved.resolution).toBe("approve");
|
|
55
|
+
expect(approved.session.gatesresolved).toBe(1);
|
|
56
|
+
expect(approved.session.id).toBe(session.id);
|
|
57
|
+
const refused = await headlessgatewait({
|
|
58
|
+
session: approved.session,
|
|
59
|
+
gate,
|
|
60
|
+
provider: { resolvegate: async () => "refuse" },
|
|
61
|
+
});
|
|
62
|
+
expect(refused.resolution).toBe("refuse");
|
|
63
|
+
expect(refused.session.gatesresolved).toBe(1);
|
|
64
|
+
await expect(
|
|
65
|
+
headlessgatewait({
|
|
66
|
+
session: closeheadlesssession(session),
|
|
67
|
+
gate,
|
|
68
|
+
provider: { resolvegate: async () => "approve" },
|
|
69
|
+
}),
|
|
70
|
+
).rejects.toThrow(/closed/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("refuses every unresolved gate under denydefault with no provider attached", async () => {
|
|
74
|
+
const session = openheadlesssession({ origin: "https://example.org", now });
|
|
75
|
+
const denied = await headlessgatewait({ session, gate });
|
|
76
|
+
expect(denied.resolution).toBe("refuse");
|
|
77
|
+
expect(denied.reason).toMatch(/denydefault/);
|
|
78
|
+
expect(denied.session.gatesresolved).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("attaches remote sessions through the same json rpc protocol the extension speaks", () => {
|
|
82
|
+
const session = openheadlesssession({ origin: "https://example.org", now });
|
|
83
|
+
const frames = remoteattachframes({ id: 1, session, runtime: "node" });
|
|
84
|
+
expect(frames.initialize).toMatchObject({ jsonrpc: "2.0", id: 1, method: "initialize" });
|
|
85
|
+
expect(frames.initialize.params).toMatchObject({ protocolversion, runtime: "node", origin: "https://example.org" });
|
|
86
|
+
expect(frames.ready).toMatchObject({ jsonrpc: "2.0", id: 1 });
|
|
87
|
+
expect((frames.ready.result as { attached: boolean }).attached).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("exposes the policy engine for external validation with no browser attached", () => {
|
|
91
|
+
expect(
|
|
92
|
+
headlesspolicyvalidate({ id: "s1", kind: "observe", summary: "observe", risk: "read" }, "https://example.org")
|
|
93
|
+
.allowed,
|
|
94
|
+
).toBe(true);
|
|
95
|
+
const verdict = headlesspolicyvalidate(
|
|
96
|
+
{ id: "s1", kind: "explode" as never, summary: "explode", risk: "read" },
|
|
97
|
+
"https://example.org",
|
|
98
|
+
);
|
|
99
|
+
expect(verdict.allowed).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("exposes the audit reader with chain verification", async () => {
|
|
103
|
+
let log = openrunlog({ runid: "run:1", sessionid: "headless", now });
|
|
104
|
+
log = await appendlogentry({
|
|
105
|
+
log,
|
|
106
|
+
kind: "step",
|
|
107
|
+
summary: "The step ran.",
|
|
108
|
+
origin: "https://example.org",
|
|
109
|
+
stepid: "observe",
|
|
110
|
+
at: now,
|
|
111
|
+
});
|
|
112
|
+
const sealed = await sealrunlog(log, now + 10);
|
|
113
|
+
const read = await headlessauditread(sealed.log);
|
|
114
|
+
expect(read.ok).toBe(true);
|
|
115
|
+
expect(read.entries).toHaveLength(1);
|
|
116
|
+
expect(read.entries[0]?.kind).toBe("step");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("exposes the memory entry point over the same profile store format", async () => {
|
|
120
|
+
const store: Map<string, unknown> = new Map();
|
|
121
|
+
const memory = headlessmemory({
|
|
122
|
+
async get<T>(key: string) {
|
|
123
|
+
return store.get(key) as T | undefined;
|
|
124
|
+
},
|
|
125
|
+
async set<T>(key: string, value: T) {
|
|
126
|
+
store.set(key, value);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
expect(await memory.getconfig()).toBeUndefined();
|
|
130
|
+
await memory.setconfig({ provider: "local" } as never);
|
|
131
|
+
expect(await memory.getconfig()).toMatchObject({ provider: "local" });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("exposes the plan, run and session entry points to hosts", () => {
|
|
135
|
+
const planned = headlessplan(plan(), ["https://example.org"]);
|
|
136
|
+
expect(planned.version).toBe(protocolversion);
|
|
137
|
+
expect(planned.workflow.origins).toEqual(["https://example.org"]);
|
|
138
|
+
expect(planned.workflow.steps[0]).toMatchObject({ id: "observe", kind: "observe" });
|
|
139
|
+
expect(() => headlessplan(plan(), ["https://elsewhere.org"])).toThrow(/outside the grants/);
|
|
140
|
+
const provider = { resolvegate: async () => "approve" as const };
|
|
141
|
+
const run = headlessrun({
|
|
142
|
+
request: {
|
|
143
|
+
planpath: "plan.json",
|
|
144
|
+
options: { format: "human", outputdir: "out", grantspath: "grants.json", interactive: false, dryrun: true },
|
|
145
|
+
},
|
|
146
|
+
file: plan(),
|
|
147
|
+
provider,
|
|
148
|
+
});
|
|
149
|
+
expect(run.provider).toBe(provider);
|
|
150
|
+
expect(run.request.planpath).toBe("plan.json");
|
|
151
|
+
const view = headlesssessionview(openheadlesssession({ origin: "https://example.org", now }));
|
|
152
|
+
expect(view).toMatchObject({ origin: "https://example.org", state: "active", telemetry: false });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("exposes typed run subscriptions that keep their own event list", () => {
|
|
156
|
+
const seen: string[] = [];
|
|
157
|
+
const subscription = headlesssubscribe((event) => seen.push(event.summary));
|
|
158
|
+
subscription.emit({ kind: "step", stepid: "observe", summary: "The step ran.", at: now });
|
|
159
|
+
subscription.emit({ kind: "gate", stepid: "type", summary: "The gate waits.", at: now + 1 });
|
|
160
|
+
expect(seen).toEqual(["The step ran.", "The gate waits."]);
|
|
161
|
+
expect(subscription.events()).toHaveLength(2);
|
|
162
|
+
expect(subscription.events()[0]?.kind).toBe("step");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("keeps the telemetry posture off unless the host opts in", () => {
|
|
166
|
+
expect(headlesstelemetryposture(false).telemetry).toBe(false);
|
|
167
|
+
expect(headlesstelemetryposture(false).reason).toMatch(/nothing by default/);
|
|
168
|
+
expect(headlesstelemetryposture(true).telemetry).toBe(true);
|
|
169
|
+
expect(headlesstelemetryposture(true).reason).toMatch(/host opt in/);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
channellive,
|
|
4
|
+
closeidlechannels,
|
|
5
|
+
defaultidlewindowms,
|
|
6
|
+
defaultheartbeatms,
|
|
7
|
+
defaulthttpstream,
|
|
8
|
+
enforcemaxclients,
|
|
9
|
+
heartbeat,
|
|
10
|
+
httpframepipeline,
|
|
11
|
+
listremotestatus,
|
|
12
|
+
openstreamchannel,
|
|
13
|
+
starttls,
|
|
14
|
+
} from "../http.js";
|
|
15
|
+
import { issuetoken, issuepairingcode, grantallowlistentry } from "../auth.js";
|
|
16
|
+
import { defaultmcpconfig } from "../mcp.js";
|
|
17
|
+
import type { clientrecord, mcpserverconfig, streamchannel } from "../types.js";
|
|
18
|
+
|
|
19
|
+
const now = 1_800_000_000_000;
|
|
20
|
+
|
|
21
|
+
/** Builds one connected client fixture. */
|
|
22
|
+
function client(id: string, transport: "stdio" | "http" = "http"): clientrecord {
|
|
23
|
+
return { id, transport, paired: true, connectedat: now - 1000, pairedat: now - 500 };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Builds one open channel fixture with its last beat offset from now. */
|
|
27
|
+
function channel(clientid: string, beatoffset: number): streamchannel {
|
|
28
|
+
return { id: `channel-${clientid}`, clientid, openedat: now - 2000, lastbeatat: now - beatoffset };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("http stream channels", () => {
|
|
32
|
+
it("opens server sent event channels and heartbeats idle ones", () => {
|
|
33
|
+
const open = openstreamchannel({ clientid: "client1", now, id: "channel-1" });
|
|
34
|
+
expect(open.id).toBe("channel-1");
|
|
35
|
+
expect(open.openedat).toBe(now);
|
|
36
|
+
expect(open.lastbeatat).toBe(now);
|
|
37
|
+
expect(open.closedat).toBeUndefined();
|
|
38
|
+
const beaten = heartbeat({ channels: [open], clientid: "client1", now: now + 5000 });
|
|
39
|
+
expect(beaten[0]?.lastbeatat).toBe(now + 5000);
|
|
40
|
+
const untouched = heartbeat({ channels: [channel("other", 0)], clientid: "client1", now: now + 5000 });
|
|
41
|
+
expect(untouched[0]?.lastbeatat).toBe(now);
|
|
42
|
+
expect(defaulthttpstream()).toEqual({
|
|
43
|
+
endpoint: "/mcp",
|
|
44
|
+
streampath: "/mcp/stream",
|
|
45
|
+
tls: { mode: "off" },
|
|
46
|
+
heartbeatms: defaultheartbeatms,
|
|
47
|
+
idlewindowms: defaultidlewindowms,
|
|
48
|
+
});
|
|
49
|
+
expect(defaultheartbeatms).toBe(30_000);
|
|
50
|
+
expect(defaultidlewindowms).toBe(90_000);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("closes dead channels past the idle window while live ones stay open", () => {
|
|
54
|
+
const live = channel("client1", 1000);
|
|
55
|
+
const dead = channel("client2", defaultidlewindowms + 1);
|
|
56
|
+
const closedalready = { ...channel("client3", 500), closedat: now - 10 };
|
|
57
|
+
const swept = closeidlechannels({ channels: [live, dead, closedalready], now, idlewindow: defaultidlewindowms });
|
|
58
|
+
expect(swept[0]?.closedat).toBeUndefined();
|
|
59
|
+
expect(swept[1]?.closedat).toBe(now);
|
|
60
|
+
expect(swept[2]?.closedat).toBe(now - 10);
|
|
61
|
+
expect(channellive(live, now, defaultidlewindowms)).toBe(true);
|
|
62
|
+
expect(channellive(dead, now, defaultidlewindowms)).toBe(false);
|
|
63
|
+
expect(channellive(closedalready, now, defaultidlewindowms)).toBe(false);
|
|
64
|
+
expect(channellive(channel("client4", 5000), now, 1000)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("tls termination", () => {
|
|
69
|
+
it("keeps the localhost default off while required mode refuses unverified peers", () => {
|
|
70
|
+
expect(starttls({ config: { mode: "off" }, now }).tls).toBe(false);
|
|
71
|
+
const negotiated = starttls({ config: { mode: "on" }, now });
|
|
72
|
+
expect(negotiated.tls).toBe(true);
|
|
73
|
+
expect(negotiated.verified).toBe(true);
|
|
74
|
+
const required = starttls({ config: { mode: "required" }, now });
|
|
75
|
+
expect(required.tls).toBe(false);
|
|
76
|
+
expect(required.reason).toMatch(/presented no certificate/i);
|
|
77
|
+
const fingerprint = starttls({
|
|
78
|
+
config: { mode: "required", certificatefingerprint: "sha256:aa" },
|
|
79
|
+
presented: { fingerprint: "sha256:bb" },
|
|
80
|
+
now,
|
|
81
|
+
});
|
|
82
|
+
expect(fingerprint.verified).toBe(false);
|
|
83
|
+
expect(fingerprint.reason).toMatch(/does not match the user configured fingerprint/i);
|
|
84
|
+
expect(
|
|
85
|
+
starttls({
|
|
86
|
+
config: { mode: "required", certificatefingerprint: "sha256:aa" },
|
|
87
|
+
presented: { fingerprint: "sha256:aa" },
|
|
88
|
+
now,
|
|
89
|
+
}),
|
|
90
|
+
).toEqual({ tls: true, verified: true });
|
|
91
|
+
expect(starttls({ config: { mode: "on", certificatefingerprint: "sha256:aa" }, now }).verified).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("client ceiling", () => {
|
|
96
|
+
it("refuses connections past the user configured maximum while an absent value stays unbounded", () => {
|
|
97
|
+
const clients = [client("client1"), client("client2")];
|
|
98
|
+
expect(enforcemaxclients({ clients, maxclients: 2 }).allowed).toBe(false);
|
|
99
|
+
expect(enforcemaxclients({ clients, maxclients: 2 }).reason).toMatch(/maximum of 2 remote clients/i);
|
|
100
|
+
expect(enforcemaxclients({ clients, maxclients: 3 }).allowed).toBe(true);
|
|
101
|
+
expect(enforcemaxclients({ clients }).allowed).toBe(true);
|
|
102
|
+
const disconnected = [
|
|
103
|
+
client("client1"),
|
|
104
|
+
{ ...client("client2"), disconnectedat: now },
|
|
105
|
+
{ ...client("client3"), disconnectedat: now },
|
|
106
|
+
];
|
|
107
|
+
expect(enforcemaxclients({ clients: disconnected, maxclients: 2 }).allowed).toBe(true);
|
|
108
|
+
expect(enforcemaxclients({ clients: disconnected, maxclients: 1 }).allowed).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("remote status", () => {
|
|
113
|
+
it("reports endpoint, tls state, client counts and channel health", async () => {
|
|
114
|
+
const token = await issuetoken({ clientid: "client1", scopes: ["browser"], now, raw: "raw" });
|
|
115
|
+
const config: mcpserverconfig = {
|
|
116
|
+
...defaultmcpconfig(),
|
|
117
|
+
enabled: true,
|
|
118
|
+
httpstream: defaulthttpstream(),
|
|
119
|
+
remoteaccess: { endpoint: "https://agent.example:7436", tls: { mode: "required" }, maxclients: 5 },
|
|
120
|
+
};
|
|
121
|
+
const status = listremotestatus({
|
|
122
|
+
config,
|
|
123
|
+
channels: [channel("client1", 1000), channel("client2", defaultidlewindowms + 1)],
|
|
124
|
+
clients: [client("client1"), { ...client("client2"), paired: false }],
|
|
125
|
+
tokens: [token.token],
|
|
126
|
+
now,
|
|
127
|
+
});
|
|
128
|
+
expect(status.endpoint).toBe("https://agent.example:7436");
|
|
129
|
+
expect(status.tls.mode).toBe("required");
|
|
130
|
+
expect(status.tls.certificaterequired).toBe(true);
|
|
131
|
+
expect(status.clients).toBe(2);
|
|
132
|
+
expect(status.paired).toBe(1);
|
|
133
|
+
expect(status.channelsopen).toBe(1);
|
|
134
|
+
expect(status.channelsdead).toBe(1);
|
|
135
|
+
expect(status.tokenslive).toBe(1);
|
|
136
|
+
const plain = listremotestatus({
|
|
137
|
+
config: { ...defaultmcpconfig(), enabled: true },
|
|
138
|
+
channels: [],
|
|
139
|
+
clients: [],
|
|
140
|
+
tokens: [],
|
|
141
|
+
now,
|
|
142
|
+
});
|
|
143
|
+
expect(plain.endpoint).toBe("/mcp");
|
|
144
|
+
expect(plain.tls.mode).toBe("off");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("ordered remote frame pipeline", () => {
|
|
149
|
+
/** Builds the full pipeline fixture set for one remote caller. */
|
|
150
|
+
async function pipelinefixture(
|
|
151
|
+
config: mcpserverconfig,
|
|
152
|
+
): Promise<{
|
|
153
|
+
tokens: Awaited<ReturnType<typeof issuetoken>>["token"][];
|
|
154
|
+
raw: string;
|
|
155
|
+
allowlist: import("../types.js").allowlistentry[];
|
|
156
|
+
}> {
|
|
157
|
+
const issued = await issuetoken({ clientid: "client1", scopes: ["browser"], now, raw: "raw-token" });
|
|
158
|
+
const allowlist = grantallowlistentry({
|
|
159
|
+
entries: [],
|
|
160
|
+
identity: { fingerprint: "fp-1", displayname: "Laptop agent" },
|
|
161
|
+
namespaces: ["browser"],
|
|
162
|
+
actor: "user",
|
|
163
|
+
now,
|
|
164
|
+
});
|
|
165
|
+
return { tokens: [issued.token], raw: "raw-token", allowlist };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
it("terminates tls before any token verification", async () => {
|
|
169
|
+
const config: mcpserverconfig = {
|
|
170
|
+
...defaultmcpconfig(),
|
|
171
|
+
enabled: true,
|
|
172
|
+
remoteaccess: {
|
|
173
|
+
endpoint: "https://agent.example",
|
|
174
|
+
tls: { mode: "required", certificatefingerprint: "sha256:aa" },
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
const fixture = await pipelinefixture(config);
|
|
178
|
+
const refused = await httpframepipeline({
|
|
179
|
+
config,
|
|
180
|
+
presented: { fingerprint: "sha256:bb" },
|
|
181
|
+
tokens: fixture.tokens,
|
|
182
|
+
rawtoken: fixture.raw,
|
|
183
|
+
allowlist: fixture.allowlist,
|
|
184
|
+
fingerprint: "fp-1",
|
|
185
|
+
toolname: "browser.readtext",
|
|
186
|
+
now,
|
|
187
|
+
});
|
|
188
|
+
expect(refused.error?.code).toBe("consentrefused");
|
|
189
|
+
expect(refused.error?.message).toMatch(/does not match the user configured fingerprint/i);
|
|
190
|
+
expect(refused.token).toBeUndefined();
|
|
191
|
+
const missing = await httpframepipeline({
|
|
192
|
+
config,
|
|
193
|
+
tokens: fixture.tokens,
|
|
194
|
+
rawtoken: fixture.raw,
|
|
195
|
+
allowlist: fixture.allowlist,
|
|
196
|
+
fingerprint: "fp-1",
|
|
197
|
+
now,
|
|
198
|
+
});
|
|
199
|
+
expect(missing.error?.message).toMatch(/presented no certificate/i);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("verifies the session token on every frame and answers with the fixed refusal", async () => {
|
|
203
|
+
const config: mcpserverconfig = { ...defaultmcpconfig(), enabled: true };
|
|
204
|
+
const fixture = await pipelinefixture(config);
|
|
205
|
+
const missing = await httpframepipeline({
|
|
206
|
+
config,
|
|
207
|
+
tokens: fixture.tokens,
|
|
208
|
+
allowlist: fixture.allowlist,
|
|
209
|
+
fingerprint: "fp-1",
|
|
210
|
+
now,
|
|
211
|
+
});
|
|
212
|
+
expect(missing.error?.message).toBe("The remote frame failed its authentication handshake.");
|
|
213
|
+
const wrong = await httpframepipeline({
|
|
214
|
+
config,
|
|
215
|
+
tokens: fixture.tokens,
|
|
216
|
+
rawtoken: "wrong",
|
|
217
|
+
allowlist: fixture.allowlist,
|
|
218
|
+
fingerprint: "fp-1",
|
|
219
|
+
now,
|
|
220
|
+
});
|
|
221
|
+
expect(wrong.error?.code).toBe("consentrefused");
|
|
222
|
+
expect(wrong.error?.message).toBe("The remote frame failed its authentication handshake.");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("refuses allowlist misses after the token verified", async () => {
|
|
226
|
+
const config: mcpserverconfig = { ...defaultmcpconfig(), enabled: true };
|
|
227
|
+
const fixture = await pipelinefixture(config);
|
|
228
|
+
const unknown = await httpframepipeline({
|
|
229
|
+
config,
|
|
230
|
+
tokens: fixture.tokens,
|
|
231
|
+
rawtoken: fixture.raw,
|
|
232
|
+
allowlist: fixture.allowlist,
|
|
233
|
+
fingerprint: "fp-unknown",
|
|
234
|
+
now,
|
|
235
|
+
});
|
|
236
|
+
expect(unknown.error?.code).toBe("consentrefused");
|
|
237
|
+
expect(unknown.error?.message).toMatch(/not on the allowlist/i);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("fails unknown namespaces fast and ungranted scopes as consent refusals", async () => {
|
|
241
|
+
const config: mcpserverconfig = { ...defaultmcpconfig(), enabled: true };
|
|
242
|
+
const fixture = await pipelinefixture(config);
|
|
243
|
+
const unknown = await httpframepipeline({
|
|
244
|
+
config,
|
|
245
|
+
tokens: fixture.tokens,
|
|
246
|
+
rawtoken: fixture.raw,
|
|
247
|
+
allowlist: fixture.allowlist,
|
|
248
|
+
fingerprint: "fp-1",
|
|
249
|
+
toolname: "carrierpigeon.readtext",
|
|
250
|
+
now,
|
|
251
|
+
});
|
|
252
|
+
expect(unknown.error?.code).toBe("params");
|
|
253
|
+
expect(unknown.error?.message).toMatch(/no reviewed namespace/i);
|
|
254
|
+
const ungranted = await httpframepipeline({
|
|
255
|
+
config,
|
|
256
|
+
tokens: fixture.tokens,
|
|
257
|
+
rawtoken: fixture.raw,
|
|
258
|
+
allowlist: fixture.allowlist,
|
|
259
|
+
fingerprint: "fp-1",
|
|
260
|
+
toolname: "memory.list",
|
|
261
|
+
now,
|
|
262
|
+
});
|
|
263
|
+
expect(ungranted.error?.code).toBe("consentrefused");
|
|
264
|
+
expect(ungranted.error?.message).toMatch(/grants no memory tools/i);
|
|
265
|
+
const clean = await httpframepipeline({
|
|
266
|
+
config,
|
|
267
|
+
tokens: fixture.tokens,
|
|
268
|
+
rawtoken: fixture.raw,
|
|
269
|
+
allowlist: fixture.allowlist,
|
|
270
|
+
fingerprint: "fp-1",
|
|
271
|
+
toolname: "browser.readtext",
|
|
272
|
+
now,
|
|
273
|
+
});
|
|
274
|
+
expect(clean.error).toBeUndefined();
|
|
275
|
+
expect(clean.token?.clientid).toBe("client1");
|
|
276
|
+
});
|
|
277
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
appendlogentry,
|
|
4
|
+
chainreportof,
|
|
5
|
+
entryhashof,
|
|
6
|
+
exportlogchain,
|
|
7
|
+
lasthashof,
|
|
8
|
+
logentryof,
|
|
9
|
+
openrunlog,
|
|
10
|
+
readverifiedlog,
|
|
11
|
+
sealrunlog,
|
|
12
|
+
verifylogchain,
|
|
13
|
+
} from "../security.js";
|
|
14
|
+
import type { immutablelogentry, storedrunlog } from "../types.js";
|
|
15
|
+
|
|
16
|
+
const now = 1_000;
|
|
17
|
+
const origin = "https://example.com";
|
|
18
|
+
|
|
19
|
+
describe("immutable log hash chain", () => {
|
|
20
|
+
async function samplelog(): Promise<storedrunlog> {
|
|
21
|
+
let log = openrunlog({ runid: "run1", sessionid: "s1", now });
|
|
22
|
+
log = await appendlogentry({
|
|
23
|
+
log,
|
|
24
|
+
kind: "grant",
|
|
25
|
+
summary: "The session started for the origin.",
|
|
26
|
+
origin,
|
|
27
|
+
at: now + 1,
|
|
28
|
+
});
|
|
29
|
+
log = await appendlogentry({
|
|
30
|
+
log,
|
|
31
|
+
kind: "step",
|
|
32
|
+
summary: "The click step completed.",
|
|
33
|
+
origin,
|
|
34
|
+
stepid: "s1",
|
|
35
|
+
at: now + 2,
|
|
36
|
+
});
|
|
37
|
+
log = await appendlogentry({
|
|
38
|
+
log,
|
|
39
|
+
kind: "deny",
|
|
40
|
+
summary: "The submitform step was denied.",
|
|
41
|
+
origin,
|
|
42
|
+
stepid: "s2",
|
|
43
|
+
at: now + 3,
|
|
44
|
+
});
|
|
45
|
+
return log;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
it("chains every entry to the hash of its predecessor and writes the entry hash at append time", async () => {
|
|
49
|
+
const log = await samplelog();
|
|
50
|
+
expect(log.entries).toHaveLength(3);
|
|
51
|
+
expect(log.entries[0]?.hash.previous).toBe("0".repeat(64));
|
|
52
|
+
for (let index = 1; index < log.entries.length; index += 1) {
|
|
53
|
+
expect(log.entries[index]?.hash.previous).toBe(log.entries[index - 1]?.hash.current);
|
|
54
|
+
}
|
|
55
|
+
for (const entry of log.entries) {
|
|
56
|
+
expect(entry.hash.current).toMatch(/^[0-9a-f]{64}$/);
|
|
57
|
+
expect(entry.hash.algorithm).toBe("sha-256");
|
|
58
|
+
}
|
|
59
|
+
expect(lasthashof(log)).toBe(log.entries[2]?.hash.current);
|
|
60
|
+
expect(lasthashof(openrunlog({ runid: "r", sessionid: "s", now }))).toBe("0".repeat(64));
|
|
61
|
+
expect(() => openrunlog({ runid: " ", sessionid: "s", now })).toThrow(/run and session ids/i);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("derives the entry hash over the canonical body only", async () => {
|
|
65
|
+
const body = { id: "e1", runid: "run1", kind: "step" as const, summary: "The step completed.", origin, at: now };
|
|
66
|
+
const one = await entryhashof({ previous: "0".repeat(64), entry: body });
|
|
67
|
+
const two = await entryhashof({ previous: "f".repeat(64), entry: body });
|
|
68
|
+
expect(one.current).not.toBe(two.current);
|
|
69
|
+
expect(one.previous).toBe("0".repeat(64));
|
|
70
|
+
const entry = await logentryof({
|
|
71
|
+
runid: "run1",
|
|
72
|
+
kind: "step",
|
|
73
|
+
summary: "The step completed.",
|
|
74
|
+
origin,
|
|
75
|
+
at: now,
|
|
76
|
+
previous: "0".repeat(64),
|
|
77
|
+
});
|
|
78
|
+
expect(entry.id).not.toBe("");
|
|
79
|
+
await expect(
|
|
80
|
+
logentryof({ runid: "run1", kind: "step", summary: " ", origin, at: now, previous: "" }),
|
|
81
|
+
).rejects.toThrow(/summary/i);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("verifies the whole chain at read time and refuses reads of a broken link", async () => {
|
|
85
|
+
const log = await samplelog();
|
|
86
|
+
const verified = await verifylogchain(log.entries);
|
|
87
|
+
expect(verified.valid).toBe(true);
|
|
88
|
+
expect(verified.reason).toMatch(/verifies from the genesis hash/i);
|
|
89
|
+
const read = await readverifiedlog(log);
|
|
90
|
+
expect(read.ok).toBe(true);
|
|
91
|
+
expect(read.entries).toHaveLength(3);
|
|
92
|
+
const tampered = log.entries.map((entry, index) =>
|
|
93
|
+
index === 1 ? { ...entry, summary: "A forged summary." } : entry,
|
|
94
|
+
);
|
|
95
|
+
const broken = await verifylogchain(tampered);
|
|
96
|
+
expect(broken.valid).toBe(false);
|
|
97
|
+
expect(broken.brokenat).toBe(1);
|
|
98
|
+
expect(broken.reason).toMatch(/tamper evidence/i);
|
|
99
|
+
const refused = await readverifiedlog({ ...log, entries: tampered });
|
|
100
|
+
expect(refused.ok).toBe(false);
|
|
101
|
+
expect(refused.entries).toEqual([]);
|
|
102
|
+
const relinked = log.entries.map((entry, index) =>
|
|
103
|
+
index === 2 ? { ...entry, hash: { ...entry.hash, previous: "0".repeat(64) } } : entry,
|
|
104
|
+
);
|
|
105
|
+
const relinkbroken = await verifylogchain(relinked);
|
|
106
|
+
expect(relinkbroken.valid).toBe(false);
|
|
107
|
+
expect(relinkbroken.brokenat).toBe(2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("seals the log at completion with a final hash and refuses every append after the seal", async () => {
|
|
111
|
+
const log = await samplelog();
|
|
112
|
+
const { log: sealed, seal } = await sealrunlog(log, now + 10);
|
|
113
|
+
expect(seal.entries).toBe(3);
|
|
114
|
+
expect(seal.sealhash.previous).toBe(log.entries[2]?.hash.current);
|
|
115
|
+
expect(seal.sealhash.current).toMatch(/^[0-9a-f]{64}$/);
|
|
116
|
+
expect(sealed.seal?.sealedat).toBe(now + 10);
|
|
117
|
+
await expect(
|
|
118
|
+
appendlogentry({ log: sealed, kind: "step", summary: "A late append.", origin, at: now + 11 }),
|
|
119
|
+
).rejects.toThrow(/accepts no append/i);
|
|
120
|
+
await expect(sealrunlog(sealed, now + 12)).rejects.toThrow(/already sealed/i);
|
|
121
|
+
await expect(sealrunlog(openrunlog({ runid: "empty", sessionid: "s", now }), now)).rejects.toThrow(
|
|
122
|
+
/at least one entry/i,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("reports the chain status per run with the seal hash and exports only verified chains", async () => {
|
|
127
|
+
const log = await samplelog();
|
|
128
|
+
const openreport = await chainreportof(log);
|
|
129
|
+
expect(openreport.valid).toBe(true);
|
|
130
|
+
expect(openreport.entries).toBe(3);
|
|
131
|
+
expect(openreport.sealhash).toBeUndefined();
|
|
132
|
+
const sealed = await sealrunlog(log, now + 10);
|
|
133
|
+
const sealedreport = await chainreportof(sealed.log);
|
|
134
|
+
expect(sealedreport.sealhash).toBe(sealed.seal.sealhash.current);
|
|
135
|
+
expect(sealedreport.sealedat).toBe(now + 10);
|
|
136
|
+
const exported = await exportlogchain(sealed.log);
|
|
137
|
+
expect(exported.chainvalid).toBe(true);
|
|
138
|
+
expect(exported.entries).toBe(3);
|
|
139
|
+
expect(exported.sealhash).toBe(sealed.seal.sealhash.current);
|
|
140
|
+
expect(exported.log).toHaveLength(3);
|
|
141
|
+
const forged: immutablelogentry[] = sealed.log.entries.map((entry, index) =>
|
|
142
|
+
index === 0 ? { ...entry, summary: "Forged." } : entry,
|
|
143
|
+
);
|
|
144
|
+
const refused = await exportlogchain({ ...sealed.log, entries: forged });
|
|
145
|
+
expect(refused.chainvalid).toBe(false);
|
|
146
|
+
expect(refused.log).toEqual([]);
|
|
147
|
+
expect(refused.entries).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("attaches provenance of origin and step to every entry", async () => {
|
|
151
|
+
const log = await samplelog();
|
|
152
|
+
expect(log.entries[0]?.origin).toBe(origin);
|
|
153
|
+
expect(log.entries[0]?.stepid).toBeUndefined();
|
|
154
|
+
expect(log.entries[1]?.stepid).toBe("s1");
|
|
155
|
+
expect(log.entries[2]?.stepid).toBe("s2");
|
|
156
|
+
});
|
|
157
|
+
});
|