ofw-mcp 2.10.0 → 2.10.1

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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "OurFamilyWizard tools for Claude Code",
9
- "version": "2.10.0"
9
+ "version": "2.10.1"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -14,7 +14,7 @@
14
14
  "displayName": "OurFamilyWizard",
15
15
  "source": "./",
16
16
  "description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
17
- "version": "2.10.0",
17
+ "version": "2.10.1",
18
18
  "author": {
19
19
  "name": "Chris Chall"
20
20
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ofw",
3
3
  "displayName": "OurFamilyWizard",
4
- "version": "2.10.0",
4
+ "version": "2.10.1",
5
5
  "description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall"
package/dist/bundle.js CHANGED
@@ -3651,7 +3651,12 @@ var require_fast_uri = __commonJS({
3651
3651
  }
3652
3652
  function resolve2(baseURI, relativeURI, options) {
3653
3653
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3654
- const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3654
+ const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3655
+ const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
3656
+ if (baseMalformed || relativeMalformed) {
3657
+ throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
3658
+ }
3659
+ const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
3655
3660
  schemelessOptions.skipEscape = true;
3656
3661
  return serialize(resolved, schemelessOptions);
3657
3662
  }
@@ -3777,6 +3782,7 @@ var require_fast_uri = __commonJS({
3777
3782
  }
3778
3783
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3779
3784
  var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
3785
+ var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
3780
3786
  function getParseError(parsed, matches) {
3781
3787
  if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
3782
3788
  return 'URI path must start with "/" when authority is present.';
@@ -3811,6 +3817,20 @@ var require_fast_uri = __commonJS({
3811
3817
  parsed.error = "URI authority must not contain a literal backslash.";
3812
3818
  malformedAuthorityOrPort = true;
3813
3819
  }
3820
+ const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
3821
+ if (introducerMatch !== null) {
3822
+ const region = introducerMatch[1];
3823
+ const normalizedRegion = region.replace(/[\t\n\r]/g, "");
3824
+ if (normalizedRegion.length >= 2) {
3825
+ if (normalizedRegion.slice(0, 2) !== "//") {
3826
+ parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
3827
+ malformedAuthorityOrPort = true;
3828
+ } else if (region.length !== normalizedRegion.length) {
3829
+ parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
3830
+ malformedAuthorityOrPort = true;
3831
+ }
3832
+ }
3833
+ }
3814
3834
  const matches = uri.match(URI_PARSE);
3815
3835
  if (matches) {
3816
3836
  parsed.scheme = matches[1];
@@ -26725,17 +26745,33 @@ function normalizeObjectSchema(schema) {
26725
26745
  }
26726
26746
  return void 0;
26727
26747
  }
26748
+ function getDotPath(path) {
26749
+ if (path.length === 0) {
26750
+ return "object root";
26751
+ }
26752
+ return path.reduce((acc, seg, index) => {
26753
+ if (index === 0) {
26754
+ return String(seg);
26755
+ }
26756
+ if (typeof seg === "number") {
26757
+ return `${acc}[${seg}]`;
26758
+ }
26759
+ return `${acc}.${seg}`;
26760
+ }, "");
26761
+ }
26728
26762
  function getParseErrorMessage(error51) {
26729
26763
  if (error51 && typeof error51 === "object") {
26764
+ if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
26765
+ return error51.issues.map((i) => {
26766
+ if (!i.path?.length) {
26767
+ return i.message;
26768
+ }
26769
+ return `${i.message} at ${getDotPath(i.path)}`;
26770
+ }).join("\n");
26771
+ }
26730
26772
  if ("message" in error51 && typeof error51.message === "string") {
26731
26773
  return error51.message;
26732
26774
  }
26733
- if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
26734
- const firstIssue = error51.issues[0];
26735
- if (firstIssue && typeof firstIssue === "object" && "message" in firstIssue) {
26736
- return String(firstIssue.message);
26737
- }
26738
- }
26739
26775
  try {
26740
26776
  return JSON.stringify(error51);
26741
26777
  } catch {
@@ -33350,16 +33386,7 @@ var Server = class extends Protocol {
33350
33386
  if (!methodSchema) {
33351
33387
  throw new Error("Schema is missing a method literal");
33352
33388
  }
33353
- let methodValue;
33354
- if (isZ4Schema(methodSchema)) {
33355
- const v4Schema = methodSchema;
33356
- const v4Def = v4Schema._zod?.def;
33357
- methodValue = v4Def?.value ?? v4Schema.value;
33358
- } else {
33359
- const v3Schema = methodSchema;
33360
- const legacyDef = v3Schema._def;
33361
- methodValue = legacyDef?.value ?? v3Schema.value;
33362
- }
33389
+ const methodValue = getLiteralValue(methodSchema);
33363
33390
  if (typeof methodValue !== "string") {
33364
33391
  throw new Error("Schema method literal must be a string");
33365
33392
  }
@@ -34547,8 +34574,17 @@ var EMPTY_COMPLETION_RESULT = {
34547
34574
  import process3 from "node:process";
34548
34575
 
34549
34576
  // node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
34577
+ var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
34550
34578
  var ReadBuffer = class {
34579
+ constructor(options) {
34580
+ this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE;
34581
+ }
34551
34582
  append(chunk2) {
34583
+ const newSize = (this._buffer?.length ?? 0) + chunk2.length;
34584
+ if (newSize > this._maxBufferSize) {
34585
+ this.clear();
34586
+ throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`);
34587
+ }
34552
34588
  this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
34553
34589
  }
34554
34590
  readMessage() {
@@ -34576,18 +34612,24 @@ function serializeMessage(message) {
34576
34612
 
34577
34613
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
34578
34614
  var StdioServerTransport = class {
34579
- constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
34615
+ constructor(_stdin = process3.stdin, _stdout = process3.stdout, options) {
34580
34616
  this._stdin = _stdin;
34581
34617
  this._stdout = _stdout;
34582
- this._readBuffer = new ReadBuffer();
34583
34618
  this._started = false;
34584
34619
  this._ondata = (chunk2) => {
34585
- this._readBuffer.append(chunk2);
34586
- this.processReadBuffer();
34620
+ try {
34621
+ this._readBuffer.append(chunk2);
34622
+ this.processReadBuffer();
34623
+ } catch (error51) {
34624
+ this.onerror?.(error51);
34625
+ this.close().catch(() => {
34626
+ });
34627
+ }
34587
34628
  };
34588
34629
  this._onerror = (error51) => {
34589
34630
  this.onerror?.(error51);
34590
34631
  };
34632
+ this._readBuffer = new ReadBuffer({ maxBufferSize: options?.maxBufferSize });
34591
34633
  }
34592
34634
  /**
34593
34635
  * Starts listening for messages on stdin.
@@ -34914,12 +34956,19 @@ var TokenManager = class {
34914
34956
  };
34915
34957
 
34916
34958
  // src/client.ts
34917
- import { dirname, join as join3 } from "path";
34959
+ import { dirname, join as join4 } from "path";
34918
34960
  import { fileURLToPath } from "url";
34919
34961
 
34920
34962
  // node_modules/@fetchproxy/protocol/dist/frames.js
34921
- var PROTOCOL_VERSION = 2;
34963
+ var PROTOCOL_VERSION = 3;
34922
34964
  var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
34965
+ function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
34966
+ const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
34967
+ out.set(mcpHelloNonce, 0);
34968
+ out.set(extHelloNonce, mcpHelloNonce.length);
34969
+ out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
34970
+ return out;
34971
+ }
34923
34972
  var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
34924
34973
  "fetch",
34925
34974
  "read_cookies",
@@ -34930,7 +34979,8 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
34930
34979
  "read_indexed_db",
34931
34980
  "read_dom",
34932
34981
  "download",
34933
- "graphql"
34982
+ "graphql",
34983
+ "write_cookies"
34934
34984
  ]);
34935
34985
 
34936
34986
  // node_modules/@fetchproxy/protocol/dist/mcp-id.js
@@ -35041,6 +35091,15 @@ function assertHttpUrl(x, label) {
35041
35091
  throw new ProtocolError(`${label}: must be http(s), got ${u.protocol}`);
35042
35092
  }
35043
35093
  }
35094
+ function assertCookiePath(x, label) {
35095
+ assertString(x, label);
35096
+ if (!x.startsWith("/") || x.startsWith("//")) {
35097
+ throw new ProtocolError(`${label}: must be an absolute path like "/campus"`);
35098
+ }
35099
+ if (x.includes("?") || x.includes("#") || x.includes("\\")) {
35100
+ throw new ProtocolError(`${label}: must not contain a query, fragment, or backslash`);
35101
+ }
35102
+ }
35044
35103
  function assertHttpsOriginOnly(x, label) {
35045
35104
  assertString(x, label);
35046
35105
  let u;
@@ -35485,13 +35544,43 @@ function validateInnerRequest(raw) {
35485
35544
  }
35486
35545
  assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
35487
35546
  assertNonEmptyKeyArray(raw.init.keys, "inner.init.keys");
35547
+ if (raw.init.path !== void 0)
35548
+ assertCookiePath(raw.init.path, "inner.init.path");
35488
35549
  for (const k of Object.keys(raw.init)) {
35489
- if (k !== "origin" && k !== "keys") {
35550
+ if (k !== "origin" && k !== "keys" && k !== "path") {
35490
35551
  throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_cookies`);
35491
35552
  }
35492
35553
  }
35493
35554
  return raw;
35494
35555
  }
35556
+ if (raw.op === "write_cookies") {
35557
+ assertObject(raw.init, "inner.init");
35558
+ assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
35559
+ if (!Array.isArray(raw.init.cookies) || raw.init.cookies.length === 0) {
35560
+ throw new ProtocolError("inner.init.cookies: must be a non-empty array");
35561
+ }
35562
+ for (const [i, entry] of raw.init.cookies.entries()) {
35563
+ assertObject(entry, `inner.init.cookies[${i}]`);
35564
+ assertString(entry.name, `inner.init.cookies[${i}].name`);
35565
+ if (!SCOPE_KEY_RE.test(entry.name)) {
35566
+ throw new ProtocolError(`inner.init.cookies[${i}].name: invalid key ${JSON.stringify(entry.name)}`);
35567
+ }
35568
+ assertString(entry.value, `inner.init.cookies[${i}].value`);
35569
+ for (const k of Object.keys(entry)) {
35570
+ if (k !== "name" && k !== "value") {
35571
+ throw new ProtocolError(`inner.init.cookies[${i}]: unexpected field ${JSON.stringify(k)}`);
35572
+ }
35573
+ }
35574
+ }
35575
+ if (raw.init.path !== void 0)
35576
+ assertCookiePath(raw.init.path, "inner.init.path");
35577
+ for (const k of Object.keys(raw.init)) {
35578
+ if (k !== "origin" && k !== "cookies" && k !== "path") {
35579
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on write_cookies`);
35580
+ }
35581
+ }
35582
+ return raw;
35583
+ }
35495
35584
  if (raw.op === "read_local_storage" || raw.op === "read_session_storage") {
35496
35585
  assertObject(raw.init, "inner.init");
35497
35586
  if (raw.init.origin === void 0) {
@@ -35678,7 +35767,7 @@ function validateInnerRequest(raw) {
35678
35767
  }
35679
35768
  return raw;
35680
35769
  }
35681
- throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download", "graphql_query"; got ${JSON.stringify(raw.op)}`);
35770
+ throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download", "graphql_query", "write_cookies"; got ${JSON.stringify(raw.op)}`);
35682
35771
  }
35683
35772
  function assertNonEmptyKeyArray(value, label) {
35684
35773
  if (!Array.isArray(value)) {
@@ -35739,6 +35828,18 @@ function validateInnerResponse(raw) {
35739
35828
  }
35740
35829
  return raw;
35741
35830
  }
35831
+ if (op === "write_cookies") {
35832
+ if (raw.written === void 0) {
35833
+ throw new ProtocolError("inner.written: missing on write_cookies response");
35834
+ }
35835
+ if (!Array.isArray(raw.written)) {
35836
+ throw new ProtocolError("inner.written: must be an array");
35837
+ }
35838
+ for (const [i, name] of raw.written.entries()) {
35839
+ assertString(name, `inner.written[${i}]`);
35840
+ }
35841
+ return raw;
35842
+ }
35742
35843
  if (op === "read_local_storage" || op === "read_session_storage") {
35743
35844
  if (raw.values === void 0) {
35744
35845
  throw new ProtocolError(`inner.values: missing on ${String(op)} response`);
@@ -36216,6 +36317,148 @@ async function awaitSessionReady(ready, opts) {
36216
36317
  }
36217
36318
  }
36218
36319
 
36320
+ // node_modules/@fetchproxy/server/dist/extension-trust.js
36321
+ import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
36322
+ import { join as join3 } from "node:path";
36323
+
36324
+ // node_modules/@fetchproxy/server/dist/identity.js
36325
+ import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
36326
+ import { join as join2 } from "node:path";
36327
+ import { homedir as homedir2 } from "node:os";
36328
+ var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
36329
+ var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
36330
+ function defaultIdentityDir() {
36331
+ return join2(homedir2(), ".fetchproxy", "identity");
36332
+ }
36333
+ function safeIdentityFileBase(serverName) {
36334
+ if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
36335
+ throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
36336
+ }
36337
+ return serverName.replace(/\//g, "_");
36338
+ }
36339
+ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
36340
+ const safeFile = safeIdentityFileBase(serverName);
36341
+ const path = join2(dir, `${safeFile}.json`);
36342
+ await mkdir(dir, { recursive: true, mode: 448 });
36343
+ try {
36344
+ const raw = await readFile(path, "utf8");
36345
+ const j2 = JSON.parse(raw);
36346
+ return {
36347
+ x25519Priv: fromB64(j2.x25519Priv),
36348
+ x25519Pub: fromB64(j2.x25519Pub),
36349
+ ed25519Priv: fromB64(j2.ed25519Priv),
36350
+ ed25519Pub: fromB64(j2.ed25519Pub),
36351
+ createdAt: j2.createdAt
36352
+ };
36353
+ } catch (e) {
36354
+ if (e.code !== "ENOENT")
36355
+ throw e;
36356
+ }
36357
+ const x = await generateX25519();
36358
+ const ed = await generateEd25519();
36359
+ const id = {
36360
+ x25519Priv: x.privateKey,
36361
+ x25519Pub: x.publicKey,
36362
+ ed25519Priv: ed.privateKey,
36363
+ ed25519Pub: ed.publicKey,
36364
+ createdAt: Date.now()
36365
+ };
36366
+ const j = {
36367
+ x25519Priv: toB64(id.x25519Priv),
36368
+ x25519Pub: toB64(id.x25519Pub),
36369
+ ed25519Priv: toB64(id.ed25519Priv),
36370
+ ed25519Pub: toB64(id.ed25519Pub),
36371
+ createdAt: id.createdAt
36372
+ };
36373
+ await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
36374
+ await chmod(path, 384);
36375
+ return id;
36376
+ }
36377
+
36378
+ // node_modules/@fetchproxy/server/dist/extension-trust.js
36379
+ function fileExtensionTrust(args) {
36380
+ return {
36381
+ allowNew: args.allowNew,
36382
+ location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
36383
+ read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
36384
+ write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
36385
+ };
36386
+ }
36387
+ var TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
36388
+ function allowNewExtensionIdentity(explicit, env = process.env) {
36389
+ if (explicit !== void 0)
36390
+ return explicit;
36391
+ return env[TRUST_NEW_EXTENSION_ENV] === "1";
36392
+ }
36393
+ function decideExtensionTrust(args) {
36394
+ const { pin, hello, allowNew, serverName } = args;
36395
+ if (!pin)
36396
+ return { decision: "first-use" };
36397
+ if (pin.identityX25519Pub === hello.identityX25519Pub && pin.identityEd25519Pub === hello.identityEd25519Pub) {
36398
+ return { decision: "pinned" };
36399
+ }
36400
+ const trustPath = args.location ?? extensionTrustPathHint(serverName);
36401
+ if (allowNew) {
36402
+ return {
36403
+ decision: "replace",
36404
+ message: `[fetchproxy] ${serverName}: accepting a NEW extension identity because ${TRUST_NEW_EXTENSION_ENV}=1 \u2014 re-pinning. Unset it once the browser you expect is connected.`
36405
+ };
36406
+ }
36407
+ return {
36408
+ decision: "refused",
36409
+ 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.`
36410
+ };
36411
+ }
36412
+ function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
36413
+ return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
36414
+ }
36415
+ function extensionTrustPathHint(serverName) {
36416
+ try {
36417
+ return extensionTrustPath(serverName);
36418
+ } catch {
36419
+ return join3(defaultIdentityDir(), "<server-name>.extension-trust.json");
36420
+ }
36421
+ }
36422
+ function isPin(x) {
36423
+ if (!x || typeof x !== "object")
36424
+ return false;
36425
+ const r = x;
36426
+ return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
36427
+ }
36428
+ async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
36429
+ const path = extensionTrustPath(serverName, dir);
36430
+ let raw;
36431
+ try {
36432
+ raw = await readFile2(path, "utf8");
36433
+ } catch (e) {
36434
+ if (e.code === "ENOENT")
36435
+ return null;
36436
+ throw e;
36437
+ }
36438
+ let parsed;
36439
+ try {
36440
+ parsed = JSON.parse(raw);
36441
+ } catch {
36442
+ throw new Error(`unreadable extension pin at ${path} (not JSON) \u2014 delete it to re-pair`);
36443
+ }
36444
+ if (!isPin(parsed)) {
36445
+ throw new Error(`unreadable extension pin at ${path} (wrong shape) \u2014 delete it to re-pair`);
36446
+ }
36447
+ return {
36448
+ identityX25519Pub: parsed.identityX25519Pub,
36449
+ identityEd25519Pub: parsed.identityEd25519Pub,
36450
+ pinnedAt: parsed.pinnedAt
36451
+ };
36452
+ }
36453
+ async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
36454
+ const path = extensionTrustPath(serverName, dir);
36455
+ await mkdir2(dir, { recursive: true, mode: 448 });
36456
+ const tmp = `${path}.tmp`;
36457
+ await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
36458
+ await chmod2(tmp, 384);
36459
+ await rename(tmp, path);
36460
+ }
36461
+
36219
36462
  // node_modules/@fetchproxy/server/dist/host.js
36220
36463
  var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
36221
36464
  var enc2 = new TextEncoder();
@@ -36269,9 +36512,12 @@ async function startHost(opts) {
36269
36512
  }
36270
36513
  resetSessionPromise();
36271
36514
  let extensionHello = null;
36515
+ let extensionClaim = null;
36272
36516
  wss.on("connection", (ws) => {
36273
36517
  let identified = null;
36274
36518
  let peerMcpId = null;
36519
+ let closed = false;
36520
+ let pinOnReady = false;
36275
36521
  ws.on("message", async (data) => {
36276
36522
  try {
36277
36523
  let frame;
@@ -36283,10 +36529,43 @@ async function startHost(opts) {
36283
36529
  return;
36284
36530
  }
36285
36531
  if (frame.type === "hello" && frame.role === "extension") {
36286
- if (extensionWs) {
36532
+ if (extensionWs || extensionClaim) {
36287
36533
  ws.close(1008, "extension already connected");
36288
36534
  return;
36289
36535
  }
36536
+ extensionClaim = ws;
36537
+ let pin;
36538
+ try {
36539
+ pin = await opts.extensionTrust.read();
36540
+ } catch (e) {
36541
+ console.error(`[fetchproxy] ${String(e)}`);
36542
+ if (extensionClaim === ws)
36543
+ extensionClaim = null;
36544
+ ws.close(1008, "extension pin unreadable");
36545
+ return;
36546
+ }
36547
+ const outcome = decideExtensionTrust({
36548
+ pin,
36549
+ hello: frame,
36550
+ allowNew: opts.extensionTrust.allowNew,
36551
+ serverName: opts.ownServerName,
36552
+ location: opts.extensionTrust.location
36553
+ });
36554
+ if (outcome.decision === "refused") {
36555
+ console.warn(outcome.message);
36556
+ if (extensionClaim === ws)
36557
+ extensionClaim = null;
36558
+ ws.close(1008, "extension identity is not the pinned one");
36559
+ return;
36560
+ }
36561
+ if (outcome.decision === "replace")
36562
+ console.warn(outcome.message);
36563
+ if (closed || ws.readyState !== import_websocket.default.OPEN) {
36564
+ if (extensionClaim === ws)
36565
+ extensionClaim = null;
36566
+ return;
36567
+ }
36568
+ pinOnReady = outcome.decision !== "pinned";
36290
36569
  identified = "extension";
36291
36570
  extensionWs = ws;
36292
36571
  extensionHello = frame;
@@ -36298,6 +36577,8 @@ async function startHost(opts) {
36298
36577
  console.error("[fetchproxy] onPairCode threw:", e);
36299
36578
  }
36300
36579
  }
36580
+ for (const slot of peers.values())
36581
+ slot.ws.send(JSON.stringify(frame));
36301
36582
  ws.send(JSON.stringify(ownHello));
36302
36583
  for (const slot of peers.values()) {
36303
36584
  ws.send(JSON.stringify(slot.helloFrame));
@@ -36333,6 +36614,8 @@ async function startHost(opts) {
36333
36614
  peers.set(frame.mcpId, { ws, helloFrame: frame });
36334
36615
  if (extensionWs)
36335
36616
  extensionWs.send(JSON.stringify(frame));
36617
+ if (extensionHello)
36618
+ ws.send(JSON.stringify(extensionHello));
36336
36619
  return;
36337
36620
  }
36338
36621
  if (frame.type === "ready") {
@@ -36344,7 +36627,7 @@ async function startHost(opts) {
36344
36627
  }
36345
36628
  const extEdPub = fromB64(extensionHello.identityEd25519Pub);
36346
36629
  const extNonce = fromB64(extensionHello.sessionNonce);
36347
- const msg = concatBytes(ownSessionNonce, extNonce);
36630
+ const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
36348
36631
  const sig = fromB64(frame.sessionSig);
36349
36632
  let sigOk = false;
36350
36633
  try {
@@ -36357,6 +36640,18 @@ async function startHost(opts) {
36357
36640
  ws.close(1008, "extension session signature invalid");
36358
36641
  return;
36359
36642
  }
36643
+ if (pinOnReady) {
36644
+ pinOnReady = false;
36645
+ try {
36646
+ await opts.extensionTrust.write({
36647
+ identityX25519Pub: extensionHello.identityX25519Pub,
36648
+ identityEd25519Pub: extensionHello.identityEd25519Pub,
36649
+ pinnedAt: Date.now()
36650
+ });
36651
+ } catch (e) {
36652
+ console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
36653
+ }
36654
+ }
36360
36655
  const extPub = fromB64(frame.extensionSessionPub);
36361
36656
  const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
36362
36657
  const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
@@ -36410,6 +36705,9 @@ async function startHost(opts) {
36410
36705
  }
36411
36706
  });
36412
36707
  ws.on("close", () => {
36708
+ closed = true;
36709
+ if (extensionClaim === ws)
36710
+ extensionClaim = null;
36413
36711
  if (identified === "extension" && extensionWs === ws) {
36414
36712
  extensionWs = null;
36415
36713
  extensionHello = null;
@@ -36500,11 +36798,84 @@ async function startPeer(opts) {
36500
36798
  resolveFirstReady = resolve2;
36501
36799
  rejectFirstReady = reject;
36502
36800
  });
36801
+ let extensionHello = null;
36802
+ let warnedUnverifiable = false;
36803
+ let cachedPin = void 0;
36804
+ const authenticateExtension = async (sessionSig, extensionSessionPub) => {
36805
+ if (!extensionHello) {
36806
+ if (opts.requireExtensionIdentity) {
36807
+ 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.`);
36808
+ return false;
36809
+ }
36810
+ if (!warnedUnverifiable) {
36811
+ warnedUnverifiable = true;
36812
+ 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.`);
36813
+ }
36814
+ return true;
36815
+ }
36816
+ const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
36817
+ let sigOk = false;
36818
+ try {
36819
+ sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
36820
+ } catch {
36821
+ sigOk = false;
36822
+ }
36823
+ if (!sigOk) {
36824
+ console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
36825
+ return false;
36826
+ }
36827
+ if (cachedPin === void 0) {
36828
+ try {
36829
+ cachedPin = await opts.extensionTrust.read();
36830
+ } catch (e) {
36831
+ console.error(`[fetchproxy] ${String(e)}`);
36832
+ return false;
36833
+ }
36834
+ }
36835
+ const pin = cachedPin;
36836
+ const outcome = decideExtensionTrust({
36837
+ pin,
36838
+ hello: extensionHello,
36839
+ allowNew: opts.extensionTrust.allowNew,
36840
+ serverName: opts.serverName,
36841
+ location: opts.extensionTrust.location
36842
+ });
36843
+ if (outcome.decision === "refused") {
36844
+ console.warn(outcome.message);
36845
+ return false;
36846
+ }
36847
+ if (outcome.decision === "replace")
36848
+ console.warn(outcome.message);
36849
+ if (outcome.decision !== "pinned") {
36850
+ try {
36851
+ const written = {
36852
+ identityX25519Pub: extensionHello.identityX25519Pub,
36853
+ identityEd25519Pub: extensionHello.identityEd25519Pub,
36854
+ pinnedAt: Date.now()
36855
+ };
36856
+ await opts.extensionTrust.write(written);
36857
+ cachedPin = written;
36858
+ } catch (e) {
36859
+ console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
36860
+ }
36861
+ }
36862
+ return true;
36863
+ };
36503
36864
  const onMessage = async (data) => {
36504
36865
  try {
36505
36866
  const raw = JSON.parse(data.toString());
36506
36867
  const frame = validateFrame(raw);
36868
+ if (frame.type === "hello" && frame.role === "extension") {
36869
+ extensionHello = frame;
36870
+ return;
36871
+ }
36507
36872
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
36873
+ const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
36874
+ if (!authorised) {
36875
+ ws.close(1008, "extension identity refused");
36876
+ rejectFirstReady(new Error("peer: extension identity refused"));
36877
+ return;
36878
+ }
36508
36879
  const extPub = fromB64(frame.extensionSessionPub);
36509
36880
  const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
36510
36881
  const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
@@ -36585,57 +36956,6 @@ async function startPeer(opts) {
36585
36956
  return handle;
36586
36957
  }
36587
36958
 
36588
- // node_modules/@fetchproxy/server/dist/identity.js
36589
- import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
36590
- import { join as join2 } from "node:path";
36591
- import { homedir as homedir2 } from "node:os";
36592
- var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
36593
- var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
36594
- function defaultIdentityDir() {
36595
- return join2(homedir2(), ".fetchproxy", "identity");
36596
- }
36597
- async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
36598
- if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
36599
- throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
36600
- }
36601
- const safeFile = serverName.replace(/\//g, "_");
36602
- const path = join2(dir, `${safeFile}.json`);
36603
- await mkdir(dir, { recursive: true, mode: 448 });
36604
- try {
36605
- const raw = await readFile(path, "utf8");
36606
- const j2 = JSON.parse(raw);
36607
- return {
36608
- x25519Priv: fromB64(j2.x25519Priv),
36609
- x25519Pub: fromB64(j2.x25519Pub),
36610
- ed25519Priv: fromB64(j2.ed25519Priv),
36611
- ed25519Pub: fromB64(j2.ed25519Pub),
36612
- createdAt: j2.createdAt
36613
- };
36614
- } catch (e) {
36615
- if (e.code !== "ENOENT")
36616
- throw e;
36617
- }
36618
- const x = await generateX25519();
36619
- const ed = await generateEd25519();
36620
- const id = {
36621
- x25519Priv: x.privateKey,
36622
- x25519Pub: x.publicKey,
36623
- ed25519Priv: ed.privateKey,
36624
- ed25519Pub: ed.publicKey,
36625
- createdAt: Date.now()
36626
- };
36627
- const j = {
36628
- x25519Priv: toB64(id.x25519Priv),
36629
- x25519Pub: toB64(id.x25519Pub),
36630
- ed25519Priv: toB64(id.ed25519Priv),
36631
- ed25519Pub: toB64(id.ed25519Pub),
36632
- createdAt: id.createdAt
36633
- };
36634
- await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
36635
- await chmod(path, 384);
36636
- return id;
36637
- }
36638
-
36639
36959
  // node_modules/@fetchproxy/server/dist/error-kind.js
36640
36960
  function classifyFetchError(error51) {
36641
36961
  if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
@@ -36717,6 +37037,39 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
36717
37037
  this.hint = hint;
36718
37038
  }
36719
37039
  };
37040
+ var FetchproxyHintedError = class extends FetchproxyProtocolError {
37041
+ /** The extension's raw rejection, unmodified. */
37042
+ originalError;
37043
+ /** What the user should actually do, in prose. */
37044
+ hint;
37045
+ constructor(originalError, hint) {
37046
+ super(`${originalError} \u2014 ${hint}`);
37047
+ this.name = "FetchproxyHintedError";
37048
+ this.originalError = originalError;
37049
+ this.hint = hint;
37050
+ }
37051
+ };
37052
+ var FetchproxyScopeError = class extends FetchproxyHintedError {
37053
+ constructor(originalError) {
37054
+ super(originalError, "the declared scope changed since you paired, so the extension is refusing the request. Revoke this MCP in the Transporter extension popup, then re-run \u2014 you will be asked to approve the new scope. This is not a version problem and does not need an update.");
37055
+ this.name = "FetchproxyScopeError";
37056
+ }
37057
+ };
37058
+ var FetchproxyNoTabError = class extends FetchproxyHintedError {
37059
+ constructor(originalError) {
37060
+ super(originalError, "open a tab on that host and sign in, then re-run. This is not a version problem and does not need an update.");
37061
+ this.name = "FetchproxyNoTabError";
37062
+ }
37063
+ };
37064
+ var SCOPE_REJECTION = /not in declared/;
37065
+ var NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
37066
+ function protocolErrorFrom(error51) {
37067
+ if (SCOPE_REJECTION.test(error51))
37068
+ return new FetchproxyScopeError(error51);
37069
+ if (NO_TAB_REJECTION.test(error51))
37070
+ return new FetchproxyNoTabError(error51);
37071
+ return new FetchproxyProtocolError(error51);
37072
+ }
36720
37073
  var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36721
37074
  url;
36722
37075
  timeoutMs;
@@ -36746,6 +37099,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36746
37099
  }
36747
37100
  };
36748
37101
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
37102
+ function normalizeCookiePath(path) {
37103
+ if (path === void 0 || path === "")
37104
+ return void 0;
37105
+ const trimmed = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
37106
+ try {
37107
+ assertCookiePath(trimmed, "path");
37108
+ } catch (e) {
37109
+ throw new Error(`FetchproxyServer: ${e instanceof Error ? e.message : String(e)} (got ${JSON.stringify(path)})`);
37110
+ }
37111
+ return trimmed;
37112
+ }
36749
37113
  function assertSubdomainLabel(label) {
36750
37114
  if (!SUBDOMAIN_LABEL_RE.test(label)) {
36751
37115
  throw new Error(`FetchproxyServer: subdomain must be a DNS label like "www" or "api" (or dot-separated like "auth.api"), got ${JSON.stringify(label)}`);
@@ -36810,6 +37174,9 @@ var FetchproxyServer = class {
36810
37174
  // them off from `pending` (fetch) and `pendingReadCookies` (legacy
36811
37175
  // string-shape) so the response routing in `onInner` stays linear.
36812
37176
  pendingStorage = /* @__PURE__ */ new Map();
37177
+ // 1.12.0+: write-cookies awaiters resolve the list of names actually
37178
+ // written, so a caller can confirm rather than assume.
37179
+ pendingWriteCookies = /* @__PURE__ */ new Map();
36813
37180
  // 0.3.0+: capture-header awaiters resolve a single string.
36814
37181
  pendingCapture = /* @__PURE__ */ new Map();
36815
37182
  // capture_redirect awaiters resolve the captured redirect URL string.
@@ -36936,6 +37303,8 @@ var FetchproxyServer = class {
36936
37303
  keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
36937
37304
  keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
36938
37305
  identityDir: opts.identityDir,
37306
+ allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
37307
+ requireExtensionIdentity: opts.requireExtensionIdentity,
36939
37308
  onPairCode: opts.onPairCode
36940
37309
  };
36941
37310
  }
@@ -37035,7 +37404,8 @@ var FetchproxyServer = class {
37035
37404
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
37036
37405
  ownDomSelectors: this.opts.domSelectors,
37037
37406
  ownGraphqlOps: this.opts.graphqlOps,
37038
- onPairCode: this.opts.onPairCode
37407
+ onPairCode: this.opts.onPairCode,
37408
+ extensionTrust: this.extensionTrust()
37039
37409
  });
37040
37410
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
37041
37411
  this.hostHandle.onExtensionDisconnect(() => {
@@ -37064,7 +37434,9 @@ var FetchproxyServer = class {
37064
37434
  localStoragePointers: this.opts.localStoragePointers,
37065
37435
  sessionStoragePointers: this.opts.sessionStoragePointers,
37066
37436
  domSelectors: this.opts.domSelectors,
37067
- graphqlOps: this.opts.graphqlOps
37437
+ graphqlOps: this.opts.graphqlOps,
37438
+ extensionTrust: this.extensionTrust(),
37439
+ requireExtensionIdentity: this.opts.requireExtensionIdentity
37068
37440
  });
37069
37441
  this.peerHandle.onInner((inner) => this.onInner(inner));
37070
37442
  this.peerHandle.onRenegotiate(() => {
@@ -37203,6 +37575,23 @@ var FetchproxyServer = class {
37203
37575
  markActive() {
37204
37576
  this.noteActivityForKeepalive();
37205
37577
  }
37578
+ /**
37579
+ * #208: this MCP's pin on the extension's identity, stored beside its own
37580
+ * identity key and so following `identityDir` wherever the caller put it.
37581
+ *
37582
+ * `allowNewExtensionIdentity` falls back to an environment variable when the
37583
+ * caller expressed no opinion, because the thirteen MCPs that construct this
37584
+ * class are separate packages: an operator whose extension re-install has
37585
+ * just locked all of them out needs one lever that does not require patching
37586
+ * every one of them.
37587
+ */
37588
+ extensionTrust() {
37589
+ return fileExtensionTrust({
37590
+ serverName: this.opts.serverName,
37591
+ dir: this.opts.identityDir,
37592
+ allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
37593
+ });
37594
+ }
37206
37595
  noteActivityForKeepalive() {
37207
37596
  const intervalMs = this.opts.keepAliveIntervalMs;
37208
37597
  if (intervalMs <= 0)
@@ -37266,6 +37655,7 @@ var FetchproxyServer = class {
37266
37655
  this.pending.delete(id);
37267
37656
  this.pendingReadCookies.delete(id);
37268
37657
  this.pendingStorage.delete(id);
37658
+ this.pendingWriteCookies.delete(id);
37269
37659
  this.pendingCapture.delete(id);
37270
37660
  this.pendingRedirect.delete(id);
37271
37661
  this.pendingDownload.delete(id);
@@ -37387,7 +37777,7 @@ var FetchproxyServer = class {
37387
37777
  port: this.opts.port
37388
37778
  });
37389
37779
  }
37390
- return new FetchproxyProtocolError(result.error);
37780
+ return protocolErrorFrom(result.error);
37391
37781
  }
37392
37782
  /**
37393
37783
  * Convenience wrapper around `fetch()`. Builds the URL from a path
@@ -37423,10 +37813,20 @@ var FetchproxyServer = class {
37423
37813
  }
37424
37814
  const url2 = isAbsolute2 ? path : `https://${host}${path}`;
37425
37815
  assertUrlInDomains("request url", url2, this.opts.domains);
37816
+ let tabUrl = `https://${host}/`;
37817
+ if (opts.viaTab !== void 0) {
37818
+ try {
37819
+ new URL(opts.viaTab);
37820
+ } catch {
37821
+ throw new Error(`FetchproxyServer.request: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
37822
+ }
37823
+ assertUrlInDomains("viaTab", opts.viaTab, this.opts.domains);
37824
+ tabUrl = opts.viaTab;
37825
+ }
37426
37826
  const init = {
37427
37827
  url: url2,
37428
37828
  method,
37429
- tabUrl: `https://${host}/`,
37829
+ tabUrl,
37430
37830
  headers: opts.headers,
37431
37831
  body: opts.body
37432
37832
  };
@@ -37638,9 +38038,14 @@ var FetchproxyServer = class {
37638
38038
  let inner;
37639
38039
  if (opts.keys !== void 0) {
37640
38040
  this.assertScopeSubset(opts.keys, this.opts.cookieKeys, "cookieKeys");
38041
+ const cookiePath = normalizeCookiePath(opts.path);
37641
38042
  const initV3 = {
38043
+ // Origin stays BARE. The path travels as its own validated field —
38044
+ // `assertHttpsOriginOnly` deliberately refuses a path here so one
38045
+ // cannot be used to re-point the read past the domain gate.
37642
38046
  origin: `https://${host}`,
37643
- keys: [...opts.keys]
38047
+ keys: [...opts.keys],
38048
+ ...cookiePath !== void 0 ? { path: cookiePath } : {}
37644
38049
  };
37645
38050
  inner = { type: "request", id, op: "read_cookies", init: initV3 };
37646
38051
  } else {
@@ -37653,10 +38058,65 @@ var FetchproxyServer = class {
37653
38058
  await this.sendInnerFrame(inner);
37654
38059
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
37655
38060
  if (!result.ok) {
37656
- throw new FetchproxyProtocolError(result.error);
38061
+ throw protocolErrorFrom(result.error);
37657
38062
  }
37658
38063
  return result.cookies;
37659
38064
  }
38065
+ /**
38066
+ * 1.12.0+: overwrite the value of cookies this MCP already declares.
38067
+ *
38068
+ * The bridge's only write verb, and it exists for one failure class. Sites
38069
+ * that ROTATE a credential cookie hand back a new value on every refresh; if
38070
+ * the MCP refreshes and keeps the result to itself, the copy in the browser's
38071
+ * cookie jar is dead, and the user gets signed out of a tab they never
38072
+ * touched — usually reported to them as "inactivity". Writing the rotated
38073
+ * value back is the only thing that repairs it.
38074
+ *
38075
+ * Requires `'write_cookies'` in capabilities, which the user approves at pair
38076
+ * time as its own line. Every name must ALSO be in declared `cookieKeys`: a
38077
+ * write can never reach a cookie the MCP was not already trusted to read, so
38078
+ * granting it cannot widen which cookies are in play — only what may be done
38079
+ * to the ones already listed.
38080
+ *
38081
+ * The extension refuses the whole request unless every named cookie already
38082
+ * exists; this refreshes a value in place and deliberately cannot author new
38083
+ * cookies. Returns the names actually written.
38084
+ */
38085
+ async writeCookies(opts) {
38086
+ if (!this.opts.capabilities.includes("write_cookies")) {
38087
+ throw new Error('FetchproxyServer.writeCookies(): MCP did not declare "write_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
38088
+ }
38089
+ const names = Object.keys(opts.cookies);
38090
+ if (names.length === 0) {
38091
+ throw new Error("FetchproxyServer.writeCookies(): no cookies given");
38092
+ }
38093
+ await this.ensureConnected();
38094
+ this.throwIfPendingPair();
38095
+ if (opts.subdomain !== void 0)
38096
+ assertSubdomainLabel(opts.subdomain);
38097
+ const baseDomain = this.resolveBaseDomain(opts.domain);
38098
+ const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
38099
+ this.assertScopeSubset(names, this.opts.cookieKeys, "cookieKeys");
38100
+ const cookiePath = normalizeCookiePath(opts.path);
38101
+ const id = this.nextRequestId++;
38102
+ const inner = {
38103
+ type: "request",
38104
+ id,
38105
+ op: "write_cookies",
38106
+ init: {
38107
+ // Bare origin, same invariant as the read path: a path must never be
38108
+ // able to move the request past the domain gate.
38109
+ origin: `https://${host}`,
38110
+ cookies: Object.entries(opts.cookies).map(([name, value]) => ({ name, value })),
38111
+ ...cookiePath !== void 0 ? { path: cookiePath } : {}
38112
+ }
38113
+ };
38114
+ const pending = new Promise((resolve2, reject) => {
38115
+ this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
38116
+ });
38117
+ await this.sendInnerFrame(inner);
38118
+ return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
38119
+ }
37660
38120
  /**
37661
38121
  * 0.3.0+: read declared localStorage keys from the user's signed-in
37662
38122
  * tab. Requires `'read_local_storage'` in capabilities AND each key
@@ -38203,7 +38663,7 @@ var FetchproxyServer = class {
38203
38663
  storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
38204
38664
  }
38205
38665
  } else {
38206
- storageCb.reject(new FetchproxyProtocolError(inner.error));
38666
+ storageCb.reject(protocolErrorFrom(inner.error));
38207
38667
  }
38208
38668
  return;
38209
38669
  }
@@ -38217,7 +38677,7 @@ var FetchproxyServer = class {
38217
38677
  captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
38218
38678
  }
38219
38679
  } else {
38220
- captureCb.reject(new FetchproxyProtocolError(inner.error));
38680
+ captureCb.reject(protocolErrorFrom(inner.error));
38221
38681
  }
38222
38682
  return;
38223
38683
  }
@@ -38231,7 +38691,7 @@ var FetchproxyServer = class {
38231
38691
  redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
38232
38692
  }
38233
38693
  } else {
38234
- redirectCb.reject(new FetchproxyProtocolError(inner.error));
38694
+ redirectCb.reject(protocolErrorFrom(inner.error));
38235
38695
  }
38236
38696
  return;
38237
38697
  }
@@ -38245,7 +38705,7 @@ var FetchproxyServer = class {
38245
38705
  idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
38246
38706
  }
38247
38707
  } else {
38248
- idbCb.reject(new FetchproxyProtocolError(inner.error));
38708
+ idbCb.reject(protocolErrorFrom(inner.error));
38249
38709
  }
38250
38710
  return;
38251
38711
  }
@@ -38259,7 +38719,7 @@ var FetchproxyServer = class {
38259
38719
  downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
38260
38720
  }
38261
38721
  } else {
38262
- downloadCb.reject(new FetchproxyProtocolError(inner.error));
38722
+ downloadCb.reject(protocolErrorFrom(inner.error));
38263
38723
  }
38264
38724
  return;
38265
38725
  }
@@ -38273,7 +38733,17 @@ var FetchproxyServer = class {
38273
38733
  graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
38274
38734
  }
38275
38735
  } else {
38276
- graphqlCb.reject(new FetchproxyProtocolError(inner.error));
38736
+ graphqlCb.reject(protocolErrorFrom(inner.error));
38737
+ }
38738
+ return;
38739
+ }
38740
+ const writeCookiesCb = this.pendingWriteCookies.get(inner.id);
38741
+ if (writeCookiesCb) {
38742
+ this.pendingWriteCookies.delete(inner.id);
38743
+ if (inner.ok && inner.op === "write_cookies") {
38744
+ writeCookiesCb.resolve([...inner.written]);
38745
+ } else {
38746
+ writeCookiesCb.reject(protocolErrorFrom(inner.ok ? "write_cookies response had the wrong op" : inner.error));
38277
38747
  }
38278
38748
  return;
38279
38749
  }
@@ -38316,6 +38786,9 @@ var FetchproxyServer = class {
38316
38786
  for (const { reject } of this.pendingStorage.values())
38317
38787
  reject(err);
38318
38788
  this.pendingStorage.clear();
38789
+ for (const { reject } of this.pendingWriteCookies.values())
38790
+ reject(err);
38791
+ this.pendingWriteCookies.clear();
38319
38792
  for (const { reject } of this.pendingCapture.values())
38320
38793
  reject(err);
38321
38794
  this.pendingCapture.clear();
@@ -38385,6 +38858,9 @@ var FetchproxyServer = class {
38385
38858
  // node_modules/@fetchproxy/bootstrap/dist/index.js
38386
38859
  var defaultFactory = (opts) => new FetchproxyServer(opts);
38387
38860
  async function bootstrap(opts) {
38861
+ return runOneLift(opts);
38862
+ }
38863
+ async function runOneLift(opts) {
38388
38864
  const envVar = opts.serverName.toUpperCase().replace(/[^A-Z0-9]/g, "_").replace(/^_+/, "") + "_DISABLE_FETCHPROXY";
38389
38865
  const envVal = process.env[envVar];
38390
38866
  if (envVal !== void 0 && envVal !== "" && envVal !== "0" && envVal !== "false") {
@@ -38453,7 +38929,9 @@ async function bootstrap(opts) {
38453
38929
  if (opts.declare.cookies.length > 0) {
38454
38930
  const joined = await server.readCookies({
38455
38931
  keys: opts.declare.cookies,
38456
- ...storageDomainOpts
38932
+ ...storageDomainOpts,
38933
+ // Cookie-only: the other buckets are origin-scoped and ignore path.
38934
+ ...opts.storagePath !== void 0 ? { path: opts.storagePath } : {}
38457
38935
  });
38458
38936
  for (const piece of joined.split("; ")) {
38459
38937
  if (!piece)
@@ -38511,12 +38989,18 @@ async function bootstrap(opts) {
38511
38989
  });
38512
38990
  indexedDbBucket[`${d.database}/${d.store}`] = values;
38513
38991
  }
38992
+ const absent = (declared, got) => declared.filter((k) => !(k in got));
38514
38993
  return {
38515
38994
  cookies,
38516
38995
  localStorage,
38517
38996
  sessionStorage,
38518
38997
  capturedHeaders,
38519
- indexedDb: indexedDbBucket
38998
+ indexedDb: indexedDbBucket,
38999
+ missing: {
39000
+ cookies: absent(opts.declare.cookies, cookies),
39001
+ localStorage: absent(opts.declare.localStorage, localStorage),
39002
+ sessionStorage: absent(opts.declare.sessionStorage, sessionStorage)
39003
+ }
38520
39004
  };
38521
39005
  } finally {
38522
39006
  try {
@@ -38590,7 +39074,7 @@ async function loginWithPassword(username, password) {
38590
39074
  // package.json
38591
39075
  var package_default = {
38592
39076
  name: "ofw-mcp",
38593
- version: "2.10.0",
39077
+ version: "2.10.1",
38594
39078
  license: "MIT",
38595
39079
  mcpName: "io.github.chrischall/ofw-mcp",
38596
39080
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
@@ -38626,14 +39110,14 @@ var package_default = {
38626
39110
  },
38627
39111
  dependencies: {
38628
39112
  "@chrischall/mcp-utils": "^0.14.0",
38629
- "@fetchproxy/bootstrap": "^1.7.0",
39113
+ "@fetchproxy/bootstrap": "^2.0.0",
38630
39114
  "@modelcontextprotocol/sdk": "^1.29.0",
38631
39115
  dotenv: "^17.4.2",
38632
39116
  zod: "^4.4.3"
38633
39117
  },
38634
39118
  devDependencies: {
38635
39119
  "@chrischall/mcp-connector": "^1.1.1",
38636
- "@cloudflare/vitest-pool-workers": "^0.18.4",
39120
+ "@cloudflare/vitest-pool-workers": "^0.19.1",
38637
39121
  "@cloudflare/workers-oauth-provider": "^0.8.1",
38638
39122
  "@cloudflare/workers-types": "^5.20260708.1",
38639
39123
  "@types/node": "^26.0.0",
@@ -38710,7 +39194,7 @@ async function resolveAuth() {
38710
39194
  // src/client.ts
38711
39195
  try {
38712
39196
  const dir = dirname(fileURLToPath(import.meta.url));
38713
- await loadDotenvSafely({ path: join3(dir, "..", ".env") });
39197
+ await loadDotenvSafely({ path: join4(dir, "..", ".env") });
38714
39198
  } catch {
38715
39199
  }
38716
39200
  function parseContentDispositionFilename(cd) {
@@ -39597,24 +40081,24 @@ async function syncAll(client2, opts, store) {
39597
40081
  // src/config.ts
39598
40082
  import { createHash } from "node:crypto";
39599
40083
  import { homedir as homedir3 } from "node:os";
39600
- import { join as join4 } from "node:path";
40084
+ import { join as join5 } from "node:path";
39601
40085
  function readCacheIdentity() {
39602
40086
  return readEnvVar("OFW_CACHE_IDENTITY") ?? readEnvVar("OFW_USERNAME") ?? "_default";
39603
40087
  }
39604
40088
  function getCacheDir() {
39605
40089
  const override = process.env.OFW_CACHE_DIR;
39606
40090
  if (override && override.trim().length > 0) return override.trim();
39607
- return join4(homedir3(), ".cache", "ofw-mcp");
40091
+ return join5(homedir3(), ".cache", "ofw-mcp");
39608
40092
  }
39609
40093
  function getCacheDbPath() {
39610
40094
  const identity = readCacheIdentity();
39611
40095
  const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
39612
- return join4(getCacheDir(), `${hash2}.db`);
40096
+ return join5(getCacheDir(), `${hash2}.db`);
39613
40097
  }
39614
40098
  function getAttachmentsDir() {
39615
40099
  const override = process.env.OFW_ATTACHMENTS_DIR;
39616
40100
  if (override && override.trim().length > 0) return override.trim();
39617
- return join4(homedir3(), "Downloads", "ofw-mcp");
40101
+ return join5(homedir3(), "Downloads", "ofw-mcp");
39618
40102
  }
39619
40103
  function getWriteMode() {
39620
40104
  const raw = process.env.OFW_WRITE_MODE;
@@ -41130,7 +41614,7 @@ async function buildInlineDelivery(input) {
41130
41614
  }
41131
41615
 
41132
41616
  // src/tools/messages.ts
41133
- import { basename as basename2, join as join5 } from "node:path";
41617
+ import { basename as basename2, join as join6 } from "node:path";
41134
41618
  var DateSchema = external_exports.looseObject({ dateTime: external_exports.string() });
41135
41619
  var SentDetailSchema = external_exports.looseObject({
41136
41620
  subject: external_exports.string().optional(),
@@ -42140,9 +42624,9 @@ ${text}` : text);
42140
42624
  if (args.saveTo) {
42141
42625
  const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
42142
42626
  const abs = expandPath2(args.saveTo);
42143
- dest = isDirArg ? join5(abs, `${fileId}-${safeName}`) : abs;
42627
+ dest = isDirArg ? join6(abs, `${fileId}-${safeName}`) : abs;
42144
42628
  } else {
42145
- dest = join5(getAttachmentsDir(), `${fileId}-${safeName}`);
42629
+ dest = join6(getAttachmentsDir(), `${fileId}-${safeName}`);
42146
42630
  }
42147
42631
  const extractOnDisk = args.extract === true;
42148
42632
  if (!args.force && cached2.downloadedPath === dest) {
@@ -43246,7 +43730,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
43246
43730
  var nodeAttachmentIO = new NodeAttachmentIO();
43247
43731
  await runMcp({
43248
43732
  name: "ofw",
43249
- version: "2.10.0",
43733
+ version: "2.10.1",
43250
43734
  // x-release-please-version
43251
43735
  deps: client,
43252
43736
  tools: [
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ const nodeAttachmentIO = new NodeAttachmentIO();
35
35
  // always succeeds before any credential check runs.
36
36
  await runMcp({
37
37
  name: 'ofw',
38
- version: '2.10.0', // x-release-please-version
38
+ version: '2.10.1', // x-release-please-version
39
39
  deps: client,
40
40
  tools: [
41
41
  registerUserTools,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofw-mcp",
3
- "version": "2.10.0",
3
+ "version": "2.10.1",
4
4
  "license": "MIT",
5
5
  "mcpName": "io.github.chrischall/ofw-mcp",
6
6
  "description": "OurFamilyWizard MCP server for Claude — developed and maintained by AI (Claude Code)",
@@ -36,14 +36,14 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@chrischall/mcp-utils": "^0.14.0",
39
- "@fetchproxy/bootstrap": "^1.7.0",
39
+ "@fetchproxy/bootstrap": "^2.0.0",
40
40
  "@modelcontextprotocol/sdk": "^1.29.0",
41
41
  "dotenv": "^17.4.2",
42
42
  "zod": "^4.4.3"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@chrischall/mcp-connector": "^1.1.1",
46
- "@cloudflare/vitest-pool-workers": "^0.18.4",
46
+ "@cloudflare/vitest-pool-workers": "^0.19.1",
47
47
  "@cloudflare/workers-oauth-provider": "^0.8.1",
48
48
  "@cloudflare/workers-types": "^5.20260708.1",
49
49
  "@types/node": "^26.0.0",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/ofw-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.10.0",
9
+ "version": "2.10.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "ofw-mcp",
14
- "version": "2.10.0",
14
+ "version": "2.10.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },