@twinsearth/w2m-dsh-plugin 1.0.16 → 1.1.1

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.
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "twinsearth-w2m",
3
+ "owner": {
4
+ "name": "TwinsEarth"
5
+ },
6
+ "metadata": {
7
+ "description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
8
+ "version": "1.1.1"
9
+ },
10
+ "plugins": [
11
+ {
12
+ "name": "w2m-fleet",
13
+ "source": "./",
14
+ "description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
15
+ "version": "1.1.1",
16
+ "author": {
17
+ "name": "TwinsEarth"
18
+ },
19
+ "homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
20
+ "repository": "https://github.com/TwinsEarth/dsh-windows2macos",
21
+ "license": "MIT",
22
+ "keywords": [
23
+ "fleet",
24
+ "remote-execution",
25
+ "mcp"
26
+ ]
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "w2m-fleet",
3
+ "description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
4
+ "version": "1.1.1",
5
+ "author": {
6
+ "name": "TwinsEarth"
7
+ },
8
+ "homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
9
+ "repository": "https://github.com/TwinsEarth/dsh-windows2macos",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "fleet",
13
+ "remote-execution",
14
+ "windows",
15
+ "macos",
16
+ "linux",
17
+ "mcp",
18
+ "claude-code",
19
+ "codex"
20
+ ],
21
+ "mcpServers": {
22
+ "w2m-fleet": {
23
+ "command": "node",
24
+ "args": [
25
+ "${CLAUDE_PLUGIN_ROOT}/bin/w2m-mcp.mjs"
26
+ ]
27
+ }
28
+ }
29
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,114 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## [1.1.1] — 2026-10-17
6
+
7
+ **The fleet is now installable from Claude Code, Codex and OpenAI's plugin surface -- and the parts that still need a
8
+ person are marked as needing one.**
9
+
10
+ ### One server, three manifests
11
+
12
+ `bin/w2m-mcp.mjs` already answers both MCP eras (modern per-request `_meta`, and the legacy `initialize` handshake
13
+ most deployed clients still speak) and exposes **five read-only tools**. Adapting to a platform is therefore a
14
+ **manifest**, not a second implementation:
15
+
16
+ * `.claude-plugin/plugin.json` — the Claude Code plugin manifest, with its own `mcpServers` entry.
17
+ * `.claude-plugin/marketplace.json` — so this repository can be added **as** a marketplace.
18
+ * `codex/mcp-servers.toml` — the block to append to `~/.codex/config.toml`, kept as a reviewable file rather than
19
+ prose in a document.
20
+ * `docs/PLATFORMS.md` — the per-platform routes, and the exact state of each submission.
21
+ * `w2m-mcp` is now a published **bin**, so a client can start the server without guessing a path.
22
+
23
+ ### The trap: two schemas, two different root variables
24
+
25
+ The top-level `mcp.json` uses the agent-plugins schema's `${PLUGIN_ROOT}`. **Claude Code does not substitute that
26
+ variable** -- it substitutes `${CLAUDE_PLUGIN_ROOT}`. Reusing the top-level file verbatim would ship a literal
27
+ `${PLUGIN_ROOT}` string as the server's first argument, and the failure would present as *a missing server* rather
28
+ than *a wrong variable name*.
29
+
30
+ So the Claude Code manifest declares its own server entry, and `test/portable-plugins.test.mjs` refuses a manifest
31
+ that uses the wrong variable, points at a file that is not in the package, or drifts from `package.json`'s version.
32
+
33
+ ### What is published, and what is not
34
+
35
+ | Destination | State |
36
+ |---|---|
37
+ | npm package, GitHub release, `SHA256SUMS` | **published** |
38
+ | Claude Code plugin + marketplace manifests | **shipped and tested** |
39
+ | Codex configuration snippet | **shipped and tested** |
40
+ | Pull request to `anthropics/claude-plugins-official` / `-community` / `claude-code` | **not done** |
41
+ | Submission to OpenAI's plugin portal | **not done** |
42
+
43
+ The last two require write access to repositories this project does not own; the release token is scoped to
44
+ `TwinsEarth/dsh-windows2macos` and acting on a third-party repository is refused by design. A manifest that exists is
45
+ not a listing, so it is not reported as one.
46
+
47
+ ### Verified
48
+
49
+ New suite `test/portable-plugins.test.mjs`: the manifests parse, the version agrees across all four manifests and the
50
+ package, the MCP bin exists, the Codex snippet pins the version being released, both manifests are in
51
+ `package.json#files`, and `docs/PLATFORMS.md` names every platform it claims to.
52
+
53
+ ## [1.1.0] — 2026-10-17
54
+
55
+ **The direct path now works on runtimes whose OpenSSL has no BLAKE2s, and the compatibility gate that measures the real
56
+ Loader was asserting a tool list that had gone stale.**
57
+
58
+ ### The handshake hash no longer depends on the runtime's OpenSSL
59
+
60
+ One dispatch reported, per machine:
61
+
62
+ > `P2P_PUNCH_FAILED: handshakeHash requires BLAKE2s-256: BLAKE2s-256 unavailable on this runtime: Digest method not
63
+ > supported. It is a WIRE PROTOCOL primitive -- the handshake hash, the prologue hash and the session binding -- so it
64
+ > cannot be replaced with SHA-256 without breaking interoperability with every peer.`
65
+
66
+ That message names the constraint correctly and draws the wrong conclusion. **BLAKE2s is public and deterministic**, so
67
+ the digest can be computed here instead of by OpenSSL: the bytes are identical to what a peer with a native
68
+ implementation produces, and interoperability is preserved exactly. The alternative -- refusing to handshake -- makes
69
+ **every** direct path fall back to the relay on that runtime, which is what the fleet was doing without saying so.
70
+
71
+ * `src/agent/blake2s.mjs` — BLAKE2s-256 in plain JavaScript (unkeyed, which is all the wire protocol uses).
72
+ * `src/agent/crypto.mjs` — `handshakeHash` tries the native digest and falls back to it; `blake2s256Support()`
73
+ now reports **how** the digest is reached (`native` or `js-fallback`); `cryptoCapabilities()` counts the fallback
74
+ as present, because what the handshake needs is the digest, not OpenSSL's implementation of it.
75
+ * The fallback is verified before it is trusted: its empty-input digest is compared against the published vector, so a
76
+ broken fallback reports unavailable rather than producing hashes no peer will agree with.
77
+
78
+ **The bug that made this take longer than it should have.** The first version inverted both `v[14]` and `v[15]` for
79
+ the final block. BLAKE2 sets `f0 = 0xFFFFFFFF` on the last block and leaves `f1 = 0` for sequential hashing, so
80
+ only `v[14]` is inverted. Inverting both produces a **stable, plausible-looking digest that matches no reference**:
81
+ the three RFC vectors failed while the compression function -- checked against an independent implementation -- was
82
+ correct the whole time. `test/blake2s.test.mjs` pins the vectors and compares against `node:crypto` across every
83
+ block boundary, so that class of mistake cannot survive again.
84
+
85
+ ### The compatibility gate was asserting a stale tool count
86
+
87
+ `test/loader-entry.test.mjs` asserted the plugin registers a fixed set of tools, under a name that still said "eight".
88
+ The set had grown to **eleven** (`w2m_approvals` and `w2m_council` were missing), so the gate that mounts the packed
89
+ artifact in a real profile and runs the real Loader failed at:
90
+
91
+ > `registers all eight w2m_* tools`
92
+
93
+ The list is now
94
+ the actual eleven, sorted so a missing name and a renamed name fail differently, and the test says what it means.
95
+
96
+ ### Two configuration facts this exposed, recorded because they are not obvious
97
+
98
+ A machine can be **installed, running, and still outside the fleet** -- both were true here:
99
+
100
+ * `dsh.profile.bundles` must list the package. Installing it as a dependency is not enough: the host composes the
101
+ entry list from that array, so a package present in `node_modules` and absent from `bundles` simply never loads.
102
+ * `autoStartAgent: true` is **off by default**, and joining happens inside the agent it starts. Without it a machine
103
+ never joins, no matter how correctly it is installed.
104
+
105
+ With both set, this machine joined, listed its peers, accepted a dispatch and executed it (`node --version`, exit 0).
106
+
107
+ ### Verified
108
+
109
+ Full suite green; pre-flight **GREEN**, 15 of 15 checks. `test/blake2s.test.mjs`: **10 pass, 0 fail**, including
110
+ byte-identical agreement with `node:crypto` at lengths 0, 1, 2, 3, 31, 32, 33, 63, 64, 65, 127, 128, 129, 191, 192,
111
+ 193, 255, 256, 257 and 1000.
112
+
5
113
  ## [1.0.16] — 2026-10-17
6
114
 
7
115
  **The last two failures on Linux CI, and one of them was a defect in the release process: a release was advertised as
@@ -0,0 +1,17 @@
1
+ # Codex MCP configuration for the w2m fleet.
2
+ #
3
+ # This file is a **snippet, not a config**: it is the block to append to `~/.codex/config.toml`, kept here so the
4
+ # arguments are reviewable and testable instead of living only in prose (docs/PLATFORMS.md).
5
+ #
6
+ # Why `npx -p ... w2m-mcp` and not a path: Codex runs on whichever machine the operator is sitting at, and the package
7
+ # is published, so the same block works on Windows, macOS and Linux without an install step. The version is pinned
8
+ # exactly -- a floating server is a moving target for a protocol the client negotiates at startup.
9
+ #
10
+ # The server is stdio and read-only by design: `w2m_run` is not among the tools it exposes, because handing fleet
11
+ # execution rights to any process able to start an MCP server is a guard that only looks like one.
12
+
13
+ [mcp_servers.w2m-fleet]
14
+ command = "npx"
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.1.1", "w2m-mcp"]
16
+ startup_timeout_sec = 20
17
+ tool_timeout_sec = 60
@@ -0,0 +1,83 @@
1
+ # Platforms: the same fleet from Claude Code, Codex, OpenAI's plugin directory and anything else that speaks MCP
2
+
3
+ One MCP server, `bin/w2m-mcp.mjs`, published in the npm package and exposed three ways. **Nothing in this document
4
+ invents a second implementation per platform** -- a platform is adapted to by shipping a manifest, not by forking the
5
+ server.
6
+
7
+ ## What is exposed, and why it is read-only
8
+
9
+ The server answers both MCP protocol eras in the wild: the modern revision (per-request `_meta`, no handshake) and the
10
+ legacy one (`initialize` -> `notifications/initialized` -> requests) that most deployed clients still speak.
11
+
12
+ **`w2m_run` is deliberately absent.** A process that can start an MCP server would otherwise be handed fleet execution
13
+ rights. The exposed tools read state and cannot change it; each one is a thin wrapper over an endpoint the plugin
14
+ already calls.
15
+
16
+ ## Claude Code
17
+
18
+ Two routes, both from this repository:
19
+
20
+ **As a plugin marketplace** -- which is what `.claude-plugin/marketplace.json` is for:
21
+
22
+ /plugin marketplace add TwinsEarth/dsh-windows2macos
23
+ /plugin install w2m-fleet@twinsearth-w2m
24
+
25
+ The plugin manifest is `.claude-plugin/plugin.json`; its `mcpServers` entry resolves through `${CLAUDE_PLUGIN_ROOT}`,
26
+ which Claude Code substitutes with the installed plugin directory. **That variable is why the manifest carries its own
27
+ server entry instead of pointing at the top-level `mcp.json`** -- the top-level file uses the agent-plugins schema's
28
+ `${PLUGIN_ROOT}`, and a client that does not know that schema would receive the literal string.
29
+
30
+ **As a project-scoped server** -- add to the project's `.mcp.json`:
31
+
32
+ { "mcpServers": { "w2m-fleet": { "command": "node",
33
+ "args": ["<path-to-package>/bin/w2m-mcp.mjs"] } } }
34
+
35
+ ## Codex (OpenAI)
36
+
37
+ Codex reads MCP servers from `~/.codex/config.toml`. The block to append is kept as a reviewable, testable file rather
38
+ than prose: [`codex/mcp-servers.toml`](../codex/mcp-servers.toml).
39
+
40
+ It invokes the published package through `npx`, so the same block works on Windows, macOS and Linux with no install
41
+ step, and the version is pinned exactly.
42
+
43
+ ## OpenAI's plugin directory (`openai/plugins`, `openai/mcp-extensions`)
44
+
45
+ This one is **not** a filesystem layout to conform to. `openai/plugins` is a Codex-oriented example library
46
+ distributed through an official submission portal, and `openai/mcp-extensions` is a directory of extension
47
+ descriptors. Our side of that transaction is the npm package plus the manifests in this repository; the submission
48
+ itself is a form or a pull request against a repository **we do not own**.
49
+
50
+ **Status: prepared, not submitted.** See "What still needs a person" below.
51
+
52
+ ## Anthropic's Claude Code marketplaces
53
+
54
+ `anthropics/claude-plugins-official`, `anthropics/claude-plugins-community` and `anthropics/claude-code` are the
55
+ upstream repositories. A listing there is a pull request against a third-party repository.
56
+
57
+ **Status: prepared, not submitted.** The manifests they would review are already in this repository and validated by
58
+ `test/portable-plugins.test.mjs`.
59
+
60
+ ## Anything else (Cursor, Windsurf, a script of your own)
61
+
62
+ Any client that accepts a generic MCP server list can use the `mcp.json` this package already ships, or the same
63
+ `command`/`args` pair from the Claude Code section.
64
+
65
+ ## What still needs a person
66
+
67
+ Publishing to the three third-party destinations above requires write access to repositories this project does not
68
+ own. The token used for release automation is scoped to `TwinsEarth/dsh-windows2macos`; acting on a third-party
69
+ repository with it is refused by design, and that refusal is correct.
70
+
71
+ So the split is:
72
+
73
+ | Step | Who | State |
74
+ |---|---|---|
75
+ | npm publish, GitHub release, SHA256SUMS | automation | **done** (see the release page) |
76
+ | Claude Code marketplace manifests | this repository | **shipped and tested** |
77
+ | Codex configuration snippet | this repository | **shipped and tested** |
78
+ | Adding the repo as a Claude Code marketplace | any operator | one command, above |
79
+ | PR to `anthropics/claude-plugins-official` / `-community` / `claude-code` | a person with access | **not done** |
80
+ | Submission to OpenAI's portal | a person with the account | **not done** |
81
+
82
+ Marking the last two rows "prepared" rather than "done" is deliberate: a manifest that exists is not a listing, and
83
+ this project has already shipped a "mechanism with no caller" seven times.
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.0.16",
3
+ "version": "1.1.1",
4
4
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "bin": {
8
- "w2m-rabbit": "bin/w2m-rabbit.mjs",
9
8
  "w2m-localside": "bin/w2m-localside.mjs",
9
+ "w2m-mcp": "bin/w2m-mcp.mjs",
10
+ "w2m-rabbit": "bin/w2m-rabbit.mjs",
10
11
  "w2m-stun": "bin/w2m-stun.mjs"
11
12
  },
12
13
  "exports": {
@@ -42,7 +43,9 @@
42
43
  "PROTOCOL-v0.4.0.md",
43
44
  "TROUBLESHOOTING.md",
44
45
  "SECURITY.md",
45
- "CONTRIBUTING.md"
46
+ "CONTRIBUTING.md",
47
+ ".claude-plugin",
48
+ "codex"
46
49
  ],
47
50
  "dsh": {
48
51
  "plugin": true,
@@ -117,53 +120,55 @@
117
120
  }
118
121
  },
