@chrischall/eventbrite-mcp 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -7287,19 +7287,207 @@ 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",
7305
7493
  "fetch_in_page",
@@ -7381,6 +7569,143 @@ var init_json_pointer = __esm({
7381
7569
  }
7382
7570
  });
7383
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
+
7384
7709
  // node_modules/@fetchproxy/protocol/dist/validate.js
7385
7710
  function assertObject(x, label) {
7386
7711
  if (typeof x !== "object" || x === null || Array.isArray(x)) {
@@ -7405,6 +7730,18 @@ function assertBase64(x, label) {
7405
7730
  if (!BASE64_RE.test(x))
7406
7731
  throw new ProtocolError(`${label}: invalid base64`);
7407
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
+ }
7408
7745
  function assertBoolean(x, label) {
7409
7746
  if (typeof x !== "boolean") {
7410
7747
  throw new ProtocolError(`${label}: must be boolean`);
@@ -7733,6 +8070,9 @@ function validateHello(raw) {
7733
8070
  if (!HOSTNAME_RE.test(d)) {
7734
8071
  throw new ProtocolError(`hello.domains: invalid hostname ${JSON.stringify(d)}`);
7735
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
+ }
7736
8076
  }
7737
8077
  if (raw.capabilities !== void 0) {
7738
8078
  if (!Array.isArray(raw.capabilities)) {
@@ -7790,6 +8130,8 @@ function validateHello(raw) {
7790
8130
  assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
7791
8131
  assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
7792
8132
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
8133
+ assertBase64Bytes(raw.sessionPub, "hello.sessionPub", 32);
8134
+ assertBase64Bytes(raw.answersExtNonce, "hello.answersExtNonce", 32);
7793
8135
  assertBase64(raw.sessionSig, "hello.sessionSig");
7794
8136
  return raw;
7795
8137
  }
@@ -7807,11 +8149,34 @@ function validateHello(raw) {
7807
8149
  }
7808
8150
  throw new ProtocolError(`hello.role: must be 'server' or 'extension', got ${String(role)}`);
7809
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
+ }
7810
8174
  function validateReady(raw) {
7811
8175
  assertString(raw.mcpId, "ready.mcpId");
7812
8176
  if (!isValidMcpId(raw.mcpId))
7813
8177
  throw new ProtocolError("ready.mcpId: invalid format");
7814
8178
  assertBase64(raw.extensionSessionPub, "ready.extensionSessionPub");
8179
+ assertBase64Bytes(raw.mcpSessionPub, "ready.mcpSessionPub", 32);
7815
8180
  assertBase64(raw.sessionSig, "ready.sessionSig");
7816
8181
  return raw;
7817
8182
  }
@@ -7830,7 +8195,7 @@ function validatePairPending(raw) {
7830
8195
  throw new ProtocolError("pair-pending.mcpId: invalid format");
7831
8196
  assertString(raw.pairCode, "pair-pending.pairCode");
7832
8197
  if (!PAIR_CODE_RE.test(raw.pairCode)) {
7833
- 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)}`);
7834
8199
  }
7835
8200
  return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
7836
8201
  }
@@ -8279,6 +8644,7 @@ var init_validate = __esm({
8279
8644
  init_frames();
8280
8645
  init_mcp_id();
8281
8646
  init_json_pointer();
8647
+ init_public_suffix();
8282
8648
  ProtocolError = class extends Error {
8283
8649
  constructor(message) {
8284
8650
  super(message);
@@ -8292,10 +8658,10 @@ var init_validate = __esm({
8292
8658
  HEADER_NAME_RE = /^[A-Za-z0-9_\-]{1,128}$/;
8293
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;
8294
8660
  CAPTURE_PATH_RE = /^\/[A-Za-z0-9._~%\-/]*\*?$/;
8295
- DOM_SELECTOR_RE = /^[^-]{1,512}$/;
8661
+ DOM_SELECTOR_RE = /^[^\x00-\x1f\x7f]{1,512}$/;
8296
8662
  DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
8297
8663
  GRAPHQL_OP_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]{0,127}$/;
8298
- PAIR_CODE_RE = /^\d{3}-\d{3}$/;
8664
+ PAIR_CODE_RE = /^\d{4}-\d{4}$/;
8299
8665
  KNOWN_RESPONSE_OPS = /* @__PURE__ */ new Set([
8300
8666
  ...KNOWN_CAPABILITIES,
8301
8667
  "graphql_query"
@@ -8303,180 +8669,33 @@ var init_validate = __esm({
8303
8669
  }
8304
8670
  });
8305
8671
 
8306
- // node_modules/@fetchproxy/protocol/dist/crypto.js
8307
- async function exportRaw(key, format) {
8308
- const buf = await subtle.exportKey(format, key);
8309
- return new Uint8Array(buf);
8310
- }
8311
- async function generateX25519() {
8312
- const kp = await subtle.generateKey({ name: "X25519" }, true, [
8313
- "deriveBits"
8314
- ]);
8315
- const priv = await exportRaw(kp.privateKey, "pkcs8");
8316
- const pub = await exportRaw(kp.publicKey, "raw");
8317
- return { privateKey: priv.slice(-32), publicKey: pub };
8318
- }
8319
- async function generateEd25519() {
8320
- const kp = await subtle.generateKey({ name: "Ed25519" }, true, [
8321
- "sign",
8322
- "verify"
8323
- ]);
8324
- const priv = await exportRaw(kp.privateKey, "pkcs8");
8325
- const pub = await exportRaw(kp.publicKey, "raw");
8326
- return { privateKey: priv.slice(-32), publicKey: pub };
8327
- }
8328
- async function importX25519Priv(raw) {
8329
- const prefix = new Uint8Array([
8330
- 48,
8331
- 46,
8332
- 2,
8333
- 1,
8334
- 0,
8335
- 48,
8336
- 5,
8337
- 6,
8338
- 3,
8339
- 43,
8340
- 101,
8341
- 110,
8342
- 4,
8343
- 34,
8344
- 4,
8345
- 32
8346
- ]);
8347
- const pkcs8 = new Uint8Array(prefix.length + raw.length);
8348
- pkcs8.set(prefix, 0);
8349
- pkcs8.set(raw, prefix.length);
8350
- return subtle.importKey("pkcs8", pkcs8, { name: "X25519" }, false, [
8351
- "deriveBits"
8352
- ]);
8353
- }
8354
- async function importX25519Pub(raw) {
8355
- return subtle.importKey("raw", raw, { name: "X25519" }, false, []);
8356
- }
8357
- async function importEd25519Priv(raw) {
8358
- const prefix = new Uint8Array([
8359
- 48,
8360
- 46,
8361
- 2,
8362
- 1,
8363
- 0,
8364
- 48,
8365
- 5,
8366
- 6,
8367
- 3,
8368
- 43,
8369
- 101,
8370
- 112,
8371
- 4,
8372
- 34,
8373
- 4,
8374
- 32
8375
- ]);
8376
- const pkcs8 = new Uint8Array(prefix.length + raw.length);
8377
- pkcs8.set(prefix, 0);
8378
- pkcs8.set(raw, prefix.length);
8379
- return subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"]);
8380
- }
8381
- async function importEd25519Pub(raw) {
8382
- return subtle.importKey("raw", raw, { name: "Ed25519" }, false, ["verify"]);
8383
- }
8384
- async function ecdhX25519(privRaw, pubRaw) {
8385
- const priv = await importX25519Priv(privRaw);
8386
- const pub = await importX25519Pub(pubRaw);
8387
- const bits = await subtle.deriveBits({ name: "X25519", public: pub }, priv, 256);
8388
- return new Uint8Array(bits);
8389
- }
8390
- async function ed25519Sign(privRaw, msg) {
8391
- const priv = await importEd25519Priv(privRaw);
8392
- const sig = await subtle.sign({ name: "Ed25519" }, priv, msg);
8393
- return new Uint8Array(sig);
8394
- }
8395
- async function ed25519Verify(pubRaw, msg, sig) {
8396
- const pub = await importEd25519Pub(pubRaw);
8397
- return subtle.verify({ name: "Ed25519" }, pub, sig, msg);
8398
- }
8399
- async function hkdfSha256(ikm, salt, info, lenBytes) {
8400
- const baseKey = await subtle.importKey("raw", ikm, { name: "HKDF" }, false, [
8401
- "deriveBits"
8402
- ]);
8403
- const bits = await subtle.deriveBits({
8404
- name: "HKDF",
8405
- hash: "SHA-256",
8406
- salt,
8407
- info
8408
- }, baseKey, lenBytes * 8);
8409
- return new Uint8Array(bits);
8410
- }
8411
- async function aesGcmSeal(key, iv, plaintext) {
8412
- const k = await subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
8413
- "encrypt"
8414
- ]);
8415
- const ct = await subtle.encrypt({ name: "AES-GCM", iv }, k, plaintext);
8416
- return new Uint8Array(ct);
8417
- }
8418
- async function aesGcmOpen(key, iv, ciphertext) {
8419
- const k = await subtle.importKey("raw", key, { name: "AES-GCM" }, false, [
8420
- "decrypt"
8421
- ]);
8422
- const pt = await subtle.decrypt({ name: "AES-GCM", iv }, k, ciphertext);
8423
- return new Uint8Array(pt);
8424
- }
8425
- async function sha256(data) {
8426
- const h = await subtle.digest("SHA-256", data);
8427
- return new Uint8Array(h);
8428
- }
8429
- var subtle;
8430
- var init_crypto = __esm({
8431
- "node_modules/@fetchproxy/protocol/dist/crypto.js"() {
8432
- subtle = globalThis.crypto.subtle;
8433
- }
8434
- });
8435
-
8436
- // node_modules/@fetchproxy/protocol/dist/encoding.js
8437
- function toB64(bytes) {
8438
- let s = "";
8439
- for (let i = 0; i < bytes.length; i++)
8440
- s += String.fromCharCode(bytes[i]);
8441
- return btoa(s);
8442
- }
8443
- function fromB64(s) {
8444
- const bin = atob(s);
8445
- const out = new Uint8Array(bin.length);
8446
- for (let i = 0; i < bin.length; i++)
8447
- out[i] = bin.charCodeAt(i);
8448
- return out;
8449
- }
8450
- function concatBytes(a, b) {
8451
- const out = new Uint8Array(a.length + b.length);
8452
- out.set(a, 0);
8453
- out.set(b, a.length);
8454
- return out;
8455
- }
8456
- var init_encoding = __esm({
8457
- "node_modules/@fetchproxy/protocol/dist/encoding.js"() {
8458
- }
8459
- });
8460
-
8461
8672
  // node_modules/@fetchproxy/protocol/dist/pair-code.js
8462
- async function derivePairCode(pub) {
8463
- const h = await sha256(pub);
8464
- const b0 = h[0] ?? 0;
8465
- const b1 = h[1] ?? 0;
8466
- const b2 = h[2] ?? 0;
8467
- const b3 = h[3] ?? 0;
8468
- const u32 = (b0 << 24 | b1 << 16 | b2 << 8 | b3) >>> 0;
8469
- const n = u32 % 1e6;
8470
- const s = n.toString().padStart(6, "0");
8471
- return `${s.slice(0, 3)}-${s.slice(3)}`;
8472
- }
8473
- async function derivePairCodeFromIds(mcpPub, extPub) {
8474
- return derivePairCode(concatBytes(mcpPub, extPub));
8475
- }
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;
8476
8694
  var init_pair_code = __esm({
8477
8695
  "node_modules/@fetchproxy/protocol/dist/pair-code.js"() {
8478
8696
  init_crypto();
8479
- init_encoding();
8697
+ enc2 = new TextEncoder();
8698
+ PAIR_LABEL = "fetchproxy/4/pair";
8480
8699
  }
8481
8700
  });
8482
8701
 
@@ -8486,10 +8705,24 @@ function randomIv() {
8486
8705
  globalThis.crypto.getRandomValues(iv);
8487
8706
  return iv;
8488
8707
  }
8489
- 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) {
8490
8723
  const iv = randomIv();
8491
- const pt = enc.encode(JSON.stringify(inner));
8492
- const ct = await aesGcmSeal(sessionKey, iv, pt);
8724
+ const pt = plaintextOf(inner);
8725
+ const ct = await aesGcmSeal(sessionKey, iv, pt, frameAad(mcpId, seq, direction));
8493
8726
  return {
8494
8727
  type: "frame",
8495
8728
  mcpId,
@@ -8498,18 +8731,18 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner) {
8498
8731
  ciphertext: toB64(ct)
8499
8732
  };
8500
8733
  }
8501
- async function openEncryptedFrame(sessionKey, frame) {
8502
- const result = await openEncryptedFrameDetailed(sessionKey, frame);
8734
+ async function openEncryptedFrame(sessionKey, frame, direction) {
8735
+ const result = await openEncryptedFrameDetailed(sessionKey, frame, direction);
8503
8736
  if (result.stage === "ok")
8504
8737
  return result.inner;
8505
8738
  throw result.error instanceof Error ? result.error : new Error(String(result.error));
8506
8739
  }
8507
- async function openEncryptedFrameDetailed(sessionKey, frame) {
8740
+ async function openEncryptedFrameDetailed(sessionKey, frame, direction) {
8508
8741
  let pt;
8509
8742
  try {
8510
8743
  const iv = fromB64(frame.iv);
8511
8744
  const ct = fromB64(frame.ciphertext);
8512
- pt = await aesGcmOpen(sessionKey, iv, ct);
8745
+ pt = await aesGcmOpen(sessionKey, iv, ct, frameAad(frame.mcpId, frame.seq, direction));
8513
8746
  } catch (error61) {
8514
8747
  return { stage: "decrypt-failed", error: error61 };
8515
8748
  }
@@ -8527,14 +8760,18 @@ async function openEncryptedFrameDetailed(sessionKey, frame) {
8527
8760
  return { stage: "validation-failed", error: error61, recoveredId };
8528
8761
  }
8529
8762
  }
8530
- var enc, dec;
8763
+ var enc3, dec, AES_GCM_TAG_BYTES, IV_B64_BYTES, MAX_FRAME_BYTES;
8531
8764
  var init_seal = __esm({
8532
8765
  "node_modules/@fetchproxy/protocol/dist/seal.js"() {
8533
8766
  init_crypto();
8534
8767
  init_encoding();
8768
+ init_frames();
8535
8769
  init_validate();
8536
- enc = new TextEncoder();
8770
+ enc3 = new TextEncoder();
8537
8771
  dec = new TextDecoder();
8772
+ AES_GCM_TAG_BYTES = 16;
8773
+ IV_B64_BYTES = 16;
8774
+ MAX_FRAME_BYTES = 42 * 1024 * 1024;
8538
8775
  }
8539
8776
  });
8540
8777
 
@@ -8548,6 +8785,7 @@ var init_dist = __esm({
8548
8785
  init_pair_code();
8549
8786
  init_seal();
8550
8787
  init_encoding();
8788
+ init_public_suffix();
8551
8789
  init_json_pointer();
8552
8790
  }
8553
8791
  });
@@ -10847,7 +11085,7 @@ var require_websocket = __commonJS({
10847
11085
  var http = __require("http");
10848
11086
  var net = __require("net");
10849
11087
  var tls = __require("tls");
10850
- var { randomBytes, createHash } = __require("crypto");
11088
+ var { randomBytes: randomBytes2, createHash } = __require("crypto");
10851
11089
  var { Duplex, Readable } = __require("stream");
10852
11090
  var { URL: URL2 } = __require("url");
10853
11091
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -11385,7 +11623,7 @@ var require_websocket = __commonJS({
11385
11623
  }
11386
11624
  }
11387
11625
  const defaultPort = isSecure ? 443 : 80;
11388
- const key = randomBytes(16).toString("base64");
11626
+ const key = randomBytes2(16).toString("base64");
11389
11627
  const request = isSecure ? https.request : http.request;
11390
11628
  const protocolSet = /* @__PURE__ */ new Set();
11391
11629
  let perMessageDeflate;
@@ -12297,7 +12535,7 @@ var init_wrapper = __esm({
12297
12535
  async function buildServerHello(opts) {
12298
12536
  const sessionNonce = new Uint8Array(32);
12299
12537
  globalThis.crypto.getRandomValues(sessionNonce);
12300
- 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));
12301
12539
  const hello = {
12302
12540
  type: "hello",
12303
12541
  protocolVersion: PROTOCOL_VERSION,
@@ -12310,6 +12548,8 @@ async function buildServerHello(opts) {
12310
12548
  identityX25519Pub: toB64(opts.identity.x25519Pub),
12311
12549
  identityEd25519Pub: toB64(opts.identity.ed25519Pub),
12312
12550
  sessionNonce: toB64(sessionNonce),
12551
+ sessionPub: toB64(opts.sessionPub),
12552
+ answersExtNonce: toB64(opts.answersExtNonce),
12313
12553
  sessionSig: toB64(sig)
12314
12554
  };
12315
12555
  if (opts.accepts && opts.accepts.length > 0)
@@ -12371,32 +12611,109 @@ var init_build_server_hello = __esm({
12371
12611
  }
12372
12612
  });
12373
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
+
12374
12629
  // node_modules/@fetchproxy/server/dist/session.js
12375
- var SessionState;
12630
+ var MAX_INFLIGHT_INBOUND_SEQS, SessionState;
12376
12631
  var init_session = __esm({
12377
12632
  "node_modules/@fetchproxy/server/dist/session.js"() {
12633
+ MAX_INFLIGHT_INBOUND_SEQS = 1024;
12378
12634
  SessionState = class {
12379
12635
  sessionKey;
12380
12636
  outboundSeq = 0;
12381
12637
  lastInboundSeq = 0;
12638
+ inflightInbound = /* @__PURE__ */ new Set();
12639
+ saturationWarned = false;
12382
12640
  constructor(sessionKey) {
12383
12641
  this.sessionKey = sessionKey;
12384
12642
  }
12643
+ /**
12644
+ * Whether the caller holding this claim verdict should log saturation now.
12645
+ * True only on the TRANSITION into it: a line per dropped frame turned a
12646
+ * flood — or a set that never drains — into a log flood amplifying the very
12647
+ * condition it reported (#376). An `'ok'` claim re-arms the latch, since it
12648
+ * is the one signal every caller sees that the set has drained below the
12649
+ * bound; a replay needs no capacity and leaves it alone. Latched per
12650
+ * session, so a renegotiated one starts armed. A set hovering at the bound
12651
+ * can still alternate ok/saturated, but that logs at the rate frames finish
12652
+ * opening, not the rate they arrive.
12653
+ */
12654
+ saturationWarningDue(claim2) {
12655
+ if (claim2 === "ok")
12656
+ this.saturationWarned = false;
12657
+ if (claim2 !== "saturated" || this.saturationWarned)
12658
+ return false;
12659
+ this.saturationWarned = true;
12660
+ return true;
12661
+ }
12385
12662
  nextOutboundSeq() {
12386
12663
  this.outboundSeq += 1;
12387
12664
  return this.outboundSeq;
12388
12665
  }
12389
- acceptInboundSeq(seq) {
12666
+ /**
12667
+ * Take this seq out of circulation for the frame about to be opened, and
12668
+ * say whether it was available. SYNCHRONOUS and single-shot: call it before
12669
+ * the first `await` of the receive path, or a duplicate frame read in the
12670
+ * same pass will be judged against a counter neither frame has moved yet.
12671
+ *
12672
+ * Anything but `'ok'` means "not this frame's to process": `'replay'` for
12673
+ * a spent seq or a duplicate of one still in flight, `'saturated'` when the
12674
+ * in-flight bound is full. Replay is checked first, since refusing a stale
12675
+ * seq needs no capacity. Every `'ok'` MUST be answered by exactly one
12676
+ * {@link commitInboundSeq} or {@link releaseInboundSeq}.
12677
+ */
12678
+ claimInboundSeq(seq) {
12390
12679
  if (seq <= this.lastInboundSeq)
12391
- return false;
12392
- this.lastInboundSeq = seq;
12393
- return true;
12680
+ return "replay";
12681
+ if (this.inflightInbound.has(seq))
12682
+ return "replay";
12683
+ if (this.inflightInbound.size >= MAX_INFLIGHT_INBOUND_SEQS)
12684
+ return "saturated";
12685
+ this.inflightInbound.add(seq);
12686
+ return "ok";
12687
+ }
12688
+ /**
12689
+ * Give a claim back without spending the seq — for a frame that did not
12690
+ * authenticate, which is a frame that never happened. The counter does not
12691
+ * move, so the genuine frames behind it, carrying lower numbers, are still
12692
+ * accepted. Idempotent, so a caller may release in a `catch` after a commit
12693
+ * it is not sure ran.
12694
+ */
12695
+ releaseInboundSeq(seq) {
12696
+ this.inflightInbound.delete(seq);
12697
+ }
12698
+ /**
12699
+ * Record a seq as spent. Call only for a frame that authenticated. Never
12700
+ * moves the counter backwards, so an out-of-order commit cannot reopen a
12701
+ * seq an earlier one already closed.
12702
+ */
12703
+ commitInboundSeq(seq) {
12704
+ this.inflightInbound.delete(seq);
12705
+ if (seq > this.lastInboundSeq)
12706
+ this.lastInboundSeq = seq;
12394
12707
  }
12395
12708
  };
12396
12709
  }
12397
12710
  });
12398
12711
 
12399
12712
  // node_modules/@fetchproxy/server/dist/session-ready.js
12713
+ function protocolVersionCloseReason(info) {
12714
+ const far = info.peer === "extension" ? "the extension" : "the other MCP";
12715
+ return `protocol version mismatch: this MCP speaks ${info.ourVersion}, ${far} speaks ${info.theirVersion}`;
12716
+ }
12400
12717
  async function awaitSessionReady(ready, opts) {
12401
12718
  const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
12402
12719
  if (ms <= 0)
@@ -12415,7 +12732,7 @@ async function awaitSessionReady(ready, opts) {
12415
12732
  clearTimeout(timer);
12416
12733
  }
12417
12734
  }
12418
- var SESSION_READY_TIMEOUT_MS, FetchproxySessionNotReadyError, FetchproxyHelloRejectedError;
12735
+ var SESSION_READY_TIMEOUT_MS, FetchproxySessionNotReadyError, FetchproxyHelloRejectedError, MIN_VERSION, FetchproxyProtocolVersionError;
12419
12736
  var init_session_ready = __esm({
12420
12737
  "node_modules/@fetchproxy/server/dist/session-ready.js"() {
12421
12738
  SESSION_READY_TIMEOUT_MS = 3e4;
@@ -12447,11 +12764,29 @@ var init_session_ready = __esm({
12447
12764
  Object.setPrototypeOf(this, new.target.prototype);
12448
12765
  }
12449
12766
  };
12767
+ MIN_VERSION = "3.0.0";
12768
+ FetchproxyProtocolVersionError = class extends Error {
12769
+ /** The protocol version this process speaks. */
12770
+ ourVersion;
12771
+ /** The protocol version the far end announced in the hello we refused. */
12772
+ theirVersion;
12773
+ peer;
12774
+ constructor(info) {
12775
+ 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`;
12776
+ super(`protocol version mismatch: this MCP speaks fetchproxy protocol ${info.ourVersion}, ${far}`);
12777
+ this.name = "FetchproxyProtocolVersionError";
12778
+ this.ourVersion = info.ourVersion;
12779
+ this.theirVersion = info.theirVersion;
12780
+ this.peer = info.peer;
12781
+ Object.setPrototypeOf(this, new.target.prototype);
12782
+ }
12783
+ };
12450
12784
  }
12451
12785
  });
