honeybook-mcp 0.4.3 → 0.4.5

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "HoneyBook client-portal tools for Claude Code",
10
- "version": "0.4.3"
10
+ "version": "0.4.5"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "HoneyBook",
16
16
  "source": "./",
17
17
  "description": "HoneyBook client-portal MCP for Claude — view wedding-vendor contracts and invoices via MCP",
18
- "version": "0.4.3",
18
+ "version": "0.4.5",
19
19
  "author": {
20
20
  "name": "Chris Chall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "honeybook",
3
3
  "displayName": "HoneyBook",
4
- "version": "0.4.3",
4
+ "version": "0.4.5",
5
5
  "description": "HoneyBook client-portal MCP for Claude — view wedding-vendor contracts and invoices via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall",
package/README.md CHANGED
@@ -86,7 +86,7 @@ HoneyBook has no public client-portal API. This MCP reuses the same auth state y
86
86
  **Per-vendor activation:**
87
87
 
88
88
  1. Make sure the vendor's portal tab is open (the magic link from their email).
89
- 2. In Claude, call `use_magic_link` with the magic-link URL — the tool asks the fetchproxy extension to snapshot the page's `localStorage["jStorage"]` and the `hb-api-fingerprint` request header, then closes the bridge. No headless browser is spawned.
89
+ 2. In Claude, call `use_magic_link` with the magic-link URL — the tool asks the fetchproxy extension to snapshot the auth fields out of the page's `localStorage["HONEYBOOK_REACT_CURR_USER"]`, then closes the bridge. The tab only has to be open and signed in; nothing is read off a live request. No headless browser is spawned.
90
90
  3. All other tools use the most-recently-activated session by default. Pass `origin` explicitly when multiple vendors are active.
91
91
 
92
92
  Sessions are stored in memory and persisted to `~/.honeybook-mcp/sessions.json` (mode 0600) so they survive MCP restarts. Re-run `use_magic_link` when a session expires.
@@ -111,7 +111,8 @@ Tools that touch a vendor accept an optional `origin` argument (e.g. `https://ac
111
111
  - **"HoneyBook auth expired"** — re-open the vendor's magic link in Chrome and re-run `use_magic_link`.
112
112
  - **"No active HoneyBook session"** — call `use_magic_link` first.
113
113
  - **"fetchproxy capture failed"** — install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), then open the vendor's magic link in that browser.
114
- - **"hb-api-fingerprint header not captured"** — refresh the portal tab so the page makes an `api.honeybook.com/api/v2/*` request, then retry.
114
+ - **"fetchproxy capture timed out"** — the extension found no signed-in portal tab to read. Open the vendor's magic link, confirm the portal page has loaded, then retry.
115
+ - **"no confirmed browser session"** — the extension is connected but has not approved this MCP. Open the Transporter popup and approve the pair code it shows, then retry.
115
116
 
116
117
  ## Security
117
118
 
package/dist/auth.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // ────────────────────────────────────────────────────────────────────────────
2
- // Session capture — Pattern A template (read_local_storage + capture_header)
2
+ // Session capture — Pattern A template (read_local_storage)
3
3
  // ────────────────────────────────────────────────────────────────────────────
4
4
  //
5
5
  // Mirrors the canonical "browser-bootstrap + Node-direct" shape from
@@ -20,15 +20,23 @@
20
20
  // 2. fetchproxy bootstrap (this module).
21
21
  // The user has signed into their vendor portal via a magic link in
22
22
  // their real Chrome (the fetchproxy 0.3.0 extension is installed).
23
- // We call `@fetchproxy/bootstrap` once to:
24
- // • snapshot localStorage["jStorage"] — the HoneyBook web app
25
- // stores the bearer token + user id + trusted device id + the
26
- // HB_CURR_USER blob here, all under one key
27
- // • capture the `hb-api-fingerprint` request header on the first
28
- // api.honeybook.com/api/v2/* call the page makes — this is a
29
- // per-device FingerprintJS signal that the API requires on
30
- // every request (it's NOT in jStorage)
31
- // Then we close the bridge and operate from Node thereafter.
23
+ // We call `@fetchproxy/bootstrap` once to read the auth fields out
24
+ // of localStorage["HONEYBOOK_REACT_CURR_USER"], then close the
25
+ // bridge and operate from Node thereafter.
26
+ //
27
+ // HoneyBook used to keep this state in the AngularJS `jStorage`
28
+ // blob (HB_AUTH_TOKEN / HB_AUTH_USER_ID / HB_TRUSTED_DEVICE /
29
+ // HB_CURR_USER). It has since moved to the React-era
30
+ // `HONEYBOOK_REACT_CURR_USER` key; on a live signed-in portal
31
+ // jStorage is down to HB_TRUSTED_DEVICE, SESSION_COMPANY_ID,
32
+ // REDIRECT_URL and __jstorage_meta. We read the React key and keep
33
+ // the surviving jStorage trusted-device as a fallback.
34
+ //
35
+ // We no longer capture the `hb-api-fingerprint` request header.
36
+ // The API accepts requests without it (verified against
37
+ // /api/v2/users/{uid}/workspace_files), and capturing it was the
38
+ // only part of bootstrap that had to block on a live outgoing
39
+ // request — the "portal tab already went idle" timeout.
32
40
  //
33
41
  // 3. Error
34
42
  // Nothing to authenticate with. We throw a message that names the
@@ -36,14 +44,14 @@
36
44
  // extension installed) and retry the tool call.
37
45
  //
38
46
  // Why fetchproxy is only a one-shot read:
39
- // The bootstrap call snapshots both buckets (localStorage +
40
- // captured header) and returns. The MCP then operates from Node with
41
- // direct fetch — latency and reliability are not coupled to the
42
- // browser bridge for normal tool calls. When the bearer token
43
- // eventually expires, the existing 401 handler in `HoneyBookClient`
44
- // surfaces a helpful "re-bootstrap" error and the user re-runs the
45
- // capture (typically via the `use_magic_link` tool, which now wraps
46
- // this function instead of running Puppeteer).
47
+ // The bootstrap call snapshots the declared storage fields and
48
+ // returns. The MCP then operates from Node with direct fetch — latency
49
+ // and reliability are not coupled to the browser bridge for normal
50
+ // tool calls. When the bearer token eventually expires, the
51
+ // expired-session handler in `HoneyBookClient` surfaces a helpful
52
+ // "re-bootstrap" error and the user re-runs the capture (typically via
53
+ // the `use_magic_link` tool, which now wraps this function instead of
54
+ // running Puppeteer).
47
55
  //
48
56
  // Multi-domain scope:
49
57
  // HoneyBook serves both the main app (honeybook.com) and per-vendor
@@ -93,44 +101,54 @@ export async function captureSessionViaFetchproxy(opts) {
93
101
  // portal subdomains (*.hbportal.co). The extension matches on suffix,
94
102
  // so listing both apexes covers any vendor.
95
103
  //
96
- // Both are needed:
97
- // - `hbportal.co` is where the vendor magic-link tab lives, so
98
- // `jStorage` reads target it (via `storageDomain` below).
99
- // - `honeybook.com` is required for the `hb-api-fingerprint`
100
- // capture, because that header rides outgoing requests to
101
- // `api.honeybook.com/api/v2/*` and the extension gates
102
- // every captureHeader `host` against declared domains.
104
+ // `hbportal.co` is where the vendor magic-link tab lives, so the
105
+ // storage reads target it (via `storageDomain` below).
106
+ // `honeybook.com` stays declared because the same session is valid
107
+ // on the main app and callers may hold either origin.
103
108
  //
104
109
  // 0.4.1+ requires multi-domain MCPs to pick a `storageDomain` so
105
110
  // the cookie / localStorage / sessionStorage / indexedDb reads
106
- // know which tab to target. captureRequestHeader is independently
107
- // routed by its declared { host, path? }.
111
+ // know which tab to target.
108
112
  domains: ['honeybook.com', 'hbportal.co'],
109
113
  storageDomain: 'hbportal.co',
110
114
  declare: {
111
115
  cookies: [],
112
- // We don't need the full `jStorage` blob (it's ~8KB of mostly
113
- // unused state). 0.4.0's JSON-pointer extraction lets us
114
- // declare exactly the four fields the MCP uses; bootstrap
115
- // reads `jStorage` once and applies all pointers in one call.
116
- // The extension popup also shows the pointer paths verbatim
117
- // so the user can see precisely what's being read.
116
+ // `HONEYBOOK_REACT_CURR_USER` is ~6.5KB of mostly unused user
117
+ // state. 0.4.0's JSON-pointer extraction lets us declare exactly
118
+ // the fields the MCP uses; bootstrap reads each storage key once
119
+ // and applies all pointers in one call. The extension popup shows
120
+ // the pointer paths verbatim so the user can see precisely what's
121
+ // being read.
118
122
  localStorage: [],
119
123
  localStoragePointers: [
120
- { outputKey: 'HB_AUTH_TOKEN', storageKey: 'jStorage', jsonPointer: '/HB_AUTH_TOKEN' },
121
- { outputKey: 'HB_AUTH_USER_ID', storageKey: 'jStorage', jsonPointer: '/HB_AUTH_USER_ID' },
122
- { outputKey: 'HB_TRUSTED_DEVICE', storageKey: 'jStorage', jsonPointer: '/HB_TRUSTED_DEVICE' },
123
- { outputKey: 'HB_COMPANY_NAME', storageKey: 'jStorage', jsonPointer: '/HB_CURR_USER/company/company_name' },
124
+ {
125
+ outputKey: 'HB_AUTH_TOKEN',
126
+ storageKey: 'HONEYBOOK_REACT_CURR_USER',
127
+ jsonPointer: '/authentication_token',
128
+ },
129
+ { outputKey: 'HB_AUTH_USER_ID', storageKey: 'HONEYBOOK_REACT_CURR_USER', jsonPointer: '/_id' },
130
+ {
131
+ outputKey: 'HB_TRUSTED_DEVICE',
132
+ storageKey: 'HONEYBOOK_REACT_CURR_USER',
133
+ jsonPointer: '/trusted_device',
134
+ },
135
+ {
136
+ outputKey: 'HB_COMPANY_NAME',
137
+ storageKey: 'HONEYBOOK_REACT_CURR_USER',
138
+ jsonPointer: '/company/company_name',
139
+ },
140
+ // The one field of the old jStorage blob that survived the
141
+ // migration. Kept as a fallback so a further reshuffle of the
142
+ // React key doesn't cost us the trusted-device value too.
143
+ {
144
+ outputKey: 'HB_TRUSTED_DEVICE_LEGACY',
145
+ storageKey: 'jStorage',
146
+ jsonPointer: '/HB_TRUSTED_DEVICE',
147
+ },
124
148
  ],
125
149
  sessionStorage: [],
126
- captureHeaders: [
127
- // The page's first call to api.honeybook.com/api/v2/* carries the
128
- // `hb-api-fingerprint` header — a per-device FingerprintJS
129
- // signal that the API requires on every subsequent request.
130
- // It's NOT stored in jStorage; the only place to get it is off
131
- // a real outgoing request.
132
- { host: 'api.honeybook.com', path: '/api/v2/*', headerName: 'hb-api-fingerprint' },
133
- ],
150
+ // Nothing to sniff off a live request any more — see the header note above.
151
+ captureHeaders: [],
134
152
  },
135
153
  // 0.4.0: surface the pair code (six digits) on stderr so the user
136
154
  // can verify it against the extension popup. fetchproxy 0.4.0
@@ -139,9 +157,8 @@ export async function captureSessionViaFetchproxy(opts) {
139
157
  onPairCode: (code) => {
140
158
  process.stderr.write(`[honeybook-mcp] fetchproxy pair code: ${code}\n`);
141
159
  },
142
- // 0.4.0: capture_request_header blocks on a real outgoing request
143
- // to api.honeybook.com/api/v2/*. Surface a hint so the user knows
144
- // to interact with the portal if the bootstrap appears to stall.
160
+ // Surface the library's own progress hint so a stalled bootstrap
161
+ // tells the user what it is waiting on.
145
162
  onWaiting: (hint) => {
146
163
  process.stderr.write(`[honeybook-mcp] ${hint}\n`);
147
164
  },
@@ -154,53 +171,54 @@ export async function captureSessionViaFetchproxy(opts) {
154
171
  throw new Error(`HoneyBook auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${bridgeError.hint ?? ''}`.trimEnd());
155
172
  }
156
173
  const msg = e instanceof Error ? e.message : String(e);
157
- // The hb-api-fingerprint capture rejects with a bare `timeout` when no
158
- // api.honeybook.com/api/v2/* request fires before its one-shot listener
159
- // window elapses. This is the common "tab already loaded and idle" case:
160
- // a HoneyBook portal fires its API calls on load, then goes quiet, so by
161
- // the time bootstrap has paired, read jStorage, and armed the capture
162
- // listener there's no fresh request left to sniff the header off. The fix
163
- // is to make the portal issue a *new* request (refresh the tab, open a
164
- // workspace) WHILE the capture is waiting — NOT to (re)open the magic
165
- // link, which is already open. classifyBridgeError keys off the error
166
- // class, so the capture timeout surfaces as a FetchproxyProtocolError
174
+ // The extension gates on the scope approved at pair time, so widening the
175
+ // declaration — as the HONEYBOOK_REACT_CURR_USER migration did — is refused
176
+ // until the user re-approves. Every user paired before that upgrade hits
177
+ // this exactly once. The library types the error with a `.hint` carrying
178
+ // the one remedy (revoke in the popup, re-run, approve the new scope), and
179
+ // that remedy is already inlined in `.message`; the generic suffix below is
180
+ // actively wrong here, because the magic link is already open. Match on the
181
+ // class name rather than `instanceof` so a duplicated @fetchproxy/server
182
+ // copy in the dependency tree can't silently defeat the check.
183
+ const errName = e?.name;
184
+ if (errName === 'FetchproxyScopeError' || /not in declared set/i.test(msg)) {
185
+ throw new Error(`HoneyBook auth: ${msg}`);
186
+ }
187
+ // Now that nothing blocks on a live outgoing request, a timeout means the
188
+ // extension never produced the declared storage reads — i.e. there is no
189
+ // signed-in portal tab for it to read from. classifyBridgeError keys off
190
+ // the error class, so this surfaces as a FetchproxyProtocolError
167
191
  // ('protocol'), not 'timeout' — match on the message too.
168
192
  if (bridgeError.type === 'timeout' || /timeout/i.test(msg)) {
169
- throw new Error('HoneyBook auth: fetchproxy capture timed out waiting for the hb-api-fingerprint header. ' +
170
- 'That header can only be read off a live api.honeybook.com request, and a portal tab that has ' +
171
- 'already finished loading sits idle. Keep the vendor magic-link URL open in Chrome (with the ' +
172
- 'fetchproxy extension installed), then re-run use_magic_link and — within ~30s, while it waits — ' +
173
- 'refresh the portal tab (or open a workspace/file) so the page issues a fresh API request.');
193
+ throw new Error('HoneyBook auth: fetchproxy capture timed out. The extension never returned the vendor ' +
194
+ "portal's stored session, which usually means no tab is open and signed in on that portal. " +
195
+ 'Open the vendor magic-link URL in Chrome (with the fetchproxy extension installed), confirm ' +
196
+ 'the portal page has loaded, then re-run use_magic_link.');
174
197
  }
175
198
  throw new Error(`HoneyBook auth: fetchproxy capture failed: ${msg} — ` +
176
199
  'open the vendor magic-link URL in Chrome (with the fetchproxy 0.3.0 extension installed), ' +
177
200
  'then retry.');
178
201
  }
179
202
  // 0.4.0: pointer extractions land in `session.localStorage` keyed by
180
- // their declared `outputKey`. The raw `jStorage` JSON is never copied
181
- // into this process — only the four specific fields we actually need.
203
+ // their declared `outputKey`. No raw blob is ever copied into this
204
+ // process — only the specific fields we actually need.
182
205
  const authToken = session.localStorage['HB_AUTH_TOKEN'];
183
206
  const userId = session.localStorage['HB_AUTH_USER_ID'];
184
- const trustedDevice = session.localStorage['HB_TRUSTED_DEVICE'];
207
+ // Only these two are load-bearing. `hb-trusted-device` is sent when we have
208
+ // it but the API returns 200 without it, so a missing value must not fail
209
+ // the capture — requiring fields the API doesn't is what turned an upstream
210
+ // storage rename into a total outage.
211
+ const trustedDevice = session.localStorage['HB_TRUSTED_DEVICE'] || session.localStorage['HB_TRUSTED_DEVICE_LEGACY'];
185
212
  const companyNameFromHb = session.localStorage['HB_COMPANY_NAME'];
186
213
  if (!authToken) {
187
- throw new Error('HoneyBook auth: HB_AUTH_TOKEN not found at jStorage./HB_AUTH_TOKEN. ' +
214
+ throw new Error('HoneyBook auth: no auth token at HONEYBOOK_REACT_CURR_USER./authentication_token. ' +
188
215
  'Sign into the vendor portal via the magic-link URL and retry.');
189
216
  }
190
217
  if (!userId) {
191
- throw new Error('HoneyBook auth: HB_AUTH_USER_ID not found at jStorage./HB_AUTH_USER_ID.');
192
- }
193
- if (!trustedDevice) {
194
- throw new Error('HoneyBook auth: HB_TRUSTED_DEVICE not found at jStorage./HB_TRUSTED_DEVICE.');
195
- }
196
- const fingerprint = session.capturedHeaders['hb-api-fingerprint'];
197
- if (!fingerprint) {
198
- throw new Error('HoneyBook auth: hb-api-fingerprint header not captured. ' +
199
- 'Interact with the vendor portal (refresh the page, open a workspace) so the browser makes an ' +
200
- 'api.honeybook.com/api/v2/* request, then retry.');
218
+ throw new Error('HoneyBook auth: no user id at HONEYBOOK_REACT_CURR_USER./_id.');
201
219
  }
202
- // HB_CURR_USER may not be populated immediately on a fresh tab; fall
203
- // back to the portal subdomain so the user has a readable label.
220
+ // `company` is null on a client-side portal user, so this pointer usually
221
+ // resolves to nothing; fall back to the portal subdomain for a readable label.
204
222
  let companyName = companyNameFromHb || '';
205
223
  if (!companyName) {
206
224
  try {
@@ -215,8 +233,7 @@ export async function captureSessionViaFetchproxy(opts) {
215
233
  companyName,
216
234
  authToken,
217
235
  userId,
218
- trustedDevice,
219
- fingerprint,
236
+ ...(trustedDevice ? { trustedDevice } : {}),
220
237
  capturedAt: Date.now(),
221
238
  };
222
239
  sessionStore.add(captured);