@chrischall/eventbrite-mcp 0.3.0 → 0.3.2

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
@@ -7287,21 +7287,210 @@ var init_errors = __esm({
7287
7287
  }
7288
7288
  });
7289
7289
 
7290
+ // node_modules/@fetchproxy/protocol/dist/crypto.js
7291
+ async function exportRaw(key, format) {
7292
+ const buf = await subtle.exportKey(format, key);
7293
+ return new Uint8Array(buf);
7294
+ }
7295
+ async function generateX25519() {
7296
+ const kp = await subtle.generateKey({ name: "X25519" }, true, [
7297
+ "deriveBits"
7298
+ ]);
7299
+ const priv = await exportRaw(kp.privateKey, "pkcs8");
7300
+ const pub = await exportRaw(kp.publicKey, "raw");
7301
+ return { privateKey: priv.slice(-32), publicKey: pub };
7302
+ }
7303
+ async function generateEd25519() {
7304
+ const kp = await subtle.generateKey({ name: "Ed25519" }, true, [
7305
+ "sign",
7306
+ "verify"
7307
+ ]);
7308
+ const priv = await exportRaw(kp.privateKey, "pkcs8");
7309
+ const pub = await exportRaw(kp.publicKey, "raw");
7310
+ return { privateKey: priv.slice(-32), publicKey: pub };
7311
+ }
7312
+ async function importX25519Priv(raw) {
7313
+ const prefix = new Uint8Array([
7314
+ 48,
7315
+ 46,
7316
+ 2,
7317
+ 1,
7318
+ 0,
7319
+ 48,
7320
+ 5,
7321
+ 6,
7322
+ 3,
7323
+ 43,
7324
+ 101,
7325
+ 110,
7326
+ 4,
7327
+ 34,
7328
+ 4,
7329
+ 32
7330
+ ]);
7331
+ const pkcs8 = new Uint8Array(prefix.length + raw.length);
7332
+ pkcs8.set(prefix, 0);
7333
+ pkcs8.set(raw, prefix.length);
7334
+ return subtle.importKey("pkcs8", pkcs8, { name: "X25519" }, false, [
7335
+ "deriveBits"
7336
+ ]);
7337
+ }
7338
+ async function importX25519Pub(raw) {
7339
+ return subtle.importKey("raw", raw, { name: "X25519" }, false, []);
7340
+ }
7341
+ async function importEd25519Priv(raw) {
7342
+ const prefix = new Uint8Array([
7343
+ 48,
7344
+ 46,
7345
+ 2,
7346
+ 1,
7347
+ 0,
7348
+ 48,
7349
+ 5,
7350
+ 6,
7351
+ 3,
7352
+ 43,
7353
+ 101,
7354
+ 112,
7355
+ 4,
7356
+ 34,
7357
+ 4,
7358
+ 32
7359
+ ]);
7360
+ const pkcs8 = new Uint8Array(prefix.length + raw.length);
7361
+ pkcs8.set(prefix, 0);
7362
+ pkcs8.set(raw, prefix.length);
7363
+ return subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"]);
7364
+ }
7365
+ async function importEd25519Pub(raw) {
7366
+ return subtle.importKey("raw", raw, { name: "Ed25519" }, false, ["verify"]);
7367
+ }
7368
+ async function ecdhX25519(privRaw, pubRaw) {
7369
+ const priv = await importX25519Priv(privRaw);
7370
+ const pub = await importX25519Pub(pubRaw);
7371
+ const bits = await subtle.deriveBits({ name: "X25519", public: pub }, priv, 256);
7372
+ return new Uint8Array(bits);
7373
+ }
7374
+ async function ed25519Sign(privRaw, msg) {
7375
+ const priv = await importEd25519Priv(privRaw);
7376
+ const sig = await subtle.sign({ name: "Ed25519" }, priv, msg);
7377
+ return new Uint8Array(sig);
7378
+ }
7379
+ async function ed25519Verify(pubRaw, msg, sig) {
7380
+ const pub = await importEd25519Pub(pubRaw);
7381
+ return subtle.verify({ name: "Ed25519" }, pub, sig, msg);
7382
+ }
7383
+ async function hkdfSha256(ikm, salt, info, lenBytes) {
7384
+ const baseKey = await subtle.importKey("raw", ikm, { name: "HKDF" }, false, [
7385
+ "deriveBits"
7386
+ ]);
7387
+ const bits = await subtle.deriveBits({
7388
+ name: "HKDF",
7389
+ hash: "SHA-256",
7390
+ salt,
7391
+ info
7392
+ }, baseKey, lenBytes * 8);
7393
+ return new Uint8Array(bits);
7394
+ }
7395
+ async function aesGcmSeal(key, iv, plaintext, aad) {
7396
+ const k = await subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
7397
+ "encrypt"
7398
+ ]);
7399
+ const ct = await subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad }, k, plaintext);
7400
+ return new Uint8Array(ct);
7401
+ }
7402
+ async function aesGcmOpen(key, iv, ciphertext, aad) {
7403
+ const k = await subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
7404
+ "decrypt"
7405
+ ]);
7406
+ const pt = await subtle.decrypt({ name: "AES-GCM", iv, additionalData: aad }, k, ciphertext);
7407
+ return new Uint8Array(pt);
7408
+ }
7409
+ async function sha256(data) {
7410
+ const h = await subtle.digest("SHA-256", data);
7411
+ return new Uint8Array(h);
7412
+ }
7413
+ var subtle;
7414
+ var init_crypto = __esm({
7415
+ "node_modules/@fetchproxy/protocol/dist/crypto.js"() {
7416
+ subtle = globalThis.crypto.subtle;
7417
+ }
7418
+ });
7419
+
7420
+ // node_modules/@fetchproxy/protocol/dist/encoding.js
7421
+ function toB64(bytes) {
7422
+ let s = "";
7423
+ for (let i = 0; i < bytes.length; i++)
7424
+ s += String.fromCharCode(bytes[i]);
7425
+ return btoa(s);
7426
+ }
7427
+ function fromB64(s) {
7428
+ const bin = atob(s);
7429
+ const out = new Uint8Array(bin.length);
7430
+ for (let i = 0; i < bin.length; i++)
7431
+ out[i] = bin.charCodeAt(i);
7432
+ return out;
7433
+ }
7434
+ var init_encoding = __esm({
7435
+ "node_modules/@fetchproxy/protocol/dist/encoding.js"() {
7436
+ }
7437
+ });
7438
+
7290
7439
  // node_modules/@fetchproxy/protocol/dist/frames.js
7291
- function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
7292
- const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
7440
+ function helloSignaturePayload(mcpId, sessionNonce, sessionPub, answersExtNonce) {
7441
+ const id = enc.encode(mcpId);
7442
+ const out = new Uint8Array(id.length + sessionNonce.length + sessionPub.length + answersExtNonce.length);
7443
+ out.set(id, 0);
7444
+ out.set(sessionNonce, id.length);
7445
+ out.set(sessionPub, id.length + sessionNonce.length);
7446
+ out.set(answersExtNonce, id.length + sessionNonce.length + sessionPub.length);
7447
+ return out;
7448
+ }
7449
+ function answersNoExtSession(answersExtNonce) {
7450
+ let bytes;
7451
+ try {
7452
+ bytes = fromB64(answersExtNonce);
7453
+ } catch {
7454
+ return false;
7455
+ }
7456
+ if (bytes.length !== ANSWERS_NO_EXT_SESSION.length)
7457
+ return false;
7458
+ for (const b of bytes)
7459
+ if (b !== 0)
7460
+ return false;
7461
+ return true;
7462
+ }
7463
+ function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub, mcpSessionPub) {
7464
+ const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length + mcpSessionPub.length);
7293
7465
  out.set(mcpHelloNonce, 0);
7294
7466
  out.set(extHelloNonce, mcpHelloNonce.length);
7295
7467
  out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
7468
+ out.set(mcpSessionPub, mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
7296
7469
  return out;
7297
7470
  }