12452
12786
 
12453
12787
  // node_modules/@fetchproxy/server/dist/identity.js
12454
- import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
12788
+ import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink } from "node:fs/promises";
12789
+ import { randomBytes } from "node:crypto";
12455
12790
  import { isAbsolute, join as join2 } from "node:path";
12456
12791
  import { homedir } from "node:os";
12457
12792
  function envIdentityDir() {
@@ -12505,16 +12840,45 @@ function parseIdentity(text) {
12505
12840
  createdAt: j.createdAt
12506
12841
  };
12507
12842
  }
12843
+ async function openIdentityDir(dir) {
12844
+ await mkdir(dir, { recursive: true, mode: 448 });
12845
+ try {
12846
+ await chmod(dir, 448);
12847
+ } catch (e) {
12848
+ const code = e.code;
12849
+ if (code !== "EROFS" && code !== "EPERM")
12850
+ throw e;
12851
+ }
12852
+ }
12853
+ async function sweepStagingFiles(dir, serverName) {
12854
+ const base = safeIdentityFileBase(serverName).replace(/[.]/g, "\\.");
12855
+ const staging = new RegExp(`^${base}\\.json\\.[0-9a-f]{${STAGING_BYTES * 2}}\\.tmp$`);
12856
+ let names;
12857
+ try {
12858
+ names = await readdir(dir);
12859
+ } catch {
12860
+ return;
12861
+ }
12862
+ await Promise.all(names.filter((name) => staging.test(name)).map((name) => unlink(join2(dir, name)).catch(() => void 0)));
12863
+ }
12508
12864
  async function writeIdentityFile(dir, serverName, id) {
12509
12865
  const path = identityFilePath(dir, serverName);
12510
- await mkdir(dir, { recursive: true, mode: 448 });
12511
- await writeFile(path, serializeIdentity(id), { mode: 384 });
12512
- await chmod(path, 384);
12866
+ await openIdentityDir(dir);
12867
+ await sweepStagingFiles(dir, serverName);
12868
+ const tmp = `${path}.${randomBytes(STAGING_BYTES).toString("hex")}.tmp`;
12869
+ try {
12870
+ await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
12871
+ await chmod(tmp, 384);
12872
+ await rename(tmp, path);
12873
+ } catch (e) {
12874
+ await rm(tmp, { force: true });
12875
+ throw e;
12876
+ }
12513
12877
  return path;
12514
12878
  }
