@roarkanalytics/sdk-mcp 4.12.0 → 4.12.2
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/CHANGELOG.md +14 -0
- package/code-tool.d.mts +18 -6
- package/code-tool.d.mts.map +1 -1
- package/code-tool.d.ts +18 -6
- package/code-tool.d.ts.map +1 -1
- package/code-tool.js +82 -13
- package/code-tool.js.map +1 -1
- package/code-tool.mjs +80 -12
- package/code-tool.mjs.map +1 -1
- package/http.d.mts.map +1 -1
- package/http.d.ts.map +1 -1
- package/http.js +14 -1
- package/http.js.map +1 -1
- package/http.mjs +15 -2
- package/http.mjs.map +1 -1
- package/oauth.d.mts +57 -1
- package/oauth.d.mts.map +1 -1
- package/oauth.d.ts +57 -1
- package/oauth.d.ts.map +1 -1
- package/oauth.js +133 -1
- package/oauth.js.map +1 -1
- package/oauth.mjs +128 -0
- package/oauth.mjs.map +1 -1
- package/package.json +1 -1
- package/server.js +1 -1
- package/server.mjs +1 -1
- package/src/code-tool.ts +94 -12
- package/src/http.ts +16 -1
- package/src/oauth.ts +171 -2
- package/src/server.ts +1 -1
package/src/code-tool.ts
CHANGED
|
@@ -205,14 +205,71 @@ const remoteStainlessHandler = async ({
|
|
|
205
205
|
const DENO_WORKER_SOCKET_SUFFIX = '-deno-http.sock';
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
|
-
*
|
|
208
|
+
* Whether this Deno wants the worker's Unix socket named in `--allow-net`.
|
|
209
209
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
210
|
+
* Deno changed its mind about this, and the two behaviors are mutually exclusive:
|
|
211
|
+
*
|
|
212
|
+
* Deno 2.7 and earlier a Unix socket is a file. Listening on it is covered by
|
|
213
|
+
* --allow-read/--allow-write on its path, and --allow-net
|
|
214
|
+
* parses every entry as `host[:port]`, so a `unix:` entry
|
|
215
|
+
* is a hard parse error: "invalid port in 'unix:/tmp/...'".
|
|
216
|
+
* Deno 2.9 and later listening additionally requires --allow-net=unix:<path>,
|
|
217
|
+
* and without it the worker dies with NotCapable.
|
|
218
|
+
*
|
|
219
|
+
* There is therefore no single flag string that works on both, and this package is
|
|
220
|
+
* published for users to run against whatever Deno they have. So ask the binary
|
|
221
|
+
* instead of guessing from a version string: run a no-op script with the flag and
|
|
222
|
+
* see whether it is accepted. The probe binds nothing (the script is `0`), and the
|
|
223
|
+
* result is cached per executable because the answer cannot change under us.
|
|
224
|
+
*/
|
|
225
|
+
const unixAllowNetSupport = new Map<string, boolean>();
|
|
226
|
+
|
|
227
|
+
export const denoAcceptsUnixAllowNet = (
|
|
228
|
+
denoPath: string,
|
|
229
|
+
probe: (command: string, args: string[]) => { status: number | null },
|
|
230
|
+
): boolean => {
|
|
231
|
+
const cached = unixAllowNetSupport.get(denoPath);
|
|
232
|
+
if (cached !== undefined) return cached;
|
|
233
|
+
|
|
234
|
+
// A path that is never created: --allow-net is parsed before the script runs, which
|
|
235
|
+
// is the only thing being tested here.
|
|
236
|
+
const { status } = probe(denoPath, [
|
|
237
|
+
'run',
|
|
238
|
+
'--allow-net=unix:/nonexistent-roark-deno-capability-probe.sock',
|
|
239
|
+
'data:text/javascript,0',
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
// A null status means the probe never ran at all - the binary could not be spawned,
|
|
243
|
+
// or it was killed by the timeout or a signal - as opposed to running and rejecting
|
|
244
|
+
// the flag, which is an exit code. Answer conservatively for this call but do NOT
|
|
245
|
+
// cache it: a single transient failure would otherwise pin the answer to
|
|
246
|
+
// "unsupported" for the life of the process, and on Deno 2.9+ that means every
|
|
247
|
+
// later execute call dies with NotCapable.
|
|
248
|
+
if (status === null) return false;
|
|
249
|
+
|
|
250
|
+
const accepted = status === 0;
|
|
251
|
+
unixAllowNetSupport.set(denoPath, accepted);
|
|
252
|
+
return accepted;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Grants the Deno worker its own Unix socket, and nothing else.
|
|
257
|
+
*
|
|
258
|
+
* deno-http-worker generates the socket path while it builds the spawn arguments, so
|
|
259
|
+
* this is the first point at which it can be read at all. Two things happen here:
|
|
260
|
+
*
|
|
261
|
+
* - The socket is added to `--allow-net` only on the Deno versions that require it
|
|
262
|
+
* (see denoAcceptsUnixAllowNet). Adding it unconditionally is what broke the
|
|
263
|
+
* published image, whose Deno rejected the syntax and exited before serving
|
|
264
|
+
* anything, surfacing only as the far vaguer "Deno exited before being ready".
|
|
265
|
+
* - The grant deno-http-worker makes itself, `--allow-read`/`--allow-write` on the
|
|
266
|
+
* socket, is asserted rather than assumed, so a future version that stops making
|
|
267
|
+
* it fails here instead of at runtime.
|
|
268
|
+
*
|
|
269
|
+
* Throws rather than falling back to an unrestricted `--allow-net`, so anything
|
|
270
|
+
* unexpected fails closed instead of silently widening what the sandbox can reach.
|
|
214
271
|
*/
|
|
215
|
-
export const scopeAllowNetToWorkerSocket = (spawnArgs: string[]): string[] => {
|
|
272
|
+
export const scopeAllowNetToWorkerSocket = (spawnArgs: string[], addSocketToAllowNet: boolean): string[] => {
|
|
216
273
|
// Skip flags: deno-http-worker also appends the socket to `--allow-read`/`--allow-write`,
|
|
217
274
|
// so match only the bare path it passes through to the bootstrap script.
|
|
218
275
|
const socketPath = spawnArgs.find((arg) => !arg.startsWith('-') && arg.endsWith(DENO_WORKER_SOCKET_SUFFIX));
|
|
@@ -221,6 +278,19 @@ export const scopeAllowNetToWorkerSocket = (spawnArgs: string[]): string[] => {
|
|
|
221
278
|
'Could not find the Deno worker socket path in its spawn arguments, so network access could not be scoped to it.',
|
|
222
279
|
);
|
|
223
280
|
}
|
|
281
|
+
|
|
282
|
+
const grants = (flag: string) =>
|
|
283
|
+
spawnArgs.some((arg) => arg.startsWith(flag) && arg.slice(flag.length).split(',').includes(socketPath));
|
|
284
|
+
|
|
285
|
+
const missing = ['--allow-read=', '--allow-write='].filter((flag) => !grants(flag));
|
|
286
|
+
if (missing.length > 0) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`deno-http-worker did not grant the worker socket ${socketPath} on ${missing.join(' or ')}, ` +
|
|
289
|
+
'so the Deno sandbox would not be able to serve requests.',
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!addSocketToAllowNet) return spawnArgs;
|
|
224
294
|
return spawnArgs.map((arg) => (arg.startsWith('--allow-net=') ? `${arg},unix:${socketPath}` : arg));
|
|
225
295
|
};
|
|
226
296
|
|
|
@@ -248,7 +318,7 @@ const localDenoHandler = async ({
|
|
|
248
318
|
const packageNodeModulesPath = path.resolve(packageRoot, 'node_modules');
|
|
249
319
|
|
|
250
320
|
// Check if deno is in PATH
|
|
251
|
-
const { execSync, spawn } = await import('node:child_process');
|
|
321
|
+
const { execSync, spawn, spawnSync } = await import('node:child_process');
|
|
252
322
|
try {
|
|
253
323
|
execSync('command -v deno', { stdio: 'ignore' });
|
|
254
324
|
denoPath = 'deno';
|
|
@@ -286,24 +356,36 @@ const localDenoHandler = async ({
|
|
|
286
356
|
|
|
287
357
|
const allowRead = allowReadPaths.join(',');
|
|
288
358
|
|
|
359
|
+
const addSocketToAllowNet = denoAcceptsUnixAllowNet(denoPath, (command, probeArgs) =>
|
|
360
|
+
spawnSync(command, probeArgs, {
|
|
361
|
+
stdio: 'ignore',
|
|
362
|
+
// spawnSync blocks, so an unbounded probe would hang the server rather than
|
|
363
|
+
// fail one call. Deno's first run can populate its cache directory, hence
|
|
364
|
+
// seconds rather than milliseconds, and DENO_NO_UPDATE_CHECK keeps a version
|
|
365
|
+
// check off that path.
|
|
366
|
+
timeout: 10_000,
|
|
367
|
+
env: { ...process.env, DENO_NO_UPDATE_CHECK: '1' },
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
370
|
+
|
|
289
371
|
const worker = await newDenoHTTPWorker(url.pathToFileURL(workerPath), {
|
|
290
372
|
denoExecutable: denoPath,
|
|
291
373
|
runFlags: [
|
|
292
374
|
`--node-modules-dir=manual`,
|
|
293
375
|
`--allow-read=${allowRead}`,
|
|
294
376
|
// Only the Roark API is reachable from the sandbox. The worker's Unix socket
|
|
295
|
-
// is added to this same allowlist by scopeAllowNetToWorkerSocket below
|
|
377
|
+
// is added to this same allowlist by scopeAllowNetToWorkerSocket below, on the
|
|
378
|
+
// Deno versions that require it.
|
|
296
379
|
`--allow-net=${baseURLHostname}`,
|
|
297
380
|
// Allow environment variables because instantiating the client will try to read from them,
|
|
298
381
|
// even though they are not set.
|
|
299
382
|
'--allow-env',
|
|
300
383
|
],
|
|
301
384
|
// deno-http-worker picks the worker's Unix socket path while it builds the spawn
|
|
302
|
-
// arguments, so it is not known when runFlags are constructed above.
|
|
303
|
-
//
|
|
304
|
-
// that one socket rather than opening up network access to every host.
|
|
385
|
+
// arguments, so it is not known when runFlags are constructed above. Intercept the
|
|
386
|
+
// spawn to grant that one socket rather than opening up network access to every host.
|
|
305
387
|
spawnFunc: (command, spawnArgs, spawnOptions) =>
|
|
306
|
-
spawn(command, scopeAllowNetToWorkerSocket(spawnArgs), spawnOptions),
|
|
388
|
+
spawn(command, scopeAllowNetToWorkerSocket(spawnArgs, addSocketToAllowNet), spawnOptions),
|
|
307
389
|
printOutput: true,
|
|
308
390
|
spawnOptions: {
|
|
309
391
|
cwd: path.dirname(workerPath),
|
package/src/http.ts
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
ORIGIN_CONNECTOR,
|
|
16
16
|
protectedResourceMetadata,
|
|
17
17
|
requireBearer,
|
|
18
|
+
requireLiveBearer,
|
|
19
|
+
resolveApiBaseUrl,
|
|
18
20
|
UnauthorizedError,
|
|
19
21
|
} from './oauth';
|
|
20
22
|
import { initMcpServer, newMcpServer } from './server';
|
|
@@ -52,7 +54,20 @@ const newServer = async ({
|
|
|
52
54
|
// not be able to make us do that.
|
|
53
55
|
let authOptions: Partial<ClientOptions>;
|
|
54
56
|
if (mcpOptions.oauth) {
|
|
55
|
-
|
|
57
|
+
const connector = connectorFrom(req);
|
|
58
|
+
const authorized = requireBearer(req, mcpOptions.oauth, connector);
|
|
59
|
+
// Presence is not validity. Ask customer-api whether the token is still live, so a revoked or
|
|
60
|
+
// expired one is answered with 401 + WWW-Authenticate here - the signal a client needs to
|
|
61
|
+
// re-run the OAuth flow - rather than completing the handshake and failing later inside a
|
|
62
|
+
// tool result, where nothing is listening for it.
|
|
63
|
+
//
|
|
64
|
+
// The base URL is resolved the way the SDK resolves it, NOT defaulted to prod: this server is
|
|
65
|
+
// launched without `clientOptions`, so on any stage but prod a prod default would check the
|
|
66
|
+
// token against a server that never issued it and refuse every request.
|
|
67
|
+
await requireLiveBearer(authorized.bearerToken, mcpOptions.oauth, connector, {
|
|
68
|
+
baseURL: resolveApiBaseUrl(clientOptions),
|
|
69
|
+
});
|
|
70
|
+
authOptions = authorized;
|
|
56
71
|
} else {
|
|
57
72
|
authOptions = parseClientAuthHeaders(req, false);
|
|
58
73
|
}
|
package/src/oauth.ts
CHANGED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
// rotated by the client's refresh token), so it is forwarded to customer-api as
|
|
10
10
|
// the SDK bearer unchanged: customer-api validates it on every call.
|
|
11
11
|
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
12
13
|
import { IncomingMessage } from 'node:http';
|
|
13
14
|
import { ClientOptions } from '@roarkanalytics/sdk';
|
|
15
|
+
import { readEnv } from './util';
|
|
14
16
|
|
|
15
17
|
export type OAuthConfig = {
|
|
16
18
|
/** Authorization server issuer, e.g. https://mcp-oauth.api.roark.ai */
|
|
@@ -121,7 +123,7 @@ export const requireBearer = (
|
|
|
121
123
|
req: IncomingMessage,
|
|
122
124
|
config: OAuthConfig,
|
|
123
125
|
connector: Connector = ORIGIN_CONNECTOR,
|
|
124
|
-
): Partial<ClientOptions> => {
|
|
126
|
+
): Partial<ClientOptions> & { bearerToken: string } => {
|
|
125
127
|
const token = extractBearer(req);
|
|
126
128
|
if (!token) {
|
|
127
129
|
throw new UnauthorizedError('Missing bearer token', bearerChallenge(config, connector));
|
|
@@ -129,9 +131,176 @@ export const requireBearer = (
|
|
|
129
131
|
// Assigned rather than conditionally spread: a spread of `cond && { project }` is not checked
|
|
130
132
|
// against `ClientOptions`, so on an SDK without the option the pin would compile and then
|
|
131
133
|
// silently vanish. This way the dependency is enforced by the build.
|
|
132
|
-
const options: Partial<ClientOptions> = { bearerToken: token };
|
|
134
|
+
const options: Partial<ClientOptions> & { bearerToken: string } = { bearerToken: token };
|
|
133
135
|
if (connector.kind === 'project') {
|
|
134
136
|
options.project = connector.projectId;
|
|
135
137
|
}
|
|
136
138
|
return options;
|
|
137
139
|
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* What customer-api says about a bearer: `unknown` means it could not be asked, not that the
|
|
143
|
+
* token is fine.
|
|
144
|
+
*/
|
|
145
|
+
export type BearerVerdict = 'valid' | 'invalid' | 'unknown';
|
|
146
|
+
|
|
147
|
+
/** The SDK's own default, for when neither an option nor the environment names one. */
|
|
148
|
+
export const DEFAULT_API_BASE_URL = 'https://api.roark.ai';
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The base URL the SDK will actually call, resolved exactly the way the SDK resolves it:
|
|
152
|
+
* explicit option, then `ROARK_BASE_URL`, then the default.
|
|
153
|
+
*
|
|
154
|
+
* This has to agree with the SDK or the liveness check asks the wrong host. `launchStreamableHTTPServer`
|
|
155
|
+
* passes no `clientOptions` at all, so for the deployed server the answer always comes from the
|
|
156
|
+
* environment: a hardcoded prod default would send every check to prod while data calls went to
|
|
157
|
+
* whatever `ROARK_BASE_URL` names. On any stage but prod that turns a valid token into a 401 from
|
|
158
|
+
* a server it was never issued for, and this file would then refuse every request - a dead
|
|
159
|
+
* connector, which is strictly worse than the bug it is here to fix.
|
|
160
|
+
*/
|
|
161
|
+
export const resolveApiBaseUrl = (clientOptions: Pick<ClientOptions, 'baseURL'> = {}): string =>
|
|
162
|
+
clientOptions.baseURL ?? readEnv('ROARK_BASE_URL') ?? DEFAULT_API_BASE_URL;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* How long a verdict is reused. `newServer` runs per POST, not per session, so an uncached check
|
|
166
|
+
* would add a round trip to every single MCP request. The window is what a revoked token buys:
|
|
167
|
+
* it can still complete a handshake and read the tool list for up to this long. It cannot read
|
|
168
|
+
* anything, because every data path carries the same token to customer-api and is refused there
|
|
169
|
+
* immediately.
|
|
170
|
+
*/
|
|
171
|
+
const BEARER_CACHE_TTL_MS = 60_000;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* An unreachable customer-api is handled by the catch below, but a customer-api that accepts the
|
|
175
|
+
* connection and then never answers is not: without a deadline every MCP request would hang on
|
|
176
|
+
* it, which is worse than the failure this check exists to report. A hang becomes `unknown` like
|
|
177
|
+
* any other transport failure.
|
|
178
|
+
*/
|
|
179
|
+
const BEARER_CHECK_TIMEOUT_MS = 3_000;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Bounded so that spraying invented tokens cannot grow the process's memory. Keys are hashes,
|
|
183
|
+
* not the tokens themselves: these are live credentials and there is no reason to keep a second
|
|
184
|
+
* copy of them in a long-lived map.
|
|
185
|
+
*/
|
|
186
|
+
const BEARER_CACHE_MAX = 1_000;
|
|
187
|
+
|
|
188
|
+
const bearerVerdicts = new Map<string, { verdict: 'valid' | 'invalid'; expiresAt: number }>();
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Checks already in flight, so concurrent requests bearing the same uncached token make one call
|
|
192
|
+
* rather than one each. That is the common shape right after a verdict expires, and whenever a
|
|
193
|
+
* client opens several requests at once.
|
|
194
|
+
*/
|
|
195
|
+
const inFlightChecks = new Map<string, Promise<BearerVerdict>>();
|
|
196
|
+
|
|
197
|
+
/** Test seam. */
|
|
198
|
+
export const clearBearerVerdictCache = (): void => {
|
|
199
|
+
bearerVerdicts.clear();
|
|
200
|
+
inFlightChecks.clear();
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const hashToken = (token: string): string => createHash('sha256').update(token).digest('hex');
|
|
204
|
+
|
|
205
|
+
const rememberVerdict = (key: string, verdict: 'valid' | 'invalid', expiresAt: number): void => {
|
|
206
|
+
// Evict the oldest entry rather than clearing the map. A `clear()` would let anyone spraying
|
|
207
|
+
// invented tokens flush every valid verdict too, making legitimate callers pay a round trip
|
|
208
|
+
// each and handing customer-api the extra work. Map iterates in insertion order, so the first
|
|
209
|
+
// key is the oldest.
|
|
210
|
+
if (bearerVerdicts.size >= BEARER_CACHE_MAX) {
|
|
211
|
+
const oldest = bearerVerdicts.keys().next();
|
|
212
|
+
if (!oldest.done) bearerVerdicts.delete(oldest.value);
|
|
213
|
+
}
|
|
214
|
+
bearerVerdicts.set(key, { verdict, expiresAt });
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export interface VerifyBearerDeps {
|
|
218
|
+
/** customer-api base URL. Use {@link resolveApiBaseUrl} rather than passing a literal. */
|
|
219
|
+
baseURL: string;
|
|
220
|
+
fetchImpl?: typeof fetch;
|
|
221
|
+
now?: () => number;
|
|
222
|
+
ttlMs?: number;
|
|
223
|
+
timeoutMs?: number;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Asks customer-api whether a bearer is still live, via the `/v1/me` endpoint it already exposes
|
|
228
|
+
* for exactly this ("who is this credential"), and caches the answer briefly.
|
|
229
|
+
*
|
|
230
|
+
* Only a 401 counts as invalid. A 403 means the token authenticated and then failed a permission
|
|
231
|
+
* check, which is a live token; treating it as invalid would tell a client to re-authorize when
|
|
232
|
+
* re-authorizing cannot help. Anything else - a 5xx, a timeout, a DNS failure - is `unknown`.
|
|
233
|
+
*/
|
|
234
|
+
export const verifyBearer = async (token: string, deps: VerifyBearerDeps): Promise<BearerVerdict> => {
|
|
235
|
+
const {
|
|
236
|
+
baseURL,
|
|
237
|
+
fetchImpl = fetch,
|
|
238
|
+
now = Date.now,
|
|
239
|
+
ttlMs = BEARER_CACHE_TTL_MS,
|
|
240
|
+
timeoutMs = BEARER_CHECK_TIMEOUT_MS,
|
|
241
|
+
} = deps;
|
|
242
|
+
const key = hashToken(token);
|
|
243
|
+
|
|
244
|
+
const cached = bearerVerdicts.get(key);
|
|
245
|
+
if (cached && cached.expiresAt > now()) return cached.verdict;
|
|
246
|
+
|
|
247
|
+
const alreadyRunning = inFlightChecks.get(key);
|
|
248
|
+
if (alreadyRunning) return alreadyRunning;
|
|
249
|
+
|
|
250
|
+
const check = (async (): Promise<BearerVerdict> => {
|
|
251
|
+
let response: Response;
|
|
252
|
+
try {
|
|
253
|
+
response = await fetchImpl(`${stripSlash(baseURL)}/v1/me`, {
|
|
254
|
+
method: 'GET',
|
|
255
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
256
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
257
|
+
});
|
|
258
|
+
} catch {
|
|
259
|
+
return 'unknown';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const verdict: BearerVerdict =
|
|
263
|
+
response.status === 401 ? 'invalid'
|
|
264
|
+
: response.ok ? 'valid'
|
|
265
|
+
: 'unknown';
|
|
266
|
+
// An `unknown` is never cached: a blip would otherwise pin the answer for the whole TTL.
|
|
267
|
+
if (verdict !== 'unknown') rememberVerdict(key, verdict, now() + ttlMs);
|
|
268
|
+
return verdict;
|
|
269
|
+
})();
|
|
270
|
+
|
|
271
|
+
inFlightChecks.set(key, check);
|
|
272
|
+
try {
|
|
273
|
+
return await check;
|
|
274
|
+
} finally {
|
|
275
|
+
inFlightChecks.delete(key);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Rejects a bearer customer-api has already disowned, with the 401 + WWW-Authenticate that tells
|
|
281
|
+
* an MCP client to re-run the OAuth flow.
|
|
282
|
+
*
|
|
283
|
+
* Without this the resource server only checked that a token was PRESENT. A revoked, expired or
|
|
284
|
+
* entirely invented token completed `initialize` and `tools/list`, and the caller learned nothing
|
|
285
|
+
* until a tool call failed with an opaque 401 in its result text. No data was reachable - every
|
|
286
|
+
* data path revalidates at customer-api - but a client that never sees a 401 from the resource
|
|
287
|
+
* server has no reason to refresh, which for a 24h token means a connector that looks connected
|
|
288
|
+
* and silently cannot work.
|
|
289
|
+
*
|
|
290
|
+
* `unknown` is allowed through deliberately. Failing closed would turn a customer-api blip into a
|
|
291
|
+
* dead connector for everyone, and it would buy no confidentiality: the token still has to pass
|
|
292
|
+
* customer-api on the very next call that touches data.
|
|
293
|
+
*/
|
|
294
|
+
export const requireLiveBearer = async (
|
|
295
|
+
token: string,
|
|
296
|
+
config: OAuthConfig,
|
|
297
|
+
connector: Connector,
|
|
298
|
+
deps: VerifyBearerDeps,
|
|
299
|
+
): Promise<void> => {
|
|
300
|
+
const verdict = await verifyBearer(token, deps);
|
|
301
|
+
if (verdict !== 'invalid') return;
|
|
302
|
+
throw new UnauthorizedError(
|
|
303
|
+
'The access token is expired or has been revoked',
|
|
304
|
+
bearerChallenge(config, connector, 'invalid_token', 'The access token is expired or has been revoked'),
|
|
305
|
+
);
|
|
306
|
+
};
|
package/src/server.ts
CHANGED
|
@@ -28,7 +28,7 @@ export const newMcpServer = async ({
|
|
|
28
28
|
new McpServer(
|
|
29
29
|
{
|
|
30
30
|
name: 'roarkanalytics_sdk_api',
|
|
31
|
-
version: '4.12.
|
|
31
|
+
version: '4.12.2', // x-release-please-version
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
instructions: await getInstructions({ stainlessApiKey, customInstructionsPath }),
|