@chrischall/eventbrite-mcp 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -7287,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
  });
@@ -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,14 +12611,31 @@ 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();
12382
12639
  constructor(sessionKey) {
12383
12640
  this.sessionKey = sessionKey;
12384
12641
  }
@@ -12386,17 +12643,55 @@ var init_session = __esm({
12386
12643
  this.outboundSeq += 1;
12387
12644
  return this.outboundSeq;
12388
12645
  }
12389
- acceptInboundSeq(seq) {
12646
+ /**
12647
+ * Take this seq out of circulation for the frame about to be opened, and
12648
+ * say whether it was available. SYNCHRONOUS and single-shot: call it before
12649
+ * the first `await` of the receive path, or a duplicate frame read in the
12650
+ * same pass will be judged against a counter neither frame has moved yet.
12651
+ *
12652
+ * Answering false means "not this frame's to process" — replayed, or a
12653
+ * duplicate of one still in flight. Every true MUST be answered by exactly
12654
+ * one {@link commitInboundSeq} or {@link releaseInboundSeq}.
12655
+ */
12656
+ claimInboundSeq(seq) {
12390
12657
  if (seq <= this.lastInboundSeq)
12391
12658
  return false;
12392
- this.lastInboundSeq = seq;
12659
+ if (this.inflightInbound.has(seq))
12660
+ return false;
12661
+ if (this.inflightInbound.size >= MAX_INFLIGHT_INBOUND_SEQS)
12662
+ return false;
12663
+ this.inflightInbound.add(seq);
12393
12664
  return true;
12394
12665
  }
12666
+ /**
12667
+ * Give a claim back without spending the seq — for a frame that did not
12668
+ * authenticate, which is a frame that never happened. The counter does not
12669
+ * move, so the genuine frames behind it, carrying lower numbers, are still
12670
+ * accepted. Idempotent, so a caller may release in a `catch` after a commit
12671
+ * it is not sure ran.
12672
+ */
12673
+ releaseInboundSeq(seq) {
12674
+ this.inflightInbound.delete(seq);
12675
+ }
12676
+ /**
12677
+ * Record a seq as spent. Call only for a frame that authenticated. Never
12678
+ * moves the counter backwards, so an out-of-order commit cannot reopen a
12679
+ * seq an earlier one already closed.
12680
+ */
12681
+ commitInboundSeq(seq) {
12682
+ this.inflightInbound.delete(seq);
12683
+ if (seq > this.lastInboundSeq)
12684
+ this.lastInboundSeq = seq;
12685
+ }
12395
12686
  };
12396
12687
  }
12397
12688
  });
12398
12689
 
12399
12690
  // node_modules/@fetchproxy/server/dist/session-ready.js
12691
+ function protocolVersionCloseReason(info) {
12692
+ const far = info.peer === "extension" ? "the extension" : "the other MCP";
12693
+ return `protocol version mismatch: this MCP speaks ${info.ourVersion}, ${far} speaks ${info.theirVersion}`;
12694
+ }
12400
12695
  async function awaitSessionReady(ready, opts) {
12401
12696
  const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
12402
12697
  if (ms <= 0)
@@ -12415,7 +12710,7 @@ async function awaitSessionReady(ready, opts) {
12415
12710
  clearTimeout(timer);
12416
12711
  }
12417
12712
  }
12418
- var SESSION_READY_TIMEOUT_MS, FetchproxySessionNotReadyError, FetchproxyHelloRejectedError;
12713
+ var SESSION_READY_TIMEOUT_MS, FetchproxySessionNotReadyError, FetchproxyHelloRejectedError, MIN_VERSION, FetchproxyProtocolVersionError;
12419
12714
  var init_session_ready = __esm({
12420
12715
  "node_modules/@fetchproxy/server/dist/session-ready.js"() {
12421
12716
  SESSION_READY_TIMEOUT_MS = 3e4;
@@ -12447,6 +12742,23 @@ var init_session_ready = __esm({
12447
12742
  Object.setPrototypeOf(this, new.target.prototype);
12448
12743
  }
12449
12744
  };
12745
+ MIN_VERSION = "3.0.0";
12746
+ FetchproxyProtocolVersionError = class extends Error {
12747
+ /** The protocol version this process speaks. */
12748
+ ourVersion;
12749
+ /** The protocol version the far end announced in the hello we refused. */
12750
+ theirVersion;
12751
+ peer;
12752
+ constructor(info) {
12753
+ const far = info.peer === "extension" ? `the attached browser extension speaks ${info.theirVersion} \u2014 update Transporter (the fetchproxy extension) to ${MIN_VERSION} or later` : `the MCP holding the bridge port speaks ${info.theirVersion} \u2014 upgrade @fetchproxy/server to ${MIN_VERSION} or later in that MCP`;
12754
+ super(`protocol version mismatch: this MCP speaks fetchproxy protocol ${info.ourVersion}, ${far}`);
12755
+ this.name = "FetchproxyProtocolVersionError";
12756
+ this.ourVersion = info.ourVersion;
12757
+ this.theirVersion = info.theirVersion;
12758
+ this.peer = info.peer;
12759
+ Object.setPrototypeOf(this, new.target.prototype);
12760
+ }
12761
+ };
12450
12762
  }
12451
12763
  });
12452
12764
 
@@ -12536,13 +12848,32 @@ var init_identity = __esm({
12536
12848
 
12537
12849
  // node_modules/@fetchproxy/server/dist/extension-trust.js
12538
12850
  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";
12851
+ import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
12852
+ function envTrustDir(env = process.env) {
12853
+ const raw = env[TRUST_DIR_ENV];
12854
+ if (raw === void 0)
12855
+ return void 0;
12856
+ const dir = raw.trim();
12857
+ if (dir !== "" && isAbsolute2(dir))
12858
+ return dir;
12859
+ console.warn(`[fetchproxy] ignoring ${TRUST_DIR_ENV}=${JSON.stringify(raw)}: it is not an absolute path. The extension pin will be written beside the identity instead, which is not where you pointed it \u2014 if that directory is read-only, no pin is ever kept.`);
12860
+ return void 0;
12861
+ }
12862
+ function resolveTrustDir(explicit, identityDir) {
12863
+ if (explicit !== void 0)
12864
+ return explicit;
12865
+ return envTrustDir() ?? identityDir ?? defaultIdentityDir();
12866
+ }
12867
+ function defaultTrustDir() {
12868
+ return resolveTrustDir();
12869
+ }
12540
12870
  function fileExtensionTrust(args) {
12871
+ const dir = resolveTrustDir(args.trustDir, args.dir);
12541
12872
  return {
12542
12873
  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())
12874
+ location: extensionTrustPath(args.serverName, dir),
12875
+ read: () => readExtensionPin(args.serverName, dir),
12876
+ write: (pin) => writeExtensionPin(args.serverName, pin, dir)
12546
12877
  };
12547
12878
  }
12548
12879
  function allowNewExtensionIdentity(explicit, env = process.env) {
@@ -12569,14 +12900,14 @@ function decideExtensionTrust(args) {
12569
12900
  message: `[fetchproxy] ${serverName}: refusing an extension whose identity is not the one this MCP paired with. If you re-installed the extension or moved to another browser, re-pair deliberately: run this MCP once with ${TRUST_NEW_EXTENSION_ENV}=1, or delete ${trustPath}. If you did neither, something else is answering as your browser.`
12570
12901
  };
12571
12902
  }
12572
- function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
12903
+ function extensionTrustPath(serverName, dir = defaultTrustDir()) {
12573
12904
  return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
12574
12905
  }
12575
12906
  function extensionTrustPathHint(serverName) {
12576
12907
  try {
12577
12908
  return extensionTrustPath(serverName);
12578
12909
  } catch {
12579
- return join3(defaultIdentityDir(), "<server-name>.extension-trust.json");
12910
+ return join3(defaultTrustDir(), "<server-name>.extension-trust.json");
12580
12911
  }
12581
12912
  }
12582
12913
  function isPin(x) {
@@ -12585,7 +12916,7 @@ function isPin(x) {
12585
12916
  const r = x;
12586
12917
  return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
12587
12918
  }
12588
- async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
12919
+ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
12589
12920
  const path = extensionTrustPath(serverName, dir);
12590
12921
  let raw;
12591
12922
  try {
@@ -12610,7 +12941,7 @@ async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
12610
12941
  pinnedAt: parsed.pinnedAt
12611
12942
  };
12612
12943
  }
12613
- async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
12944
+ async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
12614
12945
  const path = extensionTrustPath(serverName, dir);
12615
12946
  await mkdir2(dir, { recursive: true, mode: 448 });
12616
12947
  const tmp = `${path}.tmp`;
@@ -12618,28 +12949,67 @@ async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
12618
12949
  await chmod2(tmp, 384);
12619
12950
  await rename(tmp, path);
12620
12951
  }