12515
12879
  async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
12516
12880
  const path = identityFilePath(dir, serverName);
12517
- await mkdir(dir, { recursive: true, mode: 448 });
12881
+ await openIdentityDir(dir);
12518
12882
  try {
12519
12883
  return parseIdentity(await readFile(path, "utf8"));
12520
12884
  } catch (e) {
@@ -12525,24 +12889,44 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
12525
12889
  await writeIdentityFile(dir, serverName, id);
12526
12890
  return id;
12527
12891
  }
12528
- var SAFE_PLAIN, SAFE_SCOPED;
12892
+ var SAFE_PLAIN, SAFE_SCOPED, STAGING_BYTES;
12529
12893
  var init_identity = __esm({
12530
12894
  "node_modules/@fetchproxy/server/dist/identity.js"() {
12531
12895
  init_dist();
12532
12896
  SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
12533
12897
  SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
12898
+ STAGING_BYTES = 6;
12534
12899
  }
12535
12900
  });
12536
12901
 
12537
12902
  // node_modules/@fetchproxy/server/dist/extension-trust.js
12538
- import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
12539
- import { join as join3 } from "node:path";
12903
+ import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
12904
+ import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
12905
+ function envTrustDir(env = process.env) {
12906
+ const raw = env[TRUST_DIR_ENV];
12907
+ if (raw === void 0)
12908
+ return void 0;
12909
+ const dir = raw.trim();
12910
+ if (dir !== "" && isAbsolute2(dir))
12911
+ return dir;
12912
+ 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.`);
12913
+ return void 0;
12914
+ }
12915
+ function resolveTrustDir(explicit, identityDir) {
12916
+ if (explicit !== void 0)
12917
+ return explicit;
12918
+ return envTrustDir() ?? identityDir ?? defaultIdentityDir();
12919
+ }
12920
+ function defaultTrustDir() {
12921
+ return resolveTrustDir();
12922
+ }
12540
12923
  function fileExtensionTrust(args) {
12924
+ const dir = resolveTrustDir(args.trustDir, args.dir);
12541
12925
  return {
12542
12926
  allowNew: args.allowNew,
12543
- location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
12544
- read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
12545
- write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
12927
+ location: extensionTrustPath(args.serverName, dir),
12928
+ read: () => readExtensionPin(args.serverName, dir),
12929
+ write: (pin) => writeExtensionPin(args.serverName, pin, dir)
12546
12930
  };
12547
12931
  }
12548
12932
  function allowNewExtensionIdentity(explicit, env = process.env) {
@@ -12569,14 +12953,14 @@ function decideExtensionTrust(args) {
12569
12953
  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.`
12570
12954
  };
12571
12955
  }
12572
- function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
12956
+ function extensionTrustPath(serverName, dir = defaultTrustDir()) {
12573
12957
  return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
12574
12958
  }
12575
12959
  function extensionTrustPathHint(serverName) {
12576
12960
  try {
12577
12961
  return extensionTrustPath(serverName);
12578
12962
  } catch {
12579
- return join3(defaultIdentityDir(), "<server-name>.extension-trust.json");
12963
+ return join3(defaultTrustDir(), "<server-name>.extension-trust.json");
12580
12964
  }
12581
12965
  }
12582
12966
  function isPin(x) {
@@ -12585,7 +12969,7 @@ function isPin(x) {
12585
12969
  const r = x;
12586
12970
  return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
12587
12971
  }
12588
- async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
12972
+ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
12589
12973
  const path = extensionTrustPath(serverName, dir);
12590
12974
  let raw;
12591
12975
  try {
@@ -12610,36 +12994,75 @@ async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
12610
12994
  pinnedAt: parsed.pinnedAt
12611
12995
  };
12612
12996
  }
12613
- async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
12997
+ async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
12614
12998
  const path = extensionTrustPath(serverName, dir);
12615
12999
  await mkdir2(dir, { recursive: true, mode: 448 });
12616
13000
  const tmp = `${path}.tmp`;
12617
13001
  await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
12618
13002
  await chmod2(tmp, 384);
12619
- await rename(tmp, path);
13003
+ await rename2(tmp, path);
12620
13004
  }
12621
- var TRUST_NEW_EXTENSION_ENV;
13005
+ var TRUST_DIR_ENV, TRUST_NEW_EXTENSION_ENV;
12622
13006
  var init_extension_trust = __esm({
12623
13007
  "node_modules/@fetchproxy/server/dist/extension-trust.js"() {
12624
13008
  init_identity();
13009
+ TRUST_DIR_ENV = "FETCHPROXY_TRUST_DIR";
12625
13010
  TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
12626
13011
  }
12627
13012
  });
12628
13013
 
12629
13014
  // node_modules/@fetchproxy/server/dist/host.js
