@muxoai/sdk 1.2.3 → 1.3.0

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.
Files changed (183) hide show
  1. package/.agents/plugins/marketplace.json +22 -0
  2. package/.claude-plugin/marketplace.json +17 -0
  3. package/.codex/config.toml +5 -0
  4. package/ARCHITECTURE.md +111 -0
  5. package/README.md +21 -12
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +894 -0
  8. package/dist/config.d.ts +20 -0
  9. package/dist/config.js +89 -0
  10. package/dist/engine.d.ts +156 -0
  11. package/dist/engine.js +876 -0
  12. package/dist/index.d.ts +7 -0
  13. package/dist/index.js +7 -0
  14. package/dist/mcp.d.ts +45 -0
  15. package/dist/mcp.js +914 -0
  16. package/dist/nl.d.ts +34 -0
  17. package/dist/nl.js +577 -0
  18. package/dist/providers/composio-instant.d.ts +14 -0
  19. package/dist/providers/composio-instant.js +814 -0
  20. package/dist/providers/composio-sdk.d.ts +41 -0
  21. package/dist/providers/composio-sdk.js +572 -0
  22. package/dist/providers/microsoft-governance.d.ts +66 -0
  23. package/dist/providers/microsoft-governance.js +415 -0
  24. package/dist/providers/stripe-billing.d.ts +41 -0
  25. package/dist/providers/stripe-billing.js +372 -0
  26. package/dist/providers/stripe-projects.d.ts +53 -0
  27. package/dist/providers/stripe-projects.js +636 -0
  28. package/dist/providers/supermemory.d.ts +64 -0
  29. package/dist/providers/supermemory.js +224 -0
  30. package/{index.d.ts → dist/sdk.d.ts} +77 -1
  31. package/dist/sdk.js +270 -0
  32. package/dist/server.d.ts +10 -0
  33. package/dist/server.js +1277 -0
  34. package/dist/store.d.ts +216 -0
  35. package/dist/store.js +1449 -0
  36. package/dist/types.d.ts +285 -0
  37. package/dist/types.js +1 -0
  38. package/dist/ui/console-html.d.ts +2 -0
  39. package/dist/ui/console-html.js +2343 -0
  40. package/package.json +66 -6
  41. package/plugins/muxo/.app.json +8 -0
  42. package/plugins/muxo/.claude-plugin/plugin.json +16 -0
  43. package/plugins/muxo/.codex-plugin/plugin.json +55 -0
  44. package/plugins/muxo/.mcp.json +40 -0
  45. package/plugins/muxo/assets/logo.png +0 -0
  46. package/plugins/muxo/assets/logo.svg +6 -0
  47. package/plugins/muxo/hooks/hooks.json +27 -0
  48. package/plugins/muxo/hooks/session-start.sh +85 -0
  49. package/plugins/muxo/hooks/user-prompt-submit.sh +45 -0
  50. package/plugins/muxo/mcp.json +38 -0
  51. package/plugins/muxo/plugin.json +59 -0
  52. package/plugins/muxo/skills/muxo/SKILL.md +25 -0
  53. package/plugins/muxo/skills/muxo/references/cli.md +57 -0
  54. package/plugins/muxo/skills/muxo/references/mcp.md +25 -0
  55. package/plugins/muxo/skills/setup/SKILL.md +28 -0
  56. package/src/catalogues/stripe-projects-catalog.json +9832 -0
  57. package/vendor/microsoft-governance/README.md +363 -0
  58. package/vendor/microsoft-governance/dist/audit.d.ts +26 -0
  59. package/vendor/microsoft-governance/dist/audit.d.ts.map +1 -0
  60. package/vendor/microsoft-governance/dist/audit.js +107 -0
  61. package/vendor/microsoft-governance/dist/audit.js.map +1 -0
  62. package/vendor/microsoft-governance/dist/cascade-containment.d.ts +84 -0
  63. package/vendor/microsoft-governance/dist/cascade-containment.d.ts.map +1 -0
  64. package/vendor/microsoft-governance/dist/cascade-containment.js +387 -0
  65. package/vendor/microsoft-governance/dist/cascade-containment.js.map +1 -0
  66. package/vendor/microsoft-governance/dist/client.d.ts +34 -0
  67. package/vendor/microsoft-governance/dist/client.d.ts.map +1 -0
  68. package/vendor/microsoft-governance/dist/client.js +112 -0
  69. package/vendor/microsoft-governance/dist/client.js.map +1 -0
  70. package/vendor/microsoft-governance/dist/context-poisoning.d.ts +58 -0
  71. package/vendor/microsoft-governance/dist/context-poisoning.d.ts.map +1 -0
  72. package/vendor/microsoft-governance/dist/context-poisoning.js +410 -0
  73. package/vendor/microsoft-governance/dist/context-poisoning.js.map +1 -0
  74. package/vendor/microsoft-governance/dist/credential-vault.d.ts +213 -0
  75. package/vendor/microsoft-governance/dist/credential-vault.d.ts.map +1 -0
  76. package/vendor/microsoft-governance/dist/credential-vault.js +567 -0
  77. package/vendor/microsoft-governance/dist/credential-vault.js.map +1 -0
  78. package/vendor/microsoft-governance/dist/discovery.d.ts +80 -0
  79. package/vendor/microsoft-governance/dist/discovery.d.ts.map +1 -0
  80. package/vendor/microsoft-governance/dist/discovery.js +404 -0
  81. package/vendor/microsoft-governance/dist/discovery.js.map +1 -0
  82. package/vendor/microsoft-governance/dist/encryption/channel.d.ts +29 -0
  83. package/vendor/microsoft-governance/dist/encryption/channel.d.ts.map +1 -0
  84. package/vendor/microsoft-governance/dist/encryption/channel.js +78 -0
  85. package/vendor/microsoft-governance/dist/encryption/channel.js.map +1 -0
  86. package/vendor/microsoft-governance/dist/encryption/index.d.ts +9 -0
  87. package/vendor/microsoft-governance/dist/encryption/index.d.ts.map +1 -0
  88. package/vendor/microsoft-governance/dist/encryption/index.js +23 -0
  89. package/vendor/microsoft-governance/dist/encryption/index.js.map +1 -0
  90. package/vendor/microsoft-governance/dist/encryption/mesh-client.d.ts +267 -0
  91. package/vendor/microsoft-governance/dist/encryption/mesh-client.d.ts.map +1 -0
  92. package/vendor/microsoft-governance/dist/encryption/mesh-client.js +1001 -0
  93. package/vendor/microsoft-governance/dist/encryption/mesh-client.js.map +1 -0
  94. package/vendor/microsoft-governance/dist/encryption/ratchet.d.ts +38 -0
  95. package/vendor/microsoft-governance/dist/encryption/ratchet.d.ts.map +1 -0
  96. package/vendor/microsoft-governance/dist/encryption/ratchet.js +223 -0
  97. package/vendor/microsoft-governance/dist/encryption/ratchet.js.map +1 -0
  98. package/vendor/microsoft-governance/dist/encryption/registry-client.d.ts +136 -0
  99. package/vendor/microsoft-governance/dist/encryption/registry-client.d.ts.map +1 -0
  100. package/vendor/microsoft-governance/dist/encryption/registry-client.js +369 -0
  101. package/vendor/microsoft-governance/dist/encryption/registry-client.js.map +1 -0
  102. package/vendor/microsoft-governance/dist/encryption/x3dh.d.ts +80 -0
  103. package/vendor/microsoft-governance/dist/encryption/x3dh.d.ts.map +1 -0
  104. package/vendor/microsoft-governance/dist/encryption/x3dh.js +238 -0
  105. package/vendor/microsoft-governance/dist/encryption/x3dh.js.map +1 -0
  106. package/vendor/microsoft-governance/dist/framework-adapter.d.ts +63 -0
  107. package/vendor/microsoft-governance/dist/framework-adapter.d.ts.map +1 -0
  108. package/vendor/microsoft-governance/dist/framework-adapter.js +253 -0
  109. package/vendor/microsoft-governance/dist/framework-adapter.js.map +1 -0
  110. package/vendor/microsoft-governance/dist/identity.d.ts +124 -0
  111. package/vendor/microsoft-governance/dist/identity.d.ts.map +1 -0
  112. package/vendor/microsoft-governance/dist/identity.js +446 -0
  113. package/vendor/microsoft-governance/dist/identity.js.map +1 -0
  114. package/vendor/microsoft-governance/dist/index.d.ts +39 -0
  115. package/vendor/microsoft-governance/dist/index.d.ts.map +1 -0
  116. package/vendor/microsoft-governance/dist/index.js +95 -0
  117. package/vendor/microsoft-governance/dist/index.js.map +1 -0
  118. package/vendor/microsoft-governance/dist/kill-switch.d.ts +22 -0
  119. package/vendor/microsoft-governance/dist/kill-switch.d.ts.map +1 -0
  120. package/vendor/microsoft-governance/dist/kill-switch.js +60 -0
  121. package/vendor/microsoft-governance/dist/kill-switch.js.map +1 -0
  122. package/vendor/microsoft-governance/dist/lifecycle.d.ts +50 -0
  123. package/vendor/microsoft-governance/dist/lifecycle.d.ts.map +1 -0
  124. package/vendor/microsoft-governance/dist/lifecycle.js +113 -0
  125. package/vendor/microsoft-governance/dist/lifecycle.js.map +1 -0
  126. package/vendor/microsoft-governance/dist/mcp.d.ts +41 -0
  127. package/vendor/microsoft-governance/dist/mcp.d.ts.map +1 -0
  128. package/vendor/microsoft-governance/dist/mcp.js +233 -0
  129. package/vendor/microsoft-governance/dist/mcp.js.map +1 -0
  130. package/vendor/microsoft-governance/dist/metrics.d.ts +123 -0
  131. package/vendor/microsoft-governance/dist/metrics.d.ts.map +1 -0
  132. package/vendor/microsoft-governance/dist/metrics.js +289 -0
  133. package/vendor/microsoft-governance/dist/metrics.js.map +1 -0
  134. package/vendor/microsoft-governance/dist/oci-manifest.d.ts +49 -0
  135. package/vendor/microsoft-governance/dist/oci-manifest.d.ts.map +1 -0
  136. package/vendor/microsoft-governance/dist/oci-manifest.js +182 -0
  137. package/vendor/microsoft-governance/dist/oci-manifest.js.map +1 -0
  138. package/vendor/microsoft-governance/dist/policy-backends/cedar.d.ts +15 -0
  139. package/vendor/microsoft-governance/dist/policy-backends/cedar.d.ts.map +1 -0
  140. package/vendor/microsoft-governance/dist/policy-backends/cedar.js +88 -0
  141. package/vendor/microsoft-governance/dist/policy-backends/cedar.js.map +1 -0
  142. package/vendor/microsoft-governance/dist/policy-backends/opa.d.ts +17 -0
  143. package/vendor/microsoft-governance/dist/policy-backends/opa.d.ts.map +1 -0
  144. package/vendor/microsoft-governance/dist/policy-backends/opa.js +74 -0
  145. package/vendor/microsoft-governance/dist/policy-backends/opa.js.map +1 -0
  146. package/vendor/microsoft-governance/dist/policy.d.ts +77 -0
  147. package/vendor/microsoft-governance/dist/policy.d.ts.map +1 -0
  148. package/vendor/microsoft-governance/dist/policy.js +760 -0
  149. package/vendor/microsoft-governance/dist/policy.js.map +1 -0
  150. package/vendor/microsoft-governance/dist/prompt-defense.d.ts +37 -0
  151. package/vendor/microsoft-governance/dist/prompt-defense.d.ts.map +1 -0
  152. package/vendor/microsoft-governance/dist/prompt-defense.js +229 -0
  153. package/vendor/microsoft-governance/dist/prompt-defense.js.map +1 -0
  154. package/vendor/microsoft-governance/dist/protocol-facets.d.ts +87 -0
  155. package/vendor/microsoft-governance/dist/protocol-facets.d.ts.map +1 -0
  156. package/vendor/microsoft-governance/dist/protocol-facets.js +728 -0
  157. package/vendor/microsoft-governance/dist/protocol-facets.js.map +1 -0
  158. package/vendor/microsoft-governance/dist/rings.d.ts +17 -0
  159. package/vendor/microsoft-governance/dist/rings.d.ts.map +1 -0
  160. package/vendor/microsoft-governance/dist/rings.js +61 -0
  161. package/vendor/microsoft-governance/dist/rings.js.map +1 -0
  162. package/vendor/microsoft-governance/dist/sandbox.d.ts +61 -0
  163. package/vendor/microsoft-governance/dist/sandbox.d.ts.map +1 -0
  164. package/vendor/microsoft-governance/dist/sandbox.js +199 -0
  165. package/vendor/microsoft-governance/dist/sandbox.js.map +1 -0
  166. package/vendor/microsoft-governance/dist/surface-parity.d.ts +45 -0
  167. package/vendor/microsoft-governance/dist/surface-parity.d.ts.map +1 -0
  168. package/vendor/microsoft-governance/dist/surface-parity.js +216 -0
  169. package/vendor/microsoft-governance/dist/surface-parity.js.map +1 -0
  170. package/vendor/microsoft-governance/dist/trust.d.ts +28 -0
  171. package/vendor/microsoft-governance/dist/trust.d.ts.map +1 -0
  172. package/vendor/microsoft-governance/dist/trust.js +209 -0
  173. package/vendor/microsoft-governance/dist/trust.js.map +1 -0
  174. package/vendor/microsoft-governance/dist/types.d.ts +422 -0
  175. package/vendor/microsoft-governance/dist/types.d.ts.map +1 -0
  176. package/vendor/microsoft-governance/dist/types.js +29 -0
  177. package/vendor/microsoft-governance/dist/types.js.map +1 -0
  178. package/vendor/microsoft-governance/dist/verify.d.ts +135 -0
  179. package/vendor/microsoft-governance/dist/verify.d.ts.map +1 -0
  180. package/vendor/microsoft-governance/dist/verify.js +356 -0
  181. package/vendor/microsoft-governance/dist/verify.js.map +1 -0
  182. package/vendor/microsoft-governance/package.json +64 -0
  183. package/index.js +0 -1