12621
- var TRUST_NEW_EXTENSION_ENV;
12952
+ var TRUST_DIR_ENV, TRUST_NEW_EXTENSION_ENV;
12622
12953
  var init_extension_trust = __esm({
12623
12954
  "node_modules/@fetchproxy/server/dist/extension-trust.js"() {
12624
12955
  init_identity();
12956
+ TRUST_DIR_ENV = "FETCHPROXY_TRUST_DIR";
12625
12957
  TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
12626
12958
  }
12627
12959
  });
12628
12960
 
12629
12961
  // node_modules/@fetchproxy/server/dist/host.js
12962
+ function envAllowsLocalOrigins(env = process.env) {
12963
+ const raw = env[ALLOW_LOCAL_ORIGINS_ENV];
12964
+ if (raw === void 0 || raw.trim() === "")
12965
+ return false;
12966
+ if (raw.trim() === "1")
12967
+ return true;
12968
+ console.warn(`[fetchproxy] ignoring ${ALLOW_LOCAL_ORIGINS_ENV}=${JSON.stringify(raw)}: the only value that turns it on is 1. Upgrades from a null or localhost origin stay refused.`);
12969
+ return false;
12970
+ }
12971
+ function originVerdict(origin, allowLocal) {
12972
+ if (origin === void 0)
12973
+ return { allow: true };
12974
+ if (HTTP_ORIGIN_RE.test(origin)) {
12975
+ if (PUBLIC_ORIGIN_RE.test(origin)) {
12976
+ return { allow: false, reason: "origin not allowed", escapable: false };
12977
+ }
12978
+ return allowLocal ? { allow: true } : { allow: false, reason: "localhost page origin not allowed", escapable: true };
12979
+ }
12980
+ if (origin.toLowerCase() === OPAQUE_ORIGIN) {
12981
+ return allowLocal ? { allow: true } : { allow: false, reason: "opaque (null) origin not allowed", escapable: true };
12982
+ }
12983
+ if (EXTENSION_ORIGIN_RE.test(origin))
12984
+ return { allow: true };
12985
+ return { allow: false, reason: "origin not allowed", escapable: false };
12986
+ }
12630
12987
  async function startHost(opts) {
12988
+ const allowLocalOrigins = envAllowsLocalOrigins();
12989
+ if (allowLocalOrigins) {
12990
+ console.warn(`[fetchproxy] ${ALLOW_LOCAL_ORIGINS_ENV}=1: accepting WebSocket upgrades from null and localhost page origins. This is a development escape \u2014 any local page the browser has open can reach this concentrator while it is set.`);
12991
+ }
12992
+ const warnedOrigins = /* @__PURE__ */ new Set();
12631
12993
  const wss = new import_websocket_server.default({
12632
12994
  server: opts.httpServer,
12995
+ maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES,
12633
12996
  verifyClient: (info, cb) => {
12634
12997
  const origin = info.req.headers.origin;
12635
- if (origin && PUBLIC_ORIGIN_RE.test(origin)) {
12636
- cb(false, 403, "origin not allowed");
12998
+ const verdict = originVerdict(origin, allowLocalOrigins);
12999
+ if (verdict.allow) {
13000
+ cb(true);
12637
13001
  return;
12638
13002
  }
12639
- cb(true);
13003
+ if (verdict.escapable && origin !== void 0 && warnedOrigins.size < 8) {
13004
+ if (!warnedOrigins.has(origin)) {
13005
+ warnedOrigins.add(origin);
13006
+ console.warn(`[fetchproxy] refused a WebSocket upgrade from ${JSON.stringify(origin)}: ${verdict.reason}. If that is you, developing against the bridge, set ${ALLOW_LOCAL_ORIGINS_ENV}=1 on this MCP \u2014 never on a deployed one.`);
13007
+ }
13008
+ }
13009
+ cb(false, 403, verdict.reason);
12640
13010
  }
12641
13011
  });
12642
- const ownHello = await buildServerHello({
13012
+ const ownHelloBase = {
12643
13013
  // 2.6.0: tell the extension it can say WHY it refused a hello, instead of
12644
13014
  // leaving us to time out and guess.
12645
13015
  accepts: ["hello-rejected"],
@@ -12658,14 +13028,26 @@ async function startHost(opts) {
12658
13028
  sessionStoragePointers: opts.ownSessionStoragePointers,
12659
13029
  domSelectors: opts.ownDomSelectors,
12660
13030
  graphqlOps: opts.ownGraphqlOps
12661
- });
12662
- const ownSessionNonce = fromB64(ownHello.sessionNonce);
13031
+ };
13032
+ const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
12663
13033
  let extensionWs = null;
12664
13034
  const peers = /* @__PURE__ */ new Map();
12665
13035
  const ownInnerListeners = [];
12666
13036
  const disconnectListeners = [];
12667
13037
  const pendingPairListeners = [];
12668
13038
  let ownSession = null;
13039
+ let ownEphemeral = null;
13040
+ function dropOwnSessionAndEphemeral() {
13041
+ if (ownEphemeral)
13042
+ ownEphemeral.priv.fill(0);
13043
+ ownEphemeral = null;
13044
+ return null;
13045
+ }
13046
+ function installOwnEphemeral(next) {
13047
+ if (ownEphemeral)
13048
+ ownEphemeral.priv.fill(0);
13049
+ ownEphemeral = next;
13050
+ }
12669
13051
  let ownPendingPairCode = null;
12670
13052
  let resolveOwnSession;
12671
13053
  let rejectOwnSession;
@@ -12679,21 +13061,74 @@ async function startHost(opts) {
12679
13061
  });
12680
13062
  }
12681
13063
  resetSessionPromise();
13064
+ let ownSessionRefusal = null;
13065
+ function refuseVersionMismatch(ws, raw, identified) {
13066
+ const peek = peekHelloVersion(raw);
13067
+ if (!peek || peek.protocolVersion === PROTOCOL_VERSION)
13068
+ return false;
13069
+ const err = new FetchproxyProtocolVersionError({
13070
+ ourVersion: PROTOCOL_VERSION,
13071
+ theirVersion: peek.protocolVersion,
13072
+ peer: peek.mcpId === null ? "extension" : "mcp"
13073
+ });
13074
+ console.warn(`[fetchproxy] host: ${err.message}`);
13075
+ const nothingBetterAttached = extensionWs === null && extensionClaim === null && ownSession === null;
13076
+ if (err.peer === "extension" && identified !== "peer" && nothingBetterAttached) {
13077
+ ownSessionRefusal = err;
13078
+ rejectOwnSession(err);
13079
+ resetSessionPromise();
13080
+ rejectOwnSession(err);
13081
+ }
13082
+ try {
13083
+ ws.close(1002, protocolVersionCloseReason(err));
13084
+ } catch {
13085
+ }
13086
+ return true;
13087
+ }
12682
13088
  let extensionHello = null;
12683
13089
  let extensionClaim = null;
13090
+ const pairCodeFor = async (hello, mint) => {
13091
+ if (!mint)
13092
+ return null;
13093
+ try {
13094
+ return await pairTranscript(opts.ownIdentity.x25519Pub, fromB64(hello.identityX25519Pub), mint.nonce, fromB64(hello.sessionNonce), mint.pub);
13095
+ } catch (e) {
13096
+ console.error("[fetchproxy] could not derive the pair code:", e);
13097
+ return null;
13098
+ }
13099
+ };
12684
13100
  wss.on("connection", (ws) => {
12685
13101
  let identified = null;
12686
13102
  let peerMcpId = null;
12687
13103
  let closed = false;
12688
13104
  let pinOnReady = false;
13105
+ const handshakeTimer = setTimeout(() => {
13106
+ if (identified || closed)
13107
+ return;
13108
+ try {
13109
+ ws.close(1008, "handshake timeout");
13110
+ } catch {
13111
+ }
13112
+ }, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
13113
+ handshakeTimer.unref?.();
13114
+ ws.on("error", (e) => {
13115
+ console.warn(`[fetchproxy] host: socket error: ${String(e)}`);
13116
+ });
12689
13117
  ws.on("message", async (data) => {
12690
13118
  try {
13119
+ let raw;
13120
+ try {
13121
+ raw = JSON.parse(data.toString());
13122
+ } catch {
13123
+ ws.close(1002, "protocol error");
13124
+ return;
13125
+ }
12691
13126
  let frame;
12692
13127
  try {
12693
- const raw = JSON.parse(data.toString());
12694
13128
  frame = validateFrame(raw);
12695
13129
  } catch {
12696
- ws.close(1002, "protocol error");
13130
+ if (!refuseVersionMismatch(ws, raw, identified))
13131
+ ws.close(1002, "protocol error");
12697
13132
  return;
12698
13133
  }
12699
13134
  if (frame.type === "hello" && frame.role === "extension") {
@@ -12737,25 +13172,43 @@ async function startHost(opts) {
12737
13172
  identified = "extension";
12738
13173
  extensionWs = ws;
12739
13174
  extensionHello = frame;
12740
- if (opts.onPairCode) {
13175
+ if (ownSessionRefusal) {
13176
+ ownSessionRefusal = null;
13177
+ resetSessionPromise();
13178
+ }
13179
+ const mintedKeypair = await generateSessionKeypair();
13180
+ const mintedHello = await buildServerHello({
13181
+ ...ownHelloBase,
13182
+ sessionPub: mintedKeypair.publicKey,
13183
+ // The echo Rule B's gate reads, inside the signed payload: this
13184
+ // hello answers the extension session whose hello triggered it.
13185
+ answersExtNonce: fromB64(frame.sessionNonce)
13186
+ });
13187
+ if (extensionWs !== ws) {
13188
+ mintedKeypair.privateKey.fill(0);
13189
+ return;
13190
+ }
13191
+ const mint = {
13192
+ nonce: fromB64(mintedHello.sessionNonce),
13193
+ pub: mintedKeypair.publicKey
13194
+ };
13195
+ installOwnEphemeral({ ...mint, priv: mintedKeypair.privateKey });
13196
+ for (const slot of peers.values())
13197
+ slot.ws.send(JSON.stringify(frame));
13198
+ ws.send(JSON.stringify(mintedHello));
13199
+ const helloPairCode = await pairCodeFor(frame, mint);
13200
+ if (opts.onPairCode && helloPairCode !== null) {
12741
13201
  try {
12742
- const code = await derivePairCodeFromIds(opts.ownIdentity.x25519Pub, fromB64(frame.identityX25519Pub));
12743
- opts.onPairCode(code);
13202
+ opts.onPairCode(helloPairCode);
12744
13203
  } catch (e) {
12745
13204
  console.error("[fetchproxy] onPairCode threw:", e);
12746
13205
  }
12747
13206
  }
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
13207
  return;
12755
13208
  }
12756
13209
  if (frame.type === "hello" && frame.role === "server") {
12757
13210
  const peerEdPub = fromB64(frame.identityEd25519Pub);
12758
- const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
13211
+ const peerSigMsg = helloSignaturePayload(frame.mcpId, fromB64(frame.sessionNonce), fromB64(frame.sessionPub), fromB64(frame.answersExtNonce));
12759
13212
  const peerSig = fromB64(frame.sessionSig);
12760
13213
  let peerSigOk = false;
12761
13214
  try {
@@ -12780,10 +13233,12 @@ async function startHost(opts) {
12780
13233
  identified = "peer";
12781
13234
  peerMcpId = frame.mcpId;
12782
13235
  peers.set(frame.mcpId, { ws, helloFrame: frame });
12783
- if (extensionWs)
12784
- extensionWs.send(JSON.stringify(frame));
12785
- if (extensionHello)
13236
+ if (extensionHello && answersNoExtSession(frame.answersExtNonce)) {
12786
13237
  ws.send(JSON.stringify(extensionHello));
13238
+ }
13239
+ if (extensionWs && extensionHello && frame.answersExtNonce === extensionHello.sessionNonce) {
13240
+ extensionWs.send(JSON.stringify(frame));
13241
+ }
12787
13242
  return;
12788
13243
  }
12789
13244
  if (frame.type === "ready") {
@@ -12793,9 +13248,14 @@ async function startHost(opts) {
12793
13248
  ws.close(1002, "ready before extension hello");
12794
13249
  return;
12795
13250
  }
13251
+ const held = ownEphemeral;
13252
+ if (!held || frame.mcpSessionPub !== toB64(held.pub)) {
13253
+ console.warn("[fetchproxy] discarding a ready for a session ephemeral this host no longer holds (the extension reconnected while a hello was in flight)");
13254
+ return;
13255
+ }
12796
13256
  const extEdPub = fromB64(extensionHello.identityEd25519Pub);
12797
13257
  const extNonce = fromB64(extensionHello.sessionNonce);
12798
- const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
13258
+ const msg = readySignaturePayload(held.nonce, extNonce, fromB64(frame.extensionSessionPub), held.pub);
12799
13259
  const sig = fromB64(frame.sessionSig);
12800
13260
  let sigOk = false;
12801
13261
  try {
@@ -12821,8 +13281,9 @@ async function startHost(opts) {
12821
13281
  }
12822
13282
  }
12823
13283
  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);
13284
+ const shared = await ecdhX25519(held.priv, extPub);
13285
+ const salt = await transcriptHash(held.nonce, extNonce, held.pub, extPub);
13286
+ const key = await hkdfSha256(shared, salt, enc4.encode(HKDF_SESSION_INFO), 32);
12826
13287
  if (extensionWs !== ws)
12827
13288
  return;
12828
13289
  ownSession = new SessionState(key);
@@ -12838,11 +13299,19 @@ async function startHost(opts) {
12838
13299
  if (frame.type === "frame") {
12839
13300
  if (identified === "extension") {
12840
13301
  if (frame.mcpId === opts.ownMcpId) {
12841
- if (!ownSession)
13302
+ const session = ownSession;
13303
+ if (!session)
12842
13304
  return;
12843
- if (!ownSession.acceptInboundSeq(frame.seq))
13305
+ if (!session.claimInboundSeq(frame.seq))
12844
13306
  return;
12845
- const inner = await openEncryptedFrame(ownSession.sessionKey, frame);
13307
+ let inner;
13308
+ try {
13309
+ inner = await openEncryptedFrame(session.sessionKey, frame, "e2s");
13310
+ } catch (e) {
13311
+ session.releaseInboundSeq(frame.seq);
13312
+ throw e;
13313
+ }
13314
+ session.commitInboundSeq(frame.seq);
12846
13315
  ownInnerListeners.forEach((cb) => cb(inner));
12847
13316
  } else {
12848
13317
  const slot = peers.get(frame.mcpId);
@@ -12866,8 +13335,16 @@ async function startHost(opts) {
12866
13335
  }
12867
13336
  if (frame.type === "pair-pending" && identified === "extension") {
12868
13337
  if (frame.mcpId === opts.ownMcpId) {
12869
- ownPendingPairCode = frame.pairCode;
12870
- pendingPairListeners.forEach((cb) => cb(frame.pairCode));
13338
+ const hello = extensionHello;
13339
+ const mint = ownEphemeral;
13340
+ const derived = hello === null ? null : await pairCodeFor(hello, mint);
13341
+ if (derived === null || frame.pairCode !== derived) {
13342
+ console.error(`[fetchproxy] ${opts.ownServerName}: the extension's pair code (${frame.pairCode}) does not match the one this MCP derived from the pair transcript` + (derived === null ? " (none \u2014 this MCP could not derive its own)" : ` (${derived})`) + " \u2014 refusing to pair (possible MITM between this MCP and the extension)");
13343
+ ws.close(1008, "pair code mismatch");
13344
+ return;
13345
+ }
13346
+ ownPendingPairCode = derived;
13347
+ pendingPairListeners.forEach((cb) => cb(derived));
12871
13348
  } else {
12872
13349
  const slot = peers.get(frame.mcpId);
12873
13350
  if (slot)
@@ -12884,6 +13361,7 @@ async function startHost(opts) {
12884
13361
  });
12885
13362
  ws.on("close", () => {
12886
13363
  closed = true;
13364
+ clearTimeout(handshakeTimer);
12887
13365
  if (extensionClaim === ws)
12888
13366
  extensionClaim = null;
12889
13367
  if (identified === "extension" && extensionWs === ws) {
@@ -12892,7 +13370,7 @@ async function startHost(opts) {
12892
13370
  if (!ownSession) {
12893
13371
  rejectOwnSession(new Error("extension disconnected before ready"));
12894
13372
  }
12895
- ownSession = null;
13373
+ ownSession = dropOwnSessionAndEphemeral();
12896
13374
  ownPendingPairCode = null;
12897
13375
  resetSessionPromise();
12898
13376
  disconnectListeners.forEach((cb) => cb());
@@ -12931,7 +13409,8 @@ async function startHost(opts) {
12931
13409
  });
12932
13410
  if (!extensionWs)
12933
13411
  throw new Error("host: no extension connected");
12934
- const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
13412
+ const plaintext = encodeOutboundInnerFrame(opts.ownMcpId, inner);
13413
+ const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), plaintext, "s2e");
12935
13414
  extensionWs.send(JSON.stringify(sealed));
12936
13415
  },
12937
13416
  onOwnInner: (cb) => {
@@ -12948,17 +13427,24 @@ async function startHost(opts) {
12948
13427
  sessionLinked: () => ownSession !== null
12949
13428
  };
12950
13429
  }
12951
- var PUBLIC_ORIGIN_RE, enc2;
13430
+ var HTTP_ORIGIN_RE, PUBLIC_ORIGIN_RE, EXTENSION_ORIGIN_RE, OPAQUE_ORIGIN, ALLOW_LOCAL_ORIGINS_ENV, HANDSHAKE_TIMEOUT_MS, MAX_PAYLOAD_BYTES, enc4;
12952
13431
  var init_host = __esm({
12953
13432
  "node_modules/@fetchproxy/server/dist/host.js"() {
12954
13433
  init_wrapper();
12955
13434
  init_dist();
12956
13435
  init_build_server_hello();
13436
+ init_frame_size();
12957
13437
  init_session();
12958
13438
  init_session_ready();
12959
13439
  init_extension_trust();
12960
- PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
12961
- enc2 = new TextEncoder();
13440
+ HTTP_ORIGIN_RE = /^https?:\/\//i;
13441
+ PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost|\[::1\])(:|$))/i;
13442
+ EXTENSION_ORIGIN_RE = /^(chrome-extension|moz-extension|safari-web-extension):\/\//i;
13443
+ OPAQUE_ORIGIN = "null";
13444
+ ALLOW_LOCAL_ORIGINS_ENV = "FETCHPROXY_ALLOW_LOCAL_ORIGINS";
13445
+ HANDSHAKE_TIMEOUT_MS = 15e3;
13446
+ MAX_PAYLOAD_BYTES = MAX_FRAME_BYTES;
13447
+ enc4 = new TextEncoder();
12962
13448
  }
12963
13449
  });
12964
13450
 
@@ -12966,10 +13452,26 @@ var init_host = __esm({
12966
13452
  async function startPeer(opts) {
12967
13453
  const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`);
12968
13454
  await new Promise((resolve, reject) => {
12969
- ws.once("open", () => resolve());
12970
- ws.once("error", reject);
13455
+ const onOpen = () => {
13456
+ cleanup();
13457
+ resolve();
13458
+ };
13459
+ const onError = (e) => {
13460
+ cleanup();
13461
+ reject(e);
13462
+ };
13463
+ const cleanup = () => {
13464
+ ws.off("open", onOpen);
13465
+ ws.off("error", onError);
13466
+ };
13467
+ ws.once("open", onOpen);
13468
+ ws.once("error", onError);
12971
13469
  });
12972
- const hello = await buildServerHello({
13470
+ ws.on("error", (e) => {
13471
+ console.warn(`[fetchproxy] peer: socket error: ${String(e)}`);
13472
+ });
13473
+ const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
13474
+ const helloBase = {
12973
13475
  identity: opts.identity,
12974
13476
  mcpId: opts.mcpId,
12975
13477
  serverName: opts.serverName,
@@ -12989,15 +13491,39 @@ async function startPeer(opts) {
12989
13491
  // `extensionConnected()` / `sessionLinked()` can go back to false.
12990
13492
  // 2.6.0: `hello-rejected` for the same reason, from the extension.
12991
13493
  accepts: ["extension-disconnected", "hello-rejected"]
13494
+ };
13495
+ const bootstrapKeypair = await generateSessionKeypair();
13496
+ let bootstrapPriv = bootstrapKeypair.privateKey;
13497
+ const hello = await buildServerHello({
13498
+ ...helloBase,
13499
+ sessionPub: bootstrapKeypair.publicKey,
13500
+ // At dial this peer has been told of no extension session, and saying so
13501
+ // is what keeps the host from forwarding a hello whose ephemeral is not
13502
+ // one a session may be opened from.
13503
+ answersExtNonce: ANSWERS_NO_EXT_SESSION
12992
13504
  });
12993
- const sessionNonce = fromB64(hello.sessionNonce);
12994
13505
  ws.send(JSON.stringify(hello));
12995
13506
  const innerListeners = [];
12996
13507
  const renegotiateListeners = [];
12997
13508
  const pendingPairListeners = [];
12998
13509
  const closeListeners = [];
12999
13510
  let session = null;
13511
+ let sessionEphemeral = null;
13512
+ function dropSessionEphemeral() {
13513
+ if (sessionEphemeral)
13514
+ sessionEphemeral.priv.fill(0);
13515
+ sessionEphemeral = null;
13516
+ }
13517
+ function installSessionEphemeral(next) {
13518
+ dropSessionEphemeral();
13519
+ if (bootstrapPriv) {
13520
+ bootstrapPriv.fill(0);
13521
+ bootstrapPriv = null;
13522
+ }
13523
+ sessionEphemeral = next;
13524
+ }
13000
13525
  let pendingPairCode = null;
13526
+ let warnedUnverifiablePairCode = false;
13001
13527
  let resolveFirstReady;
13002
13528
  let rejectFirstReady;
13003
13529
  const sessionPromise = new Promise((resolve, reject) => {
@@ -13006,58 +13532,60 @@ async function startPeer(opts) {
13006
13532
  });
13007
13533
  let extensionHello = null;
13008
13534
  let extensionGone = false;
13009
- let warnedUnverifiable = false;
13010
13535
  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;
13536
+ const pairCodeFor = async (hello2, mint) => {
13537
+ if (!mint)
13538
+ return null;
13539
+ try {
13540
+ return await pairTranscript(opts.identity.x25519Pub, fromB64(hello2.identityX25519Pub), mint.nonce, fromB64(hello2.sessionNonce), mint.pub);
13541
+ } catch (e) {
13542
+ console.error("[fetchproxy] could not derive the pair code:", e);
13543
+ return null;
13022
13544
  }
13023
- const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
13545
+ };
13546
+ const authenticateExtension = async (sessionSig, extensionSessionPub, held, hello2) => {
13547
+ if (!hello2) {
13548
+ console.error(`[fetchproxy] ${opts.serverName}: no extension hello has been relayed to this peer, so there is nothing to derive a v4 session from \u2014 refusing. Upgrade the MCP holding the bridge port to 3.0.0 or later.`);
13549
+ return null;
13550
+ }
13551
+ const payload = readySignaturePayload(held.nonce, fromB64(hello2.sessionNonce), fromB64(extensionSessionPub), held.pub);
13024
13552
  let sigOk = false;
13025
13553
  try {
13026
- sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
13554
+ sigOk = await ed25519Verify(fromB64(hello2.identityEd25519Pub), payload, fromB64(sessionSig));
13027
13555
  } catch {
13028
13556
  sigOk = false;
13029
13557
  }
13030
13558
  if (!sigOk) {
13031
13559
  console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
13032
- return false;
13560
+ return null;
13033
13561
  }
13034
13562
  if (cachedPin === void 0) {
13035
13563
  try {
13036
13564
  cachedPin = await opts.extensionTrust.read();
13037
13565
  } catch (e) {
13038
13566
  console.error(`[fetchproxy] ${String(e)}`);
13039
- return false;
13567
+ return null;
13040
13568
  }
13041
13569
  }
13042
13570
  const pin = cachedPin;
13043
13571
  const outcome = decideExtensionTrust({
13044
13572
  pin,
13045
- hello: extensionHello,
13573
+ hello: hello2,
13046
13574
  allowNew: opts.extensionTrust.allowNew,
13047
13575
  serverName: opts.serverName,
13048
13576
  location: opts.extensionTrust.location
13049
13577
  });
13050
13578
  if (outcome.decision === "refused") {
13051
13579
  console.warn(outcome.message);
13052
- return false;
13580
+ return null;
13053
13581
  }
13054
13582
  if (outcome.decision === "replace")
13055
13583
  console.warn(outcome.message);
13056
13584
  if (outcome.decision !== "pinned") {
13057
13585
  try {
13058
13586
  const written = {
13059
- identityX25519Pub: extensionHello.identityX25519Pub,
13060
- identityEd25519Pub: extensionHello.identityEd25519Pub,
13587
+ identityX25519Pub: hello2.identityX25519Pub,
13588
+ identityEd25519Pub: hello2.identityEd25519Pub,
13061
13589
  pinnedAt: Date.now()
13062
13590
  };
13063
13591
  await opts.extensionTrust.write(written);
@@ -13066,32 +13594,62 @@ async function startPeer(opts) {
13066
13594
  console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
13067
13595
  }
13068
13596
  }
13069
- return true;
13597
+ return hello2;
13070
13598
  };
13071
13599
  const onMessage = async (data) => {
13600
+ let raw;
13072
13601
  try {
13073
- const raw = JSON.parse(data.toString());
13602
+ raw = JSON.parse(data.toString());
13074
13603
  const frame = validateFrame(raw);
13075
13604
  if (frame.type === "hello" && frame.role === "extension") {
13076
13605
  extensionHello = frame;
13606
+ const mintedKeypair = await generateSessionKeypair();
13607
+ const mintedHello = await buildServerHello({
13608
+ ...helloBase,
13609
+ sessionPub: mintedKeypair.publicKey,
13610
+ answersExtNonce: fromB64(frame.sessionNonce)
13611
+ });
13612
+ if (extensionHello !== frame) {
13613
+ mintedKeypair.privateKey.fill(0);
13614
+ return;
13615
+ }
13616
+ installSessionEphemeral({
13617
+ nonce: fromB64(mintedHello.sessionNonce),
13618
+ pub: mintedKeypair.publicKey,
13619
+ priv: mintedKeypair.privateKey
13620
+ });
13621
+ ws.send(JSON.stringify(mintedHello));
13077
13622
  return;
13078
13623
  }
13079
13624
  if (frame.type === "extension-disconnected") {
13080
13625
  extensionHello = null;
13081
13626
  extensionGone = true;
13627
+ dropSessionEphemeral();
13082
13628
  pendingPairCode = null;
13083
13629
  return;
13084
13630
  }
13085
13631
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
13086
- const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
13087
- if (!authorised) {
13632
+ const held = sessionEphemeral;
13633
+ const heldExtHello = extensionHello;
13634
+ if (!held || frame.mcpSessionPub !== toB64(held.pub)) {
13635
+ console.warn(`[fetchproxy] ${opts.serverName}: discarding a ready for a session ephemeral this peer no longer holds (the extension reconnected while a hello was in flight)`);
13636
+ return;
13637
+ }
13638
+ const authenticated = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub, held, heldExtHello);
13639
+ if (!authenticated) {
13088
13640
  ws.close(1008, "extension identity refused");
13089
13641
  rejectFirstReady(new Error("peer: extension identity refused"));
13090
13642
  return;
13091
13643
  }
13092
13644
  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);
13645
+ const extNonce = fromB64(authenticated.sessionNonce);
13646
+ const shared = await ecdhX25519(held.priv, extPub);
13647
+ const salt = await transcriptHash(held.nonce, extNonce, held.pub, extPub);
13648
+ const sessionKey = await hkdfSha256(shared, salt, enc5.encode(HKDF_SESSION_INFO), 32);
13649
+ if (sessionEphemeral !== held) {
13650
+ console.warn(`[fetchproxy] ${opts.serverName}: dropping a session derived against an ephemeral that was superseded while it was being derived`);
13651
+ return;
13652
+ }
13095
13653
  const isRenegotiation = session !== null;
13096
13654
  session = new SessionState(sessionKey);
13097
13655
  extensionGone = false;
@@ -13107,16 +13665,42 @@ async function startPeer(opts) {
13107
13665
  rejectFirstReady(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
13108
13666
  }
13109
13667
  if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
13110
- pendingPairCode = frame.pairCode;
13111
- pendingPairListeners.forEach((cb) => cb(frame.pairCode));
13668
+ const hello2 = extensionHello;
13669
+ const mint = sessionEphemeral;
13670
+ const derived = hello2 === null ? null : await pairCodeFor(hello2, mint);
13671
+ if (derived === null) {
13672
+ if (!warnedUnverifiablePairCode) {
13673
+ warnedUnverifiablePairCode = true;
13674
+ console.warn(`[fetchproxy] ${opts.serverName}: a pair code arrived that this peer cannot derive for itself (no extension identity has been relayed to it, or no session ephemeral has been minted in answer to one), so it is not being shown. If the MCP holding the bridge port is older than 1.12.0, upgrading it closes this.`);
13675
+ }
13676
+ return;
13677
+ }
13678
+ if (frame.pairCode !== derived) {
13679
+ console.error(`[fetchproxy] ${opts.serverName}: the extension's pair code (${frame.pairCode}) does not match the one this peer derived from the pair transcript (${derived}) \u2014 refusing to pair (possible MITM between this MCP and the extension)`);
13680
+ ws.close(1008, "pair code mismatch");
13681
+ return;
13682
+ }
13683
+ pendingPairCode = derived;
13684
+ pendingPairListeners.forEach((cb) => cb(derived));
13112
13685
  return;
13113
13686
  }
13114
13687
  if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
13115
- if (!session)
13688
+ const inboundSession = session;
13689
+ if (!inboundSession)
13116
13690
  return;
13117
- if (!session.acceptInboundSeq(frame.seq))
13691
+ if (!inboundSession.claimInboundSeq(frame.seq))
13118
13692
  return;
13119
- const result = await openEncryptedFrameDetailed(session.sessionKey, frame);
13693
+ let result;
13694
+ try {
13695
+ result = await openEncryptedFrameDetailed(inboundSession.sessionKey, frame, "e2s");
13696
+ } catch (e) {
13697
+ inboundSession.releaseInboundSeq(frame.seq);
13698
+ throw e;
13699
+ }
13700
+ if (result.stage !== "decrypt-failed")
13701
+ inboundSession.commitInboundSeq(frame.seq);
13702
+ else
13703
+ inboundSession.releaseInboundSeq(frame.seq);
13120
13704
  if (result.stage === "ok") {
13121
13705
  innerListeners.forEach((cb) => cb(result.inner));
13122
13706
  } else if (result.stage === "decrypt-failed") {
@@ -13133,12 +13717,25 @@ async function startPeer(opts) {
13133
13717
  }
13134
13718
  }
13135
13719
  } catch (e) {
13136
- rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
13720
+ const peek = peekHelloVersion(raw);
13721
+ const mismatch = peek && peek.protocolVersion !== PROTOCOL_VERSION ? new FetchproxyProtocolVersionError({
13722
+ ourVersion: PROTOCOL_VERSION,
13723
+ theirVersion: peek.protocolVersion,
13724
+ peer: peek.mcpId === null ? "extension" : "mcp"
13725
+ }) : null;
13726
+ if (mismatch)
13727
+ console.warn(`[fetchproxy] ${opts.serverName}: ${mismatch.message}`);
13728
+ rejectFirstReady(mismatch ?? (e instanceof Error ? e : new Error(String(e))));
13137
13729
  }
13138
13730
  };
13139
13731
  ws.on("message", onMessage);
13140
13732
  ws.once("close", () => {
13141
13733
  rejectFirstReady(new Error("peer WS closed before ready"));
13734
+ dropSessionEphemeral();
13735
+ if (bootstrapPriv) {
13736
+ bootstrapPriv.fill(0);
13737
+ bootstrapPriv = null;
13738
+ }
13142
13739
  closeListeners.forEach((cb) => cb());
13143
13740
  });
13144
13741
  sessionPromise.catch(() => {
@@ -13152,7 +13749,8 @@ async function startPeer(opts) {
13152
13749
  pendingPairCode: () => pendingPairCode
13153
13750
  });
13154
13751
  const s = session;
13155
- const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
13752
+ const plaintext = encodeOutboundInnerFrame(opts.mcpId, inner);
13753
+ const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), plaintext, "s2e");
13156
13754
  ws.send(JSON.stringify(sealed));
13157
13755
  },
13158
13756
  onInner: (cb) => {
@@ -13174,16 +13772,17 @@ async function startPeer(opts) {
13174
13772
  };
13175
13773
  return handle;
13176
13774
  }
13177
- var enc3;
13775
+ var enc5;
13178
13776
  var init_peer = __esm({
13179
13777
  "node_modules/@fetchproxy/server/dist/peer.js"() {
13180
13778
  init_wrapper();
13181
13779
  init_dist();
13182
13780
  init_build_server_hello();
13781
+ init_frame_size();
13183
13782
  init_session();
13184
13783
  init_session_ready();
13185
13784
  init_extension_trust();
13186
- enc3 = new TextEncoder();
13785
+ enc5 = new TextEncoder();
13187
13786
  }
13188
13787
  });
13189
13788
 
@@ -13225,6 +13824,8 @@ function classifyBridgeError(err) {
13225
13824
  return "session_not_ready";
13226
13825
  if (err instanceof FetchproxyTimeoutError)
13227
13826
  return "timeout";
13827
+ if (err instanceof FetchproxyWaitedError)
13828
+ return "timeout";
13228
13829
  if (err instanceof FetchproxyBridgeDownError)
13229
13830
  return "bridge_down";
13230
13831
  if (err instanceof FetchproxyHttpError)
@@ -13262,7 +13863,19 @@ function envWsHost() {
13262
13863
  return void 0;
13263
13864
  return host;
13264
13865
  }
13265
- function protocolErrorFrom(error61) {
13866
+ function waitedHint(op) {
13867
+ const tail = " This is not a version problem and does not need an update.";
13868
+ if (op === "capture" || op === "capture_redirect") {
13869
+ return "nothing matched before the window closed. Is a tab open on the declared domain and signed in? The wait resolves on the NEXT matching request the PAGE makes, so an idle tab times out however long you wait \u2014 interact with the page, or raise the window." + tail;
13870
+ }
13871
+ if (op === "download") {
13872
+ return "the download did not finish before the window closed. Is a tab open on the declared domain and signed in, and is the file still transferring?" + tail;
13873
+ }
13874
+ return "the extension waited and nothing arrived. Is a tab open on the declared domain and signed in?" + tail;
13875
+ }
13876
+ function protocolErrorFrom(error61, op) {
13877
+ if (error61 === "timeout")
13878
+ return new FetchproxyWaitedError(error61, op);
13266
13879
  if (SCOPE_REJECTION.test(error61))
13267
13880
  return new FetchproxyScopeError(error61);
13268
13881
  if (TAB_OPENING_REJECTION.test(error61))
@@ -13271,6 +13884,11 @@ function protocolErrorFrom(error61) {
13271
13884
  return new FetchproxyNoTabError(error61);
13272
13885
  return new FetchproxyProtocolError(error61);
13273
13886
  }
13887
+ function verbDeadlineMs(transportMs, requestedMs, graceMs = VERB_DEADLINE_GRACE_MS) {
13888
+ if (requestedMs === void 0)
13889
+ return transportMs;
13890
+ return Math.max(requestedMs + graceMs, transportMs);
13891
+ }
13274
13892
  function normalizeCookiePath(path) {
13275
13893
  if (path === void 0 || path === "")
13276
13894
  return void 0;
@@ -13306,7 +13924,7 @@ function assertUrlInDomains(field, url2, domains) {
13306
13924
  const declared = domains.map((d) => JSON.stringify(d)).join(", ");
13307
13925
  throw new Error(`FetchproxyServer: ${field} host "${host}" is outside declared domains [${declared}] \u2014 must be one of them or a subdomain`);
13308
13926
  }
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;
13927
+ var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyHintedError, FetchproxyScopeError, FetchproxyNoTabError, FetchproxyTabOpeningError, FetchproxyWaitedError, SCOPE_REJECTION, NO_TAB_REJECTION, TAB_OPENING_REJECTION, VERB_DEADLINE_GRACE_MS, FetchproxyTimeoutError, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, FetchproxyServer;
13310
13928
  var init_ws_server = __esm({
13311
13929
  "node_modules/@fetchproxy/server/dist/ws-server.js"() {
13312
13930
  init_dist();
@@ -13388,9 +14006,16 @@ var init_ws_server = __esm({
13388
14006
  this.name = "FetchproxyTabOpeningError";
13389
14007
  }
13390
14008
  };
14009
+ FetchproxyWaitedError = class extends FetchproxyHintedError {
14010
+ constructor(originalError, op) {
14011
+ super(originalError, waitedHint(op));
14012
+ this.name = "FetchproxyWaitedError";
14013
+ }
14014
+ };
13391
14015
  SCOPE_REJECTION = /not in declared/;
13392
14016
  NO_TAB_REJECTION = /no tab matching (?!.*(content script loaded|one is still opening))/;
13393
14017
  TAB_OPENING_REJECTION = /no tab matching .*one is still opening/;
14018
+ VERB_DEADLINE_GRACE_MS = 15e3;
13394
14019
  FetchproxyTimeoutError = class extends FetchproxyProtocolError {
13395
14020
  url;
13396
14021
  timeoutMs;
@@ -13409,16 +14034,20 @@ var init_ws_server = __esm({
13409
14034
  */
13410
14035
  retryAttempted;
13411
14036
  /**
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.
14037
+ * 2.4.2+ (#277): what the CALL asked for, whenever it asked for anything.
14038
+ *
14039
+ * Until #237 this was recorded only when the call asked for MORE than the
14040
+ * deadline, because asking for more is what the cap silently ignored. The
14041
+ * cap is gone the call's own `timeoutMs` now GOVERNS its deadline
14042
+ * (`verbDeadlineMs`) — so the interesting case inverted: a timeout here
14043
+ * means the bridge did not answer within the window the CALLER chose, and
14044
+ * the message says which number that was and where it came from.
13417
14045
  */
13418
14046
  requestedTimeoutMs;
13419
14047
  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.` : ""));
14048
+ const graceMs = args.graceMs ?? VERB_DEADLINE_GRACE_MS;
14049
+ const callerChose = args.requestedTimeoutMs !== void 0 && args.timeoutMs === args.requestedTimeoutMs + graceMs;
14050
+ super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms` + (callerChose ? ` \u2014 the ${args.requestedTimeoutMs}ms this call asked for, plus ${graceMs}ms for the reply to reach this server. The extension runs its own timer on the value you passed, so a window that is genuinely too short reports what did not happen rather than this.` : ""));
13422
14051
  this.name = "FetchproxyTimeoutError";
13423
14052
  this.url = args.url;
13424
14053
  this.timeoutMs = args.timeoutMs;
@@ -13516,6 +14145,11 @@ var init_ws_server = __esm({
13516
14145
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
13517
14146
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
13518
14147
  }
14148
+ for (const d of opts.domains) {
14149
+ if (typeof d === "string" && isPublicSuffix(d)) {
14150
+ throw new Error(`FetchproxyServer: opts.domains entry ${JSON.stringify(d)} is a public suffix \u2014 declare a registrable domain (e.g. "example.${d}"), not the suffix every site under it shares`);
14151
+ }
14152
+ }
13519
14153
  let capabilities;
13520
14154
  if (opts.capabilities === void 0) {
13521
14155
  capabilities = ["fetch"];
@@ -13581,6 +14215,7 @@ var init_ws_server = __esm({
13581
14215
  // back-door is `0` (explicit opt-out) if a caller genuinely wants
13582
14216
  // the legacy hang-forever / fail-once-on-SW-eviction behavior.
13583
14217
  fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
14218
+ verbDeadlineGraceMs: opts.verbDeadlineGraceMs ?? VERB_DEADLINE_GRACE_MS,
13584
14219
  bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
13585
14220
  // 0.10.0+ (#72): keep-alive defaults to 25s — round-3 #71 cohort
13586
14221
  // wave showed every Pattern A consumer was opting into this same
@@ -13599,6 +14234,7 @@ var init_ws_server = __esm({
13599
14234
  keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
13600
14235
  keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
13601
14236
  identityDir: opts.identityDir,
14237
+ trustDir: opts.trustDir,
13602
14238
  allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
13603
14239
  requireExtensionIdentity: opts.requireExtensionIdentity,
13604
14240
  onPairCode: opts.onPairCode
@@ -13875,7 +14511,9 @@ var init_ws_server = __esm({
13875
14511
  }
13876
14512
  /**
13877
14513
  * #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.
14514
+ * identity key and so following `identityDir` wherever the caller put it
14515
+ * unless `trustDir` (or `FETCHPROXY_TRUST_DIR`) says otherwise, which is
14516
+ * what a host that provisions the identity read-only has to say.
13879
14517
  *
13880
14518
  * `allowNewExtensionIdentity` falls back to an environment variable when the
13881
14519
  * caller expressed no opinion, because the thirteen MCPs that construct this
@@ -13887,6 +14525,7 @@ var init_ws_server = __esm({
13887
14525
  return fileExtensionTrust({
13888
14526
  serverName: this.opts.serverName,
13889
14527
  dir: this.opts.identityDir,
14528
+ trustDir: this.opts.trustDir,
13890
14529
  allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
13891
14530
  });
13892
14531
  }
@@ -14049,9 +14688,11 @@ var init_ws_server = __esm({
14049
14688
  return pending;
14050
14689
  }
14051
14690
  async _withVerbTimeout(pending, pendingMap, id, url2, requestedTimeoutMs) {
14052
- const timeoutMs = this.opts.fetchTimeoutMs;
14053
- if (timeoutMs === void 0 || timeoutMs <= 0)
14691
+ const transportMs = this.opts.fetchTimeoutMs;
14692
+ if (transportMs === void 0 || transportMs <= 0)
14054
14693
  return pending;
14694
+ const graceMs = this.opts.verbDeadlineGraceMs;
14695
+ const timeoutMs = verbDeadlineMs(transportMs, requestedTimeoutMs, graceMs);
14055
14696
  let timer;
14056
14697
  const start = Date.now();
14057
14698
  try {
@@ -14067,6 +14708,7 @@ var init_ws_server = __esm({
14067
14708
  port: this.opts.port,
14068
14709
  elapsedMs: Date.now() - start,
14069
14710
  retryAttempted: false,
14711
+ graceMs,
14070
14712
  ...requestedTimeoutMs !== void 0 ? { requestedTimeoutMs } : {}
14071
14713
  }));
14072
14714
  }, timeoutMs);
@@ -14127,9 +14769,9 @@ var init_ws_server = __esm({
14127
14769
  if (opts.subdomain !== void 0)
14128
14770
  assertSubdomainLabel(opts.subdomain);
14129
14771
  const baseDomain = this.resolveBaseDomain(opts.domain);
14130
- const isAbsolute2 = path.startsWith("http://") || path.startsWith("https://");
14772
+ const isAbsolute3 = path.startsWith("http://") || path.startsWith("https://");
14131
14773
  let host;
14132
- if (isAbsolute2) {
14774
+ if (isAbsolute3) {
14133
14775
  try {
14134
14776
  host = new URL(path).host;
14135
14777
  } catch {
@@ -14138,7 +14780,7 @@ var init_ws_server = __esm({
14138
14780
  } else {
14139
14781
  host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
14140
14782
  }
14141
- const url2 = isAbsolute2 ? path : `https://${host}${path}`;
14783
+ const url2 = isAbsolute3 ? path : `https://${host}${path}`;
14142
14784
  assertUrlInDomains("request url", url2, this.opts.domains);
14143
14785
  let tabUrl = `https://${host}/`;
14144
14786
  if (opts.viaTab !== void 0) {
@@ -14627,8 +15269,9 @@ var init_ws_server = __esm({
14627
15269
  this.pendingCapture,
14628
15270
  id,
14629
15271
  `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.
15272
+ // #237: this GOVERNS the deadline (window + reply grace, floored at the
15273
+ // transport bound). Under #277 it did not, and was passed only so the
15274
+ // timeout could name the number that actually fired.
14632
15275
  opts.timeoutMs
14633
15276
  );
14634
15277
  }
@@ -14720,8 +15363,9 @@ var init_ws_server = __esm({
14720
15363
  this.pendingRedirect,
14721
15364
  id,
14722
15365
  `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.
15366
+ // #237: this GOVERNS the deadline (window + reply grace, floored at the
15367
+ // transport bound). Under #277 it did not, and was passed only so the
15368
+ // timeout could name the number that actually fired.
14725
15369
  opts.timeoutMs
14726
15370
  );
14727
15371
  }
@@ -14810,10 +15454,11 @@ var init_ws_server = __esm({
14810
15454
  this.pendingDownload,
14811
15455
  id,
14812
15456
  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.
15457
+ // `download` takes a per-call timeoutMs like the two capture verbs, so
15458
+ // it is governed by it like them (#237). It used to hit the identical
15459
+ // CAP and need the identical explanation; missing it then was the point
15460
+ // of #279 — "both verbs" was counted off the two call sites in view
15461
+ // rather than off the type. Three is still the number.
14817
15462
  opts.timeoutMs
14818
15463
  );
14819
15464
  }
@@ -15042,7 +15687,7 @@ var init_ws_server = __esm({
15042
15687
  captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
15043
15688
  }
15044
15689
  } else {
15045
- captureCb.reject(protocolErrorFrom(inner.error));
15690
+ captureCb.reject(protocolErrorFrom(inner.error, "capture"));
15046
15691
  }
15047
15692
  return;
15048
15693
  }
@@ -15056,7 +15701,7 @@ var init_ws_server = __esm({
15056
15701
  redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
15057
15702
  }
15058
15703
  } else {
15059
- redirectCb.reject(protocolErrorFrom(inner.error));
15704
+ redirectCb.reject(protocolErrorFrom(inner.error, "capture_redirect"));
15060
15705
  }
15061
15706
  return;
15062
15707
  }
@@ -15084,7 +15729,7 @@ var init_ws_server = __esm({
15084
15729
  downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
15085
15730
  }
15086
15731
  } else {
15087
- downloadCb.reject(protocolErrorFrom(inner.error));
15732
+ downloadCb.reject(protocolErrorFrom(inner.error, "download"));
15088
15733
  }
15089
15734
  return;
15090
15735
  }
@@ -15187,11 +15832,21 @@ var init_ws_server = __esm({
15187
15832
  /**
15188
15833
  * 2.5.0: the extension link as {@link BridgeSessionState}, read off
15189
15834
  * 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
15835
+ * seen" because a code could only have come from a `pair-pending` frame
15836
+ * the extension sent. Where the extension is KNOWN to be gone (a host's
15193
15837
  * socket closed, a peer told by a 2.5.0+ host) the handle has already
15194
15838
  * dropped the code, so the precedence never lies there (#283).
15839
+ *
15840
+ * M1 (bridge review 2026-09-10): a code is now only ever present when the
15841
+ * handle DERIVED it from both identity pubs, and both handles set and clear
15842
+ * that derivation in lockstep with the extension hello — so the precedence
15843
+ * has nothing left to decide: a pair code implies the hello, which implies
15844
+ * `extensionConnected`. A peer behind a pre-1.12.0 host is relayed no
15845
+ * extension hello, cannot derive, and therefore reports
15846
+ * `extension_disconnected` with a null code where it used to report the
15847
+ * wire's number as `pair_pending` — the honest state for a peer that has
15848
+ * never been told an extension is there. That is the trade: a number nobody
15849
+ * can check is worse than no number.
15195
15850
  */
15196
15851
  sessionSnapshot() {
15197
15852
  const handle = this.hostHandle ?? this.peerHandle;
@@ -22806,12 +23461,12 @@ var $ZodE164 = /* @__PURE__ */ $constructor("$ZodE164", (inst, def) => {
22806
23461
  $ZodStringFormat.init(inst, def);
22807
23462
  });
22808
23463
  var CC_SANITIZE = /[- ]/g;
22809
- function isLuhnAlgo(digits) {
22810
- let length = digits.length;
23464
+ function isLuhnAlgo(digits2) {
23465
+ let length = digits2.length;
22811
23466
  let bit = 1;
22812
23467
  let sum = 0;
22813
23468
  while (length) {
22814
- const value = +digits[--length];
23469
+ const value = +digits2[--length];
22815
23470
  bit ^= 1;
22816
23471
  sum += bit ? [0, 2, 4, 6, 8, 1, 3, 5, 7, 9][value] : value;
22817
23472
  }
@@ -37543,8 +38198,8 @@ function hex2(_params) {
37543
38198
  return _stringFormat(ZodCustomStringFormat, "hex", regexes_exports.hex, _params);
37544
38199
  }
37545
38200
  function hash(alg, params) {
37546
- const enc5 = params?.enc ?? "hex";
37547
- const format = `${alg}_${enc5}`;
38201
+ const enc7 = params?.enc ?? "hex";
38202
+ const format = `${alg}_${enc7}`;
37548
38203
  const regex = regexes_exports[format];
37549
38204
  if (!regex)
37550
38205
  throw new Error(`Unrecognized hash format: ${format}`);
@@ -45018,7 +45673,7 @@ var pageSchema = {
45018
45673
  init_errors();
45019
45674
 
45020
45675
  // src/version.ts
45021
- var VERSION = "0.3.1";
45676
+ var VERSION = "0.3.2";
45022
45677
 
45023
45678
  // src/client.ts
45024
45679
  import { dirname, join } from "path";
@@ -45577,7 +46232,7 @@ var schemaEventStatus = external_exports.enum(["all", "live", "draft", "started"
45577
46232
  function qs(params) {
45578
46233
  return params.size > 0 ? `?${params}` : "";
45579
46234
  }
45580
- function enc4(id) {
46235
+ function enc6(id) {
45581
46236
  if (/^\.+$/.test(id)) {
45582
46237
  throw new McpToolError(`Invalid id '${id}'.`, {
45583
46238
  hint: "Ids must be Eventbrite object ids (normally digits), not path segments."
@@ -45659,7 +46314,7 @@ function registerAccountTools(server, deps) {
45659
46314
  if (continuation) params.set("continuation", continuation);
45660
46315
  const data = await client2.request(
45661
46316
  "GET",
45662
- `/organizations/${enc4(org_id)}/events/${qs(params)}`
46317
+ `/organizations/${enc6(org_id)}/events/${qs(params)}`
45663
46318
  );
45664
46319
  return viewResponse(view, data);
45665
46320
  }
@@ -45682,7 +46337,7 @@ function registerAccountTools(server, deps) {
45682
46337
  if (continuation) params.set("continuation", continuation);
45683
46338
  const data = await client2.request(
45684
46339
  "GET",
45685
- `/organizations/${enc4(org_id)}/attendees/${qs(params)}`
46340
+ `/organizations/${enc6(org_id)}/attendees/${qs(params)}`
45686
46341
  );
45687
46342
  return viewResponse(view, data);
45688
46343
  }
@@ -45703,7 +46358,7 @@ function registerAccountTools(server, deps) {
45703
46358
  if (continuation) params.set("continuation", continuation);
45704
46359
  const data = await client2.request(
45705
46360
  "GET",
45706
- `/organizations/${enc4(org_id)}/orders/${qs(params)}`
46361
+ `/organizations/${enc6(org_id)}/orders/${qs(params)}`
45707
46362
  );
45708
46363
  return viewResponse(view, data);
45709
46364
  }
@@ -45743,7 +46398,7 @@ function registerAccountTools(server, deps) {
45743
46398
  if (continuation) params.set("continuation", continuation);
45744
46399
  return viewResponse(
45745
46400
  view,
45746
- await client2.request("GET", `/organizations/${enc4(org_id)}/${noun}/${qs(params)}`)
46401
+ await client2.request("GET", `/organizations/${enc6(org_id)}/${noun}/${qs(params)}`)
45747
46402
  );
45748
46403
  }
45749
46404
  );
@@ -45773,7 +46428,7 @@ function registerAccountTools(server, deps) {
45773
46428
  if (continuation) params.set("continuation", continuation);
45774
46429
  return viewResponse(
45775
46430
  view,
45776
- await client2.request("GET", `/organizations/${enc4(org_id)}/reports/${kind}/${qs(params)}`)
46431
+ await client2.request("GET", `/organizations/${enc6(org_id)}/reports/${kind}/${qs(params)}`)
45777
46432
  );
45778
46433
  }
45779
46434
  );
@@ -45877,7 +46532,7 @@ function registerEventTools(server, deps) {
45877
46532
  if (continuation) params.set("continuation", continuation);
45878
46533
  return viewResponse(
45879
46534
  view,
45880
- await client2.request("GET", `/events/${enc4(event_id)}/attendees/${qs(params)}`)
46535
+ await client2.request("GET", `/events/${enc6(event_id)}/attendees/${qs(params)}`)
45881
46536
  );
45882
46537
  }
45883
46538
  );
@@ -45894,7 +46549,7 @@ function registerEventTools(server, deps) {
45894
46549
  },
45895
46550
  async ({ event_id, attendee_id, view }) => viewResponse(
45896
46551
  view,
45897
- await client2.request("GET", `/events/${enc4(event_id)}/attendees/${enc4(attendee_id)}/`)
46552
+ await client2.request("GET", `/events/${enc6(event_id)}/attendees/${enc6(attendee_id)}/`)
45898
46553
  )
45899
46554
  );
45900
46555
  server.registerTool(
@@ -45917,7 +46572,7 @@ function registerEventTools(server, deps) {
45917
46572
  if (continuation) params.set("continuation", continuation);
45918
46573
  return viewResponse(
45919
46574
  view,
45920
- await client2.request("GET", `/events/${enc4(event_id)}/orders/${qs(params)}`)
46575
+ await client2.request("GET", `/events/${enc6(event_id)}/orders/${qs(params)}`)
45921
46576
  );
45922
46577
  }
45923
46578
  );
@@ -45936,7 +46591,7 @@ function registerEventTools(server, deps) {
45936
46591
  view,
45937
46592
  await client2.request(
45938
46593
  "GET",
45939
- `/events/${enc4(event_id)}/ticket_classes/${enc4(ticket_class_id)}/`
46594
+ `/events/${enc6(event_id)}/ticket_classes/${enc6(ticket_class_id)}/`
45940
46595
  )
45941
46596
  )
45942
46597
  );
@@ -45955,7 +46610,7 @@ function registerEventTools(server, deps) {
45955
46610
  view,
45956
46611
  await client2.request(
45957
46612
  "GET",
45958
- `/events/${enc4(event_id)}/${canned ? "canned_questions" : "questions"}/`
46613
+ `/events/${enc6(event_id)}/${canned ? "canned_questions" : "questions"}/`
45959
46614
  )
45960
46615
  )
45961
46616
  );
@@ -45978,7 +46633,7 @@ function registerLookupTools(server, deps) {
45978
46633
  async ({ order_id, expand, view }) => {
45979
46634
  const params = new URLSearchParams();
45980
46635
  if (expand) params.set("expand", expand);
45981
- return viewResponse(view, await client2.request("GET", `/orders/${enc4(order_id)}/${qs(params)}`));
46636
+ return viewResponse(view, await client2.request("GET", `/orders/${enc6(order_id)}/${qs(params)}`));
45982
46637
  }
45983
46638
  );
45984
46639
  server.registerTool(
@@ -45991,7 +46646,7 @@ function registerLookupTools(server, deps) {
45991
46646
  view: viewArg()
45992
46647
  }
45993
46648
  },
45994
- async ({ venue_id, view }) => viewResponse(view, await client2.request("GET", `/venues/${enc4(venue_id)}/`))
46649
+ async ({ venue_id, view }) => viewResponse(view, await client2.request("GET", `/venues/${enc6(venue_id)}/`))
45995
46650
  );
45996
46651
  server.registerTool(
45997
46652
  "eb_venue_events",
@@ -46011,7 +46666,7 @@ function registerLookupTools(server, deps) {
46011
46666
  if (continuation) params.set("continuation", continuation);
46012
46667
  return viewResponse(
46013
46668
  view,
46014
- await client2.request("GET", `/venues/${enc4(venue_id)}/events/${qs(params)}`)
46669
+ await client2.request("GET", `/venues/${enc6(venue_id)}/events/${qs(params)}`)
46015
46670
  );
46016
46671
  }
46017
46672
  );
@@ -46025,7 +46680,7 @@ function registerLookupTools(server, deps) {
46025
46680
  view: viewArg()
46026
46681
  }
46027
46682
  },
46028
- async ({ organizer_id, view }) => viewResponse(view, await client2.request("GET", `/organizers/${enc4(organizer_id)}/`))
46683
+ async ({ organizer_id, view }) => viewResponse(view, await client2.request("GET", `/organizers/${enc6(organizer_id)}/`))
46029
46684
  );
46030
46685
  server.registerTool(
46031
46686
  "eb_organizer_events",
@@ -46047,7 +46702,7 @@ function registerLookupTools(server, deps) {
46047
46702
  if (continuation) params.set("continuation", continuation);
46048
46703
  return viewResponse(
46049
46704
  view,
46050
- await client2.request("GET", `/organizers/${enc4(organizer_id)}/events/${qs(params)}`)
46705
+ await client2.request("GET", `/organizers/${enc6(organizer_id)}/events/${qs(params)}`)
46051
46706
  );
46052
46707
  }
46053
46708
  );
@@ -46069,7 +46724,7 @@ function registerLookupTools(server, deps) {
46069
46724
  if (continuation) params.set("continuation", continuation);
46070
46725
  return viewResponse(
46071
46726
  view,
46072
- await client2.request("GET", `/series/${enc4(series_id)}/events/${qs(params)}`)
46727
+ await client2.request("GET", `/series/${enc6(series_id)}/events/${qs(params)}`)
46073
46728
  );
46074
46729
  }
46075
46730
  );
@@ -46083,7 +46738,7 @@ function registerLookupTools(server, deps) {
46083
46738
  view: viewArg()
46084
46739
  }
46085
46740
  },
46086
- async ({ user_id, view }) => viewResponse(view, await client2.request("GET", `/users/${enc4(user_id)}/`))
46741
+ async ({ user_id, view }) => viewResponse(view, await client2.request("GET", `/users/${enc6(user_id)}/`))
46087
46742
  );
46088
46743
  }
46089
46744