ofw-mcp 2.10.2 → 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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "OurFamilyWizard tools for Claude Code",
9
- "version": "2.10.2"
9
+ "version": "2.12.0"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -14,7 +14,7 @@
14
14
  "displayName": "OurFamilyWizard",
15
15
  "source": "./",
16
16
  "description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
17
- "version": "2.10.2",
17
+ "version": "2.12.0",
18
18
  "author": {
19
19
  "name": "Chris Chall"
20
20
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ofw",
3
3
  "displayName": "OurFamilyWizard",
4
- "version": "2.10.2",
4
+ "version": "2.12.0",
5
5
  "description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall"
package/README.md CHANGED
@@ -275,7 +275,7 @@ Calendar events sit between the two message tiers: they have no draft stage (a c
275
275
  ## Development
276
276
 
277
277
  ```bash
278
- npm test # run the vitest suite
278
+ npm test # tsc typecheck, then the vitest suite
279
279
  npm run build # tsc → dist/, then esbuild bundle → dist/bundle.js
280
280
  npm run dev # node --env-file=.env dist/index.js (requires built dist)
281
281
  ```
package/dist/auth.js CHANGED
@@ -1,16 +1,23 @@
1
1
  // ────────────────────────────────────────────────────────────────────────────
2
- // Auth resolution — Pattern A template
2
+ // Auth resolution — Pattern A, on the shared resolver
3
3
  // ────────────────────────────────────────────────────────────────────────────
4
4
  //
5
- // This file is the canonical shape for "browser-bootstrap + Node-direct"
6
- // auth used across our MCP servers. The other six MCPs in this family
7
- // (resy-mcp, opentable-mcp, splitwise-mcp, …) will model their auth
8
- // resolution after this one keep the structure flat, the path-selection
9
- // explicit, and the error messages actionable.
5
+ // This file used to BE the canonical shape, and said so: the other MCPs in
6
+ // this family were told to model their resolution on it. That shape now lives
7
+ // in `resolveAuthPattern` (@chrischall/mcp-utils), which owns the one thing
8
+ // every copy of it had to agree on the priority order — and nothing else.
9
+ // What stays here is what is genuinely OFW's: how each path gets a token, and
10
+ // what to say when it cannot.
10
11
  //
11
- // THE THREE PATHS, in priority order:
12
+ // The migration waited on somewhere to put the expiry. The shared result
13
+ // carried `{ credential, source }` only, so adopting it meant discarding the
14
+ // `tokenExpiry` this file reads from the browser tab — which is the whole
15
+ // reason it reads it. `PatternResult.expiresAt` closed that (mcp-utils#148).
12
16
  //
13
- // 1. Env-var credentials (existing behavior)
17
+ // THE THREE PATHS, in priority order — now expressed by which resolvers are
18
+ // PROVIDED, since `resolveAuthPattern` runs the first configured one:
19
+ //
20
+ // 1. Env-var credentials (`sessionScrape`)
14
21
  // OFW_USERNAME + OFW_PASSWORD set → POST the login form, get a token.
15
22
  // This is the legacy path. It runs unchanged when both vars are set
16
23
  // so existing users (Claude Desktop with mcpb env config, etc.) are
@@ -45,7 +52,7 @@
45
52
  // - `./auth-password.js` (loginWithPassword) is a separate module
46
53
  // specifically so it can be mocked here too. This keeps the
47
54
  // selection logic independent of either implementation.
48
- import { parseBoolEnv, readEnvVar } from '@chrischall/mcp-utils';
55
+ import { parseBoolEnv, readEnvVar, resolveAuthPattern } from '@chrischall/mcp-utils';
49
56
  import { bootstrap } from '@fetchproxy/bootstrap';
50
57
  import { classifyBridgeError } from '@chrischall/mcp-utils/fetchproxy';
51
58
  import { loginWithPassword } from './auth-password.js';
@@ -63,62 +70,85 @@ function fetchproxyDisabled() {
63
70
  * The field exists for logging / future cache-keying only.
64
71
  */
65
72
  export async function resolveAuth() {
66
- // ── Path 1: env-var credentials (unchanged from pre-fetchproxy behavior).
73
+ // Which paths are CONFIGURED. `resolveAuthPattern` runs the first one
74
+ // provided, in the fleet's fixed priority order (token → oauth →
75
+ // sessionScrape → fetchproxy), so "env beats fetchproxy" is expressed by
76
+ // which keys exist rather than by an if/else here.
77
+ const pattern = {};
78
+ // ── Path 1 (sessionScrape): env-var credentials.
67
79
  // `readEnvVar` trims and treats blank / `"undefined"` / `"null"` /
68
80
  // `${UNEXPANDED}` placeholders as unset — defends against MCP hosts that
69
81
  // pass `.mcp.json` env blocks through without variable expansion.
70
82
  const username = readEnvVar('OFW_USERNAME');
71
83
  const password = readEnvVar('OFW_PASSWORD');
72
84
  if (username && password) {
73
- const { token, expiresAt } = await loginWithPassword(username, password);
74
- return { token, expiresAt, source: 'env' };
85
+ pattern.sessionScrape = async () => {
86
+ const { token, expiresAt } = await loginWithPassword(username, password);
87
+ return { credential: token, source: 'env', expiresAt };
88
+ };
75
89
  }
76
- // ── Path 2: fetchproxy fallback (new).
90
+ // ── Path 2 (fetchproxy): lift the session out of a signed-in browser tab.
77
91
  if (!fetchproxyDisabled()) {
78
- try {
79
- const session = await bootstrap({
80
- serverName: pkg.name,
81
- version: pkg.version,
82
- // OFW serves both ofw.ourfamilywizard.com and www.ourfamilywizard.com;
83
- // the API + auth token live on the apex. The extension matches on
84
- // suffix, so listing the apex covers both.
85
- domains: ['ourfamilywizard.com'],
86
- declare: {
87
- cookies: [],
88
- // The web app stores the Bearer token in localStorage["auth"] and
89
- // its expiry (ISO string) in localStorage["tokenExpiry"]. Mirroring
90
- // both means our 401-replay logic can be slightly smarter, and the
91
- // expiry surfaces correctly in diagnostics.
92
- localStorage: ['auth', 'tokenExpiry'],
93
- sessionStorage: [],
94
- captureHeaders: [],
95
- },
96
- });
97
- const token = session.localStorage['auth'];
98
- const expiryRaw = session.localStorage['tokenExpiry'];
99
- if (!token) {
100
- throw new Error('localStorage["auth"] missing on ourfamilywizard.com. ' +
101
- 'Sign into OFW in your browser (with the fetchproxy extension installed) and retry.');
92
+ pattern.fetchproxy = async () => {
93
+ try {
94
+ const session = await bootstrap({
95
+ serverName: pkg.name,
96
+ version: pkg.version,
97
+ // OFW serves both ofw.ourfamilywizard.com and www.ourfamilywizard.com;
98
+ // the API + auth token live on the apex. The extension matches on
99
+ // suffix, so listing the apex covers both.
100
+ domains: ['ourfamilywizard.com'],
101
+ declare: {
102
+ cookies: [],
103
+ // The web app stores the Bearer token in localStorage["auth"] and
104
+ // its expiry (ISO string) in localStorage["tokenExpiry"]. Mirroring
105
+ // both means our 401-replay logic can be slightly smarter, and the
106
+ // expiry surfaces correctly in diagnostics.
107
+ localStorage: ['auth', 'tokenExpiry'],
108
+ sessionStorage: [],
109
+ captureHeaders: [],
110
+ },
111
+ });
112
+ const token = session.localStorage['auth'];
113
+ const expiryRaw = session.localStorage['tokenExpiry'];
114
+ if (!token) {
115
+ throw new Error('localStorage["auth"] missing on ourfamilywizard.com. ' +
116
+ 'Sign into OFW in your browser (with the fetchproxy extension installed) and retry.');
117
+ }
118
+ return {
119
+ credential: token,
120
+ source: 'fetchproxy',
121
+ ...(expiryRaw ? { expiresAt: new Date(expiryRaw) } : {}),
122
+ };
102
123
  }
103
- return {
104
- token,
105
- expiresAt: expiryRaw ? new Date(expiryRaw) : undefined,
106
- source: 'fetchproxy',
107
- };
108
- }
109
- catch (e) {
110
- // FetchproxyBridgeDownError only escapes bootstrap() after the lazy-revive retry fails surface .hint verbatim (actionable "click toolbar icon" copy).
111
- if (classifyBridgeError(e) === 'bridge_down') {
112
- const downErr = e;
113
- throw new Error(`OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`);
124
+ catch (e) {
125
+ // FetchproxyBridgeDownError only escapes bootstrap() after the lazy-revive retry fails — surface .hint verbatim (actionable "click toolbar icon" copy).
126
+ if (classifyBridgeError(e) === 'bridge_down') {
127
+ const downErr = e;
128
+ throw new Error(`OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`);
129
+ }
130
+ const msg = e instanceof Error ? e.message : String(e);
131
+ throw new Error(`OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`);
114
132
  }
115
- const msg = e instanceof Error ? e.message : String(e);
116
- throw new Error(`OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`);
117
- }
133
+ };
134
+ }
135
+ // ── Path 3: nothing configured. Raised HERE rather than letting
136
+ // `resolveAuthPattern` throw its generic "no auth configured" message,
137
+ // because this one names OFW's own two fixes side-by-side and the generic
138
+ // one cannot.
139
+ if (!pattern.sessionScrape && !pattern.fetchproxy) {
140
+ throw new Error('OFW auth: set OFW_USERNAME + OFW_PASSWORD, ' +
141
+ 'or install the fetchproxy extension and sign into ourfamilywizard.com ' +
142
+ '(unset OFW_DISABLE_FETCHPROXY if it is set).');
118
143
  }
119
- // ── Path 3: nothing configured. Surface both fixes side-by-side so the
120
- // user can pick whichever fits their setup.
121
- throw new Error('OFW auth: set OFW_USERNAME + OFW_PASSWORD, ' +
122
- 'or install the fetchproxy extension and sign into ourfamilywizard.com ' +
123
- '(unset OFW_DISABLE_FETCHPROXY if it is set).');
144
+ // Errors from the winning path propagate UNWRAPPED, which is what keeps the
145
+ // bridge-down `.hint` above intact.
146
+ const { credential, source, expiresAt } = await resolveAuthPattern(pattern);
147
+ return {
148
+ token: credential,
149
+ // The pattern's `source` is a free-form string; ours is a two-value union,
150
+ // and both resolvers above set it to a member of that union.
151
+ source: source,
152
+ ...(expiresAt ? { expiresAt } : {}),
153
+ };
124
154
  }