ofw-mcp 2.11.0 → 2.12.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;
@@ -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);
@@ -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,
@@ -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;
@@ -37684,8 +38019,8 @@ var FetchproxyServer = class {
37684
38019
  async _fetchOnceWithTimeout(init) {
37685
38020
  const id = this.nextRequestId++;
37686
38021
  const inner = { type: "request", id, op: "fetch", init };
37687
- const pending = new Promise((resolve2) => {
37688
- this.pending.set(id, resolve2);
38022
+ const pending = new Promise((resolve3) => {
38023
+ this.pending.set(id, resolve3);
37689
38024
  });
37690
38025
  await this.sendInnerFrame(inner);
37691
38026
  const timeoutMs = this.opts.fetchTimeoutMs;
@@ -37696,12 +38031,12 @@ var FetchproxyServer = class {
37696
38031
  try {
37697
38032
  return await Promise.race([
37698
38033
  pending,
37699
- new Promise((resolve2) => {
38034
+ new Promise((resolve3) => {
37700
38035
  timer = setTimeout(() => {
37701
38036
  this.pending.delete(id);
37702
38037
  const elapsedMs = Date.now() - start;
37703
38038
  const error51 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
37704
- resolve2({
38039
+ resolve3({
37705
38040
  ok: false,
37706
38041
  error: error51,
37707
38042
  kind: "timeout",
@@ -38063,8 +38398,8 @@ var FetchproxyServer = class {
38063
38398
  const tabUrl = `https://${host}/`;
38064
38399
  inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
38065
38400
  }
38066
- const pending = new Promise((resolve2) => {
38067
- this.pendingReadCookies.set(id, resolve2);
38401
+ const pending = new Promise((resolve3) => {
38402
+ this.pendingReadCookies.set(id, resolve3);
38068
38403
  });
38069
38404
  await this.sendInnerFrame(inner);
38070
38405
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
@@ -38122,8 +38457,8 @@ var FetchproxyServer = class {
38122
38457
  ...cookiePath !== void 0 ? { path: cookiePath } : {}
38123
38458
  }
38124
38459
  };
38125
- const pending = new Promise((resolve2, reject) => {
38126
- this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
38460
+ const pending = new Promise((resolve3, reject) => {
38461
+ this.pendingWriteCookies.set(id, { resolve: resolve3, reject });
38127
38462
  });
38128
38463
  await this.sendInnerFrame(inner);
38129
38464
  return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
@@ -38185,8 +38520,8 @@ var FetchproxyServer = class {
38185
38520
  ...opts.pointers ? { pointers: { ...opts.pointers } } : {}
38186
38521
  }
38187
38522
  };
38188
- const pending = new Promise((resolve2, reject) => {
38189
- this.pendingStorage.set(id, { resolve: resolve2, reject });
38523
+ const pending = new Promise((resolve3, reject) => {
38524
+ this.pendingStorage.set(id, { resolve: resolve3, reject });
38190
38525
  });
38191
38526
  await this.sendInnerFrame(inner);
38192
38527
  return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
@@ -38290,8 +38625,8 @@ var FetchproxyServer = class {
38290
38625
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38291
38626
  }
38292
38627
  };
38293
- const pending = new Promise((resolve2, reject) => {
38294
- this.pendingCapture.set(id, { resolve: resolve2, reject });
38628
+ const pending = new Promise((resolve3, reject) => {
38629
+ this.pendingCapture.set(id, { resolve: resolve3, reject });
38295
38630
  });
38296
38631
  await this.sendInnerFrame(inner);
38297
38632
  return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
@@ -38375,8 +38710,8 @@ var FetchproxyServer = class {
38375
38710
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38376
38711
  }
38377
38712
  };
38378
- const pending = new Promise((resolve2, reject) => {
38379
- this.pendingRedirect.set(id, { resolve: resolve2, reject });
38713
+ const pending = new Promise((resolve3, reject) => {
38714
+ this.pendingRedirect.set(id, { resolve: resolve3, reject });
38380
38715
  });
38381
38716
  await this.sendInnerFrame(inner);
38382
38717
  return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
@@ -38457,8 +38792,8 @@ var FetchproxyServer = class {
38457
38792
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38458
38793
  }
38459
38794
  };
38460
- const pending = new Promise((resolve2, reject) => {
38461
- this.pendingDownload.set(id, { resolve: resolve2, reject });
38795
+ const pending = new Promise((resolve3, reject) => {
38796
+ this.pendingDownload.set(id, { resolve: resolve3, reject });
38462
38797
  });
38463
38798
  await this.sendInnerFrame(inner);
38464
38799
  return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
@@ -38505,8 +38840,8 @@ var FetchproxyServer = class {
38505
38840
  keys: [...opts.keys]
38506
38841
  }
38507
38842
  };
38508
- const pending = new Promise((resolve2, reject) => {
38509
- this.pendingIdb.set(id, { resolve: resolve2, reject });
38843
+ const pending = new Promise((resolve3, reject) => {
38844
+ this.pendingIdb.set(id, { resolve: resolve3, reject });
38510
38845
  });
38511
38846
  await this.sendInnerFrame(inner);
38512
38847
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
@@ -38545,8 +38880,8 @@ var FetchproxyServer = class {
38545
38880
  op: "read_dom",
38546
38881
  init: { origin, names: [...opts.names] }
38547
38882
  };
38548
- const pending = new Promise((resolve2, reject) => {
38549
- this.pendingStorage.set(id, { resolve: resolve2, reject });
38883
+ const pending = new Promise((resolve3, reject) => {
38884
+ this.pendingStorage.set(id, { resolve: resolve3, reject });
38550
38885
  });
38551
38886
  await this.sendInnerFrame(inner);
38552
38887
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
@@ -38591,8 +38926,8 @@ var FetchproxyServer = class {
38591
38926
  ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
38592
38927
  }
38593
38928
  };
38594
- const pending = new Promise((resolve2, reject) => {
38595
- this.pendingGraphql.set(id, { resolve: resolve2, reject });
38929
+ const pending = new Promise((resolve3, reject) => {
38930
+ this.pendingGraphql.set(id, { resolve: resolve3, reject });
38596
38931
  });
38597
38932
  await this.sendInnerFrame(inner);
38598
38933
  return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
@@ -39085,7 +39420,7 @@ async function loginWithPassword(username, password) {
39085
39420
  // package.json
39086
39421
  var package_default = {
39087
39422
  name: "ofw-mcp",
39088
- version: "2.11.0",
39423
+ version: "2.12.0",
39089
39424
  license: "MIT",
39090
39425
  mcpName: "io.github.chrischall/ofw-mcp",
39091
39426
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
@@ -39106,18 +39441,20 @@ var package_default = {
39106
39441
  ".claude-plugin",
39107
39442
  "skills",
39108
39443
  ".mcp.json",
39109
- "server.json"
39444
+ "server.json",
39445
+ "mint.yaml"
39110
39446
  ],
39111
39447
  scripts: {
39112
39448
  build: "tsc && npm run bundle",
39113
39449
  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
39450
  dev: "node --env-file=.env dist/index.js",
39115
- test: "vitest run",
39116
- "test:coverage": "vitest run --coverage",
39117
- "test:watch": "vitest"
39451
+ test: "npm run typecheck && vitest run",
39452
+ "test:coverage": "npm run typecheck && vitest run --coverage",
39453
+ "test:watch": "vitest",
39454
+ typecheck: "tsc -p tsconfig.json --noEmit"
39118
39455
  },
39119
39456
  dependencies: {
39120
- "@chrischall/mcp-utils": "^0.14.0",
39457
+ "@chrischall/mcp-utils": "^0.18.0",
39121
39458
  "@fetchproxy/bootstrap": "^2.0.0",
39122
39459
  "@modelcontextprotocol/sdk": "^1.29.0",
39123
39460
  dotenv: "^17.4.2",
@@ -39137,66 +39474,118 @@ function fetchproxyDisabled() {
39137
39474
  return parseBoolEnv("OFW_DISABLE_FETCHPROXY");
39138
39475
  }
39139
39476
  async function resolveAuth() {
39477
+ const pattern = {};
39140
39478
  const username = readEnvVar("OFW_USERNAME");
39141
39479
  const password = readEnvVar("OFW_PASSWORD");
39142
39480
  if (username && password) {
39143
- const { token, expiresAt } = await loginWithPassword(username, password);
39144
- return { token, expiresAt, source: "env" };
39481
+ pattern.sessionScrape = async () => {
39482
+ const { token, expiresAt: expiresAt2 } = await loginWithPassword(username, password);
39483
+ return { credential: token, source: "env", expiresAt: expiresAt2 };
39484
+ };
39145
39485
  }
39146
39486
  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: []
39487
+ pattern.fetchproxy = async () => {
39488
+ try {
39489
+ const session = await bootstrap({
39490
+ serverName: package_default.name,
39491
+ version: package_default.version,
39492
+ // OFW serves both ofw.ourfamilywizard.com and www.ourfamilywizard.com;
39493
+ // the API + auth token live on the apex. The extension matches on
39494
+ // suffix, so listing the apex covers both.
39495
+ domains: ["ourfamilywizard.com"],
39496
+ declare: {
39497
+ cookies: [],
39498
+ // The web app stores the Bearer token in localStorage["auth"] and
39499
+ // its expiry (ISO string) in localStorage["tokenExpiry"]. Mirroring
39500
+ // both means our 401-replay logic can be slightly smarter, and the
39501
+ // expiry surfaces correctly in diagnostics.
39502
+ localStorage: ["auth", "tokenExpiry"],
39503
+ sessionStorage: [],
39504
+ captureHeaders: []
39505
+ }
39506
+ });
39507
+ const token = session.localStorage["auth"];
39508
+ const expiryRaw = session.localStorage["tokenExpiry"];
39509
+ if (!token) {
39510
+ throw new Error(
39511
+ 'localStorage["auth"] missing on ourfamilywizard.com. Sign into OFW in your browser (with the fetchproxy extension installed) and retry.'
39512
+ );
39164
39513
  }
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;
39514
+ return {
39515
+ credential: token,
39516
+ source: "fetchproxy",
39517
+ ...expiryRaw ? { expiresAt: new Date(expiryRaw) } : {}
39518
+ };
39519
+ } catch (e) {
39520
+ if (classifyBridgeError(e) === "bridge_down") {
39521
+ const downErr = e;
39522
+ throw new Error(
39523
+ `OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
39524
+ );
39525
+ }
39526
+ const msg = e instanceof Error ? e.message : String(e);
39181
39527
  throw new Error(
39182
- `OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
39528
+ `OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`
39183
39529
  );
39184
39530
  }
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
- }
39531
+ };
39532
+ }
39533
+ if (!pattern.sessionScrape && !pattern.fetchproxy) {
39534
+ throw new Error(
39535
+ "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)."
39536
+ );
39190
39537
  }
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)."
39538
+ const { credential, source, expiresAt } = await resolveAuthPattern(pattern);
39539
+ return {
39540
+ token: credential,
39541
+ // The pattern's `source` is a free-form string; ours is a two-value union,
39542
+ // and both resolvers above set it to a member of that union.
39543
+ source,
39544
+ ...expiresAt ? { expiresAt } : {}
39545
+ };
39546
+ }
39547
+
39548
+ // src/session-cache.ts
39549
+ function sessionCachePath(env = process.env) {
39550
+ return resolveStateFile({
39551
+ env,
39552
+ envVar: "OFW_SESSION_FILE",
39553
+ subdir: ".ofw-mcp",
39554
+ fileName: "session.json"
39555
+ });
39556
+ }
39557
+ function isTokens(raw) {
39558
+ if (raw === null || typeof raw !== "object") return false;
39559
+ const t = raw;
39560
+ return typeof t.accessToken === "string" && t.accessToken !== "" && typeof t.expiresAt === "number" && (t.refreshToken === void 0 || typeof t.refreshToken === "string");
39561
+ }
39562
+ function createSessionCache(opts = {}) {
39563
+ const env = opts.env ?? process.env;
39564
+ if (opts.injectedResolver === true) return null;
39565
+ if (!parseBoolEnv("OFW_SESSION_CACHE", { env, default: true })) return null;
39566
+ const username = readEnvVar("OFW_USERNAME", { env });
39567
+ const password = readEnvVar("OFW_PASSWORD", { env });
39568
+ if (username === void 0 || password === void 0) return null;
39569
+ return createFileStatePersistence({
39570
+ filePath: sessionCachePath(env),
39571
+ // Joined on a NUL, written as an escape rather than a literal byte: a
39572
+ // password may contain spaces, so a space-joined pair could collide with
39573
+ // a different pair by shifting the boundary between the two halves.
39574
+ boundTo: [username.trim().toLowerCase(), password].join("\0"),
39575
+ validate: (raw) => isTokens(raw) ? raw : null
39576
+ });
39577
+ }
39578
+ function reportCacheWriteFailure(err) {
39579
+ const detail = err instanceof Error ? err.message : String(err);
39580
+ console.error(
39581
+ `[ofw-mcp] could not cache the session token (${detail}); continuing without the cache \u2014 every restart will re-authenticate until this is fixed.`
39193
39582
  );
39194
39583
  }
39195
39584
 
39196
39585
  // src/client.ts
39197
39586
  try {
39198
- const dir = dirname(fileURLToPath(import.meta.url));
39199
- await loadDotenvSafely({ path: join4(dir, "..", ".env") });
39587
+ const dir = dirname2(fileURLToPath(import.meta.url));
39588
+ await loadDotenvSafely({ path: join5(dir, "..", ".env") });
39200
39589
  } catch {
39201
39590
  }
39202
39591
  function parseContentDispositionFilename(cd) {
@@ -39227,9 +39616,9 @@ var OFWClient = class {
39227
39616
  // Bearer-token lifecycle is delegated to the shared, race-safe TokenManager
39228
39617
  // (proactive refresh inside the skew window, single-flight refresh so a burst
39229
39618
  // 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.
39619
+ // guarded against double-refresh). It is created lazily, and mints on first
39620
+ // use through the function form of `initial` — see `mint` below for why that
39621
+ // form rather than a seeded placeholder.
39233
39622
  tokenManager;
39234
39623
  // Optional injected auth resolver. When set, the refresh callback uses it
39235
39624
  // instead of the module-level global `resolveAuth` (env-var → fetchproxy
@@ -39243,22 +39632,28 @@ var OFWClient = class {
39243
39632
  }
39244
39633
  getTokenManager() {
39245
39634
  if (!this.tokenManager) {
39635
+ const mint = async () => {
39636
+ const { token, expiresAt } = await (this.authResolver ?? resolveAuth)();
39637
+ return {
39638
+ accessToken: token,
39639
+ refreshToken: OFW_REFRESH_SENTINEL,
39640
+ expiresAt: (expiresAt ?? new Date(Date.now() + OFW_TOKEN_TTL_MS)).getTime()
39641
+ };
39642
+ };
39246
39643
  this.tokenManager = new TokenManager({
39247
- initial: { accessToken: "", refreshToken: OFW_REFRESH_SENTINEL, expiresAt: 0 },
39644
+ initial: mint,
39645
+ persistence: createSessionCache({ injectedResolver: this.authResolver !== void 0 }) ?? void 0,
39646
+ onPersistError: reportCacheWriteFailure,
39647
+ // A failed renewal IS a failed login here, so the library's
39648
+ // re-mint-on-revoked recovery would just repeat the call that failed.
39649
+ isRefreshRevoked: () => false,
39248
39650
  skewMs: OFW_TOKEN_EXPIRY_SKEW_MS,
39249
39651
  // Map OFW's mint/refresh onto the refresh callback. `resolveAuth()`
39250
39652
  // returns a token and a best-effort expiry; when the fetchproxy path
39251
39653
  // can't supply one we fall back to the same 6h estimate the password
39252
39654
  // path uses (the 401-replay covers a wrong guess). We re-arm the
39253
39655
  // 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
- }
39656
+ refresh: mint
39262
39657
  });
39263
39658
  }
39264
39659
  return this.tokenManager;
@@ -40082,25 +40477,25 @@ async function syncAll(client2, opts, store) {
40082
40477
 
40083
40478
  // src/config.ts
40084
40479
  import { createHash } from "node:crypto";
40085
- import { homedir as homedir3 } from "node:os";
40086
- import { join as join5 } from "node:path";
40480
+ import { homedir as homedir4 } from "node:os";
40481
+ import { join as join6 } from "node:path";
40087
40482
  function readCacheIdentity() {
40088
40483
  return readEnvVar("OFW_CACHE_IDENTITY") ?? readEnvVar("OFW_USERNAME") ?? "_default";
40089
40484
  }
40090
40485
  function getCacheDir() {
40091
40486
  const override = process.env.OFW_CACHE_DIR;
40092
40487
  if (override && override.trim().length > 0) return override.trim();
40093
- return join5(homedir3(), ".cache", "ofw-mcp");
40488
+ return join6(homedir4(), ".cache", "ofw-mcp");
40094
40489
  }
40095
40490
  function getCacheDbPath() {
40096
40491
  const identity = readCacheIdentity();
40097
40492
  const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
40098
- return join5(getCacheDir(), `${hash2}.db`);
40493
+ return join6(getCacheDir(), `${hash2}.db`);
40099
40494
  }
40100
40495
  function getAttachmentsDir() {
40101
40496
  const override = process.env.OFW_ATTACHMENTS_DIR;
40102
40497
  if (override && override.trim().length > 0) return override.trim();
40103
- return join5(homedir3(), "Downloads", "ofw-mcp");
40498
+ return join6(homedir4(), "Downloads", "ofw-mcp");
40104
40499
  }
40105
40500
  function getWriteMode() {
40106
40501
  const raw = process.env.OFW_WRITE_MODE;
@@ -41470,8 +41865,8 @@ async function extractAttachment(bytes, mimeType, fileName, opts = {}) {
41470
41865
  }
41471
41866
 
41472
41867
  // src/tools/attachments.ts
41473
- import { readFileSync, statSync, mkdirSync, writeFileSync } from "node:fs";
41474
- import { basename, dirname as dirname2, extname } from "node:path";
41868
+ import { readFileSync as readFileSync2, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
41869
+ import { basename, dirname as dirname3, extname } from "node:path";
41475
41870
  var MIME_BY_EXT = {
41476
41871
  ".pdf": "application/pdf",
41477
41872
  ".png": "image/png",
@@ -41545,14 +41940,14 @@ var NodeAttachmentIO = class {
41545
41940
  }
41546
41941
  readDownloaded(path) {
41547
41942
  try {
41548
- return readFileSync(path);
41943
+ return readFileSync2(path);
41549
41944
  } catch {
41550
41945
  return null;
41551
41946
  }
41552
41947
  }
41553
41948
  writeDownload(dest, bytes) {
41554
- mkdirSync(dirname2(dest), { recursive: true });
41555
- writeFileSync(dest, bytes);
41949
+ mkdirSync2(dirname3(dest), { recursive: true });
41950
+ writeFileSync2(dest, bytes);
41556
41951
  }
41557
41952
  };
41558
41953
 
@@ -41616,7 +42011,7 @@ async function buildInlineDelivery(input) {
41616
42011
  }
41617
42012
 
41618
42013
  // src/tools/messages.ts
41619
- import { basename as basename2, join as join6 } from "node:path";
42014
+ import { basename as basename2, join as join7 } from "node:path";
41620
42015
 
41621
42016
  // src/tools/pagination.ts
41622
42017
  function pageState(input) {
@@ -42711,9 +43106,9 @@ ${text}` : text);
42711
43106
  if (args.saveTo) {
42712
43107
  const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
42713
43108
  const abs = expandPath2(args.saveTo);
42714
- dest = isDirArg ? join6(abs, `${fileId}-${safeName}`) : abs;
43109
+ dest = isDirArg ? join7(abs, `${fileId}-${safeName}`) : abs;
42715
43110
  } else {
42716
- dest = join6(getAttachmentsDir(), `${fileId}-${safeName}`);
43111
+ dest = join7(getAttachmentsDir(), `${fileId}-${safeName}`);
42717
43112
  }
42718
43113
  const extractOnDisk = args.extract === true;
42719
43114
  if (!args.force && cached2.downloadedPath === dest) {
@@ -43209,8 +43604,8 @@ function registerJournalTools(server, client2) {
43209
43604
 
43210
43605
  // src/cache/node.ts
43211
43606
  import { DatabaseSync } from "node:sqlite";
43212
- import { mkdirSync as mkdirSync2, chmodSync, existsSync } from "node:fs";
43213
- import { dirname as dirname3 } from "node:path";
43607
+ import { mkdirSync as mkdirSync3, chmodSync as chmodSync2, existsSync as existsSync2 } from "node:fs";
43608
+ import { dirname as dirname4 } from "node:path";
43214
43609
 
43215
43610
  // src/cache/store.ts
43216
43611
  function rowFromDb(r) {
@@ -43794,10 +44189,10 @@ var NodeSqlDriver = class {
43794
44189
  }
43795
44190
  };
43796
44191
  function enforceCachePermissions(dbPath) {
43797
- chmodSync(dirname3(dbPath), 448);
43798
- chmodSync(dbPath, 384);
44192
+ chmodSync2(dirname4(dbPath), 448);
44193
+ chmodSync2(dbPath, 384);
43799
44194
  for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
43800
- if (existsSync(sibling)) chmodSync(sibling, 384);
44195
+ if (existsSync2(sibling)) chmodSync2(sibling, 384);
43801
44196
  }
43802
44197
  }
43803
44198
  var OFWCache = class _OFWCache extends LocalCacheStore {
@@ -43808,7 +44203,7 @@ var OFWCache = class _OFWCache extends LocalCacheStore {
43808
44203
  db;
43809
44204
  static open(path) {
43810
44205
  const memory = path === ":memory:";
43811
- if (!memory) mkdirSync2(dirname3(path), { recursive: true });
44206
+ if (!memory) mkdirSync3(dirname4(path), { recursive: true });
43812
44207
  const db = new DatabaseSync(path);
43813
44208
  if (!memory) enforceCachePermissions(path);
43814
44209
  db.exec("PRAGMA journal_mode = WAL");
@@ -43838,7 +44233,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
43838
44233
  var nodeAttachmentIO = new NodeAttachmentIO();
43839
44234
  await runMcp({
43840
44235
  name: "ofw",
43841
- version: "2.11.0",
44236
+ version: "2.12.0",
43842
44237
  // x-release-please-version
43843
44238
  deps: client,
43844
44239
  tools: [