@mpurdon/mcp-freshbooks 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Purdon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,221 @@
1
+ # freshbooks-mcp
2
+
3
+ A local MCP (Model Context Protocol) server that gives Claude Desktop access to your FreshBooks invoices. List, view, create, update, send, and (soft-)delete invoices, plus list clients and items so you can construct invoices end-to-end.
4
+
5
+ - **Transport:** stdio (single user, runs on your machine)
6
+ - **Auth:** OAuth2 with refresh-token persistence at `~/.freshbooks-mcp/tokens.json` (mode 0600)
7
+ - **Runtime:** Node 20+, TypeScript built to ESM
8
+
9
+ ## Prerequisites
10
+
11
+ - Node.js 20 or newer
12
+ - A FreshBooks account with API access
13
+ - Claude Desktop (https://claude.ai/download)
14
+ - **mkcert** — generates a locally-trusted TLS cert so the OAuth callback can run over HTTPS (required by FreshBooks):
15
+ ```bash
16
+ brew install mkcert
17
+ mkcert -install # installs the local CA — only needed once per machine
18
+ ```
19
+
20
+ ## 1. Create a FreshBooks developer app
21
+
22
+ FreshBooks uses OAuth2, so you need a developer app to get a Client ID and Secret.
23
+
24
+ 1. Sign in at https://my.freshbooks.com.
25
+ 2. Open https://my.freshbooks.com/#/developer.
26
+ 3. Click **Create an App**. Give it a name like `Claude MCP (local)` and a short description. The app type is "Private app" or similar — there is no review process for private apps.
27
+ 4. Under **Redirect URIs**, add exactly:
28
+ ```
29
+ https://localhost:8765/callback
30
+ ```
31
+ (If you need a different port, set `FRESHBOOKS_REDIRECT_URI` in the environment before running setup, and add the matching URL to your app. Must be HTTPS.)
32
+ 5. Save. Copy the **Client ID** and **Client Secret** somewhere private — you'll paste them into a `.env` file in the next step.
33
+
34
+ The required scopes are the defaults (read/write on accounting resources). FreshBooks will prompt you to consent during the OAuth flow.
35
+
36
+ ## 2. Install and build
37
+
38
+ ```bash
39
+ npm install
40
+ npm run build
41
+ ```
42
+
43
+ ## 3. Run setup (one-time OAuth)
44
+
45
+ Create a `.env` in the project root (copy from `.env.example`):
46
+
47
+ ```
48
+ FRESHBOOKS_CLIENT_ID=your_client_id
49
+ FRESHBOOKS_CLIENT_SECRET=your_client_secret
50
+ ```
51
+
52
+ Then:
53
+
54
+ ```bash
55
+ npm run setup
56
+ ```
57
+
58
+ What happens:
59
+
60
+ 1. Your default browser opens to `https://auth.freshbooks.com/oauth/authorize/...`. Sign in and approve.
61
+ 2. FreshBooks redirects to `https://localhost:8765/callback` with an auth code. The setup script catches it via a one-shot local HTTPS listener (using a mkcert-generated cert) and exchanges it for tokens.
62
+ 3. The script calls `/auth/api/v1/users/me` to discover your `account_id` and business name.
63
+ 4. Tokens are written to `~/.freshbooks-mcp/tokens.json` with mode 0600. The directory `~/.freshbooks-mcp` is created with mode 0700.
64
+ 5. The script prints a Claude Desktop config snippet — copy it.
65
+
66
+ If you have multiple businesses on your FreshBooks account, the script picks the first one and prints a note. To target a different one, set `FRESHBOOKS_ACCOUNT_ID` and `FRESHBOOKS_BUSINESS_ID` in the server env (these override the discovered values). See "Multiple businesses on one FreshBooks account" below.
67
+
68
+ ## 4. Add to Claude Desktop config
69
+
70
+ Open the Claude Desktop config file:
71
+
72
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
73
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
74
+
75
+ Paste the snippet that `npm run setup` printed into the `mcpServers` object. It looks like:
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "freshbooks": {
81
+ "command": "node",
82
+ "args": ["/Users/mp/.claude/mcp-servers/freshbooks/dist/index.js"],
83
+ "env": {
84
+ "FRESHBOOKS_CLIENT_ID": "your_client_id",
85
+ "FRESHBOOKS_CLIENT_SECRET": "your_client_secret"
86
+ }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ Both `FRESHBOOKS_CLIENT_ID` and `FRESHBOOKS_CLIENT_SECRET` are required at runtime: the server uses them to refresh the access token when it expires (every ~12h).
93
+
94
+ The server also reads a `.env` file in the project root on startup as a fallback, so any `FRESHBOOKS_*` var not present in the `env` block above is picked up from there. The `env` block always takes precedence. Recognized vars: `FRESHBOOKS_CLIENT_ID`, `FRESHBOOKS_CLIENT_SECRET`, `FRESHBOOKS_REDIRECT_URI`, `FRESHBOOKS_TOKENS_PATH`, `FRESHBOOKS_ACCOUNT_ID`, `FRESHBOOKS_BUSINESS_ID` — see `.env.example`.
95
+
96
+ Restart Claude Desktop. You should see `freshbooks` appear in the tools menu.
97
+
98
+ ## Available tools
99
+
100
+ All tools accept structured JSON input. Below are the inputs for each.
101
+
102
+ ### Invoices
103
+
104
+ - **`list_invoices`** — `{ status?, client_id?, date_from?, date_to?, search?, page?, per_page? }`
105
+ Filter by `v3_status` (`draft`, `sent`, `viewed`, `paid`, `overdue`, ...), client, date range, or invoice number substring. Default `per_page` is 20.
106
+ - **`get_invoice`** — `{ invoice_id: number }`
107
+ - **`create_invoice`** — `{ client_id, lines: [{ description, qty, unit_cost, name?, taxName1?, taxAmount1? }], currency_code?, due_offset_days?, notes?, terms?, invoice_number?, create_date? }`
108
+ `unit_cost` is a decimal (e.g. `75.00` for $75), not cents. Tax amounts are percentages (e.g. `13` for 13%).
109
+ - **`update_invoice`** — `{ invoice_id, notes?, terms?, due_offset_days?, invoice_number?, lines? }`
110
+ Partial update. Note: if you pass `lines`, it **replaces** all line items on the invoice.
111
+ - **`send_invoice`** — `{ invoice_id, recipients?, subject?, body? }`
112
+ Emails the invoice (FreshBooks `action_email: true`). Defaults to the client's email on file.
113
+ - **`delete_invoice`** — `{ invoice_id }`
114
+ **Soft delete.** Sets `vis_state=1`. The invoice is recoverable from the FreshBooks UI's deleted items view; this server does not expose a permanent-delete tool.
115
+
116
+ ### Clients
117
+
118
+ - **`list_clients`** — `{ search?, page?, per_page? }`
119
+ - **`get_client`** — `{ client_id: number }`
120
+
121
+ ### Items
122
+
123
+ - **`list_items`** — `{ search?, page?, per_page? }`
124
+ Saved line items, useful when constructing invoices.
125
+
126
+ ### Account
127
+
128
+ - **`get_account_info`** — `{}`
129
+ Returns `{ account_id, business_id, business_name }`. Use this to verify setup.
130
+
131
+ ## Example prompts in Claude Desktop
132
+
133
+ - "List all overdue invoices."
134
+ - "Show me invoice 12345."
135
+ - "Find clients matching 'Acme'."
136
+ - "Draft an invoice to client 678910 for 8 hours of consulting at $150/hr, due in 30 days."
137
+ - "Email invoice 12345 to billing@example.com with subject 'March invoice'."
138
+
139
+ Claude will ask for confirmation before destructive actions like `delete_invoice` or `send_invoice` — review the proposed input before approving.
140
+
141
+ ## Security notes
142
+
143
+ - Tokens are stored at `~/.freshbooks-mcp/tokens.json` with mode 0600. The server refuses to start if the file is group- or world-readable.
144
+ - The client secret is loaded from environment variables, not from disk.
145
+ - `Authorization` headers are never logged. The client redacts request bodies in error messages.
146
+ - All tool inputs are validated with strict Zod schemas (extra fields are rejected).
147
+ - The OAuth setup uses a CSRF `state` parameter and verifies it on the callback.
148
+ - The server binds the OAuth callback listener to `127.0.0.1` only.
149
+
150
+ ## Troubleshooting
151
+
152
+ ### `Refusing to read ~/.freshbooks-mcp/tokens.json: permissions are 644, must be 600`
153
+
154
+ The token file is too permissive. Fix:
155
+
156
+ ```bash
157
+ chmod 600 ~/.freshbooks-mcp/tokens.json
158
+ chmod 700 ~/.freshbooks-mcp
159
+ ```
160
+
161
+ ### `FreshBooks token refresh failed ... Re-run npm run setup to re-authorize.`
162
+
163
+ The refresh token has been revoked or expired. Re-run `npm run setup`. Common causes:
164
+
165
+ - You changed your FreshBooks password.
166
+ - You revoked the app's access in your FreshBooks settings.
167
+ - The refresh token has been unused for a very long time.
168
+
169
+ ### `Could not refresh FreshBooks access token at startup`
170
+
171
+ Same as above. The server proactively refreshes if the access token is within 60s of expiry, so this surfaces stale-refresh-token problems early.
172
+
173
+ ### Claude Desktop says the server crashed / disappeared
174
+
175
+ 1. Check Claude Desktop's log file (Help -> Open Developer Tools -> Console for the renderer; the main process log is in the same directory as the config file).
176
+ 2. Run the server manually to see its stderr:
177
+ ```bash
178
+ FRESHBOOKS_CLIENT_ID=... FRESHBOOKS_CLIENT_SECRET=... node /Users/mp/.claude/mcp-servers/freshbooks/dist/index.js
179
+ ```
180
+ It will sit waiting for stdio JSON-RPC; press Ctrl+C to exit. If startup failed it prints `[freshbooks-mcp] fatal: ...` to stderr.
181
+ 3. Common fix: re-run `npm run build` after pulling updates.
182
+
183
+ ### `429 Too Many Requests` showing up
184
+
185
+ The server respects FreshBooks' `Retry-After` header automatically (single retry). If you're hitting the limit consistently, increase `per_page` to fetch more per call, or cache results in your conversation.
186
+
187
+ ### Multiple businesses on one FreshBooks account
188
+
189
+ `npm run setup` picks the first business and prints a note. To target a different one, set these in the server's `env` block (they take precedence over the discovered values in `tokens.json`):
190
+
191
+ ```json
192
+ "env": {
193
+ "FRESHBOOKS_CLIENT_ID": "your_client_id",
194
+ "FRESHBOOKS_CLIENT_SECRET": "your_client_secret",
195
+ "FRESHBOOKS_ACCOUNT_ID": "abc123",
196
+ "FRESHBOOKS_BUSINESS_ID": "456"
197
+ }
198
+ ```
199
+
200
+ `FRESHBOOKS_ACCOUNT_ID` is used for `/accounting` and `/uploads` paths (invoices, clients, items, attachments); `FRESHBOOKS_BUSINESS_ID` is used for `/projects` and `/timetracking` paths (time entries, timesheets). You can find both by hitting `https://api.freshbooks.com/auth/api/v1/users/me` with your access token, or by running the `get_account_info` tool. (Editing `~/.freshbooks-mcp/tokens.json` directly still works too — the server re-reads it on each request — but env vars are the cleaner option.)
201
+
202
+ ## File layout
203
+
204
+ ```
205
+ src/
206
+ index.ts # MCP server entry (stdio)
207
+ setup.ts # OAuth setup CLI
208
+ freshbooks/
209
+ auth.ts # token load/save/refresh
210
+ client.ts # API client with auto-refresh + 429 handling
211
+ types.ts # FreshBooks DTOs
212
+ tools/
213
+ invoices.ts # list/get/create/update/send/delete
214
+ clients.ts # list/get
215
+ items.ts # list
216
+ account.ts # get_account_info
217
+ ```
218
+
219
+ ## License
220
+
221
+ Private — for personal use.
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Token persistence and refresh for FreshBooks OAuth2.
3
+ *
4
+ * Tokens are stored at ~/.freshbooks-mcp/tokens.json with mode 0600.
5
+ * The file holds the access_token, refresh_token, expiry, and the
6
+ * discovered account_id + business_name so we don't refetch on every start.
7
+ */
8
+ import { promises as fs } from "node:fs";
9
+ import path from "node:path";
10
+ import os from "node:os";
11
+ const TOKEN_URL = "https://api.freshbooks.com/auth/oauth/token";
12
+ export function defaultTokensPath() {
13
+ const override = process.env.FRESHBOOKS_TOKENS_PATH;
14
+ if (override && override.length > 0) {
15
+ return override.startsWith("~")
16
+ ? path.join(os.homedir(), override.slice(1))
17
+ : override;
18
+ }
19
+ return path.join(os.homedir(), ".freshbooks-mcp", "tokens.json");
20
+ }
21
+ /**
22
+ * Optional override for the account_id used in /accounting and /uploads paths.
23
+ * When unset, the value discovered during setup (stored in tokens.json) is used.
24
+ */
25
+ export function accountIdOverride() {
26
+ const v = process.env.FRESHBOOKS_ACCOUNT_ID;
27
+ return v && v.trim().length > 0 ? v.trim() : undefined;
28
+ }
29
+ /**
30
+ * Optional override for the business_id used in /projects and /timetracking paths.
31
+ * When unset, the value discovered during setup (stored in tokens.json) is used.
32
+ */
33
+ export function businessIdOverride() {
34
+ const v = process.env.FRESHBOOKS_BUSINESS_ID;
35
+ if (!v || v.trim().length === 0)
36
+ return undefined;
37
+ const n = Number(v.trim());
38
+ if (!Number.isInteger(n) || n <= 0) {
39
+ throw new Error(`FRESHBOOKS_BUSINESS_ID must be a positive integer, got: ${v}`);
40
+ }
41
+ return n;
42
+ }
43
+ export function loadAppCredentials() {
44
+ const client_id = process.env.FRESHBOOKS_CLIENT_ID;
45
+ const client_secret = process.env.FRESHBOOKS_CLIENT_SECRET;
46
+ const redirect_uri = process.env.FRESHBOOKS_REDIRECT_URI ?? "https://localhost:8765/callback";
47
+ if (!client_id || !client_secret) {
48
+ throw new Error("Missing FRESHBOOKS_CLIENT_ID or FRESHBOOKS_CLIENT_SECRET. " +
49
+ "Set them in your environment (or in the env block of your Claude Desktop config) " +
50
+ "and re-run setup if you have not yet authorized.");
51
+ }
52
+ return { client_id, client_secret, redirect_uri };
53
+ }
54
+ /**
55
+ * Load tokens from disk. Refuses to read a world- or group-readable file.
56
+ */
57
+ export async function loadTokens(filePath = defaultTokensPath()) {
58
+ let stat;
59
+ try {
60
+ stat = await fs.stat(filePath);
61
+ }
62
+ catch {
63
+ throw new Error(`No tokens file at ${filePath}. Run the freshbooks-setup command to authorize FreshBooks.`);
64
+ }
65
+ // On non-Windows, enforce 0600. mode is the low 9 bits of stat.mode.
66
+ if (process.platform !== "win32") {
67
+ const mode = stat.mode & 0o777;
68
+ if (mode & 0o077) {
69
+ throw new Error(`Refusing to read ${filePath}: permissions are ${mode.toString(8)}, ` +
70
+ `must be 600. Run: chmod 600 ${filePath}`);
71
+ }
72
+ }
73
+ const raw = await fs.readFile(filePath, "utf8");
74
+ const parsed = JSON.parse(raw);
75
+ if (!parsed.access_token || !parsed.refresh_token || !parsed.account_id) {
76
+ throw new Error(`Tokens file at ${filePath} is missing required fields. Re-run \`npm run setup\`.`);
77
+ }
78
+ return parsed;
79
+ }
80
+ /**
81
+ * Persist tokens with mode 0600. Creates the parent dir if needed.
82
+ */
83
+ export async function saveTokens(tokens, filePath = defaultTokensPath()) {
84
+ const dir = path.dirname(filePath);
85
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
86
+ const json = JSON.stringify(tokens, null, 2);
87
+ // Write with restrictive mode from the start.
88
+ await fs.writeFile(filePath, json, { mode: 0o600, encoding: "utf8" });
89
+ // Belt-and-suspenders: chmod in case umask interfered.
90
+ if (process.platform !== "win32") {
91
+ await fs.chmod(filePath, 0o600);
92
+ }
93
+ }
94
+ /**
95
+ * Exchange an authorization code for tokens.
96
+ */
97
+ export async function exchangeAuthCode(creds, code) {
98
+ const body = {
99
+ grant_type: "authorization_code",
100
+ client_id: creds.client_id,
101
+ client_secret: creds.client_secret,
102
+ redirect_uri: creds.redirect_uri,
103
+ code,
104
+ };
105
+ return doTokenRequest(body);
106
+ }
107
+ /**
108
+ * Use a refresh token to get a new access token.
109
+ */
110
+ export async function refreshAccessToken(creds, refresh_token) {
111
+ const body = {
112
+ grant_type: "refresh_token",
113
+ client_id: creds.client_id,
114
+ client_secret: creds.client_secret,
115
+ redirect_uri: creds.redirect_uri,
116
+ refresh_token,
117
+ };
118
+ return doTokenRequest(body);
119
+ }
120
+ async function doTokenRequest(body) {
121
+ const res = await fetch(TOKEN_URL, {
122
+ method: "POST",
123
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
124
+ body: JSON.stringify(body),
125
+ });
126
+ if (!res.ok) {
127
+ const text = await res.text();
128
+ // Never echo the request body — it contains the client_secret.
129
+ throw new Error(`FreshBooks token endpoint returned ${res.status}: ${truncate(text, 500)}`);
130
+ }
131
+ const data = (await res.json());
132
+ if (!data.access_token || !data.refresh_token) {
133
+ throw new Error("FreshBooks token response missing access_token or refresh_token.");
134
+ }
135
+ return data;
136
+ }
137
+ function truncate(s, max) {
138
+ return s.length > max ? `${s.slice(0, max)}...` : s;
139
+ }
140
+ /**
141
+ * Compute expires_at (epoch seconds) from a Date and expires_in seconds,
142
+ * subtracting a 60s safety margin so we refresh before the server thinks it expired.
143
+ */
144
+ export function computeExpiresAt(expires_in, now = new Date()) {
145
+ return Math.floor(now.getTime() / 1000) + expires_in - 60;
146
+ }
147
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/freshbooks/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Typed FreshBooks API client.
3
+ *
4
+ * - Auto-refreshes the access token on 401 (single retry).
5
+ * - Respects 429 Retry-After (single retry with capped backoff).
6
+ * - Unwraps the FreshBooks `{ response: { result: { ... } } }` envelope.
7
+ * - Builds URLs with URL/URLSearchParams — no string concatenation.
8
+ * - attachFileToInvoice: uploads a file to FreshBooks and associates it with an invoice.
9
+ */
10
+ import fs from "node:fs/promises";
11
+ import path from "node:path";
12
+ import { accountIdOverride, businessIdOverride, computeExpiresAt, loadTokens, refreshAccessToken, saveTokens, } from "./auth.js";
13
+ const API_BASE = "https://api.freshbooks.com";
14
+ export class FreshBooksError extends Error {
15
+ status;
16
+ body;
17
+ constructor(message, status, body) {
18
+ super(message);
19
+ this.name = "FreshBooksError";
20
+ this.status = status;
21
+ this.body = body;
22
+ }
23
+ }
24
+ export class FreshBooksClient {
25
+ creds;
26
+ tokens;
27
+ tokensPath;
28
+ timeoutMs;
29
+ constructor(opts) {
30
+ this.creds = opts.creds;
31
+ this.tokens = opts.tokens;
32
+ this.tokensPath = opts.tokensPath;
33
+ this.timeoutMs = opts.timeoutMs ?? 30_000;
34
+ }
35
+ // FRESHBOOKS_ACCOUNT_ID / FRESHBOOKS_BUSINESS_ID env vars take precedence over
36
+ // the values discovered during setup — useful when one FreshBooks login owns
37
+ // multiple businesses and you want to target one other than the first.
38
+ get accountId() {
39
+ return accountIdOverride() ?? this.tokens.account_id;
40
+ }
41
+ get businessName() {
42
+ return this.tokens.business_name;
43
+ }
44
+ get businessId() {
45
+ return businessIdOverride() ?? this.tokens.business_id;
46
+ }
47
+ /**
48
+ * Low-level request. Handles auth refresh on 401 and Retry-After on 429.
49
+ */
50
+ async request(method, pathname, options = {}) {
51
+ return this.requestWithRetry(method, pathname, options, 0);
52
+ }
53
+ async requestWithRetry(method, pathname, options, attempt) {
54
+ // Reload tokens from disk on the first attempt so changes to tokens.json
55
+ // (e.g. switching account_id) take effect without restarting the server.
56
+ if (attempt === 0) {
57
+ try {
58
+ this.tokens = await loadTokens(this.tokensPath);
59
+ }
60
+ catch {
61
+ // If reload fails, continue with cached tokens.
62
+ }
63
+ }
64
+ const url = new URL(pathname, API_BASE);
65
+ if (options.query) {
66
+ for (const [k, v] of Object.entries(options.query)) {
67
+ if (v === undefined || v === null || v === "")
68
+ continue;
69
+ url.searchParams.set(k, String(v));
70
+ }
71
+ }
72
+ const headers = {
73
+ Authorization: `Bearer ${this.tokens.access_token}`,
74
+ "Api-Version": "alpha",
75
+ Accept: "application/json",
76
+ };
77
+ let body;
78
+ if (options.body !== undefined) {
79
+ headers["Content-Type"] = "application/json";
80
+ body = JSON.stringify(options.body);
81
+ }
82
+ const controller = new AbortController();
83
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
84
+ let res;
85
+ try {
86
+ res = await fetch(url.toString(), {
87
+ method,
88
+ headers,
89
+ body,
90
+ signal: controller.signal,
91
+ });
92
+ }
93
+ finally {
94
+ clearTimeout(timer);
95
+ }
96
+ // 401 -> try one refresh, then retry once.
97
+ if (res.status === 401 && attempt === 0) {
98
+ await this.refresh();
99
+ return this.requestWithRetry(method, pathname, options, attempt + 1);
100
+ }
101
+ // 429 -> respect Retry-After (single retry).
102
+ if (res.status === 429 && attempt === 0) {
103
+ const retryAfter = Number(res.headers.get("retry-after") ?? "1");
104
+ const delayMs = Math.min(Math.max(retryAfter, 1), 30) * 1000;
105
+ await new Promise((r) => setTimeout(r, delayMs));
106
+ return this.requestWithRetry(method, pathname, options, attempt + 1);
107
+ }
108
+ if (!res.ok) {
109
+ const text = await res.text();
110
+ let parsed = text;
111
+ try {
112
+ parsed = JSON.parse(text);
113
+ }
114
+ catch {
115
+ /* leave as text */
116
+ }
117
+ throw new FreshBooksError(`FreshBooks ${method} ${pathname} failed: ${res.status}`, res.status, parsed);
118
+ }
119
+ if (res.status === 204)
120
+ return undefined;
121
+ const text = await res.text();
122
+ if (text.length === 0)
123
+ return undefined;
124
+ return JSON.parse(text);
125
+ }
126
+ /**
127
+ * Refresh the access token and persist the new bundle.
128
+ * Throws with a clear "re-run setup" hint if the refresh token is rejected.
129
+ */
130
+ async refresh() {
131
+ try {
132
+ const fresh = await refreshAccessToken(this.creds, this.tokens.refresh_token);
133
+ this.tokens = {
134
+ ...this.tokens,
135
+ access_token: fresh.access_token,
136
+ refresh_token: fresh.refresh_token,
137
+ expires_at: computeExpiresAt(fresh.expires_in),
138
+ updated_at: new Date().toISOString(),
139
+ };
140
+ await saveTokens(this.tokens, this.tokensPath);
141
+ }
142
+ catch (err) {
143
+ const msg = err instanceof Error ? err.message : String(err);
144
+ throw new Error(`FreshBooks token refresh failed (${msg}). ` +
145
+ "Re-run the freshbooks-setup command to re-authorize.");
146
+ }
147
+ }
148
+ // ---------- Domain helpers (unwrap FreshBooks envelopes) ----------
149
+ /**
150
+ * Accounting endpoints wrap responses in { response: { result: { ... } } }.
151
+ * If `resultKey` is provided, returns result[key] (e.g. "invoice", "client").
152
+ * If empty string, returns the whole result object (used for list endpoints
153
+ * where the result holds the collection plus pagination siblings).
154
+ */
155
+ async accounting(method, pathname, resultKey, options = {}) {
156
+ const env = await this.request(method, pathname, options);
157
+ if (!env || !env.response || !env.response.result) {
158
+ throw new FreshBooksError(`Unexpected response shape from ${pathname}`, 200, env);
159
+ }
160
+ if (resultKey === "") {
161
+ return env.response.result;
162
+ }
163
+ return env.response.result[resultKey];
164
+ }
165
+ /**
166
+ * Identity endpoints (/auth/api/v1/...) return { response: { ... } } without `result`.
167
+ */
168
+ async identity(pathname) {
169
+ const env = await this.request("GET", pathname);
170
+ if (!env || !env.response) {
171
+ throw new FreshBooksError(`Unexpected identity response from ${pathname}`, 200, env);
172
+ }
173
+ return env.response;
174
+ }
175
+ /**
176
+ * Upload a local file as a FreshBooks attachment, then associate it with an invoice.
177
+ *
178
+ * FreshBooks two-step process:
179
+ * 1. POST /uploads/account/{id}/attachments (multipart/form-data)
180
+ * → returns { jwt, public_id } to identify the upload
181
+ * 2. POST /accounting/account/{id}/invoices/invoices/{inv}/attachments
182
+ * → binds the upload to the invoice
183
+ */
184
+ async attachFileToInvoice(invoiceId, filePath) {
185
+ const fileName = path.basename(filePath);
186
+ const fileBuffer = await fs.readFile(filePath);
187
+ // ── Step 1: upload ────────────────────────────────────────────────────────
188
+ const formData = new FormData();
189
+ const blob = new Blob([fileBuffer], {
190
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
191
+ });
192
+ formData.append("content", blob, fileName);
193
+ const uploadUrl = `${API_BASE}/uploads/account/${this.tokens.account_id}/attachments`;
194
+ const uploadRes = await fetch(uploadUrl, {
195
+ method: "POST",
196
+ headers: { Authorization: `Bearer ${this.tokens.access_token}` },
197
+ body: formData,
198
+ });
199
+ if (!uploadRes.ok) {
200
+ const text = await uploadRes.text();
201
+ throw new FreshBooksError(`Attachment upload failed: ${uploadRes.status}`, uploadRes.status, text);
202
+ }
203
+ // The uploads endpoint returns several possible shapes depending on API version.
204
+ // Try each known path before giving up.
205
+ const uploadJson = (await uploadRes.json());
206
+ const att = uploadJson?.["response"]?.["result"] ??
207
+ uploadJson?.["attachment"] ??
208
+ uploadJson;
209
+ const jwt = att?.["jwt"] ?? undefined;
210
+ const publicId = att?.["public_id"] ?? undefined;
211
+ if (!jwt && !publicId) {
212
+ throw new FreshBooksError(`Attachment upload succeeded but response contained no jwt or public_id`, uploadRes.status, uploadJson);
213
+ }
214
+ // ── Step 2: associate with invoice ────────────────────────────────────────
215
+ const attachBody = { name: fileName };
216
+ if (jwt)
217
+ attachBody["jwt"] = jwt;
218
+ if (publicId)
219
+ attachBody["public_id"] = publicId;
220
+ const assocPath = `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}/attachments`;
221
+ await this.request("POST", assocPath, { body: { attachment: attachBody } });
222
+ return { fileName, attached: true };
223
+ }
224
+ }
225
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/freshbooks/client.ts"],"names":[],"mappings":"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"}