ofw-mcp 2.16.2 → 2.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -9444,7 +9444,7 @@ var require_websocket = __commonJS({
9444
9444
  var http = __require("http");
9445
9445
  var net = __require("net");
9446
9446
  var tls = __require("tls");
9447
- var { randomBytes: randomBytes2, createHash: createHash2 } = __require("crypto");
9447
+ var { randomBytes: randomBytes3, createHash: createHash2 } = __require("crypto");
9448
9448
  var { Duplex, Readable } = __require("stream");
9449
9449
  var { URL: URL2 } = __require("url");
9450
9450
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -9982,7 +9982,7 @@ var require_websocket = __commonJS({
9982
9982
  }
9983
9983
  }
9984
9984
  const defaultPort = isSecure ? 443 : 80;
9985
- const key = randomBytes2(16).toString("base64");
9985
+ const key = randomBytes3(16).toString("base64");
9986
9986
  const request = isSecure ? https.request : http.request;
9987
9987
  const protocolSet = /* @__PURE__ */ new Set();
9988
9988
  let perMessageDeflate;
@@ -17513,12 +17513,12 @@ var $ZodE164 = /* @__PURE__ */ $constructor("$ZodE164", (inst, def) => {
17513
17513
  $ZodStringFormat.init(inst, def);
17514
17514
  });
17515
17515
  var CC_SANITIZE = /[- ]/g;
17516
- function isLuhnAlgo(digits) {
17517
- let length = digits.length;
17516
+ function isLuhnAlgo(digits2) {
17517
+ let length = digits2.length;
17518
17518
  let bit = 1;
17519
17519
  let sum = 0;
17520
17520
  while (length) {
17521
- const value = digits.charCodeAt(--length) - 48;
17521
+ const value = digits2.charCodeAt(--length) - 48;
17522
17522
  bit ^= 1;
17523
17523
  sum += bit ? [0, 2, 4, 6, 8, 1, 3, 5, 7, 9][value] : value;
17524
17524
  }
@@ -32880,8 +32880,8 @@ function hex2(_params) {
32880
32880
  return _stringFormat(ZodCustomStringFormat, "hex", regexes_exports.hex, _params);
32881
32881
  }
32882
32882
  function hash(alg, params) {
32883
- const enc4 = params?.enc ?? "hex";
32884
- const format = `${alg}_${enc4}`;
32883
+ const enc6 = params?.enc ?? "hex";
32884
+ const format = `${alg}_${enc6}`;
32885
32885
  const regex = regexes_exports[format];
32886
32886
  if (!regex)
32887
32887
  throw new Error(`Unrecognized hash format: ${format}`);
@@ -40837,16 +40837,193 @@ var TokenManager = class {
40837
40837
  import { dirname as dirname2, join as join5 } from "path";
40838
40838
  import { fileURLToPath } from "url";
40839
40839
 
40840
+ // node_modules/@fetchproxy/protocol/dist/crypto.js
40841
+ var subtle = globalThis.crypto.subtle;
40842
+ async function exportRaw(key, format) {
40843
+ const buf = await subtle.exportKey(format, key);
40844
+ return new Uint8Array(buf);
40845
+ }
40846
+ async function generateX25519() {
40847
+ const kp = await subtle.generateKey({ name: "X25519" }, true, [
40848
+ "deriveBits"
40849
+ ]);
40850
+ const priv = await exportRaw(kp.privateKey, "pkcs8");
40851
+ const pub = await exportRaw(kp.publicKey, "raw");
40852
+ return { privateKey: priv.slice(-32), publicKey: pub };
40853
+ }
40854
+ async function generateEd25519() {
40855
+ const kp = await subtle.generateKey({ name: "Ed25519" }, true, [
40856
+ "sign",
40857
+ "verify"
40858
+ ]);
40859
+ const priv = await exportRaw(kp.privateKey, "pkcs8");
40860
+ const pub = await exportRaw(kp.publicKey, "raw");
40861
+ return { privateKey: priv.slice(-32), publicKey: pub };
40862
+ }
40863
+ async function importX25519Priv(raw) {
40864
+ const prefix = new Uint8Array([
40865
+ 48,
40866
+ 46,
40867
+ 2,
40868
+ 1,
40869
+ 0,
40870
+ 48,
40871
+ 5,
40872
+ 6,
40873
+ 3,
40874
+ 43,
40875
+ 101,
40876
+ 110,
40877
+ 4,
40878
+ 34,
40879
+ 4,
40880
+ 32
40881
+ ]);
40882
+ const pkcs8 = new Uint8Array(prefix.length + raw.length);
40883
+ pkcs8.set(prefix, 0);
40884
+ pkcs8.set(raw, prefix.length);
40885
+ return subtle.importKey("pkcs8", pkcs8, { name: "X25519" }, false, [
40886
+ "deriveBits"
40887
+ ]);
40888
+ }
40889
+ async function importX25519Pub(raw) {
40890
+ return subtle.importKey("raw", raw, { name: "X25519" }, false, []);
40891
+ }
40892
+ async function importEd25519Priv(raw) {
40893
+ const prefix = new Uint8Array([
40894
+ 48,
40895
+ 46,
40896
+ 2,
40897
+ 1,
40898
+ 0,
40899
+ 48,
40900
+ 5,
40901
+ 6,
40902
+ 3,
40903
+ 43,
40904
+ 101,
40905
+ 112,
40906
+ 4,
40907
+ 34,
40908
+ 4,
40909
+ 32
40910
+ ]);
40911
+ const pkcs8 = new Uint8Array(prefix.length + raw.length);
40912
+ pkcs8.set(prefix, 0);
40913
+ pkcs8.set(raw, prefix.length);
40914
+ return subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"]);
40915
+ }
40916
+ async function importEd25519Pub(raw) {
40917
+ return subtle.importKey("raw", raw, { name: "Ed25519" }, false, ["verify"]);
40918
+ }
40919
+ async function ecdhX25519(privRaw, pubRaw) {
40920
+ const priv = await importX25519Priv(privRaw);
40921
+ const pub = await importX25519Pub(pubRaw);
40922
+ const bits = await subtle.deriveBits({ name: "X25519", public: pub }, priv, 256);
40923
+ return new Uint8Array(bits);
40924
+ }
40925
+ async function ed25519Sign(privRaw, msg) {
40926
+ const priv = await importEd25519Priv(privRaw);
40927
+ const sig = await subtle.sign({ name: "Ed25519" }, priv, msg);
40928
+ return new Uint8Array(sig);
40929
+ }
40930
+ async function ed25519Verify(pubRaw, msg, sig) {
40931
+ const pub = await importEd25519Pub(pubRaw);
40932
+ return subtle.verify({ name: "Ed25519" }, pub, sig, msg);
40933
+ }
40934
+ async function hkdfSha256(ikm, salt, info, lenBytes) {
40935
+ const baseKey = await subtle.importKey("raw", ikm, { name: "HKDF" }, false, [
40936
+ "deriveBits"
40937
+ ]);
40938
+ const bits = await subtle.deriveBits({
40939
+ name: "HKDF",
40940
+ hash: "SHA-256",
40941
+ salt,
40942
+ info
40943
+ }, baseKey, lenBytes * 8);
40944
+ return new Uint8Array(bits);
40945
+ }
40946
+ async function aesGcmSeal(key, iv, plaintext, aad) {
40947
+ const k = await subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
40948
+ "encrypt"
40949
+ ]);
40950
+ const ct = await subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad }, k, plaintext);
40951
+ return new Uint8Array(ct);
40952
+ }
40953
+ async function aesGcmOpen(key, iv, ciphertext, aad) {
40954
+ const k = await subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
40955
+ "decrypt"
40956
+ ]);
40957
+ const pt = await subtle.decrypt({ name: "AES-GCM", iv, additionalData: aad }, k, ciphertext);
40958
+ return new Uint8Array(pt);
40959
+ }
40960
+ async function sha256(data) {
40961
+ const h = await subtle.digest("SHA-256", data);
40962
+ return new Uint8Array(h);
40963
+ }
40964
+
40965
+ // node_modules/@fetchproxy/protocol/dist/encoding.js
40966
+ function toB64(bytes) {
40967
+ let s = "";
40968
+ for (let i = 0; i < bytes.length; i++)
40969
+ s += String.fromCharCode(bytes[i]);
40970
+ return btoa(s);
40971
+ }
40972
+ function fromB64(s) {
40973
+ const bin = atob(s);
40974
+ const out = new Uint8Array(bin.length);
40975
+ for (let i = 0; i < bin.length; i++)
40976
+ out[i] = bin.charCodeAt(i);
40977
+ return out;
40978
+ }
40979
+
40840
40980
  // node_modules/@fetchproxy/protocol/dist/frames.js
40841
- var PROTOCOL_VERSION = 3;
40842
- var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
40843
- function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
40844
- const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
40981
+ var PROTOCOL_VERSION = 4;
40982
+ var HKDF_SESSION_INFO = "fetchproxy/4.0.0/session";
40983
+ var enc = new TextEncoder();
40984
+ function helloSignaturePayload(mcpId, sessionNonce, sessionPub, answersExtNonce) {
40985
+ const id = enc.encode(mcpId);
40986
+ const out = new Uint8Array(id.length + sessionNonce.length + sessionPub.length + answersExtNonce.length);
40987
+ out.set(id, 0);
40988
+ out.set(sessionNonce, id.length);
40989
+ out.set(sessionPub, id.length + sessionNonce.length);
40990
+ out.set(answersExtNonce, id.length + sessionNonce.length + sessionPub.length);
40991
+ return out;
40992
+ }
40993
+ var ANSWERS_NO_EXT_SESSION = new Uint8Array(32);
40994
+ function answersNoExtSession(answersExtNonce) {
40995
+ let bytes;
40996
+ try {
40997
+ bytes = fromB64(answersExtNonce);
40998
+ } catch {
40999
+ return false;
41000
+ }
41001
+ if (bytes.length !== ANSWERS_NO_EXT_SESSION.length)
41002
+ return false;
41003
+ for (const b of bytes)
41004
+ if (b !== 0)
41005
+ return false;
41006
+ return true;
41007
+ }
41008
+ function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub, mcpSessionPub) {
41009
+ const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length + mcpSessionPub.length);
40845
41010
  out.set(mcpHelloNonce, 0);
40846
41011
  out.set(extHelloNonce, mcpHelloNonce.length);
40847
41012
  out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
41013
+ out.set(mcpSessionPub, mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
40848
41014
  return out;
40849
41015
  }
41016
+ async function transcriptHash(mcpNonce, extNonce, mcpSessionPub, extSessionPub) {
41017
+ const out = new Uint8Array(mcpNonce.length + extNonce.length + mcpSessionPub.length + extSessionPub.length);
41018
+ out.set(mcpNonce, 0);
41019
+ out.set(extNonce, mcpNonce.length);
41020
+ out.set(mcpSessionPub, mcpNonce.length + extNonce.length);
41021
+ out.set(extSessionPub, mcpNonce.length + extNonce.length + mcpSessionPub.length);
41022
+ return sha256(out);
41023
+ }
41024
+ function frameAad(mcpId, seq, direction) {
41025
+ return enc.encode(`fetchproxy/4/frame\0${mcpId}\0${seq}\0${direction}`);
41026
+ }
40850
41027
  var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
40851
41028
  "fetch",
40852
41029
  "fetch_in_page",
@@ -40917,6 +41094,138 @@ function isValidJsonPointer(pointer) {
40917
41094
  return true;
40918
41095
  }
40919
41096
 