13015
+ function envAllowsLocalOrigins(env = process.env) {
13016
+ const raw = env[ALLOW_LOCAL_ORIGINS_ENV];
13017
+ if (raw === void 0 || raw.trim() === "")
13018
+ return false;
13019
+ if (raw.trim() === "1")
13020
+ return true;
13021
+ 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.`);
13022
+ return false;
13023
+ }
13024
+ function originVerdict(origin, allowLocal) {
13025
+ if (origin === void 0)
13026
+ return { allow: true };
13027
+ if (HTTP_ORIGIN_RE.test(origin)) {
13028
+ if (PUBLIC_ORIGIN_RE.test(origin)) {
13029
+ return { allow: false, reason: "origin not allowed", escapable: false };
13030
+ }
13031
+ return allowLocal ? { allow: true } : { allow: false, reason: "localhost page origin not allowed", escapable: true };
13032
+ }
13033
+ if (origin.toLowerCase() === OPAQUE_ORIGIN) {
13034
+ return allowLocal ? { allow: true } : { allow: false, reason: "opaque (null) origin not allowed", escapable: true };
13035
+ }
13036
+ if (EXTENSION_ORIGIN_RE.test(origin))
13037
+ return { allow: true };
13038
+ return { allow: false, reason: "origin not allowed", escapable: false };
13039
+ }
12630
13040
  async function startHost(opts) {
13041
+ const allowLocalOrigins = envAllowsLocalOrigins();
13042
+ if (allowLocalOrigins) {
13043
+ 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.`);
13044
+ }
13045
+ const warnedOrigins = /* @__PURE__ */ new Set();
12631
13046
  const wss = new import_websocket_server.default({
12632
13047
  server: opts.httpServer,
13048
+ maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES,
12633
13049
  verifyClient: (info, cb) => {
12634
13050
  const origin = info.req.headers.origin;
12635
- if (origin && PUBLIC_ORIGIN_RE.test(origin)) {
12636
- cb(false, 403, "origin not allowed");
13051
+ const verdict = originVerdict(origin, allowLocalOrigins);
13052
+ if (verdict.allow) {
13053
+ cb(true);
12637
13054
  return;
12638
13055
  }
12639
- cb(true);
13056
+ if (verdict.escapable && origin !== void 0 && warnedOrigins.size < 8) {
13057
+ if (!warnedOrigins.has(origin)) {
13058
+ warnedOrigins.add(origin);
13059
+ 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.`);
13060
+ }
13061
+ }
13062
+ cb(false, 403, verdict.reason);
12640
13063
  }
12641
13064
  });
12642
- const ownHello = await buildServerHello({
13065
+ const ownHelloBase = {
12643
13066
  // 2.6.0: tell the extension it can say WHY it refused a hello, instead of
12644
13067
  // leaving us to time out and guess.
12645
13068
  accepts: ["hello-rejected"],
@@ -12658,14 +13081,26 @@ async function startHost(opts) {
12658
13081
  sessionStoragePointers: opts.ownSessionStoragePointers,
12659
13082
  domSelectors: opts.ownDomSelectors,
12660
13083
  graphqlOps: opts.ownGraphqlOps
12661
- });
12662
- const ownSessionNonce = fromB64(ownHello.sessionNonce);
13084
+ };
13085
+ const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
12663
13086
  let extensionWs = null;
12664
13087
  const peers = /* @__PURE__ */ new Map();
12665
13088
  const ownInnerListeners = [];
12666
13089
  const disconnectListeners = [];
12667
13090
  const pendingPairListeners = [];
12668
13091
  let ownSession = null;
13092
+ let ownEphemeral = null;
13093
+ function dropOwnSessionAndEphemeral() {
13094
+ if (ownEphemeral)
13095
+ ownEphemeral.priv.fill(0);
13096
+ ownEphemeral = null;
13097
+ return null;
13098
+ }
13099
+ function installOwnEphemeral(next) {
13100
+ if (ownEphemeral)
13101
+ ownEphemeral.priv.fill(0);
13102
+ ownEphemeral = next;
13103
+ }
12669
13104
  let ownPendingPairCode = null;
12670
13105
  let resolveOwnSession;
12671
13106
  let rejectOwnSession;
@@ -12679,21 +13114,74 @@ async function startHost(opts) {
12679
13114
  });
12680
13115
  }
12681
13116
  resetSessionPromise();
13117
+ let ownSessionRefusal = null;
13118
+ function refuseVersionMismatch(ws, raw, identified) {
13119
+ const peek = peekHelloVersion(raw);
13120
+ if (!peek || peek.protocolVersion === PROTOCOL_VERSION)
13121
+ return false;
13122
+ const err = new FetchproxyProtocolVersionError({
13123
+ ourVersion: PROTOCOL_VERSION,
13124
+ theirVersion: peek.protocolVersion,
13125
+ peer: peek.mcpId === null ? "extension" : "mcp"
13126
+ });
13127
+ console.warn(`[fetchproxy] host: ${err.message}`);
13128
+ const nothingBetterAttached = extensionWs === null && extensionClaim === null && ownSession === null;
13129
+ if (err.peer === "extension" && identified !== "peer" && nothingBetterAttached) {
13130
+ ownSessionRefusal = err;
13131
+ rejectOwnSession(err);
13132
+ resetSessionPromise();
13133
+ rejectOwnSession(err);
13134
+ }
13135
+ try {
13136
+ ws.close(1002, protocolVersionCloseReason(err));
13137
+ } catch {
13138
+ }
13139
+ return true;
13140
+ }
12682
13141
  let extensionHello = null;
12683
13142
  let extensionClaim = null;
13143
+ const pairCodeFor = async (hello, mint) => {
13144
+ if (!mint)
13145
+ return null;
13146
+ try {
13147
+ return await pairTranscript(opts.ownIdentity.x25519Pub, fromB64(hello.identityX25519Pub), mint.nonce, fromB64(hello.sessionNonce), mint.pub);
13148
+ } catch (e) {
13149
+ console.error("[fetchproxy] could not derive the pair code:", e);
13150
+ return null;
13151
+ }
13152
+ };
12684
13153
  wss.on("connection", (ws) => {
12685
13154
  let identified = null;
12686
13155
  let peerMcpId = null;
12687
13156
  let closed = false;
12688
13157
  let pinOnReady = false;
13158
+ const handshakeTimer = setTimeout(() => {
13159
+ if (identified || closed)
13160
+ return;
13161
+ try {
13162
+ ws.close(1008, "handshake timeout");
13163
+ } catch {
13164
+ }
13165
+ }, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
13166
+ handshakeTimer.unref?.();
13167
+ ws.on("error", (e) => {
13168
+ console.warn(`[fetchproxy] host: socket error: ${String(e)}`);
13169
+ });
12689
13170
  ws.on("message", async (data) => {
12690
13171
  try {
13172
+ let raw;
13173
+ try {
13174
+ raw = JSON.parse(data.toString());
13175
+ } catch {
13176
+ ws.close(1002, "protocol error");
13177
+ return;
13178
+ }
12691
13179
  let frame;
12692
13180
  try {
12693
- const raw = JSON.parse(data.toString());
12694
13181
  frame = validateFrame(raw);
12695
13182
  } catch {
12696
- ws.close(1002, "protocol error");
13183
+ if (!refuseVersionMismatch(ws, raw, identified))
13184
+ ws.close(1002, "protocol error");
12697
13185
  return;
12698
13186
  }
12699
13187
  if (frame.type === "hello" && frame.role === "extension") {
@@ -12737,25 +13225,43 @@ async function startHost(opts) {
12737
13225
  identified = "extension";
12738
13226
  extensionWs = ws;
12739
13227
  extensionHello = frame;
12740
- if (opts.onPairCode) {
13228
+ if (ownSessionRefusal) {
13229
+ ownSessionRefusal = null;
13230
+ resetSessionPromise();
13231
+ }
13232
+ const mintedKeypair = await generateSessionKeypair();
13233
+ const mintedHello = await buildServerHello({
13234
+ ...ownHelloBase,
13235
+ sessionPub: mintedKeypair.publicKey,
13236
+ // The echo Rule B's gate reads, inside the signed payload: this
13237
+ // hello answers the extension session whose hello triggered it.
13238
+ answersExtNonce: fromB64(frame.sessionNonce)
13239
+ });
13240
+ if (extensionWs !== ws) {
13241
+ mintedKeypair.privateKey.fill(0);
13242
+ return;
13243
+ }
13244
+ const mint = {
13245
+ nonce: fromB64(mintedHello.sessionNonce),
13246
+ pub: mintedKeypair.publicKey
13247
+ };
13248
+ installOwnEphemeral({ ...mint, priv: mintedKeypair.privateKey });
13249
+ for (const slot of peers.values())
13250
+ slot.ws.send(JSON.stringify(frame));
13251
+ ws.send(JSON.stringify(mintedHello));
13252
+ const helloPairCode = await pairCodeFor(frame, mint);
13253
+ if (opts.onPairCode && helloPairCode !== null) {
12741
13254
  try {
12742
- const code = await derivePairCodeFromIds(opts.ownIdentity.x25519Pub, fromB64(frame.identityX25519Pub));
12743
- opts.onPairCode(code);
13255
+ opts.onPairCode(helloPairCode);
12744
13256
  } catch (e) {
12745
13257
  console.error("[fetchproxy] onPairCode threw:", e);
12746
13258
  }
12747
13259
  }
12748
- for (const slot of peers.values())
12749
- slot.ws.send(JSON.stringify(frame));
12750
- ws.send(JSON.stringify(ownHello));
12751
- for (const slot of peers.values()) {
12752
- ws.send(JSON.stringify(slot.helloFrame));
12753
- }
12754
13260
  return;
12755
13261
  }
12756
13262
  if (frame.type === "hello" && frame.role === "server") {
12757
13263
  const peerEdPub = fromB64(frame.identityEd25519Pub);
12758
- const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
13264
+ const peerSigMsg = helloSignaturePayload(frame.mcpId, fromB64(frame.sessionNonce), fromB64(frame.sessionPub), fromB64(frame.answersExtNonce));
12759
13265
  const peerSig = fromB64(frame.sessionSig);
12760
13266
  let peerSigOk = false;
12761
13267
  try {
@@ -12780,10 +13286,12 @@ async function startHost(opts) {
12780
13286
  identified = "peer";
12781
13287
  peerMcpId = frame.mcpId;
12782
13288
  peers.set(frame.mcpId, { ws, helloFrame: frame });
12783
- if (extensionWs)
12784
- extensionWs.send(JSON.stringify(frame));
12785
- if (extensionHello)
13289
+ if (extensionHello && answersNoExtSession(frame.answersExtNonce)) {
12786
13290
  ws.send(JSON.stringify(extensionHello));
13291
+ }
13292
+ if (extensionWs && extensionHello && frame.answersExtNonce === extensionHello.sessionNonce) {
13293
+ extensionWs.send(JSON.stringify(frame));
13294
+ }
12787
13295
  return;
12788
13296
  }
12789
13297
  if (frame.type === "ready") {
@@ -12793,9 +13301,14 @@ async function startHost(opts) {
12793
13301
  ws.close(1002, "ready before extension hello");
12794
13302
  return;
12795
13303
  }
13304
+ const held = ownEphemeral;
13305
+ if (!held || frame.mcpSessionPub !== toB64(held.pub)) {
13306
+ console.warn("[fetchproxy] discarding a ready for a session ephemeral this host no longer holds (the extension reconnected while a hello was in flight)");
13307
+ return;
13308
+ }
12796
13309
  const extEdPub = fromB64(extensionHello.identityEd25519Pub);
12797
13310
  const extNonce = fromB64(extensionHello.sessionNonce);
12798
- const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
13311
+ const msg = readySignaturePayload(held.nonce, extNonce, fromB64(frame.extensionSessionPub), held.pub);
12799
13312
  const sig = fromB64(frame.sessionSig);
12800
13313
  let sigOk = false;
12801
13314
  try {
@@ -12821,8 +13334,9 @@ async function startHost(opts) {
12821
13334
  }
12822
13335
  }
12823
13336
  const extPub = fromB64(frame.extensionSessionPub);
12824
- const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
12825
- const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
13337
+ const shared = await ecdhX25519(held.priv, extPub);
13338
+ const salt = await transcriptHash(held.nonce, extNonce, held.pub, extPub);
13339
+ const key = await hkdfSha256(shared, salt, enc4.encode(HKDF_SESSION_INFO), 32);
12826
13340
  if (extensionWs !== ws)
12827
13341
  return;
12828
13342
  ownSession = new SessionState(key);
@@ -12838,11 +13352,23 @@ async function startHost(opts) {
12838
13352
  if (frame.type === "frame") {
12839
13353
  if (identified === "extension") {
12840
13354
  if (frame.mcpId === opts.ownMcpId) {
12841
- if (!ownSession)
13355
+ const session = ownSession;
13356
+ if (!session)
12842
13357
  return;
12843
- if (!ownSession.acceptInboundSeq(frame.seq))
13358
+ const claim2 = session.claimInboundSeq(frame.seq);
13359
+ if (session.saturationWarningDue(claim2)) {
13360
+ console.warn(`[fetchproxy] ${opts.ownServerName}: dropped an inbound frame (seq ${frame.seq}) unread \u2014 too many frames from the extension are still being opened (inbound claims saturated). Not a replay. Further drops are not logged until the in-flight set drains.`);
13361
+ }
13362
+ if (claim2 !== "ok")
12844
13363
  return;
12845
- const inner = await openEncryptedFrame(ownSession.sessionKey, frame);
13364
+ let inner;
13365
+ try {
13366
+ inner = await openEncryptedFrame(session.sessionKey, frame, "e2s");
13367
+ } catch (e) {
13368
+ session.releaseInboundSeq(frame.seq);
13369
+ throw e;
13370
+ }
13371
+ session.commitInboundSeq(frame.seq);
12846
13372
  ownInnerListeners.forEach((cb) => cb(inner));
12847
13373
  } else {
12848
13374
  const slot = peers.get(frame.mcpId);
@@ -12866,8 +13392,16 @@ async function startHost(opts) {
12866
13392
  }
12867
13393
  if (frame.type === "pair-pending" && identified === "extension") {
12868
13394
  if (frame.mcpId === opts.ownMcpId) {
12869
- ownPendingPairCode = frame.pairCode;
12870
- pendingPairListeners.forEach((cb) => cb(frame.pairCode));
13395
+ const hello = extensionHello;
13396
+ const mint = ownEphemeral;
13397
+ const derived = hello === null ? null : await pairCodeFor(hello, mint);
13398
+ if (derived === null || frame.pairCode !== derived) {
13399
+ 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)");
13400
+ ws.close(1008, "pair code mismatch");
13401
+ return;
13402
+ }
13403
+ ownPendingPairCode = derived;
13404
+ pendingPairListeners.forEach((cb) => cb(derived));
12871
13405
  } else {
12872
13406
  const slot = peers.get(frame.mcpId);
12873
13407
  if (slot)
@@ -12884,6 +13418,7 @@ async function startHost(opts) {
12884
13418
  });
12885
13419
  ws.on("close", () => {
12886
13420
  closed = true;
13421
+ clearTimeout(handshakeTimer);
12887
13422
  if (extensionClaim === ws)
12888
13423
  extensionClaim = null;
12889
13424
  if (identified === "extension" && extensionWs === ws) {
@@ -12892,7 +13427,7 @@ async function startHost(opts) {
12892
13427
  if (!ownSession) {
12893
13428
  rejectOwnSession(new Error("extension disconnected before ready"));
12894
13429
  }
12895
- ownSession = null;
13430
+ ownSession = dropOwnSessionAndEphemeral();
12896
13431
  ownPendingPairCode = null;
12897
13432
  resetSessionPromise();
12898
13433
  disconnectListeners.forEach((cb) => cb());
@@ -12931,7 +13466,8 @@ async function startHost(opts) {
12931
13466
  });
12932
13467
  if (!extensionWs)
12933
13468
  throw new Error("host: no extension connected");
12934
- const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
13469
+ const plaintext = encodeOutboundInnerFrame(opts.ownMcpId, inner);
13470
+ const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), plaintext, "s2e");
12935
13471
  extensionWs.send(JSON.stringify(sealed));
12936
13472
  },
12937
13473
  onOwnInner: (cb) => {
@@ -12948,28 +13484,53 @@ async function startHost(opts) {
12948
13484
  sessionLinked: () => ownSession !== null
12949
13485
  };
12950
13486
  }
12951
- var PUBLIC_ORIGIN_RE, enc2;
13487
+ var HTTP_ORIGIN_RE, PUBLIC_ORIGIN_RE, EXTENSION_ORIGIN_RE, OPAQUE_ORIGIN, ALLOW_LOCAL_ORIGINS_ENV, HANDSHAKE_TIMEOUT_MS, MAX_PAYLOAD_BYTES, enc4;
12952
13488
  var init_host = __esm({
12953
13489
  "node_modules/@fetchproxy/server/dist/host.js"() {
12954
13490
  init_wrapper();
12955
13491
  init_dist();
12956
13492
  init_build_server_hello();
13493
+ init_frame_size();
12957
13494
  init_session();
12958
13495
  init_session_ready();
12959
13496
  init_extension_trust();
12960
- PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
12961
- enc2 = new TextEncoder();
13497
+ HTTP_ORIGIN_RE = /^https?:\/\//i;
13498
+ PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost|\[::1\])(:|$))/i;
13499
+ EXTENSION_ORIGIN_RE = /^(chrome-extension|moz-extension|safari-web-extension):\/\//i;
13500
+ OPAQUE_ORIGIN = "null";
13501
+ ALLOW_LOCAL_ORIGINS_ENV = "FETCHPROXY_ALLOW_LOCAL_ORIGINS";
13502
+ HANDSHAKE_TIMEOUT_MS = 15e3;
13503
+ MAX_PAYLOAD_BYTES = MAX_FRAME_BYTES;
13504
+ enc4 = new TextEncoder();
12962
13505
  }
12963
13506
  });
12964
13507
 
12965
13508
  // node_modules/@fetchproxy/server/dist/peer.js
12966
13509
  async function startPeer(opts) {
12967
- const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`);
13510
+ const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`, {
13511
+ maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES
13512
+ });
12968
13513
  await new Promise((resolve, reject) => {
12969
- ws.once("open", () => resolve());
12970
- ws.once("error", reject);
13514
+ const onOpen = () => {
13515
+ cleanup();
13516
+ resolve();
13517
+ };
13518
+ const onError = (e) => {
13519
+ cleanup();
13520
+ reject(e);
13521
+ };
13522
+ const cleanup = () => {
13523
+ ws.off("open", onOpen);
13524
+ ws.off("error", onError);
13525
+ };
13526
+ ws.once("open", onOpen);
13527
+ ws.once("error", onError);
12971
13528
  });
12972
- const hello = await buildServerHello({
13529
+ ws.on("error", (e) => {
13530
+ console.warn(`[fetchproxy] peer: socket error: ${String(e)}`);
13531
+ });
13532
+ const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
13533
+ const helloBase = {
12973
13534
  identity: opts.identity,
12974
13535
  mcpId: opts.mcpId,
12975
13536
  serverName: opts.serverName,
@@ -12989,15 +13550,39 @@ async function startPeer(opts) {
12989
13550
  // `extensionConnected()` / `sessionLinked()` can go back to false.
12990
13551
  // 2.6.0: `hello-rejected` for the same reason, from the extension.
12991
13552
  accepts: ["extension-disconnected", "hello-rejected"]
13553
+ };
13554
+ const bootstrapKeypair = await generateSessionKeypair();
13555
+ let bootstrapPriv = bootstrapKeypair.privateKey;
13556
+ const hello = await buildServerHello({
13557
+ ...helloBase,
13558
+ sessionPub: bootstrapKeypair.publicKey,
13559
+ // At dial this peer has been told of no extension session, and saying so
13560
+ // is what keeps the host from forwarding a hello whose ephemeral is not
13561
+ // one a session may be opened from.
13562
+ answersExtNonce: ANSWERS_NO_EXT_SESSION
12992
13563
  });
12993
- const sessionNonce = fromB64(hello.sessionNonce);
12994
13564
  ws.send(JSON.stringify(hello));
12995
13565
  const innerListeners = [];
12996
13566
  const renegotiateListeners = [];
12997
13567
  const pendingPairListeners = [];
12998
13568
  const closeListeners = [];
12999
13569
  let session = null;
13570
+ let sessionEphemeral = null;
13571
+ function dropSessionEphemeral() {
13572
+ if (sessionEphemeral)
13573
+ sessionEphemeral.priv.fill(0);
13574
+ sessionEphemeral = null;
13575
+ }
13576
+ function installSessionEphemeral(next) {
13577
+ dropSessionEphemeral();
13578
+ if (bootstrapPriv) {
13579
+ bootstrapPriv.fill(0);
13580
+ bootstrapPriv = null;
13581
+ }
13582
+ sessionEphemeral = next;
13583
+ }
13000
13584
  let pendingPairCode = null;
13585
+ let warnedUnverifiablePairCode = false;
13001
13586
  let resolveFirstReady;
13002
13587
  let rejectFirstReady;
13003
13588
  const sessionPromise = new Promise((resolve, reject) => {
@@ -13006,58 +13591,60 @@ async function startPeer(opts) {
13006
13591
  });
13007
13592
  let extensionHello = null;
13008
13593
  let extensionGone = false;
13009
- let warnedUnverifiable = false;
13010
13594
  let cachedPin = void 0;
13011
- const authenticateExtension = async (sessionSig, extensionSessionPub) => {
13012
- if (!extensionHello) {
13013
- if (opts.requireExtensionIdentity) {
13014
- 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.`);
13015
- return false;
13016
- }
13017
- if (!warnedUnverifiable) {
13018
- warnedUnverifiable = true;
13019
- 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.`);
13020
- }
13021
- return true;
13595
+ const pairCodeFor = async (hello2, mint) => {
13596
+ if (!mint)
13597
+ return null;
13598
+ try {
13599
+ return await pairTranscript(opts.identity.x25519Pub, fromB64(hello2.identityX25519Pub), mint.nonce, fromB64(hello2.sessionNonce), mint.pub);
13600
+ } catch (e) {
13601
+ console.error("[fetchproxy] could not derive the pair code:", e);
13602
+ return null;
13022
13603
  }
13023
- const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
13604
+ };
13605
+ const authenticateExtension = async (sessionSig, extensionSessionPub, held, hello2) => {
13606
+ if (!hello2) {
13607
+ 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.`);
13608
+ return null;
13609
+ }
13610
+ const payload = readySignaturePayload(held.nonce, fromB64(hello2.sessionNonce), fromB64(extensionSessionPub), held.pub);
13024
13611
  let sigOk = false;
13025
13612
  try {
13026
- sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
13613
+ sigOk = await ed25519Verify(fromB64(hello2.identityEd25519Pub), payload, fromB64(sessionSig));
13027
13614
  } catch {
13028
13615
  sigOk = false;
13029
13616
  }
13030
13617
  if (!sigOk) {
13031
13618
  console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
13032
- return false;
13619
+ return null;
13033
13620
  }
13034
13621
  if (cachedPin === void 0) {
13035
13622
  try {
13036
13623
  cachedPin = await opts.extensionTrust.read();
13037
13624
  } catch (e) {
13038
13625
  console.error(`[fetchproxy] ${String(e)}`);
13039
- return false;
13626
+ return null;
13040
13627
  }
13041
13628
  }
13042
13629
  const pin = cachedPin;
13043
13630
  const outcome = decideExtensionTrust({
13044
13631
  pin,
13045
- hello: extensionHello,
13632
+ hello: hello2,
13046
13633
  allowNew: opts.extensionTrust.allowNew,
13047
13634
  serverName: opts.serverName,
13048
13635
  location: opts.extensionTrust.location
13049
13636
  });
13050
13637
  if (outcome.decision === "refused") {
13051
13638
  console.warn(outcome.message);
13052
- return false;
13639
+ return null;
13053
13640
  }
13054
13641
  if (outcome.decision === "replace")
13055
13642
  console.warn(outcome.message);
13056
13643
  if (outcome.decision !== "pinned") {
13057
13644
  try {
13058
13645
  const written = {
13059
- identityX25519Pub: extensionHello.identityX25519Pub,
13060
- identityEd25519Pub: extensionHello.identityEd25519Pub,
13646
+ identityX25519Pub: hello2.identityX25519Pub,
13647
+ identityEd25519Pub: hello2.identityEd25519Pub,
13061
13648
  pinnedAt: Date.now()
13062
13649
  };
13063
13650
  await opts.extensionTrust.write(written);
@@ -13066,32 +13653,62 @@ async function startPeer(opts) {
13066
13653
  console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
13067
13654
  }
13068
13655
  }
13069
- return true;
13656
+ return hello2;
13070
13657
  };
13071
13658
  const onMessage = async (data) => {
13659
+ let raw;
13072
13660
  try {
13073
- const raw = JSON.parse(data.toString());
13661
+ raw = JSON.parse(data.toString());
13074
13662
  const frame = validateFrame(raw);
13075
13663
  if (frame.type === "hello" && frame.role === "extension") {
13076
13664
  extensionHello = frame;
13665
+ const mintedKeypair = await generateSessionKeypair();
13666
+ const mintedHello = await buildServerHello({
13667
+ ...helloBase,
13668
+ sessionPub: mintedKeypair.publicKey,
13669
+ answersExtNonce: fromB64(frame.sessionNonce)
13670
+ });
13671
+ if (extensionHello !== frame) {
13672
+ mintedKeypair.privateKey.fill(0);
13673
+ return;
13674
+ }
13675
+ installSessionEphemeral({
13676
+ nonce: fromB64(mintedHello.sessionNonce),
13677
+ pub: mintedKeypair.publicKey,
13678
+ priv: mintedKeypair.privateKey
13679
+ });
13680
+ ws.send(JSON.stringify(mintedHello));
13077
13681
  return;
13078
13682
  }
13079
13683
  if (frame.type === "extension-disconnected") {
13080
13684
  extensionHello = null;
13081
13685
  extensionGone = true;
13686
+ dropSessionEphemeral();
13082
13687
  pendingPairCode = null;
13083
13688
  return;
13084
13689
  }
13085
13690
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
13086
- const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
13087
- if (!authorised) {
13691
+ const held = sessionEphemeral;
13692
+ const heldExtHello = extensionHello;
13693
+ if (!held || frame.mcpSessionPub !== toB64(held.pub)) {
13694
+ 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)`);
13695
+ return;
13696
+ }
13697
+ const authenticated = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub, held, heldExtHello);
13698
+ if (!authenticated) {
13088
13699
  ws.close(1008, "extension identity refused");
13089
13700
  rejectFirstReady(new Error("peer: extension identity refused"));
13090
13701
  return;
13091
13702
  }
13092
13703
  const extPub = fromB64(frame.extensionSessionPub);
13093
- const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
13094
- const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
13704
+ const extNonce = fromB64(authenticated.sessionNonce);
13705
+ const shared = await ecdhX25519(held.priv, extPub);
13706
+ const salt = await transcriptHash(held.nonce, extNonce, held.pub, extPub);
13707
+ const sessionKey = await hkdfSha256(shared, salt, enc5.encode(HKDF_SESSION_INFO), 32);
13708
+ if (sessionEphemeral !== held) {
13709
+ console.warn(`[fetchproxy] ${opts.serverName}: dropping a session derived against an ephemeral that was superseded while it was being derived`);
13710
+ return;
13711
+ }
13095
13712
  const isRenegotiation = session !== null;
13096
13713
  session = new SessionState(sessionKey);
13097
13714
  extensionGone = false;
@@ -13107,16 +13724,46 @@ async function startPeer(opts) {
13107
13724
  rejectFirstReady(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
13108
13725
  }
13109
13726
  if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
13110
- pendingPairCode = frame.pairCode;
13111
- pendingPairListeners.forEach((cb) => cb(frame.pairCode));
13727
+ const hello2 = extensionHello;
13728
+ const mint = sessionEphemeral;
13729
+ const derived = hello2 === null ? null : await pairCodeFor(hello2, mint);
13730
+ if (derived === null) {
13731
+ if (!warnedUnverifiablePairCode) {
13732
+ warnedUnverifiablePairCode = true;
13733
+ 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.`);
13734
+ }
13735
+ return;
13736
+ }
13737
+ if (frame.pairCode !== derived) {
13738
+ 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)`);
13739
+ ws.close(1008, "pair code mismatch");
13740
+ return;
13741
+ }
13742
+ pendingPairCode = derived;
13743
+ pendingPairListeners.forEach((cb) => cb(derived));
13112
13744
  return;
13113
13745
  }
13114
13746
  if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
13115
- if (!session)
13747
+ const inboundSession = session;
13748
+ if (!inboundSession)
13116
13749
  return;
13117
- if (!session.acceptInboundSeq(frame.seq))
13750
+ const claim2 = inboundSession.claimInboundSeq(frame.seq);
13751
+ if (inboundSession.saturationWarningDue(claim2)) {
13752
+ console.warn(`[fetchproxy] ${opts.serverName}: dropped an inbound frame (seq ${frame.seq}) unread \u2014 too many frames from the extension are still being opened (inbound claims saturated). Not a replay. Further drops are not logged until the in-flight set drains.`);
13753
+ }
13754
+ if (claim2 !== "ok")
13118
13755
  return;
13119
- const result = await openEncryptedFrameDetailed(session.sessionKey, frame);
13756
+ let result;
13757
+ try {
13758
+ result = await openEncryptedFrameDetailed(inboundSession.sessionKey, frame, "e2s");
13759
+ } catch (e) {
13760
+ inboundSession.releaseInboundSeq(frame.seq);
13761
+ throw e;
13762
+ }
13763
+ if (result.stage !== "decrypt-failed")
13764
+ inboundSession.commitInboundSeq(frame.seq);
13765
+ else
13766
+ inboundSession.releaseInboundSeq(frame.seq);
13120
13767
  if (result.stage === "ok") {
13121
13768
  innerListeners.forEach((cb) => cb(result.inner));
13122
13769
  } else if (result.stage === "decrypt-failed") {
@@ -13133,12 +13780,25 @@ async function startPeer(opts) {
13133
13780
  }
13134
13781
  }
13135
13782
  } catch (e) {
13136
- rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
13783
+ const peek = peekHelloVersion(raw);
13784
+ const mismatch = peek && peek.protocolVersion !== PROTOCOL_VERSION ? new FetchproxyProtocolVersionError({
13785
+ ourVersion: PROTOCOL_VERSION,
13786
+ theirVersion: peek.protocolVersion,
13787
+ peer: peek.mcpId === null ? "extension" : "mcp"
13788
+ }) : null;
13789
+ if (mismatch)
13790
+ console.warn(`[fetchproxy] ${opts.serverName}: ${mismatch.message}`);
13791
+ rejectFirstReady(mismatch ?? (e instanceof Error ? e : new Error(String(e))));
13137
13792
  }
13138
13793
  };
13139
13794
  ws.on("message", onMessage);
13140
13795
  ws.once("close", () => {
13141
13796
  rejectFirstReady(new Error("peer WS closed before ready"));
13797
+ dropSessionEphemeral();
13798
+ if (bootstrapPriv) {
13799
+ bootstrapPriv.fill(0);
13800
+ bootstrapPriv = null;
13801
+ }
13142
13802
  closeListeners.forEach((cb) => cb());
13143
13803
  });
13144
13804
  sessionPromise.catch(() => {
@@ -13152,7 +13812,8 @@ async function startPeer(opts) {
13152
13812
  pendingPairCode: () => pendingPairCode
13153
13813
  });
13154
13814
  const s = session;
13155
- const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
13815
+ const plaintext = encodeOutboundInnerFrame(opts.mcpId, inner);
13816
+ const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), plaintext, "s2e");
13156
13817
  ws.send(JSON.stringify(sealed));
13157
13818
  },
13158
13819
  onInner: (cb) => {
@@ -13174,16 +13835,18 @@ async function startPeer(opts) {
13174
13835
  };
13175
13836
  return handle;
13176
13837
  }
13177
- var enc3;
13838
+ var enc5;
13178
13839
  var init_peer = __esm({
13179
13840
  "node_modules/@fetchproxy/server/dist/peer.js"() {
13180
13841
  init_wrapper();
13181
13842
  init_dist();
13182
13843
  init_build_server_hello();
13844
+ init_frame_size();
13845
+ init_host();
13183
13846
  init_session();
13184
13847
  init_session_ready();
13185
13848
  init_extension_trust();
13186
- enc3 = new TextEncoder();
13849
+ enc5 = new TextEncoder();
13187
13850
  }
13188
13851
  });
13189
13852
 
@@ -13225,6 +13888,8 @@ function classifyBridgeError(err) {
13225
13888
  return "session_not_ready";
13226
13889
  if (err instanceof FetchproxyTimeoutError)
13227
13890
  return "timeout";
13891
+ if (err instanceof FetchproxyWaitedError)
13892
+ return "timeout";
13228
13893
  if (err instanceof FetchproxyBridgeDownError)
13229
13894
  return "bridge_down";
13230
13895
  if (err instanceof FetchproxyHttpError)
@@ -13262,7 +13927,19 @@ function envWsHost() {
13262
13927
  return void 0;
13263
13928
  return host;
13264
13929
  }
13265
- function protocolErrorFrom(error61) {
13930
+ function waitedHint(op) {
13931
+ const tail = " This is not a version problem and does not need an update.";
13932
+ if (op === "capture" || op === "capture_redirect") {
13933
+ 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;
13934
+ }
13935
+ if (op === "download") {
13936
+ 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;
13937
+ }
13938
+ return "the extension waited and nothing arrived. Is a tab open on the declared domain and signed in?" + tail;
13939
+ }
13940
+ function protocolErrorFrom(error61, op) {
13941
+ if (error61 === "timeout")
13942
+ return new FetchproxyWaitedError(error61, op);
13266
13943
  if (SCOPE_REJECTION.test(error61))
13267
13944
  return new FetchproxyScopeError(error61);
13268
13945
  if (TAB_OPENING_REJECTION.test(error61))
@@ -13271,6 +13948,11 @@ function protocolErrorFrom(error61) {
13271
13948
  return new FetchproxyNoTabError(error61);
13272
13949
  return new FetchproxyProtocolError(error61);
13273
13950
  }
13951
+ function verbDeadlineMs(transportMs, requestedMs, graceMs = VERB_DEADLINE_GRACE_MS) {
13952
+ if (requestedMs === void 0)
13953
+ return transportMs;
13954
+ return Math.max(requestedMs + graceMs, transportMs);
13955
+ }
13274
13956
  function normalizeCookiePath(path) {
13275
13957
  if (path === void 0 || path === "")
13276
13958
  return void 0;
@@ -13306,7 +13988,7 @@ function assertUrlInDomains(field, url2, domains) {
13306
13988
  const declared = domains.map((d) => JSON.stringify(d)).join(", ");
13307
13989
  throw new Error(`FetchproxyServer: ${field} host "${host}" is outside declared domains [${declared}] \u2014 must be one of them or a subdomain`);
13308
13990
  }
13309
- var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyHintedError, FetchproxyScopeError, FetchproxyNoTabError, FetchproxyTabOpeningError, SCOPE_REJECTION, NO_TAB_REJECTION, TAB_OPENING_REJECTION, FetchproxyTimeoutError, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, FetchproxyServer;
13991
+ 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;
13310
13992
  var init_ws_server = __esm({
13311
13993
  "node_modules/@fetchproxy/server/dist/ws-server.js"() {
13312
13994
  init_dist();
@@ -13388,9 +14070,16 @@ var init_ws_server = __esm({
13388
14070
  this.name = "FetchproxyTabOpeningError";
13389
14071
  }
13390
14072
  };
14073
+ FetchproxyWaitedError = class extends FetchproxyHintedError {
14074
+ constructor(originalError, op) {
14075
+ super(originalError, waitedHint(op));
14076
+ this.name = "FetchproxyWaitedError";
14077
+ }
14078
+ };
13391
14079
  SCOPE_REJECTION = /not in declared/;
13392
14080
  NO_TAB_REJECTION = /no tab matching (?!.*(content script loaded|one is still opening))/;
13393
14081
  TAB_OPENING_REJECTION = /no tab matching .*one is still opening/;
14082
+ VERB_DEADLINE_GRACE_MS = 15e3;
13394
14083
  FetchproxyTimeoutError = class extends FetchproxyProtocolError {
13395
14084
  url;
13396
14085
  timeoutMs;
@@ -13409,16 +14098,20 @@ var init_ws_server = __esm({
13409
14098
  */
13410
14099
  retryAttempted;
13411
14100
  /**
13412
- * 2.4.2+ (#277): what the CALL asked for, when that is more than the
13413
- * deadline that actually fired. A per-call `timeoutMs` is forwarded to the
13414
- * extension but does not raise this server's `fetchTimeoutMs`, so the
13415
- * shorter deadline wins and used to report only its own number, a value
13416
- * the caller never supplied, from an option the message never named.
14101
+ * 2.4.2+ (#277): what the CALL asked for, whenever it asked for anything.
14102
+ *
14103
+ * Until #237 this was recorded only when the call asked for MORE than the
14104
+ * deadline, because asking for more is what the cap silently ignored. The
14105
+ * cap is gone the call's own `timeoutMs` now GOVERNS its deadline
14106
+ * (`verbDeadlineMs`) — so the interesting case inverted: a timeout here
14107
+ * means the bridge did not answer within the window the CALLER chose, and
14108
+ * the message says which number that was and where it came from.
13417
14109
  */
13418
14110
  requestedTimeoutMs;
13419
14111
  constructor(args) {
13420
- const capped = args.requestedTimeoutMs !== void 0 && args.requestedTimeoutMs > args.timeoutMs;
13421
- super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms` + (capped ? ` \u2014 that is this server's fetchTimeoutMs, not the ${args.requestedTimeoutMs}ms this call asked for. A per-call timeoutMs cannot exceed it; raise fetchTimeoutMs on the transport to wait longer.` : ""));
14112
+ const graceMs = args.graceMs ?? VERB_DEADLINE_GRACE_MS;
14113
+ const callerChose = args.requestedTimeoutMs !== void 0 && args.timeoutMs === args.requestedTimeoutMs + graceMs;
14114
+ 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.` : ""));
13422
14115
  this.name = "FetchproxyTimeoutError";
13423
14116
  this.url = args.url;
13424
14117
  this.timeoutMs = args.timeoutMs;
@@ -13516,6 +14209,11 @@ var init_ws_server = __esm({
13516
14209
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
13517
14210
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
13518
14211
  }
14212
+ for (const d of opts.domains) {
14213
+ if (typeof d === "string" && isPublicSuffix(d)) {
14214
+ 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`);
14215
+ }
14216
+ }
13519
14217
  let capabilities;
