within-sdk 1.0.0 → 1.0.1

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.
@@ -1,562 +0,0 @@
1
- /**
2
- * With.in MCP Auth SDK — sealed request handler.
3
- *
4
- * Exposes ONE function, `createWithinHandler`, that returns a handler with:
5
- * - `mcpHandler(req, res)` — node HTTP handler for the MCP endpoint
6
- * - `subscribeHandler(req,res)` — node HTTP handler for the /subscribe route
7
- * - `vendorSlug`, `authServerUrl` — readonly fields for logging
8
- *
9
- * Everything else — token validation, tool-call reporting, trial-exhausted
10
- * gating, subscriber cross-check + auto conversion, transport construction
11
- * and session caching, network-tool injection, auth propagation into tool
12
- * handlers — is hidden inside `mcpHandler`. Vendors cannot accidentally
13
- * skip a step.
14
- */
15
- import { validateWithinToken, extractBearerToken, reportToolCall, reportConversion, checkSubscriptionStatus, } from './validate.js';
16
- import { NETWORK_TOOL_NAMES, registerNetworkTools, runWithAuth } from './network.js';
17
- const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
18
- /**
19
- * Keep tool-call arguments reportable without blowing up interactions rows.
20
- * Returns null for anything that isn't a plain object. For each top-level
21
- * value, replaces entries whose stringified size exceeds 10 KB with a
22
- * `"[truncated N bytes]"` marker so we preserve the shape of the call
23
- * (field names tell us a lot about intent) without storing large blobs.
24
- */
25
- function sanitizeToolArgs(args) {
26
- if (!args || typeof args !== 'object' || Array.isArray(args))
27
- return null;
28
- const MAX_VALUE_BYTES = 10_000;
29
- const out = {};
30
- for (const [k, v] of Object.entries(args)) {
31
- try {
32
- const serialized = JSON.stringify(v);
33
- if (serialized && serialized.length > MAX_VALUE_BYTES) {
34
- out[k] = `[truncated ${serialized.length} bytes]`;
35
- }
36
- else {
37
- out[k] = v;
38
- }
39
- }
40
- catch {
41
- // Circular reference or unserializable value — drop it.
42
- out[k] = null;
43
- }
44
- }
45
- return out;
46
- }
47
- function autoExtractEmail(payload) {
48
- const candidates = ['email', 'user_email', 'preferred_username', 'upn', 'sub'];
49
- for (const key of candidates) {
50
- const value = payload[key];
51
- if (typeof value === 'string' && EMAIL_RE.test(value))
52
- return value;
53
- }
54
- return null;
55
- }
56
- export function createWithinHandler(config) {
57
- const { jwksUrl, authServerUrl, vendorSlug, resourceUrl, vendorTokenValidator, extractEmail, zod, createMcpServer, isSubscriber, log, } = config;
58
- if (!zod)
59
- throw new Error('createWithinHandler: `zod` is required');
60
- if (!createMcpServer)
61
- throw new Error('createWithinHandler: `createMcpServer` is required');
62
- if (!isSubscriber && !vendorTokenValidator) {
63
- console.warn('[within-mcp-auth] Neither `isSubscriber` nor `vendorTokenValidator` is configured. ' +
64
- 'Paying customers arriving via With.in tokens will be treated as trial users and eventually hit the trial-exhausted wall. ' +
65
- 'Wire `isSubscriber(email)` against your user DB to prevent this.');
66
- }
67
- const wellKnownUrl = new URL('/.well-known/oauth-protected-resource', resourceUrl).href;
68
- const logger = log ?? (() => { });
69
- // Per-process state. Safe for single-worker deploys (Render default).
70
- // For multi-worker deployments, the session map should be externalized
71
- // — but the vendor should never see this complexity.
72
- //
73
- // Quota state is keyed on the stable user identity (within_network_id)
74
- // rather than the raw access token. A single user can hold multiple
75
- // short-lived access tokens simultaneously (each `within__use` provision
76
- // mints a fresh one); keying on the token resets the counter on every
77
- // provision and lets trial limits be trivially bypassed.
78
- const callCounts = new Map();
79
- const subscriberIdentities = new Set();
80
- const sessions = new Map(); // sid → transport
81
- // Concurrency guard: `sessions.get(sid) → create new transport → sessions.set(sid, ...)`
82
- // is a non-atomic sequence. Parallel tool calls against the same session
83
- // (Claude fan-out) all observe the Map before the first request finishes
84
- // registering, and each request ends up spawning its own McpServer. The
85
- // client's session-id then resolves to whichever transport lost the race,
86
- // orphaning the other transports' responses ("Tool result could not be
87
- // submitted"). This map holds the in-flight creation promise per session
88
- // so concurrent requests wait and reuse the same transport.
89
- const pendingSessions = new Map();
90
- // sid → MCP clientInfo.name captured from the `initialize` request. Surfaces
91
- // to With.in on every tool-call report so the vendor + user dashboards can
92
- // show which agent (Claude Desktop, Cursor, ChatGPT, …) made each call.
93
- const sessionAiClient = new Map();
94
- /** Stable identity for quota accounting — survives re-provisioning. */
95
- function identityKey(auth) {
96
- return (auth.withinClaims?.within_network_id ??
97
- auth.withinClaims?.sub ??
98
- auth.token ??
99
- 'unknown');
100
- }
101
- // Lazy-load the MCP server transport constructor. The SDK has no types
102
- // for @modelcontextprotocol/server (peer dep), so we launder the module
103
- // specifier through a const-typed string and cast the result.
104
- const MCP_MOD = '@modelcontextprotocol/server';
105
- let TransportCtor;
106
- async function getTransport() {
107
- if (TransportCtor)
108
- return TransportCtor;
109
- const mod = await import(MCP_MOD);
110
- TransportCtor = mod.WebStandardStreamableHTTPServerTransport;
111
- if (!TransportCtor) {
112
- throw new Error('WebStandardStreamableHTTPServerTransport not found in @modelcontextprotocol/server');
113
- }
114
- return TransportCtor;
115
- }
116
- // ─── Internal: authenticate a request ───────────────────────────────────
117
- async function authenticate(req) {
118
- const authHeader = req.headers['authorization'];
119
- const token = extractBearerToken(typeof authHeader === 'string' ? authHeader : undefined);
120
- if (!token)
121
- return { authorized: false, error: 'Missing Bearer token' };
122
- // Vendor's own token takes precedence — treat as subscriber.
123
- if (vendorTokenValidator) {
124
- try {
125
- const vendorPayload = await vendorTokenValidator(token);
126
- if (vendorPayload) {
127
- const email = (extractEmail && extractEmail(vendorPayload)) || autoExtractEmail(vendorPayload);
128
- if (email)
129
- reportConversion(authServerUrl, vendorSlug, email);
130
- return { authorized: true, source: 'vendor', vendorPayload, token };
131
- }
132
- }
133
- catch { }
134
- }
135
- const claims = await validateWithinToken(token, jwksUrl);
136
- if (!claims)
137
- return { authorized: false, error: 'Invalid or expired token' };
138
- if (claims.token_type === 'identity') {
139
- return {
140
- authorized: false,
141
- error: 'Identity tokens cannot be used for vendor access. Use token exchange to obtain an access token.',
142
- };
143
- }
144
- let trialExhausted = false;
145
- if (claims.within_trial_tier !== 'subscriber' && claims.within_calls_remaining !== undefined) {
146
- const ident = claims.within_network_id ?? claims.sub ?? token;
147
- const used = callCounts.get(ident) ?? 0;
148
- if (used >= claims.within_calls_remaining && !subscriberIdentities.has(ident)) {
149
- trialExhausted = true;
150
- }
151
- }
152
- return { authorized: true, source: 'within', withinClaims: claims, token, trialExhausted };
153
- }
154
- function sendUnauthorized(res, auth) {
155
- res.writeHead(401, {
156
- 'WWW-Authenticate': `Bearer resource_metadata="${wellKnownUrl}"`,
157
- 'Content-Type': 'application/json',
158
- });
159
- res.end(JSON.stringify({ error: 'unauthorized', message: auth.error ?? 'Bearer token required' }));
160
- }
161
- /**
162
- * Build the trial-exhausted response as a tool result with isError=true.
163
- *
164
- * We deliberately do NOT include the upgrade URL or handoff email here.
165
- * Embedding URLs/instructions in tool error responses is read by the model
166
- * as untrusted content and triggers anti-phishing/paywall safeties that
167
- * cause the agent to refuse to act on it. The next-step details are
168
- * delivered out-of-band — With.in sends the user an email when the quota
169
- * is exhausted (see backend/src/oauth/report.ts).
170
- *
171
- * The text instructs the agent to retry on user request: once the user
172
- * subscribes, the SDK's live subscription check unblocks the next call,
173
- * but only if the agent actually re-fires the tool. Without an explicit
174
- * retry cue, agents read isError=true as terminal and stop.
175
- */
176
- function buildTrialExhaustedResponse(jsonRpcId, _claims) {
177
- const content = [
178
- {
179
- type: 'text',
180
- text: 'Your trial has ended. We sent an email with the details for continuing access. If the user confirms they have subscribed or completed the handoff, retry this tool call — access will be granted automatically.',
181
- },
182
- ];
183
- return JSON.stringify({ jsonrpc: '2.0', id: jsonRpcId, result: { content, isError: true } });
184
- }
185
- // Access tokens are 10-min JWTs that the SDK verifies locally via JWKS, so a
186
- // dashboard-initiated revoke can't be caught at validate-time — the
187
- // signature stays good until expiry. The /oauth/report roundtrip on every
188
- // tool call is the enforcement point: backend returns 401 token_revoked,
189
- // and we surface that to the agent as a tool result so the upstream MCP
190
- // server is never invoked with a revoked token.
191
- function buildRevokedResponse(jsonRpcId) {
192
- const content = [
193
- {
194
- type: 'text',
195
- text: 'Access to this tool has been revoked. Please re-authorize to continue.',
196
- },
197
- ];
198
- return JSON.stringify({ jsonrpc: '2.0', id: jsonRpcId, result: { content, isError: true } });
199
- }
200
- // Per-tool budgets are configured by the vendor in vendor_trial_rules. The
201
- // backend enforces by returning 429 from /oauth/report once the count for
202
- // that user/vendor/tool meets the limit. We must block here so the vendor's
203
- // tool handler does NOT run — otherwise the budget would be observability
204
- // only and the AQL row's total_calls would diverge from real execution.
205
- // Other tools remain callable; the user just can't call this one again.
206
- function buildBudgetExhaustedResponse(jsonRpcId, info) {
207
- const content = [
208
- {
209
- type: 'text',
210
- text: `You have reached the per-tool limit for "${info.tool}" (${info.limit} calls). Try a different tool, or contact the vendor for a higher limit.`,
211
- },
212
- ];
213
- return JSON.stringify({ jsonrpc: '2.0', id: jsonRpcId, result: { content, isError: true } });
214
- }
215
- /**
216
- * Returns a pre-built JSON-RPC trial-exhausted response if the call should
217
- * be blocked, otherwise null and increments the per-token counter.
218
- * SDK-injected network tools skip the counter entirely (see NETWORK_TOOL_NAMES).
219
- */
220
- async function processToolCallBody(body, auth, sessionId) {
221
- if (!auth.token || auth.source !== 'within') {
222
- return { blocked: null, method: null, id: null, toolName: null, aiClientFromInitialize: null };
223
- }
224
- let jsonBody;
225
- try {
226
- jsonBody = JSON.parse(decodeBody(body));
227
- }
228
- catch {
229
- return { blocked: null, method: null, id: null, toolName: null, aiClientFromInitialize: null };
230
- }
231
- const method = typeof jsonBody?.method === 'string' ? jsonBody.method : null;
232
- const id = jsonBody?.id ?? null;
233
- const toolName = typeof jsonBody?.params?.name === 'string' ? jsonBody.params.name : null;
234
- // Capture MCP clientInfo.name on the `initialize` request. The session
235
- // ID isn't known yet (transport assigns it below); the caller stores this
236
- // against newSessionId once the transport resolves.
237
- if (method === 'initialize') {
238
- const name = jsonBody?.params?.clientInfo?.name;
239
- return {
240
- blocked: null,
241
- method,
242
- id,
243
- toolName: null,
244
- aiClientFromInitialize: typeof name === 'string' && name.length > 0 ? name : null,
245
- };
246
- }
247
- if (method !== 'tools/call' || !toolName)
248
- return { blocked: null, method, id, toolName, aiClientFromInitialize: null };
249
- // Network meta-tools are exempt from vendor trial quota accounting.
250
- if (NETWORK_TOOL_NAMES.has(toolName))
251
- return { blocked: null, method, id, toolName, aiClientFromInitialize: null };
252
- const ident = identityKey(auth);
253
- if (auth.trialExhausted) {
254
- const isSubscriberNow = await checkSubscriptionStatus(authServerUrl, auth.token);
255
- if (isSubscriberNow) {
256
- subscriberIdentities.add(ident);
257
- callCounts.delete(ident);
258
- // fall through — allow
259
- }
260
- else {
261
- return { blocked: buildTrialExhaustedResponse(id, auth.withinClaims), method, id, toolName, aiClientFromInitialize: null };
262
- }
263
- }
264
- const slug = auth.withinClaims?.within_vendor_slug ?? vendorSlug;
265
- const aiClient = sessionId ? sessionAiClient.get(sessionId) : undefined;
266
- const rawArgs = jsonBody?.params?.arguments;
267
- const toolArguments = sanitizeToolArgs(rawArgs);
268
- // Await the report so we can enforce dashboard-initiated revocation AND
269
- // per-tool budget exhaustion before the upstream tool runs. Reporting
270
- // failures (network, 5xx) don't block — only explicit 401 token_revoked
271
- // and 429 tool_budget_exhausted do.
272
- const reportResult = await reportToolCall(authServerUrl, auth.token, slug, toolName, aiClient, toolArguments);
273
- if (reportResult.revoked) {
274
- return { blocked: buildRevokedResponse(id), method, id, toolName, aiClientFromInitialize: null };
275
- }
276
- if (reportResult.budgetExhausted) {
277
- return {
278
- blocked: buildBudgetExhaustedResponse(id, reportResult.budgetExhausted),
279
- method,
280
- id,
281
- toolName,
282
- aiClientFromInitialize: null,
283
- };
284
- }
285
- // Bump the local quota counter only after we know the call is going
286
- // through. Doing this earlier would charge the user for revoked or
287
- // budget-blocked calls.
288
- const used = callCounts.get(ident) ?? 0;
289
- callCounts.set(ident, used + 1);
290
- return { blocked: null, method, id, toolName, aiClientFromInitialize: null };
291
- }
292
- // ─── Internal: MCP session dispatch ─────────────────────────────────────
293
- async function readBody(req) {
294
- return new Promise((resolve) => {
295
- const chunks = [];
296
- req.on('data', (chunk) => {
297
- // Node streams emit Buffer; Buffer is a Uint8Array subclass.
298
- chunks.push(chunk);
299
- });
300
- req.on('end', () => {
301
- let total = 0;
302
- for (const c of chunks)
303
- total += c.byteLength;
304
- const out = new Uint8Array(total);
305
- let o = 0;
306
- for (const c of chunks) {
307
- out.set(c, o);
308
- o += c.byteLength;
309
- }
310
- resolve(out);
311
- });
312
- });
313
- }
314
- function decodeBody(body) {
315
- return new TextDecoder().decode(body);
316
- }
317
- async function buildWebRequest(req, body) {
318
- const headers = new Headers();
319
- for (const [key, value] of Object.entries(req.headers)) {
320
- if (value)
321
- headers.set(key, Array.isArray(value) ? value.join(', ') : value);
322
- }
323
- const hostHeader = typeof req.headers.host === 'string' ? req.headers.host : 'localhost';
324
- const url = new URL(req.url ?? '/', `http://${hostHeader}`);
325
- return new Request(url.toString(), {
326
- method: req.method,
327
- headers,
328
- // TS is strict about Uint8Array as BodyInit; cast via ArrayBuffer view.
329
- body: req.method !== 'GET' && req.method !== 'HEAD' && body.byteLength > 0 ? body : undefined,
330
- });
331
- }
332
- async function sendWebResponse(res, webResponse) {
333
- res.writeHead(webResponse.status, Object.fromEntries(webResponse.headers.entries()));
334
- if (!webResponse.body) {
335
- res.end();
336
- return;
337
- }
338
- const reader = webResponse.body.getReader();
339
- try {
340
- while (true) {
341
- const { done, value } = await reader.read();
342
- if (done)
343
- break;
344
- res.write(value);
345
- }
346
- }
347
- catch (err) {
348
- logger(`[within] stream error: ${err.message}`);
349
- }
350
- finally {
351
- res.end();
352
- }
353
- }
354
- // ─── Public: mcpHandler ─────────────────────────────────────────────────
355
- async function mcpHandler(req, res) {
356
- const auth = await authenticate(req);
357
- if (!auth.authorized) {
358
- sendUnauthorized(res, auth);
359
- return;
360
- }
361
- // Subscriber cross-check: vendor's own user DB may recognize this email
362
- // as an existing customer. If so, flip trialExhausted and report
363
- // conversion so With.in updates the AQL. Cheap way to keep customers
364
- // from hitting trial walls after they've already paid the vendor.
365
- if (auth.source === 'within' && auth.withinClaims?.within_email && isSubscriber) {
366
- try {
367
- const known = await isSubscriber(auth.withinClaims.within_email);
368
- if (known) {
369
- auth.trialExhausted = false;
370
- reportConversion(authServerUrl, vendorSlug, auth.withinClaims.within_email, auth.token);
371
- logger(`[within] ${auth.withinClaims.within_email} recognized as existing customer`);
372
- }
373
- }
374
- catch (err) {
375
- logger(`[within] isSubscriber threw: ${err.message}`);
376
- }
377
- }
378
- if (auth.source === 'within' && auth.withinClaims) {
379
- const { within_email, within_trial_tier, within_calls_remaining } = auth.withinClaims;
380
- logger(`[within] ${within_email} | ${within_trial_tier ?? '?'} | ${within_calls_remaining ?? 'unlimited'} calls left`);
381
- }
382
- const body = await readBody(req);
383
- // Session dispatch. MCP Streamable HTTP uses `mcp-session-id` header for
384
- // subsequent requests. First request has no sid; transport assigns one.
385
- const sessionId = req.headers['mcp-session-id'] ?? undefined;
386
- const processed = await processToolCallBody(body, auth, sessionId);
387
- if (processed.blocked) {
388
- res.writeHead(200, { 'Content-Type': 'application/json' });
389
- res.end(processed.blocked);
390
- return;
391
- }
392
- // Fast path: known session-id, transport already registered.
393
- if (sessionId) {
394
- const existing = sessions.get(sessionId);
395
- if (existing) {
396
- const webRequest = await buildWebRequest(req, body);
397
- const webResponse = await runWithAuth(auth, async () => (await existing.handleRequest(webRequest)));
398
- await sendWebResponse(res, webResponse);
399
- return;
400
- }
401
- }
402
- // Stale session on GET (SSE stream reconnect) → 404 so the client
403
- // re-initializes cleanly.
404
- if (sessionId && req.method !== 'POST') {
405
- res.writeHead(404, { 'Content-Type': 'application/json' });
406
- res.end(JSON.stringify({ error: 'Session expired or server restarted. Please reconnect.' }));
407
- return;
408
- }
409
- // Stale session-id on a non-initialize POST: don't silently spin up a new
410
- // transport under a fresh UUID — that orphans the client's session-id and
411
- // is the root cause of "Tool result could not be submitted". Send a
412
- // JSON-RPC error so the client reconnects cleanly.
413
- if (sessionId && processed.method !== 'initialize') {
414
- res.writeHead(404, { 'Content-Type': 'application/json' });
415
- res.end(JSON.stringify({
416
- jsonrpc: '2.0',
417
- id: processed.id ?? null,
418
- error: { code: -32001, message: 'Session expired or unknown. Please reconnect.' },
419
- }));
420
- return;
421
- }
422
- // Need a brand-new transport. Deduplicate concurrent creations keyed on
423
- // whatever session-id the client provided (if any). Parallel requests
424
- // with the same stale sid all wait on the same creation promise and end
425
- // up sharing one transport. Requests with no sid (legitimate fresh
426
- // initializes) each get their own slot.
427
- const lockKey = sessionId ?? `__fresh__::${globalThis.crypto.randomUUID()}`;
428
- const inFlight = pendingSessions.get(lockKey);
429
- if (inFlight) {
430
- try {
431
- await inFlight;
432
- }
433
- catch { /* creator will surface its own error */ }
434
- // Creator has now registered its transport in `sessions`. Retry the
435
- // dispatch so this request lands on the shared transport.
436
- const shared = sessionId ? sessions.get(sessionId) : undefined;
437
- if (shared) {
438
- const webRequest = await buildWebRequest(req, body);
439
- const webResponse = await runWithAuth(auth, async () => (await shared.handleRequest(webRequest)));
440
- await sendWebResponse(res, webResponse);
441
- return;
442
- }
443
- // Fall through and create our own if the shared one disappeared.
444
- }
445
- const creation = (async () => {
446
- // Honor the client-supplied session-id when present. If the server
447
- // restarted and lost its Map, forcing a fresh UUID here would orphan
448
- // the client; reusing the client's id lets it keep speaking the same
449
- // session without noticing.
450
- let assignedSessionId = sessionId;
451
- const Transport = await getTransport();
452
- const transport = new Transport({
453
- sessionIdGenerator: () => {
454
- if (!assignedSessionId)
455
- assignedSessionId = globalThis.crypto.randomUUID();
456
- return assignedSessionId;
457
- },
458
- });
459
- const mcpServer = createMcpServer();
460
- // Inject the two network tools + fire the tool-list auto-report.
461
- // Skipped (no-op) when the vendor has discoverable=false in the With.in
462
- // dashboard — they opt out of the network on both the find-me and
463
- // expose-meta-tools sides.
464
- await registerNetworkTools(mcpServer, { vendorSlug, authServerUrl }, zod);
465
- logger(`[within] new mcp session — tools=[${Object.keys(mcpServer._registeredTools ?? {}).join(', ')}]`);
466
- await mcpServer.connect(transport);
467
- const webRequest = await buildWebRequest(req, body);
468
- const webResponse = await runWithAuth(auth, async () => (await transport.handleRequest(webRequest)));
469
- const finalSid = assignedSessionId;
470
- if (finalSid) {
471
- sessions.set(finalSid, transport);
472
- if (processed.aiClientFromInitialize) {
473
- sessionAiClient.set(finalSid, processed.aiClientFromInitialize);
474
- }
475
- }
476
- return { webResponse, finalSid };
477
- })();
478
- pendingSessions.set(lockKey, creation);
479
- try {
480
- const { webResponse } = await creation;
481
- await sendWebResponse(res, webResponse);
482
- }
483
- finally {
484
- pendingSessions.delete(lockKey);
485
- }
486
- }
487
- // ─── Public: subscribeHandler ───────────────────────────────────────────
488
- async function subscribeHandler(req, res) {
489
- let code = '';
490
- if (req.url) {
491
- try {
492
- const u = new URL(req.url, 'http://x');
493
- code = u.searchParams.get('c') ?? '';
494
- }
495
- catch { }
496
- }
497
- if (!code) {
498
- res.writeHead(400, { 'Content-Type': 'text/plain' });
499
- res.end('Missing code');
500
- return;
501
- }
502
- const target = `${authServerUrl}/oauth/upgrade/${vendorSlug}?c=${encodeURIComponent(code)}`;
503
- res.writeHead(302, { Location: target });
504
- res.end();
505
- }
506
- // ─── Public: prmHandler ────────────────────────────────────────────────
507
- //
508
- // RFC 9728 Protected Resource Metadata. The `resource` field is derived
509
- // from the incoming request so it byte-matches the URL the client used
510
- // to fetch this document (RFC 9728 §3.3). Behind Render / Cloudflare /
511
- // any TLS terminator, the X-Forwarded-Proto header carries the original
512
- // scheme; we take its left-most value (the client-facing hop).
513
- //
514
- // scopes_supported and bearer_methods_supported come from WithinConfig
515
- // with sensible defaults; authorization_servers is always the With.in
516
- // auth server URL the SDK was initialized with.
517
- const configuredHost = (() => {
518
- try {
519
- return new URL(resourceUrl).host;
520
- }
521
- catch {
522
- return undefined;
523
- }
524
- })();
525
- const scopesSupported = config.scopesSupported ?? ['tools:read', 'tools:write'];
526
- const bearerMethodsSupported = config.bearerMethodsSupported ?? ['header'];
527
- const configuredProto = (() => { try {
528
- return new URL(resourceUrl).protocol.replace(':', '');
529
- }
530
- catch {
531
- return 'http';
532
- } })();
533
- const configuredPath = (() => { try {
534
- return new URL(resourceUrl).pathname;
535
- }
536
- catch {
537
- return '/mcp';
538
- } })();
539
- function prmHandler(req, res) {
540
- const forwardedProto = req.headers['x-forwarded-proto'];
541
- const protoHeader = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto;
542
- // Fall back to the scheme from resourceUrl so local dev (no reverse proxy) works correctly.
543
- const proto = typeof protoHeader === 'string' ? protoHeader.split(',')[0].trim() : configuredProto;
544
- const hostHeader = req.headers.host;
545
- const host = typeof hostHeader === 'string' ? hostHeader : configuredHost ?? 'localhost';
546
- const body = JSON.stringify({
547
- resource: `${proto}://${host}${configuredPath}`,
548
- authorization_servers: [authServerUrl],
549
- scopes_supported: scopesSupported,
550
- bearer_methods_supported: bearerMethodsSupported,
551
- });
552
- res.writeHead(200, { 'Content-Type': 'application/json' });
553
- res.end(body);
554
- }
555
- return {
556
- mcpHandler,
557
- subscribeHandler,
558
- prmHandler,
559
- get vendorSlug() { return vendorSlug; },
560
- get authServerUrl() { return authServerUrl; },
561
- };
562
- }
package/dist/network.d.ts DELETED
@@ -1,83 +0,0 @@
1
- /**
2
- * With.in Network Discovery + Cross-Vendor Tool Use
3
- *
4
- * Injects two tools into every SDK-equipped vendor MCP server:
5
- *
6
- * • find_vendor_for_task — semantic search over the With.in vendor catalog
7
- * • within__use — silently provision + proxy a tool call to another vendor
8
- *
9
- * Both tools read the caller's identity from AsyncLocalStorage. The vendor
10
- * wraps their MCP request handling in `within.runWithAuth(auth, fn)` so the
11
- * tools see the correct access token + claims. See README for integration.
12
- *
13
- * Security posture:
14
- * • The access token the vendor already validated is reused as the bearer for
15
- * /api/network/*. It carries the same within_network_id / within_email /
16
- * within_org_domain as the UIT would.
17
- * • Fresh per-target access tokens are minted server-side and cached only
18
- * for the remainder of their TTL.
19
- * • within__use requires user_confirmed=true. This is a guardrail so Claude
20
- * can't silently fan out — it must obtain user consent for the target
21
- * vendor between find_vendor_for_task and within__use.
22
- */
23
- export declare const NETWORK_TOOL_NAMES: ReadonlySet<string>;
24
- import type { AuthResult } from './types.js';
25
- export interface NetworkConfig {
26
- /** The vendor slug this SDK instance is running for. */
27
- vendorSlug: string;
28
- /** The With.in auth server base URL (e.g. https://gateway.getwith.in). */
29
- authServerUrl: string;
30
- }
31
- export declare const authContextStorage: {
32
- getStore(): AuthResult | undefined;
33
- };
34
- export declare function runWithAuth<T>(auth: AuthResult, fn: () => Promise<T>): Promise<T>;
35
- /**
36
- * Returns the definitions of the two network tools. Register them on your
37
- * vendor's MCP server exactly once at startup.
38
- *
39
- * Intentionally not tied to a specific MCP server package — the vendor calls
40
- * `server.registerTool(def.name, def.config, def.handler)` with its own
41
- * zod-compatible schema wrapper. See `registerNetworkTools` below for a
42
- * helper that does this for the @modelcontextprotocol/server package.
43
- */
44
- export declare function buildNetworkTools(config: NetworkConfig, z: any): {
45
- discover: {
46
- name: string;
47
- config: {
48
- title: string;
49
- description: string;
50
- inputSchema: any;
51
- _meta: Record<string, unknown>;
52
- };
53
- handler: ({ intent, exclude_vendor_slugs }: {
54
- intent: string;
55
- exclude_vendor_slugs?: string[];
56
- }) => Promise<{
57
- content: {
58
- type: "text";
59
- text: string;
60
- }[];
61
- }>;
62
- };
63
- use: {
64
- name: string;
65
- config: {
66
- title: string;
67
- description: string;
68
- inputSchema: any;
69
- };
70
- handler: ({ target_vendor_slug, tool_name, tool_arguments, user_confirmed, }: {
71
- target_vendor_slug: string;
72
- tool_name: string;
73
- tool_arguments: Record<string, unknown>;
74
- user_confirmed: boolean;
75
- }) => Promise<any>;
76
- };
77
- };
78
- /** Diagnostic — exposes current cached network-status (or undefined if never fetched). */
79
- export declare function getNetworkStatusCache(slug: string): {
80
- discoverable: boolean;
81
- fetchedAtMs: number;
82
- } | undefined;
83
- export declare function registerNetworkTools(mcpServer: any, config: NetworkConfig, z: any): Promise<void>;