41097
+ // node_modules/@fetchproxy/protocol/dist/public-suffix.js
41098
+ var CCTLD_ADMIN_LABELS = /* @__PURE__ */ new Set([
41099
+ "ac",
41100
+ "co",
41101
+ "com",
41102
+ "edu",
41103
+ "gob",
41104
+ "gov",
41105
+ "mil",
41106
+ "net",
41107
+ "nom",
41108
+ "org",
41109
+ "sch"
41110
+ ]);
41111
+ var LISTED_PUBLIC_SUFFIXES = /* @__PURE__ */ new Set([
41112
+ // --- ICANN, under ccTLDs, outside CCTLD_ADMIN_LABELS ---
41113
+ "ad.jp",
41114
+ "ed.jp",
41115
+ "gr.jp",
41116
+ "lg.jp",
41117
+ "ne.jp",
41118
+ "or.jp",
41119
+ "go.kr",
41120
+ "ne.kr",
41121
+ "or.kr",
41122
+ "pe.kr",
41123
+ "re.kr",
41124
+ "go.id",
41125
+ "my.id",
41126
+ "or.id",
41127
+ "web.id",
41128
+ "go.th",
41129
+ "in.th",
41130
+ "or.th",
41131
+ "me.uk",
41132
+ "ltd.uk",
41133
+ "plc.uk",
41134
+ "nhs.uk",
41135
+ "mod.uk",
41136
+ "police.uk",
41137
+ "parliament.uk",
41138
+ "asn.au",
41139
+ "id.au",
41140
+ "gen.in",
41141
+ "ind.in",
41142
+ "firm.in",
41143
+ "eu.org",
41144
+ // --- Vendor ("private") suffixes: one entry = every customer's site ---
41145
+ "github.io",
41146
+ "githubusercontent.com",
41147
+ "gitlab.io",
41148
+ "bitbucket.io",
41149
+ "pages.dev",
41150
+ "workers.dev",
41151
+ "r2.dev",
41152
+ "trycloudflare.com",
41153
+ "vercel.app",
41154
+ "now.sh",
41155
+ "netlify.app",
41156
+ "netlify.live",
41157
+ "herokuapp.com",
41158
+ "herokussl.com",
41159
+ "appspot.com",
41160
+ "web.app",
41161
+ "firebaseapp.com",
41162
+ "cloudfunctions.net",
41163
+ "blogspot.com",
41164
+ "azurewebsites.net",
41165
+ "azurestaticapps.net",
41166
+ "azureedge.net",
41167
+ "azurecontainer.io",
41168
+ "cloudapp.azure.com",
41169
+ "trafficmanager.net",
41170
+ "s3.amazonaws.com",
41171
+ "compute.amazonaws.com",
41172
+ "compute-1.amazonaws.com",
41173
+ "elasticbeanstalk.com",
41174
+ "cloudfront.net",
41175
+ "awsapprunner.com",
41176
+ "amplifyapp.com",
41177
+ "fly.dev",
41178
+ "onrender.com",
41179
+ "railway.app",
41180
+ "koyeb.app",
41181
+ "deno.dev",
41182
+ "replit.dev",
41183
+ "repl.co",
41184
+ "glitch.me",
41185
+ "surge.sh",
41186
+ "neocities.org",
41187
+ "pythonanywhere.com",
41188
+ "readthedocs.io",
41189
+ "gitbook.io",
41190
+ "myshopify.com",
41191
+ "wixsite.com",
41192
+ "editorx.io",
41193
+ "webflow.io",
41194
+ "bubbleapps.io",
41195
+ "notion.site",
41196
+ "framer.app",
41197
+ "framer.website",
41198
+ "ngrok.io",
41199
+ "ngrok.app",
41200
+ "ngrok-free.app",
41201
+ "loca.lt",
41202
+ "duckdns.org",
41203
+ "ddns.net",
41204
+ "no-ip.org",
41205
+ "dyndns.org",
41206
+ "hopto.org",
41207
+ "zapto.org"
41208
+ ]);
41209
+ function isPublicSuffix(host) {
41210
+ const normalised = host.trim().toLowerCase().replace(/\.$/, "");
41211
+ if (normalised === "")
41212
+ return false;
41213
+ const labels = normalised.split(".");
41214
+ if (labels.some((l) => l === ""))
41215
+ return false;
41216
+ if (labels.length === 1)
41217
+ return true;
41218
+ if (LISTED_PUBLIC_SUFFIXES.has(normalised))
41219
+ return true;
41220
+ if (labels.length === 2) {
41221
+ const [secondLevel, tld] = labels;
41222
+ if (secondLevel && tld && /^[a-z]{2}$/.test(tld) && CCTLD_ADMIN_LABELS.has(secondLevel)) {
41223
+ return true;
41224
+ }
41225
+ }
41226
+ return false;
41227
+ }
41228
+
40920
41229
  // node_modules/@fetchproxy/protocol/dist/validate.js
40921
41230
  var ProtocolError = class extends Error {
40922
41231
  constructor(message) {
@@ -40953,6 +41262,18 @@ function assertBase64(x, label) {
40953
41262
  if (!BASE64_RE.test(x))
40954
41263
  throw new ProtocolError(`${label}: invalid base64`);
40955
41264
  }
41265
+ function base64ByteLength(s) {
41266
+ if (s.length % 4 !== 0)
41267
+ return null;
41268
+ const pad2 = s.endsWith("==") ? 2 : s.endsWith("=") ? 1 : 0;
41269
+ return s.length / 4 * 3 - pad2;
41270
+ }
41271
+ function assertBase64Bytes(x, label, bytes) {
41272
+ assertBase64(x, label);
41273
+ if (base64ByteLength(x) !== bytes) {
41274
+ throw new ProtocolError(`${label}: expected base64 of ${bytes} raw bytes`);
41275
+ }
41276
+ }
40956
41277
  function assertBoolean(x, label) {
40957
41278
  if (typeof x !== "boolean") {
40958
41279
  throw new ProtocolError(`${label}: must be boolean`);
@@ -41174,7 +41495,7 @@ function assertIndexedDbScopesArray(value, label) {
41174
41495
  }
41175
41496
  }
41176
41497
  }
41177
- var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
41498
+ var DOM_SELECTOR_RE = /^[^\x00-\x1f\x7f]{1,512}$/;
41178
41499
  var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
41179
41500
  var GRAPHQL_OP_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]{0,127}$/;
41180
41501
  function assertDomSelectorsArray(value, label) {
@@ -41285,6 +41606,9 @@ function validateHello(raw) {
41285
41606
  if (!HOSTNAME_RE.test(d)) {
41286
41607
  throw new ProtocolError(`hello.domains: invalid hostname ${JSON.stringify(d)}`);
41287
41608
  }
41609
+ if (isPublicSuffix(d)) {
41610
+ throw new ProtocolError(`hello.domains: ${JSON.stringify(d)} is a public suffix \u2014 declare a registrable domain (e.g. "example.${d}"), not the suffix every site under it shares`);
41611
+ }
41288
41612
  }
41289
41613
  if (raw.capabilities !== void 0) {
41290
41614
  if (!Array.isArray(raw.capabilities)) {
@@ -41342,6 +41666,8 @@ function validateHello(raw) {
41342
41666
  assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
41343
41667
  assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
41344
41668
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
41669
+ assertBase64Bytes(raw.sessionPub, "hello.sessionPub", 32);
41670
+ assertBase64Bytes(raw.answersExtNonce, "hello.answersExtNonce", 32);
41345
41671
  assertBase64(raw.sessionSig, "hello.sessionSig");
41346
41672
  return raw;
41347
41673
  }
@@ -41359,11 +41685,34 @@ function validateHello(raw) {
41359
41685
  }
41360
41686
  throw new ProtocolError(`hello.role: must be 'server' or 'extension', got ${String(role)}`);
41361
41687
  }
41688
+ function peekHelloVersion(raw) {
41689
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
41690
+ return null;
41691
+ const frame = raw;
41692
+ if (frame.type !== "hello")
41693
+ return null;
41694
+ const protocolVersion = frame.protocolVersion;
41695
+ if (typeof protocolVersion !== "number" || !Number.isInteger(protocolVersion))
41696
+ return null;
41697
+ const accepts = [];
41698
+ if (Array.isArray(frame.accepts)) {
41699
+ for (const a of frame.accepts) {
41700
+ if (typeof a === "string")
41701
+ accepts.push(a);
41702
+ }
41703
+ }
41704
+ return {
41705
+ protocolVersion,
41706
+ mcpId: isValidMcpId(frame.mcpId) ? frame.mcpId : null,
41707
+ accepts
41708
+ };
41709
+ }
41362
41710
  function validateReady(raw) {
41363
41711
  assertString(raw.mcpId, "ready.mcpId");
41364
41712
  if (!isValidMcpId(raw.mcpId))
41365
41713
  throw new ProtocolError("ready.mcpId: invalid format");
41366
41714
  assertBase64(raw.extensionSessionPub, "ready.extensionSessionPub");
41715
+ assertBase64Bytes(raw.mcpSessionPub, "ready.mcpSessionPub", 32);
41367
41716
  assertBase64(raw.sessionSig, "ready.sessionSig");
41368
41717
  return raw;
41369
41718
  }
@@ -41376,14 +41725,14 @@ function validateEncrypted(raw) {
41376
41725
  assertBase64(raw.ciphertext, "frame.ciphertext");
41377
41726
  return raw;
41378
41727
  }
41379
- var PAIR_CODE_RE = /^\d{3}-\d{3}$/;
41728
+ var PAIR_CODE_RE = /^\d{4}-\d{4}$/;
41380
41729
  function validatePairPending(raw) {
41381
41730
  assertString(raw.mcpId, "pair-pending.mcpId");
41382
41731
  if (!isValidMcpId(raw.mcpId))
41383
41732
  throw new ProtocolError("pair-pending.mcpId: invalid format");
41384
41733
  assertString(raw.pairCode, "pair-pending.pairCode");
41385
41734
  if (!PAIR_CODE_RE.test(raw.pairCode)) {
41386
- throw new ProtocolError(`pair-pending.pairCode: must match XXX-XXX, got ${String(raw.pairCode)}`);
41735
+ throw new ProtocolError(`pair-pending.pairCode: must match XXXX-XXXX, got ${String(raw.pairCode)}`);
41387
41736
  }
41388
41737
  return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
41389
41738
  }
@@ -41831,180 +42180,59 @@ function validateInnerResponse(raw) {
41831
42180
  throw new ProtocolError(`inner.ok: must be boolean, got ${typeof raw.ok}`);
41832
42181
  }
41833
42182
 
41834
- // node_modules/@fetchproxy/protocol/dist/crypto.js
41835
- var subtle = globalThis.crypto.subtle;
41836
- async function exportRaw(key, format) {
41837
- const buf = await subtle.exportKey(format, key);
41838
- return new Uint8Array(buf);
41839
- }
41840
- async function generateX25519() {
41841
- const kp = await subtle.generateKey({ name: "X25519" }, true, [
41842
- "deriveBits"
41843
- ]);
41844
- const priv = await exportRaw(kp.privateKey, "pkcs8");
41845
- const pub = await exportRaw(kp.publicKey, "raw");
41846
- return { privateKey: priv.slice(-32), publicKey: pub };
41847
- }
41848
- async function generateEd25519() {
41849
- const kp = await subtle.generateKey({ name: "Ed25519" }, true, [
41850
- "sign",
41851
- "verify"
41852
- ]);
41853
- const priv = await exportRaw(kp.privateKey, "pkcs8");
41854
- const pub = await exportRaw(kp.publicKey, "raw");
41855
- return { privateKey: priv.slice(-32), publicKey: pub };
41856
- }
41857
- async function importX25519Priv(raw) {
41858
- const prefix = new Uint8Array([
41859
- 48,
41860
- 46,
41861
- 2,
41862
- 1,
41863
- 0,
41864
- 48,
41865
- 5,
41866
- 6,
41867
- 3,
41868
- 43,
41869
- 101,
41870
- 110,
41871
- 4,
41872
- 34,
41873
- 4,
41874
- 32
41875
- ]);
41876
- const pkcs8 = new Uint8Array(prefix.length + raw.length);
41877
- pkcs8.set(prefix, 0);
41878
- pkcs8.set(raw, prefix.length);
41879
- return subtle.importKey("pkcs8", pkcs8, { name: "X25519" }, false, [
41880
- "deriveBits"
41881
- ]);
41882
- }
41883
- async function importX25519Pub(raw) {
41884
- return subtle.importKey("raw", raw, { name: "X25519" }, false, []);
41885
- }
41886
- async function importEd25519Priv(raw) {
41887
- const prefix = new Uint8Array([
41888
- 48,
41889
- 46,
41890
- 2,
41891
- 1,
41892
- 0,
41893
- 48,
41894
- 5,
41895
- 6,
41896
- 3,
41897
- 43,
41898
- 101,
41899
- 112,
41900
- 4,
41901
- 34,
41902
- 4,
41903
- 32
41904
- ]);
41905
- const pkcs8 = new Uint8Array(prefix.length + raw.length);
41906
- pkcs8.set(prefix, 0);
41907
- pkcs8.set(raw, prefix.length);
41908
- return subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"]);
41909
- }
41910
- async function importEd25519Pub(raw) {
41911
- return subtle.importKey("raw", raw, { name: "Ed25519" }, false, ["verify"]);
41912
- }
41913
- async function ecdhX25519(privRaw, pubRaw) {
41914
- const priv = await importX25519Priv(privRaw);
41915
- const pub = await importX25519Pub(pubRaw);
41916
- const bits = await subtle.deriveBits({ name: "X25519", public: pub }, priv, 256);
41917
- return new Uint8Array(bits);
41918
- }
41919
- async function ed25519Sign(privRaw, msg) {
41920
- const priv = await importEd25519Priv(privRaw);
41921
- const sig = await subtle.sign({ name: "Ed25519" }, priv, msg);
41922
- return new Uint8Array(sig);
41923
- }
41924
- async function ed25519Verify(pubRaw, msg, sig) {
41925
- const pub = await importEd25519Pub(pubRaw);
41926
- return subtle.verify({ name: "Ed25519" }, pub, sig, msg);
41927
- }
41928
- async function hkdfSha256(ikm, salt, info, lenBytes) {
41929
- const baseKey = await subtle.importKey("raw", ikm, { name: "HKDF" }, false, [
41930
- "deriveBits"
41931
- ]);
41932
- const bits = await subtle.deriveBits({
41933
- name: "HKDF",
41934
- hash: "SHA-256",
41935
- salt,
41936
- info
41937
- }, baseKey, lenBytes * 8);
41938
- return new Uint8Array(bits);
41939
- }
41940
- async function aesGcmSeal(key, iv, plaintext) {
41941
- const k = await subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
41942
- "encrypt"
41943
- ]);
41944
- const ct = await subtle.encrypt({ name: "AES-GCM", iv }, k, plaintext);
41945
- return new Uint8Array(ct);
41946
- }
41947
- async function aesGcmOpen(key, iv, ciphertext) {
41948
- const k = await subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
41949
- "decrypt"
41950
- ]);
41951
- const pt = await subtle.decrypt({ name: "AES-GCM", iv }, k, ciphertext);
41952
- return new Uint8Array(pt);
41953
- }
41954
- async function sha256(data) {
41955
- const h = await subtle.digest("SHA-256", data);
41956
- return new Uint8Array(h);
41957
- }
41958
-
41959
- // node_modules/@fetchproxy/protocol/dist/encoding.js
41960
- function toB64(bytes) {
41961
- let s = "";
41962
- for (let i = 0; i < bytes.length; i++)
41963
- s += String.fromCharCode(bytes[i]);
41964
- return btoa(s);
41965
- }
41966
- function fromB64(s) {
41967
- const bin = atob(s);
41968
- const out = new Uint8Array(bin.length);
41969
- for (let i = 0; i < bin.length; i++)
41970
- out[i] = bin.charCodeAt(i);
41971
- return out;
41972
- }
41973
- function concatBytes(a, b) {
41974
- const out = new Uint8Array(a.length + b.length);
41975
- out.set(a, 0);
41976
- out.set(b, a.length);
41977
- return out;
41978
- }
41979
-
41980
42183
  // node_modules/@fetchproxy/protocol/dist/pair-code.js