@@ -0,0 +1,1001 @@
1
+ "use strict";
2
+ // Copyright (c) Microsoft Corporation.
3
+ // Licensed under the MIT License.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.MeshClient = void 0;
6
+ /**
7
+ * AgentMesh transport client — WebSocket connection to relay with
8
+ * plaintext-peer support, KNOCK pending queue, and wsFactory hook.
9
+ *
10
+ * Spec: docs/specs/AGENTMESH-WIRE-1.0.md Sections 9, 10, 12
11
+ *
12
+ * Features added for AzureClaw compatibility:
13
+ * - plaintextPeers: bypass E2E encryption for legacy peers (e.g., Rust controller)
14
+ * - wsFactory: custom WebSocket constructor for HTTPS_PROXY CONNECT tunneling
15
+ * - KNOCK pending queue: handle race between KNOCK and first message
16
+ */
17
+ const channel_1 = require("./channel");
18
+ const registry_client_1 = require("./registry-client");
19
+ /**
20
+ * Derive the canonical AGT-main agent DID from an Ed25519 public key.
21
+ *
22
+ * Format: `did:mesh:<sha256(public_key)[:32]>` (32 hex chars = 16 bytes
23
+ * of digest). Matches the server's derivation in
24
+ * `agent-governance-python/agent-mesh/src/agentmesh/registry/app.py`
25
+ * (`key_hash = hashlib.sha256(public_key).hexdigest()[:32]`).
26
+ *
27
+ * If `fallback` looks like a `did:mesh:` already (callers that compute
28
+ * it themselves), prefer that; otherwise compute fresh from the public
29
+ * key. Sync function — relies on Node's `node:crypto` or the browser's
30
+ * SubtleCrypto can't be used synchronously so we hand-roll SHA-256 if
31
+ * neither is available. In practice the SDK only runs in Node so this
32
+ * just falls through to `require("node:crypto")`.
33
+ */
34
+ function computeCanonicalDid(ed25519Public, fallback) {
35
+ if (fallback && fallback.startsWith("did:mesh:"))
36
+ return fallback;
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
38
+ const nodeCrypto = require("node:crypto");
39
+ const hex = nodeCrypto.createHash("sha256").update(ed25519Public).digest("hex");
40
+ return `did:mesh:${hex.slice(0, 32)}`;
41
+ }
42
+ /**
43
+ * High-level mesh client for agent-to-agent communication.
44
+ *
45
+ * Manages WebSocket connection to the relay, session establishment
46
+ * (KNOCK + X3DH), and message encryption/decryption. Supports
47
+ * plaintext peers for legacy interop.
48
+ */
49
+ class MeshClient {
50
+ options;
51
+ sessions = new Map();
52
+ plaintextPeers;
53
+ knockPending = new Map();
54
+ knockAccepted = new Set();
55
+ messageHandlers = [];
56
+ knockHandlers = [];
57
+ errorHandlers = [];
58
+ disconnectHandlers = [];
59
+ e2eVerifiedHandlers = [];
60
+ /** Tracks peers whose first encrypted message we've seen — feeds onE2EVerified. */
61
+ e2eVerifiedSet = new Set();
62
+ ws = null;
63
+ connected = false;
64
+ knockTimeout;
65
+ autoReconnect;
66
+ maxReconnectAttempts;
67
+ reconnectBaseDelayMs;
68
+ reconnectMaxDelayMs;
69
+ reconnectAttempts = 0;
70
+ reconnectTimer = null;
71
+ clientInitiatedClose = false;
72
+ preKnockBufferSize;
73
+ preKnockBufferTtlMs;
74
+ maxBufferedPeers;
75
+ /**
76
+ * Per-peer buffer for encrypted message frames that arrived before the
77
+ * peer's KNOCK was processed. Drained when the KNOCK is later accepted.
78
+ * Mirrors vendored agentmesh-sdk patch #16.
79
+ */
80
+ preKnockBuffer = new Map();
81
+ registry;
82
+ autoRegister;
83
+ oneTimePrekeyCount;
84
+ registered = false;
85
+ /**
86
+ * The agent DID used on every wire-level send (`connect.from`,
87
+ * `mesh_send.from`, `knock.from`, …). Starts at `options.agentDid` so
88
+ * back-compat is preserved when no registry is configured or when
89
+ * registering against a pre-2533 registry. When `registerSelf()` runs
90
+ * against a POP-aware registry (AGT main since 2026-05-23), the server
91
+ * returns the canonical `did:mesh:<sha256(public_key)[:32]>` and we
92
+ * adopt it here — every subsequent frame uses the canonical form so
93
+ * relay and registry lookups agree.
94
+ */
95
+ activeDid;
96
+ /**
97
+ * Public read-only view of the active DID. Caller code that previously
98
+ * read `options.agentDid` and stored it should read this instead so
99
+ * post-registration DID swaps are visible.
100
+ */
101
+ get currentDid() {
102
+ return this.activeDid;
103
+ }
104
+ constructor(options) {
105
+ this.options = options;
106
+ // Compute the canonical AGT main DID locally so the connect frame
107
+ // can include the matching `from` field even on the very first
108
+ // connect (registerSelf runs AFTER connect resolves; the relay's
109
+ // POP gate validates `from == "did:mesh:" + sha256(public_key)[:32]`
110
+ // on the connect frame, NOT on the eventual server-derived DID).
111
+ // We replace whatever caller-supplied DID was passed because the
112
+ // POP-aware relay/registry will reject any other format. Pre-POP
113
+ // deployments don't care, so this is back-compat-safe.
114
+ this.activeDid = computeCanonicalDid(options.keyManager.identityKeyEd, options.agentDid);
115
+ this.plaintextPeers = new Set(options.plaintextPeers ?? []);
116
+ this.knockTimeout = options.knockTimeout ?? 10_000;
117
+ this.autoReconnect = options.autoReconnect ?? true;
118
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
119
+ this.reconnectBaseDelayMs = options.reconnectBaseDelayMs ?? 1000;
120
+ this.reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? 60_000;
121
+ this.preKnockBufferSize = options.preKnockBufferSize ?? 5;
122
+ this.preKnockBufferTtlMs = options.preKnockBufferTtlMs ?? 3_000;
123
+ this.maxBufferedPeers = options.maxBufferedPeers ?? 100;
124
+ this.autoRegister = options.autoRegister ?? true;
125
+ this.oneTimePrekeyCount = options.oneTimePrekeyCount ?? 20;
126
+ if (options.registryClient) {
127
+ this.registry = options.registryClient;
128
+ }
129
+ else if (options.registryUrl) {
130
+ // Wire an Ed25519-Timestamp signer to every registry call so
131
+ // `PUT /v1/agents/{did}/prekeys` and the other authed endpoints
132
+ // (heartbeat, reputation) pass `verify_ed25519_timestamp_auth`.
133
+ // The signer uses the same identity key the relay POP / connect-frame
134
+ // signature uses, so the registry can map `authed_did == activeDid`.
135
+ // The caller can override by passing their own `authSigner` via
136
+ // `registryClientOptions`.
137
+ const callerOpts = options.registryClientOptions ?? {};
138
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
139
+ const self = this;
140
+ const authSigner = callerOpts.authSigner ?? {
141
+ get did() { return self.activeDid; }, // late-bind: DID can change after register
142
+ sign: (m) => options.keyManager.signMessage(m),
143
+ };
144
+ this.registry = new registry_client_1.RegistryClient({
145
+ baseUrl: options.registryUrl,
146
+ ...callerOpts,
147
+ authSigner,
148
+ });
149
+ }
150
+ else {
151
+ this.registry = null;
152
+ }
153
+ }
154
+ /**
155
+ * Access the RegistryClient (built from registryUrl or injected via
156
+ * registryClient). Returns null only if MeshClient was constructed
157
+ * without a registry URL or client — which means discover/peer lookup
158
+ * features are disabled.
159
+ */
160
+ getRegistry() {
161
+ return this.registry;
162
+ }
163
+ // ── Plaintext peers ─────────────────────────────────────────────
164
+ addPlaintextPeer(peerId) {
165
+ this.plaintextPeers.add(peerId);
166
+ }
167
+ removePlaintextPeer(peerId) {
168
+ this.plaintextPeers.delete(peerId);
169
+ }
170
+ isPlaintextPeer(peerId) {
171
+ return this.plaintextPeers.has(peerId);
172
+ }
173
+ // ── Connection ──────────────────────────────────────────────────
174
+ async connect() {
175
+ if (this.connected)
176
+ return;
177
+ const wsUrl = this.options.relayUrl.replace(/^http/, "ws") + "/ws";
178
+ const wsFactory = this.options.wsFactory ?? ((url) => new WebSocket(url));
179
+ this.ws = wsFactory(wsUrl);
180
+ await new Promise((resolve, reject) => {
181
+ this.ws.onopen = () => {
182
+ this.connected = true;
183
+ // POP-enabled connect frame (AGT main since 2026-05-23 PR #2632).
184
+ // Pre-POP relays ignore the extra fields — back-compat preserved.
185
+ // The relay verifies:
186
+ // - DID equals `did:mesh:` + sha256(public_key)[:32]
187
+ // - Ed25519 signature over the timestamp string (NOT pub||ts)
188
+ // -- distinct from registry POP which signs pub||ts
189
+ //
190
+ // Encoding gotcha: the relay decodes with stdlib `base64.b64decode`
191
+ // (standard base64 with +/=), while the registry decodes with
192
+ // `urlsafe_b64decode` (base64url with -_). Mismatch in upstream
193
+ // server code; we have to match each endpoint's expectation
194
+ // separately. Emitting standard base64 here so the relay's
195
+ // `base64.b64decode(pub_b64)` and signature decode succeed.
196
+ const pubB64 = Buffer.from(this.options.keyManager.identityKeyEd).toString("base64");
197
+ const tsStr = new Date().toISOString();
198
+ const sig = this.options.keyManager.signMessage(new TextEncoder().encode(tsStr));
199
+ const sigB64 = Buffer.from(sig).toString("base64");
200
+ this.sendFrame({
201
+ v: 1,
202
+ type: "connect",
203
+ from: this.activeDid,
204
+ public_key: pubB64,
205
+ timestamp: tsStr,
206
+ signature: sigB64,
207
+ });
208
+ // Request any messages queued while offline (inbox replay)
209
+ this.sendFrame({
210
+ v: 1,
211
+ type: "fetch_pending",
212
+ from: this.activeDid,
213
+ });
214
+ resolve();
215
+ };
216
+ this.ws.onerror = (e) => {
217
+ // Surface to AzureClaw-style observers BEFORE rejecting connect.
218
+ // Once connect resolves, subsequent ws errors flow through this same path.
219
+ const errEvent = e;
220
+ const detail = errEvent?.message ?? errEvent?.type ?? "ws-error";
221
+ for (const h of this.errorHandlers) {
222
+ try {
223
+ h("ws", this.activeDid, detail);
224
+ }
225
+ catch { /* swallow handler errors */ }
226
+ }
227
+ if (!this.connected)
228
+ reject(new Error(`WebSocket error: ${e}`));
229
+ };
230
+ this.ws.onmessage = (event) => {
231
+ // Guard against malformed frames (upstream #1998) — combines with
232
+ // our additive event-hook handler.
233
+ let frame;
234
+ try {
235
+ frame = JSON.parse(String(event.data));
236
+ }
237
+ catch (err) {
238
+ console.warn(`MeshClient: dropping malformed frame (JSON parse): ${err}`);
239
+ return;
240
+ }
241
+ this.handleFrame(frame).catch((err) => {
242
+ console.warn(`MeshClient: handler error for frame type=${String(frame.type)}: ${err}`);
243
+ });
244
+ };
245
+ this.ws.onclose = (event) => {
246
+ const wasConnected = this.connected;
247
+ this.connected = false;
248
+ if (wasConnected) {
249
+ // Distinguish client-initiated disconnect (1000 Normal Closure) from server / network drops.
250
+ const code = event?.code;
251
+ const isClientInitiated = code === 1000 || this.clientInitiatedClose;
252
+ const reason = isClientInitiated ? "client" : "server";
253
+ for (const h of this.disconnectHandlers) {
254
+ try {
255
+ h(reason, code);
256
+ }
257
+ catch { /* swallow handler errors */ }
258
+ }
259
+ // Auto-reconnect on non-client closures (network drops, relay restart).
260
+ // Mirrors vendored agentmesh-sdk patch #9: never give up by default,
261
+ // exponential backoff capped at 60s. Caller can opt out via
262
+ // autoReconnect: false in MeshClientOptions.
263
+ if (!isClientInitiated && this.autoReconnect) {
264
+ this.scheduleReconnect();
265
+ }
266
+ }
267
+ this.clientInitiatedClose = false;
268
+ };
269
+ });
270
+ // Successful connect — reset reconnect counter.
271
+ this.reconnectAttempts = 0;
272
+ // Auto-register the agent in the registry on first successful connect.
273
+ // Idempotent: a re-connect after a relay restart skips this. The
274
+ // registry POST is idempotent on its end too (409 = already present).
275
+ if (this.autoRegister && this.registry && !this.registered) {
276
+ await this.registerSelf();
277
+ }
278
+ }
279
+ /**
280
+ * Publish this agent in the registry and upload an X3DH pre-key bundle.
281
+ *
282
+ * - Generates a signed pre-key and `oneTimePrekeyCount` one-time
283
+ * pre-keys via `keyManager.generateSignedPreKey()` /
284
+ * `generateOneTimePreKeys()`.
285
+ * - Capabilities are `[displayName?, ...options.capabilities]` so peers
286
+ * can find this agent via `registry.discover(displayName)`.
287
+ * - Throws RegistryError on transport / 4xx (other than 409) / 5xx
288
+ * failure. Callers that want best-effort registration should catch.
289
+ *
290
+ * Safe to call directly when `autoRegister: false` was used.
291
+ */
292
+ async registerSelf() {
293
+ if (!this.registry) {
294
+ throw new Error("MeshClient.registerSelf: no registry configured");
295
+ }
296
+ const km = this.options.keyManager;
297
+ // identityKey is the long-term X25519 public key derived from the
298
+ // Ed25519 signing key in the X3DHKeyManager constructor.
299
+ // identityKeyEd is the Ed25519 public key — peers MUST receive it
300
+ // to verify the signed pre-key signature (see x3dh.ts verifyBundle).
301
+ const identityKey = km.identityKey.publicKey;
302
+ const identityKeyEd = km.identityKeyEd;
303
+ const signedPreKey = km.generateSignedPreKey();
304
+ const oneTimePreKeys = km.generateOneTimePreKeys(this.oneTimePrekeyCount);
305
+ // Capabilities: include displayName so name-based discover() works.
306
+ const caps = [];
307
+ const dn = this.options.displayName;
308
+ if (dn)
309
+ caps.push(dn);
310
+ for (const c of this.options.capabilities ?? []) {
311
+ if (c && !caps.includes(c))
312
+ caps.push(c);
313
+ }
314
+ const metadata = { ...(this.options.registrationMetadata ?? {}) };
315
+ if (dn && metadata.display_name === undefined)
316
+ metadata.display_name = dn;
317
+ // POP-aware registration (AGT registry built from main since 2026-05-23,
318
+ // PR #2533). The registry verifies Ed25519(public_key_b64 || timestamp)
319
+ // against the supplied `public_key` (which MUST be the Ed25519 key,
320
+ // not the X25519 key — the server uses pynacl `VerifyKey` on it).
321
+ // Server then derives the canonical DID as
322
+ // `did:mesh:<sha256(public_key)[:32]>` and returns it. We adopt that
323
+ // DID as the active DID so subsequent registry lookups + relay
324
+ // `connect.from` agree with the server's view.
325
+ const popSigner = { sign: (m) => km.signMessage(m) };
326
+ const registerResult = await this.registry.register(this.activeDid, // ignored by POP-aware registries; kept for legacy back-compat
327
+ identityKeyEd, // POP path uses the Ed25519 public; legacy path tolerates
328
+ // either key since it only forwards to peers verbatim.
329
+ caps, metadata, popSigner);
330
+ // Adopt the canonical server DID. Legacy registries return the
331
+ // caller-supplied DID, so this is a no-op when registering against an
332
+ // older deployment.
333
+ if (registerResult.did && registerResult.did !== this.activeDid) {
334
+ this.activeDid = registerResult.did;
335
+ }
336
+ // Prekey upload must use the canonical DID (the registry indexes
337
+ // prekeys by the DID it derived, not the one the SDK started with).
338
+ await this.registry.uploadPrekeys(this.activeDid, identityKey, identityKeyEd, signedPreKey, oneTimePreKeys);
339
+ this.registered = true;
340
+ }
341
+ /**
342
+ * Discover peers advertising a given capability. Returns [] if no
343
+ * registry is configured.
344
+ */
345
+ async discover(capability, limit = 50) {
346
+ if (!this.registry)
347
+ return [];
348
+ return this.registry.discover(capability, limit);
349
+ }
350
+ /**
351
+ * Convenience: fetch a peer's pre-key bundle from the registry, then
352
+ * call `establishSession`. Throws if no registry is configured or no
353
+ * bundle is published for `peerId`.
354
+ */
355
+ async establishSessionWithPeer(peerId) {
356
+ const existing = this.sessions.get(peerId);
357
+ if (existing)
358
+ return existing;
359
+ if (this.isPlaintextPeer(peerId)) {
360
+ return this.establishSession(peerId, {});
361
+ }
362
+ if (!this.registry) {
363
+ throw new Error("MeshClient.establishSessionWithPeer: no registry configured");
364
+ }
365
+ const bundle = await this.registry.fetchPrekeys(peerId);
366
+ if (!bundle) {
367
+ throw new Error(`MeshClient.establishSessionWithPeer: no prekey bundle for ${peerId}`);
368
+ }
369
+ return this.establishSession(peerId, bundle);
370
+ }
371
+ scheduleReconnect() {
372
+ if (this.reconnectTimer)
373
+ return; // already scheduled
374
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
375
+ for (const h of this.errorHandlers) {
376
+ try {
377
+ h("ws", this.activeDid, `auto-reconnect gave up after ${this.reconnectAttempts} attempts`);
378
+ }
379
+ catch { /* swallow */ }
380
+ }
381
+ return;
382
+ }
383
+ const exp = Math.min(this.reconnectBaseDelayMs * 2 ** this.reconnectAttempts, this.reconnectMaxDelayMs);
384
+ // Light jitter (±20%) to avoid thundering-herd reconnects across many sandboxes.
385
+ const jitter = exp * (0.8 + Math.random() * 0.4);
386
+ this.reconnectAttempts++;
387
+ this.reconnectTimer = setTimeout(() => {
388
+ this.reconnectTimer = null;
389
+ void this.reconnect().catch((err) => {
390
+ for (const h of this.errorHandlers) {
391
+ try {
392
+ h("ws", this.activeDid, `reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
393
+ }
394
+ catch { /* swallow */ }
395
+ }
396
+ // Schedule next attempt — onclose may not fire if connect() rejected before ws.onopen.
397
+ this.scheduleReconnect();
398
+ });
399
+ }, Math.round(jitter));
400
+ }
401
+ async disconnect() {
402
+ // Cancel any pending reconnect — caller asked to stay disconnected.
403
+ if (this.reconnectTimer) {
404
+ clearTimeout(this.reconnectTimer);
405
+ this.reconnectTimer = null;
406
+ }
407
+ this.reconnectAttempts = 0;
408
+ // Drop any buffered pre-KNOCK frames + their eviction timers.
409
+ for (const peer of [...this.preKnockBuffer.keys()]) {
410
+ this.dropPreKnockBuffer(peer);
411
+ }
412
+ if (!this.connected || !this.ws)
413
+ return;
414
+ this.clientInitiatedClose = true;
415
+ this.sendFrame({ v: 1, type: "disconnect", from: this.activeDid });
416
+ this.ws.close(1000, "client disconnect");
417
+ this.connected = false;
418
+ this.ws = null;
419
+ }
420
+ /**
421
+ * Disconnect and reconnect to the relay.
422
+ *
423
+ * Resets the WebSocket connection and sends a fresh connect +
424
+ * fetch_pending sequence, triggering inbox replay for any messages
425
+ * queued while offline.
426
+ */
427
+ async reconnect() {
428
+ this.connected = false;
429
+ if (this.ws) {
430
+ try {
431
+ this.ws.close();
432
+ }
433
+ catch {
434
+ /* ignore close errors */
435
+ }
436
+ this.ws = null;
437
+ }
438
+ await this.connect();
439
+ }
440
+ get isConnected() {
441
+ return this.connected && this.ws !== null;
442
+ }
443
+ // ── Sending ─────────────────────────────────────────────────────
444
+ async send(peerId, payload) {
445
+ if (!this.isConnected)
446
+ throw new Error("Not connected to relay");
447
+ const messageId = crypto.randomUUID();
448
+ if (this.isPlaintextPeer(peerId)) {
449
+ // Legacy plaintext path — no encryption
450
+ this.sendFrame({
451
+ v: 1,
452
+ type: "message",
453
+ from: this.activeDid,
454
+ to: peerId,
455
+ id: messageId,
456
+ ts: new Date().toISOString(),
457
+ ciphertext: btoa(JSON.stringify(payload)),
458
+ plaintext: true,
459
+ });
460
+ this.incrementSessionCount(peerId, true);
461
+ return;
462
+ }
463
+ // Encrypted path
464
+ let session = this.sessions.get(peerId);
465
+ if (!session || !session.channel) {
466
+ throw new Error(`No encrypted session with ${peerId}. Call establishSession() first.`);
467
+ }
468
+ const encrypted = session.channel.send(new TextEncoder().encode(JSON.stringify(payload)));
469
+ this.sendFrame({
470
+ v: 1,
471
+ type: "message",
472
+ from: this.activeDid,
473
+ to: peerId,
474
+ id: messageId,
475
+ ts: new Date().toISOString(),
476
+ header: {
477
+ dh: this.uint8ToBase64(encrypted.header.dhPublicKey),
478
+ pn: encrypted.header.previousChainLength,
479
+ n: encrypted.header.messageNumber,
480
+ },
481
+ ciphertext: this.uint8ToBase64(encrypted.ciphertext),
482
+ });
483
+ session.messageCount++;
484
+ }
485
+ // ── Session establishment ───────────────────────────────────────
486
+ async establishSession(peerId, peerBundle) {
487
+ // Check for existing session
488
+ const existing = this.sessions.get(peerId);
489
+ if (existing)
490
+ return existing;
491
+ if (this.isPlaintextPeer(peerId)) {
492
+ const session = {
493
+ peerId,
494
+ channel: null,
495
+ isPlaintext: true,
496
+ createdAt: new Date(),
497
+ messageCount: 0,
498
+ };
499
+ this.sessions.set(peerId, session);
500
+ return session;
501
+ }
502
+ // Send KNOCK
503
+ const knockId = crypto.randomUUID();
504
+ // X3DH + SecureChannel — compute establishment FIRST so we can embed it
505
+ // in the KNOCK frame. This lets the receiver auto-bootstrap the responder
506
+ // session before any ciphertext arrives, eliminating the
507
+ // "No encrypted session" race that vendored agentmesh-sdk patch #4b
508
+ // worked around. Backwards-compatible: receivers that don't understand
509
+ // the `establishment` field fall back to manual acceptSession() calls.
510
+ const [channel, establishment] = channel_1.SecureChannel.createSender(this.options.keyManager, peerBundle, new TextEncoder().encode(`${this.activeDid}|${peerId}`));
511
+ this.sendFrame({
512
+ v: 1,
513
+ type: "knock",
514
+ from: this.activeDid,
515
+ to: peerId,
516
+ id: knockId,
517
+ ts: new Date().toISOString(),
518
+ intent: { action: "establish_session" },
519
+ establishment: this.serializeEstablishment(establishment),
520
+ });
521
+ const session = {
522
+ peerId,
523
+ channel,
524
+ isPlaintext: false,
525
+ createdAt: new Date(),
526
+ messageCount: 0,
527
+ };
528
+ this.sessions.set(peerId, session);
529
+ return session;
530
+ }
531
+ acceptSession(peerId, establishment) {
532
+ const channel = channel_1.SecureChannel.createReceiver(this.options.keyManager, establishment, new TextEncoder().encode(`${peerId}|${this.activeDid}`));
533
+ const session = {
534
+ peerId,
535
+ channel,
536
+ isPlaintext: false,
537
+ createdAt: new Date(),
538
+ messageCount: 0,
539
+ };
540
+ this.sessions.set(peerId, session);
541
+ this.knockAccepted.add(peerId);
542
+ return session;
543
+ }
544
+ getSession(peerId) {
545
+ return this.sessions.get(peerId);
546
+ }
547
+ closeSession(peerId) {
548
+ const session = this.sessions.get(peerId);
549
+ if (!session)
550
+ return false;
551
+ if (session.channel)
552
+ session.channel.close();
553
+ this.sessions.delete(peerId);
554
+ this.knockAccepted.delete(peerId);
555
+ return true;
556
+ }
557
+ // ── Handlers ────────────────────────────────────────────────────
558
+ onMessage(handler) {
559
+ this.messageHandlers.push(handler);
560
+ }
561
+ onKnock(handler) {
562
+ this.knockHandlers.push(handler);
563
+ }
564
+ /**
565
+ * Register a callback for transport-level errors.
566
+ *
567
+ * Fires for: WebSocket errors (handshake, mid-stream), decrypt failures,
568
+ * KNOCK protocol errors, frame validation, and `session_desync` events
569
+ * (recoverable ratchet drift — caller should re-establishSession to that
570
+ * peer; differs from "decrypt" which means no session existed at all).
571
+ * Multiple handlers may be registered; each is invoked in registration
572
+ * order. Handler exceptions are swallowed so one buggy observer cannot
573
+ * break the others.
574
+ */
575
+ onError(handler) {
576
+ this.errorHandlers.push(handler);
577
+ }
578
+ /**
579
+ * Register a callback for transport disconnect events.
580
+ *
581
+ * `reason` is `"client"` for caller-initiated `disconnect()`, `"server"`
582
+ * for relay-side closes (network drop, relay restart), and `"ws-error"`
583
+ * when an error event fires on an already-connected socket.
584
+ */
585
+ onDisconnect(handler) {
586
+ this.disconnectHandlers.push(handler);
587
+ }
588
+ /**
589
+ * Register a callback that fires the first time we successfully decrypt
590
+ * a message from a given peer (i.e. the X3DH+Double-Ratchet session
591
+ * with that peer is fully end-to-end verified).
592
+ *
593
+ * `isFirstPeer` is `true` only for the very first verified peer in the
594
+ * client's lifetime; subsequent peers fire with `false`. This lets
595
+ * orchestrators print "mesh online" once and "+ peer X verified" for
596
+ * the rest.
597
+ */
598
+ onE2EVerified(handler) {
599
+ this.e2eVerifiedHandlers.push(handler);
600
+ }
601
+ // ── Heartbeat ───────────────────────────────────────────────────
602
+ sendHeartbeat() {
603
+ if (!this.isConnected)
604
+ return;
605
+ this.sendFrame({
606
+ v: 1,
607
+ type: "heartbeat",
608
+ from: this.activeDid,
609
+ ts: new Date().toISOString(),
610
+ });
611
+ }
612
+ // ── Frame handling ──────────────────────────────────────────────
613
+ async handleFrame(frame) {
614
+ const type = frame.type;
615
+ const from = frame.from;
616
+ if (type === "message") {
617
+ await this.handleMessage(frame);
618
+ }
619
+ else if (type === "knock") {
620
+ await this.handleKnock(frame);
621
+ }
622
+ else if (type === "knock_accept") {
623
+ this.handleKnockAccept(frame);
624
+ }
625
+ else if (type === "knock_reject") {
626
+ this.handleKnockReject(frame);
627
+ }
628
+ else if (type === "ack") {
629
+ // ACK processed — nothing to do
630
+ }
631
+ else if (type === "pending_messages") {
632
+ await this.handlePendingMessages(frame);
633
+ }
634
+ }
635
+ async handleMessage(frame) {
636
+ const from = frame.from;
637
+ // Check KNOCK pending queue — wait for resolution if KNOCK is in-flight
638
+ if (!this.knockAccepted.has(from) && !this.isPlaintextPeer(from)) {
639
+ const pending = this.knockPending.get(from);
640
+ if (pending) {
641
+ // Wait for KNOCK resolution
642
+ const accepted = await new Promise((resolve) => {
643
+ const originalResolve = pending.resolve;
644
+ pending.resolve = (val) => {
645
+ originalResolve(val);
646
+ resolve(val);
647
+ };
648
+ });
649
+ if (!accepted)
650
+ return; // KNOCK rejected — drop message
651
+ }
652
+ }
653
+ let payload;
654
+ let isPlaintext = false;
655
+ if (frame.plaintext || this.isPlaintextPeer(from)) {
656
+ // Legacy plaintext
657
+ payload = JSON.parse(atob(frame.ciphertext));
658
+ isPlaintext = true;
659
+ }
660
+ else {
661
+ // Encrypted
662
+ const session = this.sessions.get(from);
663
+ if (!session?.channel) {
664
+ // Gap-G4 (vendored agentmesh-sdk patch #16): pre-KNOCK message buffer.
665
+ // The relay does not guarantee inter-frame ordering — an encrypted
666
+ // message can land before its KNOCK is processed. Drop-on-floor was
667
+ // the upstream behaviour; instead, buffer the raw frame here and
668
+ // drain it from handleKnock() once the KNOCK is accepted. Capped at
669
+ // preKnockBufferSize entries per peer with preKnockBufferTtlMs TTL.
670
+ if (this.preKnockBufferSize > 0 && !this.isPlaintextPeer(from)) {
671
+ this.bufferPreKnockFrame(from, frame);
672
+ }
673
+ else {
674
+ for (const h of this.errorHandlers) {
675
+ try {
676
+ h("decrypt", from, "no session for encrypted message — dropping");
677
+ }
678
+ catch { /* swallow */ }
679
+ }
680
+ }
681
+ return;
682
+ }
683
+ const header = frame.header;
684
+ const encrypted = {
685
+ header: {
686
+ dhPublicKey: this.base64ToUint8(header.dh),
687
+ previousChainLength: header.pn,
688
+ messageNumber: header.n,
689
+ },
690
+ ciphertext: this.base64ToUint8(frame.ciphertext),
691
+ };
692
+ let plaintext;
693
+ try {
694
+ plaintext = session.channel.receive(encrypted);
695
+ }
696
+ catch (err) {
697
+ // Gap-G3 (vendored agentmesh-sdk patch #13): on decrypt failure inside
698
+ // an existing session, the ratchet is desynchronised — every
699
+ // subsequent inbound frame will fail the same way. Tear down the
700
+ // broken session so the next establishSession() to this peer runs a
701
+ // fresh X3DH + KNOCK round, and surface a dedicated "session_desync"
702
+ // error kind so callers can distinguish recoverable ratchet drift
703
+ // (re-establish & retry) from genuine tampering (drop & alert).
704
+ const detail = err instanceof Error ? err.message : String(err);
705
+ this.closeSession(from);
706
+ this.knockAccepted.delete(from);
707
+ for (const h of this.errorHandlers) {
708
+ try {
709
+ h("session_desync", from, detail);
710
+ }
711
+ catch { /* swallow */ }
712
+ }
713
+ return;
714
+ }
715
+ payload = JSON.parse(new TextDecoder().decode(plaintext));
716
+ session.messageCount++;
717
+ // First successfully-decrypted message from this peer means E2E
718
+ // is end-to-end verified (KNOCK + X3DH + Double Ratchet all worked).
719
+ // Surface to observers exactly once per peer per process lifetime.
720
+ if (!this.e2eVerifiedSet.has(from)) {
721
+ this.e2eVerifiedSet.add(from);
722
+ const isFirstPeer = this.e2eVerifiedSet.size === 1;
723
+ for (const h of this.e2eVerifiedHandlers) {
724
+ try {
725
+ h(from, isFirstPeer);
726
+ }
727
+ catch { /* swallow handler errors */ }
728
+ }
729
+ }
730
+ }
731
+ // Send ACK
732
+ this.sendFrame({ v: 1, type: "ack", id: frame.id });
733
+ // Notify handlers
734
+ for (const handler of this.messageHandlers) {
735
+ try {
736
+ handler(from, payload, isPlaintext);
737
+ }
738
+ catch { /* swallow handler errors */ }
739
+ }
740
+ }
741
+ /**
742
+ * Handle a batch of pending messages replayed by the relay on reconnect.
743
+ *
744
+ * The relay may respond to a fetch_pending frame with a
745
+ * pending_messages frame containing an array of queued messages.
746
+ * Each message is dispatched through the standard handleMessage path.
747
+ */
748
+ async handlePendingMessages(frame) {
749
+ const messages = frame.messages;
750
+ if (!messages || !Array.isArray(messages))
751
+ return;
752
+ for (const msg of messages) {
753
+ await this.handleMessage(msg);
754
+ }
755
+ }
756
+ async handleKnock(frame) {
757
+ const from = frame.from;
758
+ const intent = frame.intent;
759
+ // Register pending entry so concurrent handleMessage calls wait for
760
+ // the verdict. handleMessage may wrap `resolve` to add its own waiter;
761
+ // we look up the (possibly-wrapped) function when we resolve below.
762
+ //
763
+ // The timer is the abort path: if no verdict arrives within
764
+ // knockTimeout, resolve waiters to false and clear the entry. A
765
+ // `timedOut` flag prevents a slow handler from sending knock_accept
766
+ // after the timer has already told waiters the KNOCK was rejected.
767
+ let timedOut = false;
768
+ const timer = setTimeout(() => {
769
+ timedOut = true;
770
+ const entry = this.knockPending.get(from);
771
+ if (entry) {
772
+ this.knockPending.delete(from);
773
+ entry.resolve(false);
774
+ }
775
+ }, this.knockTimeout);
776
+ this.knockPending.set(from, { resolve: () => { }, timer });
777
+ // Evaluate via registered handlers. Use try/finally so the timer is
778
+ // cleared and the pending entry resolved even if a handler throws —
779
+ // otherwise the entry would leak and a re-KNOCK from the same peer
780
+ // would race against stale state.
781
+ let accepted = true;
782
+ try {
783
+ for (const handler of this.knockHandlers) {
784
+ if (!(await handler(from, intent))) {
785
+ accepted = false;
786
+ break;
787
+ }
788
+ }
789
+ }
790
+ finally {
791
+ clearTimeout(timer);
792
+ if (!timedOut) {
793
+ const entry = this.knockPending.get(from);
794
+ if (entry) {
795
+ this.knockPending.delete(from);
796
+ entry.resolve(accepted);
797
+ }
798
+ }
799
+ }
800
+ // If the timer beat the handler eval, waiters were already told
801
+ // "rejected"; honor that decision when responding to the relay.
802
+ if (timedOut)
803
+ accepted = false;
804
+ if (accepted) {
805
+ // Auto-bootstrap responder session if establishment data was embedded
806
+ // in the knock. Mirrors vendored agentmesh-sdk patch #4b — receiver no
807
+ // longer needs to call acceptSession() manually before the first
808
+ // encrypted message arrives.
809
+ const est = frame.establishment;
810
+ if (est && !this.sessions.has(from)) {
811
+ try {
812
+ const establishment = this.deserializeEstablishment(est);
813
+ this.acceptSession(from, establishment);
814
+ }
815
+ catch (err) {
816
+ for (const h of this.errorHandlers) {
817
+ try {
818
+ h("knock", from, `failed to bootstrap responder from KNOCK: ${err instanceof Error ? err.message : String(err)}`);
819
+ }
820
+ catch { /* swallow */ }
821
+ }
822
+ accepted = false;
823
+ }
824
+ }
825
+ // Always record acceptance so the encrypted-message gate stops
826
+ // waiting/buffering. (`acceptSession` also sets this, but keep the
827
+ // legacy-peer path covered too.)
828
+ if (accepted)
829
+ this.knockAccepted.add(from);
830
+ }
831
+ if (accepted) {
832
+ this.sendFrame({
833
+ v: 1,
834
+ type: "knock_accept",
835
+ from: this.activeDid,
836
+ to: from,
837
+ id: crypto.randomUUID(),
838
+ knock_id: frame.id,
839
+ ts: new Date().toISOString(),
840
+ });
841
+ // Gap-G4: drain any encrypted frames that arrived for this peer
842
+ // before its KNOCK was processed. The session is now established
843
+ // (via auto-bootstrap above or a prior acceptSession call), so the
844
+ // buffered frames can be replayed through the normal handleMessage
845
+ // path and decrypted against the fresh session.
846
+ await this.drainPreKnockBuffer(from);
847
+ }
848
+ else {
849
+ this.sendFrame({
850
+ v: 1,
851
+ type: "knock_reject",
852
+ from: this.activeDid,
853
+ to: from,
854
+ id: crypto.randomUUID(),
855
+ knock_id: frame.id,
856
+ reason: "policy_denied",
857
+ ts: new Date().toISOString(),
858
+ });
859
+ // Drop any buffered frames — KNOCK was rejected so we cannot decrypt
860
+ // them anyway. Avoids unbounded buffer growth for hostile peers.
861
+ this.dropPreKnockBuffer(from);
862
+ }
863
+ }
864
+ handleKnockAccept(frame) {
865
+ const from = frame.from;
866
+ this.knockAccepted.add(from);
867
+ }
868
+ handleKnockReject(frame) {
869
+ const from = frame.from;
870
+ this.closeSession(from);
871
+ }
872
+ // ── Pre-KNOCK buffer (Gap-G4 / vendored patch #16) ──────────────
873
+ bufferPreKnockFrame(from, frame) {
874
+ let entries = this.preKnockBuffer.get(from);
875
+ if (!entries) {
876
+ // Enforce global peer cap before adding a new peer's buffer.
877
+ if (this.preKnockBuffer.size >= this.maxBufferedPeers) {
878
+ const oldestPeer = this.preKnockBuffer.keys().next().value;
879
+ this.dropPreKnockBuffer(oldestPeer);
880
+ for (const h of this.errorHandlers) {
881
+ try {
882
+ h("frame", oldestPeer, `pre-knock buffer evicted: global peer cap (${this.maxBufferedPeers}) reached`);
883
+ }
884
+ catch { /* swallow */ }
885
+ }
886
+ }
887
+ entries = [];
888
+ this.preKnockBuffer.set(from, entries);
889
+ }
890
+ // Cap: drop oldest when full to keep newest message (sender most likely
891
+ // to retransmit nothing — newer frames carry the most recent ratchet
892
+ // state and have the best chance of being decryptable).
893
+ if (entries.length >= this.preKnockBufferSize) {
894
+ const evicted = entries.shift();
895
+ if (evicted)
896
+ clearTimeout(evicted.timer);
897
+ }
898
+ const timer = setTimeout(() => {
899
+ const list = this.preKnockBuffer.get(from);
900
+ if (!list)
901
+ return;
902
+ const idx = list.findIndex((e) => e.frame === frame);
903
+ if (idx >= 0)
904
+ list.splice(idx, 1);
905
+ if (list.length === 0)
906
+ this.preKnockBuffer.delete(from);
907
+ }, this.preKnockBufferTtlMs);
908
+ entries.push({ frame, timer });
909
+ }
910
+ async drainPreKnockBuffer(from) {
911
+ const entries = this.preKnockBuffer.get(from);
912
+ if (!entries || entries.length === 0)
913
+ return;
914
+ this.preKnockBuffer.delete(from);
915
+ for (const entry of entries) {
916
+ clearTimeout(entry.timer);
917
+ try {
918
+ await this.handleMessage(entry.frame);
919
+ }
920
+ catch (err) {
921
+ for (const h of this.errorHandlers) {
922
+ try {
923
+ h("decrypt", from, `pre-knock drain failed: ${err instanceof Error ? err.message : String(err)}`);
924
+ }
925
+ catch { /* swallow */ }
926
+ }
927
+ }
928
+ }
929
+ }
930
+ dropPreKnockBuffer(from) {
931
+ const entries = this.preKnockBuffer.get(from);
932
+ if (!entries)
933
+ return;
934
+ for (const entry of entries)
935
+ clearTimeout(entry.timer);
936
+ this.preKnockBuffer.delete(from);
937
+ }
938
+ // ── Establishment (de)serialization ─────────────────────────────
939
+ serializeEstablishment(est) {
940
+ const out = {
941
+ ik: this.uint8ToBase64(est.initiatorIdentityKey),
942
+ ek: this.uint8ToBase64(est.ephemeralPublicKey),
943
+ };
944
+ if (typeof est.usedOneTimeKeyId === "number")
945
+ out.otk = est.usedOneTimeKeyId;
946
+ return out;
947
+ }
948
+ deserializeEstablishment(obj) {
949
+ const ik = obj.ik;
950
+ const ek = obj.ek;
951
+ if (typeof ik !== "string" || typeof ek !== "string") {
952
+ throw new Error("malformed establishment: missing ik/ek");
953
+ }
954
+ const result = {
955
+ initiatorIdentityKey: this.base64ToUint8(ik),
956
+ ephemeralPublicKey: this.base64ToUint8(ek),
957
+ };
958
+ if (typeof obj.otk === "number")
959
+ result.usedOneTimeKeyId = obj.otk;
960
+ return result;
961
+ }
962
+ // ── Utilities ───────────────────────────────────────────────────
963
+ sendFrame(frame) {
964
+ if (this.ws && this.connected) {
965
+ this.ws.send(JSON.stringify(frame));
966
+ }
967
+ }
968
+ incrementSessionCount(peerId, isPlaintext) {
969
+ let session = this.sessions.get(peerId);
970
+ if (!session) {
971
+ session = { peerId, channel: null, isPlaintext, createdAt: new Date(), messageCount: 0 };
972
+ this.sessions.set(peerId, session);
973
+ }
974
+ session.messageCount++;
975
+ }
976
+ uint8ToBase64(data) {
977
+ // Use Buffer in Node.js to avoid stack overflow on large payloads
978
+ if (typeof Buffer !== "undefined") {
979
+ return Buffer.from(data).toString("base64");
980
+ }
981
+ // Browser fallback — loop-based
982
+ let binary = "";
983
+ for (let i = 0; i < data.length; i++) {
984
+ binary += String.fromCharCode(data[i]);
985
+ }
986
+ return btoa(binary);
987
+ }
988
+ base64ToUint8(b64) {
989
+ if (typeof Buffer !== "undefined") {
990
+ return new Uint8Array(Buffer.from(b64, "base64"));
991
+ }
992
+ const binary = atob(b64);
993
+ const bytes = new Uint8Array(binary.length);
994
+ for (let i = 0; i < binary.length; i++) {
995
+ bytes[i] = binary.charCodeAt(i);
996
+ }
997
+ return bytes;
998
+ }
999
+ }
1000
+ exports.MeshClient = MeshClient;
1001
+ //# sourceMappingURL=mesh-client.js.map