ofw-mcp 2.11.0 → 2.13.0

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
@@ -2991,7 +2991,7 @@ var require_compile = __commonJS({
2991
2991
  const schOrFunc = root.refs[ref];
2992
2992
  if (schOrFunc)
2993
2993
  return schOrFunc;
2994
- let _sch = resolve2.call(this, root, ref);
2994
+ let _sch = resolve3.call(this, root, ref);
2995
2995
  if (_sch === void 0) {
2996
2996
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
2997
2997
  const { schemaId } = this.opts;
@@ -3018,7 +3018,7 @@ var require_compile = __commonJS({
3018
3018
  function sameSchemaEnv(s1, s2) {
3019
3019
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3020
3020
  }
3021
- function resolve2(root, ref) {
3021
+ function resolve3(root, ref) {
3022
3022
  let sch;
3023
3023
  while (typeof (sch = this.refs[ref]) == "string")
3024
3024
  ref = sch;
@@ -3314,8 +3314,8 @@ var require_utils = __commonJS({
3314
3314
  var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3315
3315
  var HOST_DELIM_RE = /[@/?#:]/g;
3316
3316
  var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
3317
- function reescapeHostDelimiters(host, isIP) {
3318
- const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3317
+ function reescapeHostDelimiters(host, isIP2) {
3318
+ const re = isIP2 ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3319
3319
  re.lastIndex = 0;
3320
3320
  return host.replace(re, (ch) => HOST_DELIMS[ch]);
3321
3321
  }
@@ -3649,7 +3649,7 @@ var require_fast_uri = __commonJS({
3649
3649
  }
3650
3650
  return uri;
3651
3651
  }
3652
- function resolve2(baseURI, relativeURI, options) {
3652
+ function resolve3(baseURI, relativeURI, options) {
3653
3653
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3654
3654
  const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3655
3655
  const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
@@ -3804,7 +3804,7 @@ var require_fast_uri = __commonJS({
3804
3804
  fragment: void 0
3805
3805
  };
3806
3806
  let malformedAuthorityOrPort = false;
3807
- let isIP = false;
3807
+ let isIP2 = false;
3808
3808
  if (options.reference === "suffix") {
3809
3809
  if (options.scheme) {
3810
3810
  uri = options.scheme + ":" + uri;
@@ -3853,9 +3853,9 @@ var require_fast_uri = __commonJS({
3853
3853
  if (ipv4result === false) {
3854
3854
  const ipv6result = normalizeIPv6(parsed.host);
3855
3855
  parsed.host = ipv6result.host.toLowerCase();
3856
- isIP = ipv6result.isIPV6;
3856
+ isIP2 = ipv6result.isIPV6;
3857
3857
  } else {
3858
- isIP = true;
3858
+ isIP2 = true;
3859
3859
  }
3860
3860
  }
3861
3861
  if (parsed.scheme === void 0 && parsed.userinfo === void 0 && parsed.host === void 0 && parsed.port === void 0 && parsed.query === void 0 && !parsed.path) {
@@ -3872,7 +3872,7 @@ var require_fast_uri = __commonJS({
3872
3872
  }
3873
3873
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
3874
3874
  if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
3875
- if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
3875
+ if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP2 === false && nonSimpleDomain(parsed.host)) {
3876
3876
  try {
3877
3877
  parsed.host = new URL("http://" + parsed.host).hostname;
3878
3878
  } catch (e) {
@@ -3886,7 +3886,7 @@ var require_fast_uri = __commonJS({
3886
3886
  parsed.scheme = unescape(parsed.scheme);
3887
3887
  }
3888
3888
  if (parsed.host !== void 0) {
3889
- parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
3889
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP2);
3890
3890
  }
3891
3891
  }
3892
3892
  if (parsed.path) {
@@ -3933,7 +3933,7 @@ var require_fast_uri = __commonJS({
3933
3933
  var fastUri = {
3934
3934
  SCHEMES,
3935
3935
  normalize,
3936
- resolve: resolve2,
3936
+ resolve: resolve3,
3937
3937
  resolveComponent,
3938
3938
  equal,
3939
3939
  serialize,
@@ -7199,7 +7199,7 @@ var require_permessage_deflate = __commonJS({
7199
7199
  acceptAsServer(offers) {
7200
7200
  const opts = this._options;
7201
7201
  const accepted = offers.find((params) => {
7202
- if (opts.serverNoContextTakeover === false && params.server_no_context_takeover || params.server_max_window_bits && (opts.serverMaxWindowBits === false || typeof opts.serverMaxWindowBits === "number" && opts.serverMaxWindowBits > params.server_max_window_bits) || typeof opts.clientMaxWindowBits === "number" && !params.client_max_window_bits) {
7202
+ if (opts.serverNoContextTakeover === false && params.server_no_context_takeover || params.server_max_window_bits && (opts.serverMaxWindowBits === false || typeof opts.serverMaxWindowBits === "number" && opts.serverMaxWindowBits > params.server_max_window_bits) || typeof opts.clientMaxWindowBits === "number" && (typeof params.client_max_window_bits === "number" ? opts.clientMaxWindowBits > params.client_max_window_bits : !params.client_max_window_bits)) {
7203
7203
  return false;
7204
7204
  }
7205
7205
  return true;
@@ -9161,7 +9161,7 @@ var require_websocket = __commonJS({
9161
9161
  var http = __require("http");
9162
9162
  var net = __require("net");
9163
9163
  var tls = __require("tls");
9164
- var { randomBytes, createHash: createHash2 } = __require("crypto");
9164
+ var { randomBytes: randomBytes2, createHash: createHash2 } = __require("crypto");
9165
9165
  var { Duplex, Readable } = __require("stream");
9166
9166
  var { URL: URL2 } = __require("url");
9167
9167
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -9699,7 +9699,7 @@ var require_websocket = __commonJS({
9699
9699
  }
9700
9700
  }
9701
9701
  const defaultPort = isSecure ? 443 : 80;
9702
- const key = randomBytes(16).toString("base64");
9702
+ const key = randomBytes2(16).toString("base64");
9703
9703
  const request = isSecure ? https.request : http.request;
9704
9704
  const protocolSet = /* @__PURE__ */ new Set();
9705
9705
  let perMessageDeflate;
@@ -32550,7 +32550,7 @@ var Protocol = class {
32550
32550
  return;
32551
32551
  }
32552
32552
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
32553
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
32553
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
32554
32554
  options?.signal?.throwIfAborted();
32555
32555
  }
32556
32556
  } catch (error51) {
@@ -32567,7 +32567,7 @@ var Protocol = class {
32567
32567
  */
32568
32568
  request(request, resultSchema, options) {
32569
32569
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
32570
- return new Promise((resolve2, reject) => {
32570
+ return new Promise((resolve3, reject) => {
32571
32571
  const earlyReject = (error51) => {
32572
32572
  reject(error51);
32573
32573
  };
@@ -32645,7 +32645,7 @@ var Protocol = class {
32645
32645
  if (!parseResult.success) {
32646
32646
  reject(parseResult.error);
32647
32647
  } else {
32648
- resolve2(parseResult.data);
32648
+ resolve3(parseResult.data);
32649
32649
  }
32650
32650
  } catch (error51) {
32651
32651
  reject(error51);
@@ -32906,12 +32906,12 @@ var Protocol = class {
32906
32906
  }
32907
32907
  } catch {
32908
32908
  }
32909
- return new Promise((resolve2, reject) => {
32909
+ return new Promise((resolve3, reject) => {
32910
32910
  if (signal.aborted) {
32911
32911
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
32912
32912
  return;
32913
32913
  }
32914
- const timeoutId = setTimeout(resolve2, interval);
32914
+ const timeoutId = setTimeout(resolve3, interval);
32915
32915
  signal.addEventListener("abort", () => {
32916
32916
  clearTimeout(timeoutId);
32917
32917
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -34002,7 +34002,7 @@ var McpServer = class {
34002
34002
  let task = createTaskResult.task;
34003
34003
  const pollInterval = task.pollInterval ?? 5e3;
34004
34004
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
34005
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
34005
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
34006
34006
  const updatedTask = await extra.taskStore.getTask(taskId);
34007
34007
  if (!updatedTask) {
34008
34008
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -34666,64 +34666,17 @@ var StdioServerTransport = class {
34666
34666
  this.onclose?.();
34667
34667
  }
34668
34668
  send(message) {
34669
- return new Promise((resolve2) => {
34669
+ return new Promise((resolve3) => {
34670
34670
  const json2 = serializeMessage(message);
34671
34671
  if (this._stdout.write(json2)) {
34672
- resolve2();
34672
+ resolve3();
34673
34673
  } else {
34674
- this._stdout.once("drain", resolve2);
34674
+ this._stdout.once("drain", resolve3);
34675
34675
  }
34676
34676
  });
34677
34677
  }
34678
34678
  };
34679
34679
 
34680
- // node_modules/@chrischall/mcp-utils/dist/server/index.js
34681
- async function createMcpServer(opts) {
34682
- const server = new McpServer({ name: opts.name, version: opts.version });
34683
- if (opts.banner !== void 0) {
34684
- console.error(opts.banner);
34685
- }
34686
- const deps = opts.deps;
34687
- for (const register of opts.tools) {
34688
- await register(server, deps);
34689
- }
34690
- return server;
34691
- }
34692
- function withGracefulShutdown(server, opts = {}) {
34693
- const shouldExit = opts.exit ?? true;
34694
- let shuttingDown = false;
34695
- const handler = (signal) => {
34696
- if (shuttingDown)
34697
- return;
34698
- shuttingDown = true;
34699
- void (async () => {
34700
- try {
34701
- if (opts.onSignal)
34702
- await opts.onSignal(signal);
34703
- await server.close();
34704
- } catch (err) {
34705
- console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
34706
- } finally {
34707
- if (shouldExit)
34708
- process.exit(0);
34709
- }
34710
- })();
34711
- };
34712
- process.on("SIGINT", () => handler("SIGINT"));
34713
- process.on("SIGTERM", () => handler("SIGTERM"));
34714
- }
34715
- async function runMcp(opts) {
34716
- const server = await createMcpServer(opts);
34717
- const shutdown = opts.shutdown ?? true;
34718
- if (shutdown !== false) {
34719
- withGracefulShutdown(server, shutdown === true ? {} : shutdown);
34720
- }
34721
- const spec = opts.transport ?? "stdio";
34722
- const transport = spec === "stdio" ? new StdioServerTransport() : spec;
34723
- await server.connect(transport);
34724
- return server;
34725
- }
34726
-
34727
34680
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
34728
34681
  var McpToolError = class extends Error {
34729
34682
  /** Actionable remediation text, when one applies. */
@@ -34736,6 +34689,9 @@ var McpToolError = class extends Error {
34736
34689
  Object.setPrototypeOf(this, new.target.prototype);
34737
34690
  }
34738
34691
  };
34692
+ function createHelpfulError(message, opts) {
34693
+ return new McpToolError(message, opts);
34694
+ }
34739
34695
  var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
34740
34696
  var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
34741
34697
  var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
@@ -34773,6 +34729,81 @@ function textResult(data) {
34773
34729
  function rawTextResult(text) {
34774
34730
  return { content: [{ type: "text", text }] };
34775
34731
  }
34732
+ function errorResult(message) {
34733
+ return {
34734
+ content: [{ type: "text", text: redactSecrets(message) }],
34735
+ isError: true
34736
+ };
34737
+ }
34738
+
34739
+ // node_modules/@chrischall/mcp-utils/dist/server/index.js
34740
+ function hintResultOrRethrow(err) {
34741
+ if (err instanceof McpToolError && err.hint) {
34742
+ return errorResult(`${err.message}
34743
+
34744
+ Hint: ${err.hint}`);
34745
+ }
34746
+ throw err;
34747
+ }
34748
+ function surfaceToolHints(server) {
34749
+ const register = server.registerTool.bind(server);
34750
+ server.registerTool = (name, config2, cb) => register(name, config2, (...args) => {
34751
+ let result;
34752
+ try {
34753
+ result = cb(...args);
34754
+ } catch (err) {
34755
+ return hintResultOrRethrow(err);
34756
+ }
34757
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
34758
+ });
34759
+ }
34760
+ async function createMcpServer(opts) {
34761
+ const server = new McpServer({ name: opts.name, version: opts.version });
34762
+ if (opts.surfaceHints !== false)
34763
+ surfaceToolHints(server);
34764
+ if (opts.banner !== void 0) {
34765
+ console.error(opts.banner);
34766
+ }
34767
+ const deps = opts.deps;
34768
+ for (const register of opts.tools) {
34769
+ await register(server, deps);
34770
+ }
34771
+ return server;
34772
+ }
34773
+ function withGracefulShutdown(server, opts = {}) {
34774
+ const shouldExit = opts.exit ?? true;
34775
+ let shuttingDown = false;
34776
+ const handler = (signal) => {
34777
+ if (shuttingDown)
34778
+ return;
34779
+ shuttingDown = true;
34780
+ void (async () => {
34781
+ try {
34782
+ if (opts.onSignal)
34783
+ await opts.onSignal(signal);
34784
+ await server.close();
34785
+ } catch (err) {
34786
+ console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
34787
+ } finally {
34788
+ if (shouldExit)
34789
+ process.exit(0);
34790
+ }
34791
+ })();
34792
+ };
34793
+ process.on("SIGINT", () => handler("SIGINT"));
34794
+ process.on("SIGTERM", () => handler("SIGTERM"));
34795
+ }
34796
+ async function runMcp(opts) {
34797
+ const server = await createMcpServer(opts);
34798
+ const shutdown = opts.shutdown ?? true;
34799
+ if (shutdown !== false) {
34800
+ withGracefulShutdown(server, shutdown === true ? {} : shutdown);
34801
+ }
34802
+ const spec = opts.transport ?? "stdio";
34803
+ const transport = spec === "stdio" ? new StdioServerTransport() : spec;
34804
+ await server.connect(transport);
34805
+ return server;
34806
+ }
34776
34807
 
34777
34808
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
34778
34809
  import { homedir } from "node:os";
@@ -34844,6 +34875,34 @@ async function fileBlob(path, opts = {}) {
34844
34875
  return blob;
34845
34876
  }
34846
34877
 
34878
+ // node_modules/@chrischall/mcp-utils/dist/http/index.js
34879
+ var RateLimitedError = class extends Error {
34880
+ status = 429;
34881
+ constructor(service) {
34882
+ super(`Rate limited (429) by ${service} after retries.`);
34883
+ this.name = "RateLimitedError";
34884
+ Object.setPrototypeOf(this, new.target.prototype);
34885
+ }
34886
+ };
34887
+ var RequestTimeoutError = class extends Error {
34888
+ timeoutMs;
34889
+ constructor(service, timeoutMs) {
34890
+ super(`Request to ${service} timed out after ${timeoutMs}ms.`);
34891
+ this.name = "RequestTimeoutError";
34892
+ this.timeoutMs = timeoutMs;
34893
+ Object.setPrototypeOf(this, new.target.prototype);
34894
+ }
34895
+ };
34896
+ var ApiError = class extends Error {
34897
+ status;
34898
+ constructor(status, message) {
34899
+ super(message);
34900
+ this.name = "ApiError";
34901
+ this.status = status;
34902
+ Object.setPrototypeOf(this, new.target.prototype);
34903
+ }
34904
+ };
34905
+
34847
34906
  // node_modules/@chrischall/mcp-utils/dist/zod/parse-lenient.js
34848
34907
  function parseLenient(schema, raw, opts) {
34849
34908
  const result = schema.safeParse(raw);
@@ -34878,58 +34937,329 @@ var pageSchema = {
34878
34937
  page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
34879
34938
  };
34880
34939
 
34940
+ // node_modules/@chrischall/mcp-utils/dist/auth/index.js
34941
+ async function resolveAuthPattern(pattern) {
34942
+ const ordered = [
34943
+ pattern.token,
34944
+ pattern.oauth,
34945
+ pattern.sessionScrape,
34946
+ pattern.fetchproxy
34947
+ ];
34948
+ for (const resolver of ordered) {
34949
+ if (resolver)
34950
+ return resolver();
34951
+ }
34952
+ throw createHelpfulError("No auth configured. Provide a token, OAuth credentials, login credentials, or sign in in your browser (fetchproxy fallback).", { hint: "Set one of the supported credential env vars, or sign in in your browser." });
34953
+ }
34954
+
34881
34955
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
34956
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
34957
+ import { dirname, join as join2, resolve as resolve2 } from "node:path";
34958
+ import { homedir as homedir2 } from "node:os";
34959
+ import { randomBytes, createHmac } from "node:crypto";
34960
+ function bindingDigest(secret, salt) {
34961
+ return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
34962
+ }
34963
+ function isEnvelope(raw) {
34964
+ return raw !== null && typeof raw === "object" && raw.v === 1 && "state" in raw;
34965
+ }
34966
+ var StatePersistenceError = class extends Error {
34967
+ cause;
34968
+ constructor(cause) {
34969
+ super(cause instanceof Error ? cause.message : String(cause));
34970
+ this.name = "StatePersistenceError";
34971
+ this.cause = cause;
34972
+ Object.setPrototypeOf(this, new.target.prototype);
34973
+ }
34974
+ };
34975
+ function createFileStatePersistence(opts) {
34976
+ const { filePath, validate } = opts;
34977
+ const secret = opts.boundTo;
34978
+ return {
34979
+ load() {
34980
+ if (!existsSync(filePath))
34981
+ return null;
34982
+ try {
34983
+ const raw = JSON.parse(readFileSync(filePath, "utf8"));
34984
+ let state;
34985
+ if (isEnvelope(raw)) {
34986
+ const b = raw.boundTo;
34987
+ if (secret === void 0) {
34988
+ if (b !== void 0)
34989
+ return null;
34990
+ } else {
34991
+ if (b === void 0)
34992
+ return null;
34993
+ if (bindingDigest(secret, b.salt) !== b.digest)
34994
+ return null;
34995
+ }
34996
+ state = raw.state;
34997
+ } else {
34998
+ if (secret !== void 0)
34999
+ return null;
35000
+ state = raw;
35001
+ }
35002
+ if (validate !== void 0)
35003
+ return validate(state);
35004
+ return state;
35005
+ } catch {
35006
+ return null;
35007
+ }
35008
+ },
35009
+ save(state) {
35010
+ const dir = dirname(filePath);
35011
+ const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
35012
+ const dirExisted = existsSync(dir);
35013
+ try {
35014
+ mkdirSync(dir, { recursive: true, mode: 448 });
35015
+ if (!dirExisted)
35016
+ chmodSync(dir, 448);
35017
+ let boundTo;
35018
+ if (secret !== void 0) {
35019
+ const salt = randomBytes(16).toString("hex");
35020
+ boundTo = { salt, digest: bindingDigest(secret, salt) };
35021
+ }
35022
+ const envelope = { v: 1, ...boundTo !== void 0 ? { boundTo } : {}, state };
35023
+ writeFileSync(tmp, JSON.stringify(envelope, null, 2), { mode: 384 });
35024
+ chmodSync(tmp, 384);
35025
+ renameSync(tmp, filePath);
35026
+ chmodSync(filePath, 384);
35027
+ } catch (err) {
35028
+ try {
35029
+ if (existsSync(tmp))
35030
+ unlinkSync(tmp);
35031
+ } catch {
35032
+ }
35033
+ throw err;
35034
+ }
35035
+ },
35036
+ clear() {
35037
+ try {
35038
+ if (existsSync(filePath))
35039
+ unlinkSync(filePath);
35040
+ } catch {
35041
+ }
35042
+ }
35043
+ };
35044
+ }
35045
+ function homeOf(env) {
35046
+ return readEnvVar("HOME", { env }) ?? homedir2();
35047
+ }
35048
+ function resolveStateDir(opts = {}) {
35049
+ const env = opts.env;
35050
+ return join2(readEnvVar("MCP_DATA_DIR", { env }) ?? homeOf(env), opts.subdir ?? "");
35051
+ }
35052
+ function resolveStateFile(opts) {
35053
+ const override = opts.envVar !== void 0 ? readEnvVar(opts.envVar, { env: opts.env }) : void 0;
35054
+ if (override !== void 0) {
35055
+ if (override === "~")
35056
+ return homeOf(opts.env);
35057
+ if (override.startsWith("~/"))
35058
+ return join2(homeOf(opts.env), override.slice(2));
35059
+ return resolve2(override);
35060
+ }
35061
+ const dirOpts = { env: opts.env };
35062
+ if (opts.subdir !== void 0)
35063
+ dirOpts.subdir = opts.subdir;
35064
+ return join2(resolveStateDir(dirOpts), opts.fileName);
35065
+ }
34882
35066
  var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
35067
+ function defaultIsRefreshRevoked(err) {
35068
+ if (err instanceof RateLimitedError || err instanceof RequestTimeoutError)
35069
+ return false;
35070
+ if (err instanceof ApiError && err.status >= 500)
35071
+ return false;
35072
+ return true;
35073
+ }
35074
+ function isBearerTokens(raw) {
35075
+ if (raw === null || typeof raw !== "object")
35076
+ return false;
35077
+ const t = raw;
35078
+ if (typeof t.accessToken !== "string" || t.accessToken === "")
35079
+ return false;
35080
+ if (typeof t.expiresAt !== "number" || !Number.isFinite(t.expiresAt))
35081
+ return false;
35082
+ return t.refreshToken === void 0 || typeof t.refreshToken === "string";
35083
+ }
34883
35084
  var TokenManager = class {
34884
- accessToken;
34885
- refreshToken;
34886
- expiresAt;
35085
+ tokens;
35086
+ bootstrapFn;
34887
35087
  refreshFn;
34888
35088
  skewMs;
35089
+ persistence;
35090
+ now;
35091
+ isRefreshRevokedFn;
35092
+ onPersistErrorFn;
34889
35093
  inFlight;
35094
+ bootstrapInFlight;
35095
+ /**
35096
+ * Persistence is consulted at most once per process. Without this the
35097
+ * revoked-token recovery below re-reads the SAME rejected record — `clear()`
35098
+ * is optional on {@link StatePersistence} and its failures are swallowed, so
35099
+ * recovery must not depend on it. After the first read the in-memory tokens
35100
+ * (or their deliberate absence) are the truth.
35101
+ */
35102
+ persistenceRead = false;
34890
35103
  constructor(opts) {
34891
- this.accessToken = opts.initial.accessToken;
34892
- this.refreshToken = opts.initial.refreshToken;
34893
- this.expiresAt = opts.initial.expiresAt;
35104
+ if (typeof opts.initial === "function") {
35105
+ this.bootstrapFn = opts.initial;
35106
+ } else {
35107
+ this.tokens = { ...opts.initial };
35108
+ }
34894
35109
  this.refreshFn = opts.refresh;
34895
35110
  this.skewMs = opts.skewMs ?? TOKEN_REFRESH_SKEW_MS;
35111
+ this.persistence = opts.persistence;
35112
+ this.now = opts.now ?? Date.now;
35113
+ this.isRefreshRevokedFn = opts.isRefreshRevoked ?? defaultIsRefreshRevoked;
35114
+ this.onPersistErrorFn = opts.onPersistError;
34896
35115
  }
34897
35116
  /** Whether the token is within the skew window of (or past) expiry. */
34898
35117
  needsRefresh() {
34899
- return Date.now() >= this.expiresAt - this.skewMs;
35118
+ if (this.tokens === void 0)
35119
+ return false;
35120
+ return this.now() >= this.tokens.expiresAt - this.skewMs;
35121
+ }
35122
+ /**
35123
+ * A stored token is worth using when it is still valid, OR when it carries a
35124
+ * refresh token — an expired-but-refreshable token still saves the login,
35125
+ * which is the expensive half.
35126
+ */
35127
+ isUsable(t) {
35128
+ return this.now() < t.expiresAt - this.skewMs || t.refreshToken !== void 0;
35129
+ }
35130
+ /** Read persisted tokens, guarding shape and usability. Never throws. */
35131
+ async loadPersisted() {
35132
+ if (this.persistence === void 0 || this.persistenceRead)
35133
+ return null;
35134
+ this.persistenceRead = true;
35135
+ try {
35136
+ const raw = await this.persistence.load();
35137
+ if (!isBearerTokens(raw) || !this.isUsable(raw))
35138
+ return null;
35139
+ return raw;
35140
+ } catch {
35141
+ return null;
35142
+ }
35143
+ }
35144
+ /**
35145
+ * Write tokens. Silent by default (a lost write costs a future login, not this
35146
+ * request); throws a {@link StatePersistenceError} when `onPersistError` does.
35147
+ */
35148
+ async persist(t) {
35149
+ if (this.persistence === void 0)
35150
+ return;
35151
+ try {
35152
+ await this.persistence.save(t);
35153
+ } catch (err) {
35154
+ if (this.onPersistErrorFn === void 0)
35155
+ return;
35156
+ try {
35157
+ this.onPersistErrorFn(err);
35158
+ } catch (hookErr) {
35159
+ throw new StatePersistenceError(hookErr);
35160
+ }
35161
+ }
35162
+ }
35163
+ /** Discard persisted tokens (a refresh they could not satisfy). Never throws. */
35164
+ async clearPersisted() {
35165
+ if (this.persistence?.clear === void 0)
35166
+ return;
35167
+ try {
35168
+ await this.persistence.clear();
35169
+ } catch {
35170
+ }
35171
+ }
35172
+ /** The current tokens, single-flighting the bootstrap if there are none. */
35173
+ ensureTokens() {
35174
+ if (this.tokens !== void 0)
35175
+ return Promise.resolve(this.tokens);
35176
+ if (this.bootstrapInFlight === void 0) {
35177
+ this.bootstrapInFlight = this.runBootstrap().finally(() => {
35178
+ this.bootstrapInFlight = void 0;
35179
+ });
35180
+ }
35181
+ return this.bootstrapInFlight;
35182
+ }
35183
+ /** One bootstrap attempt: persisted tokens if usable, else the login. */
35184
+ async runBootstrap() {
35185
+ const stored = await this.loadPersisted();
35186
+ if (stored !== null) {
35187
+ this.tokens = stored;
35188
+ return stored;
35189
+ }
35190
+ if (this.bootstrapFn === void 0) {
35191
+ throw new Error("TokenManager: no tokens and no bootstrap function to mint them.");
35192
+ }
35193
+ const fresh = await this.bootstrapFn();
35194
+ this.tokens = fresh;
35195
+ try {
35196
+ await this.persist(fresh);
35197
+ } catch (err) {
35198
+ throw err instanceof StatePersistenceError ? err.cause : err;
35199
+ }
35200
+ return fresh;
34900
35201
  }
34901
35202
  /**
34902
35203
  * Single-flight refresh. Concurrent callers share one in-flight promise; it is
34903
35204
  * cleared on settle (success or failure) so a subsequent refresh can proceed.
34904
35205
  */
34905
35206
  refreshNow() {
34906
- if (!this.inFlight) {
34907
- const rt = this.refreshToken;
34908
- if (rt === void 0) {
34909
- return Promise.reject(new Error("TokenManager: cannot refresh \u2014 no refresh token is available."));
34910
- }
34911
- this.inFlight = (async () => {
34912
- const tok = await this.refreshFn(rt);
34913
- this.accessToken = tok.accessToken;
34914
- if (tok.refreshToken !== void 0 && tok.refreshToken !== "") {
34915
- this.refreshToken = tok.refreshToken;
34916
- }
34917
- this.expiresAt = tok.expiresAt;
34918
- })().finally(() => {
35207
+ if (this.inFlight === void 0) {
35208
+ this.inFlight = this.runRefresh().finally(() => {
34919
35209
  this.inFlight = void 0;
34920
35210
  });
34921
35211
  }
34922
35212
  return this.inFlight;
34923
35213
  }
35214
+ /** One refresh attempt against the current refresh token. */
35215
+ async runRefresh() {
35216
+ const current = this.tokens ?? await this.ensureTokens();
35217
+ const rt = current.refreshToken;
35218
+ if (rt === void 0) {
35219
+ throw new Error("TokenManager: cannot refresh \u2014 no refresh token is available.");
35220
+ }
35221
+ const tok = await this.refreshFn(rt);
35222
+ this.tokens = {
35223
+ accessToken: tok.accessToken,
35224
+ // Rotation is optional: keep the current refresh token when none comes back.
35225
+ refreshToken: tok.refreshToken !== void 0 && tok.refreshToken !== "" ? tok.refreshToken : rt,
35226
+ expiresAt: tok.expiresAt
35227
+ };
35228
+ await this.persist(this.tokens);
35229
+ }
35230
+ /**
35231
+ * Recover from a refresh the current credential could not satisfy — commonly
35232
+ * a refresh token restored from a previous process and revoked since. Without
35233
+ * a bootstrap to fall back on this is terminal; with one, re-minting beats
35234
+ * staying broken forever. Shared so the two entry points cannot diverge.
35235
+ */
35236
+ async reBootstrap(err) {
35237
+ if (err instanceof StatePersistenceError)
35238
+ throw err.cause;
35239
+ if (this.bootstrapFn === void 0)
35240
+ throw err;
35241
+ if (!this.isRefreshRevokedFn(err))
35242
+ throw err;
35243
+ this.tokens = void 0;
35244
+ await this.clearPersisted();
35245
+ return this.ensureTokens();
35246
+ }
34924
35247
  /** Get a valid access token, refreshing proactively inside the skew window. */
34925
35248
  async getAccessToken() {
34926
- if (this.needsRefresh())
34927
- await this.refreshNow();
34928
- return this.accessToken;
35249
+ let tokens = this.tokens ?? await this.ensureTokens();
35250
+ if (this.needsRefresh()) {
35251
+ try {
35252
+ await this.refreshNow();
35253
+ } catch (err) {
35254
+ return (await this.reBootstrap(err)).accessToken;
35255
+ }
35256
+ tokens = this.tokens ?? tokens;
35257
+ }
35258
+ return tokens.accessToken;
34929
35259
  }
34930
- /** Current absolute expiry (epoch ms). */
35260
+ /** Current absolute expiry (epoch ms), or `0` before the first bootstrap. */
34931
35261
  getExpiresAt() {
34932
- return this.expiresAt;
35262
+ return this.tokens?.expiresAt ?? 0;
34933
35263
  }
34934
35264
  /**
34935
35265
  * Run an authenticated request with reactive 401-replay. `call` receives a
@@ -34947,16 +35277,21 @@ var TokenManager = class {
34947
35277
  const usedToken = await this.getAccessToken();
34948
35278
  let res = await call(usedToken);
34949
35279
  if (res.status === 401) {
34950
- if (this.accessToken === usedToken)
34951
- await this.refreshNow();
34952
- res = await call(this.accessToken);
35280
+ if (this.tokens?.accessToken === usedToken) {
35281
+ try {
35282
+ await this.refreshNow();
35283
+ } catch (err) {
35284
+ await this.reBootstrap(err);
35285
+ }
35286
+ }
35287
+ res = await call(this.tokens?.accessToken ?? usedToken);
34953
35288
  }
34954
35289
  return res;
34955
35290
  }
34956
35291
  };
34957
35292
 
34958
35293
  // src/client.ts
34959
- import { dirname, join as join4 } from "path";
35294
+ import { dirname as dirname2, join as join5 } from "path";
34960
35295
  import { fileURLToPath } from "url";
34961
35296
 
34962
35297
  // node_modules/@fetchproxy/protocol/dist/frames.js
@@ -36133,7 +36468,7 @@ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
36133
36468
  async function electRole(opts) {
36134
36469
  const server = createServer();
36135
36470
  const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
36136
- return new Promise((resolve2, reject) => {
36471
+ return new Promise((resolve3, reject) => {
36137
36472
  let timer;
36138
36473
  const clearBindTimer = () => {
36139
36474
  if (timer) {
@@ -36149,7 +36484,7 @@ async function electRole(opts) {
36149
36484
  server.close();
36150
36485
  } catch {
36151
36486
  }
36152
- resolve2({ role: "peer" });
36487
+ resolve3({ role: "peer" });
36153
36488
  } else {
36154
36489
  reject(e);
36155
36490
  }
@@ -36157,7 +36492,7 @@ async function electRole(opts) {
36157
36492
  const onListening = () => {
36158
36493
  clearBindTimer();
36159
36494
  server.removeListener("error", onError);
36160
- resolve2({ role: "host", server });
36495
+ resolve3({ role: "host", server });
36161
36496
  };
36162
36497
  server.once("error", onError);
36163
36498
  server.once("listening", onListening);
@@ -36319,16 +36654,16 @@ async function awaitSessionReady(ready, opts) {
36319
36654
 
36320
36655
  // node_modules/@fetchproxy/server/dist/extension-trust.js
36321
36656
  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";
36657
+ import { join as join4 } from "node:path";
36323
36658
 
36324
36659
  // node_modules/@fetchproxy/server/dist/identity.js
36325
36660
  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";
36661
+ import { join as join3 } from "node:path";
36662
+ import { homedir as homedir3 } from "node:os";
36328
36663
  var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
36329
36664
  var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
36330
36665
  function defaultIdentityDir() {
36331
- return join2(homedir2(), ".fetchproxy", "identity");
36666
+ return join3(homedir3(), ".fetchproxy", "identity");
36332
36667
  }
36333
36668
  function safeIdentityFileBase(serverName) {
36334
36669
  if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
@@ -36338,7 +36673,7 @@ function safeIdentityFileBase(serverName) {
36338
36673
  }
36339
36674
  async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
36340
36675
  const safeFile = safeIdentityFileBase(serverName);
36341
- const path = join2(dir, `${safeFile}.json`);
36676
+ const path = join3(dir, `${safeFile}.json`);
36342
36677
  await mkdir(dir, { recursive: true, mode: 448 });
36343
36678
  try {
36344
36679
  const raw = await readFile(path, "utf8");
@@ -36410,13 +36745,13 @@ function decideExtensionTrust(args) {
36410
36745
  };
36411
36746
  }
36412
36747
  function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
36413
- return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
36748
+ return join4(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
36414
36749
  }
36415
36750
  function extensionTrustPathHint(serverName) {
36416
36751
  try {
36417
36752
  return extensionTrustPath(serverName);
36418
36753
  } catch {
36419
- return join3(defaultIdentityDir(), "<server-name>.extension-trust.json");
36754
+ return join4(defaultIdentityDir(), "<server-name>.extension-trust.json");
36420
36755
  }
36421
36756
  }
36422
36757
  function isPin(x) {
@@ -36503,8 +36838,8 @@ async function startHost(opts) {
36503
36838
  let rejectOwnSession;
36504
36839
  let ownSessionReady;
36505
36840
  function resetSessionPromise() {
36506
- ownSessionReady = new Promise((resolve2, reject) => {
36507
- resolveOwnSession = resolve2;
36841
+ ownSessionReady = new Promise((resolve3, reject) => {
36842
+ resolveOwnSession = resolve3;
36508
36843
  rejectOwnSession = reject;
36509
36844
  });
36510
36845
  ownSessionReady.catch(() => {
@@ -36725,7 +37060,7 @@ async function startHost(opts) {
36725
37060
  });
36726
37061
  });
36727
37062
  return {
36728
- close: () => new Promise((resolve2) => {
37063
+ close: () => new Promise((resolve3) => {
36729
37064
  for (const client2 of wss.clients) {
36730
37065
  try {
36731
37066
  client2.terminate();
@@ -36733,7 +37068,7 @@ async function startHost(opts) {
36733
37068
  }
36734
37069
  }
36735
37070
  wss.close(() => {
36736
- opts.httpServer.close(() => resolve2());
37071
+ opts.httpServer.close(() => resolve3());
36737
37072
  });
36738
37073
  }),
36739
37074
  sendOwnInner: async (inner) => {
@@ -36763,8 +37098,8 @@ async function startHost(opts) {
36763
37098
  var enc3 = new TextEncoder();
36764
37099
  async function startPeer(opts) {
36765
37100
  const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`);
36766
- await new Promise((resolve2, reject) => {
36767
- ws.once("open", () => resolve2());
37101
+ await new Promise((resolve3, reject) => {
37102
+ ws.once("open", () => resolve3());
36768
37103
  ws.once("error", reject);
36769
37104
  });
36770
37105
  const hello = await buildServerHello({
@@ -36794,8 +37129,8 @@ async function startPeer(opts) {
36794
37129
  let pendingPairCode = null;
36795
37130
  let resolveFirstReady;
36796
37131
  let rejectFirstReady;
36797
- const sessionPromise = new Promise((resolve2, reject) => {
36798
- resolveFirstReady = resolve2;
37132
+ const sessionPromise = new Promise((resolve3, reject) => {
37133
+ resolveFirstReady = resolve3;
36799
37134
  rejectFirstReady = reject;
36800
37135
  });
36801
37136
  let extensionHello = null;
@@ -36996,6 +37331,7 @@ function classifyBridgeError(err) {
36996
37331
  }
36997
37332
 
36998
37333
  // node_modules/@fetchproxy/server/dist/ws-server.js
37334
+ import { isIP } from "node:net";
36999
37335
  function envWsPort() {
37000
37336
  const raw = process.env.FETCHPROXY_WS_PORT;
37001
37337
  if (raw === void 0 || raw.trim() === "")
@@ -37007,6 +37343,15 @@ function envWsPort() {
37007
37343
  return void 0;
37008
37344
  return port;
37009
37345
  }
37346
+ function envWsHost() {
37347
+ const raw = process.env.FETCHPROXY_WS_HOST;
37348
+ if (raw === void 0)
37349
+ return void 0;
37350
+ const host = raw.trim();
37351
+ if (host === "" || isIP(host) === 0)
37352
+ return void 0;
37353
+ return host;
37354
+ }
37010
37355
  var FetchproxyProtocolError = class extends Error {
37011
37356
  constructor(message) {
37012
37357
  super(message);
@@ -37255,7 +37600,7 @@ var FetchproxyServer = class {
37255
37600
  }
37256
37601
  this.opts = {
37257
37602
  port: opts.port ?? envWsPort() ?? 37149,
37258
- host: opts.host ?? "127.0.0.1",
37603
+ host: opts.host ?? envWsHost() ?? "127.0.0.1",
37259
37604
  serverName: opts.serverName,
37260
37605
  version: opts.version,
37261
37606
  domains: [...opts.domains],
@@ -37541,6 +37886,7 @@ var FetchproxyServer = class {
37541
37886
  return {
37542
37887
  role: this.role,
37543
37888
  port: this.opts.port,
37889
+ host: this.opts.host,
37544
37890
  serverVersion: this.opts.version,
37545
37891
  fetchTimeoutMs: this.opts.fetchTimeoutMs ?? 0,
37546
37892
  bridgeReviveDelayMs: this.opts.bridgeReviveDelayMs ?? 0,
@@ -37684,8 +38030,8 @@ var FetchproxyServer = class {
37684
38030
  async _fetchOnceWithTimeout(init) {
37685
38031
  const id = this.nextRequestId++;
37686
38032
  const inner = { type: "request", id, op: "fetch", init };
37687
- const pending = new Promise((resolve2) => {
37688
- this.pending.set(id, resolve2);
38033
+ const pending = new Promise((resolve3) => {
38034
+ this.pending.set(id, resolve3);
37689
38035
  });
37690
38036
  await this.sendInnerFrame(inner);
37691
38037
  const timeoutMs = this.opts.fetchTimeoutMs;
@@ -37696,12 +38042,12 @@ var FetchproxyServer = class {
37696
38042
  try {
37697
38043
  return await Promise.race([
37698
38044
  pending,
37699
- new Promise((resolve2) => {
38045
+ new Promise((resolve3) => {
37700
38046
  timer = setTimeout(() => {
37701
38047
  this.pending.delete(id);
37702
38048
  const elapsedMs = Date.now() - start;
37703
38049
  const error51 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
37704
- resolve2({
38050
+ resolve3({
37705
38051
  ok: false,
37706
38052
  error: error51,
37707
38053
  kind: "timeout",
@@ -38063,8 +38409,8 @@ var FetchproxyServer = class {
38063
38409
  const tabUrl = `https://${host}/`;
38064
38410
  inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
38065
38411
  }
38066
- const pending = new Promise((resolve2) => {
38067
- this.pendingReadCookies.set(id, resolve2);
38412
+ const pending = new Promise((resolve3) => {
38413
+ this.pendingReadCookies.set(id, resolve3);
38068
38414
  });
38069
38415
  await this.sendInnerFrame(inner);
38070
38416
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
@@ -38122,8 +38468,8 @@ var FetchproxyServer = class {
38122
38468
  ...cookiePath !== void 0 ? { path: cookiePath } : {}
38123
38469
  }
38124
38470
  };
38125
- const pending = new Promise((resolve2, reject) => {
38126
- this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
38471
+ const pending = new Promise((resolve3, reject) => {
38472
+ this.pendingWriteCookies.set(id, { resolve: resolve3, reject });
38127
38473
  });
38128
38474
  await this.sendInnerFrame(inner);
38129
38475
  return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
@@ -38185,8 +38531,8 @@ var FetchproxyServer = class {
38185
38531
  ...opts.pointers ? { pointers: { ...opts.pointers } } : {}
38186
38532
  }
38187
38533
  };
38188
- const pending = new Promise((resolve2, reject) => {
38189
- this.pendingStorage.set(id, { resolve: resolve2, reject });
38534
+ const pending = new Promise((resolve3, reject) => {
38535
+ this.pendingStorage.set(id, { resolve: resolve3, reject });
38190
38536
  });
38191
38537
  await this.sendInnerFrame(inner);
38192
38538
  return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
@@ -38290,8 +38636,8 @@ var FetchproxyServer = class {
38290
38636
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38291
38637
  }
38292
38638
  };
38293
- const pending = new Promise((resolve2, reject) => {
38294
- this.pendingCapture.set(id, { resolve: resolve2, reject });
38639
+ const pending = new Promise((resolve3, reject) => {
38640
+ this.pendingCapture.set(id, { resolve: resolve3, reject });
38295
38641
  });
38296
38642
  await this.sendInnerFrame(inner);
38297
38643
  return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
@@ -38375,8 +38721,8 @@ var FetchproxyServer = class {
38375
38721
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38376
38722
  }
38377
38723
  };
38378
- const pending = new Promise((resolve2, reject) => {
38379
- this.pendingRedirect.set(id, { resolve: resolve2, reject });
38724
+ const pending = new Promise((resolve3, reject) => {
38725
+ this.pendingRedirect.set(id, { resolve: resolve3, reject });
38380
38726
  });
38381
38727
  await this.sendInnerFrame(inner);
38382
38728
  return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
@@ -38457,8 +38803,8 @@ var FetchproxyServer = class {
38457
38803
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38458
38804
  }
38459
38805
  };
38460
- const pending = new Promise((resolve2, reject) => {
38461
- this.pendingDownload.set(id, { resolve: resolve2, reject });
38806
+ const pending = new Promise((resolve3, reject) => {
38807
+ this.pendingDownload.set(id, { resolve: resolve3, reject });
38462
38808
  });
38463
38809
  await this.sendInnerFrame(inner);
38464
38810
  return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
@@ -38505,8 +38851,8 @@ var FetchproxyServer = class {
38505
38851
  keys: [...opts.keys]
38506
38852
  }
38507
38853
  };
38508
- const pending = new Promise((resolve2, reject) => {
38509
- this.pendingIdb.set(id, { resolve: resolve2, reject });
38854
+ const pending = new Promise((resolve3, reject) => {
38855
+ this.pendingIdb.set(id, { resolve: resolve3, reject });
38510
38856
  });
38511
38857
  await this.sendInnerFrame(inner);
38512
38858
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
@@ -38545,8 +38891,8 @@ var FetchproxyServer = class {
38545
38891
  op: "read_dom",
38546
38892
  init: { origin, names: [...opts.names] }
38547
38893
  };
38548
- const pending = new Promise((resolve2, reject) => {
38549
- this.pendingStorage.set(id, { resolve: resolve2, reject });
38894
+ const pending = new Promise((resolve3, reject) => {
38895
+ this.pendingStorage.set(id, { resolve: resolve3, reject });
38550
38896
  });
38551
38897
  await this.sendInnerFrame(inner);
38552
38898
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
@@ -38591,8 +38937,8 @@ var FetchproxyServer = class {
38591
38937
  ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
38592
38938
  }
38593
38939
  };
38594
- const pending = new Promise((resolve2, reject) => {
38595
- this.pendingGraphql.set(id, { resolve: resolve2, reject });
38940
+ const pending = new Promise((resolve3, reject) => {
38941
+ this.pendingGraphql.set(id, { resolve: resolve3, reject });
38596
38942
  });
38597
38943
  await this.sendInnerFrame(inner);
38598
38944
  return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
@@ -39085,7 +39431,7 @@ async function loginWithPassword(username, password) {
39085
39431
  // package.json
39086
39432
  var package_default = {
39087
39433
  name: "ofw-mcp",
39088
- version: "2.11.0",
39434
+ version: "2.13.0",
39089
39435
  license: "MIT",
39090
39436
  mcpName: "io.github.chrischall/ofw-mcp",
39091
39437
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
@@ -39106,19 +39452,21 @@ var package_default = {
39106
39452
  ".claude-plugin",
39107
39453
  "skills",
39108
39454
  ".mcp.json",
39109
- "server.json"
39455
+ "server.json",
39456
+ "mint.yaml"
39110
39457
  ],
39111
39458
  scripts: {
39112
39459
  build: "tsc && npm run bundle",
39113
39460
  bundle: `esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js`,
39114
39461
  dev: "node --env-file=.env dist/index.js",
39115
- test: "vitest run",
39116
- "test:coverage": "vitest run --coverage",
39117
- "test:watch": "vitest"
39462
+ test: "npm run typecheck && vitest run",
39463
+ "test:coverage": "npm run typecheck && vitest run --coverage",
39464
+ "test:watch": "vitest",
39465
+ typecheck: "tsc -p tsconfig.json --noEmit"
39118
39466
  },
39119
39467
  dependencies: {
39120
- "@chrischall/mcp-utils": "^0.14.0",
39121
- "@fetchproxy/bootstrap": "^2.0.0",
39468
+ "@chrischall/mcp-utils": "^0.18.0",
39469
+ "@fetchproxy/bootstrap": "^2.2.0",
39122
39470
  "@modelcontextprotocol/sdk": "^1.29.0",
39123
39471
  dotenv: "^17.4.2",
39124
39472
  zod: "^4.4.3"
@@ -39137,66 +39485,118 @@ function fetchproxyDisabled() {
39137
39485
  return parseBoolEnv("OFW_DISABLE_FETCHPROXY");
39138
39486
  }
39139
39487
  async function resolveAuth() {
39488
+ const pattern = {};
39140
39489
  const username = readEnvVar("OFW_USERNAME");
39141
39490
  const password = readEnvVar("OFW_PASSWORD");
39142
39491
  if (username && password) {
39143
- const { token, expiresAt } = await loginWithPassword(username, password);
39144
- return { token, expiresAt, source: "env" };
39492
+ pattern.sessionScrape = async () => {
39493
+ const { token, expiresAt: expiresAt2 } = await loginWithPassword(username, password);
39494
+ return { credential: token, source: "env", expiresAt: expiresAt2 };
39495
+ };
39145
39496
  }
39146
39497
  if (!fetchproxyDisabled()) {
39147
- try {
39148
- const session = await bootstrap({
39149
- serverName: package_default.name,
39150
- version: package_default.version,
39151
- // OFW serves both ofw.ourfamilywizard.com and www.ourfamilywizard.com;
39152
- // the API + auth token live on the apex. The extension matches on
39153
- // suffix, so listing the apex covers both.
39154
- domains: ["ourfamilywizard.com"],
39155
- declare: {
39156
- cookies: [],
39157
- // The web app stores the Bearer token in localStorage["auth"] and
39158
- // its expiry (ISO string) in localStorage["tokenExpiry"]. Mirroring
39159
- // both means our 401-replay logic can be slightly smarter, and the
39160
- // expiry surfaces correctly in diagnostics.
39161
- localStorage: ["auth", "tokenExpiry"],
39162
- sessionStorage: [],
39163
- captureHeaders: []
39498
+ pattern.fetchproxy = async () => {
39499
+ try {
39500
+ const session = await bootstrap({
39501
+ serverName: package_default.name,
39502
+ version: package_default.version,
39503
+ // OFW serves both ofw.ourfamilywizard.com and www.ourfamilywizard.com;
39504
+ // the API + auth token live on the apex. The extension matches on
39505
+ // suffix, so listing the apex covers both.
39506
+ domains: ["ourfamilywizard.com"],
39507
+ declare: {
39508
+ cookies: [],
39509
+ // The web app stores the Bearer token in localStorage["auth"] and
39510
+ // its expiry (ISO string) in localStorage["tokenExpiry"]. Mirroring
39511
+ // both means our 401-replay logic can be slightly smarter, and the
39512
+ // expiry surfaces correctly in diagnostics.
39513
+ localStorage: ["auth", "tokenExpiry"],
39514
+ sessionStorage: [],
39515
+ captureHeaders: []
39516
+ }
39517
+ });
39518
+ const token = session.localStorage["auth"];
39519
+ const expiryRaw = session.localStorage["tokenExpiry"];
39520
+ if (!token) {
39521
+ throw new Error(
39522
+ 'localStorage["auth"] missing on ourfamilywizard.com. Sign into OFW in your browser (with the fetchproxy extension installed) and retry.'
39523
+ );
39164
39524
  }
39165
- });
39166
- const token = session.localStorage["auth"];
39167
- const expiryRaw = session.localStorage["tokenExpiry"];
39168
- if (!token) {
39169
- throw new Error(
39170
- 'localStorage["auth"] missing on ourfamilywizard.com. Sign into OFW in your browser (with the fetchproxy extension installed) and retry.'
39171
- );
39172
- }
39173
- return {
39174
- token,
39175
- expiresAt: expiryRaw ? new Date(expiryRaw) : void 0,
39176
- source: "fetchproxy"
39177
- };
39178
- } catch (e) {
39179
- if (classifyBridgeError(e) === "bridge_down") {
39180
- const downErr = e;
39525
+ return {
39526
+ credential: token,
39527
+ source: "fetchproxy",
39528
+ ...expiryRaw ? { expiresAt: new Date(expiryRaw) } : {}
39529
+ };
39530
+ } catch (e) {
39531
+ if (classifyBridgeError(e) === "bridge_down") {
39532
+ const downErr = e;
39533
+ throw new Error(
39534
+ `OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
39535
+ );
39536
+ }
39537
+ const msg = e instanceof Error ? e.message : String(e);
39181
39538
  throw new Error(
39182
- `OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
39539
+ `OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`
39183
39540
  );
39184
39541
  }
39185
- const msg = e instanceof Error ? e.message : String(e);
39186
- throw new Error(
39187
- `OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`
39188
- );
39189
- }
39542
+ };
39543
+ }
39544
+ if (!pattern.sessionScrape && !pattern.fetchproxy) {
39545
+ throw new Error(
39546
+ "OFW auth: set OFW_USERNAME + OFW_PASSWORD, or install the fetchproxy extension and sign into ourfamilywizard.com (unset OFW_DISABLE_FETCHPROXY if it is set)."
39547
+ );
39190
39548
  }
39191
- throw new Error(
39192
- "OFW auth: set OFW_USERNAME + OFW_PASSWORD, or install the fetchproxy extension and sign into ourfamilywizard.com (unset OFW_DISABLE_FETCHPROXY if it is set)."
39549
+ const { credential, source, expiresAt } = await resolveAuthPattern(pattern);
39550
+ return {
39551
+ token: credential,
39552
+ // The pattern's `source` is a free-form string; ours is a two-value union,
39553
+ // and both resolvers above set it to a member of that union.
39554
+ source,
39555
+ ...expiresAt ? { expiresAt } : {}
39556
+ };
39557
+ }
39558
+
39559
+ // src/session-cache.ts
39560
+ function sessionCachePath(env = process.env) {
39561
+ return resolveStateFile({
39562
+ env,
39563
+ envVar: "OFW_SESSION_FILE",
39564
+ subdir: ".ofw-mcp",
39565
+ fileName: "session.json"
39566
+ });
39567
+ }
39568
+ function isTokens(raw) {
39569
+ if (raw === null || typeof raw !== "object") return false;
39570
+ const t = raw;
39571
+ return typeof t.accessToken === "string" && t.accessToken !== "" && typeof t.expiresAt === "number" && (t.refreshToken === void 0 || typeof t.refreshToken === "string");
39572
+ }
39573
+ function createSessionCache(opts = {}) {
39574
+ const env = opts.env ?? process.env;
39575
+ if (opts.injectedResolver === true) return null;
39576
+ if (!parseBoolEnv("OFW_SESSION_CACHE", { env, default: true })) return null;
39577
+ const username = readEnvVar("OFW_USERNAME", { env });
39578
+ const password = readEnvVar("OFW_PASSWORD", { env });
39579
+ if (username === void 0 || password === void 0) return null;
39580
+ return createFileStatePersistence({
39581
+ filePath: sessionCachePath(env),
39582
+ // Joined on a NUL, written as an escape rather than a literal byte: a
39583
+ // password may contain spaces, so a space-joined pair could collide with
39584
+ // a different pair by shifting the boundary between the two halves.
39585
+ boundTo: [username.trim().toLowerCase(), password].join("\0"),
39586
+ validate: (raw) => isTokens(raw) ? raw : null
39587
+ });
39588
+ }
39589
+ function reportCacheWriteFailure(err) {
39590
+ const detail = err instanceof Error ? err.message : String(err);
39591
+ console.error(
39592
+ `[ofw-mcp] could not cache the session token (${detail}); continuing without the cache \u2014 every restart will re-authenticate until this is fixed.`
39193
39593
  );
39194
39594
  }
39195
39595
 
39196
39596
  // src/client.ts
39197
39597
  try {
39198
- const dir = dirname(fileURLToPath(import.meta.url));
39199
- await loadDotenvSafely({ path: join4(dir, "..", ".env") });
39598
+ const dir = dirname2(fileURLToPath(import.meta.url));
39599
+ await loadDotenvSafely({ path: join5(dir, "..", ".env") });
39200
39600
  } catch {
39201
39601
  }
39202
39602
  function parseContentDispositionFilename(cd) {
@@ -39227,9 +39627,9 @@ var OFWClient = class {
39227
39627
  // Bearer-token lifecycle is delegated to the shared, race-safe TokenManager
39228
39628
  // (proactive refresh inside the skew window, single-flight refresh so a burst
39229
39629
  // of concurrent callers coalesces onto ONE `resolveAuth()`, and a 401-replay
39230
- // guarded against double-refresh). It is created lazily, seeded with an
39231
- // already-expired placeholder token so the first request drives the refresh
39232
- // callback — i.e. the original "log in on first request" behavior.
39630
+ // guarded against double-refresh). It is created lazily, and mints on first
39631
+ // use through the function form of `initial` — see `mint` below for why that
39632
+ // form rather than a seeded placeholder.
39233
39633
  tokenManager;
39234
39634
  // Optional injected auth resolver. When set, the refresh callback uses it
39235
39635
  // instead of the module-level global `resolveAuth` (env-var → fetchproxy
@@ -39243,22 +39643,28 @@ var OFWClient = class {
39243
39643
  }
39244
39644
  getTokenManager() {
39245
39645
  if (!this.tokenManager) {
39646
+ const mint = async () => {
39647
+ const { token, expiresAt } = await (this.authResolver ?? resolveAuth)();
39648
+ return {
39649
+ accessToken: token,
39650
+ refreshToken: OFW_REFRESH_SENTINEL,
39651
+ expiresAt: (expiresAt ?? new Date(Date.now() + OFW_TOKEN_TTL_MS)).getTime()
39652
+ };
39653
+ };
39246
39654
  this.tokenManager = new TokenManager({
39247
- initial: { accessToken: "", refreshToken: OFW_REFRESH_SENTINEL, expiresAt: 0 },
39655
+ initial: mint,
39656
+ persistence: createSessionCache({ injectedResolver: this.authResolver !== void 0 }) ?? void 0,
39657
+ onPersistError: reportCacheWriteFailure,
39658
+ // A failed renewal IS a failed login here, so the library's
39659
+ // re-mint-on-revoked recovery would just repeat the call that failed.
39660
+ isRefreshRevoked: () => false,
39248
39661
  skewMs: OFW_TOKEN_EXPIRY_SKEW_MS,
39249
39662
  // Map OFW's mint/refresh onto the refresh callback. `resolveAuth()`
39250
39663
  // returns a token and a best-effort expiry; when the fetchproxy path
39251
39664
  // can't supply one we fall back to the same 6h estimate the password
39252
39665
  // path uses (the 401-replay covers a wrong guess). We re-arm the
39253
39666
  // sentinel so the manager can refresh again later.
39254
- refresh: async () => {
39255
- const { token, expiresAt } = await (this.authResolver ?? resolveAuth)();
39256
- return {
39257
- accessToken: token,
39258
- refreshToken: OFW_REFRESH_SENTINEL,
39259
- expiresAt: (expiresAt ?? new Date(Date.now() + OFW_TOKEN_TTL_MS)).getTime()
39260
- };
39261
- }
39667
+ refresh: mint
39262
39668
  });
39263
39669
  }
39264
39670
  return this.tokenManager;
@@ -40082,25 +40488,25 @@ async function syncAll(client2, opts, store) {
40082
40488
 
40083
40489
  // src/config.ts
40084
40490
  import { createHash } from "node:crypto";
40085
- import { homedir as homedir3 } from "node:os";
40086
- import { join as join5 } from "node:path";
40491
+ import { homedir as homedir4 } from "node:os";
40492
+ import { join as join6 } from "node:path";
40087
40493
  function readCacheIdentity() {
40088
40494
  return readEnvVar("OFW_CACHE_IDENTITY") ?? readEnvVar("OFW_USERNAME") ?? "_default";
40089
40495
  }
40090
40496
  function getCacheDir() {
40091
40497
  const override = process.env.OFW_CACHE_DIR;
40092
40498
  if (override && override.trim().length > 0) return override.trim();
40093
- return join5(homedir3(), ".cache", "ofw-mcp");
40499
+ return join6(homedir4(), ".cache", "ofw-mcp");
40094
40500
  }
40095
40501
  function getCacheDbPath() {
40096
40502
  const identity = readCacheIdentity();
40097
40503
  const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
40098
- return join5(getCacheDir(), `${hash2}.db`);
40504
+ return join6(getCacheDir(), `${hash2}.db`);
40099
40505
  }
40100
40506
  function getAttachmentsDir() {
40101
40507
  const override = process.env.OFW_ATTACHMENTS_DIR;
40102
40508
  if (override && override.trim().length > 0) return override.trim();
40103
- return join5(homedir3(), "Downloads", "ofw-mcp");
40509
+ return join6(homedir4(), "Downloads", "ofw-mcp");
40104
40510
  }
40105
40511
  function getWriteMode() {
40106
40512
  const raw = process.env.OFW_WRITE_MODE;
@@ -41470,8 +41876,8 @@ async function extractAttachment(bytes, mimeType, fileName, opts = {}) {
41470
41876
  }
41471
41877
 
41472
41878
  // src/tools/attachments.ts
41473
- import { readFileSync, statSync, mkdirSync, writeFileSync } from "node:fs";
41474
- import { basename, dirname as dirname2, extname } from "node:path";
41879
+ import { readFileSync as readFileSync2, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
41880
+ import { basename, dirname as dirname3, extname } from "node:path";
41475
41881
  var MIME_BY_EXT = {
41476
41882
  ".pdf": "application/pdf",
41477
41883
  ".png": "image/png",
@@ -41545,14 +41951,14 @@ var NodeAttachmentIO = class {
41545
41951
  }
41546
41952
  readDownloaded(path) {
41547
41953
  try {
41548
- return readFileSync(path);
41954
+ return readFileSync2(path);
41549
41955
  } catch {
41550
41956
  return null;
41551
41957
  }
41552
41958
  }
41553
41959
  writeDownload(dest, bytes) {
41554
- mkdirSync(dirname2(dest), { recursive: true });
41555
- writeFileSync(dest, bytes);
41960
+ mkdirSync2(dirname3(dest), { recursive: true });
41961
+ writeFileSync2(dest, bytes);
41556
41962
  }
41557
41963
  };
41558
41964
 
@@ -41616,7 +42022,7 @@ async function buildInlineDelivery(input) {
41616
42022
  }
41617
42023
 
41618
42024
  // src/tools/messages.ts
41619
- import { basename as basename2, join as join6 } from "node:path";
42025
+ import { basename as basename2, join as join7 } from "node:path";
41620
42026
 
41621
42027
  // src/tools/pagination.ts
41622
42028
  function pageState(input) {
@@ -42711,9 +43117,9 @@ ${text}` : text);
42711
43117
  if (args.saveTo) {
42712
43118
  const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
42713
43119
  const abs = expandPath2(args.saveTo);
42714
- dest = isDirArg ? join6(abs, `${fileId}-${safeName}`) : abs;
43120
+ dest = isDirArg ? join7(abs, `${fileId}-${safeName}`) : abs;
42715
43121
  } else {
42716
- dest = join6(getAttachmentsDir(), `${fileId}-${safeName}`);
43122
+ dest = join7(getAttachmentsDir(), `${fileId}-${safeName}`);
42717
43123
  }
42718
43124
  const extractOnDisk = args.extract === true;
42719
43125
  if (!args.force && cached2.downloadedPath === dest) {
@@ -43209,8 +43615,8 @@ function registerJournalTools(server, client2) {
43209
43615
 
43210
43616
  // src/cache/node.ts
43211
43617
  import { DatabaseSync } from "node:sqlite";
43212
- import { mkdirSync as mkdirSync2, chmodSync, existsSync } from "node:fs";
43213
- import { dirname as dirname3 } from "node:path";
43618
+ import { mkdirSync as mkdirSync3, chmodSync as chmodSync2, existsSync as existsSync2 } from "node:fs";
43619
+ import { dirname as dirname4 } from "node:path";
43214
43620
 
43215
43621
  // src/cache/store.ts
43216
43622
  function rowFromDb(r) {
@@ -43794,10 +44200,10 @@ var NodeSqlDriver = class {
43794
44200
  }
43795
44201
  };
43796
44202
  function enforceCachePermissions(dbPath) {
43797
- chmodSync(dirname3(dbPath), 448);
43798
- chmodSync(dbPath, 384);
44203
+ chmodSync2(dirname4(dbPath), 448);
44204
+ chmodSync2(dbPath, 384);
43799
44205
  for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
43800
- if (existsSync(sibling)) chmodSync(sibling, 384);
44206
+ if (existsSync2(sibling)) chmodSync2(sibling, 384);
43801
44207
  }
43802
44208
  }
43803
44209
  var OFWCache = class _OFWCache extends LocalCacheStore {
@@ -43808,7 +44214,7 @@ var OFWCache = class _OFWCache extends LocalCacheStore {
43808
44214
  db;
43809
44215
  static open(path) {
43810
44216
  const memory = path === ":memory:";
43811
- if (!memory) mkdirSync2(dirname3(path), { recursive: true });
44217
+ if (!memory) mkdirSync3(dirname4(path), { recursive: true });
43812
44218
  const db = new DatabaseSync(path);
43813
44219
  if (!memory) enforceCachePermissions(path);
43814
44220
  db.exec("PRAGMA journal_mode = WAL");
@@ -43838,7 +44244,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
43838
44244
  var nodeAttachmentIO = new NodeAttachmentIO();
43839
44245
  await runMcp({
43840
44246
  name: "ofw",
43841
- version: "2.11.0",
44247
+ version: "2.13.0",
43842
44248
  // x-release-please-version
43843
44249
  deps: client,
43844
44250
  tools: [