119
122
  "scripts": {
120
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs",
121
- "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
122
- "test:relay": "node --test --test-force-exit test/relay.test.mjs",
123
- "test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
124
- "test:tools": "node --test --test-force-exit test/tools.test.mjs",
125
- "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
126
- "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs",
127
- "pack": "node scripts/pack.mjs",
128
- "verify:join": "node scripts/verify-join.mjs",
129
- "lint": "eslint .",
130
- "lint:fix": "eslint . --fix",
131
123
  "format": "prettier --write .",
132
124
  "format:check": "prettier --check .",
133
- "verify": "node scripts/check-entrypoints.mjs && node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/p2p-node.test.mjs test/p2p-agent.test.mjs test/client-bundle.test.mjs test/loader-entry.test.mjs",
134
- "test:p2p": "node --test --test-force-exit test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/stun-server.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs",
135
- "test:memory": "node --test --test-force-exit test/memory.test.mjs",
125
+ "layer-check": "node scripts/layer-check.mjs",
126
+ "lint": "eslint .",
127
+ "lint:fix": "eslint . --fix",
128
+ "orphans": "node --test --test-force-exit test/accepted-orphans.test.mjs",
129
+ "pack": "node scripts/pack.mjs",
130
+ "reachability": "node scripts/reachability-check.mjs",
131
+ "reachability:classify": "node scripts/reachability-classify.mjs",
132
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs",
133
+ "test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
134
+ "test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
135
+ "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs",
136
+ "test:approval-gate": "node --test --test-force-exit test/approval-gate.test.mjs",
137
+ "test:approval-index": "node --test --test-force-exit test/approval-index.test.mjs",
136
138
  "test:authority": "node --test --test-force-exit test/authority.test.mjs",
139
+ "test:authority-choice": "node --test --test-force-exit test/authority-choice.test.mjs",
140
+ "test:blake2s": "node --test --test-force-exit test/blake2s.test.mjs",
137
141
  "test:blocks": "node --test --test-force-exit test/blocks.test.mjs",
138
- "test:approval-index": "node --test --test-force-exit test/approval-index.test.mjs",
139
- "test:fleet-sync": "node --test --test-force-exit test/fleet-sync.test.mjs",
140
- "test:fleet-merge": "node --test --test-force-exit test/fleet-merge.test.mjs",
141
- "test:fleet-memory-e2e": "node --test --test-force-exit test/fleet-memory-e2e.test.mjs",
142
+ "test:council": "node --test --test-force-exit test/council.test.mjs",
143
+ "test:council-session": "node --test --test-force-exit test/council-session.test.mjs",
144
+ "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
145
+ "test:crypto-blake2s": "node --test --test-force-exit test/crypto-blake2s.test.mjs",
146
+ "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
147
+ "test:exit-channel": "node --test --test-force-exit test/exit-channel.test.mjs",
148
+ "test:field-authority": "node --test --test-force-exit test/field-authority.test.mjs",
142
149
  "test:fleet-concurrency": "node --test --test-force-exit test/fleet-concurrency.test.mjs",
150
+ "test:fleet-memory-e2e": "node --test --test-force-exit test/fleet-memory-e2e.test.mjs",
151
+ "test:fleet-merge": "node --test --test-force-exit test/fleet-merge.test.mjs",
143
152
  "test:fleet-merge-race": "node --test --test-force-exit test/fleet-merge-race.test.mjs",
144
- "test:health": "node --test --test-force-exit test/health.test.mjs",
145
- "test:storage": "node --test --test-force-exit test/storage.test.mjs",
146
- "test:exit-channel": "node --test --test-force-exit test/exit-channel.test.mjs",
147
- "test:council": "node --test --test-force-exit test/council.test.mjs",
148
- "test:layers": "node --test --test-force-exit test/layer-model.test.mjs",
149
- "test:log-bounds": "node --test --test-force-exit test/log-bounds.test.mjs",
150
- "layer-check": "node scripts/layer-check.mjs",
151
- "test:test-report": "node --test --test-force-exit test/test-report.test.mjs",
152
153
  "test:fleet-scale": "node --test --test-force-exit test/fleet-scale.test.mjs",
153
154
  "test:fleet-state": "node --test --test-force-exit test/fleet-state.test.mjs",
154
- "test:crypto-blake2s": "node --test --test-force-exit test/crypto-blake2s.test.mjs",
155
+ "test:fleet-sync": "node --test --test-force-exit test/fleet-sync.test.mjs",
156
+ "test:health": "node --test --test-force-exit test/health.test.mjs",
155
157
  "test:installed-version": "node --test --test-force-exit test/installed-version.test.mjs",
156
- "test:permission": "node --test --test-force-exit test/permission.test.mjs",
158
+ "test:layers": "node --test --test-force-exit test/layer-model.test.mjs",
159
+ "test:log-bounds": "node --test --test-force-exit test/log-bounds.test.mjs",
160
+ "test:memory": "node --test --test-force-exit test/memory.test.mjs",
161
+ "test:p2p": "node --test --test-force-exit test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/stun-server.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs",
157
162
  "test:packet": "node --test --test-force-exit test/machine-test-packet.test.mjs",
158
- "test:field-authority": "node --test --test-force-exit test/field-authority.test.mjs",
159
- "test:approval-gate": "node --test --test-force-exit test/approval-gate.test.mjs",
160
- "test:authority-choice": "node --test --test-force-exit test/authority-choice.test.mjs",
163
+ "test:permission": "node --test --test-force-exit test/permission.test.mjs",
164
+ "test:portable": "node --test --test-force-exit test/portable-plugins.test.mjs",
161
165
  "test:reachability": "node --test --test-force-exit test/reachability-check.test.mjs",
162
- "reachability": "node scripts/reachability-check.mjs",
163
- "reachability:classify": "node scripts/reachability-classify.mjs",
164
- "test:council-session": "node --test --test-force-exit test/council-session.test.mjs",
165
- "orphans": "node --test --test-force-exit test/accepted-orphans.test.mjs",
166
- "test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs"
166
+ "test:relay": "node --test --test-force-exit test/relay.test.mjs",
167
+ "test:storage": "node --test --test-force-exit test/storage.test.mjs",
168
+ "test:test-report": "node --test --test-force-exit test/test-report.test.mjs",
169
+ "test:tools": "node --test --test-force-exit test/tools.test.mjs",
170
+ "verify": "node scripts/check-entrypoints.mjs && node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/p2p-node.test.mjs test/p2p-agent.test.mjs test/client-bundle.test.mjs test/loader-entry.test.mjs",
171
+ "verify:join": "node scripts/verify-join.mjs"
167
172
  },