13520
14218
  if (opts.capabilities === void 0) {
13521
14219
  capabilities = ["fetch"];
@@ -13581,6 +14279,7 @@ var init_ws_server = __esm({
13581
14279
  // back-door is `0` (explicit opt-out) if a caller genuinely wants
13582
14280
  // the legacy hang-forever / fail-once-on-SW-eviction behavior.
13583
14281
  fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
14282
+ verbDeadlineGraceMs: opts.verbDeadlineGraceMs ?? VERB_DEADLINE_GRACE_MS,
13584
14283
  bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
13585
14284
  // 0.10.0+ (#72): keep-alive defaults to 25s — round-3 #71 cohort
13586
14285
  // wave showed every Pattern A consumer was opting into this same
@@ -13599,6 +14298,7 @@ var init_ws_server = __esm({
13599
14298
  keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
13600
14299
  keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
13601
14300
  identityDir: opts.identityDir,
14301
+ trustDir: opts.trustDir,
13602
14302
  allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
13603
14303
  requireExtensionIdentity: opts.requireExtensionIdentity,
13604
14304
  onPairCode: opts.onPairCode
@@ -13875,7 +14575,9 @@ var init_ws_server = __esm({
13875
14575
  }
13876
14576
  /**
13877
14577
  * #208: this MCP's pin on the extension's identity, stored beside its own
13878
- * identity key and so following `identityDir` wherever the caller put it.
14578
+ * identity key and so following `identityDir` wherever the caller put it
14579
+ * unless `trustDir` (or `FETCHPROXY_TRUST_DIR`) says otherwise, which is
14580
+ * what a host that provisions the identity read-only has to say.
13879
14581
  *
13880
14582
  * `allowNewExtensionIdentity` falls back to an environment variable when the
13881
14583
  * caller expressed no opinion, because the thirteen MCPs that construct this
@@ -13887,6 +14589,7 @@ var init_ws_server = __esm({
13887
14589
  return fileExtensionTrust({
13888
14590
  serverName: this.opts.serverName,
13889
14591
  dir: this.opts.identityDir,
14592
+ trustDir: this.opts.trustDir,
13890
14593
  allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
13891
14594
  });
13892
14595
  }
@@ -14049,9 +14752,11 @@ var init_ws_server = __esm({
14049
14752
  return pending;
14050
14753
  }
14051
14754
  async _withVerbTimeout(pending, pendingMap, id, url2, requestedTimeoutMs) {
14052
- const timeoutMs = this.opts.fetchTimeoutMs;
14053
- if (timeoutMs === void 0 || timeoutMs <= 0)
14755
+ const transportMs = this.opts.fetchTimeoutMs;
14756
+ if (transportMs === void 0 || transportMs <= 0)
14054
14757
  return pending;
14758
+ const graceMs = this.opts.verbDeadlineGraceMs;
14759
+ const timeoutMs = verbDeadlineMs(transportMs, requestedTimeoutMs, graceMs);
14055
14760
  let timer;
14056
14761
  const start = Date.now();
14057
14762
  try {
@@ -14067,6 +14772,7 @@ var init_ws_server = __esm({
14067
14772
  port: this.opts.port,
14068
14773
  elapsedMs: Date.now() - start,
14069
14774
  retryAttempted: false,
14775
+ graceMs,
14070
14776
  ...requestedTimeoutMs !== void 0 ? { requestedTimeoutMs } : {}
14071
14777
  }));
14072
14778
  }, timeoutMs);
@@ -14127,9 +14833,9 @@ var init_ws_server = __esm({
14127
14833
  if (opts.subdomain !== void 0)
14128
14834
  assertSubdomainLabel(opts.subdomain);
14129
14835
  const baseDomain = this.resolveBaseDomain(opts.domain);
14130
- const isAbsolute2 = path.startsWith("http://") || path.startsWith("https://");
14836
+ const isAbsolute3 = path.startsWith("http://") || path.startsWith("https://");
14131
14837
  let host;
14132
- if (isAbsolute2) {
14838
+ if (isAbsolute3) {
14133
14839
  try {
14134
14840
  host = new URL(path).host;
14135
14841
  } catch {
@@ -14138,7 +14844,7 @@ var init_ws_server = __esm({
14138
14844
  } else {
14139
14845
  host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
14140
14846
  }
14141
- const url2 = isAbsolute2 ? path : `https://${host}${path}`;
14847
+ const url2 = isAbsolute3 ? path : `https://${host}${path}`;
14142
14848
  assertUrlInDomains("request url", url2, this.opts.domains);
14143
14849
  let tabUrl = `https://${host}/`;
14144
14850
  if (opts.viaTab !== void 0) {
@@ -14627,8 +15333,9 @@ var init_ws_server = __esm({
14627
15333
  this.pendingCapture,
14628
15334
  id,
14629
15335
  `https://${opts.host}${opts.path ?? "/*"}`,
14630
- // #277: not to extend the deadline so a timeout can say the deadline,
14631
- // and not the value asked for here, is what fired.
15336
+ // #237: this GOVERNS the deadline (window + reply grace, floored at the
15337
+ // transport bound). Under #277 it did not, and was passed only so the
15338
+ // timeout could name the number that actually fired.
14632
15339
  opts.timeoutMs
14633
15340
  );
14634
15341
  }
@@ -14720,8 +15427,9 @@ var init_ws_server = __esm({
14720
15427
  this.pendingRedirect,
14721
15428
  id,
14722
15429
  `https://${opts.host}${opts.path ?? "/*"}`,
14723
- // #277: not to extend the deadline so a timeout can say the deadline,
14724
- // and not the value asked for here, is what fired.
15430
+ // #237: this GOVERNS the deadline (window + reply grace, floored at the
15431
+ // transport bound). Under #277 it did not, and was passed only so the
15432
+ // timeout could name the number that actually fired.
14725
15433
  opts.timeoutMs
14726
15434
  );
14727
15435
  }
@@ -14810,10 +15518,11 @@ var init_ws_server = __esm({
14810
15518
  this.pendingDownload,
14811
15519
  id,
14812
15520
  opts.url,
14813
- // #277, same as the two capture verbs: `download` takes a per-call
14814
- // timeoutMs too, so it hits the identical cap and needs the identical
14815
- // explanation. Missing it was the point of #279 "both verbs" was
14816
- // counted off the two call sites in view rather than off the type.
15521
+ // `download` takes a per-call timeoutMs like the two capture verbs, so
15522
+ // it is governed by it like them (#237). It used to hit the identical
15523
+ // CAP and need the identical explanation; missing it then was the point
15524
+ // of #279 — "both verbs" was counted off the two call sites in view
15525
+ // rather than off the type. Three is still the number.
14817
15526
  opts.timeoutMs
14818
15527
  );
14819
15528
  }
@@ -15042,7 +15751,7 @@ var init_ws_server = __esm({
15042
15751
  captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
15043
15752
  }
15044
15753
  } else {
15045
- captureCb.reject(protocolErrorFrom(inner.error));
15754
+ captureCb.reject(protocolErrorFrom(inner.error, "capture"));
15046
15755
  }
15047
15756
  return;
15048
15757
  }
@@ -15056,7 +15765,7 @@ var init_ws_server = __esm({
15056
15765
  redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
15057
15766
  }
15058
15767
  } else {
15059
- redirectCb.reject(protocolErrorFrom(inner.error));
15768
+ redirectCb.reject(protocolErrorFrom(inner.error, "capture_redirect"));
15060
15769
  }
15061
15770
  return;
15062
15771
  }
@@ -15084,7 +15793,7 @@ var init_ws_server = __esm({
15084
15793
  downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
15085
15794
  }
15086
15795
  } else {
15087
- downloadCb.reject(protocolErrorFrom(inner.error));
15796
+ downloadCb.reject(protocolErrorFrom(inner.error, "download"));
15088
15797
  }
15089
15798
  return;
15090
15799
  }
@@ -15187,11 +15896,21 @@ var init_ws_server = __esm({
15187
15896
  /**
15188
15897
  * 2.5.0: the extension link as {@link BridgeSessionState}, read off
15189
15898
  * whichever handle is live. A pending pair code outranks "extension not
15190
- * seen" because the code could only have come from the extension — a
15191
- * peer behind an older host never sees the extension hello but does see
15192
- * pair-pending frames. Where the extension is KNOWN to be gone (a host's
15899
+ * seen" because a code could only have come from a `pair-pending` frame
15900
+ * the extension sent. Where the extension is KNOWN to be gone (a host's
15193
15901
  * socket closed, a peer told by a 2.5.0+ host) the handle has already
15194
15902
  * dropped the code, so the precedence never lies there (#283).
15903
+ *
15904
+ * M1 (bridge review 2026-09-10): a code is now only ever present when the
15905
+ * handle DERIVED it from both identity pubs, and both handles set and clear
15906
+ * that derivation in lockstep with the extension hello — so the precedence
15907
+ * has nothing left to decide: a pair code implies the hello, which implies
15908
+ * `extensionConnected`. A peer behind a pre-1.12.0 host is relayed no
15909
+ * extension hello, cannot derive, and therefore reports
15910
+ * `extension_disconnected` with a null code where it used to report the
15911
+ * wire's number as `pair_pending` — the honest state for a peer that has
15912
+ * never been told an extension is there. That is the trade: a number nobody
15913
+ * can check is worse than no number.
15195
15914
  */
15196
15915
  sessionSnapshot() {
15197
15916
  const handle = this.hostHandle ?? this.peerHandle;
@@ -22806,12 +23525,12 @@ var $ZodE164 = /* @__PURE__ */ $constructor("$ZodE164", (inst, def) => {
22806
23525
  $ZodStringFormat.init(inst, def);
22807
23526
  });
22808
23527
  var CC_SANITIZE = /[- ]/g;
22809
- function isLuhnAlgo(digits) {
22810
- let length = digits.length;
23528
+ function isLuhnAlgo(digits2) {
23529
+ let length = digits2.length;
22811
23530
  let bit = 1;
22812
23531
  let sum = 0;
22813
23532
  while (length) {
22814
- const value = +digits[--length];
23533
+ const value = +digits2[--length];
22815
23534
  bit ^= 1;
22816
23535
  sum += bit ? [0, 2, 4, 6, 8, 1, 3, 5, 7, 9][value] : value;
22817
23536
  }
@@ -37543,8 +38262,8 @@ function hex2(_params) {
37543
38262
  return _stringFormat(ZodCustomStringFormat, "hex", regexes_exports.hex, _params);
37544
38263
  }
37545
38264
  function hash(alg, params) {
37546
- const enc5 = params?.enc ?? "hex";
37547
- const format = `${alg}_${enc5}`;
38265
+ const enc7 = params?.enc ?? "hex";
38266
+ const format = `${alg}_${enc7}`;
37548
38267
  const regex = regexes_exports[format];
37549
38268
  if (!regex)
37550
38269
  throw new Error(`Unrecognized hash format: ${format}`);
@@ -45018,7 +45737,7 @@ var pageSchema = {
45018
45737
  init_errors();
45019
45738
 
45020
45739
  // src/version.ts
45021
- var VERSION = "0.3.1";
45740
+ var VERSION = "0.3.3";
45022
45741
 
45023
45742
  // src/client.ts
45024
45743
  import { dirname, join } from "path";
@@ -45577,7 +46296,7 @@ var schemaEventStatus = external_exports.enum(["all", "live", "draft", "started"
45577
46296
  function qs(params) {
45578
46297
  return params.size > 0 ? `?${params}` : "";
45579
46298
  }
45580
- function enc4(id) {
46299
+ function enc6(id) {
45581
46300
  if (/^\.+$/.test(id)) {
45582
46301
  throw new McpToolError(`Invalid id '${id}'.`, {
45583
46302
  hint: "Ids must be Eventbrite object ids (normally digits), not path segments."
@@ -45659,7 +46378,7 @@ function registerAccountTools(server, deps) {
45659
46378
  if (continuation) params.set("continuation", continuation);
45660
46379
  const data = await client2.request(
45661
46380
  "GET",
45662
- `/organizations/${enc4(org_id)}/events/${qs(params)}`
46381
+ `/organizations/${enc6(org_id)}/events/${qs(params)}`
45663
46382
  );
45664
46383
  return viewResponse(view, data);
45665
46384
  }
@@ -45682,7 +46401,7 @@ function registerAccountTools(server, deps) {
45682
46401
  if (continuation) params.set("continuation", continuation);
45683
46402
  const data = await client2.request(
45684
46403
  "GET",
45685
- `/organizations/${enc4(org_id)}/attendees/${qs(params)}`
46404
+ `/organizations/${enc6(org_id)}/attendees/${qs(params)}`
45686
46405
  );
45687
46406
  return viewResponse(view, data);
45688
46407
  }
@@ -45703,7 +46422,7 @@ function registerAccountTools(server, deps) {
45703
46422
  if (continuation) params.set("continuation", continuation);
45704
46423
  const data = await client2.request(
45705
46424
  "GET",
45706
- `/organizations/${enc4(org_id)}/orders/${qs(params)}`
46425
+ `/organizations/${enc6(org_id)}/orders/${qs(params)}`
45707
46426
  );
45708
46427
  return viewResponse(view, data);
45709
46428
  }
@@ -45743,7 +46462,7 @@ function registerAccountTools(server, deps) {
45743
46462
  if (continuation) params.set("continuation", continuation);
45744
46463
  return viewResponse(
45745
46464
  view,
45746
- await client2.request("GET", `/organizations/${enc4(org_id)}/${noun}/${qs(params)}`)
46465
+ await client2.request("GET", `/organizations/${enc6(org_id)}/${noun}/${qs(params)}`)
45747
46466
  );
45748
46467
  }
45749
46468
  );
@@ -45773,7 +46492,7 @@ function registerAccountTools(server, deps) {
45773
46492
  if (continuation) params.set("continuation", continuation);
45774
46493
  return viewResponse(
45775
46494
  view,
45776
- await client2.request("GET", `/organizations/${enc4(org_id)}/reports/${kind}/${qs(params)}`)
46495
+ await client2.request("GET", `/organizations/${enc6(org_id)}/reports/${kind}/${qs(params)}`)
45777
46496
  );
45778
46497
  }
45779
46498
  );
@@ -45877,7 +46596,7 @@ function registerEventTools(server, deps) {
45877
46596
  if (continuation) params.set("continuation", continuation);
45878
46597
  return viewResponse(
45879
46598
  view,
45880
- await client2.request("GET", `/events/${enc4(event_id)}/attendees/${qs(params)}`)
46599
+ await client2.request("GET", `/events/${enc6(event_id)}/attendees/${qs(params)}`)
45881
46600
  );
45882
46601
  }
45883
46602
  );
@@ -45894,7 +46613,7 @@ function registerEventTools(server, deps) {
45894
46613
  },
45895
46614
  async ({ event_id, attendee_id, view }) => viewResponse(
45896
46615
  view,
45897
- await client2.request("GET", `/events/${enc4(event_id)}/attendees/${enc4(attendee_id)}/`)
46616
+ await client2.request("GET", `/events/${enc6(event_id)}/attendees/${enc6(attendee_id)}/`)
45898
46617
  )
45899
46618
  );
45900
46619
  server.registerTool(
@@ -45917,7 +46636,7 @@ function registerEventTools(server, deps) {
45917
46636
  if (continuation) params.set("continuation", continuation);
45918
46637
  return viewResponse(
45919
46638
  view,
45920
- await client2.request("GET", `/events/${enc4(event_id)}/orders/${qs(params)}`)
46639
+ await client2.request("GET", `/events/${enc6(event_id)}/orders/${qs(params)}`)
45921
46640
  );
45922
46641
  }
45923
46642
  );
@@ -45936,7 +46655,7 @@ function registerEventTools(server, deps) {
45936
46655
  view,
45937
46656
  await client2.request(
45938
46657
  "GET",
45939
- `/events/${enc4(event_id)}/ticket_classes/${enc4(ticket_class_id)}/`
46658
+ `/events/${enc6(event_id)}/ticket_classes/${enc6(ticket_class_id)}/`
45940
46659
  )
45941
46660
  )
45942
46661
  );
@@ -45955,7 +46674,7 @@ function registerEventTools(server, deps) {
45955
46674
  view,
45956
46675
  await client2.request(
45957
46676
  "GET",
45958
- `/events/${enc4(event_id)}/${canned ? "canned_questions" : "questions"}/`
46677
+ `/events/${enc6(event_id)}/${canned ? "canned_questions" : "questions"}/`
45959
46678
  )
45960
46679
  )
45961
46680
  );
@@ -45978,7 +46697,7 @@ function registerLookupTools(server, deps) {
45978
46697
  async ({ order_id, expand, view }) => {
45979
46698
  const params = new URLSearchParams();
45980
46699
  if (expand) params.set("expand", expand);
45981
- return viewResponse(view, await client2.request("GET", `/orders/${enc4(order_id)}/${qs(params)}`));
46700
+ return viewResponse(view, await client2.request("GET", `/orders/${enc6(order_id)}/${qs(params)}`));
45982
46701
  }
45983
46702
  );
45984
46703
  server.registerTool(
@@ -45991,7 +46710,7 @@ function registerLookupTools(server, deps) {
45991
46710
  view: viewArg()
45992
46711
  }
45993
46712
  },
45994
- async ({ venue_id, view }) => viewResponse(view, await client2.request("GET", `/venues/${enc4(venue_id)}/`))
46713
+ async ({ venue_id, view }) => viewResponse(view, await client2.request("GET", `/venues/${enc6(venue_id)}/`))
45995
46714
  );
45996
46715
  server.registerTool(
45997
46716
  "eb_venue_events",
@@ -46011,7 +46730,7 @@ function registerLookupTools(server, deps) {
46011
46730
  if (continuation) params.set("continuation", continuation);
46012
46731
  return viewResponse(
46013
46732
  view,
46014
- await client2.request("GET", `/venues/${enc4(venue_id)}/events/${qs(params)}`)
46733
+ await client2.request("GET", `/venues/${enc6(venue_id)}/events/${qs(params)}`)
46015
46734
  );
46016
46735
  }
46017
46736
  );
@@ -46025,7 +46744,7 @@ function registerLookupTools(server, deps) {
46025
46744
  view: viewArg()
46026
46745
  }
46027
46746
  },
46028
- async ({ organizer_id, view }) => viewResponse(view, await client2.request("GET", `/organizers/${enc4(organizer_id)}/`))
46747
+ async ({ organizer_id, view }) => viewResponse(view, await client2.request("GET", `/organizers/${enc6(organizer_id)}/`))
46029
46748
  );
46030
46749
  server.registerTool(
46031
46750
  "eb_organizer_events",
@@ -46047,7 +46766,7 @@ function registerLookupTools(server, deps) {
46047
46766
  if (continuation) params.set("continuation", continuation);
46048
46767
  return viewResponse(
46049
46768
  view,
46050
- await client2.request("GET", `/organizers/${enc4(organizer_id)}/events/${qs(params)}`)
46769
+ await client2.request("GET", `/organizers/${enc6(organizer_id)}/events/${qs(params)}`)
46051
46770
  );
46052
46771
  }
46053
46772
  );
@@ -46069,7 +46788,7 @@ function registerLookupTools(server, deps) {
46069
46788
  if (continuation) params.set("continuation", continuation);
46070
46789
  return viewResponse(
46071
46790
  view,
46072
- await client2.request("GET", `/series/${enc4(series_id)}/events/${qs(params)}`)
46791
+ await client2.request("GET", `/series/${enc6(series_id)}/events/${qs(params)}`)
46073
46792
  );
46074
46793
  }
46075
46794
  );
@@ -46083,7 +46802,7 @@ function registerLookupTools(server, deps) {
46083
46802
  view: viewArg()
46084
46803
  }
46085
46804
  },
46086
- async ({ user_id, view }) => viewResponse(view, await client2.request("GET", `/users/${enc4(user_id)}/`))
46805
+ async ({ user_id, view }) => viewResponse(view, await client2.request("GET", `/users/${enc6(user_id)}/`))
46087
46806
  );
46088
46807
  }
46089
46808