@chrischall/mcp-utils 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/README.md +235 -0
- package/dist/auth/index.d.ts +223 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +267 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/config/index.d.ts +86 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +121 -0
- package/dist/config/index.js.map +1 -0
- package/dist/errors/index.d.ts +90 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +157 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/fetchproxy/index.d.ts +156 -0
- package/dist/fetchproxy/index.d.ts.map +1 -0
- package/dist/fetchproxy/index.js +197 -0
- package/dist/fetchproxy/index.js.map +1 -0
- package/dist/html/index.d.ts +142 -0
- package/dist/html/index.d.ts.map +1 -0
- package/dist/html/index.js +321 -0
- package/dist/html/index.js.map +1 -0
- package/dist/http/index.d.ts +202 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +341 -0
- package/dist/http/index.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/response/index.d.ts +22 -0
- package/dist/response/index.d.ts.map +1 -0
- package/dist/response/index.js +61 -0
- package/dist/response/index.js.map +1 -0
- package/dist/server/index.d.ts +109 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +95 -0
- package/dist/server/index.js.map +1 -0
- package/dist/session/index.d.ts +233 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +404 -0
- package/dist/session/index.js.map +1 -0
- package/dist/test/index.d.ts +124 -0
- package/dist/test/index.d.ts.map +1 -0
- package/dist/test/index.js +181 -0
- package/dist/test/index.js.map +1 -0
- package/dist/zod/index.d.ts +130 -0
- package/dist/zod/index.d.ts.map +1 -0
- package/dist/zod/index.js +184 -0
- package/dist/zod/index.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth resolver skeletons — the shared *shape* of the credential-resolution
|
|
3
|
+
* logic duplicated across the fleet (resy / opentable / ofw / zola /
|
|
4
|
+
* signupgenius / creditkarma / canvas / infinitecampus auth.ts).
|
|
5
|
+
*
|
|
6
|
+
* This module owns the **skeleton** only. Per-site parameters (which env var,
|
|
7
|
+
* which cookies to declare, how to parse the session blob, which CSRF input to
|
|
8
|
+
* scrape) are *injected*; nothing here branches on site identity. Site-specific
|
|
9
|
+
* OAuth choreographies (e.g. Skylight's `/auth/session` → `/oauth/authorize` →
|
|
10
|
+
* `/oauth/token` dance) stay per-MCP — they are not a shared shape.
|
|
11
|
+
*
|
|
12
|
+
* Four pieces:
|
|
13
|
+
* - {@link createAuthResolver} — the three-path resolver
|
|
14
|
+
* (env credential → fetchproxy one-shot read → actionable error).
|
|
15
|
+
* - {@link resolveAuthPattern} — the four-path variant
|
|
16
|
+
* (token → OAuth → session-scrape → fetchproxy), each path an injected
|
|
17
|
+
* resolver tried in priority order.
|
|
18
|
+
* - {@link sessionLoginFlow} — CSRF-scrape + cookie POST + success-marker,
|
|
19
|
+
* the shared CSRF+cookie login primitive (canvas / IC / ofw / signupgenius).
|
|
20
|
+
* - {@link createOAuth2Refresher} — an OAuth2 `refresh_token`-grant refresher
|
|
21
|
+
* with optional retry and in-flight race-safety.
|
|
22
|
+
*
|
|
23
|
+
* Security posture: env reads go through the hardened {@link readEnvVar}
|
|
24
|
+
* (placeholder / `'null'` / `'undefined'` suppression); thrown errors never
|
|
25
|
+
* echo the offending secret value and run upstream bodies through
|
|
26
|
+
* {@link truncateErrorMessage} (redaction + truncation) before surfacing.
|
|
27
|
+
*/
|
|
28
|
+
import { readEnvVar, parseBoolEnv } from '../config/index.js';
|
|
29
|
+
import { truncateErrorMessage, SessionNotAuthenticatedError, createHelpfulError, } from '../errors/index.js';
|
|
30
|
+
import { parseCookieJar } from '../http/index.js';
|
|
31
|
+
/**
|
|
32
|
+
* Build the canonical **three-path** auth resolver:
|
|
33
|
+
*
|
|
34
|
+
* 1. **Env credential** — `envVar` set (after hardened {@link readEnvVar}
|
|
35
|
+
* sanitization) → returned directly, no network.
|
|
36
|
+
* 2. **fetchproxy one-shot read** — unless `disableEnvVar` is truthy, call the
|
|
37
|
+
* injected `bootstrap` to snapshot the user's signed-in browser session,
|
|
38
|
+
* then `parseTokens` to extract the credential. fetchproxy is invoked once;
|
|
39
|
+
* it is never in the hot path.
|
|
40
|
+
* 3. **Actionable error** — nothing configured: an error naming the env var
|
|
41
|
+
* and the sign-in fallback so the user can pick a fix.
|
|
42
|
+
*
|
|
43
|
+
* The returned `source` is for diagnostics; callers must treat the credential
|
|
44
|
+
* as opaque and not branch on it.
|
|
45
|
+
*/
|
|
46
|
+
export function createAuthResolver(opts) {
|
|
47
|
+
const { envVar, disableEnvVar, bootstrap, bootstrapOptions, parseTokens, serviceName, signInHost, env, } = opts;
|
|
48
|
+
return async function resolveAuth() {
|
|
49
|
+
// ── Path 1: env-var credential (hardened against placeholder/sentinel leakage).
|
|
50
|
+
const envCredential = readEnvVar(envVar, env ? { env } : {});
|
|
51
|
+
if (envCredential) {
|
|
52
|
+
return { credential: envCredential, source: 'env' };
|
|
53
|
+
}
|
|
54
|
+
// ── Path 2: fetchproxy one-shot fallback (unless explicitly disabled).
|
|
55
|
+
const disabled = disableEnvVar !== undefined &&
|
|
56
|
+
parseBoolEnv(disableEnvVar, env ? { env } : {});
|
|
57
|
+
if (!disabled) {
|
|
58
|
+
let session;
|
|
59
|
+
try {
|
|
60
|
+
session = await bootstrap(bootstrapOptions);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
// Surface the fallback failure but point back at the env-var escape
|
|
64
|
+
// hatch. Redact + truncate the underlying message.
|
|
65
|
+
throw createHelpfulError(`Auth: no ${envVar} set, and fetchproxy fallback failed: ${truncateErrorMessage(messageOf(e))}`, { hint: `Set ${envVar}, or sign in in your browser and retry.` });
|
|
66
|
+
}
|
|
67
|
+
const credential = parseTokens(session);
|
|
68
|
+
if (credential) {
|
|
69
|
+
return { credential, source: 'fetchproxy' };
|
|
70
|
+
}
|
|
71
|
+
// Bootstrap succeeded but the declared key wasn't present → the browser
|
|
72
|
+
// tab isn't signed in. This is the stable "go authenticate" condition.
|
|
73
|
+
throw new SessionNotAuthenticatedError(serviceName, signInHost);
|
|
74
|
+
}
|
|
75
|
+
// ── Path 3: nothing configured and fetchproxy disabled.
|
|
76
|
+
throw createHelpfulError(`Auth: set ${envVar}${signInHost ? `, or sign in at ${signInHost} in your browser` : ', or sign in in your browser'}` +
|
|
77
|
+
`${disableEnvVar ? ` (unset ${disableEnvVar} if it is set)` : ''}.`, { hint: `Set ${envVar} or sign in in your browser.` });
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve auth via the four-path priority **token → OAuth → session-scrape →
|
|
82
|
+
* fetchproxy**. Runs the first *provided* resolver in that order (a missing
|
|
83
|
+
* resolver = an unconfigured path). A resolver that throws propagates — a
|
|
84
|
+
* partial-config error (the user's mistake) must surface, not silently fall
|
|
85
|
+
* through. Throws an actionable error when no path is configured at all.
|
|
86
|
+
*/
|
|
87
|
+
export async function resolveAuthPattern(pattern) {
|
|
88
|
+
const ordered = [
|
|
89
|
+
pattern.token,
|
|
90
|
+
pattern.oauth,
|
|
91
|
+
pattern.sessionScrape,
|
|
92
|
+
pattern.fetchproxy,
|
|
93
|
+
];
|
|
94
|
+
for (const resolver of ordered) {
|
|
95
|
+
if (resolver)
|
|
96
|
+
return resolver();
|
|
97
|
+
}
|
|
98
|
+
throw createHelpfulError('No auth configured. Provide a token, OAuth credentials, login credentials, ' +
|
|
99
|
+
'or sign in in your browser (fetchproxy fallback).', { hint: 'Set one of the supported credential env vars, or sign in in your browser.' });
|
|
100
|
+
}
|
|
101
|
+
const DEFAULT_LOGIN_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36';
|
|
102
|
+
/** Node's `Headers` may carry multiple `Set-Cookie`s; prefer the spec'd getter. */
|
|
103
|
+
function readSetCookies(headers) {
|
|
104
|
+
const h = headers;
|
|
105
|
+
if (typeof h.getSetCookie === 'function')
|
|
106
|
+
return h.getSetCookie();
|
|
107
|
+
const raw = headers.get('set-cookie');
|
|
108
|
+
return raw ? [raw] : [];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The shared CSRF + cookie login primitive (canvas / IC / ofw / signupgenius):
|
|
112
|
+
*
|
|
113
|
+
* 1. GET `loginUrl` — capture the session cookie(s) and scrape the CSRF token
|
|
114
|
+
* out of the page with `csrfRegex`.
|
|
115
|
+
* 2. POST `postUrl` (`application/x-www-form-urlencoded`) with the scraped CSRF
|
|
116
|
+
* token, credentials, and any `extraFields`, carrying the GET's cookies.
|
|
117
|
+
* 3. Merge `Set-Cookie`s from both responses (deduped, deletions dropped) and
|
|
118
|
+
* require the `tokenField` cookie as the success marker — its value is the
|
|
119
|
+
* returned `token`; its absence means the credentials were rejected.
|
|
120
|
+
*
|
|
121
|
+
* Per-site form parameters and the CSRF regex are injected; the flow itself is
|
|
122
|
+
* site-agnostic.
|
|
123
|
+
*/
|
|
124
|
+
export async function sessionLoginFlow(opts) {
|
|
125
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
126
|
+
const ua = opts.userAgent ?? DEFAULT_LOGIN_UA;
|
|
127
|
+
const csrfField = opts.csrfField ?? 'csrfToken';
|
|
128
|
+
const emailField = opts.emailField ?? 'email';
|
|
129
|
+
const passwordField = opts.passwordField ?? 'password';
|
|
130
|
+
// Step 1: GET the login page → session cookie + CSRF token.
|
|
131
|
+
const pageRes = await doFetch(opts.loginUrl, {
|
|
132
|
+
headers: { 'User-Agent': ua, Accept: 'text/html' },
|
|
133
|
+
});
|
|
134
|
+
if (!pageRes.ok) {
|
|
135
|
+
throw createHelpfulError(`Login page returned ${pageRes.status} ${pageRes.statusText}`, {
|
|
136
|
+
hint: 'The upstream login page is unreachable; retry later.',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const pageJar = parseCookieJar(readSetCookies(pageRes.headers));
|
|
140
|
+
const html = await pageRes.text();
|
|
141
|
+
const csrfMatch = opts.csrfRegex.exec(html);
|
|
142
|
+
const csrfToken = csrfMatch?.[1];
|
|
143
|
+
if (!csrfToken) {
|
|
144
|
+
throw createHelpfulError('CSRF token not found on the login page.', {
|
|
145
|
+
hint: 'The login page layout may have changed, or the page failed to load.',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// Step 2: POST credentials with the scraped CSRF token.
|
|
149
|
+
const body = new URLSearchParams({
|
|
150
|
+
[csrfField]: csrfToken,
|
|
151
|
+
[emailField]: opts.email,
|
|
152
|
+
[passwordField]: opts.password,
|
|
153
|
+
...opts.extraFields,
|
|
154
|
+
}).toString();
|
|
155
|
+
const postRes = await doFetch(opts.postUrl, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
redirect: 'manual',
|
|
158
|
+
headers: {
|
|
159
|
+
'User-Agent': ua,
|
|
160
|
+
Accept: 'text/html',
|
|
161
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
162
|
+
Referer: opts.loginUrl,
|
|
163
|
+
...(pageJar.cookieHeader ? { Cookie: pageJar.cookieHeader } : {}),
|
|
164
|
+
},
|
|
165
|
+
body,
|
|
166
|
+
});
|
|
167
|
+
// Step 3: merge cookies from both responses; require the success marker.
|
|
168
|
+
const merged = parseCookieJar([
|
|
169
|
+
...readSetCookies(pageRes.headers),
|
|
170
|
+
...readSetCookies(postRes.headers),
|
|
171
|
+
]);
|
|
172
|
+
const token = merged.cookies[opts.tokenField];
|
|
173
|
+
if (!token) {
|
|
174
|
+
throw createHelpfulError(`Login did not yield a ${opts.tokenField} cookie (status ${postRes.status}) — the credentials were rejected.`, {
|
|
175
|
+
hint: 'Verify the configured email and password. SSO and 2FA-protected accounts are not supported in this mode.',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return { token, cookies: merged.cookieHeader };
|
|
179
|
+
}
|
|
180
|
+
const sleep = (ms) => ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
|
|
181
|
+
/**
|
|
182
|
+
* Build a race-safe OAuth2 `refresh_token`-grant refresher. The returned
|
|
183
|
+
* function POSTs the form-encoded grant to `endpoint` and parses the standard
|
|
184
|
+
* `{ access_token, refresh_token?, expires_in? }` body.
|
|
185
|
+
*
|
|
186
|
+
* Race-safety: concurrent calls share a single in-flight exchange (the
|
|
187
|
+
* canonical token-refresh-race guard — `skylight`/`canvas`/`creditkarma`/`zola`
|
|
188
|
+
* all hand-roll this). The in-flight promise is cleared once it settles, so a
|
|
189
|
+
* later refresh starts fresh and a *rejected* exchange does not poison the next
|
|
190
|
+
* caller.
|
|
191
|
+
*
|
|
192
|
+
* Errors run through {@link truncateErrorMessage} (redaction + truncation)
|
|
193
|
+
* before surfacing, so an upstream error body can't leak a bearer token or
|
|
194
|
+
* blow up a tool result.
|
|
195
|
+
*/
|
|
196
|
+
export function createOAuth2Refresher(opts) {
|
|
197
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
198
|
+
const grantType = opts.grantType ?? 'refresh_token';
|
|
199
|
+
const maxRetries = opts.retry?.count ?? 0;
|
|
200
|
+
const retryDelayMs = opts.retry?.delayMs ?? 0;
|
|
201
|
+
let inFlight = null;
|
|
202
|
+
async function exchangeOnce() {
|
|
203
|
+
const body = new URLSearchParams({
|
|
204
|
+
grant_type: grantType,
|
|
205
|
+
refresh_token: opts.refreshToken,
|
|
206
|
+
...opts.params,
|
|
207
|
+
}).toString();
|
|
208
|
+
const res = await doFetch(opts.endpoint, {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers: {
|
|
211
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
212
|
+
Accept: 'application/json',
|
|
213
|
+
},
|
|
214
|
+
body,
|
|
215
|
+
});
|
|
216
|
+
if (!res.ok) {
|
|
217
|
+
const errText = await res.text().catch(() => '');
|
|
218
|
+
throw createHelpfulError(`OAuth2 token refresh failed: ${res.status} ${res.statusText}: ${truncateErrorMessage(errText, 200)}`, { hint: 'The refresh token may be expired or revoked — re-authenticate.' });
|
|
219
|
+
}
|
|
220
|
+
const data = (await res.json().catch(() => null));
|
|
221
|
+
const accessToken = data?.access_token;
|
|
222
|
+
if (typeof accessToken !== 'string' || accessToken.length === 0) {
|
|
223
|
+
throw createHelpfulError('OAuth2 token refresh returned no access_token.', {
|
|
224
|
+
hint: 'The token endpoint returned an unexpected body.',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const result = { accessToken };
|
|
228
|
+
if (typeof data?.refresh_token === 'string' && data.refresh_token.length > 0) {
|
|
229
|
+
result.refreshToken = data.refresh_token;
|
|
230
|
+
}
|
|
231
|
+
if (typeof data?.expires_in === 'number') {
|
|
232
|
+
result.expiresIn = data.expires_in;
|
|
233
|
+
result.expiresAt = new Date(Date.now() + data.expires_in * 1000);
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
async function exchangeWithRetry() {
|
|
238
|
+
let lastErr;
|
|
239
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
240
|
+
try {
|
|
241
|
+
return await exchangeOnce();
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
lastErr = e;
|
|
245
|
+
if (attempt < maxRetries)
|
|
246
|
+
await sleep(retryDelayMs);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
250
|
+
}
|
|
251
|
+
return function refresh() {
|
|
252
|
+
// Coalesce concurrent callers onto one exchange; clear on settle so the
|
|
253
|
+
// next call starts fresh (and a rejection doesn't stick).
|
|
254
|
+
if (inFlight)
|
|
255
|
+
return inFlight;
|
|
256
|
+
const p = exchangeWithRetry().finally(() => {
|
|
257
|
+
if (inFlight === p)
|
|
258
|
+
inFlight = null;
|
|
259
|
+
});
|
|
260
|
+
inFlight = p;
|
|
261
|
+
return p;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function messageOf(err) {
|
|
265
|
+
return err instanceof Error ? err.message : String(err);
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** An environment-variable source: typically `process.env`. */
|
|
2
|
+
export type EnvSource = Record<string, string | undefined>;
|
|
3
|
+
/** Options shared by the env-reading helpers. */
|
|
4
|
+
export interface ReadEnvOptions {
|
|
5
|
+
/** The source to read from. Defaults to {@link process.env}. */
|
|
6
|
+
env?: EnvSource;
|
|
7
|
+
/** Value to return when the variable is unset. */
|
|
8
|
+
default?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read an environment variable defensively.
|
|
12
|
+
*
|
|
13
|
+
* The value is trimmed of surrounding whitespace and treated as **unset** when
|
|
14
|
+
* it is:
|
|
15
|
+
* - absent or a non-string,
|
|
16
|
+
* - empty / whitespace-only,
|
|
17
|
+
* - the literal string `'undefined'` or `'null'`, or
|
|
18
|
+
* - an unsubstituted `${...}` placeholder.
|
|
19
|
+
*
|
|
20
|
+
* When unset, returns `opts.default` if provided, otherwise `undefined`.
|
|
21
|
+
*
|
|
22
|
+
* Consolidates the `readVar`/`readEnv`/`readEnvString`/`sanitizeEnvVar` snippet
|
|
23
|
+
* duplicated across 12+ MCP servers.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readEnvVar(key: string, opts?: ReadEnvOptions): string | undefined;
|
|
26
|
+
/** Options for {@link requireEnvVar}. */
|
|
27
|
+
export interface RequireEnvOptions {
|
|
28
|
+
/** The source to read from. Defaults to {@link process.env}. */
|
|
29
|
+
env?: EnvSource;
|
|
30
|
+
/** Remediation text appended to the thrown error ("here's how to fix it"). */
|
|
31
|
+
hint?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Like {@link readEnvVar} but throws a helpful, value-free error when the
|
|
35
|
+
* variable is unset (or a placeholder/sentinel). The error names only the
|
|
36
|
+
* variable and the optional `hint` — never the offending value — so a leaked
|
|
37
|
+
* placeholder is not echoed back to the caller.
|
|
38
|
+
*/
|
|
39
|
+
export declare function requireEnvVar(key: string, opts?: RequireEnvOptions): string;
|
|
40
|
+
/** Options for {@link parseBoolEnv}. */
|
|
41
|
+
export interface ParseBoolEnvOptions {
|
|
42
|
+
/** The source to read from. Defaults to {@link process.env}. */
|
|
43
|
+
env?: EnvSource;
|
|
44
|
+
/** Result when the variable is unset or unrecognised. Defaults to `false`. */
|
|
45
|
+
default?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse a boolean-ish environment variable. Recognises (case-insensitively)
|
|
49
|
+
* `1/true/yes/on` as `true` and `0/false/no/off` as `false`. Anything unset,
|
|
50
|
+
* placeholder/sentinel, or unrecognised falls back to `opts.default` (`false`).
|
|
51
|
+
*
|
|
52
|
+
* Consolidates the `['1','true','yes','on'].includes(...)` `*_DISABLE_*` flag
|
|
53
|
+
* pattern duplicated across the fleet.
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseBoolEnv(key: string, opts?: ParseBoolEnvOptions): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Expand a user-provided filesystem path:
|
|
58
|
+
* - a leading `~` or `~/...` expands to the user's home directory, and
|
|
59
|
+
* - any remaining relative path is resolved against the current working dir.
|
|
60
|
+
*
|
|
61
|
+
* Note: `~user` (other-user home lookup) and mid-path `~` are intentionally
|
|
62
|
+
* NOT expanded — only the current user's home is ever substituted.
|
|
63
|
+
*/
|
|
64
|
+
export declare function expandPath(p: string): string;
|
|
65
|
+
/** Options for {@link loadDotenvSafely}. */
|
|
66
|
+
export interface LoadDotenvOptions {
|
|
67
|
+
/** Path to the `.env` file. Defaults to dotenv's own resolution (CWD). */
|
|
68
|
+
path?: string;
|
|
69
|
+
/**
|
|
70
|
+
* When `true`, `.env` values overwrite already-set `process.env` entries.
|
|
71
|
+
* Defaults to `false` so real host-provided env always wins.
|
|
72
|
+
*/
|
|
73
|
+
override?: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Load a `.env` file for local development, swallowing any failure.
|
|
77
|
+
*
|
|
78
|
+
* `dotenv` is imported dynamically and the whole thing is wrapped so that a
|
|
79
|
+
* missing module (e.g. inside an mcpb bundle, where credentials arrive via the
|
|
80
|
+
* host's `mcp_config.env`) is a silent no-op rather than a crash. Real
|
|
81
|
+
* environment values take precedence unless `override` is set.
|
|
82
|
+
*
|
|
83
|
+
* @returns `true` if a `.env` file was loaded without error, `false` otherwise.
|
|
84
|
+
*/
|
|
85
|
+
export declare function loadDotenvSafely(opts?: LoadDotenvOptions): Promise<boolean>;
|
|
86
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAGA,+DAA+D;AAC/D,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE3D,iDAAiD;AACjD,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAaD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,MAAM,GAAG,SAAS,CAerF;AAED,yCAAyC;AACzC,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,MAAM,CAO/E;AAED,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAQjF;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ5C;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBrF"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Matches a value that is *entirely* an unsubstituted shell-style placeholder,
|
|
5
|
+
* e.g. `${FOO}` or `${}`. MCP hosts that forward a `.mcp.json` env block without
|
|
6
|
+
* expanding it can leak these literals into credential slots — treating them as
|
|
7
|
+
* unset is the canonical defense against the placeholder-leakage class of bugs.
|
|
8
|
+
*
|
|
9
|
+
* Only a value that is the placeholder *and nothing else* is suppressed; a real
|
|
10
|
+
* secret that happens to embed `${` is preserved.
|
|
11
|
+
*/
|
|
12
|
+
const PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
13
|
+
/**
|
|
14
|
+
* Read an environment variable defensively.
|
|
15
|
+
*
|
|
16
|
+
* The value is trimmed of surrounding whitespace and treated as **unset** when
|
|
17
|
+
* it is:
|
|
18
|
+
* - absent or a non-string,
|
|
19
|
+
* - empty / whitespace-only,
|
|
20
|
+
* - the literal string `'undefined'` or `'null'`, or
|
|
21
|
+
* - an unsubstituted `${...}` placeholder.
|
|
22
|
+
*
|
|
23
|
+
* When unset, returns `opts.default` if provided, otherwise `undefined`.
|
|
24
|
+
*
|
|
25
|
+
* Consolidates the `readVar`/`readEnv`/`readEnvString`/`sanitizeEnvVar` snippet
|
|
26
|
+
* duplicated across 12+ MCP servers.
|
|
27
|
+
*/
|
|
28
|
+
export function readEnvVar(key, opts = {}) {
|
|
29
|
+
const env = opts.env ?? process.env;
|
|
30
|
+
const raw = env[key];
|
|
31
|
+
if (typeof raw === 'string') {
|
|
32
|
+
const trimmed = raw.trim();
|
|
33
|
+
if (trimmed.length > 0 &&
|
|
34
|
+
trimmed !== 'undefined' &&
|
|
35
|
+
trimmed !== 'null' &&
|
|
36
|
+
!PLACEHOLDER_RE.test(trimmed)) {
|
|
37
|
+
return trimmed;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return opts.default;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Like {@link readEnvVar} but throws a helpful, value-free error when the
|
|
44
|
+
* variable is unset (or a placeholder/sentinel). The error names only the
|
|
45
|
+
* variable and the optional `hint` — never the offending value — so a leaked
|
|
46
|
+
* placeholder is not echoed back to the caller.
|
|
47
|
+
*/
|
|
48
|
+
export function requireEnvVar(key, opts = {}) {
|
|
49
|
+
const value = readEnvVar(key, { env: opts.env });
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
const base = `Missing required environment variable ${key}`;
|
|
52
|
+
throw new Error(opts.hint ? `${base}. ${opts.hint}` : base);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
const TRUE_TOKENS = new Set(['1', 'true', 'yes', 'on']);
|
|
57
|
+
const FALSE_TOKENS = new Set(['0', 'false', 'no', 'off']);
|
|
58
|
+
/**
|
|
59
|
+
* Parse a boolean-ish environment variable. Recognises (case-insensitively)
|
|
60
|
+
* `1/true/yes/on` as `true` and `0/false/no/off` as `false`. Anything unset,
|
|
61
|
+
* placeholder/sentinel, or unrecognised falls back to `opts.default` (`false`).
|
|
62
|
+
*
|
|
63
|
+
* Consolidates the `['1','true','yes','on'].includes(...)` `*_DISABLE_*` flag
|
|
64
|
+
* pattern duplicated across the fleet.
|
|
65
|
+
*/
|
|
66
|
+
export function parseBoolEnv(key, opts = {}) {
|
|
67
|
+
const fallback = opts.default ?? false;
|
|
68
|
+
const raw = readEnvVar(key, { env: opts.env });
|
|
69
|
+
if (raw === undefined)
|
|
70
|
+
return fallback;
|
|
71
|
+
const token = raw.toLowerCase();
|
|
72
|
+
if (TRUE_TOKENS.has(token))
|
|
73
|
+
return true;
|
|
74
|
+
if (FALSE_TOKENS.has(token))
|
|
75
|
+
return false;
|
|
76
|
+
return fallback;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Expand a user-provided filesystem path:
|
|
80
|
+
* - a leading `~` or `~/...` expands to the user's home directory, and
|
|
81
|
+
* - any remaining relative path is resolved against the current working dir.
|
|
82
|
+
*
|
|
83
|
+
* Note: `~user` (other-user home lookup) and mid-path `~` are intentionally
|
|
84
|
+
* NOT expanded — only the current user's home is ever substituted.
|
|
85
|
+
*/
|
|
86
|
+
export function expandPath(p) {
|
|
87
|
+
let expanded = p;
|
|
88
|
+
if (p === '~') {
|
|
89
|
+
expanded = homedir();
|
|
90
|
+
}
|
|
91
|
+
else if (p.startsWith('~/')) {
|
|
92
|
+
expanded = join(homedir(), p.slice(2));
|
|
93
|
+
}
|
|
94
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Load a `.env` file for local development, swallowing any failure.
|
|
98
|
+
*
|
|
99
|
+
* `dotenv` is imported dynamically and the whole thing is wrapped so that a
|
|
100
|
+
* missing module (e.g. inside an mcpb bundle, where credentials arrive via the
|
|
101
|
+
* host's `mcp_config.env`) is a silent no-op rather than a crash. Real
|
|
102
|
+
* environment values take precedence unless `override` is set.
|
|
103
|
+
*
|
|
104
|
+
* @returns `true` if a `.env` file was loaded without error, `false` otherwise.
|
|
105
|
+
*/
|
|
106
|
+
export async function loadDotenvSafely(opts = {}) {
|
|
107
|
+
try {
|
|
108
|
+
const mod = (await import(/* @vite-ignore */ 'dotenv'));
|
|
109
|
+
const result = mod.config({
|
|
110
|
+
...(opts.path !== undefined ? { path: opts.path } : {}),
|
|
111
|
+
override: opts.override ?? false,
|
|
112
|
+
quiet: true,
|
|
113
|
+
});
|
|
114
|
+
return result.error === undefined;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// dotenv unavailable (bundled runtime) — rely on process.env.
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared MCP error classes, error wrapping, and bridge-error discrimination.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates the `instanceof`-chains and remediation-message patterns found
|
|
5
|
+
* in `client.ts` / `auth.ts` across the fleet. Every error carries an optional
|
|
6
|
+
* `hint` — a "here's how to fix it" string the tool surface can show the user.
|
|
7
|
+
*
|
|
8
|
+
* The fetchproxy typed-error hierarchy (`Fetchproxy*Error`) is re-exported, not
|
|
9
|
+
* reimplemented; {@link classifyBridgeError} is a thin discriminator over it.
|
|
10
|
+
*/
|
|
11
|
+
/** Default seconds to wait before retrying a tripped bot-wall (issue #90 tuning). */
|
|
12
|
+
export declare const DEFAULT_BOT_WALL_RETRY_AFTER_S = 30;
|
|
13
|
+
/** Default truncation budget for upstream error bodies surfaced to clients. */
|
|
14
|
+
export declare const DEFAULT_ERROR_MESSAGE_MAX = 500;
|
|
15
|
+
/**
|
|
16
|
+
* Base class for every tool-facing error. Carries an optional `hint` —
|
|
17
|
+
* actionable remediation text ("set ZOLA_REFRESH_TOKEN", "sign in at compass.com")
|
|
18
|
+
* the tool surface can present separately from the message.
|
|
19
|
+
*/
|
|
20
|
+
export declare class McpToolError extends Error {
|
|
21
|
+
/** Actionable remediation text, when one applies. */
|
|
22
|
+
readonly hint?: string;
|
|
23
|
+
constructor(message: string, opts?: {
|
|
24
|
+
hint?: string;
|
|
25
|
+
cause?: unknown;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The user's browser session isn't signed in to the upstream service. Distinct
|
|
30
|
+
* from a transient bot-wall — this is a stable "go authenticate" condition.
|
|
31
|
+
*/
|
|
32
|
+
export declare class SessionNotAuthenticatedError extends McpToolError {
|
|
33
|
+
constructor(service?: string, signInHost?: string);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Transient anti-bot interstitial (PerimeterX / DataDome CAPTCHA). The request
|
|
37
|
+
* was rate-limited, NOT a missing resource — back off and retry. Kept distinct
|
|
38
|
+
* from {@link SessionNotAuthenticatedError} so callers don't misclassify a
|
|
39
|
+
* retryable wall as a stale session (issue #90).
|
|
40
|
+
*/
|
|
41
|
+
export declare class BotWallError extends McpToolError {
|
|
42
|
+
/** Suggested seconds to wait before retrying the blocked request(s). */
|
|
43
|
+
readonly retryAfterSeconds: number;
|
|
44
|
+
constructor(path: string, retryAfterSeconds?: number);
|
|
45
|
+
}
|
|
46
|
+
/** Upstream returned HTTP 429 (or an equivalent rate-limit signal). */
|
|
47
|
+
export declare class RateLimitError extends McpToolError {
|
|
48
|
+
/** Seconds the upstream asked us to wait, when it told us. */
|
|
49
|
+
readonly retryAfterSeconds?: number;
|
|
50
|
+
constructor(service: string, retryAfterSeconds?: number);
|
|
51
|
+
}
|
|
52
|
+
/** Upstream is unreachable (5xx / transport failure) — not the caller's fault. */
|
|
53
|
+
export declare class UnreachableError extends McpToolError {
|
|
54
|
+
/** Upstream HTTP status, when one was observed. */
|
|
55
|
+
readonly status?: number;
|
|
56
|
+
constructor(service: string, status?: number);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A tool requires a different auth/operation mode than the server is running in
|
|
60
|
+
* (e.g. a Pro key-mode-only report invoked while in session mode).
|
|
61
|
+
*/
|
|
62
|
+
export declare class ModeMismatchError extends McpToolError {
|
|
63
|
+
readonly currentMode: string;
|
|
64
|
+
readonly requiredMode: string;
|
|
65
|
+
readonly feature: string;
|
|
66
|
+
constructor(currentMode: string, requiredMode: string, feature: string);
|
|
67
|
+
}
|
|
68
|
+
/** Factory for an {@link McpToolError} with a remediation hint. */
|
|
69
|
+
export declare function createHelpfulError(message: string, opts?: {
|
|
70
|
+
hint?: string;
|
|
71
|
+
}): McpToolError;
|
|
72
|
+
/**
|
|
73
|
+
* Redact secrets, then cap an (upstream) error string at `max` characters,
|
|
74
|
+
* appending a `… [truncated]` marker when clipped.
|
|
75
|
+
*
|
|
76
|
+
* Security: redaction runs BEFORE truncation so a token straddling the cut
|
|
77
|
+
* boundary can't survive in a half-form. Untrusted upstream bodies must always
|
|
78
|
+
* go through this before reaching a tool result.
|
|
79
|
+
*/
|
|
80
|
+
export declare function truncateErrorMessage(text: string, max?: number): string;
|
|
81
|
+
/** Extract a string message from any thrown value. */
|
|
82
|
+
export declare function messageOf(err: unknown): string;
|
|
83
|
+
/**
|
|
84
|
+
* Prepend the tool name to an error's context and return an {@link McpToolError},
|
|
85
|
+
* preserving any `hint` and chaining the original via `cause`. The message is
|
|
86
|
+
* run through {@link truncateErrorMessage} (redaction + truncation). Re-wrapping
|
|
87
|
+
* an already-prefixed error does not double-prefix.
|
|
88
|
+
*/
|
|
89
|
+
export declare function wrapToolError(toolName: string, err: unknown): McpToolError;
|
|
90
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH,qFAAqF;AACrF,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAEjD,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C;;;;GAIG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,qDAAqD;IACrD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEX,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAOvE;AAED;;;GAGG;AACH,qBAAa,4BAA6B,SAAQ,YAAY;gBAChD,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAUlD;AAED;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,YAAY;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;gBAEvB,IAAI,EAAE,MAAM,EAAE,iBAAiB,GAAE,MAAuC;CASrF;AAED,uEAAuE;AACvE,qBAAa,cAAe,SAAQ,YAAY;IAC9C,8DAA8D;IAC9D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;gBAExB,OAAO,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM;CAOxD;AAED,kFAAkF;AAClF,qBAAa,gBAAiB,SAAQ,YAAY;IAChD,mDAAmD;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQ7C;AAED;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,YAAY;IAE/C,QAAQ,CAAC,WAAW,EAAE,MAAM;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM;gBAFf,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM;CAS3B;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,YAAY,CAE1F;AAcD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,MAAkC,GAAG,MAAM,CAKlG;AAED,sDAAsD;AACtD,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAG9C;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,YAAY,CAM1E"}
|