41981
- async function derivePairCode(pub) {
41982
- const h = await sha256(pub);
41983
- const b0 = h[0] ?? 0;
41984
- const b1 = h[1] ?? 0;
41985
- const b2 = h[2] ?? 0;
41986
- const b3 = h[3] ?? 0;
41987
- const u32 = (b0 << 24 | b1 << 16 | b2 << 8 | b3) >>> 0;
41988
- const n = u32 % 1e6;
41989
- const s = n.toString().padStart(6, "0");
41990
- return `${s.slice(0, 3)}-${s.slice(3)}`;
42184
+ var enc2 = new TextEncoder();
42185
+ var PAIR_LABEL = "fetchproxy/4/pair";
42186
+ async function pairTranscript(mcpIdentityPub, extIdentityPub, mcpNonce, extNonce, mcpSessionPub) {
42187
+ const label = enc2.encode(PAIR_LABEL);
42188
+ const parts = [mcpIdentityPub, extIdentityPub, mcpNonce, extNonce, mcpSessionPub];
42189
+ const out = new Uint8Array(label.length + 1 + parts.reduce((n, p) => n + p.length, 0));
42190
+ out.set(label, 0);
42191
+ out[label.length] = 0;
42192
+ let at = label.length + 1;
42193
+ for (const p of parts) {
42194
+ out.set(p, at);
42195
+ at += p.length;
42196
+ }
42197
+ return digits(await sha256(out));
41991
42198
  }
41992
- async function derivePairCodeFromIds(mcpPub, extPub) {
41993
- return derivePairCode(concatBytes(mcpPub, extPub));
42199
+ function digits(hash2) {
42200
+ let n = 0n;
42201
+ for (let i = 0; i < 8; i++)
42202
+ n = n << 8n | BigInt(hash2[i] ?? 0);
42203
+ const s = (n % 100000000n).toString().padStart(8, "0");
42204
+ return `${s.slice(0, 4)}-${s.slice(4)}`;
41994
42205
  }
41995
42206
 
41996
42207
  // node_modules/@fetchproxy/protocol/dist/seal.js
41997
- var enc = new TextEncoder();
42208
+ var enc3 = new TextEncoder();
41998
42209
  var dec = new TextDecoder();
41999
42210
  function randomIv() {
42000
42211
  const iv = new Uint8Array(12);
42001
42212
  globalThis.crypto.getRandomValues(iv);
42002
42213
  return iv;
42003
42214
  }
42004
- async function sealInnerFrame(sessionKey, mcpId, seq, inner) {
42215
+ var AES_GCM_TAG_BYTES = 16;
42216
+ var IV_B64_BYTES = 16;
42217
+ function base64Length(byteLength) {
42218
+ return Math.ceil(byteLength / 3) * 4;
42219
+ }
42220
+ function encodeInnerFrame(inner) {
42221
+ return enc3.encode(JSON.stringify(inner));
42222
+ }
42223
+ function plaintextOf(inner) {
42224
+ return inner instanceof Uint8Array ? inner : encodeInnerFrame(inner);
42225
+ }
42226
+ function sealedFrameWireBytes(mcpId, seq, inner) {
42227
+ const plaintext = plaintextOf(inner).length;
42228
+ const envelope = enc3.encode(JSON.stringify({ type: "frame", mcpId, seq, iv: "", ciphertext: "" })).length;
42229
+ return envelope + IV_B64_BYTES + base64Length(plaintext + AES_GCM_TAG_BYTES);
42230
+ }
42231
+ var MAX_FRAME_BYTES = 42 * 1024 * 1024;
42232
+ async function sealInnerFrame(sessionKey, mcpId, seq, inner, direction) {
42005
42233
  const iv = randomIv();
42006
- const pt = enc.encode(JSON.stringify(inner));
42007
- const ct = await aesGcmSeal(sessionKey, iv, pt);
42234
+ const pt = plaintextOf(inner);
42235
+ const ct = await aesGcmSeal(sessionKey, iv, pt, frameAad(mcpId, seq, direction));
42008
42236
  return {
42009
42237
  type: "frame",
42010
42238
  mcpId,
@@ -42013,18 +42241,18 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner) {
42013
42241
  ciphertext: toB64(ct)
42014
42242
  };
42015
42243
  }
42016
- async function openEncryptedFrame(sessionKey, frame) {
42017
- const result = await openEncryptedFrameDetailed(sessionKey, frame);
42244
+ async function openEncryptedFrame(sessionKey, frame, direction) {
42245
+ const result = await openEncryptedFrameDetailed(sessionKey, frame, direction);
42018
42246
  if (result.stage === "ok")
42019
42247
  return result.inner;
42020
42248
  throw result.error instanceof Error ? result.error : new Error(String(result.error));
42021
42249
  }
42022
- async function openEncryptedFrameDetailed(sessionKey, frame) {
42250
+ async function openEncryptedFrameDetailed(sessionKey, frame, direction) {
42023
42251
  let pt;
42024
42252
  try {
42025
42253
  const iv = fromB64(frame.iv);
42026
42254
  const ct = fromB64(frame.ciphertext);
42027
- pt = await aesGcmOpen(sessionKey, iv, ct);
42255
+ pt = await aesGcmOpen(sessionKey, iv, ct, frameAad(frame.mcpId, frame.seq, direction));
42028
42256
  } catch (error62) {
42029
42257
  return { stage: "decrypt-failed", error: error62 };
42030
42258
  }
@@ -42108,7 +42336,7 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
42108
42336
  async function buildServerHello(opts) {
42109
42337
  const sessionNonce = new Uint8Array(32);
42110
42338
  globalThis.crypto.getRandomValues(sessionNonce);
42111
- const sig = await ed25519Sign(opts.identity.ed25519Priv, concatBytes(new TextEncoder().encode(opts.mcpId), sessionNonce));
42339
+ const sig = await ed25519Sign(opts.identity.ed25519Priv, helloSignaturePayload(opts.mcpId, sessionNonce, opts.sessionPub, opts.answersExtNonce));
42112
42340
  const hello = {
42113
42341
  type: "hello",
42114
42342
  protocolVersion: PROTOCOL_VERSION,
@@ -42121,6 +42349,8 @@ async function buildServerHello(opts) {
42121
42349
  identityX25519Pub: toB64(opts.identity.x25519Pub),
42122
42350
  identityEd25519Pub: toB64(opts.identity.ed25519Pub),
42123
42351
  sessionNonce: toB64(sessionNonce),
42352
+ sessionPub: toB64(opts.sessionPub),
42353
+ answersExtNonce: toB64(opts.answersExtNonce),
42124
42354
  sessionSig: toB64(sig)
42125
42355
  };
42126
42356
  if (opts.accepts && opts.accepts.length > 0)
@@ -42177,23 +42407,91 @@ async function buildServerHello(opts) {
42177
42407
  return hello;
42178
42408
  }
42179
42409
 
42410
+ // node_modules/@fetchproxy/server/dist/frame-size.js
42411
+ function encodeOutboundInnerFrame(mcpId, inner) {
42412
+ const plaintext = encodeInnerFrame(inner);
42413
+ const wireBytes = sealedFrameWireBytes(mcpId, Number.MAX_SAFE_INTEGER, plaintext);
42414
+ if (wireBytes > MAX_FRAME_BYTES) {
42415
+ throw new Error(`fetchproxy: this request is too large for one bridge frame: ${wireBytes} bytes on the wire, cap ${MAX_FRAME_BYTES}. The request is refused; the bridge is not closed, so other calls and other MCPs on it are unaffected.`);
42416
+ }
42417
+ return plaintext;
42418
+ }
42419
+
42180
42420
  // node_modules/@fetchproxy/server/dist/session.js
42421
+ var MAX_INFLIGHT_INBOUND_SEQS = 1024;
42181
42422
  var SessionState = class {
42182
42423
  sessionKey;
42183
42424
  outboundSeq = 0;
42184
42425
  lastInboundSeq = 0;
42426
+ inflightInbound = /* @__PURE__ */ new Set();
42427
+ saturationWarned = false;
42185
42428
  constructor(sessionKey) {
42186
42429
  this.sessionKey = sessionKey;
42187
42430
  }
42431
+ /**
42432
+ * Whether the caller holding this claim verdict should log saturation now.
42433
+ * True only on the TRANSITION into it: a line per dropped frame turned a
42434
+ * flood — or a set that never drains — into a log flood amplifying the very
42435
+ * condition it reported (#376). An `'ok'` claim re-arms the latch, since it
42436
+ * is the one signal every caller sees that the set has drained below the
42437
+ * bound; a replay needs no capacity and leaves it alone. Latched per
42438
+ * session, so a renegotiated one starts armed. A set hovering at the bound
42439
+ * can still alternate ok/saturated, but that logs at the rate frames finish
42440
+ * opening, not the rate they arrive.
42441
+ */
42442
+ saturationWarningDue(claim2) {
42443
+ if (claim2 === "ok")
42444
+ this.saturationWarned = false;
42445
+ if (claim2 !== "saturated" || this.saturationWarned)
42446
+ return false;
42447
+ this.saturationWarned = true;
42448
+ return true;
42449
+ }
42188
42450
  nextOutboundSeq() {
42189
42451
  this.outboundSeq += 1;
42190
42452
  return this.outboundSeq;
42191
42453
  }
42192
- acceptInboundSeq(seq) {
42454
+ /**
42455
+ * Take this seq out of circulation for the frame about to be opened, and
42456
+ * say whether it was available. SYNCHRONOUS and single-shot: call it before
42457
+ * the first `await` of the receive path, or a duplicate frame read in the
42458
+ * same pass will be judged against a counter neither frame has moved yet.
42459
+ *
42460
+ * Anything but `'ok'` means "not this frame's to process": `'replay'` for
42461
+ * a spent seq or a duplicate of one still in flight, `'saturated'` when the
42462
+ * in-flight bound is full. Replay is checked first, since refusing a stale
42463
+ * seq needs no capacity. Every `'ok'` MUST be answered by exactly one
42464
+ * {@link commitInboundSeq} or {@link releaseInboundSeq}.
42465
+ */
42466
+ claimInboundSeq(seq) {
42193
42467
  if (seq <= this.lastInboundSeq)
42194
- return false;
42195
- this.lastInboundSeq = seq;
42196
- return true;
42468
+ return "replay";
42469
+ if (this.inflightInbound.has(seq))
42470
+ return "replay";
42471
+ if (this.inflightInbound.size >= MAX_INFLIGHT_INBOUND_SEQS)
42472
+ return "saturated";
42473
+ this.inflightInbound.add(seq);
42474
+ return "ok";
42475
+ }
42476
+ /**
42477
+ * Give a claim back without spending the seq — for a frame that did not
42478
+ * authenticate, which is a frame that never happened. The counter does not
42479
+ * move, so the genuine frames behind it, carrying lower numbers, are still
42480
+ * accepted. Idempotent, so a caller may release in a `catch` after a commit
42481
+ * it is not sure ran.
42482
+ */
42483
+ releaseInboundSeq(seq) {
42484
+ this.inflightInbound.delete(seq);
42485
+ }
42486
+ /**
42487
+ * Record a seq as spent. Call only for a frame that authenticated. Never
42488
+ * moves the counter backwards, so an out-of-order commit cannot reopen a
42489
+ * seq an earlier one already closed.
42490
+ */
42491
+ commitInboundSeq(seq) {
42492
+ this.inflightInbound.delete(seq);
42493
+ if (seq > this.lastInboundSeq)
42494
+ this.lastInboundSeq = seq;
42197
42495
  }
42198
42496
  };
42199
42497
 
@@ -42227,6 +42525,27 @@ var FetchproxyHelloRejectedError = class extends Error {
42227
42525
  Object.setPrototypeOf(this, new.target.prototype);
42228
42526
  }
42229
42527
  };
42528
+ var MIN_VERSION = "3.0.0";
42529
+ var FetchproxyProtocolVersionError = class extends Error {
42530
+ /** The protocol version this process speaks. */
42531
+ ourVersion;
42532
+ /** The protocol version the far end announced in the hello we refused. */
42533
+ theirVersion;
42534
+ peer;
42535
+ constructor(info) {
42536
+ const far = info.peer === "extension" ? `the attached browser extension speaks ${info.theirVersion} \u2014 update Transporter (the fetchproxy extension) to ${MIN_VERSION} or later` : `the MCP holding the bridge port speaks ${info.theirVersion} \u2014 upgrade @fetchproxy/server to ${MIN_VERSION} or later in that MCP`;
42537
+ super(`protocol version mismatch: this MCP speaks fetchproxy protocol ${info.ourVersion}, ${far}`);
42538
+ this.name = "FetchproxyProtocolVersionError";
42539
+ this.ourVersion = info.ourVersion;
42540
+ this.theirVersion = info.theirVersion;
42541
+ this.peer = info.peer;
42542
+ Object.setPrototypeOf(this, new.target.prototype);
42543
+ }
42544
+ };
42545
+ function protocolVersionCloseReason(info) {
42546
+ const far = info.peer === "extension" ? "the extension" : "the other MCP";
42547
+ return `protocol version mismatch: this MCP speaks ${info.ourVersion}, ${far} speaks ${info.theirVersion}`;
42548
+ }
42230
42549
  async function awaitSessionReady(ready, opts) {
42231
42550
  const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
42232
42551
  if (ms <= 0)
@@ -42247,11 +42566,12 @@ async function awaitSessionReady(ready, opts) {
42247
42566
  }
42248
42567
 
42249
42568
  // node_modules/@fetchproxy/server/dist/extension-trust.js
42250
- import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
42251
- import { join as join4 } from "node:path";
42569
+ import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
42570
+ import { isAbsolute as isAbsolute3, join as join4 } from "node:path";
42252
42571
 
42253
42572
  // node_modules/@fetchproxy/server/dist/identity.js
42254
- import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
42573
+ import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink } from "node:fs/promises";
42574
+ import { randomBytes as randomBytes2 } from "node:crypto";
42255
42575
  import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
42256
42576
  import { homedir as homedir3 } from "node:os";
42257
42577
  var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
@@ -42307,16 +42627,46 @@ function parseIdentity(text) {
42307
42627
  createdAt: j.createdAt
42308
42628
  };
42309
42629
  }
42630
+ async function openIdentityDir(dir) {
42631
+ await mkdir(dir, { recursive: true, mode: 448 });
42632
+ try {
42633
+ await chmod(dir, 448);
42634
+ } catch (e) {
42635
+ const code = e.code;
42636
+ if (code !== "EROFS" && code !== "EPERM")
42637
+ throw e;
42638
+ }
42639
+ }
42640
+ var STAGING_BYTES = 6;
42641
+ async function sweepStagingFiles(dir, serverName) {
42642
+ const base = safeIdentityFileBase(serverName).replace(/[.]/g, "\\.");
42643
+ const staging = new RegExp(`^${base}\\.json\\.[0-9a-f]{${STAGING_BYTES * 2}}\\.tmp$`);
42644
+ let names;
42645
+ try {
42646
+ names = await readdir(dir);
42647
+ } catch {
42648
+ return;
42649
+ }
42650
+ await Promise.all(names.filter((name) => staging.test(name)).map((name) => unlink(join3(dir, name)).catch(() => void 0)));
42651
+ }
42310
42652
  async function writeIdentityFile(dir, serverName, id) {
42311
42653
  const path = identityFilePath(dir, serverName);
42312
- await mkdir(dir, { recursive: true, mode: 448 });
42313
- await writeFile(path, serializeIdentity(id), { mode: 384 });
42314
- await chmod(path, 384);
42654
+ await openIdentityDir(dir);
42655
+ await sweepStagingFiles(dir, serverName);
42656
+ const tmp = `${path}.${randomBytes2(STAGING_BYTES).toString("hex")}.tmp`;
42657
+ try {
42658
+ await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
42659
+ await chmod(tmp, 384);
42660
+ await rename(tmp, path);
42661
+ } catch (e) {
42662
+ await rm(tmp, { force: true });
42663
+ throw e;
42664
+ }
42315
42665
  return path;
42316
42666
  }
42317
42667
  async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
42318
42668
  const path = identityFilePath(dir, serverName);
42319
- await mkdir(dir, { recursive: true, mode: 448 });
42669
+ await openIdentityDir(dir);
42320
42670
  try {
42321
42671
  return parseIdentity(await readFile(path, "utf8"));
42322
42672
  } catch (e) {
@@ -42329,12 +42679,32 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
42329
42679
  }
42330
42680
 
42331
42681
  // node_modules/@fetchproxy/server/dist/extension-trust.js
42682
+ var TRUST_DIR_ENV = "FETCHPROXY_TRUST_DIR";
42683
+ function envTrustDir(env = process.env) {
42684
+ const raw = env[TRUST_DIR_ENV];
42685
+ if (raw === void 0)
42686
+ return void 0;
42687
+ const dir = raw.trim();
42688
+ if (dir !== "" && isAbsolute3(dir))
42689
+ return dir;
42690
+ console.warn(`[fetchproxy] ignoring ${TRUST_DIR_ENV}=${JSON.stringify(raw)}: it is not an absolute path. The extension pin will be written beside the identity instead, which is not where you pointed it \u2014 if that directory is read-only, no pin is ever kept.`);
42691
+ return void 0;
42692
+ }
42693
+ function resolveTrustDir(explicit, identityDir) {
42694
+ if (explicit !== void 0)
42695
+ return explicit;
42696
+ return envTrustDir() ?? identityDir ?? defaultIdentityDir();
42697
+ }
42698
+ function defaultTrustDir() {
42699
+ return resolveTrustDir();
42700
+ }
42332
42701
  function fileExtensionTrust(args) {
42702
+ const dir = resolveTrustDir(args.trustDir, args.dir);
42333
42703
  return {
42334
42704
  allowNew: args.allowNew,
42335
- location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
42336
- read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
42337
- write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
42705
+ location: extensionTrustPath(args.serverName, dir),
42706
+ read: () => readExtensionPin(args.serverName, dir),
42707
+ write: (pin) => writeExtensionPin(args.serverName, pin, dir)
42338
42708
  };
42339
42709
  }
42340
42710
  var TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
@@ -42362,14 +42732,14 @@ function decideExtensionTrust(args) {
42362
42732
  message: `[fetchproxy] ${serverName}: refusing an extension whose identity is not the one this MCP paired with. If you re-installed the extension or moved to another browser, re-pair deliberately: run this MCP once with ${TRUST_NEW_EXTENSION_ENV}=1, or delete ${trustPath}. If you did neither, something else is answering as your browser.`
42363
42733
  };
42364
42734
  }
42365
- function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
42735
+ function extensionTrustPath(serverName, dir = defaultTrustDir()) {
42366
42736
  return join4(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
42367
42737
  }
42368
42738
  function extensionTrustPathHint(serverName) {
42369
42739
  try {
42370
42740
  return extensionTrustPath(serverName);
42371
42741
  } catch {
42372
- return join4(defaultIdentityDir(), "<server-name>.extension-trust.json");
42742
+ return join4(defaultTrustDir(), "<server-name>.extension-trust.json");
42373
42743
  }
42374
42744
  }
42375
42745
  function isPin(x) {
@@ -42378,7 +42748,7 @@ function isPin(x) {
42378
42748
  const r = x;
42379
42749
  return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
42380
42750
  }
42381
- async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
42751
+ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
42382
42752
  const path = extensionTrustPath(serverName, dir);
42383
42753
  let raw;
42384
42754
  try {
@@ -42403,31 +42773,75 @@ async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
42403
42773
  pinnedAt: parsed.pinnedAt
42404
42774
  };
42405
42775
  }
42406
- async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
42776
+ async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
42407
42777
  const path = extensionTrustPath(serverName, dir);
42408
42778
  await mkdir2(dir, { recursive: true, mode: 448 });
42409
42779
  const tmp = `${path}.tmp`;
42410
42780
  await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
42411
42781
  await chmod2(tmp, 384);
42412
- await rename(tmp, path);
42782
+ await rename2(tmp, path);
42413
42783
  }
42414
42784
 
42415
42785
  // node_modules/@fetchproxy/server/dist/host.js
42416
- var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
42417
- var enc2 = new TextEncoder();
42786
+ var HTTP_ORIGIN_RE = /^https?:\/\//i;
42787
+ var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost|\[::1\])(:|$))/i;
42788
+ var EXTENSION_ORIGIN_RE = /^(chrome-extension|moz-extension|safari-web-extension):\/\//i;
42789
+ var OPAQUE_ORIGIN = "null";
42790
+ var ALLOW_LOCAL_ORIGINS_ENV = "FETCHPROXY_ALLOW_LOCAL_ORIGINS";
42791
+ function envAllowsLocalOrigins(env = process.env) {
42792
+ const raw = env[ALLOW_LOCAL_ORIGINS_ENV];
42793
+ if (raw === void 0 || raw.trim() === "")
42794
+ return false;
42795
+ if (raw.trim() === "1")
42796
+ return true;
42797
+ console.warn(`[fetchproxy] ignoring ${ALLOW_LOCAL_ORIGINS_ENV}=${JSON.stringify(raw)}: the only value that turns it on is 1. Upgrades from a null or localhost origin stay refused.`);
42798
+ return false;
42799
+ }
42800
+ function originVerdict(origin, allowLocal) {
42801
+ if (origin === void 0)
42802
+ return { allow: true };
42803
+ if (HTTP_ORIGIN_RE.test(origin)) {
42804
+ if (PUBLIC_ORIGIN_RE.test(origin)) {
42805
+ return { allow: false, reason: "origin not allowed", escapable: false };
42806
+ }
42807
+ return allowLocal ? { allow: true } : { allow: false, reason: "localhost page origin not allowed", escapable: true };
42808
+ }
42809
+ if (origin.toLowerCase() === OPAQUE_ORIGIN) {
42810
+ return allowLocal ? { allow: true } : { allow: false, reason: "opaque (null) origin not allowed", escapable: true };
42811
+ }
42812
+ if (EXTENSION_ORIGIN_RE.test(origin))
42813
+ return { allow: true };
42814
+ return { allow: false, reason: "origin not allowed", escapable: false };
42815
+ }
42816
+ var HANDSHAKE_TIMEOUT_MS = 15e3;
42817
+ var MAX_PAYLOAD_BYTES = MAX_FRAME_BYTES;
42818
+ var enc4 = new TextEncoder();
42418
42819
  async function startHost(opts) {
42820
+ const allowLocalOrigins = envAllowsLocalOrigins();
42821
+ if (allowLocalOrigins) {
42822
+ console.warn(`[fetchproxy] ${ALLOW_LOCAL_ORIGINS_ENV}=1: accepting WebSocket upgrades from null and localhost page origins. This is a development escape \u2014 any local page the browser has open can reach this concentrator while it is set.`);
42823
+ }
42824
+ const warnedOrigins = /* @__PURE__ */ new Set();
42419
42825
  const wss = new import_websocket_server.default({
42420
42826
  server: opts.httpServer,
42827
+ maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES,
42421
42828
  verifyClient: (info, cb) => {
42422
42829
  const origin = info.req.headers.origin;
42423
- if (origin && PUBLIC_ORIGIN_RE.test(origin)) {
42424
- cb(false, 403, "origin not allowed");
42830
+ const verdict = originVerdict(origin, allowLocalOrigins);
42831
+ if (verdict.allow) {
42832
+ cb(true);
42425
42833
  return;
42426
42834
  }
42427
- cb(true);
42835
+ if (verdict.escapable && origin !== void 0 && warnedOrigins.size < 8) {
42836
+ if (!warnedOrigins.has(origin)) {
42837
+ warnedOrigins.add(origin);
42838
+ console.warn(`[fetchproxy] refused a WebSocket upgrade from ${JSON.stringify(origin)}: ${verdict.reason}. If that is you, developing against the bridge, set ${ALLOW_LOCAL_ORIGINS_ENV}=1 on this MCP \u2014 never on a deployed one.`);
42839
+ }
42840
+ }
42841
+ cb(false, 403, verdict.reason);
42428
42842
  }
42429
42843
  });
42430
- const ownHello = await buildServerHello({
42844
+ const ownHelloBase = {
42431
42845
  // 2.6.0: tell the extension it can say WHY it refused a hello, instead of
42432
42846
  // leaving us to time out and guess.
42433
42847
  accepts: ["hello-rejected"],
@@ -42446,14 +42860,26 @@ async function startHost(opts) {
42446
42860
  sessionStoragePointers: opts.ownSessionStoragePointers,
42447
42861
  domSelectors: opts.ownDomSelectors,
42448
42862
  graphqlOps: opts.ownGraphqlOps
42449
- });
42450
- const ownSessionNonce = fromB64(ownHello.sessionNonce);
42863
+ };
42864
+ const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
42451
42865
  let extensionWs = null;
42452
42866
  const peers = /* @__PURE__ */ new Map();
42453
42867
  const ownInnerListeners = [];
42454
42868
  const disconnectListeners = [];
42455
42869
  const pendingPairListeners = [];
42456
42870
  let ownSession = null;
42871
+ let ownEphemeral = null;
42872
+ function dropOwnSessionAndEphemeral() {
42873
+ if (ownEphemeral)
42874
+ ownEphemeral.priv.fill(0);
42875
+ ownEphemeral = null;
42876
+ return null;
42877
+ }
42878
+ function installOwnEphemeral(next) {
42879
+ if (ownEphemeral)
42880
+ ownEphemeral.priv.fill(0);
42881
+ ownEphemeral = next;
42882
+ }
42457
42883
  let ownPendingPairCode = null;
42458
42884
  let resolveOwnSession;
42459
42885
  let rejectOwnSession;
@@ -42467,21 +42893,74 @@ async function startHost(opts) {
42467
42893
  });
42468
42894
  }
42469
42895
  resetSessionPromise();
42896
+ let ownSessionRefusal = null;
42897
+ function refuseVersionMismatch(ws, raw, identified) {
42898
+ const peek = peekHelloVersion(raw);
42899
+ if (!peek || peek.protocolVersion === PROTOCOL_VERSION)
42900
+ return false;
42901
+ const err = new FetchproxyProtocolVersionError({
42902
+ ourVersion: PROTOCOL_VERSION,
42903
+ theirVersion: peek.protocolVersion,
42904
+ peer: peek.mcpId === null ? "extension" : "mcp"
42905
+ });
42906
+ console.warn(`[fetchproxy] host: ${err.message}`);
42907
+ const nothingBetterAttached = extensionWs === null && extensionClaim === null && ownSession === null;
42908
+ if (err.peer === "extension" && identified !== "peer" && nothingBetterAttached) {
42909
+ ownSessionRefusal = err;
42910
+ rejectOwnSession(err);
42911
+ resetSessionPromise();
42912
+ rejectOwnSession(err);
42913
+ }
42914
+ try {
42915
+ ws.close(1002, protocolVersionCloseReason(err));
42916
+ } catch {
42917
+ }
42918
+ return true;
42919
+ }
42470
42920
  let extensionHello = null;
42471
42921
  let extensionClaim = null;
42922
+ const pairCodeFor = async (hello, mint) => {
42923
+ if (!mint)
42924
+ return null;
42925
+ try {
42926
+ return await pairTranscript(opts.ownIdentity.x25519Pub, fromB64(hello.identityX25519Pub), mint.nonce, fromB64(hello.sessionNonce), mint.pub);
42927
+ } catch (e) {
42928
+ console.error("[fetchproxy] could not derive the pair code:", e);
42929
+ return null;
42930
+ }
42931
+ };
42472
42932
  wss.on("connection", (ws) => {
42473
42933
  let identified = null;
42474
42934
  let peerMcpId = null;
42475
42935
  let closed = false;
42476
42936
  let pinOnReady = false;
42937
+ const handshakeTimer = setTimeout(() => {
42938
+ if (identified || closed)
42939
+ return;
42940
+ try {
42941
+ ws.close(1008, "handshake timeout");
42942
+ } catch {
42943
+ }
42944
+ }, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
42945
+ handshakeTimer.unref?.();
42946
+ ws.on("error", (e) => {
42947
+ console.warn(`[fetchproxy] host: socket error: ${String(e)}`);
42948
+ });
42477
42949
  ws.on("message", async (data) => {
42478
42950
  try {
42951
+ let raw;
42952
+ try {
42953
+ raw = JSON.parse(data.toString());
42954
+ } catch {
42955
+ ws.close(1002, "protocol error");
42956
+ return;
42957
+ }
42479
42958
  let frame;
42480
42959
  try {
42481
- const raw = JSON.parse(data.toString());
42482
42960
  frame = validateFrame(raw);
42483
42961
  } catch {
42484
- ws.close(1002, "protocol error");
42962
+ if (!refuseVersionMismatch(ws, raw, identified))
42963
+ ws.close(1002, "protocol error");
42485
42964
  return;
42486
42965
  }
42487
42966
  if (frame.type === "hello" && frame.role === "extension") {
@@ -42525,25 +43004,43 @@ async function startHost(opts) {
42525
43004
  identified = "extension";
42526
43005
  extensionWs = ws;
42527
43006
  extensionHello = frame;
42528
- if (opts.onPairCode) {
43007
+ if (ownSessionRefusal) {
43008
+ ownSessionRefusal = null;
43009
+ resetSessionPromise();
43010
+ }
43011
+ const mintedKeypair = await generateSessionKeypair();
43012
+ const mintedHello = await buildServerHello({
43013
+ ...ownHelloBase,
43014
+ sessionPub: mintedKeypair.publicKey,
43015
+ // The echo Rule B's gate reads, inside the signed payload: this
43016
+ // hello answers the extension session whose hello triggered it.
43017
+ answersExtNonce: fromB64(frame.sessionNonce)
43018
+ });
43019
+ if (extensionWs !== ws) {
43020
+ mintedKeypair.privateKey.fill(0);
43021
+ return;
43022
+ }
43023
+ const mint = {
43024
+ nonce: fromB64(mintedHello.sessionNonce),
43025
+ pub: mintedKeypair.publicKey
43026
+ };
43027
+ installOwnEphemeral({ ...mint, priv: mintedKeypair.privateKey });
43028
+ for (const slot of peers.values())
43029
+ slot.ws.send(JSON.stringify(frame));
43030
+ ws.send(JSON.stringify(mintedHello));
43031
+ const helloPairCode = await pairCodeFor(frame, mint);
43032
+ if (opts.onPairCode && helloPairCode !== null) {
42529
43033
  try {
42530
- const code = await derivePairCodeFromIds(opts.ownIdentity.x25519Pub, fromB64(frame.identityX25519Pub));
42531
- opts.onPairCode(code);
43034
+ opts.onPairCode(helloPairCode);
42532
43035
  } catch (e) {
42533
43036
  console.error("[fetchproxy] onPairCode threw:", e);
42534
43037
  }
42535
43038
  }
42536
- for (const slot of peers.values())
42537
- slot.ws.send(JSON.stringify(frame));
42538
- ws.send(JSON.stringify(ownHello));
42539
- for (const slot of peers.values()) {
42540
- ws.send(JSON.stringify(slot.helloFrame));
42541
- }
42542
43039
  return;
42543
43040
  }
42544
43041
  if (frame.type === "hello" && frame.role === "server") {
42545
43042
  const peerEdPub = fromB64(frame.identityEd25519Pub);
42546
- const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
43043
+ const peerSigMsg = helloSignaturePayload(frame.mcpId, fromB64(frame.sessionNonce), fromB64(frame.sessionPub), fromB64(frame.answersExtNonce));
42547
43044
  const peerSig = fromB64(frame.sessionSig);
42548
43045
  let peerSigOk = false;
42549
43046
  try {
@@ -42568,10 +43065,12 @@ async function startHost(opts) {
42568
43065
  identified = "peer";
42569
43066
  peerMcpId = frame.mcpId;
42570
43067
  peers.set(frame.mcpId, { ws, helloFrame: frame });
42571
- if (extensionWs)
42572
- extensionWs.send(JSON.stringify(frame));
42573
- if (extensionHello)
43068
+ if (extensionHello && answersNoExtSession(frame.answersExtNonce)) {
42574
43069
  ws.send(JSON.stringify(extensionHello));
43070
+ }
43071
+ if (extensionWs && extensionHello && frame.answersExtNonce === extensionHello.sessionNonce) {
43072
+ extensionWs.send(JSON.stringify(frame));
43073
+ }
42575
43074
  return;
42576
43075
  }
42577
43076
  if (frame.type === "ready") {
@@ -42581,9 +43080,14 @@ async function startHost(opts) {
42581
43080
  ws.close(1002, "ready before extension hello");
42582
43081
  return;
42583
43082
  }
43083
+ const held = ownEphemeral;
43084
+ if (!held || frame.mcpSessionPub !== toB64(held.pub)) {
43085
+ console.warn("[fetchproxy] discarding a ready for a session ephemeral this host no longer holds (the extension reconnected while a hello was in flight)");
43086
+ return;
43087
+ }
42584
43088
  const extEdPub = fromB64(extensionHello.identityEd25519Pub);
42585
43089
  const extNonce = fromB64(extensionHello.sessionNonce);
42586
- const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
43090
+ const msg = readySignaturePayload(held.nonce, extNonce, fromB64(frame.extensionSessionPub), held.pub);
42587
43091
  const sig = fromB64(frame.sessionSig);
42588
43092
  let sigOk = false;
42589
43093
  try {
@@ -42609,8 +43113,9 @@ async function startHost(opts) {
42609
43113
  }
42610
43114
  }
42611
43115
  const extPub = fromB64(frame.extensionSessionPub);
42612
- const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
42613
- const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
43116
+ const shared = await ecdhX25519(held.priv, extPub);
43117
+ const salt = await transcriptHash(held.nonce, extNonce, held.pub, extPub);
43118
+ const key = await hkdfSha256(shared, salt, enc4.encode(HKDF_SESSION_INFO), 32);
42614
43119
  if (extensionWs !== ws)
42615
43120
  return;
42616
43121
  ownSession = new SessionState(key);
@@ -42626,11 +43131,23 @@ async function startHost(opts) {
42626
43131
  if (frame.type === "frame") {
42627
43132
  if (identified === "extension") {
42628
43133
  if (frame.mcpId === opts.ownMcpId) {
42629
- if (!ownSession)
43134
+ const session = ownSession;
43135
+ if (!session)
42630
43136
  return;
42631
- if (!ownSession.acceptInboundSeq(frame.seq))
43137
+ const claim2 = session.claimInboundSeq(frame.seq);
43138
+ if (session.saturationWarningDue(claim2)) {
43139
+ console.warn(`[fetchproxy] ${opts.ownServerName}: dropped an inbound frame (seq ${frame.seq}) unread \u2014 too many frames from the extension are still being opened (inbound claims saturated). Not a replay. Further drops are not logged until the in-flight set drains.`);
43140
+ }
43141
+ if (claim2 !== "ok")
42632
43142
  return;
42633
- const inner = await openEncryptedFrame(ownSession.sessionKey, frame);
43143
+ let inner;
43144
+ try {
43145
+ inner = await openEncryptedFrame(session.sessionKey, frame, "e2s");
43146
+ } catch (e) {
43147
+ session.releaseInboundSeq(frame.seq);
43148
+ throw e;
43149
+ }
43150
+ session.commitInboundSeq(frame.seq);
42634
43151
  ownInnerListeners.forEach((cb) => cb(inner));
42635
43152
  } else {
42636
43153
  const slot = peers.get(frame.mcpId);
@@ -42654,8 +43171,16 @@ async function startHost(opts) {
42654
43171
  }
42655
43172
  if (frame.type === "pair-pending" && identified === "extension") {
42656
43173
  if (frame.mcpId === opts.ownMcpId) {
42657
- ownPendingPairCode = frame.pairCode;
42658
- pendingPairListeners.forEach((cb) => cb(frame.pairCode));
43174
+ const hello = extensionHello;
43175
+ const mint = ownEphemeral;
43176
+ const derived2 = hello === null ? null : await pairCodeFor(hello, mint);
43177
+ if (derived2 === null || frame.pairCode !== derived2) {
43178
+ console.error(`[fetchproxy] ${opts.ownServerName}: the extension's pair code (${frame.pairCode}) does not match the one this MCP derived from the pair transcript` + (derived2 === null ? " (none \u2014 this MCP could not derive its own)" : ` (${derived2})`) + " \u2014 refusing to pair (possible MITM between this MCP and the extension)");
43179
+ ws.close(1008, "pair code mismatch");
43180
+ return;
43181
+ }
43182
+ ownPendingPairCode = derived2;
43183
+ pendingPairListeners.forEach((cb) => cb(derived2));
42659
43184
  } else {
42660
43185
  const slot = peers.get(frame.mcpId);
42661
43186
  if (slot)
@@ -42672,6 +43197,7 @@ async function startHost(opts) {
42672
43197
  });
42673
43198
  ws.on("close", () => {
42674
43199
  closed = true;
43200
+ clearTimeout(handshakeTimer);
42675
43201
  if (extensionClaim === ws)
42676
43202
  extensionClaim = null;
42677
43203
  if (identified === "extension" && extensionWs === ws) {
@@ -42680,7 +43206,7 @@ async function startHost(opts) {
42680
43206
  if (!ownSession) {
42681
43207
  rejectOwnSession(new Error("extension disconnected before ready"));
42682
43208
  }
42683
- ownSession = null;
43209
+ ownSession = dropOwnSessionAndEphemeral();
42684
43210
  ownPendingPairCode = null;
42685
43211
  resetSessionPromise();
42686
43212
  disconnectListeners.forEach((cb) => cb());
@@ -42719,7 +43245,8 @@ async function startHost(opts) {
42719
43245
  });
42720
43246
  if (!extensionWs)
42721
43247
  throw new Error("host: no extension connected");
42722
- const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
43248
+ const plaintext = encodeOutboundInnerFrame(opts.ownMcpId, inner);
43249
+ const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), plaintext, "s2e");
42723
43250
  extensionWs.send(JSON.stringify(sealed));
42724
43251
  },
42725
43252
  onOwnInner: (cb) => {
@@ -42738,14 +43265,32 @@ async function startHost(opts) {
42738
43265
  }
42739
43266
 
42740
43267
  // node_modules/@fetchproxy/server/dist/peer.js
42741
- var enc3 = new TextEncoder();
43268
+ var enc5 = new TextEncoder();
42742
43269
  async function startPeer(opts) {
42743
- const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`);
43270
+ const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`, {
43271
+ maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES
43272
+ });
42744
43273
  await new Promise((resolve3, reject) => {
42745
- ws.once("open", () => resolve3());
42746
- ws.once("error", reject);
43274
+ const onOpen = () => {
43275
+ cleanup();
43276
+ resolve3();
43277
+ };
43278
+ const onError = (e) => {
43279
+ cleanup();
43280
+ reject(e);
43281
+ };
43282
+ const cleanup = () => {
43283
+ ws.off("open", onOpen);
43284
+ ws.off("error", onError);
43285
+ };
43286
+ ws.once("open", onOpen);
43287
+ ws.once("error", onError);
42747
43288
  });
42748
- const hello = await buildServerHello({
43289
+ ws.on("error", (e) => {
43290
+ console.warn(`[fetchproxy] peer: socket error: ${String(e)}`);
43291
+ });
43292
+ const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
43293
+ const helloBase = {
42749
43294
  identity: opts.identity,
42750
43295
  mcpId: opts.mcpId,
42751
43296
  serverName: opts.serverName,
@@ -42765,15 +43310,39 @@ async function startPeer(opts) {
42765
43310
  // `extensionConnected()` / `sessionLinked()` can go back to false.
42766
43311
  // 2.6.0: `hello-rejected` for the same reason, from the extension.
42767
43312
  accepts: ["extension-disconnected", "hello-rejected"]
43313
+ };
43314
+ const bootstrapKeypair = await generateSessionKeypair();
43315
+ let bootstrapPriv = bootstrapKeypair.privateKey;
43316
+ const hello = await buildServerHello({
43317
+ ...helloBase,
43318
+ sessionPub: bootstrapKeypair.publicKey,
43319
+ // At dial this peer has been told of no extension session, and saying so
43320
+ // is what keeps the host from forwarding a hello whose ephemeral is not
43321
+ // one a session may be opened from.
43322
+ answersExtNonce: ANSWERS_NO_EXT_SESSION
42768
43323
  });
42769
- const sessionNonce = fromB64(hello.sessionNonce);
42770
43324
  ws.send(JSON.stringify(hello));
42771
43325
  const innerListeners = [];
42772
43326
  const renegotiateListeners = [];
42773
43327
  const pendingPairListeners = [];
42774
43328
  const closeListeners = [];
42775
43329
  let session = null;
43330
+ let sessionEphemeral = null;
43331
+ function dropSessionEphemeral() {
43332
+ if (sessionEphemeral)
43333
+ sessionEphemeral.priv.fill(0);
43334
+ sessionEphemeral = null;
43335
+ }
43336
+ function installSessionEphemeral(next) {
43337
+ dropSessionEphemeral();
43338
+ if (bootstrapPriv) {
43339
+ bootstrapPriv.fill(0);
43340
+ bootstrapPriv = null;
43341
+ }
43342
+ sessionEphemeral = next;
43343
+ }
42776
43344
  let pendingPairCode = null;
43345
+ let warnedUnverifiablePairCode = false;
42777
43346
  let resolveFirstReady;
42778
43347
  let rejectFirstReady;
42779
43348
  const sessionPromise = new Promise((resolve3, reject) => {
@@ -42782,58 +43351,60 @@ async function startPeer(opts) {
42782
43351
  });
42783
43352
  let extensionHello = null;
42784
43353
  let extensionGone = false;
42785
- let warnedUnverifiable = false;
42786
43354
  let cachedPin = void 0;
42787
- const authenticateExtension = async (sessionSig, extensionSessionPub) => {
42788
- if (!extensionHello) {
42789
- if (opts.requireExtensionIdentity) {
42790
- console.error(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity, so this session cannot be verified \u2014 refusing. Upgrade the MCP holding the bridge port to 1.12.0 or later.`);
42791
- return false;
42792
- }
42793
- if (!warnedUnverifiable) {
42794
- warnedUnverifiable = true;
42795
- console.warn(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity (pre-1.12.0), so this peer cannot verify which browser it is talking to. Upgrade the MCP holding the bridge port to close this.`);
42796
- }
42797
- return true;
43355
+ const pairCodeFor = async (hello2, mint) => {
43356
+ if (!mint)
43357
+ return null;
43358
+ try {
43359
+ return await pairTranscript(opts.identity.x25519Pub, fromB64(hello2.identityX25519Pub), mint.nonce, fromB64(hello2.sessionNonce), mint.pub);
43360
+ } catch (e) {
43361
+ console.error("[fetchproxy] could not derive the pair code:", e);
43362
+ return null;
43363
+ }
43364
+ };
43365
+ const authenticateExtension = async (sessionSig, extensionSessionPub, held, hello2) => {
43366
+ if (!hello2) {
43367
+ console.error(`[fetchproxy] ${opts.serverName}: no extension hello has been relayed to this peer, so there is nothing to derive a v4 session from \u2014 refusing. Upgrade the MCP holding the bridge port to 3.0.0 or later.`);
43368
+ return null;
42798
43369
  }
42799
- const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
43370
+ const payload = readySignaturePayload(held.nonce, fromB64(hello2.sessionNonce), fromB64(extensionSessionPub), held.pub);
42800
43371
  let sigOk = false;
42801
43372
  try {
42802
- sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
43373
+ sigOk = await ed25519Verify(fromB64(hello2.identityEd25519Pub), payload, fromB64(sessionSig));
42803
43374
  } catch {
42804
43375
  sigOk = false;
42805
43376
  }
42806
43377
  if (!sigOk) {
42807
43378
  console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
42808
- return false;
43379
+ return null;
42809
43380
  }
42810
43381
  if (cachedPin === void 0) {
42811
43382
  try {
42812
43383
  cachedPin = await opts.extensionTrust.read();
42813
43384
  } catch (e) {
42814
43385
  console.error(`[fetchproxy] ${String(e)}`);
42815
- return false;
43386
+ return null;
42816
43387
  }
42817
43388
  }
42818
43389
  const pin = cachedPin;
42819
43390
  const outcome = decideExtensionTrust({
42820
43391
  pin,
42821
- hello: extensionHello,
43392
+ hello: hello2,
42822
43393
  allowNew: opts.extensionTrust.allowNew,
42823
43394
  serverName: opts.serverName,
42824
43395
  location: opts.extensionTrust.location
42825
43396
  });
42826
43397
  if (outcome.decision === "refused") {
42827
43398
  console.warn(outcome.message);
42828
- return false;
43399
+ return null;
42829
43400
  }
42830
43401
  if (outcome.decision === "replace")
42831
43402
  console.warn(outcome.message);
42832
43403
  if (outcome.decision !== "pinned") {
42833
43404
  try {
42834
43405
  const written = {
42835
- identityX25519Pub: extensionHello.identityX25519Pub,
42836
- identityEd25519Pub: extensionHello.identityEd25519Pub,
43406
+ identityX25519Pub: hello2.identityX25519Pub,
43407
+ identityEd25519Pub: hello2.identityEd25519Pub,
42837
43408
  pinnedAt: Date.now()
42838
43409
  };
42839
43410
  await opts.extensionTrust.write(written);
@@ -42842,32 +43413,62 @@ async function startPeer(opts) {
42842
43413
  console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
42843
43414
  }
42844
43415
  }
42845
- return true;
43416
+ return hello2;
42846
43417
  };
42847
43418
  const onMessage = async (data) => {
43419
+ let raw;
42848
43420
  try {
42849
- const raw = JSON.parse(data.toString());
43421
+ raw = JSON.parse(data.toString());
42850
43422
  const frame = validateFrame(raw);
42851
43423
  if (frame.type === "hello" && frame.role === "extension") {
42852
43424
  extensionHello = frame;
43425
+ const mintedKeypair = await generateSessionKeypair();
43426
+ const mintedHello = await buildServerHello({
43427
+ ...helloBase,
43428
+ sessionPub: mintedKeypair.publicKey,
43429
+ answersExtNonce: fromB64(frame.sessionNonce)
43430
+ });
43431
+ if (extensionHello !== frame) {
43432
+ mintedKeypair.privateKey.fill(0);
43433
+ return;
43434
+ }
43435
+ installSessionEphemeral({
43436
+ nonce: fromB64(mintedHello.sessionNonce),
43437
+ pub: mintedKeypair.publicKey,
43438
+ priv: mintedKeypair.privateKey
43439
+ });
43440
+ ws.send(JSON.stringify(mintedHello));
42853
43441
  return;
42854
43442
  }
42855
43443
  if (frame.type === "extension-disconnected") {
42856
43444
  extensionHello = null;
42857
43445
  extensionGone = true;
43446
+ dropSessionEphemeral();
42858
43447
  pendingPairCode = null;
42859
43448
  return;
42860
43449
  }
42861
43450
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
42862
- const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
42863
- if (!authorised) {
43451
+ const held = sessionEphemeral;
43452
+ const heldExtHello = extensionHello;
43453
+ if (!held || frame.mcpSessionPub !== toB64(held.pub)) {
43454
+ console.warn(`[fetchproxy] ${opts.serverName}: discarding a ready for a session ephemeral this peer no longer holds (the extension reconnected while a hello was in flight)`);
43455
+ return;
43456
+ }
43457
+ const authenticated = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub, held, heldExtHello);
43458
+ if (!authenticated) {
42864
43459
  ws.close(1008, "extension identity refused");
42865
43460
  rejectFirstReady(new Error("peer: extension identity refused"));
42866
43461
  return;
42867
43462
  }
42868
43463
  const extPub = fromB64(frame.extensionSessionPub);
42869
- const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
42870
- const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
43464
+ const extNonce = fromB64(authenticated.sessionNonce);
43465
+ const shared = await ecdhX25519(held.priv, extPub);
43466
+ const salt = await transcriptHash(held.nonce, extNonce, held.pub, extPub);
43467
+ const sessionKey = await hkdfSha256(shared, salt, enc5.encode(HKDF_SESSION_INFO), 32);
43468
+ if (sessionEphemeral !== held) {
43469
+ console.warn(`[fetchproxy] ${opts.serverName}: dropping a session derived against an ephemeral that was superseded while it was being derived`);
43470
+ return;
43471
+ }
42871
43472
  const isRenegotiation = session !== null;
42872
43473
  session = new SessionState(sessionKey);
42873
43474
  extensionGone = false;
@@ -42883,16 +43484,46 @@ async function startPeer(opts) {
42883
43484
  rejectFirstReady(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
42884
43485
  }
42885
43486
  if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
42886
- pendingPairCode = frame.pairCode;
42887
- pendingPairListeners.forEach((cb) => cb(frame.pairCode));
43487
+ const hello2 = extensionHello;
43488
+ const mint = sessionEphemeral;
43489
+ const derived2 = hello2 === null ? null : await pairCodeFor(hello2, mint);
43490
+ if (derived2 === null) {
43491
+ if (!warnedUnverifiablePairCode) {
43492
+ warnedUnverifiablePairCode = true;
43493
+ console.warn(`[fetchproxy] ${opts.serverName}: a pair code arrived that this peer cannot derive for itself (no extension identity has been relayed to it, or no session ephemeral has been minted in answer to one), so it is not being shown. If the MCP holding the bridge port is older than 1.12.0, upgrading it closes this.`);
43494
+ }
43495
+ return;
43496
+ }
43497
+ if (frame.pairCode !== derived2) {
43498
+ console.error(`[fetchproxy] ${opts.serverName}: the extension's pair code (${frame.pairCode}) does not match the one this peer derived from the pair transcript (${derived2}) \u2014 refusing to pair (possible MITM between this MCP and the extension)`);
43499
+ ws.close(1008, "pair code mismatch");
43500
+ return;
43501
+ }
43502
+ pendingPairCode = derived2;
43503
+ pendingPairListeners.forEach((cb) => cb(derived2));
42888
43504
  return;
42889
43505
  }
42890
43506
  if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
42891
- if (!session)
43507
+ const inboundSession = session;
43508
+ if (!inboundSession)
42892
43509
  return;
42893
- if (!session.acceptInboundSeq(frame.seq))
43510
+ const claim2 = inboundSession.claimInboundSeq(frame.seq);
43511
+ if (inboundSession.saturationWarningDue(claim2)) {
43512
+ console.warn(`[fetchproxy] ${opts.serverName}: dropped an inbound frame (seq ${frame.seq}) unread \u2014 too many frames from the extension are still being opened (inbound claims saturated). Not a replay. Further drops are not logged until the in-flight set drains.`);
43513
+ }
43514
+ if (claim2 !== "ok")
42894
43515
  return;
42895
- const result = await openEncryptedFrameDetailed(session.sessionKey, frame);
43516
+ let result;
43517
+ try {
43518
+ result = await openEncryptedFrameDetailed(inboundSession.sessionKey, frame, "e2s");
43519
+ } catch (e) {
43520
+ inboundSession.releaseInboundSeq(frame.seq);
43521
+ throw e;
43522
+ }
43523
+ if (result.stage !== "decrypt-failed")
43524
+ inboundSession.commitInboundSeq(frame.seq);
43525
+ else
43526
+ inboundSession.releaseInboundSeq(frame.seq);
42896
43527
  if (result.stage === "ok") {
42897
43528
  innerListeners.forEach((cb) => cb(result.inner));
42898
43529
  } else if (result.stage === "decrypt-failed") {
@@ -42909,12 +43540,25 @@ async function startPeer(opts) {
42909
43540
  }
42910
43541
  }
42911
43542
  } catch (e) {
42912
- rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
43543
+ const peek = peekHelloVersion(raw);
43544
+ const mismatch = peek && peek.protocolVersion !== PROTOCOL_VERSION ? new FetchproxyProtocolVersionError({
43545
+ ourVersion: PROTOCOL_VERSION,
43546
+ theirVersion: peek.protocolVersion,
43547
+ peer: peek.mcpId === null ? "extension" : "mcp"
43548
+ }) : null;
43549
+ if (mismatch)
43550
+ console.warn(`[fetchproxy] ${opts.serverName}: ${mismatch.message}`);
43551
+ rejectFirstReady(mismatch ?? (e instanceof Error ? e : new Error(String(e))));
42913
43552
  }
42914
43553
  };
42915
43554
  ws.on("message", onMessage);
42916
43555
  ws.once("close", () => {
42917
43556
  rejectFirstReady(new Error("peer WS closed before ready"));
43557
+ dropSessionEphemeral();
43558
+ if (bootstrapPriv) {
43559
+ bootstrapPriv.fill(0);
43560
+ bootstrapPriv = null;
43561
+ }
42918
43562
  closeListeners.forEach((cb) => cb());
42919
43563
  });
42920
43564
  sessionPromise.catch(() => {
@@ -42928,7 +43572,8 @@ async function startPeer(opts) {
42928
43572
  pendingPairCode: () => pendingPairCode
42929
43573
  });
42930
43574
  const s = session;
42931
- const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
43575
+ const plaintext = encodeOutboundInnerFrame(opts.mcpId, inner);
43576
+ const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), plaintext, "s2e");
42932
43577
  ws.send(JSON.stringify(sealed));
42933
43578
  },
42934
43579
  onInner: (cb) => {
@@ -43119,6 +43764,12 @@ function protocolErrorFrom(error62, op) {
43119
43764
  return new FetchproxyNoTabError(error62);
43120
43765
  return new FetchproxyProtocolError(error62);
43121
43766
  }
43767
+ var VERB_DEADLINE_GRACE_MS = 15e3;
43768
+ function verbDeadlineMs(transportMs, requestedMs, graceMs = VERB_DEADLINE_GRACE_MS) {
43769
+ if (requestedMs === void 0)
43770
+ return transportMs;
43771
+ return Math.max(requestedMs + graceMs, transportMs);
43772
+ }
43122
43773
  var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
43123
43774
  url;
43124
43775
  timeoutMs;
@@ -43137,16 +43788,20 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
43137
43788
  */
43138
43789
  retryAttempted;
43139
43790
  /**
43140
- * 2.4.2+ (#277): what the CALL asked for, when that is more than the
43141
- * deadline that actually fired. A per-call `timeoutMs` is forwarded to the
43142
- * extension but does not raise this server's `fetchTimeoutMs`, so the
43143
- * shorter deadline wins — and used to report only its own number, a value
43144
- * the caller never supplied, from an option the message never named.
43791
+ * 2.4.2+ (#277): what the CALL asked for, whenever it asked for anything.
43792
+ *
43793
+ * Until #237 this was recorded only when the call asked for MORE than the
43794
+ * deadline, because asking for more is what the cap silently ignored. The
43795
+ * cap is gone — the call's own `timeoutMs` now GOVERNS its deadline
43796
+ * (`verbDeadlineMs`) — so the interesting case inverted: a timeout here
43797
+ * means the bridge did not answer within the window the CALLER chose, and
43798
+ * the message says which number that was and where it came from.
43145
43799
  */
43146
43800
  requestedTimeoutMs;
43147
43801
  constructor(args) {
43148
- const capped = args.requestedTimeoutMs !== void 0 && args.requestedTimeoutMs > args.timeoutMs;
43149
- super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms` + (capped ? ` \u2014 that is this server's fetchTimeoutMs, not the ${args.requestedTimeoutMs}ms this call asked for. A per-call timeoutMs cannot exceed it; raise fetchTimeoutMs on the transport to wait longer.` : ""));
43802
+ const graceMs = args.graceMs ?? VERB_DEADLINE_GRACE_MS;
43803
+ const callerChose = args.requestedTimeoutMs !== void 0 && args.timeoutMs === args.requestedTimeoutMs + graceMs;
43804
+ super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms` + (callerChose ? ` \u2014 the ${args.requestedTimeoutMs}ms this call asked for, plus ${graceMs}ms for the reply to reach this server. The extension runs its own timer on the value you passed, so a window that is genuinely too short reports what did not happen rather than this.` : ""));
43150
43805
  this.name = "FetchproxyTimeoutError";
43151
43806
  this.url = args.url;
43152
43807
  this.timeoutMs = args.timeoutMs;
@@ -43279,6 +43934,11 @@ var FetchproxyServer = class {
43279
43934
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
43280
43935
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
43281
43936
  }
43937
+ for (const d of opts.domains) {
43938
+ if (typeof d === "string" && isPublicSuffix(d)) {
43939
+ throw new Error(`FetchproxyServer: opts.domains entry ${JSON.stringify(d)} is a public suffix \u2014 declare a registrable domain (e.g. "example.${d}"), not the suffix every site under it shares`);
43940
+ }
43941
+ }
43282
43942
  let capabilities;
43283
43943
  if (opts.capabilities === void 0) {
43284
43944
  capabilities = ["fetch"];
@@ -43344,6 +44004,7 @@ var FetchproxyServer = class {
43344
44004
  // back-door is `0` (explicit opt-out) if a caller genuinely wants
43345
44005
  // the legacy hang-forever / fail-once-on-SW-eviction behavior.
43346
44006
  fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
44007
+ verbDeadlineGraceMs: opts.verbDeadlineGraceMs ?? VERB_DEADLINE_GRACE_MS,
43347
44008
  bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
43348
44009
  // 0.10.0+ (#72): keep-alive defaults to 25s — round-3 #71 cohort
43349
44010
  // wave showed every Pattern A consumer was opting into this same
@@ -43362,6 +44023,7 @@ var FetchproxyServer = class {
43362
44023
  keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
43363
44024
  keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
43364
44025
  identityDir: opts.identityDir,
44026
+ trustDir: opts.trustDir,
43365
44027
  allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
43366
44028
  requireExtensionIdentity: opts.requireExtensionIdentity,
43367
44029
  onPairCode: opts.onPairCode
@@ -43638,7 +44300,9 @@ var FetchproxyServer = class {
43638
44300
  }
43639
44301
  /**
43640
44302
  * #208: this MCP's pin on the extension's identity, stored beside its own
43641
- * identity key and so following `identityDir` wherever the caller put it.
44303
+ * identity key and so following `identityDir` wherever the caller put it —
44304
+ * unless `trustDir` (or `FETCHPROXY_TRUST_DIR`) says otherwise, which is
44305
+ * what a host that provisions the identity read-only has to say.
43642
44306
  *
43643
44307
  * `allowNewExtensionIdentity` falls back to an environment variable when the
43644
44308
  * caller expressed no opinion, because the thirteen MCPs that construct this
@@ -43650,6 +44314,7 @@ var FetchproxyServer = class {
43650
44314
  return fileExtensionTrust({
43651
44315
  serverName: this.opts.serverName,
43652
44316
  dir: this.opts.identityDir,
44317
+ trustDir: this.opts.trustDir,
43653
44318
  allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
43654
44319
  });
43655
44320
  }
@@ -43812,9 +44477,11 @@ var FetchproxyServer = class {
43812
44477
  return pending;
43813
44478
  }
43814
44479
  async _withVerbTimeout(pending, pendingMap, id, url2, requestedTimeoutMs) {
43815
- const timeoutMs = this.opts.fetchTimeoutMs;
43816
- if (timeoutMs === void 0 || timeoutMs <= 0)
44480
+ const transportMs = this.opts.fetchTimeoutMs;
44481
+ if (transportMs === void 0 || transportMs <= 0)
43817
44482
  return pending;
44483
+ const graceMs = this.opts.verbDeadlineGraceMs;
44484
+ const timeoutMs = verbDeadlineMs(transportMs, requestedTimeoutMs, graceMs);
43818
44485
  let timer;
43819
44486
  const start = Date.now();
43820
44487
  try {
@@ -43830,6 +44497,7 @@ var FetchproxyServer = class {
43830
44497
  port: this.opts.port,
43831
44498
  elapsedMs: Date.now() - start,
43832
44499
  retryAttempted: false,
44500
+ graceMs,
43833
44501
  ...requestedTimeoutMs !== void 0 ? { requestedTimeoutMs } : {}
43834
44502
  }));
43835
44503
  }, timeoutMs);
@@ -43890,9 +44558,9 @@ var FetchproxyServer = class {
43890
44558
  if (opts.subdomain !== void 0)
43891
44559
  assertSubdomainLabel(opts.subdomain);
43892
44560
  const baseDomain = this.resolveBaseDomain(opts.domain);
43893
- const isAbsolute3 = path.startsWith("http://") || path.startsWith("https://");
44561
+ const isAbsolute4 = path.startsWith("http://") || path.startsWith("https://");
43894
44562
  let host;
43895
- if (isAbsolute3) {
44563
+ if (isAbsolute4) {
43896
44564
  try {
43897
44565
  host = new URL(path).host;
43898
44566
  } catch {
@@ -43901,7 +44569,7 @@ var FetchproxyServer = class {
43901
44569
  } else {
43902
44570
  host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
43903
44571
  }
43904
- const url2 = isAbsolute3 ? path : `https://${host}${path}`;
44572
+ const url2 = isAbsolute4 ? path : `https://${host}${path}`;
43905
44573
  assertUrlInDomains("request url", url2, this.opts.domains);
43906
44574
  let tabUrl = `https://${host}/`;
43907
44575
  if (opts.viaTab !== void 0) {
@@ -44390,8 +45058,9 @@ var FetchproxyServer = class {
44390
45058
  this.pendingCapture,
44391
45059
  id,
44392
45060
  `https://${opts.host}${opts.path ?? "/*"}`,
44393
- // #277: not to extend the deadline — so a timeout can say the deadline,
44394
- // and not the value asked for here, is what fired.
45061
+ // #237: this GOVERNS the deadline (window + reply grace, floored at the
45062
+ // transport bound). Under #277 it did not, and was passed only so the
45063
+ // timeout could name the number that actually fired.
44395
45064
  opts.timeoutMs
44396
45065
  );
44397
45066
  }
@@ -44483,8 +45152,9 @@ var FetchproxyServer = class {
44483
45152
  this.pendingRedirect,
44484
45153
  id,
44485
45154
  `https://${opts.host}${opts.path ?? "/*"}`,
44486
- // #277: not to extend the deadline — so a timeout can say the deadline,
44487
- // and not the value asked for here, is what fired.
45155
+ // #237: this GOVERNS the deadline (window + reply grace, floored at the
45156
+ // transport bound). Under #277 it did not, and was passed only so the
45157
+ // timeout could name the number that actually fired.
44488
45158
  opts.timeoutMs
44489
45159
  );
44490
45160
  }
@@ -44573,10 +45243,11 @@ var FetchproxyServer = class {
44573
45243
  this.pendingDownload,
44574
45244
  id,
44575
45245
  opts.url,
44576
- // #277, same as the two capture verbs: `download` takes a per-call
44577
- // timeoutMs too, so it hits the identical cap and needs the identical
44578
- // explanation. Missing it was the point of #279 — "both verbs" was
44579
- // counted off the two call sites in view rather than off the type.
45246
+ // `download` takes a per-call timeoutMs like the two capture verbs, so
45247
+ // it is governed by it like them (#237). It used to hit the identical
45248
+ // CAP and need the identical explanation; missing it then was the point
45249
+ // of #279 — "both verbs" was counted off the two call sites in view
45250
+ // rather than off the type. Three is still the number.
44580
45251
  opts.timeoutMs
44581
45252
  );
44582
45253
  }
@@ -44950,11 +45621,21 @@ var FetchproxyServer = class {
44950
45621
  /**
44951
45622
  * 2.5.0: the extension link as {@link BridgeSessionState}, read off
44952
45623
  * whichever handle is live. A pending pair code outranks "extension not
44953
- * seen" because the code could only have come from the extension — a
44954
- * peer behind an older host never sees the extension hello but does see
44955
- * pair-pending frames. Where the extension is KNOWN to be gone (a host's
45624
+ * seen" because a code could only have come from a `pair-pending` frame
45625
+ * the extension sent. Where the extension is KNOWN to be gone (a host's
44956
45626
  * socket closed, a peer told by a 2.5.0+ host) the handle has already
44957
45627
  * dropped the code, so the precedence never lies there (#283).
45628
+ *
45629
+ * M1 (bridge review 2026-09-10): a code is now only ever present when the
45630
+ * handle DERIVED it from both identity pubs, and both handles set and clear
45631
+ * that derivation in lockstep with the extension hello — so the precedence
45632
+ * has nothing left to decide: a pair code implies the hello, which implies
45633
+ * `extensionConnected`. A peer behind a pre-1.12.0 host is relayed no
45634
+ * extension hello, cannot derive, and therefore reports
45635
+ * `extension_disconnected` with a null code where it used to report the
45636
+ * wire's number as `pair_pending` — the honest state for a peer that has
45637
+ * never been told an extension is there. That is the trade: a number nobody
45638
+ * can check is worse than no number.
44958
45639
  */
44959
45640
  sessionSnapshot() {
44960
45641
  const handle = this.hostHandle ?? this.peerHandle;
@@ -45004,6 +45685,7 @@ var FetchproxyServer = class {
45004
45685
 
45005
45686
  // node_modules/@fetchproxy/bootstrap/dist/index.js
45006
45687
  var defaultFactory = (opts) => new FetchproxyServer(opts);
45688
+ var CAPTURE_BOOTSTRAP_DEADLINE_MS = 45e3;
45007
45689
  async function bootstrap(opts) {
45008
45690
  return runOneLift(opts);
45009
45691
  }
@@ -45062,7 +45744,25 @@ async function runOneLift(opts) {
45062
45744
  onPairCode: opts.onPairCode,
45063
45745
  // 0.8.0+ pass-through. Only forwarded when the caller set them;
45064
45746
  // unset → server defaults apply (30000 / 2000 in 0.8.0).
45065
- ...opts.fetchTimeoutMs !== void 0 ? { fetchTimeoutMs: opts.fetchTimeoutMs } : {},
45747
+ //
45748
+ // EXCEPT when captures are declared and the caller named no deadline of
45749
+ // their own, where the default is a tie this side loses. `bootstrap` asks
45750
+ // for no per-call `timeoutMs` below, so the capture runs for the
45751
+ // EXTENSION's 30 s default — against a transport deadline that is also
45752
+ // 30 s. Two timers armed for the same instant, and this one starts first,
45753
+ // because its frame has yet to travel.
45754
+ //
45755
+ // The timing was never the problem; the MESSAGE was. Winning that race
45756
+ // replaces the extension's rejection — which explains that a capture
45757
+ // resolves on the next request the PAGE makes, so an idle tab times out —
45758
+ // with a bare `did not respond within 30000ms`, naming a number the caller
45759
+ // never chose about a mechanism it never mentions. Twelve MCPs reach
45760
+ // captures only through here, so they all inherited it.
45761
+ //
45762
+ // Only when the caller set nothing: an explicit `fetchTimeoutMs` is a
45763
+ // deliberate bound and stays exactly as given, even though it re-loses the
45764
+ // race. Bounding is theirs to decide; this only fills a default.
45765
+ ...opts.fetchTimeoutMs !== void 0 ? { fetchTimeoutMs: opts.fetchTimeoutMs } : opts.declare.captureHeaders.length > 0 ? { fetchTimeoutMs: CAPTURE_BOOTSTRAP_DEADLINE_MS } : {},
45066
45766
  ...opts.bridgeReviveDelayMs !== void 0 ? { bridgeReviveDelayMs: opts.bridgeReviveDelayMs } : {}
45067
45767
  });
45068
45768
  const storageDomainOpts = {};
@@ -45388,7 +46088,7 @@ async function loginWithPassword(username, password) {
45388
46088
  // package.json
45389
46089
  var package_default = {
45390
46090
  name: "ofw-mcp",
45391
- version: "2.16.2",
46091
+ version: "2.16.3",
45392
46092
  license: "MIT",
45393
46093
  mcpName: "io.github.chrischall/ofw-mcp",
45394
46094
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
@@ -45423,7 +46123,7 @@ var package_default = {
45423
46123
  },
45424
46124
  dependencies: {
45425
46125
  "@chrischall/mcp-utils": "^0.27.1",
45426
- "@fetchproxy/bootstrap": "^2.2.0",
46126
+ "@fetchproxy/bootstrap": "^3.0.1",
45427
46127
  "@modelcontextprotocol/sdk": "^1.30.0",
45428
46128
  dotenv: "^17.4.2",
45429
46129
  zod: "^4.5.4"
@@ -50374,7 +51074,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
50374
51074
  var nodeAttachmentIO = new NodeAttachmentIO();
50375
51075
  await runMcp({
50376
51076
  name: "ofw",
50377
- version: "2.16.2",
51077
+ version: "2.16.3",
50378
51078
  // x-release-please-version
50379
51079
  deps: client,
50380
51080
  tools: [