7298
- var PROTOCOL_VERSION, HKDF_SESSION_INFO, KNOWN_CAPABILITIES;
7471
+ async function transcriptHash(mcpNonce, extNonce, mcpSessionPub, extSessionPub) {
7472
+ const out = new Uint8Array(mcpNonce.length + extNonce.length + mcpSessionPub.length + extSessionPub.length);
7473
+ out.set(mcpNonce, 0);
7474
+ out.set(extNonce, mcpNonce.length);
7475
+ out.set(mcpSessionPub, mcpNonce.length + extNonce.length);
7476
+ out.set(extSessionPub, mcpNonce.length + extNonce.length + mcpSessionPub.length);
7477
+ return sha256(out);
7478
+ }
7479
+ function frameAad(mcpId, seq, direction) {
7480
+ return enc.encode(`fetchproxy/4/frame\0${mcpId}\0${seq}\0${direction}`);
7481
+ }
7482
+ var PROTOCOL_VERSION, HKDF_SESSION_INFO, enc, ANSWERS_NO_EXT_SESSION, KNOWN_CAPABILITIES;
7299
7483
  var init_frames = __esm({
7300
7484
  "node_modules/@fetchproxy/protocol/dist/frames.js"() {
7301
- PROTOCOL_VERSION = 3;
7302
- HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
7485
+ init_crypto();
7486
+ init_encoding();
7487
+ PROTOCOL_VERSION = 4;
7488
+ HKDF_SESSION_INFO = "fetchproxy/4.0.0/session";
7489
+ enc = new TextEncoder();
7490
+ ANSWERS_NO_EXT_SESSION = new Uint8Array(32);
7303
7491
  KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
7304
7492
  "fetch",
7493
+ "fetch_in_page",
7305
7494
  "read_cookies",
7306
7495
  "read_local_storage",
7307
7496
  "read_session_storage",
@@ -7380,6 +7569,143 @@ var init_json_pointer = __esm({
7380
7569
  }
7381
7570
  });
7382
7571
 
7572
+ // node_modules/@fetchproxy/protocol/dist/public-suffix.js
7573
+ function isPublicSuffix(host) {
7574
+ const normalised = host.trim().toLowerCase().replace(/\.$/, "");
7575
+ if (normalised === "")
7576
+ return false;
7577
+ const labels = normalised.split(".");
7578
+ if (labels.some((l) => l === ""))
7579
+ return false;
7580
+ if (labels.length === 1)
7581
+ return true;
7582
+ if (LISTED_PUBLIC_SUFFIXES.has(normalised))
7583
+ return true;
7584
+ if (labels.length === 2) {
7585
+ const [secondLevel, tld] = labels;
7586
+ if (secondLevel && tld && /^[a-z]{2}$/.test(tld) && CCTLD_ADMIN_LABELS.has(secondLevel)) {
7587
+ return true;
7588
+ }
7589
+ }
7590
+ return false;
7591
+ }
7592
+ var CCTLD_ADMIN_LABELS, LISTED_PUBLIC_SUFFIXES;
7593
+ var init_public_suffix = __esm({
7594
+ "node_modules/@fetchproxy/protocol/dist/public-suffix.js"() {
7595
+ CCTLD_ADMIN_LABELS = /* @__PURE__ */ new Set([
7596
+ "ac",
7597
+ "co",
7598
+ "com",
7599
+ "edu",
7600
+ "gob",
7601
+ "gov",
7602
+ "mil",
7603
+ "net",
7604
+ "nom",
7605
+ "org",
7606
+ "sch"
7607
+ ]);
7608
+ LISTED_PUBLIC_SUFFIXES = /* @__PURE__ */ new Set([
7609
+ // --- ICANN, under ccTLDs, outside CCTLD_ADMIN_LABELS ---
7610
+ "ad.jp",
7611
+ "ed.jp",
7612
+ "gr.jp",
7613
+ "lg.jp",
7614
+ "ne.jp",
7615
+ "or.jp",
7616
+ "go.kr",
7617
+ "ne.kr",
7618
+ "or.kr",
7619
+ "pe.kr",
7620
+ "re.kr",
7621
+ "go.id",
7622
+ "my.id",
7623
+ "or.id",
7624
+ "web.id",
7625
+ "go.th",
7626
+ "in.th",
7627
+ "or.th",
7628
+ "me.uk",
7629
+ "ltd.uk",
7630
+ "plc.uk",
7631
+ "nhs.uk",
7632
+ "mod.uk",
7633
+ "police.uk",
7634
+ "parliament.uk",
7635
+ "asn.au",
7636
+ "id.au",
7637
+ "gen.in",
7638
+ "ind.in",
7639
+ "firm.in",
7640
+ "eu.org",
7641
+ // --- Vendor ("private") suffixes: one entry = every customer's site ---
7642
+ "github.io",
7643
+ "githubusercontent.com",
7644
+ "gitlab.io",
7645
+ "bitbucket.io",
7646
+ "pages.dev",
7647
+ "workers.dev",
7648
+ "r2.dev",
7649
+ "trycloudflare.com",
7650
+ "vercel.app",
7651
+ "now.sh",
7652
+ "netlify.app",
7653
+ "netlify.live",
7654
+ "herokuapp.com",
7655
+ "herokussl.com",
7656
+ "appspot.com",
7657
+ "web.app",
7658
+ "firebaseapp.com",
7659
+ "cloudfunctions.net",
7660
+ "blogspot.com",
7661
+ "azurewebsites.net",
7662
+ "azurestaticapps.net",
7663
+ "azureedge.net",
7664
+ "azurecontainer.io",
7665
+ "cloudapp.azure.com",
7666
+ "trafficmanager.net",
7667
+ "s3.amazonaws.com",
7668
+ "compute.amazonaws.com",
7669
+ "compute-1.amazonaws.com",
7670
+ "elasticbeanstalk.com",
7671
+ "cloudfront.net",
7672
+ "awsapprunner.com",
7673
+ "amplifyapp.com",
7674
+ "fly.dev",
7675
+ "onrender.com",
7676
+ "railway.app",
7677
+ "koyeb.app",
7678
+ "deno.dev",
7679
+ "replit.dev",
7680
+ "repl.co",
7681
+ "glitch.me",
7682
+ "surge.sh",
7683
+ "neocities.org",
7684
+ "pythonanywhere.com",
7685
+ "readthedocs.io",
7686
+ "gitbook.io",
7687
+ "myshopify.com",
7688
+ "wixsite.com",
7689
+ "editorx.io",
7690
+ "webflow.io",
7691
+ "bubbleapps.io",
7692
+ "notion.site",
7693
+ "framer.app",
7694
+ "framer.website",
7695
+ "ngrok.io",
7696
+ "ngrok.app",
7697
+ "ngrok-free.app",
7698
+ "loca.lt",
7699
+ "duckdns.org",
7700
+ "ddns.net",
7701
+ "no-ip.org",
7702
+ "dyndns.org",
7703
+ "hopto.org",
7704
+ "zapto.org"
7705
+ ]);
7706
+ }
7707
+ });
7708
+
7383
7709
  // node_modules/@fetchproxy/protocol/dist/validate.js
7384
7710
  function assertObject(x, label) {
7385
7711
  if (typeof x !== "object" || x === null || Array.isArray(x)) {
@@ -7404,6 +7730,23 @@ function assertBase64(x, label) {
7404
7730
  if (!BASE64_RE.test(x))
7405
7731
  throw new ProtocolError(`${label}: invalid base64`);
7406
7732
  }
7733
+ function base64ByteLength(s) {
7734
+ if (s.length % 4 !== 0)
7735
+ return null;
7736
+ const pad = s.endsWith("==") ? 2 : s.endsWith("=") ? 1 : 0;
7737
+ return s.length / 4 * 3 - pad;
7738
+ }
7739
+ function assertBase64Bytes(x, label, bytes) {
7740
+ assertBase64(x, label);
7741
+ if (base64ByteLength(x) !== bytes) {
7742
+ throw new ProtocolError(`${label}: expected base64 of ${bytes} raw bytes`);
7743
+ }
7744
+ }
7745
+ function assertBoolean(x, label) {
7746
+ if (typeof x !== "boolean") {
7747
+ throw new ProtocolError(`${label}: must be boolean`);
7748
+ }
7749
+ }
7407
7750
  function assertPositiveInt(x, label) {
7408
7751
  if (typeof x !== "number" || !Number.isInteger(x) || x <= 0) {
7409
7752
  throw new ProtocolError(`${label}: expected positive integer`);
@@ -7697,6 +8040,10 @@ function validateFrame(raw) {
7697
8040
  return validateEncrypted(raw);
7698
8041
  if (t === "pair-pending")
7699
8042
  return validatePairPending(raw);
8043
+ if (t === "hello-rejected")
8044
+ return validateHelloRejected(raw);
8045
+ if (t === "extension-disconnected")
8046
+ return { type: "extension-disconnected" };
7700
8047
  throw new ProtocolError(`unknown frame type: ${String(t)}`);
7701
8048
  }
7702
8049
  function validateHello(raw) {
@@ -7723,6 +8070,9 @@ function validateHello(raw) {
7723
8070
  if (!HOSTNAME_RE.test(d)) {
7724
8071
  throw new ProtocolError(`hello.domains: invalid hostname ${JSON.stringify(d)}`);
7725
8072
  }
8073
+ if (isPublicSuffix(d)) {
8074
+ 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`);
8075
+ }
7726
8076
  }
7727
8077
  if (raw.capabilities !== void 0) {
7728
8078
  if (!Array.isArray(raw.capabilities)) {
@@ -7764,12 +8114,24 @@ function validateHello(raw) {
7764
8114
  if (raw.domSelectors !== void 0) {
7765
8115
  assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
7766
8116
  }
8117
+ if (raw.accepts !== void 0) {
8118
+ if (!Array.isArray(raw.accepts)) {
8119
+ throw new ProtocolError("hello.accepts: expected array");
8120
+ }
8121
+ for (const a of raw.accepts) {
8122
+ if (typeof a !== "string") {
8123
+ throw new ProtocolError(`hello.accepts: entry must be string, got ${typeof a}`);
8124
+ }
8125
+ }
8126
+ }
7767
8127
  if (raw.graphqlOps !== void 0) {
7768
8128
  assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
7769
8129
  }
7770
8130
  assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
7771
8131
  assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
7772
8132
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
8133
+ assertBase64Bytes(raw.sessionPub, "hello.sessionPub", 32);
8134
+ assertBase64Bytes(raw.answersExtNonce, "hello.answersExtNonce", 32);
7773
8135
  assertBase64(raw.sessionSig, "hello.sessionSig");
7774
8136
  return raw;
7775
8137
  }
@@ -7787,11 +8149,34 @@ function validateHello(raw) {
7787
8149
  }
7788
8150
  throw new ProtocolError(`hello.role: must be 'server' or 'extension', got ${String(role)}`);
7789
8151
  }
8152
+ function peekHelloVersion(raw) {
8153
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
8154
+ return null;
8155
+ const frame = raw;
8156
+ if (frame.type !== "hello")
8157
+ return null;
8158
+ const protocolVersion = frame.protocolVersion;
8159
+ if (typeof protocolVersion !== "number" || !Number.isInteger(protocolVersion))
8160
+ return null;
8161
+ const accepts = [];
8162
+ if (Array.isArray(frame.accepts)) {
8163
+ for (const a of frame.accepts) {
8164
+ if (typeof a === "string")
8165
+ accepts.push(a);
8166
+ }
8167
+ }
8168
+ return {
8169
+ protocolVersion,
8170
+ mcpId: isValidMcpId(frame.mcpId) ? frame.mcpId : null,
8171
+ accepts
8172
+ };
8173
+ }
7790
8174
  function validateReady(raw) {
7791
8175
  assertString(raw.mcpId, "ready.mcpId");
7792
8176
  if (!isValidMcpId(raw.mcpId))
7793
8177
  throw new ProtocolError("ready.mcpId: invalid format");
7794
8178
  assertBase64(raw.extensionSessionPub, "ready.extensionSessionPub");
8179
+ assertBase64Bytes(raw.mcpSessionPub, "ready.mcpSessionPub", 32);
7795
8180
  assertBase64(raw.sessionSig, "ready.sessionSig");
7796
8181
  return raw;
7797
8182
  }
@@ -7810,10 +8195,20 @@ function validatePairPending(raw) {
7810
8195
  throw new ProtocolError("pair-pending.mcpId: invalid format");
7811
8196
  assertString(raw.pairCode, "pair-pending.pairCode");
7812
8197
  if (!PAIR_CODE_RE.test(raw.pairCode)) {
7813
- throw new ProtocolError(`pair-pending.pairCode: must match XXX-XXX, got ${String(raw.pairCode)}`);
8198
+ throw new ProtocolError(`pair-pending.pairCode: must match XXXX-XXXX, got ${String(raw.pairCode)}`);
7814
8199
  }
7815
8200
  return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
7816
8201
  }
8202
+ function validateHelloRejected(raw) {
8203
+ assertString(raw.mcpId, "hello-rejected.mcpId");
8204
+ if (!isValidMcpId(raw.mcpId))
8205
+ throw new ProtocolError("hello-rejected.mcpId: invalid format");
8206
+ assertString(raw.reason, "hello-rejected.reason");
8207
+ if (raw.reason.length > 200) {
8208
+ throw new ProtocolError("hello-rejected.reason: must be at most 200 characters");
8209
+ }
8210
+ return { type: "hello-rejected", mcpId: raw.mcpId, reason: raw.reason };
8211
+ }
7817
8212
  function validateInnerFrame(raw) {
7818
8213
  assertObject(raw, "inner");
7819
8214
  const t = raw.type;
@@ -7845,6 +8240,14 @@ function validateInnerRequest(raw) {
7845
8240
  if (raw.init.body !== void 0) {
7846
8241
  assertString(raw.init.body, "inner.init.body");
7847
8242
  }
8243
+ if (raw.init.inPage !== void 0) {
8244
+ assertBoolean(raw.init.inPage, "inner.init.inPage");
8245
+ }
8246
+ if (raw.init.credentials !== void 0) {
8247
+ if (raw.init.credentials !== "include" && raw.init.credentials !== "omit") {
8248
+ throw new ProtocolError("inner.init.credentials: must be 'include' or 'omit'");
8249
+ }
8250
+ }
7848
8251
  return raw;
7849
8252
  }
7850
8253
  if (raw.op === "read_cookies") {
@@ -8241,6 +8644,7 @@ var init_validate = __esm({
8241
8644
  init_frames();
8242
8645
  init_mcp_id();
8243
8646
  init_json_pointer();
8647
+ init_public_suffix();
8244
8648
  ProtocolError = class extends Error {
8245
8649
  constructor(message) {
8246
8650
  super(message);
@@ -8254,10 +8658,10 @@ var init_validate = __esm({
8254
8658
  HEADER_NAME_RE = /^[A-Za-z0-9_\-]{1,128}$/;
8255
8659
  HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
8256
8660
  CAPTURE_PATH_RE = /^\/[A-Za-z0-9._~%\-/]*\*?$/;
8257
- DOM_SELECTOR_RE = /^[^-]{1,512}$/;
8661
+ DOM_SELECTOR_RE = /^[^\x00-\x1f\x7f]{1,512}$/;
8258
8662
  DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
8259
8663
  GRAPHQL_OP_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]{0,127}$/;
8260
- PAIR_CODE_RE = /^\d{3}-\d{3}$/;
8664
+ PAIR_CODE_RE = /^\d{4}-\d{4}$/;
8261
8665
  KNOWN_RESPONSE_OPS = /* @__PURE__ */ new Set([
8262
8666
  ...KNOWN_CAPABILITIES,
8263
8667
  "graphql_query"
@@ -8265,180 +8669,33 @@ var init_validate = __esm({
8265
8669
  }
8266
8670
  });
8267
8671
 
8268
- // node_modules/@fetchproxy/protocol/dist/crypto.js
8269
- async function exportRaw(key, format) {
8270
- const buf = await subtle.exportKey(format, key);
8271
- return new Uint8Array(buf);
8272
- }
8273
- async function generateX25519() {
8274
- const kp = await subtle.generateKey({ name: "X25519" }, true, [
8275
- "deriveBits"
8276
- ]);
8277
- const priv = await exportRaw(kp.privateKey, "pkcs8");
8278
- const pub = await exportRaw(kp.publicKey, "raw");
8279
- return { privateKey: priv.slice(-32), publicKey: pub };
8280
- }
8281
- async function generateEd25519() {
8282
- const kp = await subtle.generateKey({ name: "Ed25519" }, true, [
8283
- "sign",
8284
- "verify"
8285
- ]);
8286
- const priv = await exportRaw(kp.privateKey, "pkcs8");
8287
- const pub = await exportRaw(kp.publicKey, "raw");
8288
- return { privateKey: priv.slice(-32), publicKey: pub };
8289
- }
8290
- async function importX25519Priv(raw) {
8291
- const prefix = new Uint8Array([
8292
- 48,
8293
- 46,
8294
- 2,
8295
- 1,
8296
- 0,
8297
- 48,
8298
- 5,
8299
- 6,
8300
- 3,
8301
- 43,
8302
- 101,
8303
- 110,
8304
- 4,
8305
- 34,
8306
- 4,
8307
- 32
8308
- ]);
8309
- const pkcs8 = new Uint8Array(prefix.length + raw.length);
8310
- pkcs8.set(prefix, 0);
8311
- pkcs8.set(raw, prefix.length);
8312
- return subtle.importKey("pkcs8", pkcs8, { name: "X25519" }, false, [
8313
- "deriveBits"
8314
- ]);
8315
- }
8316
- async function importX25519Pub(raw) {
8317
- return subtle.importKey("raw", raw, { name: "X25519" }, false, []);
8318
- }
8319
- async function importEd25519Priv(raw) {
8320
- const prefix = new Uint8Array([
8321
- 48,
8322
- 46,
8323
- 2,
8324
- 1,
8325
- 0,
8326
- 48,
8327
- 5,
8328
- 6,
8329
- 3,
8330
- 43,
8331
- 101,
8332
- 112,
8333
- 4,
8334
- 34,
8335
- 4,
8336
- 32
8337
- ]);
8338
- const pkcs8 = new Uint8Array(prefix.length + raw.length);
8339
- pkcs8.set(prefix, 0);
8340
- pkcs8.set(raw, prefix.length);
8341
- return subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"]);
8342
- }
8343
- async function importEd25519Pub(raw) {
8344
- return subtle.importKey("raw", raw, { name: "Ed25519" }, false, ["verify"]);
8345
- }
8346
- async function ecdhX25519(privRaw, pubRaw) {
8347
- const priv = await importX25519Priv(privRaw);
8348
- const pub = await importX25519Pub(pubRaw);
8349
- const bits = await subtle.deriveBits({ name: "X25519", public: pub }, priv, 256);
8350
- return new Uint8Array(bits);
8351
- }
8352
- async function ed25519Sign(privRaw, msg) {
8353
- const priv = await importEd25519Priv(privRaw);
8354
- const sig = await subtle.sign({ name: "Ed25519" }, priv, msg);
8355
- return new Uint8Array(sig);
8356
- }
8357
- async function ed25519Verify(pubRaw, msg, sig) {
8358
- const pub = await importEd25519Pub(pubRaw);
8359
- return subtle.verify({ name: "Ed25519" }, pub, sig, msg);
8360
- }
8361
- async function hkdfSha256(ikm, salt, info, lenBytes) {
8362
- const baseKey = await subtle.importKey("raw", ikm, { name: "HKDF" }, false, [
8363
- "deriveBits"
8364
- ]);
8365
- const bits = await subtle.deriveBits({
8366
- name: "HKDF",
8367
- hash: "SHA-256",
8368
- salt,
8369
- info
8370
- }, baseKey, lenBytes * 8);
8371
- return new Uint8Array(bits);
8372
- }
8373
- async function aesGcmSeal(key, iv, plaintext) {
8374
- const k = await subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
8375
- "encrypt"
8376
- ]);
8377
- const ct = await subtle.encrypt({ name: "AES-GCM", iv }, k, plaintext);
8378
- return new Uint8Array(ct);
8379
- }
8380
- async function aesGcmOpen(key, iv, ciphertext) {
8381
- const k = await subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
8382
- "decrypt"
8383
- ]);
8384
- const pt = await subtle.decrypt({ name: "AES-GCM", iv }, k, ciphertext);
8385
- return new Uint8Array(pt);
8386
- }
8387
- async function sha256(data) {
8388
- const h = await subtle.digest("SHA-256", data);
8389
- return new Uint8Array(h);
8390
- }
8391
- var subtle;
8392
- var init_crypto = __esm({
8393
- "node_modules/@fetchproxy/protocol/dist/crypto.js"() {
8394
- subtle = globalThis.crypto.subtle;
8395
- }
8396
- });
8397
-
8398
- // node_modules/@fetchproxy/protocol/dist/encoding.js
8399
- function toB64(bytes) {
8400
- let s = "";
8401
- for (let i = 0; i < bytes.length; i++)
8402
- s += String.fromCharCode(bytes[i]);
8403
- return btoa(s);
8404
- }
8405
- function fromB64(s) {
8406
- const bin = atob(s);
8407
- const out = new Uint8Array(bin.length);
8408
- for (let i = 0; i < bin.length; i++)
8409
- out[i] = bin.charCodeAt(i);
8410
- return out;
8411
- }
8412
- function concatBytes(a, b) {
8413
- const out = new Uint8Array(a.length + b.length);
8414
- out.set(a, 0);
8415
- out.set(b, a.length);
8416
- return out;
8417
- }
8418
- var init_encoding = __esm({
8419
- "node_modules/@fetchproxy/protocol/dist/encoding.js"() {
8420
- }
8421
- });
8422
-
8423
8672
  // node_modules/@fetchproxy/protocol/dist/pair-code.js
8424
- async function derivePairCode(pub) {
8425
- const h = await sha256(pub);
8426
- const b0 = h[0] ?? 0;
8427
- const b1 = h[1] ?? 0;
8428
- const b2 = h[2] ?? 0;
8429
- const b3 = h[3] ?? 0;
8430
- const u32 = (b0 << 24 | b1 << 16 | b2 << 8 | b3) >>> 0;
8431
- const n = u32 % 1e6;
8432
- const s = n.toString().padStart(6, "0");
8433
- return `${s.slice(0, 3)}-${s.slice(3)}`;
8434
- }
8435
- async function derivePairCodeFromIds(mcpPub, extPub) {
8436
- return derivePairCode(concatBytes(mcpPub, extPub));
8437
- }
8673
+ async function pairTranscript(mcpIdentityPub, extIdentityPub, mcpNonce, extNonce, mcpSessionPub) {
8674
+ const label = enc2.encode(PAIR_LABEL);
8675
+ const parts = [mcpIdentityPub, extIdentityPub, mcpNonce, extNonce, mcpSessionPub];
8676
+ const out = new Uint8Array(label.length + 1 + parts.reduce((n, p) => n + p.length, 0));
8677
+ out.set(label, 0);
8678
+ out[label.length] = 0;
8679
+ let at = label.length + 1;
8680
+ for (const p of parts) {
8681
+ out.set(p, at);
8682
+ at += p.length;
8683
+ }
8684
+ return digits(await sha256(out));
8685
+ }
8686
+ function digits(hash2) {
8687
+ let n = 0n;
8688
+ for (let i = 0; i < 8; i++)
8689
+ n = n << 8n | BigInt(hash2[i] ?? 0);
8690
+ const s = (n % 100000000n).toString().padStart(8, "0");
8691
+ return `${s.slice(0, 4)}-${s.slice(4)}`;
8692
+ }
8693
+ var enc2, PAIR_LABEL;
8438
8694
  var init_pair_code = __esm({
8439
8695
  "node_modules/@fetchproxy/protocol/dist/pair-code.js"() {
8440
8696
  init_crypto();
8441
- init_encoding();
8697
+ enc2 = new TextEncoder();
8698
+ PAIR_LABEL = "fetchproxy/4/pair";
8442
8699
  }
8443
8700
  });
8444
8701
 
@@ -8448,10 +8705,24 @@ function randomIv() {
8448
8705
  globalThis.crypto.getRandomValues(iv);
8449
8706
  return iv;
8450
8707
  }
8451
- async function sealInnerFrame(sessionKey, mcpId, seq, inner) {
8708
+ function base64Length(byteLength) {
8709
+ return Math.ceil(byteLength / 3) * 4;
8710
+ }
8711
+ function encodeInnerFrame(inner) {
8712
+ return enc3.encode(JSON.stringify(inner));
8713
+ }
8714
+ function plaintextOf(inner) {
8715
+ return inner instanceof Uint8Array ? inner : encodeInnerFrame(inner);
8716
+ }
8717
+ function sealedFrameWireBytes(mcpId, seq, inner) {
8718
+ const plaintext = plaintextOf(inner).length;
8719
+ const envelope = enc3.encode(JSON.stringify({ type: "frame", mcpId, seq, iv: "", ciphertext: "" })).length;
8720
+ return envelope + IV_B64_BYTES + base64Length(plaintext + AES_GCM_TAG_BYTES);
8721
+ }
8722
+ async function sealInnerFrame(sessionKey, mcpId, seq, inner, direction) {
8452
8723
  const iv = randomIv();
8453
- const pt = enc.encode(JSON.stringify(inner));
8454
- const ct = await aesGcmSeal(sessionKey, iv, pt);
8724
+ const pt = plaintextOf(inner);
8725
+ const ct = await aesGcmSeal(sessionKey, iv, pt, frameAad(mcpId, seq, direction));
8455
8726
  return {
8456
8727
  type: "frame",
8457
8728
  mcpId,
@@ -8460,18 +8731,18 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner) {
8460
8731
  ciphertext: toB64(ct)
8461
8732
  };
8462
8733
  }
8463
- async function openEncryptedFrame(sessionKey, frame) {
8464
- const result = await openEncryptedFrameDetailed(sessionKey, frame);
8734
+ async function openEncryptedFrame(sessionKey, frame, direction) {
8735
+ const result = await openEncryptedFrameDetailed(sessionKey, frame, direction);
8465
8736
  if (result.stage === "ok")
8466
8737
  return result.inner;
8467
8738
  throw result.error instanceof Error ? result.error : new Error(String(result.error));
8468
8739
  }
8469
- async function openEncryptedFrameDetailed(sessionKey, frame) {
8740
+ async function openEncryptedFrameDetailed(sessionKey, frame, direction) {
8470
8741
  let pt;
8471
8742
  try {
8472
8743
  const iv = fromB64(frame.iv);
8473
8744
  const ct = fromB64(frame.ciphertext);
8474
- pt = await aesGcmOpen(sessionKey, iv, ct);
8745
+ pt = await aesGcmOpen(sessionKey, iv, ct, frameAad(frame.mcpId, frame.seq, direction));
8475
8746
  } catch (error61) {
8476
8747
  return { stage: "decrypt-failed", error: error61 };
8477
8748
  }
@@ -8489,14 +8760,18 @@ async function openEncryptedFrameDetailed(sessionKey, frame) {
8489
8760
  return { stage: "validation-failed", error: error61, recoveredId };
8490
8761
  }
8491
8762
  }
8492
- var enc, dec;
8763
+ var enc3, dec, AES_GCM_TAG_BYTES, IV_B64_BYTES, MAX_FRAME_BYTES;
8493
8764
  var init_seal = __esm({
8494
8765
  "node_modules/@fetchproxy/protocol/dist/seal.js"() {
8495
8766
  init_crypto();
8496
8767
  init_encoding();
8768
+ init_frames();
8497
8769
  init_validate();
8498
- enc = new TextEncoder();
8770
+ enc3 = new TextEncoder();
8499
8771
  dec = new TextDecoder();
8772
+ AES_GCM_TAG_BYTES = 16;
8773
+ IV_B64_BYTES = 16;
8774
+ MAX_FRAME_BYTES = 42 * 1024 * 1024;
8500
8775
  }
8501
8776
  });
8502
8777
 
@@ -8510,6 +8785,7 @@ var init_dist = __esm({
8510
8785
  init_pair_code();
8511
8786
  init_seal();
8512
8787
  init_encoding();
8788
+ init_public_suffix();
8513
8789
  init_json_pointer();
8514
8790
  }
8515
8791
  });
@@ -12259,7 +12535,7 @@ var init_wrapper = __esm({
12259
12535
  async function buildServerHello(opts) {
12260
12536
  const sessionNonce = new Uint8Array(32);
12261
12537
  globalThis.crypto.getRandomValues(sessionNonce);
12262
- const sig = await ed25519Sign(opts.identity.ed25519Priv, concatBytes(new TextEncoder().encode(opts.mcpId), sessionNonce));
12538
+ const sig = await ed25519Sign(opts.identity.ed25519Priv, helloSignaturePayload(opts.mcpId, sessionNonce, opts.sessionPub, opts.answersExtNonce));
12263
12539
  const hello = {
12264
12540
  type: "hello",
12265
12541
  protocolVersion: PROTOCOL_VERSION,
@@ -12272,8 +12548,12 @@ async function buildServerHello(opts) {
12272
12548
  identityX25519Pub: toB64(opts.identity.x25519Pub),
12273
12549
  identityEd25519Pub: toB64(opts.identity.ed25519Pub),
12274
12550
  sessionNonce: toB64(sessionNonce),
12551
+ sessionPub: toB64(opts.sessionPub),
12552
+ answersExtNonce: toB64(opts.answersExtNonce),
12275
12553
  sessionSig: toB64(sig)
12276
12554
  };
12555
+ if (opts.accepts && opts.accepts.length > 0)
12556
+ hello.accepts = [...opts.accepts];
12277
12557
  if (opts.cookieKeys && opts.cookieKeys.length > 0) {
12278
12558
  hello.cookieKeys = [...opts.cookieKeys];
12279
12559
  }
@@ -12331,14 +12611,31 @@ var init_build_server_hello = __esm({
12331
12611
  }
12332
12612
  });
12333
12613
 
12614
+ // node_modules/@fetchproxy/server/dist/frame-size.js
12615
+ function encodeOutboundInnerFrame(mcpId, inner) {
12616
+ const plaintext = encodeInnerFrame(inner);
12617
+ const wireBytes = sealedFrameWireBytes(mcpId, Number.MAX_SAFE_INTEGER, plaintext);
12618
+ if (wireBytes > MAX_FRAME_BYTES) {
12619
+ 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.`);
12620
+ }
12621
+ return plaintext;
12622
+ }
12623
+ var init_frame_size = __esm({
12624
+ "node_modules/@fetchproxy/server/dist/frame-size.js"() {
12625
+ init_dist();
12626
+ }
12627
+ });
12628
+
12334
12629
  // node_modules/@fetchproxy/server/dist/session.js
12335
- var SessionState;
12630
+ var MAX_INFLIGHT_INBOUND_SEQS, SessionState;
12336
12631
  var init_session = __esm({
12337
12632
  "node_modules/@fetchproxy/server/dist/session.js"() {
12633
+ MAX_INFLIGHT_INBOUND_SEQS = 1024;
12338
12634
  SessionState = class {
12339
12635
  sessionKey;
12340
12636
  outboundSeq = 0;
12341
12637
  lastInboundSeq = 0;
12638
+ inflightInbound = /* @__PURE__ */ new Set();
12342
12639
  constructor(sessionKey) {
12343
12640
  this.sessionKey = sessionKey;
12344
12641
  }
@@ -12346,17 +12643,55 @@ var init_session = __esm({
12346
12643
  this.outboundSeq += 1;
12347
12644
  return this.outboundSeq;
12348
12645
  }
12349
- acceptInboundSeq(seq) {
12646
+ /**
12647
+ * Take this seq out of circulation for the frame about to be opened, and
12648
+ * say whether it was available. SYNCHRONOUS and single-shot: call it before
12649
+ * the first `await` of the receive path, or a duplicate frame read in the
12650
+ * same pass will be judged against a counter neither frame has moved yet.
12651
+ *
12652
+ * Answering false means "not this frame's to process" — replayed, or a
12653
+ * duplicate of one still in flight. Every true MUST be answered by exactly
12654
+ * one {@link commitInboundSeq} or {@link releaseInboundSeq}.
12655
+ */
12656
+ claimInboundSeq(seq) {
12350
12657
  if (seq <= this.lastInboundSeq)
12351
12658
  return false;
12352
- this.lastInboundSeq = seq;
12659
+ if (this.inflightInbound.has(seq))
12660
+ return false;
12661
+ if (this.inflightInbound.size >= MAX_INFLIGHT_INBOUND_SEQS)
12662
+ return false;
12663
+ this.inflightInbound.add(seq);
12353
12664
  return true;
12354
12665
  }
12666
+ /**
12667
+ * Give a claim back without spending the seq — for a frame that did not
12668
+ * authenticate, which is a frame that never happened. The counter does not
12669
+ * move, so the genuine frames behind it, carrying lower numbers, are still
12670
+ * accepted. Idempotent, so a caller may release in a `catch` after a commit
12671
+ * it is not sure ran.
12672
+ */
12673
+ releaseInboundSeq(seq) {
12674
+ this.inflightInbound.delete(seq);
12675
+ }
12676
+ /**
12677
+ * Record a seq as spent. Call only for a frame that authenticated. Never
12678
+ * moves the counter backwards, so an out-of-order commit cannot reopen a
12679
+ * seq an earlier one already closed.
12680
+ */
12681
+ commitInboundSeq(seq) {
12682
+ this.inflightInbound.delete(seq);
12683
+ if (seq > this.lastInboundSeq)
12684
+ this.lastInboundSeq = seq;
12685
+ }
12355
12686
  };
12356
12687
  }
12357
12688
  });
12358
12689
 
12359
12690
  // node_modules/@fetchproxy/server/dist/session-ready.js
12691
+ function protocolVersionCloseReason(info) {
12692
+ const far = info.peer === "extension" ? "the extension" : "the other MCP";
12693
+ return `protocol version mismatch: this MCP speaks ${info.ourVersion}, ${far} speaks ${info.theirVersion}`;
12694
+ }
12360
12695
  async function awaitSessionReady(ready, opts) {
12361
12696
  const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
12362
12697
  if (ms <= 0)
@@ -12375,7 +12710,7 @@ async function awaitSessionReady(ready, opts) {
12375
12710
  clearTimeout(timer);
12376
12711
  }
12377
12712
  }
12378
- var SESSION_READY_TIMEOUT_MS, FetchproxySessionNotReadyError;
12713
+ var SESSION_READY_TIMEOUT_MS, FetchproxySessionNotReadyError, FetchproxyHelloRejectedError, MIN_VERSION, FetchproxyProtocolVersionError;
12379
12714
  var init_session_ready = __esm({
12380
12715
  "node_modules/@fetchproxy/server/dist/session-ready.js"() {
12381
12716
  SESSION_READY_TIMEOUT_MS = 3e4;
@@ -12396,15 +12731,52 @@ var init_session_ready = __esm({
12396
12731
  Object.setPrototypeOf(this, new.target.prototype);
12397
12732
  }
12398
12733
  };
12734
+ FetchproxyHelloRejectedError = class extends Error {
12735
+ mcpId;
12736
+ reason;
12737
+ constructor(info) {
12738
+ super(`fetchproxy: the extension refused the connection for "${info.mcpId}": ${info.reason}. This is the extension's own reason \u2014 it is not a timeout, and retrying unchanged will be refused the same way.`);
12739
+ this.name = "FetchproxyHelloRejectedError";
12740
+ this.mcpId = info.mcpId;
12741
+ this.reason = info.reason;
12742
+ Object.setPrototypeOf(this, new.target.prototype);
12743
+ }
12744
+ };
12745
+ MIN_VERSION = "3.0.0";
12746
+ FetchproxyProtocolVersionError = class extends Error {
12747
+ /** The protocol version this process speaks. */
12748
+ ourVersion;
12749
+ /** The protocol version the far end announced in the hello we refused. */
12750
+ theirVersion;
12751
+ peer;
12752
+ constructor(info) {
12753
+ 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`;
12754
+ super(`protocol version mismatch: this MCP speaks fetchproxy protocol ${info.ourVersion}, ${far}`);
12755
+ this.name = "FetchproxyProtocolVersionError";
12756
+ this.ourVersion = info.ourVersion;
12757
+ this.theirVersion = info.theirVersion;
12758
+ this.peer = info.peer;
12759
+ Object.setPrototypeOf(this, new.target.prototype);
12760
+ }
12761
+ };
12399
12762
  }
12400
12763
  });
12401
12764
 
12402
12765
  // node_modules/@fetchproxy/server/dist/identity.js
12403
12766
  import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
12404
- import { join as join2 } from "node:path";
12767
+ import { isAbsolute, join as join2 } from "node:path";
12405
12768
  import { homedir } from "node:os";
12769
+ function envIdentityDir() {
12770
+ const raw = process.env.FETCHPROXY_IDENTITY_DIR;
12771
+ if (raw === void 0)
12772
+ return void 0;
12773
+ const dir = raw.trim();
12774
+ if (dir === "" || !isAbsolute(dir))
12775
+ return void 0;
12776
+ return dir;
12777
+ }
12406
12778
  function defaultIdentityDir() {
12407
- return join2(homedir(), ".fetchproxy", "identity");
12779
+ return envIdentityDir() ?? join2(homedir(), ".fetchproxy", "identity");
12408
12780
  }
12409
12781
  function safeIdentityFileBase(serverName) {
12410
12782
  if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
@@ -12412,42 +12784,57 @@ function safeIdentityFileBase(serverName) {
12412
12784
  }
12413
12785
  return serverName.replace(/\//g, "_");
12414
12786
  }
12415
- async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
12416
- const safeFile = safeIdentityFileBase(serverName);
12417
- const path = join2(dir, `${safeFile}.json`);
12418
- await mkdir(dir, { recursive: true, mode: 448 });
12419
- try {
12420
- const raw = await readFile(path, "utf8");
12421
- const j2 = JSON.parse(raw);
12422
- return {
12423
- x25519Priv: fromB64(j2.x25519Priv),
12424
- x25519Pub: fromB64(j2.x25519Pub),
12425
- ed25519Priv: fromB64(j2.ed25519Priv),
12426
- ed25519Pub: fromB64(j2.ed25519Pub),
12427
- createdAt: j2.createdAt
12428
- };
12429
- } catch (e) {
12430
- if (e.code !== "ENOENT")
12431
- throw e;
12432
- }
12787
+ function identityFilePath(dir, serverName) {
12788
+ return join2(dir, `${safeIdentityFileBase(serverName)}.json`);
12789
+ }
12790
+ async function generateIdentity() {
12433
12791
  const x = await generateX25519();
12434
12792
  const ed = await generateEd25519();
12435
- const id = {
12793
+ return {
12436
12794
  x25519Priv: x.privateKey,
12437
12795
  x25519Pub: x.publicKey,
12438
12796
  ed25519Priv: ed.privateKey,
12439
12797
  ed25519Pub: ed.publicKey,
12440
12798
  createdAt: Date.now()
12441
12799
  };
12442
- const j = {
12800
+ }
12801
+ function serializeIdentity(id) {
12802
+ return JSON.stringify({
12443
12803
  x25519Priv: toB64(id.x25519Priv),
12444
12804
  x25519Pub: toB64(id.x25519Pub),
12445
12805
  ed25519Priv: toB64(id.ed25519Priv),
12446
12806
  ed25519Pub: toB64(id.ed25519Pub),
12447
12807
  createdAt: id.createdAt
12808
+ }, null, 2);
12809
+ }
12810
+ function parseIdentity(text) {
12811
+ const j = JSON.parse(text);
12812
+ return {
12813
+ x25519Priv: fromB64(j.x25519Priv),
12814
+ x25519Pub: fromB64(j.x25519Pub),
12815
+ ed25519Priv: fromB64(j.ed25519Priv),
12816
+ ed25519Pub: fromB64(j.ed25519Pub),
12817
+ createdAt: j.createdAt
12448
12818
  };
12449
- await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
12819
+ }
12820
+ async function writeIdentityFile(dir, serverName, id) {
12821
+ const path = identityFilePath(dir, serverName);
12822
+ await mkdir(dir, { recursive: true, mode: 448 });
12823
+ await writeFile(path, serializeIdentity(id), { mode: 384 });
12450
12824
  await chmod(path, 384);
12825
+ return path;
12826
+ }
12827
+ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
12828
+ const path = identityFilePath(dir, serverName);
12829
+ await mkdir(dir, { recursive: true, mode: 448 });
12830
+ try {
12831
+ return parseIdentity(await readFile(path, "utf8"));
12832
+ } catch (e) {
12833
+ if (e.code !== "ENOENT")
12834
+ throw e;
12835
+ }
12836
+ const id = await generateIdentity();
12837
+ await writeIdentityFile(dir, serverName, id);
12451
12838
  return id;
12452
12839
  }
12453
12840
  var SAFE_PLAIN, SAFE_SCOPED;
@@ -12461,13 +12848,32 @@ var init_identity = __esm({
12461
12848
 
12462
12849
  // node_modules/@fetchproxy/server/dist/extension-trust.js
12463
12850
  import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
12464
- import { join as join3 } from "node:path";
12851
+ import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
12852
+ function envTrustDir(env = process.env) {
12853
+ const raw = env[TRUST_DIR_ENV];
12854
+ if (raw === void 0)
12855
+ return void 0;
12856
+ const dir = raw.trim();
12857
+ if (dir !== "" && isAbsolute2(dir))
12858
+ return dir;
12859
+ 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.`);
12860
+ return void 0;
12861
+ }
12862
+ function resolveTrustDir(explicit, identityDir) {
12863
+ if (explicit !== void 0)
12864
+ return explicit;
12865
+ return envTrustDir() ?? identityDir ?? defaultIdentityDir();
12866
+ }
12867
+ function defaultTrustDir() {
12868
+ return resolveTrustDir();
12869
+ }
12465
12870
  function fileExtensionTrust(args) {
12871
+ const dir = resolveTrustDir(args.trustDir, args.dir);
12466
12872
  return {
12467
12873
  allowNew: args.allowNew,
12468
- location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
12469
- read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
12470
- write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
12874
+ location: extensionTrustPath(args.serverName, dir),
12875
+ read: () => readExtensionPin(args.serverName, dir),
12876
+ write: (pin) => writeExtensionPin(args.serverName, pin, dir)
12471
12877
  };
12472
12878
  }
12473
12879
  function allowNewExtensionIdentity(explicit, env = process.env) {
@@ -12494,14 +12900,14 @@ function decideExtensionTrust(args) {
12494
12900
  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.`
12495
12901
  };
12496
12902
  }
12497
- function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
12903
+ function extensionTrustPath(serverName, dir = defaultTrustDir()) {
12498
12904
  return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
12499
12905
  }
12500
12906
  function extensionTrustPathHint(serverName) {
12501
12907
  try {
12502
12908
  return extensionTrustPath(serverName);
12503
12909
  } catch {
12504
- return join3(defaultIdentityDir(), "<server-name>.extension-trust.json");
12910
+ return join3(defaultTrustDir(), "<server-name>.extension-trust.json");
12505
12911
  }
12506
12912
  }
12507
12913
  function isPin(x) {
@@ -12510,7 +12916,7 @@ function isPin(x) {
12510
12916
  const r = x;
12511
12917
  return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
12512
12918
  }
12513
- async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
12919
+ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
12514
12920
  const path = extensionTrustPath(serverName, dir);
12515
12921
  let raw;
12516
12922
  try {
@@ -12535,7 +12941,7 @@ async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
12535
12941
  pinnedAt: parsed.pinnedAt
12536
12942
  };
12537
12943
  }
12538
- async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
12944
+ async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
12539
12945
  const path = extensionTrustPath(serverName, dir);
12540
12946
  await mkdir2(dir, { recursive: true, mode: 448 });
12541
12947
  const tmp = `${path}.tmp`;
@@ -12543,28 +12949,70 @@ async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
12543
12949
  await chmod2(tmp, 384);
12544
12950
  await rename(tmp, path);
12545
12951
  }
12546
- var TRUST_NEW_EXTENSION_ENV;
12952
+ var TRUST_DIR_ENV, TRUST_NEW_EXTENSION_ENV;
12547
12953
  var init_extension_trust = __esm({
12548
12954
  "node_modules/@fetchproxy/server/dist/extension-trust.js"() {
12549
12955
  init_identity();
12956
+ TRUST_DIR_ENV = "FETCHPROXY_TRUST_DIR";
12550
12957
  TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
12551
12958
  }
12552
12959
  });
12553
12960
 
12554
12961
  // node_modules/@fetchproxy/server/dist/host.js
12962
+ function envAllowsLocalOrigins(env = process.env) {
12963
+ const raw = env[ALLOW_LOCAL_ORIGINS_ENV];
12964
+ if (raw === void 0 || raw.trim() === "")
12965
+ return false;
12966
+ if (raw.trim() === "1")
12967
+ return true;
12968
+ 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.`);
12969
+ return false;
12970
+ }
12971
+ function originVerdict(origin, allowLocal) {
12972
+ if (origin === void 0)
12973
+ return { allow: true };
12974
+ if (HTTP_ORIGIN_RE.test(origin)) {
12975
+ if (PUBLIC_ORIGIN_RE.test(origin)) {
12976
+ return { allow: false, reason: "origin not allowed", escapable: false };
12977
+ }
12978
+ return allowLocal ? { allow: true } : { allow: false, reason: "localhost page origin not allowed", escapable: true };
12979
+ }
12980
+ if (origin.toLowerCase() === OPAQUE_ORIGIN) {
12981
+ return allowLocal ? { allow: true } : { allow: false, reason: "opaque (null) origin not allowed", escapable: true };
12982
+ }
12983
+ if (EXTENSION_ORIGIN_RE.test(origin))
12984
+ return { allow: true };
12985
+ return { allow: false, reason: "origin not allowed", escapable: false };
12986
+ }
12555
12987
  async function startHost(opts) {
12988
+ const allowLocalOrigins = envAllowsLocalOrigins();
12989
+ if (allowLocalOrigins) {
12990
+ 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.`);
12991
+ }
12992
+ const warnedOrigins = /* @__PURE__ */ new Set();
12556
12993
  const wss = new import_websocket_server.default({
12557
12994
  server: opts.httpServer,
12995
+ maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES,
12558
12996
  verifyClient: (info, cb) => {
12559
12997
  const origin = info.req.headers.origin;
12560
- if (origin && PUBLIC_ORIGIN_RE.test(origin)) {
12561
- cb(false, 403, "origin not allowed");
12998
+ const verdict = originVerdict(origin, allowLocalOrigins);
12999
+ if (verdict.allow) {
13000
+ cb(true);
12562
13001
  return;
12563
13002
  }
12564
- cb(true);
13003
+ if (verdict.escapable && origin !== void 0 && warnedOrigins.size < 8) {
13004
+ if (!warnedOrigins.has(origin)) {
13005
+ warnedOrigins.add(origin);
13006
+ 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.`);
13007
+ }
13008
+ }
13009
+ cb(false, 403, verdict.reason);
12565
13010
  }
12566
13011
  });
12567
- const ownHello = await buildServerHello({
13012
+ const ownHelloBase = {
13013
+ // 2.6.0: tell the extension it can say WHY it refused a hello, instead of
13014
+ // leaving us to time out and guess.
13015
+ accepts: ["hello-rejected"],
12568
13016
  identity: opts.ownIdentity,
12569
13017
  mcpId: opts.ownMcpId,
12570
13018
  serverName: opts.ownServerName,
@@ -12580,14 +13028,26 @@ async function startHost(opts) {
12580
13028
  sessionStoragePointers: opts.ownSessionStoragePointers,
12581
13029
  domSelectors: opts.ownDomSelectors,
12582
13030
  graphqlOps: opts.ownGraphqlOps
12583
- });
12584
- const ownSessionNonce = fromB64(ownHello.sessionNonce);
13031
+ };
13032
+ const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
12585
13033
  let extensionWs = null;
12586
13034
  const peers = /* @__PURE__ */ new Map();
12587
13035
  const ownInnerListeners = [];
12588
13036
  const disconnectListeners = [];
12589
13037
  const pendingPairListeners = [];
12590
13038
  let ownSession = null;
13039
+ let ownEphemeral = null;
13040
+ function dropOwnSessionAndEphemeral() {
13041
+ if (ownEphemeral)
13042
+ ownEphemeral.priv.fill(0);
13043
+ ownEphemeral = null;
13044
+ return null;
13045
+ }
13046
+ function installOwnEphemeral(next) {
13047
+ if (ownEphemeral)
13048
+ ownEphemeral.priv.fill(0);
13049
+ ownEphemeral = next;
13050
+ }
12591
13051
  let ownPendingPairCode = null;
12592
13052
  let resolveOwnSession;
12593
13053
  let rejectOwnSession;
@@ -12601,21 +13061,74 @@ async function startHost(opts) {
12601
13061
  });
12602
13062
  }
12603
13063
  resetSessionPromise();
13064
+ let ownSessionRefusal = null;
13065
+ function refuseVersionMismatch(ws, raw, identified) {
13066
+ const peek = peekHelloVersion(raw);
13067
+ if (!peek || peek.protocolVersion === PROTOCOL_VERSION)
13068
+ return false;
13069
+ const err = new FetchproxyProtocolVersionError({
13070
+ ourVersion: PROTOCOL_VERSION,
13071
+ theirVersion: peek.protocolVersion,
13072
+ peer: peek.mcpId === null ? "extension" : "mcp"
13073
+ });
13074
+ console.warn(`[fetchproxy] host: ${err.message}`);
13075
+ const nothingBetterAttached = extensionWs === null && extensionClaim === null && ownSession === null;
13076
+ if (err.peer === "extension" && identified !== "peer" && nothingBetterAttached) {
13077
+ ownSessionRefusal = err;
13078
+ rejectOwnSession(err);
13079
+ resetSessionPromise();
13080
+ rejectOwnSession(err);
13081
+ }
13082
+ try {
13083
+ ws.close(1002, protocolVersionCloseReason(err));
13084
+ } catch {
13085
+ }
13086
+ return true;
13087
+ }
12604
13088
  let extensionHello = null;
12605
13089
  let extensionClaim = null;
13090
+ const pairCodeFor = async (hello, mint) => {
13091
+ if (!mint)
13092
+ return null;
13093
+ try {
13094
+ return await pairTranscript(opts.ownIdentity.x25519Pub, fromB64(hello.identityX25519Pub), mint.nonce, fromB64(hello.sessionNonce), mint.pub);
13095
+ } catch (e) {
13096
+ console.error("[fetchproxy] could not derive the pair code:", e);
13097
+ return null;
13098
+ }
13099
+ };
12606
13100
  wss.on("connection", (ws) => {
12607
13101
  let identified = null;
12608
13102
  let peerMcpId = null;
12609
13103
  let closed = false;
12610
13104
  let pinOnReady = false;
13105
+ const handshakeTimer = setTimeout(() => {
13106
+ if (identified || closed)
13107
+ return;
13108
+ try {
13109
+ ws.close(1008, "handshake timeout");
13110
+ } catch {
13111
+ }
13112
+ }, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
13113
+ handshakeTimer.unref?.();
13114
+ ws.on("error", (e) => {
13115
+ console.warn(`[fetchproxy] host: socket error: ${String(e)}`);
13116
+ });
12611
13117
  ws.on("message", async (data) => {
12612
13118
  try {
13119
+ let raw;
13120
+ try {
13121
+ raw = JSON.parse(data.toString());
13122
+ } catch {
13123
+ ws.close(1002, "protocol error");
13124
+ return;
13125
+ }
12613
13126
  let frame;
12614
13127
  try {
12615
- const raw = JSON.parse(data.toString());
12616
13128
  frame = validateFrame(raw);
12617
13129
  } catch {
12618
- ws.close(1002, "protocol error");
13130
+ if (!refuseVersionMismatch(ws, raw, identified))
13131
+ ws.close(1002, "protocol error");
12619
13132
  return;
12620
13133
  }
12621
13134
  if (frame.type === "hello" && frame.role === "extension") {
@@ -12659,25 +13172,43 @@ async function startHost(opts) {
12659
13172
  identified = "extension";
12660
13173
  extensionWs = ws;
12661
13174
  extensionHello = frame;
12662
- if (opts.onPairCode) {
13175
+ if (ownSessionRefusal) {
13176
+ ownSessionRefusal = null;
13177
+ resetSessionPromise();
13178
+ }
13179
+ const mintedKeypair = await generateSessionKeypair();
13180
+ const mintedHello = await buildServerHello({
13181
+ ...ownHelloBase,
13182
+ sessionPub: mintedKeypair.publicKey,
13183
+ // The echo Rule B's gate reads, inside the signed payload: this
13184
+ // hello answers the extension session whose hello triggered it.
13185
+ answersExtNonce: fromB64(frame.sessionNonce)
13186
+ });
13187
+ if (extensionWs !== ws) {
13188
+ mintedKeypair.privateKey.fill(0);
13189
+ return;
13190
+ }
13191
+ const mint = {
13192
+ nonce: fromB64(mintedHello.sessionNonce),
13193
+ pub: mintedKeypair.publicKey
13194
+ };
13195
+ installOwnEphemeral({ ...mint, priv: mintedKeypair.privateKey });
13196
+ for (const slot of peers.values())
13197
+ slot.ws.send(JSON.stringify(frame));
13198
+ ws.send(JSON.stringify(mintedHello));
13199
+ const helloPairCode = await pairCodeFor(frame, mint);
13200
+ if (opts.onPairCode && helloPairCode !== null) {
12663
13201
  try {
12664
- const code = await derivePairCodeFromIds(opts.ownIdentity.x25519Pub, fromB64(frame.identityX25519Pub));
12665
- opts.onPairCode(code);
13202
+ opts.onPairCode(helloPairCode);
12666
13203
  } catch (e) {
12667
13204
  console.error("[fetchproxy] onPairCode threw:", e);
12668
13205
  }
12669
13206
  }
12670
- for (const slot of peers.values())
12671
- slot.ws.send(JSON.stringify(frame));
12672
- ws.send(JSON.stringify(ownHello));
12673
- for (const slot of peers.values()) {
12674
- ws.send(JSON.stringify(slot.helloFrame));
12675
- }
12676
13207
  return;
12677
13208
  }
12678
13209
  if (frame.type === "hello" && frame.role === "server") {
12679
13210
  const peerEdPub = fromB64(frame.identityEd25519Pub);
12680
- const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
13211
+ const peerSigMsg = helloSignaturePayload(frame.mcpId, fromB64(frame.sessionNonce), fromB64(frame.sessionPub), fromB64(frame.answersExtNonce));
12681
13212
  const peerSig = fromB64(frame.sessionSig);
12682
13213
  let peerSigOk = false;
12683
13214
  try {
@@ -12702,10 +13233,12 @@ async function startHost(opts) {
12702
13233
  identified = "peer";
12703
13234
  peerMcpId = frame.mcpId;
12704
13235
  peers.set(frame.mcpId, { ws, helloFrame: frame });
12705
- if (extensionWs)
12706
- extensionWs.send(JSON.stringify(frame));
12707
- if (extensionHello)
13236
+ if (extensionHello && answersNoExtSession(frame.answersExtNonce)) {
12708
13237
  ws.send(JSON.stringify(extensionHello));
13238
+ }
13239
+ if (extensionWs && extensionHello && frame.answersExtNonce === extensionHello.sessionNonce) {
13240
+ extensionWs.send(JSON.stringify(frame));
13241
+ }
12709
13242
  return;
12710
13243
  }
12711
13244
  if (frame.type === "ready") {
@@ -12715,9 +13248,14 @@ async function startHost(opts) {
12715
13248
  ws.close(1002, "ready before extension hello");
12716
13249
  return;
12717
13250
  }
13251
+ const held = ownEphemeral;
13252
+ if (!held || frame.mcpSessionPub !== toB64(held.pub)) {
13253
+ console.warn("[fetchproxy] discarding a ready for a session ephemeral this host no longer holds (the extension reconnected while a hello was in flight)");
13254
+ return;
13255
+ }
12718
13256
  const extEdPub = fromB64(extensionHello.identityEd25519Pub);
12719
13257
  const extNonce = fromB64(extensionHello.sessionNonce);
12720
- const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
13258
+ const msg = readySignaturePayload(held.nonce, extNonce, fromB64(frame.extensionSessionPub), held.pub);
12721
13259
  const sig = fromB64(frame.sessionSig);
12722
13260
  let sigOk = false;
12723
13261
  try {
@@ -12743,8 +13281,9 @@ async function startHost(opts) {
12743
13281
  }
12744
13282
  }
12745
13283
  const extPub = fromB64(frame.extensionSessionPub);
12746
- const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
12747
- const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
13284
+ const shared = await ecdhX25519(held.priv, extPub);
13285
+ const salt = await transcriptHash(held.nonce, extNonce, held.pub, extPub);
13286
+ const key = await hkdfSha256(shared, salt, enc4.encode(HKDF_SESSION_INFO), 32);
12748
13287
  if (extensionWs !== ws)
12749
13288
  return;
12750
13289
  ownSession = new SessionState(key);
@@ -12760,11 +13299,19 @@ async function startHost(opts) {
12760
13299
  if (frame.type === "frame") {
12761
13300
  if (identified === "extension") {
12762
13301
  if (frame.mcpId === opts.ownMcpId) {
12763
- if (!ownSession)
13302
+ const session = ownSession;
13303
+ if (!session)
12764
13304
  return;
12765
- if (!ownSession.acceptInboundSeq(frame.seq))
13305
+ if (!session.claimInboundSeq(frame.seq))
12766
13306
  return;
12767
- const inner = await openEncryptedFrame(ownSession.sessionKey, frame);
13307
+ let inner;
13308
+ try {
13309
+ inner = await openEncryptedFrame(session.sessionKey, frame, "e2s");
13310
+ } catch (e) {
13311
+ session.releaseInboundSeq(frame.seq);
13312
+ throw e;
13313
+ }
13314
+ session.commitInboundSeq(frame.seq);
12768
13315
  ownInnerListeners.forEach((cb) => cb(inner));
12769
13316
  } else {
12770
13317
  const slot = peers.get(frame.mcpId);
@@ -12776,10 +13323,28 @@ async function startHost(opts) {
12776
13323
  extensionWs.send(JSON.stringify(frame));
12777
13324
  }
12778
13325
  }
13326
+ if (frame.type === "hello-rejected" && identified === "extension") {
13327
+ if (frame.mcpId === opts.ownMcpId) {
13328
+ rejectOwnSession(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
13329
+ } else {
13330
+ const slot = peers.get(frame.mcpId);
13331
+ if (slot?.helloFrame.accepts?.includes("hello-rejected")) {
13332
+ slot.ws.send(JSON.stringify(frame));
13333
+ }
13334
+ }
13335
+ }
12779
13336
  if (frame.type === "pair-pending" && identified === "extension") {
12780
13337
  if (frame.mcpId === opts.ownMcpId) {
12781
- ownPendingPairCode = frame.pairCode;
12782
- pendingPairListeners.forEach((cb) => cb(frame.pairCode));
13338
+ const hello = extensionHello;
13339
+ const mint = ownEphemeral;
13340
+ const derived = hello === null ? null : await pairCodeFor(hello, mint);
13341
+ if (derived === null || frame.pairCode !== derived) {
13342
+ console.error(`[fetchproxy] ${opts.ownServerName}: the extension's pair code (${frame.pairCode}) does not match the one this MCP derived from the pair transcript` + (derived === null ? " (none \u2014 this MCP could not derive its own)" : ` (${derived})`) + " \u2014 refusing to pair (possible MITM between this MCP and the extension)");
13343
+ ws.close(1008, "pair code mismatch");
13344
+ return;
13345
+ }
13346
+ ownPendingPairCode = derived;
13347
+ pendingPairListeners.forEach((cb) => cb(derived));
12783
13348
  } else {
12784
13349
  const slot = peers.get(frame.mcpId);
12785
13350
  if (slot)
@@ -12796,6 +13361,7 @@ async function startHost(opts) {
12796
13361
  });
12797
13362
  ws.on("close", () => {
12798
13363
  closed = true;
13364
+ clearTimeout(handshakeTimer);
12799
13365
  if (extensionClaim === ws)
12800
13366
  extensionClaim = null;
12801
13367
  if (identified === "extension" && extensionWs === ws) {
@@ -12804,9 +13370,19 @@ async function startHost(opts) {
12804
13370
  if (!ownSession) {
12805
13371
  rejectOwnSession(new Error("extension disconnected before ready"));
12806
13372
  }
12807
- ownSession = null;
13373
+ ownSession = dropOwnSessionAndEphemeral();
13374
+ ownPendingPairCode = null;
12808
13375
  resetSessionPromise();
12809
13376
  disconnectListeners.forEach((cb) => cb());
13377
+ const notice = JSON.stringify({ type: "extension-disconnected" });
13378
+ for (const slot of peers.values()) {
13379
+ if (slot.helloFrame.accepts?.includes("extension-disconnected")) {
13380
+ try {
13381
+ slot.ws.send(notice);
13382
+ } catch {
13383
+ }
13384
+ }
13385
+ }
12810
13386
  }
12811
13387
  if (identified === "peer" && peerMcpId) {
12812
13388
  if (peers.get(peerMcpId)?.ws === ws)
@@ -12833,7 +13409,8 @@ async function startHost(opts) {
12833
13409
  });
12834
13410
  if (!extensionWs)
12835
13411
  throw new Error("host: no extension connected");
12836
- const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
13412
+ const plaintext = encodeOutboundInnerFrame(opts.ownMcpId, inner);
13413
+ const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), plaintext, "s2e");
12837
13414
  extensionWs.send(JSON.stringify(sealed));
12838
13415
  },
12839
13416
  onOwnInner: (cb) => {
@@ -12845,20 +13422,29 @@ async function startHost(opts) {
12845
13422
  onPendingPair: (cb) => {
12846
13423
  pendingPairListeners.push(cb);
12847
13424
  },
12848
- pendingPairCode: () => ownPendingPairCode
13425
+ pendingPairCode: () => ownPendingPairCode,
13426
+ extensionConnected: () => extensionWs !== null,
13427
+ sessionLinked: () => ownSession !== null
12849
13428
  };
12850
13429
  }
12851
- var PUBLIC_ORIGIN_RE, enc2;
13430
+ var HTTP_ORIGIN_RE, PUBLIC_ORIGIN_RE, EXTENSION_ORIGIN_RE, OPAQUE_ORIGIN, ALLOW_LOCAL_ORIGINS_ENV, HANDSHAKE_TIMEOUT_MS, MAX_PAYLOAD_BYTES, enc4;
12852
13431
  var init_host = __esm({
12853
13432
  "node_modules/@fetchproxy/server/dist/host.js"() {
12854
13433
  init_wrapper();
12855
13434
  init_dist();
12856
13435
  init_build_server_hello();
13436
+ init_frame_size();
12857
13437
  init_session();
12858
13438
  init_session_ready();
12859
13439
  init_extension_trust();
12860
- PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
12861
- enc2 = new TextEncoder();
13440
+ HTTP_ORIGIN_RE = /^https?:\/\//i;
13441
+ PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost|\[::1\])(:|$))/i;
13442
+ EXTENSION_ORIGIN_RE = /^(chrome-extension|moz-extension|safari-web-extension):\/\//i;
13443
+ OPAQUE_ORIGIN = "null";
13444
+ ALLOW_LOCAL_ORIGINS_ENV = "FETCHPROXY_ALLOW_LOCAL_ORIGINS";
13445
+ HANDSHAKE_TIMEOUT_MS = 15e3;
13446
+ MAX_PAYLOAD_BYTES = MAX_FRAME_BYTES;
13447
+ enc4 = new TextEncoder();
12862
13448
  }
12863
13449
  });
12864
13450
 
@@ -12866,10 +13452,26 @@ var init_host = __esm({
12866
13452
  async function startPeer(opts) {
12867
13453
  const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`);
12868
13454
  await new Promise((resolve, reject) => {
12869
- ws.once("open", () => resolve());
12870
- ws.once("error", reject);
13455
+ const onOpen = () => {
13456
+ cleanup();
13457
+ resolve();
13458
+ };
13459
+ const onError = (e) => {
13460
+ cleanup();
13461
+ reject(e);
13462
+ };
13463
+ const cleanup = () => {
13464
+ ws.off("open", onOpen);
13465
+ ws.off("error", onError);
13466
+ };
13467
+ ws.once("open", onOpen);
13468
+ ws.once("error", onError);
12871
13469
  });
12872
- const hello = await buildServerHello({
13470
+ ws.on("error", (e) => {
13471
+ console.warn(`[fetchproxy] peer: socket error: ${String(e)}`);
13472
+ });
13473
+ const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
13474
+ const helloBase = {
12873
13475
  identity: opts.identity,
12874
13476
  mcpId: opts.mcpId,
12875
13477
  serverName: opts.serverName,
@@ -12884,16 +13486,44 @@ async function startPeer(opts) {
12884
13486
  domSelectors: opts.domSelectors,
12885
13487
  localStoragePointers: opts.localStoragePointers,
12886
13488
  sessionStoragePointers: opts.sessionStoragePointers,
12887
- graphqlOps: opts.graphqlOps
13489
+ graphqlOps: opts.graphqlOps,
13490
+ // 2.5.0: let a host ≥2.5.0 tell us when the extension leaves, so
13491
+ // `extensionConnected()` / `sessionLinked()` can go back to false.
13492
+ // 2.6.0: `hello-rejected` for the same reason, from the extension.
13493
+ accepts: ["extension-disconnected", "hello-rejected"]
13494
+ };
13495
+ const bootstrapKeypair = await generateSessionKeypair();
13496
+ let bootstrapPriv = bootstrapKeypair.privateKey;
13497
+ const hello = await buildServerHello({
13498
+ ...helloBase,
13499
+ sessionPub: bootstrapKeypair.publicKey,
13500
+ // At dial this peer has been told of no extension session, and saying so
13501
+ // is what keeps the host from forwarding a hello whose ephemeral is not
13502
+ // one a session may be opened from.
13503
+ answersExtNonce: ANSWERS_NO_EXT_SESSION
12888
13504
  });
12889
- const sessionNonce = fromB64(hello.sessionNonce);
12890
13505
  ws.send(JSON.stringify(hello));
12891
13506
  const innerListeners = [];
12892
13507
  const renegotiateListeners = [];
12893
13508
  const pendingPairListeners = [];
12894
13509
  const closeListeners = [];
12895
13510
  let session = null;
13511
+ let sessionEphemeral = null;
13512
+ function dropSessionEphemeral() {
13513
+ if (sessionEphemeral)
13514
+ sessionEphemeral.priv.fill(0);
13515
+ sessionEphemeral = null;
13516
+ }
13517
+ function installSessionEphemeral(next) {
13518
+ dropSessionEphemeral();
13519
+ if (bootstrapPriv) {
13520
+ bootstrapPriv.fill(0);
13521
+ bootstrapPriv = null;
13522
+ }
13523
+ sessionEphemeral = next;
13524
+ }
12896
13525
  let pendingPairCode = null;
13526
+ let warnedUnverifiablePairCode = false;
12897
13527
  let resolveFirstReady;
12898
13528
  let rejectFirstReady;
12899
13529
  const sessionPromise = new Promise((resolve, reject) => {
@@ -12901,58 +13531,61 @@ async function startPeer(opts) {
12901
13531
  rejectFirstReady = reject;
12902
13532
  });
12903
13533
  let extensionHello = null;
12904
- let warnedUnverifiable = false;
13534
+ let extensionGone = false;
12905
13535
  let cachedPin = void 0;
12906
- const authenticateExtension = async (sessionSig, extensionSessionPub) => {
12907
- if (!extensionHello) {
12908
- if (opts.requireExtensionIdentity) {
12909
- 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.`);
12910
- return false;
12911
- }
12912
- if (!warnedUnverifiable) {
12913
- warnedUnverifiable = true;
12914
- 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.`);
12915
- }
12916
- return true;
13536
+ const pairCodeFor = async (hello2, mint) => {
13537
+ if (!mint)
13538
+ return null;
13539
+ try {
13540
+ return await pairTranscript(opts.identity.x25519Pub, fromB64(hello2.identityX25519Pub), mint.nonce, fromB64(hello2.sessionNonce), mint.pub);
13541
+ } catch (e) {
13542
+ console.error("[fetchproxy] could not derive the pair code:", e);
13543
+ return null;
12917
13544
  }
12918
- const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
13545
+ };
13546
+ const authenticateExtension = async (sessionSig, extensionSessionPub, held, hello2) => {
13547
+ if (!hello2) {
13548
+ 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.`);
13549
+ return null;
13550
+ }
13551
+ const payload = readySignaturePayload(held.nonce, fromB64(hello2.sessionNonce), fromB64(extensionSessionPub), held.pub);
12919
13552
  let sigOk = false;
12920
13553
  try {
12921
- sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
13554
+ sigOk = await ed25519Verify(fromB64(hello2.identityEd25519Pub), payload, fromB64(sessionSig));
12922
13555
  } catch {
12923
13556
  sigOk = false;
12924
13557
  }
12925
13558
  if (!sigOk) {
12926
13559
  console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
12927
- return false;
13560
+ return null;
12928
13561
  }
12929
13562
  if (cachedPin === void 0) {
12930
13563
  try {
12931
13564
  cachedPin = await opts.extensionTrust.read();
12932
13565
  } catch (e) {
12933
13566
  console.error(`[fetchproxy] ${String(e)}`);
12934
- return false;
13567
+ return null;
12935
13568
  }
12936
13569
  }
12937
13570
  const pin = cachedPin;
12938
13571
  const outcome = decideExtensionTrust({
12939
13572
  pin,
12940
- hello: extensionHello,
13573
+ hello: hello2,
12941
13574
  allowNew: opts.extensionTrust.allowNew,
12942
13575
  serverName: opts.serverName,
12943
13576
  location: opts.extensionTrust.location
12944
13577
  });
12945
13578
  if (outcome.decision === "refused") {
12946
13579
  console.warn(outcome.message);
12947
- return false;
13580
+ return null;
12948
13581
  }
12949
13582
  if (outcome.decision === "replace")
12950
13583
  console.warn(outcome.message);
12951
13584
  if (outcome.decision !== "pinned") {
12952
13585
  try {
12953
13586
  const written = {
12954
- identityX25519Pub: extensionHello.identityX25519Pub,
12955
- identityEd25519Pub: extensionHello.identityEd25519Pub,
13587
+ identityX25519Pub: hello2.identityX25519Pub,
13588
+ identityEd25519Pub: hello2.identityEd25519Pub,
12956
13589
  pinnedAt: Date.now()
12957
13590
  };
12958
13591
  await opts.extensionTrust.write(written);
@@ -12961,28 +13594,65 @@ async function startPeer(opts) {
12961
13594
  console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
12962
13595
  }
12963
13596
  }
12964
- return true;
13597
+ return hello2;
12965
13598
  };
12966
13599
  const onMessage = async (data) => {
13600
+ let raw;
12967
13601
  try {
12968
- const raw = JSON.parse(data.toString());
13602
+ raw = JSON.parse(data.toString());
12969
13603
  const frame = validateFrame(raw);
12970
13604
  if (frame.type === "hello" && frame.role === "extension") {
12971
13605
  extensionHello = frame;
13606
+ const mintedKeypair = await generateSessionKeypair();
13607
+ const mintedHello = await buildServerHello({
13608
+ ...helloBase,
13609
+ sessionPub: mintedKeypair.publicKey,
13610
+ answersExtNonce: fromB64(frame.sessionNonce)
13611
+ });
13612
+ if (extensionHello !== frame) {
13613
+ mintedKeypair.privateKey.fill(0);
13614
+ return;
13615
+ }
13616
+ installSessionEphemeral({
13617
+ nonce: fromB64(mintedHello.sessionNonce),
13618
+ pub: mintedKeypair.publicKey,
13619
+ priv: mintedKeypair.privateKey
13620
+ });
13621
+ ws.send(JSON.stringify(mintedHello));
13622
+ return;
13623
+ }
13624
+ if (frame.type === "extension-disconnected") {
13625
+ extensionHello = null;
13626
+ extensionGone = true;
13627
+ dropSessionEphemeral();
13628
+ pendingPairCode = null;
12972
13629
  return;
12973
13630
  }
12974
13631
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
12975
- const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
12976
- if (!authorised) {
13632
+ const held = sessionEphemeral;
13633
+ const heldExtHello = extensionHello;
13634
+ if (!held || frame.mcpSessionPub !== toB64(held.pub)) {
13635
+ 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)`);
13636
+ return;
13637
+ }
13638
+ const authenticated = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub, held, heldExtHello);
13639
+ if (!authenticated) {
12977
13640
  ws.close(1008, "extension identity refused");
12978
13641
  rejectFirstReady(new Error("peer: extension identity refused"));
12979
13642
  return;
12980
13643
  }
12981
13644
  const extPub = fromB64(frame.extensionSessionPub);
12982
- const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
12983
- const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
13645
+ const extNonce = fromB64(authenticated.sessionNonce);
13646
+ const shared = await ecdhX25519(held.priv, extPub);
13647
+ const salt = await transcriptHash(held.nonce, extNonce, held.pub, extPub);
13648
+ const sessionKey = await hkdfSha256(shared, salt, enc5.encode(HKDF_SESSION_INFO), 32);
13649
+ if (sessionEphemeral !== held) {
13650
+ console.warn(`[fetchproxy] ${opts.serverName}: dropping a session derived against an ephemeral that was superseded while it was being derived`);
13651
+ return;
13652
+ }
12984
13653
  const isRenegotiation = session !== null;
12985
13654
  session = new SessionState(sessionKey);
13655
+ extensionGone = false;
12986
13656
  pendingPairCode = null;
12987
13657
  if (isRenegotiation) {
12988
13658
  renegotiateListeners.forEach((cb) => cb());
@@ -12991,17 +13661,46 @@ async function startPeer(opts) {
12991
13661
  }
12992
13662
  return;
12993
13663
  }
13664
+ if (frame.type === "hello-rejected" && frame.mcpId === opts.mcpId) {
13665
+ rejectFirstReady(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
13666
+ }
12994
13667
  if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
12995
- pendingPairCode = frame.pairCode;
12996
- pendingPairListeners.forEach((cb) => cb(frame.pairCode));
13668
+ const hello2 = extensionHello;
13669
+ const mint = sessionEphemeral;
13670
+ const derived = hello2 === null ? null : await pairCodeFor(hello2, mint);
13671
+ if (derived === null) {
13672
+ if (!warnedUnverifiablePairCode) {
13673
+ warnedUnverifiablePairCode = true;
13674
+ 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.`);
13675
+ }
13676
+ return;
13677
+ }
13678
+ if (frame.pairCode !== derived) {
13679
+ console.error(`[fetchproxy] ${opts.serverName}: the extension's pair code (${frame.pairCode}) does not match the one this peer derived from the pair transcript (${derived}) \u2014 refusing to pair (possible MITM between this MCP and the extension)`);
13680
+ ws.close(1008, "pair code mismatch");
13681
+ return;
13682
+ }
13683
+ pendingPairCode = derived;
13684
+ pendingPairListeners.forEach((cb) => cb(derived));
12997
13685
  return;
12998
13686
  }
12999
13687
  if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
13000
- if (!session)
13688
+ const inboundSession = session;
13689
+ if (!inboundSession)
13001
13690
  return;
13002
- if (!session.acceptInboundSeq(frame.seq))
13691
+ if (!inboundSession.claimInboundSeq(frame.seq))
13003
13692
  return;
13004
- const result = await openEncryptedFrameDetailed(session.sessionKey, frame);
13693
+ let result;
13694
+ try {
13695
+ result = await openEncryptedFrameDetailed(inboundSession.sessionKey, frame, "e2s");
13696
+ } catch (e) {
13697
+ inboundSession.releaseInboundSeq(frame.seq);
13698
+ throw e;
13699
+ }
13700
+ if (result.stage !== "decrypt-failed")
13701
+ inboundSession.commitInboundSeq(frame.seq);
13702
+ else
13703
+ inboundSession.releaseInboundSeq(frame.seq);
13005
13704
  if (result.stage === "ok") {
13006
13705
  innerListeners.forEach((cb) => cb(result.inner));
13007
13706
  } else if (result.stage === "decrypt-failed") {
@@ -13018,12 +13717,25 @@ async function startPeer(opts) {
13018
13717
  }
13019
13718
  }
13020
13719
  } catch (e) {
13021
- rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
13720
+ const peek = peekHelloVersion(raw);
13721
+ const mismatch = peek && peek.protocolVersion !== PROTOCOL_VERSION ? new FetchproxyProtocolVersionError({
13722
+ ourVersion: PROTOCOL_VERSION,
13723
+ theirVersion: peek.protocolVersion,
13724
+ peer: peek.mcpId === null ? "extension" : "mcp"
13725
+ }) : null;
13726
+ if (mismatch)
13727
+ console.warn(`[fetchproxy] ${opts.serverName}: ${mismatch.message}`);
13728
+ rejectFirstReady(mismatch ?? (e instanceof Error ? e : new Error(String(e))));
13022
13729
  }
13023
13730
  };
13024
13731
  ws.on("message", onMessage);
13025
13732
  ws.once("close", () => {
13026
13733
  rejectFirstReady(new Error("peer WS closed before ready"));
13734
+ dropSessionEphemeral();
13735
+ if (bootstrapPriv) {
13736
+ bootstrapPriv.fill(0);
13737
+ bootstrapPriv = null;
13738
+ }
13027
13739
  closeListeners.forEach((cb) => cb());
13028
13740
  });
13029
13741
  sessionPromise.catch(() => {
@@ -13037,7 +13749,8 @@ async function startPeer(opts) {
13037
13749
  pendingPairCode: () => pendingPairCode
13038
13750
  });
13039
13751
  const s = session;
13040
- const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
13752
+ const plaintext = encodeOutboundInnerFrame(opts.mcpId, inner);
13753
+ const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), plaintext, "s2e");
13041
13754
  ws.send(JSON.stringify(sealed));
13042
13755
  },
13043
13756
  onInner: (cb) => {
@@ -13050,6 +13763,8 @@ async function startPeer(opts) {
13050
13763
  pendingPairListeners.push(cb);
13051
13764
  },
13052
13765
  pendingPairCode: () => pendingPairCode,
13766
+ extensionConnected: () => extensionHello !== null,
13767
+ sessionLinked: () => session !== null && !extensionGone,
13053
13768
  onClose: (cb) => {
13054
13769
  closeListeners.push(cb);
13055
13770
  },
@@ -13057,16 +13772,17 @@ async function startPeer(opts) {
13057
13772
  };
13058
13773
  return handle;
13059
13774
  }
13060
- var enc3;
13775
+ var enc5;
13061
13776
  var init_peer = __esm({
13062
13777
  "node_modules/@fetchproxy/server/dist/peer.js"() {
13063
13778
  init_wrapper();
13064
13779
  init_dist();
13065
13780
  init_build_server_hello();
13781
+ init_frame_size();
13066
13782
  init_session();
13067
13783
  init_session_ready();
13068
13784
  init_extension_trust();
13069
- enc3 = new TextEncoder();
13785
+ enc5 = new TextEncoder();
13070
13786
  }
13071
13787
  });
13072
13788
 
@@ -13078,7 +13794,7 @@ function classifyFetchError(error61) {
13078
13794
  if (/^tab fetch failed:/.test(error61)) {
13079
13795
  return "tab_fetch_failed";
13080
13796
  }
13081
- if (/^fetch threw:/.test(error61)) {
13797
+ if (/^(in-page )?fetch threw:/.test(error61)) {
13082
13798
  return "tab_fetch_failed";
13083
13799
  }
13084
13800
  if (/^no tab matching /.test(error61)) {
@@ -13102,8 +13818,14 @@ var init_error_kind = __esm({
13102
13818
 
13103
13819
  // node_modules/@fetchproxy/server/dist/classify-bridge-error.js
13104
13820
  function classifyBridgeError(err) {
13821
+ if (err instanceof FetchproxyHelloRejectedError)
13822
+ return "hello_rejected";
13823
+ if (err instanceof FetchproxySessionNotReadyError)
13824
+ return "session_not_ready";
13105
13825
  if (err instanceof FetchproxyTimeoutError)
13106
13826
  return "timeout";
13827
+ if (err instanceof FetchproxyWaitedError)
13828
+ return "timeout";
13107
13829
  if (err instanceof FetchproxyBridgeDownError)
13108
13830
  return "bridge_down";
13109
13831
  if (err instanceof FetchproxyHttpError)
@@ -13115,6 +13837,7 @@ function classifyBridgeError(err) {
13115
13837
  var init_classify_bridge_error = __esm({
13116
13838
  "node_modules/@fetchproxy/server/dist/classify-bridge-error.js"() {
13117
13839
  init_ws_server();
13840
+ init_session_ready();
13118
13841
  }
13119
13842
  });
13120
13843
 
@@ -13140,13 +13863,32 @@ function envWsHost() {
13140
13863
  return void 0;
13141
13864
  return host;
13142
13865
  }
13143
- function protocolErrorFrom(error61) {
13866
+ function waitedHint(op) {
13867
+ const tail = " This is not a version problem and does not need an update.";
13868
+ if (op === "capture" || op === "capture_redirect") {
13869
+ return "nothing matched before the window closed. Is a tab open on the declared domain and signed in? The wait resolves on the NEXT matching request the PAGE makes, so an idle tab times out however long you wait \u2014 interact with the page, or raise the window." + tail;
13870
+ }
13871
+ if (op === "download") {
13872
+ return "the download did not finish before the window closed. Is a tab open on the declared domain and signed in, and is the file still transferring?" + tail;
13873
+ }
13874
+ return "the extension waited and nothing arrived. Is a tab open on the declared domain and signed in?" + tail;
13875
+ }
13876
+ function protocolErrorFrom(error61, op) {
13877
+ if (error61 === "timeout")
13878
+ return new FetchproxyWaitedError(error61, op);
13144
13879
  if (SCOPE_REJECTION.test(error61))
13145
13880
  return new FetchproxyScopeError(error61);
13881
+ if (TAB_OPENING_REJECTION.test(error61))
13882
+ return new FetchproxyTabOpeningError(error61);
13146
13883
  if (NO_TAB_REJECTION.test(error61))
13147
13884
  return new FetchproxyNoTabError(error61);
13148
13885
  return new FetchproxyProtocolError(error61);
13149
13886
  }
13887
+ function verbDeadlineMs(transportMs, requestedMs, graceMs = VERB_DEADLINE_GRACE_MS) {
13888
+ if (requestedMs === void 0)
13889
+ return transportMs;
13890
+ return Math.max(requestedMs + graceMs, transportMs);
13891
+ }
13150
13892
  function normalizeCookiePath(path) {
13151
13893
  if (path === void 0 || path === "")
13152
13894
  return void 0;
@@ -13182,7 +13924,7 @@ function assertUrlInDomains(field, url2, domains) {
13182
13924
  const declared = domains.map((d) => JSON.stringify(d)).join(", ");
13183
13925
  throw new Error(`FetchproxyServer: ${field} host "${host}" is outside declared domains [${declared}] \u2014 must be one of them or a subdomain`);
13184
13926
  }
13185
- var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyHintedError, FetchproxyScopeError, FetchproxyNoTabError, SCOPE_REJECTION, NO_TAB_REJECTION, FetchproxyTimeoutError, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, FetchproxyServer;
13927
+ var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyHintedError, FetchproxyScopeError, FetchproxyNoTabError, FetchproxyTabOpeningError, FetchproxyWaitedError, SCOPE_REJECTION, NO_TAB_REJECTION, TAB_OPENING_REJECTION, VERB_DEADLINE_GRACE_MS, FetchproxyTimeoutError, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, FetchproxyServer;
13186
13928
  var init_ws_server = __esm({
13187
13929
  "node_modules/@fetchproxy/server/dist/ws-server.js"() {
13188
13930
  init_dist();
@@ -13258,8 +14000,22 @@ var init_ws_server = __esm({
13258
14000
  this.name = "FetchproxyNoTabError";
13259
14001
  }
13260
14002
  };
14003
+ FetchproxyTabOpeningError = class extends FetchproxyHintedError {
14004
+ constructor(originalError) {
14005
+ super(originalError, "the extension is opening that tab now \u2014 retry in a few seconds. You do not need to open one yourself, and this is not a version problem.");
14006
+ this.name = "FetchproxyTabOpeningError";
14007
+ }
14008
+ };
14009
+ FetchproxyWaitedError = class extends FetchproxyHintedError {
14010
+ constructor(originalError, op) {
14011
+ super(originalError, waitedHint(op));
14012
+ this.name = "FetchproxyWaitedError";
14013
+ }
14014
+ };
13261
14015
  SCOPE_REJECTION = /not in declared/;
13262
- NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
14016
+ NO_TAB_REJECTION = /no tab matching (?!.*(content script loaded|one is still opening))/;
14017
+ TAB_OPENING_REJECTION = /no tab matching .*one is still opening/;
14018
+ VERB_DEADLINE_GRACE_MS = 15e3;
13263
14019
  FetchproxyTimeoutError = class extends FetchproxyProtocolError {
13264
14020
  url;
13265
14021
  timeoutMs;
@@ -13277,8 +14033,21 @@ var init_ws_server = __esm({
13277
14033
  * the first attempt.
13278
14034
  */
13279
14035
  retryAttempted;
14036
+ /**
14037
+ * 2.4.2+ (#277): what the CALL asked for, whenever it asked for anything.
14038
+ *
14039
+ * Until #237 this was recorded only when the call asked for MORE than the
14040
+ * deadline, because asking for more is what the cap silently ignored. The
14041
+ * cap is gone — the call's own `timeoutMs` now GOVERNS its deadline
14042
+ * (`verbDeadlineMs`) — so the interesting case inverted: a timeout here
14043
+ * means the bridge did not answer within the window the CALLER chose, and
14044
+ * the message says which number that was and where it came from.
14045
+ */
14046
+ requestedTimeoutMs;
13280
14047
  constructor(args) {
13281
- super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
14048
+ const graceMs = args.graceMs ?? VERB_DEADLINE_GRACE_MS;
14049
+ const callerChose = args.requestedTimeoutMs !== void 0 && args.timeoutMs === args.requestedTimeoutMs + graceMs;
14050
+ 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.` : ""));
13282
14051
  this.name = "FetchproxyTimeoutError";
13283
14052
  this.url = args.url;
13284
14053
  this.timeoutMs = args.timeoutMs;
@@ -13286,6 +14055,7 @@ var init_ws_server = __esm({
13286
14055
  this.port = args.port ?? 0;
13287
14056
  this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
13288
14057
  this.retryAttempted = args.retryAttempted ?? false;
14058
+ this.requestedTimeoutMs = args.requestedTimeoutMs ?? null;
13289
14059
  }
13290
14060
  };
13291
14061
  SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
@@ -13375,6 +14145,11 @@ var init_ws_server = __esm({
13375
14145
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
13376
14146
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
13377
14147
  }
14148
+ for (const d of opts.domains) {
14149
+ if (typeof d === "string" && isPublicSuffix(d)) {
14150
+ 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`);
14151
+ }
14152
+ }
13378
14153
  let capabilities;
13379
14154
  if (opts.capabilities === void 0) {
13380
14155
  capabilities = ["fetch"];
@@ -13440,6 +14215,7 @@ var init_ws_server = __esm({
13440
14215
  // back-door is `0` (explicit opt-out) if a caller genuinely wants
13441
14216
  // the legacy hang-forever / fail-once-on-SW-eviction behavior.
13442
14217
  fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
14218
+ verbDeadlineGraceMs: opts.verbDeadlineGraceMs ?? VERB_DEADLINE_GRACE_MS,
13443
14219
  bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
13444
14220
  // 0.10.0+ (#72): keep-alive defaults to 25s — round-3 #71 cohort
13445
14221
  // wave showed every Pattern A consumer was opting into this same
@@ -13458,6 +14234,7 @@ var init_ws_server = __esm({
13458
14234
  keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
13459
14235
  keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
13460
14236
  identityDir: opts.identityDir,
14237
+ trustDir: opts.trustDir,
13461
14238
  allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
13462
14239
  requireExtensionIdentity: opts.requireExtensionIdentity,
13463
14240
  onPairCode: opts.onPairCode
@@ -13694,6 +14471,7 @@ var init_ws_server = __esm({
13694
14471
  lastFailureReason: this.lastFailureReason,
13695
14472
  consecutiveFailures: this.consecutiveFailures,
13696
14473
  lastExtensionMessageAt: this.lastExtensionMessageAt,
14474
+ session: this.sessionSnapshot(),
13697
14475
  keepAlive: {
13698
14476
  enabled: intervalMs > 0,
13699
14477
  intervalMs,
@@ -13733,7 +14511,9 @@ var init_ws_server = __esm({
13733
14511
  }
13734
14512
  /**
13735
14513
  * #208: this MCP's pin on the extension's identity, stored beside its own
13736
- * identity key and so following `identityDir` wherever the caller put it.
14514
+ * identity key and so following `identityDir` wherever the caller put it
14515
+ * unless `trustDir` (or `FETCHPROXY_TRUST_DIR`) says otherwise, which is
14516
+ * what a host that provisions the identity read-only has to say.
13737
14517
  *
13738
14518
  * `allowNewExtensionIdentity` falls back to an environment variable when the
13739
14519
  * caller expressed no opinion, because the thirteen MCPs that construct this
@@ -13745,6 +14525,7 @@ var init_ws_server = __esm({
13745
14525
  return fileExtensionTrust({
13746
14526
  serverName: this.opts.serverName,
13747
14527
  dir: this.opts.identityDir,
14528
+ trustDir: this.opts.trustDir,
13748
14529
  allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
13749
14530
  });
13750
14531
  }
@@ -13829,9 +14610,9 @@ var init_ws_server = __esm({
13829
14610
  async _fetchOnceWithTimeout(init) {
13830
14611
  const id = this.nextRequestId++;
13831
14612
  const inner = { type: "request", id, op: "fetch", init };
13832
- const pending = new Promise((resolve) => {
14613
+ const pending = this.guardPending(new Promise((resolve) => {
13833
14614
  this.pending.set(id, resolve);
13834
- });
14615
+ }));
13835
14616
  await this.sendInnerFrame(inner);
13836
14617
  const timeoutMs = this.opts.fetchTimeoutMs;
13837
14618
  if (timeoutMs === void 0 || timeoutMs <= 0)
@@ -13878,10 +14659,40 @@ var init_ws_server = __esm({
13878
14659
  * already surface for fetch timeouts). `0`/unset opts out (unbounded),
13879
14660
  * matching the fetch path.
13880
14661
  */
13881
- async _withVerbTimeout(pending, pendingMap, id, url2) {
13882
- const timeoutMs = this.opts.fetchTimeoutMs;
13883
- if (timeoutMs === void 0 || timeoutMs <= 0)
14662
+ /**
14663
+ * Register a verb's pending promise and keep an early rejection from being
14664
+ * fatal.
14665
+ *
14666
+ * Every verb stores its resolver, then AWAITS `sendInnerFrame` before the
14667
+ * caller attaches a handler — and that send does real work (sealing, a
14668
+ * socket write), so it yields across event-loop turns. Anything that drains
14669
+ * the pending maps inside that window rejects a promise nothing is
14670
+ * listening to yet: a refused pairing (`rejectAllPending(pairingErrorMessage
14671
+ * (code))`), an extension disconnect, a `close()`. Under Node's default
14672
+ * `--unhandled-rejections=throw` that ends the PROCESS, so an MCP that
14673
+ * should have reported "approve pair code NNN-NN" dies instead, and each
14674
+ * respawn mints a fresh code nobody can approve (#329).
14675
+ *
14676
+ * The no-op catch is a listener, not a swallow. `.catch()` derives a NEW
14677
+ * promise and leaves this one's rejection intact, so `_withVerbTimeout` and
14678
+ * the caller still see it — the only thing that changes is that the
14679
+ * rejection is never unhandled.
14680
+ *
14681
+ * This is the same hazard `_withVerbTimeout`'s timer already guards against
14682
+ * from the other side, where it drops the resolver "so a late bridge
14683
+ * response doesn't become an unhandled promise that crashes the host".
14684
+ */
14685
+ guardPending(pending) {
14686
+ pending.catch(() => {
14687
+ });
14688
+ return pending;
14689
+ }
14690
+ async _withVerbTimeout(pending, pendingMap, id, url2, requestedTimeoutMs) {
14691
+ const transportMs = this.opts.fetchTimeoutMs;
14692
+ if (transportMs === void 0 || transportMs <= 0)
13884
14693
  return pending;
14694
+ const graceMs = this.opts.verbDeadlineGraceMs;
14695
+ const timeoutMs = verbDeadlineMs(transportMs, requestedTimeoutMs, graceMs);
13885
14696
  let timer;
13886
14697
  const start = Date.now();
13887
14698
  try {
@@ -13896,7 +14707,9 @@ var init_ws_server = __esm({
13896
14707
  role: this.role,
13897
14708
  port: this.opts.port,
13898
14709
  elapsedMs: Date.now() - start,
13899
- retryAttempted: false
14710
+ retryAttempted: false,
14711
+ graceMs,
14712
+ ...requestedTimeoutMs !== void 0 ? { requestedTimeoutMs } : {}
13900
14713
  }));
13901
14714
  }, timeoutMs);
13902
14715
  })
@@ -13956,9 +14769,9 @@ var init_ws_server = __esm({
13956
14769
  if (opts.subdomain !== void 0)
13957
14770
  assertSubdomainLabel(opts.subdomain);
13958
14771
  const baseDomain = this.resolveBaseDomain(opts.domain);
13959
- const isAbsolute = path.startsWith("http://") || path.startsWith("https://");
14772
+ const isAbsolute3 = path.startsWith("http://") || path.startsWith("https://");
13960
14773
  let host;
13961
- if (isAbsolute) {
14774
+ if (isAbsolute3) {
13962
14775
  try {
13963
14776
  host = new URL(path).host;
13964
14777
  } catch {
@@ -13967,7 +14780,7 @@ var init_ws_server = __esm({
13967
14780
  } else {
13968
14781
  host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
13969
14782
  }
13970
- const url2 = isAbsolute ? path : `https://${host}${path}`;
14783
+ const url2 = isAbsolute3 ? path : `https://${host}${path}`;
13971
14784
  assertUrlInDomains("request url", url2, this.opts.domains);
13972
14785
  let tabUrl = `https://${host}/`;
13973
14786
  if (opts.viaTab !== void 0) {
@@ -13984,7 +14797,15 @@ var init_ws_server = __esm({
13984
14797
  method,
13985
14798
  tabUrl,
13986
14799
  headers: opts.headers,
13987
- body: opts.body
14800
+ body: opts.body,
14801
+ // Spread rather than `inPage: opts.inPage`: the wire's validator treats
14802
+ // the key's PRESENCE as the request, and a literal `undefined` would
14803
+ // both serialize away inconsistently and read as "asked for" here.
14804
+ ...opts.inPage === true ? { inPage: true } : {},
14805
+ // Spread for the reason `inPage` is: only a deliberate 'omit' goes on
14806
+ // the wire, so an untouched request serialises byte-identically to
14807
+ // every version before this option existed.
14808
+ ...opts.credentials === "omit" ? { credentials: "omit" } : {}
13988
14809
  };
13989
14810
  const result = await this.fetch(init);
13990
14811
  if (!result.ok) {
@@ -14158,7 +14979,11 @@ var init_ws_server = __esm({
14158
14979
  last_success_at: health.lastSuccessAt,
14159
14980
  last_failure_at: health.lastFailureAt,
14160
14981
  last_failure_reason: health.lastFailureReason,
14161
- consecutive_failures: health.consecutiveFailures
14982
+ consecutive_failures: health.consecutiveFailures,
14983
+ session_state: health.session.state,
14984
+ pending_pair_code: health.session.pairCode,
14985
+ extension_connected: health.session.extensionConnected,
14986
+ last_extension_message_at: health.lastExtensionMessageAt
14162
14987
  },
14163
14988
  ...error61 ? { error: error61 } : {}
14164
14989
  };
@@ -14208,9 +15033,9 @@ var init_ws_server = __esm({
14208
15033
  const tabUrl = `https://${host}/`;
14209
15034
  inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
14210
15035
  }
14211
- const pending = new Promise((resolve) => {
15036
+ const pending = this.guardPending(new Promise((resolve) => {
14212
15037
  this.pendingReadCookies.set(id, resolve);
14213
- });
15038
+ }));
14214
15039
  await this.sendInnerFrame(inner);
14215
15040
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
14216
15041
  if (!result.ok) {
@@ -14267,9 +15092,9 @@ var init_ws_server = __esm({
14267
15092
  ...cookiePath !== void 0 ? { path: cookiePath } : {}
14268
15093
  }
14269
15094
  };
14270
- const pending = new Promise((resolve, reject) => {
15095
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14271
15096
  this.pendingWriteCookies.set(id, { resolve, reject });
14272
- });
15097
+ }));
14273
15098
  await this.sendInnerFrame(inner);
14274
15099
  return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
14275
15100
  }
@@ -14330,9 +15155,9 @@ var init_ws_server = __esm({
14330
15155
  ...opts.pointers ? { pointers: { ...opts.pointers } } : {}
14331
15156
  }
14332
15157
  };
14333
- const pending = new Promise((resolve, reject) => {
15158
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14334
15159
  this.pendingStorage.set(id, { resolve, reject });
14335
- });
15160
+ }));
14336
15161
  await this.sendInnerFrame(inner);
14337
15162
  return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
14338
15163
  }
@@ -14435,11 +15260,20 @@ var init_ws_server = __esm({
14435
15260
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
14436
15261
  }
14437
15262
  };
14438
- const pending = new Promise((resolve, reject) => {
15263
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14439
15264
  this.pendingCapture.set(id, { resolve, reject });
14440
- });
15265
+ }));
14441
15266
  await this.sendInnerFrame(inner);
14442
- return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
15267
+ return this._withVerbTimeout(
15268
+ pending,
15269
+ this.pendingCapture,
15270
+ id,
15271
+ `https://${opts.host}${opts.path ?? "/*"}`,
15272
+ // #237: this GOVERNS the deadline (window + reply grace, floored at the
15273
+ // transport bound). Under #277 it did not, and was passed only so the
15274
+ // timeout could name the number that actually fired.
15275
+ opts.timeoutMs
15276
+ );
14443
15277
  }
14444
15278
  /**
14445
15279
  * Snapshot the redirect target URL of the next request the browser
@@ -14520,11 +15354,20 @@ var init_ws_server = __esm({
14520
15354
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
14521
15355
  }
14522
15356
  };
14523
- const pending = new Promise((resolve, reject) => {
15357
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14524
15358
  this.pendingRedirect.set(id, { resolve, reject });
14525
- });
15359
+ }));
14526
15360
  await this.sendInnerFrame(inner);
14527
- return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
15361
+ return this._withVerbTimeout(
15362
+ pending,
15363
+ this.pendingRedirect,
15364
+ id,
15365
+ `https://${opts.host}${opts.path ?? "/*"}`,
15366
+ // #237: this GOVERNS the deadline (window + reply grace, floored at the
15367
+ // transport bound). Under #277 it did not, and was passed only so the
15368
+ // timeout could name the number that actually fired.
15369
+ opts.timeoutMs
15370
+ );
14528
15371
  }
14529
15372
  /**
14530
15373
  * Download `url` through the BROWSER's own network stack via
@@ -14602,11 +15445,22 @@ var init_ws_server = __esm({
14602
15445
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
14603
15446
  }
14604
15447
  };
14605
- const pending = new Promise((resolve, reject) => {
15448
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14606
15449
  this.pendingDownload.set(id, { resolve, reject });
14607
- });
15450
+ }));
14608
15451
  await this.sendInnerFrame(inner);
14609
- return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
15452
+ return this._withVerbTimeout(
15453
+ pending,
15454
+ this.pendingDownload,
15455
+ id,
15456
+ opts.url,
15457
+ // `download` takes a per-call timeoutMs like the two capture verbs, so
15458
+ // it is governed by it like them (#237). It used to hit the identical
15459
+ // CAP and need the identical explanation; missing it then was the point
15460
+ // of #279 — "both verbs" was counted off the two call sites in view
15461
+ // rather than off the type. Three is still the number.
15462
+ opts.timeoutMs
15463
+ );
14610
15464
  }
14611
15465
  /**
14612
15466
  * 0.4.0+: read declared IndexedDB keys from the user's signed-in
@@ -14650,9 +15504,9 @@ var init_ws_server = __esm({
14650
15504
  keys: [...opts.keys]
14651
15505
  }
14652
15506
  };
14653
- const pending = new Promise((resolve, reject) => {
15507
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14654
15508
  this.pendingIdb.set(id, { resolve, reject });
14655
- });
15509
+ }));
14656
15510
  await this.sendInnerFrame(inner);
14657
15511
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
14658
15512
  }
@@ -14690,9 +15544,9 @@ var init_ws_server = __esm({
14690
15544
  op: "read_dom",
14691
15545
  init: { origin, names: [...opts.names] }
14692
15546
  };
14693
- const pending = new Promise((resolve, reject) => {
15547
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14694
15548
  this.pendingStorage.set(id, { resolve, reject });
14695
- });
15549
+ }));
14696
15550
  await this.sendInnerFrame(inner);
14697
15551
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
14698
15552
  }
@@ -14736,9 +15590,9 @@ var init_ws_server = __esm({
14736
15590
  ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
14737
15591
  }
14738
15592
  };
14739
- const pending = new Promise((resolve, reject) => {
15593
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14740
15594
  this.pendingGraphql.set(id, { resolve, reject });
14741
- });
15595
+ }));
14742
15596
  await this.sendInnerFrame(inner);
14743
15597
  return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
14744
15598
  }
@@ -14833,7 +15687,7 @@ var init_ws_server = __esm({
14833
15687
  captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
14834
15688
  }
14835
15689
  } else {
14836
- captureCb.reject(protocolErrorFrom(inner.error));
15690
+ captureCb.reject(protocolErrorFrom(inner.error, "capture"));
14837
15691
  }
14838
15692
  return;
14839
15693
  }
@@ -14847,7 +15701,7 @@ var init_ws_server = __esm({
14847
15701
  redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
14848
15702
  }
14849
15703
  } else {
14850
- redirectCb.reject(protocolErrorFrom(inner.error));
15704
+ redirectCb.reject(protocolErrorFrom(inner.error, "capture_redirect"));
14851
15705
  }
14852
15706
  return;
14853
15707
  }
@@ -14875,7 +15729,7 @@ var init_ws_server = __esm({
14875
15729
  downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
14876
15730
  }
14877
15731
  } else {
14878
- downloadCb.reject(protocolErrorFrom(inner.error));
15732
+ downloadCb.reject(protocolErrorFrom(inner.error, "download"));
14879
15733
  }
14880
15734
  return;
14881
15735
  }
@@ -14975,6 +15829,35 @@ var init_ws_server = __esm({
14975
15829
  return this.peerHandle.pendingPairCode();
14976
15830
  return null;
14977
15831
  }
15832
+ /**
15833
+ * 2.5.0: the extension link as {@link BridgeSessionState}, read off
15834
+ * whichever handle is live. A pending pair code outranks "extension not
15835
+ * seen" because a code could only have come from a `pair-pending` frame
15836
+ * the extension sent. Where the extension is KNOWN to be gone (a host's
15837
+ * socket closed, a peer told by a 2.5.0+ host) the handle has already
15838
+ * dropped the code, so the precedence never lies there (#283).
15839
+ *
15840
+ * M1 (bridge review 2026-09-10): a code is now only ever present when the
15841
+ * handle DERIVED it from both identity pubs, and both handles set and clear
15842
+ * that derivation in lockstep with the extension hello — so the precedence
15843
+ * has nothing left to decide: a pair code implies the hello, which implies
15844
+ * `extensionConnected`. A peer behind a pre-1.12.0 host is relayed no
15845
+ * extension hello, cannot derive, and therefore reports
15846
+ * `extension_disconnected` with a null code where it used to report the
15847
+ * wire's number as `pair_pending` — the honest state for a peer that has
15848
+ * never been told an extension is there. That is the trade: a number nobody
15849
+ * can check is worse than no number.
15850
+ */
15851
+ sessionSnapshot() {
15852
+ const handle = this.hostHandle ?? this.peerHandle;
15853
+ const pairCode = this.currentPendingPairCode();
15854
+ if (!handle || this.role === null) {
15855
+ return { state: "not_listening", pairCode, extensionConnected: false };
15856
+ }
15857
+ const extensionConnected = handle.extensionConnected();
15858
+ const state = handle.sessionLinked() ? "linked" : pairCode !== null ? "pair_pending" : !extensionConnected ? "extension_disconnected" : "no_session";
15859
+ return { state, pairCode, extensionConnected };
15860
+ }
14978
15861
  /**
14979
15862
  * 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
14980
15863
  * message if the bridge is waiting on user approval. Used by the verb
@@ -15346,6 +16229,10 @@ function credentialHint(arm, prefix, hostLabel, source) {
15346
16229
  return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
15347
16230
  case "credential_rejected":
15348
16231
  return `${hostLabel} rejected the credential from '${source}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
16232
+ case "session_expired":
16233
+ return `The credential from '${source}' is configured, but no session is live \u2014 ${hostLabel} served a sign-in page rather than the data. Sign in again; a cookie-session portal expires these on its own, so this recurs between uses.`;
16234
+ case "verification_pending":
16235
+ return `${hostLabel} is holding the sign-in on a second factor rather than refusing it. Supply the verification code the ACCOUNT HOLDER received \u2014 the credential itself is not the problem, so changing it will not help.`;
15349
16236
  case "timeout":
15350
16237
  return `The credential from '${source}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
15351
16238
  case "http":
@@ -15356,12 +16243,102 @@ function credentialHint(arm, prefix, hostLabel, source) {
15356
16243
  return `Unexpected failure \u2014 see error.message.`;
15357
16244
  }
15358
16245
  }
16246
+ function credentialHealthcheckDescription(hostLabel) {
16247
+ return `Resolves the credential the way real tools do, then makes one authenticated request to ${hostLabel}. Reports which source supplied the credential, whether ${hostLabel} accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a ${hostLabel}-side problem'. Read-only; never returns the credential itself.`;
16248
+ }
16249
+ async function runCredentialHealthcheck(args) {
16250
+ const { prefix, hostLabel, probePath, resolveCredential, probeFn, classifyThrown, hints } = args;
16251
+ const probeUrl = probePath ? `https://${hostLabel}${probePath.startsWith("/") ? "" : "/"}${probePath}` : void 0;
16252
+ let probeStarted = 0;
16253
+ let state;
16254
+ try {
16255
+ state = await resolveCredential();
16256
+ } catch (e) {
16257
+ const classified = classifyThrown?.(e);
16258
+ const result2 = {
16259
+ ok: false,
16260
+ // Still false, and still no source: a classification explains WHY
16261
+ // nothing resolved, it does not invent a credential that did.
16262
+ credential: { source: null, resolved: false },
16263
+ // No `url`: nothing was probed, and naming one implies it was tried.
16264
+ probe: { elapsed_ms: 0 },
16265
+ error: {
16266
+ kind: classified?.kind ?? "no_credential",
16267
+ message: truncateErrorMessage(messageOf(e)),
16268
+ ...classified?.detail !== void 0 ? { detail: classified.detail } : {}
16269
+ },
16270
+ // The hint must follow the KIND beside it. Falling back to
16271
+ // `no_credential`'s copy under a classified kind would state a cause
16272
+ // the kind contradicts — the same disagreement this path exists to
16273
+ // remove. So: an inline hint wins; else the classified arm's own
16274
+ // copy (consumer override first); else, for a kind this module has
16275
+ // no copy for, the neutral `unknown` text rather than one that
16276
+ // asserts a cause; else the unclassified `no_credential` default.
16277
+ hint: classified?.hint ?? (isArm(classified?.kind) ? hints?.[classified.kind] ?? credentialHint(classified.kind, prefix, hostLabel, null) : classified !== void 0 ? credentialHint("unknown", prefix, hostLabel, null) : hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null))
16278
+ };
16279
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
16280
+ }
16281
+ const credential = {
16282
+ source: state.source,
16283
+ resolved: state.source !== null,
16284
+ ...state.detail !== void 0 ? { detail: state.detail } : {}
16285
+ };
16286
+ if (!credential.resolved) {
16287
+ const result2 = {
16288
+ ok: false,
16289
+ credential,
16290
+ probe: { elapsed_ms: 0 },
16291
+ error: { kind: "no_credential", message: "no credential source resolved" },
16292
+ hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
16293
+ };
16294
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
16295
+ }
16296
+ let arm = "ok";
16297
+ let error61;
16298
+ let status;
16299
+ let customHint;
16300
+ probeStarted = Date.now();
16301
+ try {
16302
+ await probeFn();
16303
+ } catch (e) {
16304
+ status = statusOf(e);
16305
+ const aborted2 = e instanceof Error && e.name === "AbortError";
16306
+ arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
16307
+ let kind = arm;
16308
+ let detail;
16309
+ const custom2 = classifyThrown?.(e);
16310
+ if (custom2) {
16311
+ kind = custom2.kind;
16312
+ customHint = custom2.hint;
16313
+ detail = custom2.detail;
16314
+ arm = isArm(kind) ? kind : "unknown";
16315
+ }
16316
+ error61 = {
16317
+ kind,
16318
+ // Redacted AND bounded before it reaches the result: an upstream
16319
+ // failure routinely quotes what it was sent, and a healthcheck is
16320
+ // the tool people paste into a chat when something is broken.
16321
+ message: truncateErrorMessage(messageOf(e)),
16322
+ ...detail !== void 0 ? { detail } : {}
16323
+ };
16324
+ }
16325
+ const result = {
16326
+ ok: error61 === void 0,
16327
+ credential,
16328
+ probe: {
16329
+ ...probeUrl ? { url: probeUrl } : {},
16330
+ elapsed_ms: Date.now() - probeStarted,
16331
+ ...status !== void 0 ? { status } : {}
16332
+ },
16333
+ ...error61 ? { error: error61 } : {},
16334
+ hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
16335
+ };
16336
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
16337
+ }
15359
16338
  function registerCredentialHealthcheckTool(args) {
15360
- const { server, prefix, hostLabel, probePath, resolveCredential, probeFn, classifyThrown, hints } = args;
15361
- const probeUrl = probePath ? `https://${hostLabel}${probePath}` : void 0;
15362
- server.registerTool(`${prefix}_healthcheck`, {
16339
+ args.server.registerTool(`${args.prefix}_healthcheck`, {
15363
16340
  title: "Verify credentials and upstream reachability",
15364
- description: `Resolves the credential the way real tools do, then makes one authenticated request to ${hostLabel}. Reports which source supplied the credential, whether ${hostLabel} accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a ${hostLabel}-side problem'. Call this when a real tool fails and you want to know which hop broke. Read-only; never returns the credential itself.`,
16341
+ description: `${credentialHealthcheckDescription(args.hostLabel)} Call this when a real tool fails and you want to know which hop broke.`,
15365
16342
  annotations: {
15366
16343
  title: "Verify credentials and upstream reachability",
15367
16344
  readOnlyHint: true,
@@ -15369,92 +16346,7 @@ function registerCredentialHealthcheckTool(args) {
15369
16346
  openWorldHint: true
15370
16347
  },
15371
16348
  inputSchema: {}
15372
- }, async () => {
15373
- let probeStarted = 0;
15374
- let state;
15375
- try {
15376
- state = await resolveCredential();
15377
- } catch (e) {
15378
- const classified = classifyThrown?.(e);
15379
- const result2 = {
15380
- ok: false,
15381
- // Still false, and still no source: a classification explains WHY
15382
- // nothing resolved, it does not invent a credential that did.
15383
- credential: { source: null, resolved: false },
15384
- // No `url`: nothing was probed, and naming one implies it was tried.
15385
- probe: { elapsed_ms: 0 },
15386
- error: {
15387
- kind: classified?.kind ?? "no_credential",
15388
- message: truncateErrorMessage(messageOf(e)),
15389
- ...classified?.detail !== void 0 ? { detail: classified.detail } : {}
15390
- },
15391
- // The hint must follow the KIND beside it. Falling back to
15392
- // `no_credential`'s copy under a classified kind would state a cause
15393
- // the kind contradicts — the same disagreement this path exists to
15394
- // remove. So: an inline hint wins; else the classified arm's own
15395
- // copy (consumer override first); else, for a kind this module has
15396
- // no copy for, the neutral `unknown` text rather than one that
15397
- // asserts a cause; else the unclassified `no_credential` default.
15398
- hint: classified?.hint ?? (isArm(classified?.kind) ? hints?.[classified.kind] ?? credentialHint(classified.kind, prefix, hostLabel, null) : classified !== void 0 ? credentialHint("unknown", prefix, hostLabel, null) : hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null))
15399
- };
15400
- return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
15401
- }
15402
- const credential = {
15403
- source: state.source,
15404
- resolved: state.source !== null,
15405
- ...state.detail !== void 0 ? { detail: state.detail } : {}
15406
- };
15407
- if (!credential.resolved) {
15408
- const result2 = {
15409
- ok: false,
15410
- credential,
15411
- probe: { elapsed_ms: 0 },
15412
- error: { kind: "no_credential", message: "no credential source resolved" },
15413
- hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
15414
- };
15415
- return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
15416
- }
15417
- let arm = "ok";
15418
- let error61;
15419
- let status;
15420
- let customHint;
15421
- probeStarted = Date.now();
15422
- try {
15423
- await probeFn();
15424
- } catch (e) {
15425
- status = statusOf(e);
15426
- const aborted2 = e instanceof Error && e.name === "AbortError";
15427
- arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
15428
- let kind = arm;
15429
- let detail;
15430
- const custom2 = classifyThrown?.(e);
15431
- if (custom2) {
15432
- kind = custom2.kind;
15433
- customHint = custom2.hint;
15434
- detail = custom2.detail;
15435
- }
15436
- error61 = {
15437
- kind,
15438
- // Redacted AND bounded before it reaches the result: an upstream
15439
- // failure routinely quotes what it was sent, and a healthcheck is
15440
- // the tool people paste into a chat when something is broken.
15441
- message: truncateErrorMessage(messageOf(e)),
15442
- ...detail !== void 0 ? { detail } : {}
15443
- };
15444
- }
15445
- const result = {
15446
- ok: error61 === void 0,
15447
- credential,
15448
- probe: {
15449
- ...probeUrl ? { url: probeUrl } : {},
15450
- elapsed_ms: Date.now() - probeStarted,
15451
- ...status !== void 0 ? { status } : {}
15452
- },
15453
- ...error61 ? { error: error61 } : {},
15454
- hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
15455
- };
15456
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
15457
- });
16349
+ }, async () => runCredentialHealthcheck(args));
15458
16350
  }
15459
16351
  var CREDENTIAL_ARMS;
15460
16352
  var init_healthcheck = __esm({
@@ -15464,6 +16356,8 @@ var init_healthcheck = __esm({
15464
16356
  "ok",
15465
16357
  "no_credential",
15466
16358
  "credential_rejected",
16359
+ "session_expired",
16360
+ "verification_pending",
15467
16361
  "timeout",
15468
16362
  "http",
15469
16363
  "transport",
@@ -15485,6 +16379,7 @@ __export(fetchproxy_exports, {
15485
16379
  TokenBucket: () => TokenBucket,
15486
16380
  backoffDelayMs: () => backoffDelayMs,
15487
16381
  bridgeErrorInfo: () => bridgeErrorInfo,
16382
+ bridgeHealthcheckDescription: () => bridgeHealthcheckDescription,
15488
16383
  chunk: () => chunk,
15489
16384
  classifyBotWall: () => classifyBotWall,
15490
16385
  classifyBridgeError: () => classifyBridgeError,
@@ -15497,9 +16392,11 @@ __export(fetchproxy_exports, {
15497
16392
  extractImgTags: () => extractImgTags,
15498
16393
  lastPathSegment: () => lastPathSegment,
15499
16394
  mapWithConcurrency: () => mapWithConcurrency,
16395
+ registerAdaptiveHealthcheckTool: () => registerAdaptiveHealthcheckTool,
15500
16396
  registerBridgeHealthcheckTool: () => registerBridgeHealthcheckTool,
15501
16397
  registerCredentialHealthcheckTool: () => registerCredentialHealthcheckTool,
15502
16398
  retryOnceOnTimeout: () => retryOnceOnTimeout,
16399
+ runBridgeHealthcheck: () => runBridgeHealthcheck,
15503
16400
  sleep: () => sleep,
15504
16401
  withDeadline: () => withDeadline
15505
16402
  });
@@ -15535,9 +16432,9 @@ function createFetchproxyTransport(opts) {
15535
16432
  async start() {
15536
16433
  await server.listen();
15537
16434
  if (logListening) {
15538
- console.error(`[${serverOpts.serverName}:bridge] listening on 127.0.0.1:${server.bridgeHealth().port} (role=${server.role ?? "unknown"}, version=${serverOpts.version})`);
16435
+ console.error(`[${serverOpts.serverName}:bridge] ready \u2014 127.0.0.1:${server.bridgeHealth().port} binds on the first request (version=${serverOpts.version})`);
15539
16436
  } else if (debug) {
15540
- console.error(`[${serverOpts.serverName}:bridge] listening (role=${server.role ?? "unknown"}, version=${serverOpts.version})`);
16437
+ console.error(`[${serverOpts.serverName}:bridge] ready \u2014 binds on the first request (version=${serverOpts.version})`);
15541
16438
  }
15542
16439
  },
15543
16440
  async close() {
@@ -15716,13 +16613,122 @@ function projectBridgeStatus(health) {
15716
16613
  } : {}
15717
16614
  };
15718
16615
  }
15719
- function registerBridgeHealthcheckTool(args) {
15720
- const { server, prefix, probePath, hostLabel, probeFn, classifyThrown, hints, path } = args;
15721
- const probeUrl = `https://${hostLabel}${probePath}`;
16616
+ function probeDisplayUrl(hostLabel, probePath) {
16617
+ return `https://${hostLabel}${probePath.startsWith("/") ? "" : "/"}${probePath}`;
16618
+ }
16619
+ function bridgeHealthcheckDescription(hostLabel, probePath) {
16620
+ return `Round-trips a small public ${hostLabel} URL (${probePath}) through the fetchproxy bridge and returns diagnostics: the bridge's role (host/peer/null), port, version, the extension link (linked / pair pending / not attached / never answered), the elapsed round-trip time, and a plain-English hint distinguishing 'bridge never came up' from 'extension not connected' from 'real ${hostLabel}-side problem'. Read-only, no auth required.`;
16621
+ }
16622
+ async function runBridgeHealthcheck(args) {
16623
+ const { prefix, probePath, hostLabel, probeFn, classifyThrown, hints, path } = args;
16624
+ const probeUrl = probeDisplayUrl(hostLabel, probePath);
15722
16625
  const resolveTransport = () => typeof args.transport === "function" ? args.transport() : args.transport;
15723
- server.registerTool(`${prefix}_healthcheck`, {
16626
+ let probeBody = "";
16627
+ let thrown;
16628
+ const wrappedProbe = async (p) => {
16629
+ try {
16630
+ probeBody = await probeFn(p);
16631
+ return probeBody;
16632
+ } catch (e) {
16633
+ thrown = e;
16634
+ throw e;
16635
+ }
16636
+ };
16637
+ let ok;
16638
+ let elapsedMs;
16639
+ let bridge;
16640
+ let rawError;
16641
+ let pathNow;
16642
+ if (path) {
16643
+ const start = Date.now();
16644
+ try {
16645
+ await wrappedProbe(probePath);
16646
+ ok = true;
16647
+ } catch (e) {
16648
+ ok = false;
16649
+ rawError = { kind: classifyBridgeError(e), message: truncateErrorMessage(messageOf(e)) };
16650
+ }
16651
+ elapsedMs = Date.now() - start;
16652
+ pathNow = path();
16653
+ const transport2 = resolveTransport();
16654
+ bridge = transport2 ? projectBridgeStatus(transport2.status()) : void 0;
16655
+ } else {
16656
+ const transport2 = resolveTransport();
16657
+ if (!transport2) {
16658
+ throw new Error("bridge healthcheck: transport() returned nothing and no `path` was supplied \u2014 a bridge-only healthcheck needs its bridge.");
16659
+ }
16660
+ const probeResult = await transport2.runProbe(wrappedProbe, probePath);
16661
+ ok = probeResult.ok;
16662
+ elapsedMs = probeResult.elapsed_ms;
16663
+ bridge = {
16664
+ ...probeResult.bridge,
16665
+ last_extension_message_at: transport2.status().lastExtensionMessageAt
16666
+ };
16667
+ rawError = probeResult.error;
16668
+ }
16669
+ const probe = ok ? { url: probeUrl, elapsed_ms: elapsedMs, status: 200, body_length: probeBody.length } : { url: probeUrl, elapsed_ms: elapsedMs };
16670
+ let error61;
16671
+ let bridgeHint;
16672
+ let customHint;
16673
+ let customDetail;
16674
+ if (rawError) {
16675
+ let kind = rawError.kind === "other" ? "unknown" : rawError.kind;
16676
+ if (thrown instanceof FetchproxySessionNotReadyError) {
16677
+ kind = "session_not_ready";
16678
+ bridgeHint = thrown.hint;
16679
+ } else if (thrown instanceof FetchproxyBridgeDownError) {
16680
+ bridgeHint = thrown.hint;
16681
+ }
16682
+ if (thrown !== void 0 && classifyThrown) {
16683
+ const custom2 = classifyThrown(thrown);
16684
+ if (custom2) {
16685
+ kind = custom2.kind;
16686
+ customHint = custom2.hint;
16687
+ customDetail = custom2.detail;
16688
+ }
16689
+ }
16690
+ error61 = {
16691
+ kind,
16692
+ message: rawError.message,
16693
+ ...bridgeHint !== void 0 ? { bridge_hint: bridgeHint } : {},
16694
+ ...customDetail !== void 0 ? { detail: customDetail } : {}
16695
+ };
16696
+ }
16697
+ const direct = pathNow ? pathNow.transport === "direct" : bridge === void 0;
16698
+ const arm = ok ? "ok" : error61?.kind === "session_not_ready" ? "session_not_ready" : error61?.kind === "bridge_down" ? "bridge_down" : direct ? "direct" : bridge === void 0 || bridge.role === null ? "no_role" : error61?.kind === "timeout" ? "timeout" : error61?.kind === "protocol" || error61?.kind === "http" ? "protocol" : "unknown";
16699
+ const defaultHint = healthcheckHint({
16700
+ ok,
16701
+ role: bridge?.role ?? null,
16702
+ // The real configured port from bridgeHealth(), never a literal 37149.
16703
+ port: bridge?.port ?? null,
16704
+ hostLabel,
16705
+ prefix,
16706
+ probePath,
16707
+ errorKind: error61?.kind,
16708
+ bridgeHint: error61?.kind === "bridge_down" ? bridgeHint : void 0,
16709
+ session: {
16710
+ ...bridge?.session_state !== void 0 ? { state: bridge.session_state } : {},
16711
+ pairCode: bridge?.pending_pair_code ?? (thrown instanceof FetchproxySessionNotReadyError ? thrown.pairCode : null)
16712
+ },
16713
+ direct
16714
+ });
16715
+ const result = {
16716
+ ok,
16717
+ ...bridge ? { bridge } : {},
16718
+ ...pathNow ? { transport: pathNow } : {},
16719
+ probe,
16720
+ ...error61 ? { error: error61 } : {},
16721
+ // Precedence: classifyThrown's hint > per-arm override > default ladder.
16722
+ hint: customHint ?? hints?.[arm] ?? defaultHint
16723
+ };
16724
+ return {
16725
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
16726
+ };
16727
+ }
16728
+ function registerBridgeHealthcheckTool(args) {
16729
+ args.server.registerTool(`${args.prefix}_healthcheck`, {
15724
16730
  title: "Verify the fetchproxy bridge end-to-end",
15725
- description: `Round-trips a small public ${hostLabel} URL (${probePath}) through the fetchproxy bridge and returns diagnostics: the bridge's role (host/peer/null), port, version, the extension link (linked / pair pending / not attached / never answered), the elapsed round-trip time, and a plain-English hint distinguishing 'bridge never came up' from 'extension not connected' from 'real ${hostLabel}-side problem'. Call this when a real tool fails and you want to know which hop broke. Read-only, no auth required.`,
16731
+ description: `${bridgeHealthcheckDescription(args.hostLabel, args.probePath)} Call this when a real tool fails and you want to know which hop broke.`,
15726
16732
  annotations: {
15727
16733
  title: "Verify the fetchproxy bridge end-to-end",
15728
16734
  readOnlyHint: true,
@@ -15730,109 +16736,21 @@ function registerBridgeHealthcheckTool(args) {
15730
16736
  openWorldHint: true
15731
16737
  },
15732
16738
  inputSchema: {}
15733
- }, async () => {
15734
- let probeBody = "";
15735
- let thrown;
15736
- const wrappedProbe = async (p) => {
15737
- try {
15738
- probeBody = await probeFn(p);
15739
- return probeBody;
15740
- } catch (e) {
15741
- thrown = e;
15742
- throw e;
15743
- }
15744
- };
15745
- let ok;
15746
- let elapsedMs;
15747
- let bridge;
15748
- let rawError;
15749
- let pathNow;
15750
- if (path) {
15751
- const start = Date.now();
15752
- try {
15753
- await wrappedProbe(probePath);
15754
- ok = true;
15755
- } catch (e) {
15756
- ok = false;
15757
- rawError = { kind: classifyBridgeError(e), message: truncateErrorMessage(messageOf(e)) };
15758
- }
15759
- elapsedMs = Date.now() - start;
15760
- pathNow = path();
15761
- const transport2 = resolveTransport();
15762
- bridge = transport2 ? projectBridgeStatus(transport2.status()) : void 0;
15763
- } else {
15764
- const transport2 = resolveTransport();
15765
- if (!transport2) {
15766
- throw new Error("registerBridgeHealthcheckTool: transport() returned nothing and no `path` was supplied \u2014 a bridge-only healthcheck needs its bridge.");
15767
- }
15768
- const probeResult = await transport2.runProbe(wrappedProbe, probePath);
15769
- ok = probeResult.ok;
15770
- elapsedMs = probeResult.elapsed_ms;
15771
- bridge = {
15772
- ...probeResult.bridge,
15773
- last_extension_message_at: transport2.status().lastExtensionMessageAt
15774
- };
15775
- rawError = probeResult.error;
15776
- }
15777
- const probe = ok ? { url: probeUrl, elapsed_ms: elapsedMs, status: 200, body_length: probeBody.length } : { url: probeUrl, elapsed_ms: elapsedMs };
15778
- let error61;
15779
- let bridgeHint;
15780
- let customHint;
15781
- let customDetail;
15782
- if (rawError) {
15783
- let kind = rawError.kind === "other" ? "unknown" : rawError.kind;
15784
- if (thrown instanceof FetchproxySessionNotReadyError) {
15785
- kind = "session_not_ready";
15786
- bridgeHint = thrown.hint;
15787
- } else if (thrown instanceof FetchproxyBridgeDownError) {
15788
- bridgeHint = thrown.hint;
15789
- }
15790
- if (thrown !== void 0 && classifyThrown) {
15791
- const custom2 = classifyThrown(thrown);
15792
- if (custom2) {
15793
- kind = custom2.kind;
15794
- customHint = custom2.hint;
15795
- customDetail = custom2.detail;
15796
- }
15797
- }
15798
- error61 = {
15799
- kind,
15800
- message: rawError.message,
15801
- ...bridgeHint !== void 0 ? { bridge_hint: bridgeHint } : {},
15802
- ...customDetail !== void 0 ? { detail: customDetail } : {}
15803
- };
15804
- }
15805
- const direct = pathNow ? pathNow.transport === "direct" : bridge === void 0;
15806
- const arm = ok ? "ok" : error61?.kind === "session_not_ready" ? "session_not_ready" : error61?.kind === "bridge_down" ? "bridge_down" : direct ? "direct" : bridge === void 0 || bridge.role === null ? "no_role" : error61?.kind === "timeout" ? "timeout" : error61?.kind === "protocol" || error61?.kind === "http" ? "protocol" : "unknown";
15807
- const defaultHint = healthcheckHint({
15808
- ok,
15809
- role: bridge?.role ?? null,
15810
- // The real configured port from bridgeHealth(), never a literal 37149.
15811
- port: bridge?.port ?? null,
15812
- hostLabel,
15813
- prefix,
15814
- probePath,
15815
- errorKind: error61?.kind,
15816
- bridgeHint: error61?.kind === "bridge_down" ? bridgeHint : void 0,
15817
- session: {
15818
- ...bridge?.session_state !== void 0 ? { state: bridge.session_state } : {},
15819
- pairCode: bridge?.pending_pair_code ?? (thrown instanceof FetchproxySessionNotReadyError ? thrown.pairCode : null)
15820
- },
15821
- direct
15822
- });
15823
- const result = {
15824
- ok,
15825
- ...bridge ? { bridge } : {},
15826
- ...pathNow ? { transport: pathNow } : {},
15827
- probe,
15828
- ...error61 ? { error: error61 } : {},
15829
- // Precedence: classifyThrown's hint > per-arm override > default ladder.
15830
- hint: customHint ?? hints?.[arm] ?? defaultHint
15831
- };
15832
- return {
15833
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
15834
- };
15835
- });
16739
+ }, async () => runBridgeHealthcheck(args));
16740
+ }
16741
+ function registerAdaptiveHealthcheckTool(args) {
16742
+ const { server, prefix, hostLabel, usingBridge, bridge, credential } = args;
16743
+ server.registerTool(`${prefix}_healthcheck`, {
16744
+ title: "Verify this server can reach its upstream",
16745
+ description: `Reports which hop is broken when a real tool fails, for whichever path this server is actually using. Relaying through your signed-in browser tab: ${bridgeHealthcheckDescription(hostLabel, bridge.probePath)} Signing in server-side with configured credentials: ${credentialHealthcheckDescription(hostLabel)}`,
16746
+ annotations: {
16747
+ title: "Verify this server can reach its upstream",
16748
+ readOnlyHint: true,
16749
+ idempotentHint: true,
16750
+ openWorldHint: true
16751
+ },
16752
+ inputSchema: {}
16753
+ }, async () => usingBridge() ? runBridgeHealthcheck({ server, prefix, hostLabel, ...bridge }) : runCredentialHealthcheck({ server, prefix, hostLabel, ...credential }));
15836
16754
  }
15837
16755
  var PLACEHOLDER_RE2;
15838
16756
  var init_fetchproxy = __esm({
@@ -15841,6 +16759,7 @@ var init_fetchproxy = __esm({
15841
16759
  init_errors();
15842
16760
  init_dist2();
15843
16761
  init_healthcheck();
16762
+ init_healthcheck();
15844
16763
  PLACEHOLDER_RE2 = /^\$\{[^}]*\}$/;
15845
16764
  }
15846
16765
  });
@@ -22542,12 +23461,12 @@ var $ZodE164 = /* @__PURE__ */ $constructor("$ZodE164", (inst, def) => {
22542
23461
  $ZodStringFormat.init(inst, def);
22543
23462
  });
22544
23463
  var CC_SANITIZE = /[- ]/g;
22545
- function isLuhnAlgo(digits) {
22546
- let length = digits.length;
23464
+ function isLuhnAlgo(digits2) {
23465
+ let length = digits2.length;
22547
23466
  let bit = 1;
22548
23467
  let sum = 0;
22549
23468
  while (length) {
22550
- const value = +digits[--length];
23469
+ const value = +digits2[--length];
22551
23470
  bit ^= 1;
22552
23471
  sum += bit ? [0, 2, 4, 6, 8, 1, 3, 5, 7, 9][value] : value;
22553
23472
  }
@@ -37279,8 +38198,8 @@ function hex2(_params) {
37279
38198
  return _stringFormat(ZodCustomStringFormat, "hex", regexes_exports.hex, _params);
37280
38199
  }
37281
38200
  function hash(alg, params) {
37282
- const enc5 = params?.enc ?? "hex";
37283
- const format = `${alg}_${enc5}`;
38201
+ const enc7 = params?.enc ?? "hex";
38202
+ const format = `${alg}_${enc7}`;
37284
38203
  const regex = regexes_exports[format];
37285
38204
  if (!regex)
37286
38205
  throw new Error(`Unrecognized hash format: ${format}`);
@@ -44754,7 +45673,7 @@ var pageSchema = {
44754
45673
  init_errors();
44755
45674
 
44756
45675
  // src/version.ts
44757
- var VERSION = "0.3.0";
45676
+ var VERSION = "0.3.2";
44758
45677
 
44759
45678
  // src/client.ts
44760
45679
  import { dirname, join } from "path";
@@ -45313,7 +46232,7 @@ var schemaEventStatus = external_exports.enum(["all", "live", "draft", "started"
45313
46232
  function qs(params) {
45314
46233
  return params.size > 0 ? `?${params}` : "";
45315
46234
  }
45316
- function enc4(id) {
46235
+ function enc6(id) {
45317
46236
  if (/^\.+$/.test(id)) {
45318
46237
  throw new McpToolError(`Invalid id '${id}'.`, {
45319
46238
  hint: "Ids must be Eventbrite object ids (normally digits), not path segments."
@@ -45395,7 +46314,7 @@ function registerAccountTools(server, deps) {
45395
46314
  if (continuation) params.set("continuation", continuation);
45396
46315
  const data = await client2.request(
45397
46316
  "GET",
45398
- `/organizations/${enc4(org_id)}/events/${qs(params)}`
46317
+ `/organizations/${enc6(org_id)}/events/${qs(params)}`
45399
46318
  );
45400
46319
  return viewResponse(view, data);
45401
46320
  }
@@ -45418,7 +46337,7 @@ function registerAccountTools(server, deps) {
45418
46337
  if (continuation) params.set("continuation", continuation);
45419
46338
  const data = await client2.request(
45420
46339
  "GET",
45421
- `/organizations/${enc4(org_id)}/attendees/${qs(params)}`
46340
+ `/organizations/${enc6(org_id)}/attendees/${qs(params)}`
45422
46341
  );
45423
46342
  return viewResponse(view, data);
45424
46343
  }
@@ -45439,7 +46358,7 @@ function registerAccountTools(server, deps) {
45439
46358
  if (continuation) params.set("continuation", continuation);
45440
46359
  const data = await client2.request(
45441
46360
  "GET",
45442
- `/organizations/${enc4(org_id)}/orders/${qs(params)}`
46361
+ `/organizations/${enc6(org_id)}/orders/${qs(params)}`
45443
46362
  );
45444
46363
  return viewResponse(view, data);
45445
46364
  }
@@ -45479,7 +46398,7 @@ function registerAccountTools(server, deps) {
45479
46398
  if (continuation) params.set("continuation", continuation);
45480
46399
  return viewResponse(
45481
46400
  view,
45482
- await client2.request("GET", `/organizations/${enc4(org_id)}/${noun}/${qs(params)}`)
46401
+ await client2.request("GET", `/organizations/${enc6(org_id)}/${noun}/${qs(params)}`)
45483
46402
  );
45484
46403
  }
45485
46404
  );
@@ -45509,7 +46428,7 @@ function registerAccountTools(server, deps) {
45509
46428
  if (continuation) params.set("continuation", continuation);
45510
46429
  return viewResponse(
45511
46430
  view,
45512
- await client2.request("GET", `/organizations/${enc4(org_id)}/reports/${kind}/${qs(params)}`)
46431
+ await client2.request("GET", `/organizations/${enc6(org_id)}/reports/${kind}/${qs(params)}`)
45513
46432
  );
45514
46433
  }
45515
46434
  );
@@ -45613,7 +46532,7 @@ function registerEventTools(server, deps) {
45613
46532
  if (continuation) params.set("continuation", continuation);
45614
46533
  return viewResponse(
45615
46534
  view,
45616
- await client2.request("GET", `/events/${enc4(event_id)}/attendees/${qs(params)}`)
46535
+ await client2.request("GET", `/events/${enc6(event_id)}/attendees/${qs(params)}`)
45617
46536
  );
45618
46537
  }
45619
46538
  );
@@ -45630,7 +46549,7 @@ function registerEventTools(server, deps) {
45630
46549
  },
45631
46550
  async ({ event_id, attendee_id, view }) => viewResponse(
45632
46551
  view,
45633
- await client2.request("GET", `/events/${enc4(event_id)}/attendees/${enc4(attendee_id)}/`)
46552
+ await client2.request("GET", `/events/${enc6(event_id)}/attendees/${enc6(attendee_id)}/`)
45634
46553
  )
45635
46554
  );
45636
46555
  server.registerTool(
@@ -45653,7 +46572,7 @@ function registerEventTools(server, deps) {
45653
46572
  if (continuation) params.set("continuation", continuation);
45654
46573
  return viewResponse(
45655
46574
  view,
45656
- await client2.request("GET", `/events/${enc4(event_id)}/orders/${qs(params)}`)
46575
+ await client2.request("GET", `/events/${enc6(event_id)}/orders/${qs(params)}`)
45657
46576
  );
45658
46577
  }
45659
46578
  );
@@ -45672,7 +46591,7 @@ function registerEventTools(server, deps) {
45672
46591
  view,
45673
46592
  await client2.request(
45674
46593
  "GET",
45675
- `/events/${enc4(event_id)}/ticket_classes/${enc4(ticket_class_id)}/`
46594
+ `/events/${enc6(event_id)}/ticket_classes/${enc6(ticket_class_id)}/`
45676
46595
  )
45677
46596
  )
45678
46597
  );
@@ -45691,7 +46610,7 @@ function registerEventTools(server, deps) {
45691
46610
  view,
45692
46611
  await client2.request(
45693
46612
  "GET",
45694
- `/events/${enc4(event_id)}/${canned ? "canned_questions" : "questions"}/`
46613
+ `/events/${enc6(event_id)}/${canned ? "canned_questions" : "questions"}/`
45695
46614
  )
45696
46615
  )
45697
46616
  );
@@ -45714,7 +46633,7 @@ function registerLookupTools(server, deps) {
45714
46633
  async ({ order_id, expand, view }) => {
45715
46634
  const params = new URLSearchParams();
45716
46635
  if (expand) params.set("expand", expand);
45717
- return viewResponse(view, await client2.request("GET", `/orders/${enc4(order_id)}/${qs(params)}`));
46636
+ return viewResponse(view, await client2.request("GET", `/orders/${enc6(order_id)}/${qs(params)}`));
45718
46637
  }
45719
46638
  );
45720
46639
  server.registerTool(
@@ -45727,7 +46646,7 @@ function registerLookupTools(server, deps) {
45727
46646
  view: viewArg()
45728
46647
  }
45729
46648
  },
45730
- async ({ venue_id, view }) => viewResponse(view, await client2.request("GET", `/venues/${enc4(venue_id)}/`))
46649
+ async ({ venue_id, view }) => viewResponse(view, await client2.request("GET", `/venues/${enc6(venue_id)}/`))
45731
46650
  );
45732
46651
  server.registerTool(
45733
46652
  "eb_venue_events",
@@ -45747,7 +46666,7 @@ function registerLookupTools(server, deps) {
45747
46666
  if (continuation) params.set("continuation", continuation);
45748
46667
  return viewResponse(
45749
46668
  view,
45750
- await client2.request("GET", `/venues/${enc4(venue_id)}/events/${qs(params)}`)
46669
+ await client2.request("GET", `/venues/${enc6(venue_id)}/events/${qs(params)}`)
45751
46670
  );
45752
46671
  }
45753
46672
  );
@@ -45761,7 +46680,7 @@ function registerLookupTools(server, deps) {
45761
46680
  view: viewArg()
45762
46681
  }
45763
46682
  },
45764
- async ({ organizer_id, view }) => viewResponse(view, await client2.request("GET", `/organizers/${enc4(organizer_id)}/`))
46683
+ async ({ organizer_id, view }) => viewResponse(view, await client2.request("GET", `/organizers/${enc6(organizer_id)}/`))
45765
46684
  );
45766
46685
  server.registerTool(
45767
46686
  "eb_organizer_events",
@@ -45783,7 +46702,7 @@ function registerLookupTools(server, deps) {
45783
46702
  if (continuation) params.set("continuation", continuation);
45784
46703
  return viewResponse(
45785
46704
  view,
45786
- await client2.request("GET", `/organizers/${enc4(organizer_id)}/events/${qs(params)}`)
46705
+ await client2.request("GET", `/organizers/${enc6(organizer_id)}/events/${qs(params)}`)
45787
46706
  );
45788
46707
  }
45789
46708
  );
@@ -45805,7 +46724,7 @@ function registerLookupTools(server, deps) {
45805
46724
  if (continuation) params.set("continuation", continuation);
45806
46725
  return viewResponse(
45807
46726
  view,
45808
- await client2.request("GET", `/series/${enc4(series_id)}/events/${qs(params)}`)
46727
+ await client2.request("GET", `/series/${enc6(series_id)}/events/${qs(params)}`)
45809
46728
  );
45810
46729
  }
45811
46730
  );
@@ -45819,7 +46738,7 @@ function registerLookupTools(server, deps) {
45819
46738
  view: viewArg()
45820
46739
  }
45821
46740
  },
45822
- async ({ user_id, view }) => viewResponse(view, await client2.request("GET", `/users/${enc4(user_id)}/`))
46741
+ async ({ user_id, view }) => viewResponse(view, await client2.request("GET", `/users/${enc6(user_id)}/`))
45823
46742
  );
45824
46743
  }
45825
46744