@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/flow.ts
DELETED
|
@@ -1,1190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The flow module of the 1.1.90 consolidation: every correlated variation of the flow logic interned in this one file, so the module family carries one surface without duplicate variations.
|
|
3
|
-
* The correlation is the flow a reviewed plan travels: flowlibrary holds the template store the flow starts from (schemastrict manifest validation, publisher signature verification with quarantine, grant diffs mapped onto originprofiles, fresh consent for sensitive entries, install and fork resolution into native workflow records); flowrun executes a plan file through the same pipeline the extension runs (the grants gate, the parseworkflowproposal validation, the per step consent gates routed to the consent provider, the driver execution and the sealed immutable log chain the run writes); and syncbridge moves the same manifests between machines behind an explicit opt in with digest conflict detection, so the template a flow starts from stays the template the bridge carries.
|
|
4
|
-
* Nothing is hardcoded: the registry origin and the endpoint of every provider stay user configured values, and no library, run or bridge path ever bypasses the human review.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/* ── Merged from flowlibrary.ts ── */
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
flowlibraryentry,
|
|
11
|
-
flowlibrarymanifest,
|
|
12
|
-
flowlibrarystep,
|
|
13
|
-
dataexpectation,
|
|
14
|
-
publishdescriptor,
|
|
15
|
-
workflowrecord,
|
|
16
|
-
schemaerror,
|
|
17
|
-
actionkind,
|
|
18
|
-
} from "./types.js";
|
|
19
|
-
import {
|
|
20
|
-
actionrisk,
|
|
21
|
-
librarycapabilitygate,
|
|
22
|
-
librarymanifestgate,
|
|
23
|
-
libraryquarantinegate,
|
|
24
|
-
librarysensitivegate,
|
|
25
|
-
libraryimportgate,
|
|
26
|
-
} from "./policy.js";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Flowlibrary logic of the 1.1.66 family.
|
|
30
|
-
* The shared workflow templates live here: every browsed manifest validates under schemastrict before anything else, its kinds stay inside the installed capability set, its required grants surface as a grant diff that maps onto originprofiles before any import completes, and a sensitive entry needs its fresh consent; an entry from an unverified publisher quarantines while a present publisher signature verifies against the manifest digest, and every completed import lands as a proposal that still passes the plan review and resolves into a native workflow record whose selector namespaces rewrite for the local profile.
|
|
31
|
-
* Nothing is hardcoded: the registry origin of every entry stays a user configured value, the store deduplicates entries by manifest digest, and no library operation ever bypasses the human review.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/** Derives the sha-256 hex digest of one manifest body through the web crypto the platform offers. */
|
|
35
|
-
async function sha256(payload: string): Promise<string> {
|
|
36
|
-
const bytes = new TextEncoder().encode(payload);
|
|
37
|
-
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
38
|
-
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Reads the canonical body of one manifest: the fields the digest covers, with the publisher signature excluded so the digest certifies the template alone. */
|
|
42
|
-
function manifestbody(manifest: Omit<flowlibrarymanifest, "publish">): string {
|
|
43
|
-
return JSON.stringify({
|
|
44
|
-
id: manifest.id,
|
|
45
|
-
title: manifest.title,
|
|
46
|
-
description: manifest.description,
|
|
47
|
-
version: manifest.version,
|
|
48
|
-
publisher: manifest.publisher,
|
|
49
|
-
...(manifest.registry !== undefined ? { registry: manifest.registry } : {}),
|
|
50
|
-
steps: manifest.steps,
|
|
51
|
-
kinds: manifest.kinds,
|
|
52
|
-
requiredgrants: manifest.requiredgrants,
|
|
53
|
-
dataexpectations: manifest.dataexpectations,
|
|
54
|
-
sensitive: manifest.sensitive,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Computes the manifest digest every store deduplicates by and every sync conflict detects with. */
|
|
59
|
-
export async function manifestdigest(manifest: flowlibrarymanifest): Promise<string> {
|
|
60
|
-
const { publish, ...body } = manifest;
|
|
61
|
-
void publish;
|
|
62
|
-
return sha256(manifestbody(body));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Validates one raw manifest under schemastrict: the shape errors name their path and expected shape, and the capability gate refuses kinds the installed capability set lacks. */
|
|
66
|
-
export async function validatemanifest(input: {
|
|
67
|
-
manifest: unknown;
|
|
68
|
-
capabilities: string[];
|
|
69
|
-
}): Promise<{ ok: boolean; errors: schemaerror[]; reason: string; manifest?: flowlibrarymanifest }> {
|
|
70
|
-
const errors: schemaerror[] = [];
|
|
71
|
-
const raw = input.manifest;
|
|
72
|
-
if (!Boolean(raw) || typeof raw !== "object" || Array.isArray(raw))
|
|
73
|
-
return {
|
|
74
|
-
ok: false,
|
|
75
|
-
errors: [
|
|
76
|
-
{
|
|
77
|
-
path: "manifest",
|
|
78
|
-
expected: "object",
|
|
79
|
-
found: Array.isArray(raw) ? "array" : typeof raw,
|
|
80
|
-
reason: "Every flowlibrary manifest travels as one plain object.",
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
reason: "The flowlibrary manifest is no plain object; schemastrict refuses the carrier before any import.",
|
|
84
|
-
};
|
|
85
|
-
const candidate = raw as Partial<flowlibrarymanifest> & Record<string, unknown>;
|
|
86
|
-
if (typeof candidate.id !== "string" || candidate.id.trim() === "")
|
|
87
|
-
errors.push({
|
|
88
|
-
path: "id",
|
|
89
|
-
expected: "string",
|
|
90
|
-
found: typeof candidate.id,
|
|
91
|
-
reason: "The flowlibrary manifest needs its id.",
|
|
92
|
-
});
|
|
93
|
-
if (typeof candidate.title !== "string" || candidate.title.trim() === "")
|
|
94
|
-
errors.push({
|
|
95
|
-
path: "title",
|
|
96
|
-
expected: "string",
|
|
97
|
-
found: typeof candidate.title,
|
|
98
|
-
reason: "The flowlibrary manifest needs its title.",
|
|
99
|
-
});
|
|
100
|
-
if (typeof candidate.description !== "string")
|
|
101
|
-
errors.push({
|
|
102
|
-
path: "description",
|
|
103
|
-
expected: "string",
|
|
104
|
-
found: typeof candidate.description,
|
|
105
|
-
reason: "The flowlibrary manifest needs its description.",
|
|
106
|
-
});
|
|
107
|
-
if (typeof candidate.version !== "string" || candidate.version.trim() === "")
|
|
108
|
-
errors.push({
|
|
109
|
-
path: "version",
|
|
110
|
-
expected: "string",
|
|
111
|
-
found: typeof candidate.version,
|
|
112
|
-
reason: "The flowlibrary manifest needs its version.",
|
|
113
|
-
});
|
|
114
|
-
if (typeof candidate.publisher !== "string" || candidate.publisher.trim() === "")
|
|
115
|
-
errors.push({
|
|
116
|
-
path: "publisher",
|
|
117
|
-
expected: "string",
|
|
118
|
-
found: typeof candidate.publisher,
|
|
119
|
-
reason: "The flowlibrary manifest names its publisher.",
|
|
120
|
-
});
|
|
121
|
-
if (!Array.isArray(candidate.steps) || candidate.steps.length === 0)
|
|
122
|
-
errors.push({
|
|
123
|
-
path: "steps",
|
|
124
|
-
expected: "array",
|
|
125
|
-
found: Array.isArray(candidate.steps) ? "empty array" : typeof candidate.steps,
|
|
126
|
-
reason: "The flowlibrary manifest declares its steps.",
|
|
127
|
-
});
|
|
128
|
-
if (Array.isArray(candidate.steps)) {
|
|
129
|
-
candidate.steps.forEach((step, index) => {
|
|
130
|
-
const shape = step as Partial<flowlibrarystep> | undefined;
|
|
131
|
-
if (
|
|
132
|
-
!Boolean(shape) ||
|
|
133
|
-
typeof shape !== "object" ||
|
|
134
|
-
typeof shape.id !== "string" ||
|
|
135
|
-
shape.id.trim() === "" ||
|
|
136
|
-
typeof shape.kind !== "string" ||
|
|
137
|
-
shape.kind.trim() === "" ||
|
|
138
|
-
typeof shape.label !== "string"
|
|
139
|
-
)
|
|
140
|
-
errors.push({
|
|
141
|
-
path: `steps.${index}`,
|
|
142
|
-
expected: "flowlibrarystep",
|
|
143
|
-
found: typeof step,
|
|
144
|
-
reason: "Every flowlibrary step needs its id, kind and label.",
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
if (!Array.isArray(candidate.kinds) || candidate.kinds.length === 0)
|
|
149
|
-
errors.push({
|
|
150
|
-
path: "kinds",
|
|
151
|
-
expected: "array",
|
|
152
|
-
found: typeof candidate.kinds,
|
|
153
|
-
reason: "The flowlibrary manifest declares the action kinds its steps use.",
|
|
154
|
-
});
|
|
155
|
-
if (!Array.isArray(candidate.requiredgrants))
|
|
156
|
-
errors.push({
|
|
157
|
-
path: "requiredgrants",
|
|
158
|
-
expected: "array",
|
|
159
|
-
found: typeof candidate.requiredgrants,
|
|
160
|
-
reason: "The flowlibrary manifest declares its required origin grants.",
|
|
161
|
-
});
|
|
162
|
-
if (!Array.isArray(candidate.dataexpectations))
|
|
163
|
-
errors.push({
|
|
164
|
-
path: "dataexpectations",
|
|
165
|
-
expected: "array",
|
|
166
|
-
found: typeof candidate.dataexpectations,
|
|
167
|
-
reason: "The flowlibrary manifest declares its data expectations with minimization hints.",
|
|
168
|
-
});
|
|
169
|
-
if (typeof candidate.sensitive !== "boolean")
|
|
170
|
-
errors.push({
|
|
171
|
-
path: "sensitive",
|
|
172
|
-
expected: "boolean",
|
|
173
|
-
found: typeof candidate.sensitive,
|
|
174
|
-
reason: "The flowlibrary manifest marks whether it is sensitive.",
|
|
175
|
-
});
|
|
176
|
-
const unknownfields = Object.keys(candidate).filter(
|
|
177
|
-
(key) =>
|
|
178
|
-
![
|
|
179
|
-
"id",
|
|
180
|
-
"title",
|
|
181
|
-
"description",
|
|
182
|
-
"version",
|
|
183
|
-
"publisher",
|
|
184
|
-
"registry",
|
|
185
|
-
"steps",
|
|
186
|
-
"kinds",
|
|
187
|
-
"requiredgrants",
|
|
188
|
-
"dataexpectations",
|
|
189
|
-
"sensitive",
|
|
190
|
-
"publish",
|
|
191
|
-
].includes(key),
|
|
192
|
-
);
|
|
193
|
-
for (const field of unknownfields)
|
|
194
|
-
errors.push({
|
|
195
|
-
path: field,
|
|
196
|
-
expected: "absent",
|
|
197
|
-
found: "present",
|
|
198
|
-
reason: `The flowlibrary manifest carries the unknown field ${field}; schemastrict refuses unknown fields.`,
|
|
199
|
-
});
|
|
200
|
-
const schemagate = librarymanifestgate({ errors });
|
|
201
|
-
if (!schemagate.allowed)
|
|
202
|
-
return { ok: false, errors, reason: schemagate.reason ?? "The flowlibrary manifest fails schemastrict." };
|
|
203
|
-
const manifest: flowlibrarymanifest = {
|
|
204
|
-
id: candidate.id as string,
|
|
205
|
-
title: candidate.title as string,
|
|
206
|
-
description: candidate.description as string,
|
|
207
|
-
version: candidate.version as string,
|
|
208
|
-
publisher: candidate.publisher as string,
|
|
209
|
-
...(typeof candidate.registry === "string" && candidate.registry.trim() !== ""
|
|
210
|
-
? { registry: candidate.registry }
|
|
211
|
-
: {}),
|
|
212
|
-
steps: (candidate.steps as flowlibrarystep[]).map((step) => ({
|
|
213
|
-
id: step.id,
|
|
214
|
-
kind: step.kind,
|
|
215
|
-
label: step.label,
|
|
216
|
-
...(step.target !== undefined ? { target: step.target } : {}),
|
|
217
|
-
...(step.value !== undefined ? { value: step.value } : {}),
|
|
218
|
-
...(step.namespace !== undefined ? { namespace: step.namespace } : {}),
|
|
219
|
-
})),
|
|
220
|
-
kinds: candidate.kinds as string[],
|
|
221
|
-
requiredgrants: candidate.requiredgrants as string[],
|
|
222
|
-
dataexpectations: candidate.dataexpectations as dataexpectation[],
|
|
223
|
-
sensitive: candidate.sensitive as boolean,
|
|
224
|
-
...(isPublish(candidate.publish) ? { publish: candidate.publish } : {}),
|
|
225
|
-
};
|
|
226
|
-
const kinds = [...new Set(manifest.steps.map((step) => step.kind))];
|
|
227
|
-
const capabilitygate = librarycapabilitygate({ kinds, capabilities: input.capabilities });
|
|
228
|
-
if (!capabilitygate.allowed) return { ok: false, errors: [], reason: capabilitygate.reason ?? "", manifest };
|
|
229
|
-
return {
|
|
230
|
-
ok: true,
|
|
231
|
-
errors: [],
|
|
232
|
-
reason: `The manifest ${manifest.id} of ${manifest.publisher} validates under schemastrict with ${manifest.steps.length} step${manifest.steps.length === 1 ? "" : "s"} and ${kinds.length} kind${kinds.length === 1 ? "" : "s"} inside the installed capability set.`,
|
|
233
|
-
manifest,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Narrows one raw publish descriptor into its typed shape. */
|
|
238
|
-
function isPublish(value: unknown): value is publishdescriptor {
|
|
239
|
-
if (!Boolean(value) || typeof value !== "object") return false;
|
|
240
|
-
const shape = value as Partial<publishdescriptor>;
|
|
241
|
-
return (
|
|
242
|
-
typeof shape.publisher === "string" &&
|
|
243
|
-
shape.publisher.trim() !== "" &&
|
|
244
|
-
typeof shape.signature === "string" &&
|
|
245
|
-
shape.signature.trim() !== "" &&
|
|
246
|
-
typeof shape.digest === "string" &&
|
|
247
|
-
shape.digest.trim() !== "" &&
|
|
248
|
-
typeof shape.provenance === "string" &&
|
|
249
|
-
typeof shape.publishedat === "number"
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** Verifies the publisher signature of one manifest when present: the descriptor digest must match the recomputed manifest digest and the signature must seal the publisher, the digest and the provenance, so a signature over any other body refuses in full. */
|
|
254
|
-
export async function verifypublishersignature(
|
|
255
|
-
manifest: flowlibrarymanifest,
|
|
256
|
-
): Promise<{ verified: boolean; reason: string }> {
|
|
257
|
-
if (manifest.publish === undefined)
|
|
258
|
-
return {
|
|
259
|
-
verified: false,
|
|
260
|
-
reason: `The manifest ${manifest.id} of ${manifest.publisher} carries no publisher signature; the entry quarantines until the user verifies its publisher.`,
|
|
261
|
-
};
|
|
262
|
-
const digest = await manifestdigest(manifest);
|
|
263
|
-
if (manifest.publish.digest !== digest)
|
|
264
|
-
return {
|
|
265
|
-
verified: false,
|
|
266
|
-
reason: `The publisher signature of ${manifest.publish.publisher} covers the digest ${manifest.publish.digest} while the manifest body hashes to ${digest}; the verification refuses the signature in full.`,
|
|
267
|
-
};
|
|
268
|
-
if (manifest.publish.publisher !== manifest.publisher)
|
|
269
|
-
return {
|
|
270
|
-
verified: false,
|
|
271
|
-
reason: `The publisher signature names ${manifest.publish.publisher} while the manifest carries the publisher ${manifest.publisher}; the verification refuses the signature in full.`,
|
|
272
|
-
};
|
|
273
|
-
const seal = await sha256(
|
|
274
|
-
`${manifest.publish.publisher}\n${manifest.publish.digest}\n${manifest.publish.provenance}`,
|
|
275
|
-
);
|
|
276
|
-
if (manifest.publish.signature !== seal)
|
|
277
|
-
return {
|
|
278
|
-
verified: false,
|
|
279
|
-
reason: `The publisher signature of ${manifest.publish.publisher} seals neither the manifest digest nor its provenance; the verification refuses the signature in full.`,
|
|
280
|
-
};
|
|
281
|
-
return {
|
|
282
|
-
verified: true,
|
|
283
|
-
reason: `The publisher signature of ${manifest.publish.publisher} verifies over the manifest digest ${digest.slice(0, 12)}… with its provenance ${manifest.publish.provenance}.`,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** Signs one manifest with the publisher seal so tests and publishers produce verifiable descriptors; the seal covers the digest and the provenance exactly as the verification reads them. */
|
|
288
|
-
export async function signmanifest(
|
|
289
|
-
manifest: flowlibrarymanifest,
|
|
290
|
-
provenance: string,
|
|
291
|
-
publishedat: number,
|
|
292
|
-
): Promise<publishdescriptor> {
|
|
293
|
-
const digest = await manifestdigest(manifest);
|
|
294
|
-
return {
|
|
295
|
-
publisher: manifest.publisher,
|
|
296
|
-
signature: await sha256(`${manifest.publisher}\n${digest}\n${provenance}`),
|
|
297
|
-
digest,
|
|
298
|
-
provenance,
|
|
299
|
-
publishedat,
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Builds one flowlibrary entry with its provenance attached: an unverified publisher quarantines the entry while a verified signature installs it as available. */
|
|
304
|
-
export async function libraryentryof(input: {
|
|
305
|
-
manifest: flowlibrarymanifest;
|
|
306
|
-
provenance: string;
|
|
307
|
-
now: number;
|
|
308
|
-
}): Promise<flowlibraryentry> {
|
|
309
|
-
const digest = await manifestdigest(input.manifest);
|
|
310
|
-
const verification = await verifypublishersignature(input.manifest);
|
|
311
|
-
const quarantinegate = libraryquarantinegate({
|
|
312
|
-
verified: verification.verified,
|
|
313
|
-
signaturepresent: input.manifest.publish !== undefined,
|
|
314
|
-
signaturevalid: verification.verified,
|
|
315
|
-
});
|
|
316
|
-
const state: flowlibraryentry["state"] = quarantinegate.allowed ? "available" : "quarantined";
|
|
317
|
-
return {
|
|
318
|
-
id: `${input.manifest.id}@${input.manifest.version}`,
|
|
319
|
-
manifest: input.manifest,
|
|
320
|
-
digest,
|
|
321
|
-
state,
|
|
322
|
-
provenance: `${input.provenance}; ${verification.reason}`,
|
|
323
|
-
addedat: input.now,
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** Reads the grant diff of one manifest against the grants the profile holds: the added grants map onto originprofiles at import time so the import dialog shows exactly what the entry would newly hold. */
|
|
328
|
-
export function grantdiffof(input: { manifest: flowlibrarymanifest; heldgrants: string[] }): {
|
|
329
|
-
added: string[];
|
|
330
|
-
kept: string[];
|
|
331
|
-
originmappings: Array<{ origin: string; kinds: string[] }>;
|
|
332
|
-
} {
|
|
333
|
-
const required = [...new Set(input.manifest.requiredgrants)];
|
|
334
|
-
const added = required.filter((origin) => !input.heldgrants.includes(origin));
|
|
335
|
-
const kept = required.filter((origin) => input.heldgrants.includes(origin));
|
|
336
|
-
const originmappings = required.map((origin) => ({
|
|
337
|
-
origin,
|
|
338
|
-
kinds: [...new Set(input.manifest.steps.map((step) => step.kind))],
|
|
339
|
-
}));
|
|
340
|
-
return { added, kept, originmappings };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** Reads the fresh consent descriptor a sensitive manifest needs before any import completes; a non sensitive manifest needs none. */
|
|
344
|
-
export function sensitiveconsentfor(
|
|
345
|
-
manifest: flowlibrarymanifest,
|
|
346
|
-
freshconsent: boolean,
|
|
347
|
-
): { required: boolean; reason: string } {
|
|
348
|
-
const gate = librarysensitivegate({ sensitive: manifest.sensitive, freshconsent });
|
|
349
|
-
return { required: manifest.sensitive, reason: gate.reason ?? "" };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/** Builds the proposal objective one library import lands as: the import never executes directly, it routes through the same proposal flow and plan review as every task. */
|
|
353
|
-
export function libraryproposalof(entry: flowlibraryentry): { objective: string; reviewed: boolean; reason: string } {
|
|
354
|
-
const gate = libraryimportgate({ proposal: true, planreviewed: true });
|
|
355
|
-
return {
|
|
356
|
-
objective: `Install the flowlibrary template ${entry.manifest.title} version ${entry.manifest.version} of ${entry.manifest.publisher} with ${entry.manifest.steps.length} steps and ${entry.manifest.requiredgrants.length} required grant${entry.manifest.requiredgrants.length === 1 ? "" : "s"}.`,
|
|
357
|
-
reviewed: true,
|
|
358
|
-
reason: gate.reason ?? "",
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/** Derives the review risk class of a manifest: the highest risk class of its step kinds, so a template that writes or submits reviews as sensitive exactly like a native workflow. */
|
|
363
|
-
function manifestrisk(manifest: flowlibrarymanifest): "read" | "interaction" | "sensitive" {
|
|
364
|
-
let risk: "read" | "interaction" | "sensitive" = "read";
|
|
365
|
-
for (const step of manifest.steps) {
|
|
366
|
-
const candidate = actionrisk(step.kind as actionkind);
|
|
367
|
-
if (candidate === "sensitive") return "sensitive";
|
|
368
|
-
if (candidate === "interaction") risk = "interaction";
|
|
369
|
-
}
|
|
370
|
-
return risk;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/** Resolves one manifest into a native workflow record: the steps keep their reviewed kinds while the selector namespaces rewrite for the local profile, and the record lands pending its review approval exactly like any import. */
|
|
374
|
-
export function installlibrary(input: {
|
|
375
|
-
entry: flowlibraryentry;
|
|
376
|
-
selectornamespace?: string;
|
|
377
|
-
now: number;
|
|
378
|
-
}): workflowrecord {
|
|
379
|
-
const manifest = input.entry.manifest;
|
|
380
|
-
const prefix = input.selectornamespace?.trim() ?? "";
|
|
381
|
-
const steps = manifest.steps.map((step) => ({
|
|
382
|
-
id: `${manifest.id}-${step.id}`,
|
|
383
|
-
kind: step.kind as workflowrecord["steps"][number]["kind"],
|
|
384
|
-
label: step.label,
|
|
385
|
-
...(step.target !== undefined ? { target: prefix === "" ? step.target : `${prefix} ${step.target}`.trim() } : {}),
|
|
386
|
-
...(step.value !== undefined ? { value: step.value } : {}),
|
|
387
|
-
}));
|
|
388
|
-
return {
|
|
389
|
-
id: `library:${manifest.id}:${manifest.version}`,
|
|
390
|
-
name: manifest.title,
|
|
391
|
-
version: 1,
|
|
392
|
-
origins: [...new Set(manifest.requiredgrants)],
|
|
393
|
-
steps,
|
|
394
|
-
blocks: [],
|
|
395
|
-
risk: manifestrisk(manifest),
|
|
396
|
-
reviewstate: "pending",
|
|
397
|
-
createdat: input.now,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/** Diffs the versions of one library update before it replaces the local entry: the changed steps, the added grants and the version movement all surface so the update never overwrites silently. */
|
|
402
|
-
export function updatelibrary(input: { incoming: flowlibraryentry; existing: flowlibraryentry }): {
|
|
403
|
-
changedsteps: string[];
|
|
404
|
-
addedgrants: string[];
|
|
405
|
-
versionfrom: string;
|
|
406
|
-
versionto: string;
|
|
407
|
-
replace: boolean;
|
|
408
|
-
reason: string;
|
|
409
|
-
} {
|
|
410
|
-
if (input.incoming.digest === input.existing.digest)
|
|
411
|
-
return {
|
|
412
|
-
changedsteps: [],
|
|
413
|
-
addedgrants: [],
|
|
414
|
-
versionfrom: input.existing.manifest.version,
|
|
415
|
-
versionto: input.incoming.manifest.version,
|
|
416
|
-
replace: false,
|
|
417
|
-
reason: `The incoming ${input.incoming.manifest.id} carries the same manifest digest as the installed entry; the update replaces nothing.`,
|
|
418
|
-
};
|
|
419
|
-
const existingsteps = new Set(input.existing.manifest.steps.map((step) => step.id));
|
|
420
|
-
const incomingsteps = new Set(input.incoming.manifest.steps.map((step) => step.id));
|
|
421
|
-
const changedsteps = [...new Set([...existingsteps, ...incomingsteps])].filter((id) => {
|
|
422
|
-
const before = input.existing.manifest.steps.find((step) => step.id === id);
|
|
423
|
-
const after = input.incoming.manifest.steps.find((step) => step.id === id);
|
|
424
|
-
return (
|
|
425
|
-
before === undefined ||
|
|
426
|
-
after === undefined ||
|
|
427
|
-
before.kind !== after.kind ||
|
|
428
|
-
before.target !== after.target ||
|
|
429
|
-
before.value !== after.value
|
|
430
|
-
);
|
|
431
|
-
});
|
|
432
|
-
const addedgrants = [...new Set(input.incoming.manifest.requiredgrants)].filter(
|
|
433
|
-
(origin) => !input.existing.manifest.requiredgrants.includes(origin),
|
|
434
|
-
);
|
|
435
|
-
return {
|
|
436
|
-
changedsteps,
|
|
437
|
-
addedgrants,
|
|
438
|
-
versionfrom: input.existing.manifest.version,
|
|
439
|
-
versionto: input.incoming.manifest.version,
|
|
440
|
-
replace: true,
|
|
441
|
-
reason: `The update of ${input.incoming.manifest.id} from version ${input.existing.manifest.version} to ${input.incoming.manifest.version} changes ${changedsteps.length} step${changedsteps.length === 1 ? "" : "s"} and adds ${addedgrants.length} grant${addedgrants.length === 1 ? "" : "s"}; the version diff surfaces before the replace.`,
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/** Reads the removal outcome of one library entry: the local forks stay untouched because a fork is an independent local workflow the store owns apart from its template. */
|
|
446
|
-
export function removelibrary(input: { entry: flowlibraryentry; forks: workflowrecord[] }): {
|
|
447
|
-
removed: string;
|
|
448
|
-
keptforks: string[];
|
|
449
|
-
reason: string;
|
|
450
|
-
} {
|
|
451
|
-
return {
|
|
452
|
-
removed: input.entry.id,
|
|
453
|
-
keptforks: input.forks.map((fork) => fork.id),
|
|
454
|
-
reason: `The library entry ${input.entry.manifest.title} leaves the store while its ${input.forks.length} local fork${input.forks.length === 1 ? "" : "s"} stay untouched; a fork is an independent local workflow.`,
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/** Forks one library entry into an independent local workflow: the fork copies the steps with their rewritten selectors and owns its own name from its creation on. */
|
|
459
|
-
export function forklibrary(input: { entry: flowlibraryentry; now: number }): workflowrecord {
|
|
460
|
-
const manifest = input.entry.manifest;
|
|
461
|
-
const steps = manifest.steps.map((step) => ({
|
|
462
|
-
id: `fork-${step.id}`,
|
|
463
|
-
kind: step.kind as workflowrecord["steps"][number]["kind"],
|
|
464
|
-
label: step.label,
|
|
465
|
-
...(step.target !== undefined ? { target: step.target } : {}),
|
|
466
|
-
...(step.value !== undefined ? { value: step.value } : {}),
|
|
467
|
-
}));
|
|
468
|
-
return {
|
|
469
|
-
id: `fork:${manifest.id}:${input.now}`,
|
|
470
|
-
name: `${manifest.title} fork`,
|
|
471
|
-
version: 1,
|
|
472
|
-
origins: [...new Set(manifest.requiredgrants)],
|
|
473
|
-
steps,
|
|
474
|
-
blocks: [],
|
|
475
|
-
risk: manifestrisk(manifest),
|
|
476
|
-
reviewstate: "pending",
|
|
477
|
-
createdat: input.now,
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/** Searches the flowlibrary entries for the dashboardpage browser: the query matches the title, the description and the publisher while the filter narrows the publisher, the sensitive mark or the state. */
|
|
482
|
-
export function librarysearch(input: {
|
|
483
|
-
entries: flowlibraryentry[];
|
|
484
|
-
query?: string;
|
|
485
|
-
filter?: { publisher?: string; sensitive?: boolean; state?: string };
|
|
486
|
-
}): flowlibraryentry[] {
|
|
487
|
-
const query = input.query?.trim().toLowerCase() ?? "";
|
|
488
|
-
return input.entries.filter((entry) => {
|
|
489
|
-
if (input.filter?.publisher !== undefined && entry.manifest.publisher !== input.filter.publisher) return false;
|
|
490
|
-
if (input.filter?.sensitive !== undefined && entry.manifest.sensitive !== input.filter.sensitive) return false;
|
|
491
|
-
if (input.filter?.state !== undefined && entry.state !== input.filter.state) return false;
|
|
492
|
-
if (query === "") return true;
|
|
493
|
-
return [entry.manifest.title, entry.manifest.description, entry.manifest.publisher].some((text) =>
|
|
494
|
-
text.toLowerCase().includes(query),
|
|
495
|
-
);
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/** Reads one browser row of a library entry: the publisher, the version and the required grants per entry the browser lists before any install. */
|
|
500
|
-
export function librarybrowserow(entry: flowlibraryentry): {
|
|
501
|
-
id: string;
|
|
502
|
-
title: string;
|
|
503
|
-
publisher: string;
|
|
504
|
-
version: string;
|
|
505
|
-
grants: string[];
|
|
506
|
-
sensitive: boolean;
|
|
507
|
-
state: string;
|
|
508
|
-
registry?: string;
|
|
509
|
-
} {
|
|
510
|
-
return {
|
|
511
|
-
id: entry.id,
|
|
512
|
-
title: entry.manifest.title,
|
|
513
|
-
publisher: entry.manifest.publisher,
|
|
514
|
-
version: entry.manifest.version,
|
|
515
|
-
grants: entry.manifest.requiredgrants,
|
|
516
|
-
sensitive: entry.manifest.sensitive,
|
|
517
|
-
state: entry.state,
|
|
518
|
-
...(entry.manifest.registry !== undefined ? { registry: entry.manifest.registry } : {}),
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/** Reads the full step list of one library entry the entry view shows before install: every step with its kind, label, target and namespace beside the data expectations with their minimization hints. */
|
|
523
|
-
export function librarystepsview(
|
|
524
|
-
entry: flowlibraryentry,
|
|
525
|
-
): Array<{
|
|
526
|
-
id: string;
|
|
527
|
-
kind: string;
|
|
528
|
-
label: string;
|
|
529
|
-
target?: string;
|
|
530
|
-
namespace?: string;
|
|
531
|
-
families: string[];
|
|
532
|
-
fields: string[];
|
|
533
|
-
}> {
|
|
534
|
-
return entry.manifest.steps.map((step) => {
|
|
535
|
-
const expectations = entry.manifest.dataexpectations.filter((expectation) => expectation.stepid === step.id);
|
|
536
|
-
return {
|
|
537
|
-
id: step.id,
|
|
538
|
-
kind: step.kind,
|
|
539
|
-
label: step.label,
|
|
540
|
-
...(step.target !== undefined ? { target: step.target } : {}),
|
|
541
|
-
...(step.namespace !== undefined ? { namespace: step.namespace } : {}),
|
|
542
|
-
families: expectations.map((expectation) => expectation.family),
|
|
543
|
-
fields: [...new Set(expectations.flatMap((expectation) => expectation.fields))],
|
|
544
|
-
};
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/** Reads the minimization summary of one manifest: the data families its steps expect and the fields each family limits itself to. */
|
|
549
|
-
export function dataexpectationssummary(
|
|
550
|
-
manifest: flowlibrarymanifest,
|
|
551
|
-
): Array<{ family: string; fields: string[]; steps: string[] }> {
|
|
552
|
-
const families = new Map<string, { fields: Set<string>; steps: Set<string> }>();
|
|
553
|
-
for (const expectation of manifest.dataexpectations) {
|
|
554
|
-
const entry = families.get(expectation.family) ?? { fields: new Set<string>(), steps: new Set<string>() };
|
|
555
|
-
for (const field of expectation.fields) entry.fields.add(field);
|
|
556
|
-
entry.steps.add(expectation.stepid);
|
|
557
|
-
families.set(expectation.family, entry);
|
|
558
|
-
}
|
|
559
|
-
return [...families.entries()].map(([family, entry]) => ({
|
|
560
|
-
family,
|
|
561
|
-
fields: [...entry.fields],
|
|
562
|
-
steps: [...entry.steps],
|
|
563
|
-
}));
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/** Builds one library lifecycle event for the immutable log and the store: every install, update and removal records with its entry provenance. */
|
|
567
|
-
export function libraryeventof(input: {
|
|
568
|
-
kind: "install" | "update" | "remove";
|
|
569
|
-
entryid: string;
|
|
570
|
-
title: string;
|
|
571
|
-
version: string;
|
|
572
|
-
detail: string;
|
|
573
|
-
now: number;
|
|
574
|
-
}): {
|
|
575
|
-
id: string;
|
|
576
|
-
kind: "install" | "update" | "remove";
|
|
577
|
-
entryid: string;
|
|
578
|
-
title: string;
|
|
579
|
-
version: string;
|
|
580
|
-
detail: string;
|
|
581
|
-
at: number;
|
|
582
|
-
} {
|
|
583
|
-
if (input.entryid.trim() === "") throw new Error("The library event needs its entry id.");
|
|
584
|
-
return {
|
|
585
|
-
id: `libraryevent:${input.kind}:${input.entryid}:${input.now}`,
|
|
586
|
-
kind: input.kind,
|
|
587
|
-
entryid: input.entryid,
|
|
588
|
-
title: input.title,
|
|
589
|
-
version: input.version,
|
|
590
|
-
detail: input.detail,
|
|
591
|
-
at: input.now,
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/** Exports the manifest list of the flowlibrary for audit: one entry per manifest with its digest, publisher, version, state and provenance and no step payload. */
|
|
596
|
-
export function exportlibrarymanifests(
|
|
597
|
-
entries: flowlibraryentry[],
|
|
598
|
-
): Array<{
|
|
599
|
-
id: string;
|
|
600
|
-
title: string;
|
|
601
|
-
publisher: string;
|
|
602
|
-
version: string;
|
|
603
|
-
digest: string;
|
|
604
|
-
state: string;
|
|
605
|
-
provenance: string;
|
|
606
|
-
addedat: number;
|
|
607
|
-
}> {
|
|
608
|
-
return entries.map((entry) => ({
|
|
609
|
-
id: entry.id,
|
|
610
|
-
title: entry.manifest.title,
|
|
611
|
-
publisher: entry.manifest.publisher,
|
|
612
|
-
version: entry.manifest.version,
|
|
613
|
-
digest: entry.digest,
|
|
614
|
-
state: entry.state,
|
|
615
|
-
provenance: entry.provenance,
|
|
616
|
-
addedat: entry.addedat,
|
|
617
|
-
}));
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/* ── Merged from flowrun.ts ── */
|
|
621
|
-
|
|
622
|
-
import type {
|
|
623
|
-
consentprovider,
|
|
624
|
-
flowrunevent,
|
|
625
|
-
flowrunoptions,
|
|
626
|
-
flowrunoutcome,
|
|
627
|
-
flowrunrequest,
|
|
628
|
-
flowrungate,
|
|
629
|
-
planfile,
|
|
630
|
-
planfilestep,
|
|
631
|
-
storedrunlog,
|
|
632
|
-
toolstep,
|
|
633
|
-
} from "./types.js";
|
|
634
|
-
import { protocolversion } from "./types.js";
|
|
635
|
-
import { appendlogentry, openrunlog, sealrunlog } from "./security.js";
|
|
636
|
-
import { flowrungrantgate, headlessconsentgate, resolvedrisk } from "./policy.js";
|
|
637
|
-
import { sensitiveclassesof } from "./security.js";
|
|
638
|
-
import { parseworkflowproposal } from "./protocol.js";
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Flowrun logic of the 1.1.67 family.
|
|
642
|
-
* The terminal executes a plan file through the same pipeline the extension runs: the request parses into its options, the plan file becomes a workflow proposal that passes the same parseworkflowproposal validation, the run never starts without an origin grant file or an interactive prompt, every sensitive step waits at its consent gate routed to the consent provider (the terminal prompt in the cli, a host callback in the headless library), every outcome lands in the sealed immutable log chain the run writes to its output directory, and the logstream events and the stepstimeline render for the terminal.
|
|
643
|
-
* The run never skips a gate: a refused gate revokes the run, a failed step fails it, and both exit non zero.
|
|
644
|
-
*/
|
|
645
|
-
|
|
646
|
-
/** The driver a flowrun executes its steps through: the extension driver rides the reviewed executor, the headless driver rides a remote browser session and the dry run driver validates without page effects. */
|
|
647
|
-
export interface flowdriver {
|
|
648
|
-
execute(step: planfilestep, at: number): Promise<{ state: "done" | "failed"; summary: string; duration: number }>;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/** Parses one flowrun request from terminal arguments: the plan path is mandatory, the format defaults to human, the output directory defaults to the plan directory and the flags stay user choices. */
|
|
652
|
-
export function parseflowrunrequest(args: string[]): flowrunrequest {
|
|
653
|
-
const positional: string[] = [];
|
|
654
|
-
const flags = new Map<string, string>();
|
|
655
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
656
|
-
const arg = args[index];
|
|
657
|
-
if (arg === undefined) continue;
|
|
658
|
-
if (arg.startsWith("--")) {
|
|
659
|
-
const [name, inline] = arg.slice(2).split("=", 2);
|
|
660
|
-
if (name === undefined || name.trim() === "") throw new Error(`The argument ${arg} carries no flag name.`);
|
|
661
|
-
const next = args[index + 1];
|
|
662
|
-
if (inline !== undefined) flags.set(name, inline);
|
|
663
|
-
else if (next !== undefined && !next.startsWith("--")) {
|
|
664
|
-
flags.set(name, next);
|
|
665
|
-
index += 1;
|
|
666
|
-
} else flags.set(name, "true");
|
|
667
|
-
} else positional.push(arg);
|
|
668
|
-
}
|
|
669
|
-
const planpath = positional[0];
|
|
670
|
-
if (planpath === undefined || planpath.trim() === "")
|
|
671
|
-
throw new Error("The flowrun command needs the plan file path it executes.");
|
|
672
|
-
const format = flags.get("format") ?? "human";
|
|
673
|
-
if (format !== "human" && format !== "json") throw new Error("The flowrun format must stay human or json.");
|
|
674
|
-
const outputdir = flags.get("output") ?? ".";
|
|
675
|
-
if (outputdir.trim() === "") throw new Error("The flowrun output directory must stay a non-empty path.");
|
|
676
|
-
const grantspath = flags.get("grants");
|
|
677
|
-
const interactive = flags.get("interactive") === "true";
|
|
678
|
-
const dryrun = flags.get("dryrun") === "true";
|
|
679
|
-
if (grantspath === undefined && !interactive)
|
|
680
|
-
throw new Error(
|
|
681
|
-
"The flowrun needs a grants file or the interactive flag; a run without an origin grant never starts.",
|
|
682
|
-
);
|
|
683
|
-
const options: flowrunoptions = {
|
|
684
|
-
format,
|
|
685
|
-
outputdir,
|
|
686
|
-
interactive,
|
|
687
|
-
dryrun,
|
|
688
|
-
...(grantspath !== undefined ? { grantspath } : {}),
|
|
689
|
-
};
|
|
690
|
-
return { planpath, options };
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/** Parses one origin grants file: a list of granted HTTPS origins the run holds for its whole duration. */
|
|
694
|
-
export function parsegrantsfile(value: unknown): string[] {
|
|
695
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
696
|
-
throw new Error("The grants file must be a json object.");
|
|
697
|
-
const origins = (value as Record<string, unknown>).origins;
|
|
698
|
-
if (!Array.isArray(origins) || origins.length === 0)
|
|
699
|
-
throw new Error("The grants file needs its non-empty origins list.");
|
|
700
|
-
if (!origins.every((origin) => typeof origin === "string" && origin.startsWith("https://")))
|
|
701
|
-
throw new Error("Every grants file origin must be an HTTPS origin.");
|
|
702
|
-
return origins.filter((origin): origin is string => typeof origin === "string");
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/** Builds the workflow proposal value of one plan file so the same parseworkflowproposal validation the extension runs decides whether the run starts. */
|
|
706
|
-
export function flowproposalvalue(file: planfile): Record<string, unknown> {
|
|
707
|
-
return {
|
|
708
|
-
version: protocolversion,
|
|
709
|
-
workflow: {
|
|
710
|
-
name: `flowrun ${file.goal}`.trim().slice(0, 80),
|
|
711
|
-
version: 1,
|
|
712
|
-
origins: [file.origin],
|
|
713
|
-
steps: file.steps.map((step) => ({
|
|
714
|
-
id: step.id,
|
|
715
|
-
kind: step.kind,
|
|
716
|
-
label: step.label,
|
|
717
|
-
...(step.target !== undefined ? { target: step.target } : {}),
|
|
718
|
-
...(step.value !== undefined ? { value: step.value } : {}),
|
|
719
|
-
...(step.options !== undefined ? { options: step.options } : {}),
|
|
720
|
-
})),
|
|
721
|
-
},
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/** Reads whether one plan file step is sensitive: the same risk and sensitive class derivation the extension runs decides which steps wait at a gate. */
|
|
726
|
-
export function flowstepsensitive(step: planfilestep): boolean {
|
|
727
|
-
const classes = sensitiveclassesof({
|
|
728
|
-
kind: step.kind as never,
|
|
729
|
-
...(step.value !== undefined ? { value: step.value } : {}),
|
|
730
|
-
...(step.options !== undefined ? { options: step.options } : {}),
|
|
731
|
-
});
|
|
732
|
-
if (classes.sensitive) return true;
|
|
733
|
-
try {
|
|
734
|
-
return (
|
|
735
|
-
resolvedrisk({
|
|
736
|
-
id: step.id,
|
|
737
|
-
kind: step.kind as never,
|
|
738
|
-
label: step.label,
|
|
739
|
-
summary: step.label,
|
|
740
|
-
...(step.target !== undefined ? { target: step.target } : {}),
|
|
741
|
-
...(step.value !== undefined ? { value: step.value } : {}),
|
|
742
|
-
...(step.options !== undefined ? { options: step.options } : {}),
|
|
743
|
-
risk: "sensitive",
|
|
744
|
-
} as toolstep) === "sensitive"
|
|
745
|
-
);
|
|
746
|
-
} catch {
|
|
747
|
-
return true;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/** Builds one flowrun gate wait: the gate the run holds at while the consent provider resolves it. */
|
|
752
|
-
export function flowgateof(step: planfilestep, origin: string, reason: string): flowrungate {
|
|
753
|
-
return { id: `gate:${step.id}`, kind: step.kind, origin, reason };
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/** The dry run driver: it validates each step without page effects, so a flowrun with the dry run flag walks the plan and writes its chain without touching any browser. */
|
|
757
|
-
export function dryflowdriver(): flowdriver {
|
|
758
|
-
return {
|
|
759
|
-
async execute(step) {
|
|
760
|
-
return {
|
|
761
|
-
state: "done",
|
|
762
|
-
summary: `The dry run validated the step ${step.id} of kind ${step.kind} without page effects.`,
|
|
763
|
-
duration: 0,
|
|
764
|
-
};
|
|
765
|
-
},
|
|
766
|
-
};
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
/** Reads the exit code of one flowrun state: a done run exits zero, a failed run exits one and a revoked run exits two. */
|
|
770
|
-
export function flowrunexitcodeof(state: flowrunoutcome["state"]): number {
|
|
771
|
-
return state === "done" ? 0 : state === "failed" ? 1 : 2;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
/** Renders one flowrun event stream for the terminal: the human format prints one line per event and the json format prints one json object per line. */
|
|
775
|
-
export function renderflowevents(events: flowrunevent[], format: "human" | "json"): string {
|
|
776
|
-
if (format === "json") return events.map((event) => JSON.stringify(event)).join("\n");
|
|
777
|
-
return events
|
|
778
|
-
.map(
|
|
779
|
-
(event) =>
|
|
780
|
-
`${new Date(event.at).toISOString()} ${event.kind.toUpperCase()}${event.stepid !== undefined ? ` ${event.stepid}` : ""}: ${event.summary}`,
|
|
781
|
-
)
|
|
782
|
-
.join("\n");
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
/** Derives the stepstimeline rows of one flowrun from its events: one row per step with its status, summary and the gate wait it stood at. */
|
|
786
|
-
export function flowtimelinerowsof(
|
|
787
|
-
events: flowrunevent[],
|
|
788
|
-
): Array<{ stepid: string; status: "waiting" | "done" | "failed" | "revoked"; summary: string; at: number }> {
|
|
789
|
-
const rows: Array<{
|
|
790
|
-
stepid: string;
|
|
791
|
-
status: "waiting" | "done" | "failed" | "revoked";
|
|
792
|
-
summary: string;
|
|
793
|
-
at: number;
|
|
794
|
-
}> = [];
|
|
795
|
-
for (const event of events) {
|
|
796
|
-
if (event.stepid === undefined) continue;
|
|
797
|
-
if (event.kind === "gate")
|
|
798
|
-
rows.push({ stepid: event.stepid, status: "waiting", summary: event.summary, at: event.at });
|
|
799
|
-
if (event.kind === "step")
|
|
800
|
-
rows.push({ stepid: event.stepid, status: "done", summary: event.summary, at: event.at });
|
|
801
|
-
if (event.kind === "failed")
|
|
802
|
-
rows.push({ stepid: event.stepid, status: "failed", summary: event.summary, at: event.at });
|
|
803
|
-
if (event.kind === "revoked")
|
|
804
|
-
rows.push({ stepid: event.stepid, status: "revoked", summary: event.summary, at: event.at });
|
|
805
|
-
}
|
|
806
|
-
return rows;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
/** Renders the stepstimeline of one flowrun for the terminal: one line per step row with its status marker. */
|
|
810
|
-
export function renderflowtimeline(events: flowrunevent[]): string {
|
|
811
|
-
const rows = flowtimelinerowsof(events);
|
|
812
|
-
if (rows.length === 0) return "The run carried no step.";
|
|
813
|
-
return rows
|
|
814
|
-
.map(
|
|
815
|
-
(row) =>
|
|
816
|
-
`${row.status === "done" ? "[done]" : row.status === "waiting" ? "[wait]" : row.status === "failed" ? "[fail]" : "[halt]"} ${row.stepid}: ${row.summary}`,
|
|
817
|
-
)
|
|
818
|
-
.join("\n");
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
/** Runs one plan file through the same pipeline the extension runs: the grants gate, the workflow proposal validation, the per step consent gates routed to the provider, the driver execution and the sealed immutable log chain the run writes. */
|
|
822
|
-
export async function runflow(input: {
|
|
823
|
-
request: flowrunrequest;
|
|
824
|
-
file: planfile;
|
|
825
|
-
grants: string[];
|
|
826
|
-
provider?: consentprovider;
|
|
827
|
-
driver?: flowdriver;
|
|
828
|
-
now: number;
|
|
829
|
-
clock?: () => number;
|
|
830
|
-
emit?: (event: flowrunevent) => void;
|
|
831
|
-
}): Promise<{ outcome: flowrunoutcome; events: flowrunevent[]; log: storedrunlog }> {
|
|
832
|
-
const grantsource =
|
|
833
|
-
input.request.options.grantspath !== undefined ? "file" : input.request.options.interactive ? "prompt" : "none";
|
|
834
|
-
const startgate = flowrungrantgate({ origin: input.file.origin, grantsource });
|
|
835
|
-
if (!startgate.allowed) throw new Error(startgate.reason ?? "The flowrun never starts without a granted origin.");
|
|
836
|
-
parseworkflowproposal(flowproposalvalue(input.file), input.file.origin, input.grants, input.request.options.dryrun);
|
|
837
|
-
const runid = `flowrun:${input.now}`;
|
|
838
|
-
const clock = input.clock ?? (() => input.now);
|
|
839
|
-
const events: flowrunevent[] = [];
|
|
840
|
-
const log = openrunlog({ runid, sessionid: "flowrun", now: input.now });
|
|
841
|
-
const emit = (event: flowrunevent): void => {
|
|
842
|
-
events.push(event);
|
|
843
|
-
input.emit?.(event);
|
|
844
|
-
};
|
|
845
|
-
emit({
|
|
846
|
-
kind: "start",
|
|
847
|
-
summary: `The flowrun started the plan of ${input.file.steps.length} step${input.file.steps.length === 1 ? "" : "s"} on ${input.file.origin}${input.request.options.dryrun ? " in dry run mode" : ""}.`,
|
|
848
|
-
at: input.now,
|
|
849
|
-
});
|
|
850
|
-
let current = await appendlogentry({
|
|
851
|
-
log,
|
|
852
|
-
kind: "flowrun",
|
|
853
|
-
summary: `The flowrun opened on ${input.file.origin} with the grant from the ${grantsource === "file" ? "grants file" : "interactive prompt"}.`,
|
|
854
|
-
origin: input.file.origin,
|
|
855
|
-
at: input.now,
|
|
856
|
-
});
|
|
857
|
-
const driver = input.driver ?? (input.request.options.dryrun ? dryflowdriver() : undefined);
|
|
858
|
-
if (driver === undefined)
|
|
859
|
-
throw new Error(
|
|
860
|
-
"The flowrun needs a driver: a remote endpoint the run attaches to or the dry run flag; the cli drives no local browser itself.",
|
|
861
|
-
);
|
|
862
|
-
let executed = 0;
|
|
863
|
-
for (const step of input.file.steps) {
|
|
864
|
-
if (flowstepsensitive(step)) {
|
|
865
|
-
const gate = flowgateof(
|
|
866
|
-
step,
|
|
867
|
-
input.file.origin,
|
|
868
|
-
`The step ${step.id} of kind ${step.kind} is sensitive and waits at its consent gate.`,
|
|
869
|
-
);
|
|
870
|
-
emit({ kind: "gate", stepid: step.id, summary: gate.reason, at: clock() });
|
|
871
|
-
current = await appendlogentry({
|
|
872
|
-
log: current,
|
|
873
|
-
kind: "gate",
|
|
874
|
-
summary: gate.reason,
|
|
875
|
-
origin: input.file.origin,
|
|
876
|
-
stepid: step.id,
|
|
877
|
-
at: clock(),
|
|
878
|
-
});
|
|
879
|
-
const resolution = input.provider !== undefined ? await input.provider.resolvegate(gate) : undefined;
|
|
880
|
-
const consentgate = headlessconsentgate({
|
|
881
|
-
providerpresent: input.provider !== undefined,
|
|
882
|
-
...(resolution !== undefined ? { resolution } : {}),
|
|
883
|
-
});
|
|
884
|
-
if (!consentgate.allowed) {
|
|
885
|
-
const reason = consentgate.reason ?? "The consent gate refused the step.";
|
|
886
|
-
emit({ kind: "revoked", stepid: step.id, summary: reason, at: clock() });
|
|
887
|
-
current = await appendlogentry({
|
|
888
|
-
log: current,
|
|
889
|
-
kind: "deny",
|
|
890
|
-
summary: reason,
|
|
891
|
-
origin: input.file.origin,
|
|
892
|
-
stepid: step.id,
|
|
893
|
-
at: clock(),
|
|
894
|
-
});
|
|
895
|
-
const sealed = await sealrunlog(current, clock());
|
|
896
|
-
return {
|
|
897
|
-
outcome: {
|
|
898
|
-
runid,
|
|
899
|
-
state: "revoked",
|
|
900
|
-
steps: executed,
|
|
901
|
-
exitcode: flowrunexitcodeof("revoked"),
|
|
902
|
-
sealhash: sealed.seal.sealhash.current,
|
|
903
|
-
},
|
|
904
|
-
events,
|
|
905
|
-
log: sealed.log,
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
const result = await driver.execute(step, clock());
|
|
910
|
-
executed += 1;
|
|
911
|
-
emit({
|
|
912
|
-
kind: result.state === "failed" ? "failed" : "step",
|
|
913
|
-
stepid: step.id,
|
|
914
|
-
summary: result.summary,
|
|
915
|
-
at: clock(),
|
|
916
|
-
});
|
|
917
|
-
current = await appendlogentry({
|
|
918
|
-
log: current,
|
|
919
|
-
kind: "step",
|
|
920
|
-
summary: result.summary,
|
|
921
|
-
origin: input.file.origin,
|
|
922
|
-
stepid: step.id,
|
|
923
|
-
at: clock(),
|
|
924
|
-
});
|
|
925
|
-
if (result.state === "failed") {
|
|
926
|
-
const sealed = await sealrunlog(current, clock());
|
|
927
|
-
return {
|
|
928
|
-
outcome: {
|
|
929
|
-
runid,
|
|
930
|
-
state: "failed",
|
|
931
|
-
steps: executed,
|
|
932
|
-
exitcode: flowrunexitcodeof("failed"),
|
|
933
|
-
sealhash: sealed.seal.sealhash.current,
|
|
934
|
-
},
|
|
935
|
-
events,
|
|
936
|
-
log: sealed.log,
|
|
937
|
-
};
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
emit({
|
|
941
|
-
kind: "done",
|
|
942
|
-
summary: `The flowrun completed ${executed} step${executed === 1 ? "" : "s"} and sealed its chain.`,
|
|
943
|
-
at: clock(),
|
|
944
|
-
});
|
|
945
|
-
const sealed = await sealrunlog(current, clock());
|
|
946
|
-
return {
|
|
947
|
-
outcome: {
|
|
948
|
-
runid,
|
|
949
|
-
state: "done",
|
|
950
|
-
steps: executed,
|
|
951
|
-
exitcode: flowrunexitcodeof("done"),
|
|
952
|
-
sealhash: sealed.seal.sealhash.current,
|
|
953
|
-
},
|
|
954
|
-
events,
|
|
955
|
-
log: sealed.log,
|
|
956
|
-
};
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
/** Reads the flowrun report of one finished run: the outcome beside the rendered event stream and the terminal timeline, one json document the output directory keeps. */
|
|
960
|
-
export function flowrunreport(input: { outcome: flowrunoutcome; events: flowrunevent[] }): {
|
|
961
|
-
version: string;
|
|
962
|
-
outcome: flowrunoutcome;
|
|
963
|
-
events: flowrunevent[];
|
|
964
|
-
timeline: string;
|
|
965
|
-
} {
|
|
966
|
-
return {
|
|
967
|
-
version: protocolversion,
|
|
968
|
-
outcome: input.outcome,
|
|
969
|
-
events: input.events,
|
|
970
|
-
timeline: renderflowtimeline(input.events),
|
|
971
|
-
};
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
/* ── Merged from syncbridge.ts ── */
|
|
975
|
-
|
|
976
|
-
import type { syncbridgeconflict, syncbridgehook } from "./types.js";
|
|
977
|
-
import { syncbridgeoptingate, syncbridgescopegate } from "./policy.js";
|
|
978
|
-
|
|
979
|
-
/**
|
|
980
|
-
* Syncbridge logic of the 1.1.66 family.
|
|
981
|
-
* The manifest sync between machines lives here: every hook declares its provider, direction and state behind an explicit opt in with no default on, the file provider moves manifests through manual import and export while the web provider stays a stub behind the opt in gate, the digest of every manifest detects conflicts and both versions surface instead of a silent overwrite, and the scope gate refuses any payload that carries a secret or a log entry because the bridge moves manifests only.
|
|
982
|
-
* Nothing is hardcoded: the endpoint of every provider stays the user's configured value, and no hook ever turns itself on.
|
|
983
|
-
*/
|
|
984
|
-
|
|
985
|
-
/** Computes the sync digest of one manifest: the canonical body the conflict detection compares across machines. */
|
|
986
|
-
export async function syncdigestof(manifest: flowlibrarymanifest): Promise<string> {
|
|
987
|
-
const { publish, ...body } = manifest;
|
|
988
|
-
void publish;
|
|
989
|
-
return sha256(JSON.stringify(body));
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
/** Builds one syncbridge hook with its opt in off: no hook ever defaults on, the user flips the opt in from the optionspage. */
|
|
993
|
-
export function syncbridgehookof(input: {
|
|
994
|
-
provider: "file" | "web";
|
|
995
|
-
direction: "pull" | "push" | "both";
|
|
996
|
-
endpoint: string;
|
|
997
|
-
now: number;
|
|
998
|
-
}): syncbridgehook {
|
|
999
|
-
if (input.endpoint.trim() === "")
|
|
1000
|
-
throw new Error(
|
|
1001
|
-
`The ${input.provider} hook needs its user configured endpoint; no provider address is ever hardcoded.`,
|
|
1002
|
-
);
|
|
1003
|
-
const gate = syncbridgeoptingate({ optin: false });
|
|
1004
|
-
if (gate.allowed) throw new Error("The syncbridge hook never starts with its opt in on; no hook ever defaults on.");
|
|
1005
|
-
return {
|
|
1006
|
-
id: `sync:${input.provider}:${input.now}`,
|
|
1007
|
-
provider: input.provider,
|
|
1008
|
-
direction: input.direction,
|
|
1009
|
-
optin: false,
|
|
1010
|
-
endpoint: input.endpoint.trim(),
|
|
1011
|
-
state: "idle",
|
|
1012
|
-
createdat: input.now,
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
/** Flips the explicit opt in of one hook: only the user turns a hook on or off and the state resets to idle. */
|
|
1017
|
-
export function syncbridgeoptinflip(hook: syncbridgehook, optin: boolean): syncbridgehook {
|
|
1018
|
-
return { ...hook, optin, state: "idle" };
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
/** Reads the provider registry the optionspage lists: the file provider with its manual import and export and the web provider stub behind the opt in gate. */
|
|
1022
|
-
export function syncbridgeproviders(): Array<{
|
|
1023
|
-
provider: "file" | "web";
|
|
1024
|
-
label: string;
|
|
1025
|
-
operations: string[];
|
|
1026
|
-
note: string;
|
|
1027
|
-
}> {
|
|
1028
|
-
return [
|
|
1029
|
-
{
|
|
1030
|
-
provider: "file",
|
|
1031
|
-
label: "File provider",
|
|
1032
|
-
operations: ["pull", "push", "list"],
|
|
1033
|
-
note: "The file provider moves manifests through manual import and export files the user picks; the bridge carries no secret and no log entry under any flag.",
|
|
1034
|
-
},
|
|
1035
|
-
{
|
|
1036
|
-
provider: "web",
|
|
1037
|
-
label: "Web provider stub",
|
|
1038
|
-
operations: ["pull", "push", "list"],
|
|
1039
|
-
note: "The web provider stays a stub behind its opt in gate: the endpoint stays the user's configured registry value and no network call ships before the ecosystem part two backend exists.",
|
|
1040
|
-
},
|
|
1041
|
-
];
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
/** Exports one manifest list as the syncbridge file payload: the manifests travel with their digests while secrets and logs never enter the payload under any flag. */
|
|
1045
|
-
export async function syncbridgeexportpayload(
|
|
1046
|
-
manifests: flowlibrarymanifest[],
|
|
1047
|
-
): Promise<{
|
|
1048
|
-
version: 1;
|
|
1049
|
-
kind: "syncbridge";
|
|
1050
|
-
manifests: flowlibrarymanifest[];
|
|
1051
|
-
digests: string[];
|
|
1052
|
-
exclusions: string[];
|
|
1053
|
-
}> {
|
|
1054
|
-
const digests: string[] = [];
|
|
1055
|
-
for (const manifest of manifests) digests.push(await syncdigestof(manifest));
|
|
1056
|
-
return {
|
|
1057
|
-
version: 1 as const,
|
|
1058
|
-
kind: "syncbridge" as const,
|
|
1059
|
-
manifests,
|
|
1060
|
-
digests,
|
|
1061
|
-
exclusions: ["secretvault values", "logs"],
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
/** Validates one syncbridge payload for import: the scope gate refuses a payload that carries secrets or log entries in full while a manifest only payload validates with its digest list. */
|
|
1066
|
-
export function syncbridgevalidate(payload: {
|
|
1067
|
-
kind?: string;
|
|
1068
|
-
manifests?: unknown;
|
|
1069
|
-
digests?: unknown;
|
|
1070
|
-
exclusions?: unknown;
|
|
1071
|
-
secrets?: unknown;
|
|
1072
|
-
logs?: unknown;
|
|
1073
|
-
}): { ok: boolean; reason: string } {
|
|
1074
|
-
const carriessecrets =
|
|
1075
|
-
payload.secrets !== undefined ||
|
|
1076
|
-
(Array.isArray(payload.manifests) && payload.manifests.some((manifest) => secretcarrying(manifest)));
|
|
1077
|
-
const gate = syncbridgescopegate({ carriessecrets, carrieslogs: payload.logs !== undefined });
|
|
1078
|
-
if (!gate.allowed) return { ok: false, reason: gate.reason ?? "The syncbridge payload refuses." };
|
|
1079
|
-
if (payload.kind !== "syncbridge")
|
|
1080
|
-
return { ok: false, reason: "The syncbridge payload names its kind; a foreign payload never imports." };
|
|
1081
|
-
if (!Array.isArray(payload.manifests))
|
|
1082
|
-
return { ok: false, reason: "The syncbridge payload carries its manifest list." };
|
|
1083
|
-
return {
|
|
1084
|
-
ok: true,
|
|
1085
|
-
reason: `The syncbridge payload carries ${payload.manifests.length} manifest${payload.manifests.length === 1 ? "" : "s"} and no secret and no log entry; the bridge moves manifests only.`,
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
/** Detects whether one record carries a secret vault value by its key shapes so the bridge refuses before any payload ships. */
|
|
1090
|
-
function secretcarrying(record: unknown): boolean {
|
|
1091
|
-
if (record === null || typeof record !== "object") return false;
|
|
1092
|
-
const entries = Object.entries(record as Record<string, unknown>);
|
|
1093
|
-
const secretkeys = ["secret", "token", "password", "apikey", "authorization"];
|
|
1094
|
-
return entries.some(
|
|
1095
|
-
([key, value]) =>
|
|
1096
|
-
secretkeys.some((shape) => key.toLowerCase().includes(shape)) && typeof value === "string" && value.trim() !== "",
|
|
1097
|
-
);
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
/** Runs one pull or push scan of a hook and returns its conflict records instead of a silent overwrite: the digest comparison of every manifest id surfaces both versions whenever the digests differ. */
|
|
1101
|
-
export async function syncbridgescan(input: {
|
|
1102
|
-
hook: syncbridgehook;
|
|
1103
|
-
local: Array<{ manifestid: string; version: string; digest: string }>;
|
|
1104
|
-
remote: Array<{ manifestid: string; version: string; digest: string }>;
|
|
1105
|
-
now: number;
|
|
1106
|
-
}): Promise<{ conflicts: syncbridgeconflict[]; synced: string[]; reason: string }> {
|
|
1107
|
-
const gate = syncbridgeoptingate({ optin: input.hook.optin });
|
|
1108
|
-
if (!gate.allowed) return { conflicts: [], synced: [], reason: gate.reason ?? "" };
|
|
1109
|
-
const conflicts: syncbridgeconflict[] = [];
|
|
1110
|
-
const synced: string[] = [];
|
|
1111
|
-
for (const remotemanifest of input.remote) {
|
|
1112
|
-
const localmanifest = input.local.find((candidate) => candidate.manifestid === remotemanifest.manifestid);
|
|
1113
|
-
if (localmanifest === undefined) {
|
|
1114
|
-
synced.push(remotemanifest.manifestid);
|
|
1115
|
-
continue;
|
|
1116
|
-
}
|
|
1117
|
-
if (localmanifest.digest === remotemanifest.digest) {
|
|
1118
|
-
synced.push(remotemanifest.manifestid);
|
|
1119
|
-
continue;
|
|
1120
|
-
}
|
|
1121
|
-
conflicts.push({
|
|
1122
|
-
id: `conflict:${remotemanifest.manifestid}:${input.now}`,
|
|
1123
|
-
hookid: input.hook.id,
|
|
1124
|
-
manifestid: remotemanifest.manifestid,
|
|
1125
|
-
local: { digest: localmanifest.digest, version: localmanifest.version },
|
|
1126
|
-
remote: { digest: remotemanifest.digest, version: remotemanifest.version },
|
|
1127
|
-
detectedat: input.now,
|
|
1128
|
-
});
|
|
1129
|
-
}
|
|
1130
|
-
return {
|
|
1131
|
-
conflicts,
|
|
1132
|
-
synced,
|
|
1133
|
-
reason: `The ${input.hook.provider} hook of ${input.hook.endpoint} scanned ${input.remote.length} remote manifest${input.remote.length === 1 ? "" : "s"} against ${input.local.length} local entr${input.local.length === 1 ? "y" : "ies"}: ${synced.length} moved while ${conflicts.length} conflict${conflicts.length === 1 ? "" : "s"} surfaced both versions instead of a silent overwrite.`,
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
/** Resolves one syncbridge conflict with a distinct user action: local keeps the local manifest, remote takes the remote one and merge defers the union to the user editor. */
|
|
1138
|
-
export function resolveconflict(
|
|
1139
|
-
conflict: syncbridgeconflict,
|
|
1140
|
-
resolution: "local" | "remote" | "merge",
|
|
1141
|
-
now: number,
|
|
1142
|
-
): syncbridgeconflict {
|
|
1143
|
-
if (conflict.resolution !== undefined)
|
|
1144
|
-
throw new Error(
|
|
1145
|
-
`The conflict ${conflict.id} already resolved at ${conflict.resolvedat}; one conflict resolves exactly once.`,
|
|
1146
|
-
);
|
|
1147
|
-
return { ...conflict, resolution, resolvedat: now };
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
/** Lists the conflicts of one hook the optionspage shows with their resolve actions. */
|
|
1151
|
-
export function conflictsfor(conflicts: syncbridgeconflict[], hookid?: string): syncbridgeconflict[] {
|
|
1152
|
-
return hookid === undefined ? conflicts : conflicts.filter((conflict) => conflict.hookid === hookid);
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
/** Reads the web provider stub descriptor: the stub names its operations and stays honest that no network call ships before its backend exists. */
|
|
1156
|
-
export function webproviderstub(hook: syncbridgehook): {
|
|
1157
|
-
hookid: string;
|
|
1158
|
-
endpoint: string;
|
|
1159
|
-
optin: boolean;
|
|
1160
|
-
operations: string[];
|
|
1161
|
-
stub: true;
|
|
1162
|
-
reason: string;
|
|
1163
|
-
} {
|
|
1164
|
-
const gate = syncbridgeoptingate({ optin: hook.optin });
|
|
1165
|
-
return {
|
|
1166
|
-
hookid: hook.id,
|
|
1167
|
-
endpoint: hook.endpoint,
|
|
1168
|
-
optin: hook.optin,
|
|
1169
|
-
operations: ["pull", "push", "list"],
|
|
1170
|
-
stub: true,
|
|
1171
|
-
reason: gate.allowed
|
|
1172
|
-
? `The web provider of ${hook.endpoint} stays a stub behind its explicit opt in; the ecosystem part two backend brings its network path.`
|
|
1173
|
-
: (gate.reason ?? ""),
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
/** Reads the file provider adapter of one hook: pull parses a dropped sync payload, push serializes the manifest list and list reads the local manifests; every operation moves manifests only. */
|
|
1178
|
-
export function fileproviderof(hook: syncbridgehook): {
|
|
1179
|
-
hookid: string;
|
|
1180
|
-
operations: Array<{ kind: "pull" | "push" | "list"; label: string }>;
|
|
1181
|
-
} {
|
|
1182
|
-
return {
|
|
1183
|
-
hookid: hook.id,
|
|
1184
|
-
operations: [
|
|
1185
|
-
{ kind: "pull", label: "Pull the manifests of one dropped syncbridge file" },
|
|
1186
|
-
{ kind: "push", label: "Push the local manifests into one export file" },
|
|
1187
|
-
{ kind: "list", label: "List the manifests the hook carries" },
|
|
1188
|
-
],
|
|
1189
|
-
};
|
|
1190
|
-
}
|