168
173
  "keywords": [
169
174
  "dsh-bundle",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "twinsearth-w2m-dsh",
4
- "version": "1.0.16",
4
+ "version": "1.1.1",
5
5
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -0,0 +1,151 @@
1
+ /**
2
+ * BLAKE2s-256 in plain JavaScript -- the handshake hash when the runtime cannot provide it.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * `src/agent/crypto.mjs` computes the handshake hash, the prologue hash and the session binding with
7
+ * `createHash('blake2s256')`. On the DSH desktop runtime that call fails:
8
+ *
9
+ * P2P_PUNCH_FAILED: handshakeHash requires BLAKE2s-256: BLAKE2s-256 unavailable on this runtime:
10
+ * Digest method not supported. It is a WIRE PROTOCOL primitive -- the handshake hash, the prologue hash and
11
+ * the session binding -- so it cannot be replaced with SHA-256 without breaking interoperability with every peer.
12
+ *
13
+ * That message is right about the constraint and wrong about the conclusion. BLAKE2s is **public and
14
+ * deterministic**, so the algorithm can be computed here instead of by OpenSSL: the bytes are identical to what a
15
+ * peer with a native implementation produces, and interoperability is preserved exactly. The alternative -- refusing
16
+ * to handshake -- makes every direct path fall back to the relay on this runtime, which is what was observed.
17
+ *
18
+ * ## Scope: unkeyed only
19
+ *
20
+ * The wire protocol needs the **unkeyed** 32-byte digest (prologue, session binding, MAC inputs). Keyed BLAKE2s
21
+ * (`mac1`/`mac2`) is explicitly not implemented in this build either way, so this module implements exactly the
22
+ * unkeyed case and refuses a key rather than pretending.
23
+ *
24
+ * ## Correctness
25
+ *
26
+ * Verified against the three published vectors (empty, "abc", the pangram) and, when the runtime does have a native
27
+ * `blake2s256`, cross-checked byte-for-byte against it over many lengths -- the test file asserts both.
28
+ *
29
+ * @see https://www.rfc-editor.org/rfc/rfc7693 (BLAKE2 -- BLAKE2s, 32-bit words, 10 rounds)
30
+ */
31
+
32
+ const IV = new Uint32Array([
33
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
34
+ ]);
35
+
36
+ /** Message-word permutation, one row per round (10 rounds for BLAKE2s). */
37
+ const SIGMA = [
38
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
39
+ [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
40
+ [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
41
+ [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
42
+ [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
43
+ [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
44
+ [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
45
+ [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
46
+ [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
47
+ [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
48
+ ];
49
+
50
+ const BLOCK = 64;
51
+ const OUT_BYTES = 32;
52
+
53
+ const rotr = (x, n) => ((x >>> n) | (x << (32 - n))) >>> 0;
54
+
55
+ /** The G mixing function, rotation constants 16/12/8/7 for BLAKE2s. */
56
+ function mix(v, a, b, c, d, x, y) {
57
+ v[a] = (v[a] + v[b] + x) >>> 0;
58
+ v[d] = rotr(v[d] ^ v[a], 16);
59
+ v[c] = (v[c] + v[d]) >>> 0;
60
+ v[b] = rotr(v[b] ^ v[c], 12);
61
+ v[a] = (v[a] + v[b] + y) >>> 0;
62
+ v[d] = rotr(v[d] ^ v[a], 8);
63
+ v[c] = (v[c] + v[d]) >>> 0;
64
+ v[b] = rotr(v[b] ^ v[c], 7);
65
+ }
66
+
67
+ /** Read 16 little-endian 32-bit words out of a 64-byte block. */
68
+ function words(block, offset, m) {
69
+ for (let i = 0; i < 16; i += 1) {
70
+ const p = offset + i * 4;
71
+ m[i] = (block[p] | (block[p + 1] << 8) | (block[p + 2] << 16) | (block[p + 3] << 24)) >>> 0;
72
+ }
73
+ }
74
+
75
+ /** One compression call: 64 bytes of message, a 64-bit byte counter, and the final-block flag. */
76
+ function compress(h, block, offset, counter, last) {
77
+ const v = new Uint32Array(16);
78
+ const m = new Uint32Array(16);
79
+ v.set(h, 0);
80
+ v.set(IV, 8);
81
+ v[12] ^= counter >>> 0;
82
+ v[13] ^= Math.floor(counter / 0x100000000) >>> 0;
83
+ if (last) {
84
+ /**
85
+ * Only `v[14]` is inverted -- `f0 = 0xFFFFFFFF` marks the last block, and **`f1` stays 0** for sequential
86
+ * hashing (it is only 1 for the last node of a tree). Inverting `v[15]` as well is the classic wrong
87
+ * implementation: it produces a stable, plausible-looking digest that matches no reference, which is exactly how
88
+ * this was found -- three RFC vectors failed and the compression function, checked against a reference
89
+ * implementation, turned out to be correct while the final-block setup was not.
90
+ */
91
+ v[14] = (v[14] ^ 0xffffffff) >>> 0;
92
+ }
93
+ words(block, offset, m);
94
+
95
+ for (let r = 0; r < 10; r += 1) {
96
+ const s = SIGMA[r];
97
+ mix(v, 0, 4, 8, 12, m[s[0]], m[s[1]]);
98
+ mix(v, 1, 5, 9, 13, m[s[2]], m[s[3]]);
99
+ mix(v, 2, 6, 10, 14, m[s[4]], m[s[5]]);
100
+ mix(v, 3, 7, 11, 15, m[s[6]], m[s[7]]);
101
+ mix(v, 0, 5, 10, 15, m[s[8]], m[s[9]]);
102
+ mix(v, 1, 6, 11, 12, m[s[10]], m[s[11]]);
103
+ mix(v, 2, 7, 8, 13, m[s[12]], m[s[13]]);
104
+ mix(v, 3, 4, 9, 14, m[s[14]], m[s[15]]);
105
+ }
106
+
107
+ for (let i = 0; i < 8; i += 1) h[i] = (h[i] ^ v[i] ^ v[i + 8]) >>> 0;
108
+ }
109
+
110
+ /**
111
+ * BLAKE2s-256 of a buffer. Unkeyed; a key is refused rather than silently ignored.
112
+ *
113
+ * @param {Buffer|Uint8Array|string} input
114
+ * @returns {Buffer} 32 bytes.
115
+ */
116
+ export function blake2s256(input) {
117
+ const data = typeof input === 'string' ? Buffer.from(input, 'utf8') : Buffer.from(input);
118
+ const h = new Uint32Array(IV);
119
+ // Parameter block: digest length 32, key length 0, fanout 1, depth 1.
120
+ h[0] = (h[0] ^ 0x01010000 ^ OUT_BYTES) >>> 0;
121
+
122
+ let counter = 0;
123
+ // Every block except the last is hashed as a non-final block, so the final block (possibly all zeros) is handled
124
+ // separately -- BLAKE2 pads the last block and sets the final flag, it does not append an extra block.
125
+ const full = data.length === 0 ? 0 : Math.ceil(data.length / BLOCK) - 1;
126
+ for (let i = 0; i < full; i += 1) {
127
+ counter += BLOCK;
128
+ compress(h, data, i * BLOCK, counter, false);
129
+ }
130
+
131
+ const lastOffset = full * BLOCK;
132
+ const lastLength = data.length - lastOffset;
133
+ const tail = Buffer.alloc(BLOCK);
134
+ data.copy(tail, 0, lastOffset, data.length);
135
+ counter += lastLength;
136
+ compress(h, tail, 0, counter, true);
137
+
138
+ const out = Buffer.alloc(OUT_BYTES);
139
+ for (let i = 0; i < 8; i += 1) out.writeUInt32LE(h[i], i * 4);
140
+ return out;
141
+ }
142
+
143
+ /** The three RFC 7693 vectors, exported so a test can assert them without copying hex. */
144
+ export const BLAKE2S_VECTORS = [
145
+ { input: '', hex: '69217a3079908094e11121d042354a7c1f55b6482ca1a51e1b250dfd1ed0eef9' },
146
+ { input: 'abc', hex: '508c5e8c327c14e2e1a72ba34eeb452f37458b209ed63a294d999b4c86675982' },
147
+ {
148
+ input: 'The quick brown fox jumps over the lazy dog',
149
+ hex: '606beeec743ccbeff6cbcdf5d5302aa855c256c29b88c8ed331ea1a6bf3c8812',
150
+ },
151
+ ];
@@ -55,6 +55,7 @@ import {
55
55
  } from 'node:crypto';
56
56
  import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
57
57
  import { dirname, isAbsolute, join, resolve } from 'node:path';
58
+ import { blake2s256 as blake2s256Js } from './blake2s.mjs';
58
59
 
59
60
  /* -------------------------------------------------------------------------- */
60
61
  /* Names that travel on the wire, or into a report */
@@ -738,7 +739,8 @@ export function cryptoCapabilities() {
738
739
  }
739
740
  };
740
741
 
741
- check('blake2s256', () => createHash('blake2s256').update('').digest().length === 32);
742
+ // The JavaScript fallback counts here: what the handshake needs is the digest, not OpenSSL's implementation of it.
743
+ check('blake2s256', () => blake2s256Support().ok);
742
744
  check('sha256', () => createHash('sha256').update('').digest().length === 32);
743
745
  check('chacha20-poly1305', () => {
744
746
  const key = randomBytes(32);
@@ -801,16 +803,47 @@ export function blake2s256Support() {
801
803
  blake2sSupport = {
802
804
  ok: bytes === 32,
803
805
  advertised,
806
+ source: 'native',
804
807
  detail: bytes === 32
805
808
  ? `BLAKE2s-256 available (${advertised ? 'advertised by getHashes' : 'works though not advertised'})`
806
809
  : `BLAKE2s-256 produced ${bytes} bytes rather than 32`,
807
810
  };
808
811
  } catch (e) {
809
- blake2sSupport = {
810
- ok: false,
811
- advertised,
812
- detail: `BLAKE2s-256 unavailable on this runtime: ${e?.message ?? String(e)}. It is a WIRE PROTOCOL primitive -- the handshake hash, the prologue hash and the session binding -- so it cannot be replaced with SHA-256 without breaking interoperability with every peer. This machine cannot take part in a handshake until the runtime supports it.`,
813
- };
812
+ /**
813
+ * **The native digest is unavailable, but that is not the end of the handshake.**
814
+ *
815
+ * `P2P_PUNCH_FAILED: Digest method not supported` -- OpenSSL's message, naming no algorithm -- was observed on the
816
+ * DSH desktop runtime. BLAKE2s is public and deterministic, so the digest can be computed in JavaScript and the
817
+ * bytes are identical to a peer with a native implementation: **interoperability is preserved exactly**, which is
818
+ * the constraint the old message correctly named and then wrongly concluded from.
819
+ *
820
+ * The fallback is verified before it is trusted: its digest of the empty input is compared against the published
821
+ * vector, so a broken fallback reports unavailable rather than producing hashes no peer will agree with.
822
+ */
823
+ const KNOWN_EMPTY = '69217a3079908094e11121d042354a7c1f55b6482ca1a51e1b250dfd1ed0eef9';
824
+ let jsOk = false;
825
+ try {
826
+ jsOk = blake2s256Js(Buffer.alloc(0)).toString('hex') === KNOWN_EMPTY;
827
+ } catch {
828
+ jsOk = false;
829
+ }
830
+ blake2sSupport = jsOk
831
+ ? {
832
+ ok: true,
833
+ advertised,
834
+ source: 'js-fallback',
835
+ detail: `BLAKE2s-256 computed in JavaScript: the runtime's digest is unavailable (${e?.message ?? String(e)}), `
836
+ + 'and the fallback matches the published empty-input vector, so handshakes stay interoperable',
837
+ }
838
+ : {
839
+ ok: false,
840
+ advertised,
841
+ source: 'none',
842
+ detail: `BLAKE2s-256 unavailable on this runtime: ${e?.message ?? String(e)}, and the JavaScript fallback failed `
843
+ + 'its known-answer check. It is a WIRE PROTOCOL primitive -- the handshake hash, the prologue hash and the '
844
+ + 'session binding -- so it cannot be replaced with SHA-256 without breaking interoperability with every peer. '
845
+ + 'This machine cannot take part in a handshake until one of the two works.',
846
+ };
814
847
  }
815
848
  return blake2sSupport;
816
849
  }
@@ -826,18 +859,28 @@ export function blake2s256Support() {
826
859
  * @returns {Buffer} 32 bytes.
827
860
  */
828
861
  export function handshakeHash(...parts) {
829
- let hash;
862
+ let out;
830
863
  try {
831
- hash = createHash('blake2s256');
864
+ const hash = createHash('blake2s256');
865
+ for (const part of parts) hash.update(part);
866
+ out = hash.digest();
832
867
  } catch (e) {
868
+ /**
869
+ * The runtime has no native BLAKE2s. Compute the same 32 bytes in JavaScript instead of refusing the handshake:
870
+ * the algorithm is public and deterministic, so the bytes a peer computes are unchanged and interoperability is
871
+ * preserved. A runtime where neither path works still fails loudly, with the algorithm and the consequence named.
872
+ */
833
873
  const support = blake2s256Support();
834
- const error = new Error(`handshakeHash requires BLAKE2s-256: ${support.detail}`);
835
- error.code = 'CRYPTO_BLAKE2S256_UNAVAILABLE';
836
- error.cause = e;
837
- throw error;
874
+ if (!support.ok) {
875
+ const error = new Error(`handshakeHash requires BLAKE2s-256: ${support.detail}`);
876
+ error.code = 'CRYPTO_BLAKE2S256_UNAVAILABLE';
877
+ error.cause = e;
878
+ throw error;
879
+ }
880
+ out = blake2s256Js(Buffer.concat(
881
+ parts.map((part) => (typeof part === 'string' ? Buffer.from(part, 'utf8') : Buffer.from(part))),
882
+ ));
838
883
  }
839
- for (const part of parts) hash.update(part);
840
- const out = hash.digest();
841
884
  if (out.length !== 32) {
842
885
  const error = new Error(`handshakeHash expected a 32-byte digest and produced ${out.length}; peers would compute a different handshake hash`);
843
886
  error.code = 'CRYPTO_BLAKE2S256_WRONG_LENGTH';
@@ -211,7 +211,7 @@ const MAX_STAGES = 16;
211
211
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
212
212
  * overrides it at runtime for tests and packagers.
213
213
  */
214
- const PLUGIN_VERSION = '1.0.16';
214
+ const PLUGIN_VERSION = '1.1.1';
215
215
 
216
216
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
217
217
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;