@mastra/connect 1.1.0-alpha.3 → 1.1.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +63 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/credential.d.ts +5 -0
- package/dist/credential.d.ts.map +1 -1
- package/dist/index.cjs +936 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +936 -168
- package/dist/index.js.map +1 -1
- package/dist/multi-connection.d.ts +9 -0
- package/dist/multi-connection.d.ts.map +1 -1
- package/dist/tools.d.ts +26 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/user-connect-page.d.ts +24 -0
- package/dist/user-connect-page.d.ts.map +1 -0
- package/dist/user-connect-routes.d.ts +72 -0
- package/dist/user-connect-routes.d.ts.map +1 -0
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { SpanType, resolveCurrentSpan } from "@mastra/core/observability";
|
|
4
4
|
import { createTool } from "@mastra/core/tools";
|
|
5
5
|
import { hkdfSync } from "crypto";
|
|
6
|
+
import { MASTRA_RESOURCE_ID_KEY } from "@mastra/core/request-context";
|
|
6
7
|
//#region src/errors.ts
|
|
7
8
|
const MAX_DETAIL_LENGTH = 2e3;
|
|
8
9
|
var MastraConnectError = class extends Error {
|
|
@@ -235,6 +236,20 @@ function stripTrailingSlashes(url) {
|
|
|
235
236
|
while (end > 0 && url[end - 1] === "/") end--;
|
|
236
237
|
return url.slice(0, end);
|
|
237
238
|
}
|
|
239
|
+
/** Header asserting the end-user resource key on platform data-path calls. */
|
|
240
|
+
const RESOURCE_ID_HEADER = "x-mastra-resource-id";
|
|
241
|
+
/**
|
|
242
|
+
* Validates an end-user resource key against the platform contract: an opaque
|
|
243
|
+
* string of 1–256 characters after trimming. Returns the trimmed key.
|
|
244
|
+
* Mirrors the platform's `resourceIdSchema` so an invalid key fails here —
|
|
245
|
+
* before any platform call — instead of as an opaque 400.
|
|
246
|
+
*/
|
|
247
|
+
function validateResourceId(resourceId) {
|
|
248
|
+
if (typeof resourceId !== "string") throw new MastraConnectError("invalid_options", `Invalid resourceId: expected a string, got ${typeof resourceId}.`);
|
|
249
|
+
const trimmed = resourceId.trim();
|
|
250
|
+
if (trimmed.length < 1 || trimmed.length > 256) throw new MastraConnectError("invalid_options", `Invalid resourceId: expected 1-256 characters after trimming, got ${trimmed.length}.`);
|
|
251
|
+
return trimmed;
|
|
252
|
+
}
|
|
238
253
|
function resolveClient(options) {
|
|
239
254
|
const accessToken = options?.accessToken?.trim() || process.env.MASTRA_PLATFORM_ACCESS_TOKEN?.trim() || process.env.MASTRA_PLATFORM_SECRET_KEY?.trim();
|
|
240
255
|
if (!accessToken) throw new MastraConnectError("missing_access_token", "Missing Mastra platform access token: set MASTRA_PLATFORM_ACCESS_TOKEN (or MASTRA_PLATFORM_SECRET_KEY), or pass client.accessToken.");
|
|
@@ -247,11 +262,14 @@ function resolveClient(options) {
|
|
|
247
262
|
if (orgId) headers["x-organization-id"] = orgId;
|
|
248
263
|
const projectId = options?.projectId?.trim() || process.env.MASTRA_PROJECT_ID?.trim();
|
|
249
264
|
if (projectId) headers["x-mastra-project-id"] = projectId;
|
|
265
|
+
const resourceId = options?.resourceId !== void 0 ? validateResourceId(options.resourceId) : void 0;
|
|
266
|
+
if (resourceId !== void 0) headers[RESOURCE_ID_HEADER] = resourceId;
|
|
250
267
|
return {
|
|
251
268
|
baseUrl,
|
|
252
269
|
headers,
|
|
253
270
|
fetch: options?.fetch ?? globalThis.fetch,
|
|
254
|
-
accessToken
|
|
271
|
+
accessToken,
|
|
272
|
+
resourceId
|
|
255
273
|
};
|
|
256
274
|
}
|
|
257
275
|
function redact(message, accessToken) {
|
|
@@ -343,12 +361,31 @@ const connectionSchema = z.object({
|
|
|
343
361
|
connectedByUserId: z.string().nullish(),
|
|
344
362
|
connectedAt: z.string().nullish(),
|
|
345
363
|
createdAt: z.string().nullish(),
|
|
346
|
-
accountLabel: z.string().nullish()
|
|
364
|
+
accountLabel: z.string().nullish(),
|
|
365
|
+
/** End-user resource key the connection is scoped to; null/absent = global. */
|
|
366
|
+
resourceId: z.string().nullish()
|
|
347
367
|
});
|
|
348
368
|
const connectionListSchema = z.object({ connections: z.array(connectionSchema) });
|
|
369
|
+
/**
|
|
370
|
+
* One credential-form field from the platform catalog, as the hosted connect
|
|
371
|
+
* surface needs it. Lenient subset of the platform's `VendorAuthField`.
|
|
372
|
+
*/
|
|
373
|
+
const catalogAuthFieldSchema = z.object({
|
|
374
|
+
name: z.string(),
|
|
375
|
+
label: z.string().nullish(),
|
|
376
|
+
description: z.string().nullish(),
|
|
377
|
+
placeholder: z.string().nullish(),
|
|
378
|
+
secret: z.boolean().nullish(),
|
|
379
|
+
required: z.boolean().nullish(),
|
|
380
|
+
order: z.number().nullish()
|
|
381
|
+
});
|
|
349
382
|
const integrationCatalogEntrySchema = z.object({
|
|
350
383
|
id: z.string(),
|
|
351
|
-
capabilities: z.object({ mcp: z.boolean().optional() })
|
|
384
|
+
capabilities: z.object({ mcp: z.boolean().optional() }),
|
|
385
|
+
displayName: z.string().nullish(),
|
|
386
|
+
logoUrl: z.string().nullish(),
|
|
387
|
+
authType: z.string().nullish(),
|
|
388
|
+
authFields: z.array(catalogAuthFieldSchema).nullish()
|
|
352
389
|
});
|
|
353
390
|
const integrationCatalogResponseSchema = z.object({ integrations: z.array(integrationCatalogEntrySchema) });
|
|
354
391
|
/**
|
|
@@ -385,7 +422,11 @@ const connectionContextSchema = z.object({
|
|
|
385
422
|
metadata: z.record(z.string(), z.unknown()).nullable()
|
|
386
423
|
});
|
|
387
424
|
async function listProjectConnections(client, projectId) {
|
|
388
|
-
const
|
|
425
|
+
const query = client.resourceId !== void 0 ? `?${new URLSearchParams({
|
|
426
|
+
resourceId: client.resourceId,
|
|
427
|
+
includeGlobal: "true"
|
|
428
|
+
})}` : "";
|
|
429
|
+
const response = await platformFetch(client, `/v2/projects/${encodeURIComponent(projectId)}/connections${query}`);
|
|
389
430
|
if (!response.ok) await throwPlatformError(response, `listing connections for project ${projectId}`);
|
|
390
431
|
const parsed = connectionListSchema.safeParse(await parsePlatformJson(response, `listing connections for project ${projectId}`));
|
|
391
432
|
if (!parsed.success) throw new MastraConnectError("platform_error", `Platform returned an unexpected connection list shape for project ${projectId}.`);
|
|
@@ -539,6 +580,34 @@ async function proxyRequestWithResponse(client, connectionId, options) {
|
|
|
539
580
|
};
|
|
540
581
|
}
|
|
541
582
|
}
|
|
583
|
+
const connectSessionSchema = z.object({
|
|
584
|
+
connectionId: z.string(),
|
|
585
|
+
integrationId: z.string(),
|
|
586
|
+
connectUrl: z.string(),
|
|
587
|
+
sessionToken: z.string(),
|
|
588
|
+
expiresAt: z.string()
|
|
589
|
+
});
|
|
590
|
+
/**
|
|
591
|
+
* Creates a project-scoped connect session. A resource-scoped client passes
|
|
592
|
+
* its resource key in the body, so the resulting connection is owned by that
|
|
593
|
+
* end user; an unscoped client creates a global connection.
|
|
594
|
+
*/
|
|
595
|
+
async function createProjectConnectSession(client, projectId, integrationId) {
|
|
596
|
+
const response = await platformFetch(client, `/v2/projects/${encodeURIComponent(projectId)}/integrations/${encodeURIComponent(integrationId)}/connect-sessions`, {
|
|
597
|
+
method: "POST",
|
|
598
|
+
headers: { "content-type": "application/json" },
|
|
599
|
+
body: JSON.stringify(client.resourceId !== void 0 ? { resourceId: client.resourceId } : {})
|
|
600
|
+
});
|
|
601
|
+
if (!response.ok) await throwPlatformError(response, `creating a connect session for ${integrationId}`);
|
|
602
|
+
const parsed = connectSessionSchema.safeParse(await parsePlatformJson(response, `creating a connect session for ${integrationId}`));
|
|
603
|
+
if (!parsed.success) throw new MastraConnectError("platform_error", `Platform returned an unexpected connect-session shape for ${integrationId}.`);
|
|
604
|
+
return parsed.data;
|
|
605
|
+
}
|
|
606
|
+
/** Disconnects a connection. The platform tears down the vendor side asynchronously. */
|
|
607
|
+
async function disconnectConnection(client, connectionId) {
|
|
608
|
+
const response = await platformFetch(client, `/v2/connections/${encodeURIComponent(connectionId)}`, { method: "DELETE" });
|
|
609
|
+
if (!response.ok) await throwPlatformError(response, `disconnecting connection ${connectionId}`);
|
|
610
|
+
}
|
|
542
611
|
//#endregion
|
|
543
612
|
//#region src/toolset.ts
|
|
544
613
|
/**
|
|
@@ -667,7 +736,8 @@ function toNamedConnections(candidates) {
|
|
|
667
736
|
named.push({
|
|
668
737
|
id: connection.id,
|
|
669
738
|
name,
|
|
670
|
-
accountLabel: connection.accountLabel ?? null
|
|
739
|
+
accountLabel: connection.accountLabel ?? null,
|
|
740
|
+
resourceId: connection.resourceId ?? null
|
|
671
741
|
});
|
|
672
742
|
}
|
|
673
743
|
return named;
|
|
@@ -785,8 +855,9 @@ function listConnectionsToolKey(integrationId) {
|
|
|
785
855
|
* of the active connections. Deterministic, no I/O — reads a captured snapshot.
|
|
786
856
|
*/
|
|
787
857
|
function buildListConnectionsTool(integrationId, connections) {
|
|
858
|
+
const listToolKey = listConnectionsToolKey(integrationId);
|
|
788
859
|
return createTool({
|
|
789
|
-
id:
|
|
860
|
+
id: listToolKey,
|
|
790
861
|
description: `List the available connections for the ${integrationId} provider. Use the returned 'name' values as connection_name on other ${integrationId} tools.`,
|
|
791
862
|
inputSchema: z.object({}),
|
|
792
863
|
outputSchema: z.object({ connections: z.array(z.object({
|
|
@@ -852,6 +923,7 @@ function buildProxyMultiConnectionTools(input) {
|
|
|
852
923
|
*/
|
|
853
924
|
async function buildMcpMultiConnectionTools(input) {
|
|
854
925
|
const { registration, connections, allowTools, disallowTools, requireApproval, client, mcpClients, resolverId } = input;
|
|
926
|
+
const globalClient = input.globalClient ?? client;
|
|
855
927
|
const requireApprovalFor = Array.isArray(requireApproval) ? compileToolMatcher(requireApproval) : void 0;
|
|
856
928
|
const listToolKey = listConnectionsToolKey(registration.integrationId);
|
|
857
929
|
const innerAllowTools = allowTools?.filter((name) => name !== listToolKey);
|
|
@@ -868,7 +940,7 @@ async function buildMcpMultiConnectionTools(input) {
|
|
|
868
940
|
const cacheKey = `${registration.integrationId}::${connection.id}`;
|
|
869
941
|
let entry = mcpClients.get(cacheKey);
|
|
870
942
|
if (!entry) {
|
|
871
|
-
const transport = platformMcpTransport(client, connection.id);
|
|
943
|
+
const transport = platformMcpTransport(connection.resourceId == null ? globalClient : client, connection.id);
|
|
872
944
|
entry = {
|
|
873
945
|
integrationId: registration.integrationId,
|
|
874
946
|
connectionId: connection.id,
|
|
@@ -15443,7 +15515,7 @@ function findFreeSlotsTool(proxy) {
|
|
|
15443
15515
|
if (totalDuration >= minDurationMs) freeSlots.push({
|
|
15444
15516
|
start: input.timeMin,
|
|
15445
15517
|
end: input.timeMax,
|
|
15446
|
-
durationMinutes: Math.floor(totalDuration /
|
|
15518
|
+
durationMinutes: Math.floor(totalDuration / 6e4)
|
|
15447
15519
|
});
|
|
15448
15520
|
} else {
|
|
15449
15521
|
const firstBusyPeriod = mergedBusyPeriods[0];
|
|
@@ -15454,7 +15526,7 @@ function findFreeSlotsTool(proxy) {
|
|
|
15454
15526
|
if (gapDuration >= minDurationMs) freeSlots.push({
|
|
15455
15527
|
start: input.timeMin,
|
|
15456
15528
|
end: firstBusyPeriod.start,
|
|
15457
|
-
durationMinutes: Math.floor(gapDuration /
|
|
15529
|
+
durationMinutes: Math.floor(gapDuration / 6e4)
|
|
15458
15530
|
});
|
|
15459
15531
|
}
|
|
15460
15532
|
}
|
|
@@ -15469,7 +15541,7 @@ function findFreeSlotsTool(proxy) {
|
|
|
15469
15541
|
if (gapDuration >= minDurationMs) freeSlots.push({
|
|
15470
15542
|
start: currentPeriod.end,
|
|
15471
15543
|
end: nextPeriod.start,
|
|
15472
|
-
durationMinutes: Math.floor(gapDuration /
|
|
15544
|
+
durationMinutes: Math.floor(gapDuration / 6e4)
|
|
15473
15545
|
});
|
|
15474
15546
|
}
|
|
15475
15547
|
}
|
|
@@ -15481,7 +15553,7 @@ function findFreeSlotsTool(proxy) {
|
|
|
15481
15553
|
if (gapDuration >= minDurationMs) freeSlots.push({
|
|
15482
15554
|
start: lastBusyPeriod.end,
|
|
15483
15555
|
end: input.timeMax,
|
|
15484
|
-
durationMinutes: Math.floor(gapDuration /
|
|
15556
|
+
durationMinutes: Math.floor(gapDuration / 6e4)
|
|
15485
15557
|
});
|
|
15486
15558
|
}
|
|
15487
15559
|
}
|
|
@@ -45981,11 +46053,12 @@ function cleanBlock(block) {
|
|
|
45981
46053
|
].includes(key)) continue;
|
|
45982
46054
|
if (value === null) continue;
|
|
45983
46055
|
if (Array.isArray(value)) cleanedContent[key] = value;
|
|
45984
|
-
else if (isRecord$1(value))
|
|
45985
|
-
|
|
45986
|
-
|
|
46056
|
+
else if (isRecord$1(value)) {
|
|
46057
|
+
if (value["object"] === "block") {
|
|
46058
|
+
const nestedBlock = cleanBlock(value);
|
|
46059
|
+
if (nestedBlock !== null) cleanedContent[key] = nestedBlock;
|
|
46060
|
+
} else cleanedContent[key] = value;
|
|
45987
46061
|
} else cleanedContent[key] = value;
|
|
45988
|
-
else cleanedContent[key] = value;
|
|
45989
46062
|
}
|
|
45990
46063
|
cleaned[blockType] = cleanedContent;
|
|
45991
46064
|
}
|
|
@@ -68645,9 +68718,11 @@ function updatePriceTool(proxy) {
|
|
|
68645
68718
|
...input.tax_behavior !== void 0 && { tax_behavior: input.tax_behavior }
|
|
68646
68719
|
};
|
|
68647
68720
|
const params = new URLSearchParams();
|
|
68648
|
-
for (const [key, value] of Object.entries(updateData)) if (value !== void 0 && value !== null)
|
|
68649
|
-
|
|
68650
|
-
|
|
68721
|
+
for (const [key, value] of Object.entries(updateData)) if (value !== void 0 && value !== null) {
|
|
68722
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
68723
|
+
for (const [nestedKey, nestedValue] of Object.entries(value)) if (nestedValue !== void 0 && nestedValue !== null) params.append(`${key}[${nestedKey}]`, String(nestedValue));
|
|
68724
|
+
} else params.append(key, String(value));
|
|
68725
|
+
}
|
|
68651
68726
|
const proxyConfig = {
|
|
68652
68727
|
endpoint: `/v1/prices/${encodeURIComponent(input.id)}`,
|
|
68653
68728
|
data: params.toString(),
|
|
@@ -75230,8 +75305,266 @@ function resolveProviderConnection(request, candidates) {
|
|
|
75230
75305
|
const DEFAULT_TTL_MS$1 = 3e4;
|
|
75231
75306
|
/** Minimum wait after a failed platform fetch before another background revalidation. */
|
|
75232
75307
|
const FAILURE_COOLDOWN_MS$1 = 3e4;
|
|
75308
|
+
/** Maximum resource-scoped snapshots kept per shared state; least recently used evicts first. */
|
|
75309
|
+
const MAX_RESOURCE_SNAPSHOTS = 50;
|
|
75310
|
+
/**
|
|
75311
|
+
* Grace period after a resource snapshot is evicted before its MCP clients
|
|
75312
|
+
* become eligible for disconnect, so an agent call that resolved tools just
|
|
75313
|
+
* before eviction does not lose its transport mid-tool-call.
|
|
75314
|
+
*/
|
|
75315
|
+
const EVICTION_LINGER_MS = 6e4;
|
|
75316
|
+
const GLOBAL_SCOPE_KEY = "global";
|
|
75233
75317
|
let nextResolverId = 0;
|
|
75234
75318
|
/**
|
|
75319
|
+
* Maximum shared states kept in the registry. Each distinct option set (for
|
|
75320
|
+
* example an inline `fetch` closure or a rotated access token) mints one
|
|
75321
|
+
* state holding snapshots and open MCP transports; without a bound those
|
|
75322
|
+
* accumulate for the life of the process. Least recently acquired evicts
|
|
75323
|
+
* first and is disconnected; a resolver still holding an evicted state keeps
|
|
75324
|
+
* working — it just refetches and reopens transports on its next resolution.
|
|
75325
|
+
*/
|
|
75326
|
+
const MAX_SHARED_STATES = 32;
|
|
75327
|
+
const sharedStates = /* @__PURE__ */ new Map();
|
|
75328
|
+
const functionIds = /* @__PURE__ */ new WeakMap();
|
|
75329
|
+
let nextFunctionId = 0;
|
|
75330
|
+
/** Stable per-process id for a function identity (custom fetch), for use in the registry key. */
|
|
75331
|
+
function identityId(fn) {
|
|
75332
|
+
if (!fn) return void 0;
|
|
75333
|
+
let id = functionIds.get(fn);
|
|
75334
|
+
if (id === void 0) {
|
|
75335
|
+
id = ++nextFunctionId;
|
|
75336
|
+
functionIds.set(fn, id);
|
|
75337
|
+
}
|
|
75338
|
+
return id;
|
|
75339
|
+
}
|
|
75340
|
+
/** Deterministic JSON with sorted object keys, so option objects written in any order share a key. */
|
|
75341
|
+
function stableStringify(value) {
|
|
75342
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
75343
|
+
if (value && typeof value === "object") return `{${Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`).join(",")}}`;
|
|
75344
|
+
return JSON.stringify(value) ?? "undefined";
|
|
75345
|
+
}
|
|
75346
|
+
function sharedStateKey(projectId, ttlMs, options) {
|
|
75347
|
+
return stableStringify({
|
|
75348
|
+
projectId,
|
|
75349
|
+
ttlMs,
|
|
75350
|
+
providers: options.providers,
|
|
75351
|
+
allowTools: options.allowTools,
|
|
75352
|
+
disallowTools: options.disallowTools,
|
|
75353
|
+
requireApproval: options.requireApproval,
|
|
75354
|
+
client: {
|
|
75355
|
+
baseUrl: options.client?.baseUrl,
|
|
75356
|
+
orgId: options.client?.orgId,
|
|
75357
|
+
accessToken: options.client?.accessToken,
|
|
75358
|
+
fetch: identityId(options.client?.fetch)
|
|
75359
|
+
}
|
|
75360
|
+
});
|
|
75361
|
+
}
|
|
75362
|
+
function acquireSharedState(projectId, ttlMs, options, normalizedProviders) {
|
|
75363
|
+
const key = sharedStateKey(projectId, ttlMs, options);
|
|
75364
|
+
let state = sharedStates.get(key);
|
|
75365
|
+
if (!state) {
|
|
75366
|
+
state = {
|
|
75367
|
+
resolverId: ++nextResolverId,
|
|
75368
|
+
projectId,
|
|
75369
|
+
ttlMs,
|
|
75370
|
+
options,
|
|
75371
|
+
normalizedProviders,
|
|
75372
|
+
mcpClients: /* @__PURE__ */ new Map(),
|
|
75373
|
+
scopes: /* @__PURE__ */ new Map(),
|
|
75374
|
+
evictionHolds: [],
|
|
75375
|
+
lastAcquiredAt: Date.now()
|
|
75376
|
+
};
|
|
75377
|
+
sharedStates.set(key, state);
|
|
75378
|
+
evictSharedStates(key);
|
|
75379
|
+
}
|
|
75380
|
+
state.lastAcquiredAt = Date.now();
|
|
75381
|
+
return state;
|
|
75382
|
+
}
|
|
75383
|
+
/** Evicts and disconnects the least recently acquired states beyond the registry bound. */
|
|
75384
|
+
function evictSharedStates(justAcquiredKey) {
|
|
75385
|
+
let excess = sharedStates.size - MAX_SHARED_STATES;
|
|
75386
|
+
if (excess <= 0) return;
|
|
75387
|
+
const candidates = Array.from(sharedStates.entries()).filter(([key]) => key !== justAcquiredKey).sort((a, b) => a[1].lastAcquiredAt - b[1].lastAcquiredAt);
|
|
75388
|
+
for (const [key, state] of candidates) {
|
|
75389
|
+
if (excess <= 0) break;
|
|
75390
|
+
sharedStates.delete(key);
|
|
75391
|
+
excess -= 1;
|
|
75392
|
+
setTimeout(() => void disconnectState(state).catch(() => {}), EVICTION_LINGER_MS).unref?.();
|
|
75393
|
+
}
|
|
75394
|
+
}
|
|
75395
|
+
function scopeKey(resourceId) {
|
|
75396
|
+
return resourceId === void 0 ? GLOBAL_SCOPE_KEY : `resource:${resourceId}`;
|
|
75397
|
+
}
|
|
75398
|
+
/** Gets or creates the scope for a resource key, touching its LRU clock. */
|
|
75399
|
+
function acquireScope(state, resourceId) {
|
|
75400
|
+
const key = scopeKey(resourceId);
|
|
75401
|
+
let scope = state.scopes.get(key);
|
|
75402
|
+
if (!scope) {
|
|
75403
|
+
scope = {
|
|
75404
|
+
resourceId,
|
|
75405
|
+
client: resolveClient({
|
|
75406
|
+
...state.options.client,
|
|
75407
|
+
projectId: state.projectId,
|
|
75408
|
+
...resourceId === void 0 ? {} : { resourceId }
|
|
75409
|
+
}),
|
|
75410
|
+
lastUsedAt: Date.now()
|
|
75411
|
+
};
|
|
75412
|
+
state.scopes.set(key, scope);
|
|
75413
|
+
if (resourceId !== void 0) evictResourceScopes(state);
|
|
75414
|
+
}
|
|
75415
|
+
scope.lastUsedAt = Date.now();
|
|
75416
|
+
return scope;
|
|
75417
|
+
}
|
|
75418
|
+
/**
|
|
75419
|
+
* Bounds the per-resource snapshot map. Evicted scopes' MCP keys get a
|
|
75420
|
+
* linger hold so transports stay alive for calls resolved just before
|
|
75421
|
+
* eviction; a timer re-runs the prune once the hold expires. Scopes with a
|
|
75422
|
+
* fetch in flight are skipped this round.
|
|
75423
|
+
*/
|
|
75424
|
+
function evictResourceScopes(state) {
|
|
75425
|
+
const resourceScopes = Array.from(state.scopes.entries()).filter(([key]) => key !== GLOBAL_SCOPE_KEY);
|
|
75426
|
+
let excess = resourceScopes.length - MAX_RESOURCE_SNAPSHOTS;
|
|
75427
|
+
if (excess <= 0) return;
|
|
75428
|
+
resourceScopes.sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt);
|
|
75429
|
+
let scheduledPrune = false;
|
|
75430
|
+
for (const [key, scope] of resourceScopes) {
|
|
75431
|
+
if (excess <= 0) break;
|
|
75432
|
+
if (scope.inflight) continue;
|
|
75433
|
+
state.scopes.delete(key);
|
|
75434
|
+
excess -= 1;
|
|
75435
|
+
if (scope.cache && scope.cache.mcpKeys.size > 0) {
|
|
75436
|
+
state.evictionHolds.push({
|
|
75437
|
+
mcpKeys: scope.cache.mcpKeys,
|
|
75438
|
+
expiresAt: Date.now() + EVICTION_LINGER_MS
|
|
75439
|
+
});
|
|
75440
|
+
if (!scheduledPrune) {
|
|
75441
|
+
scheduledPrune = true;
|
|
75442
|
+
setTimeout(() => void pruneStaleMcpClients(state), 60010).unref?.();
|
|
75443
|
+
}
|
|
75444
|
+
}
|
|
75445
|
+
}
|
|
75446
|
+
}
|
|
75447
|
+
/**
|
|
75448
|
+
* Disconnects MCP clients no longer referenced by any scope's current
|
|
75449
|
+
* snapshot or by an unexpired eviction hold. Runs after every snapshot
|
|
75450
|
+
* refresh (a connection removed on the platform drops its transport, as
|
|
75451
|
+
* before) and when eviction holds expire.
|
|
75452
|
+
*/
|
|
75453
|
+
async function pruneStaleMcpClients(state) {
|
|
75454
|
+
if (state.closing) return;
|
|
75455
|
+
const now = Date.now();
|
|
75456
|
+
state.evictionHolds = state.evictionHolds.filter((hold) => hold.expiresAt > now);
|
|
75457
|
+
const active = /* @__PURE__ */ new Set();
|
|
75458
|
+
for (const scope of state.scopes.values()) for (const key of scope.cache?.mcpKeys ?? []) active.add(key);
|
|
75459
|
+
for (const hold of state.evictionHolds) for (const key of hold.mcpKeys) active.add(key);
|
|
75460
|
+
const stale = Array.from(state.mcpClients.entries()).filter(([key]) => !active.has(key));
|
|
75461
|
+
for (const [key] of stale) state.mcpClients.delete(key);
|
|
75462
|
+
await Promise.allSettled(stale.map(([, entry]) => entry.client.disconnect()));
|
|
75463
|
+
}
|
|
75464
|
+
/**
|
|
75465
|
+
* Loads the connection list and the integration catalog for one scope. A
|
|
75466
|
+
* catalog failure only matters when an active connection needs catalog-backed
|
|
75467
|
+
* MCP discovery; otherwise checked-in HTTP providers resolve with an empty
|
|
75468
|
+
* catalog.
|
|
75469
|
+
*/
|
|
75470
|
+
async function loadSnapshotInputs(state, client) {
|
|
75471
|
+
const [connectionsResult, catalogResult] = await Promise.allSettled([listProjectConnections(client, state.projectId), listIntegrations(client)]);
|
|
75472
|
+
if (connectionsResult.status === "rejected") throw connectionsResult.reason;
|
|
75473
|
+
const connections = connectionsResult.value;
|
|
75474
|
+
if (catalogResult.status === "fulfilled") return {
|
|
75475
|
+
connections,
|
|
75476
|
+
catalog: catalogResult.value,
|
|
75477
|
+
catalogAvailable: true
|
|
75478
|
+
};
|
|
75479
|
+
const { overrides, only } = state.normalizedProviders;
|
|
75480
|
+
const checkedIn = new Set(PROVIDERS.map((registration) => registration.integrationId));
|
|
75481
|
+
if (connections.some((connection) => connection.status === "active" && !checkedIn.has(connection.integrationId) && !overrides[connection.integrationId]?.disabled && (only === void 0 || only.has(connection.integrationId)))) throw catalogResult.reason;
|
|
75482
|
+
const reason = catalogResult.reason;
|
|
75483
|
+
console.warn(`[@mastra/connect] Platform catalog unavailable (${reason instanceof Error ? reason.message : String(reason)}); resolving checked-in providers only.`);
|
|
75484
|
+
return {
|
|
75485
|
+
connections,
|
|
75486
|
+
catalog: [],
|
|
75487
|
+
catalogAvailable: false
|
|
75488
|
+
};
|
|
75489
|
+
}
|
|
75490
|
+
/**
|
|
75491
|
+
* Fetches a fresh snapshot for one scope, deduplicating concurrent calls per
|
|
75492
|
+
* scope. Rejects on failure. A refresh requested while the state is closing
|
|
75493
|
+
* starts after the close completes (against a freshly acquired scope, since
|
|
75494
|
+
* closing clears the scope map).
|
|
75495
|
+
*/
|
|
75496
|
+
function refreshScope(state, scope) {
|
|
75497
|
+
if (state.closing) return state.closing.then(() => refreshScope(state, acquireScope(state, scope.resourceId)));
|
|
75498
|
+
if (!scope.inflight) scope.inflight = (async () => {
|
|
75499
|
+
try {
|
|
75500
|
+
const { connections, catalog, catalogAvailable } = await loadSnapshotInputs(state, scope.client);
|
|
75501
|
+
const requests = buildRequests(state.normalizedProviders, catalog, catalogAvailable);
|
|
75502
|
+
const clientOptions = {
|
|
75503
|
+
...state.options.client,
|
|
75504
|
+
projectId: state.projectId,
|
|
75505
|
+
...scope.resourceId === void 0 ? {} : { resourceId: scope.resourceId }
|
|
75506
|
+
};
|
|
75507
|
+
const { record, mcpKeys } = await mapTools(connections, requests, state.options, clientOptions, scope.client, state.mcpClients, state.resolverId);
|
|
75508
|
+
scope.cache = {
|
|
75509
|
+
snapshot: record,
|
|
75510
|
+
fetchedAt: Date.now(),
|
|
75511
|
+
mcpKeys
|
|
75512
|
+
};
|
|
75513
|
+
scope.lastFailureAt = void 0;
|
|
75514
|
+
await pruneStaleMcpClients(state);
|
|
75515
|
+
return record;
|
|
75516
|
+
} catch (error) {
|
|
75517
|
+
scope.lastFailureAt = Date.now();
|
|
75518
|
+
throw error;
|
|
75519
|
+
} finally {
|
|
75520
|
+
scope.inflight = void 0;
|
|
75521
|
+
}
|
|
75522
|
+
})();
|
|
75523
|
+
return scope.inflight;
|
|
75524
|
+
}
|
|
75525
|
+
/** Serves a scope's snapshot: fresh from cache, stale with background revalidation, or a blocking first fetch. */
|
|
75526
|
+
async function resolveScope(state, resourceId) {
|
|
75527
|
+
const scope = acquireScope(state, resourceId);
|
|
75528
|
+
if (scope.cache && Date.now() - scope.cache.fetchedAt < state.ttlMs) return scope.cache.snapshot;
|
|
75529
|
+
if (scope.cache) {
|
|
75530
|
+
const staleSnapshot = scope.cache.snapshot;
|
|
75531
|
+
if (!(scope.lastFailureAt !== void 0 && Date.now() - scope.lastFailureAt < FAILURE_COOLDOWN_MS$1) && !scope.inflight && !state.closing) {
|
|
75532
|
+
const staleFetchedAt = scope.cache.fetchedAt;
|
|
75533
|
+
refreshScope(state, scope).catch((error) => {
|
|
75534
|
+
console.warn(`[@mastra/connect] Keeping cached tools (fetched ${Date.now() - staleFetchedAt}ms ago); platform refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
75535
|
+
});
|
|
75536
|
+
}
|
|
75537
|
+
return staleSnapshot;
|
|
75538
|
+
}
|
|
75539
|
+
return refreshScope(state, scope);
|
|
75540
|
+
}
|
|
75541
|
+
/**
|
|
75542
|
+
* Closes every MCP transport and clears every cached snapshot in the shared
|
|
75543
|
+
* state. Any refresh in progress settles first so it cannot repopulate the
|
|
75544
|
+
* cache or register an MCP client after cleanup; refreshes requested
|
|
75545
|
+
* meanwhile wait for the close and start afterwards. The state stays usable:
|
|
75546
|
+
* the next resolution fetches fresh.
|
|
75547
|
+
*/
|
|
75548
|
+
function disconnectState(state) {
|
|
75549
|
+
state.closing ??= (async () => {
|
|
75550
|
+
try {
|
|
75551
|
+
let pending = Array.from(state.scopes.values(), (scope) => scope.inflight).filter(Boolean);
|
|
75552
|
+
while (pending.length > 0) {
|
|
75553
|
+
await Promise.allSettled(pending);
|
|
75554
|
+
pending = Array.from(state.scopes.values(), (scope) => scope.inflight).filter(Boolean);
|
|
75555
|
+
}
|
|
75556
|
+
state.scopes.clear();
|
|
75557
|
+
state.evictionHolds = [];
|
|
75558
|
+
const clients = Array.from(state.mcpClients.values(), (entry) => entry.client);
|
|
75559
|
+
state.mcpClients.clear();
|
|
75560
|
+
await Promise.allSettled(clients.map((mcp) => mcp.disconnect()));
|
|
75561
|
+
} finally {
|
|
75562
|
+
state.closing = void 0;
|
|
75563
|
+
}
|
|
75564
|
+
})();
|
|
75565
|
+
return state.closing;
|
|
75566
|
+
}
|
|
75567
|
+
/**
|
|
75235
75568
|
* Returns a live toolset resolver over the project's Platform connections.
|
|
75236
75569
|
* HTTP providers are loaded from the shipped `PROVIDERS` registry. MCP providers
|
|
75237
75570
|
* are discovered from the Platform integration catalog. Tools from every supported
|
|
@@ -75260,13 +75593,14 @@ function tools(options = {}) {
|
|
|
75260
75593
|
if (!projectId) throw new MastraConnectError("missing_project_id", "Missing project id: set MASTRA_PROJECT_ID or pass projectId.");
|
|
75261
75594
|
if (options.ttlMs !== void 0 && (!Number.isFinite(options.ttlMs) || options.ttlMs < 0)) throw new MastraConnectError("invalid_options", `Invalid ttlMs (${options.ttlMs}): expected a finite number of milliseconds >= 0.`);
|
|
75262
75595
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS$1;
|
|
75263
|
-
|
|
75596
|
+
if (options.client?.resourceId !== void 0) throw new MastraConnectError("invalid_options", "Set resourceId on the tools() options (tools({ resourceId })), not on client.resourceId.");
|
|
75597
|
+
const resourceIdOption = options.resourceId;
|
|
75598
|
+
if (resourceIdOption !== void 0 && typeof resourceIdOption !== "string" && typeof resourceIdOption !== "function") throw new MastraConnectError("invalid_options", `Invalid resourceId: expected a string or a function, got ${typeof resourceIdOption}.`);
|
|
75599
|
+
const staticResourceId = typeof resourceIdOption === "string" ? validateResourceId(resourceIdOption) : void 0;
|
|
75600
|
+
resolveClient({
|
|
75264
75601
|
...options.client,
|
|
75265
75602
|
projectId
|
|
75266
|
-
};
|
|
75267
|
-
const client = resolveClient(clientOptions);
|
|
75268
|
-
const resolverId = ++nextResolverId;
|
|
75269
|
-
const mcpClients = /* @__PURE__ */ new Map();
|
|
75603
|
+
});
|
|
75270
75604
|
const normalizedProviders = normalizeProviderOverrides(options.providers);
|
|
75271
75605
|
const providerOverrides = normalizedProviders.overrides;
|
|
75272
75606
|
validateProviderIds(providerOverrides);
|
|
@@ -75275,90 +75609,45 @@ function tools(options = {}) {
|
|
|
75275
75609
|
rejectRemovedAutoApproveTools(providerOverrides);
|
|
75276
75610
|
if (options.allowTools !== void 0 && options.disallowTools !== void 0) throw new MastraConnectError("invalid_options", "Top-level allowTools and disallowTools are mutually exclusive; set at most one.");
|
|
75277
75611
|
if (options.requireApproval !== void 0 && typeof options.requireApproval !== "boolean" && !(Array.isArray(options.requireApproval) && options.requireApproval.every((name) => typeof name === "string"))) throw new MastraConnectError("invalid_options", "Top-level requireApproval must be a boolean or an array of tool keys.");
|
|
75278
|
-
|
|
75279
|
-
|
|
75280
|
-
|
|
75281
|
-
|
|
75282
|
-
|
|
75283
|
-
* Loads the connection list and the integration catalog. A catalog failure
|
|
75284
|
-
* only matters when an active connection needs catalog-backed MCP discovery;
|
|
75285
|
-
* otherwise checked-in HTTP providers resolve with an empty catalog.
|
|
75286
|
-
*/
|
|
75287
|
-
const loadSnapshotInputs = async () => {
|
|
75288
|
-
const [connectionsResult, catalogResult] = await Promise.allSettled([listProjectConnections(client, projectId), listIntegrations(client)]);
|
|
75289
|
-
if (connectionsResult.status === "rejected") throw connectionsResult.reason;
|
|
75290
|
-
const connections = connectionsResult.value;
|
|
75291
|
-
if (catalogResult.status === "fulfilled") return {
|
|
75292
|
-
connections,
|
|
75293
|
-
catalog: catalogResult.value,
|
|
75294
|
-
catalogAvailable: true
|
|
75295
|
-
};
|
|
75296
|
-
const checkedIn = new Set(PROVIDERS.map((registration) => registration.integrationId));
|
|
75297
|
-
if (connections.some((connection) => connection.status === "active" && !checkedIn.has(connection.integrationId) && !providerOverrides[connection.integrationId]?.disabled && (normalizedProviders.only === void 0 || normalizedProviders.only.has(connection.integrationId)))) throw catalogResult.reason;
|
|
75298
|
-
const reason = catalogResult.reason;
|
|
75299
|
-
console.warn(`[@mastra/connect] Platform catalog unavailable (${reason instanceof Error ? reason.message : String(reason)}); resolving checked-in providers only.`);
|
|
75300
|
-
return {
|
|
75301
|
-
connections,
|
|
75302
|
-
catalog: [],
|
|
75303
|
-
catalogAvailable: false
|
|
75304
|
-
};
|
|
75612
|
+
const state = acquireSharedState(projectId, ttlMs, options, normalizedProviders);
|
|
75613
|
+
const validateDynamicResourceId = (raw) => raw === void 0 || raw === null ? void 0 : validateResourceId(raw);
|
|
75614
|
+
const resolve = (ctx) => {
|
|
75615
|
+
if (typeof resourceIdOption === "function") return Promise.resolve(resourceIdOption(ctx)).then((raw) => resolveScope(state, validateDynamicResourceId(raw)));
|
|
75616
|
+
return resolveScope(state, staticResourceId);
|
|
75305
75617
|
};
|
|
75306
|
-
|
|
75307
|
-
|
|
75308
|
-
|
|
75309
|
-
|
|
75310
|
-
|
|
75311
|
-
|
|
75312
|
-
|
|
75313
|
-
try {
|
|
75314
|
-
const { connections, catalog, catalogAvailable } = await loadSnapshotInputs();
|
|
75315
|
-
const snapshot = await mapTools(connections, buildRequests(normalizedProviders, catalog, catalogAvailable), options, clientOptions, client, mcpClients, resolverId);
|
|
75316
|
-
cache = {
|
|
75317
|
-
snapshot,
|
|
75318
|
-
fetchedAt: Date.now()
|
|
75319
|
-
};
|
|
75320
|
-
lastFailureAt = void 0;
|
|
75321
|
-
return snapshot;
|
|
75322
|
-
} catch (error) {
|
|
75323
|
-
lastFailureAt = Date.now();
|
|
75324
|
-
throw error;
|
|
75325
|
-
} finally {
|
|
75326
|
-
inflight = void 0;
|
|
75327
|
-
}
|
|
75328
|
-
})();
|
|
75329
|
-
return inflight;
|
|
75330
|
-
};
|
|
75331
|
-
const resolve = async () => {
|
|
75332
|
-
if (cache && Date.now() - cache.fetchedAt < ttlMs) return cache.snapshot;
|
|
75333
|
-
if (cache) {
|
|
75334
|
-
const staleSnapshot = cache.snapshot;
|
|
75335
|
-
if (!(lastFailureAt !== void 0 && Date.now() - lastFailureAt < FAILURE_COOLDOWN_MS$1) && !inflight && !closing) {
|
|
75336
|
-
const staleFetchedAt = cache.fetchedAt;
|
|
75337
|
-
refresh().catch((error) => {
|
|
75338
|
-
console.warn(`[@mastra/connect] Keeping cached tools (fetched ${Date.now() - staleFetchedAt}ms ago); platform refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
75339
|
-
});
|
|
75340
|
-
}
|
|
75341
|
-
return staleSnapshot;
|
|
75342
|
-
}
|
|
75343
|
-
return refresh();
|
|
75344
|
-
};
|
|
75345
|
-
const resolver = Object.assign(resolve, {
|
|
75346
|
-
refresh,
|
|
75347
|
-
disconnect: () => {
|
|
75348
|
-
closing ??= (async () => {
|
|
75349
|
-
try {
|
|
75350
|
-
while (inflight) await inflight.catch(() => void 0);
|
|
75351
|
-
cache = void 0;
|
|
75352
|
-
const clients = Array.from(mcpClients.values(), (entry) => entry.client);
|
|
75353
|
-
mcpClients.clear();
|
|
75354
|
-
await Promise.allSettled(clients.map((mcp) => mcp.disconnect()));
|
|
75355
|
-
} finally {
|
|
75356
|
-
closing = void 0;
|
|
75357
|
-
}
|
|
75358
|
-
})();
|
|
75359
|
-
return closing;
|
|
75618
|
+
const resolver = finalizeResolver(Object.assign(resolve, promiseSurface(resolve), {
|
|
75619
|
+
refresh: () => {
|
|
75620
|
+
if (typeof resourceIdOption === "function") return Promise.resolve(resourceIdOption(void 0)).then((raw) => {
|
|
75621
|
+
const resourceId = validateDynamicResourceId(raw);
|
|
75622
|
+
return refreshScope(state, acquireScope(state, resourceId));
|
|
75623
|
+
});
|
|
75624
|
+
return refreshScope(state, acquireScope(state, staticResourceId));
|
|
75360
75625
|
},
|
|
75626
|
+
disconnect: () => disconnectState(state),
|
|
75361
75627
|
with: (extra) => withExtraTools(resolver, extra)
|
|
75628
|
+
}));
|
|
75629
|
+
return resolver;
|
|
75630
|
+
}
|
|
75631
|
+
/**
|
|
75632
|
+
* The Promise surface shared by every resolver shape. Each method resolves
|
|
75633
|
+
* fresh, so `await resolver` always reflects the current snapshot.
|
|
75634
|
+
*/
|
|
75635
|
+
function promiseSurface(resolve) {
|
|
75636
|
+
return {
|
|
75637
|
+
then: (onFulfilled, onRejected) => resolve().then(onFulfilled, onRejected),
|
|
75638
|
+
catch: (onRejected) => resolve().catch(onRejected),
|
|
75639
|
+
finally: (onFinally) => resolve().finally(onFinally)
|
|
75640
|
+
};
|
|
75641
|
+
}
|
|
75642
|
+
/**
|
|
75643
|
+
* Adds the `Symbol.toStringTag` the Promise interface requires. Done with
|
|
75644
|
+
* `defineProperty` because async functions carry a non-writable inherited
|
|
75645
|
+
* tag ('AsyncFunction') that a plain assignment would throw on.
|
|
75646
|
+
*/
|
|
75647
|
+
function finalizeResolver(resolver) {
|
|
75648
|
+
Object.defineProperty(resolver, Symbol.toStringTag, {
|
|
75649
|
+
value: "ToolsResolver",
|
|
75650
|
+
configurable: true
|
|
75362
75651
|
});
|
|
75363
75652
|
return resolver;
|
|
75364
75653
|
}
|
|
@@ -75367,8 +75656,9 @@ function tools(options = {}) {
|
|
|
75367
75656
|
* (and MCP clients) for at once. Callbacks are expected to produce a small,
|
|
75368
75657
|
* closed set of option shapes (one per role, say); the cap exists so a
|
|
75369
75658
|
* callback that accidentally derives options from unbounded per-request data
|
|
75370
|
-
* cannot
|
|
75371
|
-
*
|
|
75659
|
+
* cannot retain an unbounded number of resolver wrappers. Eviction is
|
|
75660
|
+
* least-recently-used; the separately bounded shared-state registry owns
|
|
75661
|
+
* transport cleanup so resource-only variants cannot disconnect each other.
|
|
75372
75662
|
*/
|
|
75373
75663
|
const MAX_OPTION_VARIANTS = 32;
|
|
75374
75664
|
/**
|
|
@@ -75383,15 +75673,6 @@ const MAX_OPTION_VARIANTS = 32;
|
|
|
75383
75673
|
function dynamicTools(getOptions) {
|
|
75384
75674
|
const variants = /* @__PURE__ */ new Map();
|
|
75385
75675
|
let closing;
|
|
75386
|
-
const disconnectEvicted = (entry) => {
|
|
75387
|
-
entry.resolver.disconnect().catch((error) => {
|
|
75388
|
-
console.warn(`[@mastra/connect] Failed to disconnect evicted tools variant: ${error instanceof Error ? error.message : String(error)}`);
|
|
75389
|
-
});
|
|
75390
|
-
};
|
|
75391
|
-
const releaseVariant = (entry) => {
|
|
75392
|
-
entry.inUse -= 1;
|
|
75393
|
-
if (entry.evicted && entry.inUse === 0) disconnectEvicted(entry);
|
|
75394
|
-
};
|
|
75395
75676
|
const acquireVariant = async (ctx) => {
|
|
75396
75677
|
if (closing) await closing;
|
|
75397
75678
|
const options = await getOptions(ctx);
|
|
@@ -75402,40 +75683,23 @@ function dynamicTools(getOptions) {
|
|
|
75402
75683
|
if (existing) {
|
|
75403
75684
|
variants.delete(key);
|
|
75404
75685
|
variants.set(key, existing);
|
|
75405
|
-
existing.inUse += 1;
|
|
75406
75686
|
return existing;
|
|
75407
75687
|
}
|
|
75408
|
-
const entry = {
|
|
75409
|
-
resolver: tools(options),
|
|
75410
|
-
inUse: 1,
|
|
75411
|
-
evicted: false
|
|
75412
|
-
};
|
|
75688
|
+
const entry = { resolver: tools(options) };
|
|
75413
75689
|
variants.set(key, entry);
|
|
75414
75690
|
if (variants.size > MAX_OPTION_VARIANTS) {
|
|
75415
|
-
const [oldestKey
|
|
75691
|
+
const [oldestKey] = variants.entries().next().value;
|
|
75416
75692
|
variants.delete(oldestKey);
|
|
75417
75693
|
console.warn(`[@mastra/connect] tools() options callback produced more than ${MAX_OPTION_VARIANTS} distinct options values; evicting the least recently used variant. Derive options from a small, closed set of inputs (e.g. roles), not per-request data.`);
|
|
75418
|
-
oldest.evicted = true;
|
|
75419
|
-
if (oldest.inUse === 0) disconnectEvicted(oldest);
|
|
75420
75694
|
}
|
|
75421
75695
|
return entry;
|
|
75422
75696
|
};
|
|
75423
75697
|
const resolve = async (ctx) => {
|
|
75424
|
-
|
|
75425
|
-
try {
|
|
75426
|
-
return await entry.resolver(ctx);
|
|
75427
|
-
} finally {
|
|
75428
|
-
releaseVariant(entry);
|
|
75429
|
-
}
|
|
75698
|
+
return (await acquireVariant(ctx)).resolver(ctx);
|
|
75430
75699
|
};
|
|
75431
|
-
const resolver = Object.assign(resolve, {
|
|
75700
|
+
const resolver = finalizeResolver(Object.assign(resolve, promiseSurface(resolve), {
|
|
75432
75701
|
refresh: async () => {
|
|
75433
|
-
|
|
75434
|
-
try {
|
|
75435
|
-
return await entry.resolver.refresh();
|
|
75436
|
-
} finally {
|
|
75437
|
-
releaseVariant(entry);
|
|
75438
|
-
}
|
|
75702
|
+
return (await acquireVariant(void 0)).resolver.refresh();
|
|
75439
75703
|
},
|
|
75440
75704
|
disconnect: () => {
|
|
75441
75705
|
closing ??= (async () => {
|
|
@@ -75450,7 +75714,7 @@ function dynamicTools(getOptions) {
|
|
|
75450
75714
|
return closing;
|
|
75451
75715
|
},
|
|
75452
75716
|
with: (extra) => withExtraTools(resolver, extra)
|
|
75453
|
-
});
|
|
75717
|
+
}));
|
|
75454
75718
|
return resolver;
|
|
75455
75719
|
}
|
|
75456
75720
|
/** Identity registry for non-serializable option values (e.g. a custom `client.fetch`). */
|
|
@@ -75500,7 +75764,7 @@ function withExtraTools(base, extra) {
|
|
|
75500
75764
|
...extraTools
|
|
75501
75765
|
};
|
|
75502
75766
|
};
|
|
75503
|
-
const resolver = Object.assign(resolve, {
|
|
75767
|
+
const resolver = finalizeResolver(Object.assign(resolve, promiseSurface(resolve), {
|
|
75504
75768
|
refresh: async () => {
|
|
75505
75769
|
const [baseTools, extraTools] = await Promise.all([base.refresh(), resolveExtra()]);
|
|
75506
75770
|
return {
|
|
@@ -75510,7 +75774,7 @@ function withExtraTools(base, extra) {
|
|
|
75510
75774
|
},
|
|
75511
75775
|
disconnect: () => base.disconnect(),
|
|
75512
75776
|
with: (more) => withExtraTools(resolver, more)
|
|
75513
|
-
});
|
|
75777
|
+
}));
|
|
75514
75778
|
return resolver;
|
|
75515
75779
|
}
|
|
75516
75780
|
/**
|
|
@@ -75616,9 +75880,19 @@ function buildRequests(providers, catalog, catalogAvailable) {
|
|
|
75616
75880
|
}
|
|
75617
75881
|
return requests;
|
|
75618
75882
|
}
|
|
75619
|
-
/**
|
|
75883
|
+
/**
|
|
75884
|
+
* Maps one platform connection list snapshot to a flat tool record without
|
|
75885
|
+
* allowing ambiguous tool ownership. Returns the `${integrationId}::${connectionId}`
|
|
75886
|
+
* keys the snapshot's MCP tools depend on; the caller prunes clients no scope
|
|
75887
|
+
* references anymore.
|
|
75888
|
+
*/
|
|
75620
75889
|
async function mapTools(connections, requests, options, clientOptions, client, mcpClients, resolverId) {
|
|
75621
75890
|
const byIntegrationId = groupByIntegrationId(connections);
|
|
75891
|
+
const globalClient = clientOptions.resourceId === void 0 ? client : resolveClient({
|
|
75892
|
+
...clientOptions,
|
|
75893
|
+
resourceId: void 0
|
|
75894
|
+
});
|
|
75895
|
+
const clientFor = (connectionId) => connections.find((connection) => connection.id === connectionId)?.resourceId == null ? globalClient : client;
|
|
75622
75896
|
const activeMcpKeys = /* @__PURE__ */ new Set();
|
|
75623
75897
|
const result = {};
|
|
75624
75898
|
const toolOwners = /* @__PURE__ */ new Map();
|
|
@@ -75635,27 +75909,28 @@ async function mapTools(connections, requests, options, clientOptions, client, m
|
|
|
75635
75909
|
connectionId: request.options.connectionId
|
|
75636
75910
|
}, candidates);
|
|
75637
75911
|
if (resolution.kind === "skip") continue;
|
|
75638
|
-
if (resolution.kind === "single")
|
|
75639
|
-
|
|
75640
|
-
|
|
75641
|
-
|
|
75642
|
-
|
|
75643
|
-
|
|
75644
|
-
|
|
75645
|
-
|
|
75646
|
-
|
|
75647
|
-
|
|
75648
|
-
|
|
75649
|
-
|
|
75650
|
-
|
|
75651
|
-
|
|
75652
|
-
|
|
75653
|
-
|
|
75654
|
-
|
|
75655
|
-
|
|
75656
|
-
|
|
75657
|
-
|
|
75658
|
-
|
|
75912
|
+
if (resolution.kind === "single") {
|
|
75913
|
+
if (request.registration.transport === "mcp") {
|
|
75914
|
+
activeMcpKeys.add(`${integrationId}::${resolution.connectionId}`);
|
|
75915
|
+
providerTools = await discoverMcpTools({
|
|
75916
|
+
registration: request.registration,
|
|
75917
|
+
connectionId: resolution.connectionId,
|
|
75918
|
+
allowTools: request.options.allowTools,
|
|
75919
|
+
disallowTools: request.options.disallowTools,
|
|
75920
|
+
requireApproval: request.options.requireApproval,
|
|
75921
|
+
client: clientFor(resolution.connectionId),
|
|
75922
|
+
mcpClients,
|
|
75923
|
+
resolverId
|
|
75924
|
+
});
|
|
75925
|
+
} else {
|
|
75926
|
+
providerTools = request.registration.createTools({
|
|
75927
|
+
connectionId: resolution.connectionId,
|
|
75928
|
+
client: clientOptions,
|
|
75929
|
+
...providerFilterOptions(request.options)
|
|
75930
|
+
});
|
|
75931
|
+
applyProxyRequireApproval(providerTools, integrationId, request.options.requireApproval);
|
|
75932
|
+
}
|
|
75933
|
+
} else if (request.registration.transport === "mcp") {
|
|
75659
75934
|
for (const connection of resolution.connections) activeMcpKeys.add(`${integrationId}::${connection.id}`);
|
|
75660
75935
|
providerTools = await buildMcpMultiConnectionTools({
|
|
75661
75936
|
registration: request.registration,
|
|
@@ -75664,6 +75939,7 @@ async function mapTools(connections, requests, options, clientOptions, client, m
|
|
|
75664
75939
|
disallowTools: request.options.disallowTools,
|
|
75665
75940
|
requireApproval: request.options.requireApproval,
|
|
75666
75941
|
client,
|
|
75942
|
+
globalClient,
|
|
75667
75943
|
mcpClients,
|
|
75668
75944
|
resolverId
|
|
75669
75945
|
});
|
|
@@ -75702,10 +75978,10 @@ async function mapTools(connections, requests, options, clientOptions, client, m
|
|
|
75702
75978
|
];
|
|
75703
75979
|
for (const entry of defaultEntries) if (!matchedDefaultEntries.has(entry)) console.warn(`[@mastra/connect] Top-level entry '${entry}' matched no tools on any resolved provider.`);
|
|
75704
75980
|
}
|
|
75705
|
-
|
|
75706
|
-
|
|
75707
|
-
|
|
75708
|
-
|
|
75981
|
+
return {
|
|
75982
|
+
record: result,
|
|
75983
|
+
mcpKeys: activeMcpKeys
|
|
75984
|
+
};
|
|
75709
75985
|
}
|
|
75710
75986
|
/**
|
|
75711
75987
|
* Applies the top-level default filter to a provider that set no filter of
|
|
@@ -76092,11 +76368,503 @@ function selectChannelConnection(integrationId, pinnedId, candidates) {
|
|
|
76092
76368
|
/**
|
|
76093
76369
|
* Fetches the raw credential for a connection, for building custom
|
|
76094
76370
|
* interactions (own SDKs, MCP servers). Never log the result.
|
|
76371
|
+
*
|
|
76372
|
+
* Pass `resourceId` when the connection is scoped to an end user: the call
|
|
76373
|
+
* then asserts `x-mastra-resource-id`, which the platform requires for
|
|
76374
|
+
* resource-scoped connections (mismatch or omission → not found).
|
|
76095
76375
|
*/
|
|
76096
76376
|
async function credential(connectionId, options) {
|
|
76097
|
-
return getCredential(resolveClient(options?.
|
|
76377
|
+
return getCredential(resolveClient(options?.resourceId !== void 0 ? {
|
|
76378
|
+
...options?.client,
|
|
76379
|
+
resourceId: options.resourceId
|
|
76380
|
+
} : options?.client), connectionId);
|
|
76381
|
+
}
|
|
76382
|
+
//#endregion
|
|
76383
|
+
//#region src/user-connect-page.ts
|
|
76384
|
+
function escapeHtml(value) {
|
|
76385
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
76386
|
+
}
|
|
76387
|
+
/**
|
|
76388
|
+
* Serializes data for embedding inside an inline `<script>`: escapes `<` so a
|
|
76389
|
+
* value containing `<\/script>` cannot break out of the tag, and the JS line
|
|
76390
|
+
* separators that are invalid inside string literals.
|
|
76391
|
+
*/
|
|
76392
|
+
function toScriptJson(value) {
|
|
76393
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
76394
|
+
}
|
|
76395
|
+
/** Only https URLs are rendered as a logo; anything else is dropped. */
|
|
76396
|
+
function safeLogoUrl(logoUrl) {
|
|
76397
|
+
if (!logoUrl) return null;
|
|
76398
|
+
try {
|
|
76399
|
+
const parsed = new URL(logoUrl);
|
|
76400
|
+
return parsed.protocol === "https:" ? parsed.href : null;
|
|
76401
|
+
} catch {
|
|
76402
|
+
return null;
|
|
76403
|
+
}
|
|
76404
|
+
}
|
|
76405
|
+
function renderConnectPage(params) {
|
|
76406
|
+
const displayName = params.provider.displayName?.trim() || params.provider.id;
|
|
76407
|
+
const logoUrl = safeLogoUrl(params.provider.logoUrl);
|
|
76408
|
+
const data = toScriptJson({
|
|
76409
|
+
basePath: params.basePath,
|
|
76410
|
+
providerId: params.provider.id,
|
|
76411
|
+
displayName,
|
|
76412
|
+
returnTo: params.returnTo
|
|
76413
|
+
});
|
|
76414
|
+
return `<!doctype html>
|
|
76415
|
+
<html lang="en">
|
|
76416
|
+
<head>
|
|
76417
|
+
<meta charset="utf-8" />
|
|
76418
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
76419
|
+
<meta name="robots" content="noindex" />
|
|
76420
|
+
<title>Connect ${escapeHtml(displayName)}</title>
|
|
76421
|
+
<style>
|
|
76422
|
+
:root { color-scheme: light dark; }
|
|
76423
|
+
body { font-family: system-ui, -apple-system, sans-serif; display: flex; min-height: 100vh; margin: 0; align-items: center; justify-content: center; background: Canvas; color: CanvasText; }
|
|
76424
|
+
main { text-align: center; padding: 2rem; max-width: 26rem; }
|
|
76425
|
+
img.logo { width: 56px; height: 56px; border-radius: 12px; object-fit: contain; }
|
|
76426
|
+
h1 { font-size: 1.25rem; margin: 1rem 0 0.5rem; }
|
|
76427
|
+
p { color: GrayText; font-size: 0.9rem; line-height: 1.4; }
|
|
76428
|
+
button { font: inherit; padding: 0.6rem 1.6rem; border-radius: 8px; border: 1px solid GrayText; background: ButtonFace; color: ButtonText; cursor: pointer; }
|
|
76429
|
+
button:disabled { opacity: 0.6; cursor: default; }
|
|
76430
|
+
#status { min-height: 1.5rem; margin-top: 1rem; }
|
|
76431
|
+
#status.error { color: #c0392b; }
|
|
76432
|
+
a { color: inherit; }
|
|
76433
|
+
</style>
|
|
76434
|
+
</head>
|
|
76435
|
+
<body>
|
|
76436
|
+
<main>
|
|
76437
|
+
${logoUrl ? `<img class="logo" src="${escapeHtml(logoUrl)}" alt="" />` : ""}
|
|
76438
|
+
<h1>Connect ${escapeHtml(displayName)}</h1>
|
|
76439
|
+
<p>You'll be sent to ${escapeHtml(displayName)} to authorize access. This window stays open until the connection is ready.</p>
|
|
76440
|
+
<button id="connect" type="button">Connect ${escapeHtml(displayName)}</button>
|
|
76441
|
+
<p id="status" role="status"></p>
|
|
76442
|
+
</main>
|
|
76443
|
+
<script>
|
|
76444
|
+
(function () {
|
|
76445
|
+
var data = ${data};
|
|
76446
|
+
var button = document.getElementById('connect');
|
|
76447
|
+
var status = document.getElementById('status');
|
|
76448
|
+
var POLL_INTERVAL_MS = 2000;
|
|
76449
|
+
var MAX_POLLS = 150; // 5 minutes
|
|
76450
|
+
|
|
76451
|
+
function setStatus(text, isError) {
|
|
76452
|
+
status.textContent = text;
|
|
76453
|
+
status.className = isError ? 'error' : '';
|
|
76454
|
+
}
|
|
76455
|
+
|
|
76456
|
+
function finish(connectionId) {
|
|
76457
|
+
if (data.returnTo) {
|
|
76458
|
+
var sep = data.returnTo.indexOf('?') === -1 ? '?' : '&';
|
|
76459
|
+
window.location.assign(
|
|
76460
|
+
data.returnTo + sep + 'connectionId=' + encodeURIComponent(connectionId) + '&status=active',
|
|
76461
|
+
);
|
|
76462
|
+
return;
|
|
76463
|
+
}
|
|
76464
|
+
setStatus('Connected. You can close this window.', false);
|
|
76465
|
+
}
|
|
76466
|
+
|
|
76467
|
+
function poll(connectionId, popup, attempt) {
|
|
76468
|
+
if (attempt >= MAX_POLLS) {
|
|
76469
|
+
button.disabled = false;
|
|
76470
|
+
setStatus('Timed out waiting for the connection. Try again.', true);
|
|
76471
|
+
return;
|
|
76472
|
+
}
|
|
76473
|
+
fetch(data.basePath + '/connections/' + encodeURIComponent(connectionId) + '/status', {
|
|
76474
|
+
credentials: 'same-origin',
|
|
76475
|
+
headers: { accept: 'application/json' },
|
|
76476
|
+
})
|
|
76477
|
+
.then(function (res) {
|
|
76478
|
+
// A pending connection may briefly be invisible; keep polling on 404.
|
|
76479
|
+
if (res.status === 404) return { status: 'pending' };
|
|
76480
|
+
if (!res.ok) throw new Error('status check failed');
|
|
76481
|
+
return res.json();
|
|
76482
|
+
})
|
|
76483
|
+
.then(function (body) {
|
|
76484
|
+
if (body.status === 'active') {
|
|
76485
|
+
if (popup && !popup.closed) popup.close();
|
|
76486
|
+
finish(connectionId);
|
|
76487
|
+
return;
|
|
76488
|
+
}
|
|
76489
|
+
if (body.status === 'error') {
|
|
76490
|
+
button.disabled = false;
|
|
76491
|
+
setStatus('The connection failed. Try again.', true);
|
|
76492
|
+
return;
|
|
76493
|
+
}
|
|
76494
|
+
setTimeout(function () {
|
|
76495
|
+
poll(connectionId, popup, attempt + 1);
|
|
76496
|
+
}, POLL_INTERVAL_MS);
|
|
76497
|
+
})
|
|
76498
|
+
.catch(function () {
|
|
76499
|
+
setTimeout(function () {
|
|
76500
|
+
poll(connectionId, popup, attempt + 1);
|
|
76501
|
+
}, POLL_INTERVAL_MS);
|
|
76502
|
+
});
|
|
76503
|
+
}
|
|
76504
|
+
|
|
76505
|
+
button.addEventListener('click', function () {
|
|
76506
|
+
button.disabled = true;
|
|
76507
|
+
setStatus('Starting…', false);
|
|
76508
|
+
// Open the popup synchronously inside the click gesture so popup blockers
|
|
76509
|
+
// allow it; it is navigated once the session exists.
|
|
76510
|
+
var popup = window.open('about:blank', 'mastra-connect', 'width=600,height=700');
|
|
76511
|
+
fetch(data.basePath + '/' + encodeURIComponent(data.providerId) + '/sessions', {
|
|
76512
|
+
method: 'POST',
|
|
76513
|
+
credentials: 'same-origin',
|
|
76514
|
+
headers: { accept: 'application/json' },
|
|
76515
|
+
})
|
|
76516
|
+
.then(function (res) {
|
|
76517
|
+
if (!res.ok) throw new Error('session creation failed');
|
|
76518
|
+
return res.json();
|
|
76519
|
+
})
|
|
76520
|
+
.then(function (session) {
|
|
76521
|
+
if (popup && !popup.closed) {
|
|
76522
|
+
popup.location = session.connectUrl;
|
|
76523
|
+
setStatus('Finish authorizing in the popup…', false);
|
|
76524
|
+
} else {
|
|
76525
|
+
// Popup blocked: offer a manual link instead of auto-navigating away.
|
|
76526
|
+
status.textContent = '';
|
|
76527
|
+
var link = document.createElement('a');
|
|
76528
|
+
link.href = session.connectUrl;
|
|
76529
|
+
link.target = '_blank';
|
|
76530
|
+
link.rel = 'noopener';
|
|
76531
|
+
link.textContent = 'Popup blocked — click here to authorize';
|
|
76532
|
+
status.appendChild(link);
|
|
76533
|
+
}
|
|
76534
|
+
poll(session.connectionId, popup, 0);
|
|
76535
|
+
})
|
|
76536
|
+
.catch(function () {
|
|
76537
|
+
if (popup && !popup.closed) popup.close();
|
|
76538
|
+
button.disabled = false;
|
|
76539
|
+
setStatus('Could not start the connection. Try again.', true);
|
|
76540
|
+
});
|
|
76541
|
+
});
|
|
76542
|
+
})();
|
|
76543
|
+
<\/script>
|
|
76544
|
+
</body>
|
|
76545
|
+
</html>`;
|
|
76546
|
+
}
|
|
76547
|
+
//#endregion
|
|
76548
|
+
//#region src/user-connect-routes.ts
|
|
76549
|
+
/** Route paths that can never be used as a provider id (they collide with static segments). */
|
|
76550
|
+
const RESERVED_PROVIDER_IDS = /* @__PURE__ */ new Set([
|
|
76551
|
+
"providers",
|
|
76552
|
+
"connections",
|
|
76553
|
+
"sessions"
|
|
76554
|
+
]);
|
|
76555
|
+
const NO_STORE = "no-store, private";
|
|
76556
|
+
function errorResponse(c, error) {
|
|
76557
|
+
c.header("cache-control", NO_STORE);
|
|
76558
|
+
return c.json({ error: {
|
|
76559
|
+
code: error.code,
|
|
76560
|
+
message: error.message
|
|
76561
|
+
} }, error.status);
|
|
76562
|
+
}
|
|
76563
|
+
/**
|
|
76564
|
+
* Maps a platform-call failure to a safe end-user response. Platform detail
|
|
76565
|
+
* strings never reach the end user — this is an unauthenticated-adjacent
|
|
76566
|
+
* surface, and the details describe the developer's platform account.
|
|
76567
|
+
*/
|
|
76568
|
+
function toRouteError(error) {
|
|
76569
|
+
if (error instanceof MastraConnectError) {
|
|
76570
|
+
if (error.code === "connection_not_found") return {
|
|
76571
|
+
status: 404,
|
|
76572
|
+
code: "connection_not_found",
|
|
76573
|
+
message: "Connection not found."
|
|
76574
|
+
};
|
|
76575
|
+
if (error.code === "missing_access_token" || error.code === "missing_project_id" || error.code === "invalid_options") return {
|
|
76576
|
+
status: 500,
|
|
76577
|
+
code: "misconfigured",
|
|
76578
|
+
message: "Connect routes are not configured correctly."
|
|
76579
|
+
};
|
|
76580
|
+
return {
|
|
76581
|
+
status: 502,
|
|
76582
|
+
code: "platform_error",
|
|
76583
|
+
message: "Platform request failed."
|
|
76584
|
+
};
|
|
76585
|
+
}
|
|
76586
|
+
return {
|
|
76587
|
+
status: 500,
|
|
76588
|
+
code: "internal_error",
|
|
76589
|
+
message: "Unexpected error."
|
|
76590
|
+
};
|
|
76591
|
+
}
|
|
76592
|
+
function logRouteError(c, error) {
|
|
76593
|
+
try {
|
|
76594
|
+
const logger = c.get?.("mastra")?.getLogger?.();
|
|
76595
|
+
if (logger?.error) {
|
|
76596
|
+
logger.error(`[userConnectRoutes] ${error instanceof Error ? error.message : String(error)}`);
|
|
76597
|
+
return;
|
|
76598
|
+
}
|
|
76599
|
+
} catch {}
|
|
76600
|
+
console.error("[userConnectRoutes]", error instanceof Error ? error.message : error);
|
|
76601
|
+
}
|
|
76602
|
+
const UNAUTHORIZED = {
|
|
76603
|
+
status: 401,
|
|
76604
|
+
code: "unauthorized",
|
|
76605
|
+
message: "No end-user identity for this request. Configure server.auth with mapUserToResourceId, or pass a resourceId callback to userConnectRoutes()."
|
|
76606
|
+
};
|
|
76607
|
+
/** Fixed base used to prove a relative `returnTo` cannot escape the origin. */
|
|
76608
|
+
const RETURN_TO_BASE = "https://relative.invalid";
|
|
76609
|
+
/**
|
|
76610
|
+
* Validates a `returnTo` redirect target. Relative paths (`/done`) are always
|
|
76611
|
+
* allowed; protocol-relative (`//host`), backslash tricks, and non-http(s)
|
|
76612
|
+
* schemes are rejected; absolute URLs require their origin to be allowlisted.
|
|
76613
|
+
*/
|
|
76614
|
+
function validateReturnTo(raw, allowedOrigins) {
|
|
76615
|
+
if (raw === void 0 || raw === "") return {
|
|
76616
|
+
ok: true,
|
|
76617
|
+
value: null
|
|
76618
|
+
};
|
|
76619
|
+
if (raw.startsWith("/")) {
|
|
76620
|
+
if (raw.startsWith("//") || raw.startsWith("/\\")) return { ok: false };
|
|
76621
|
+
let resolved;
|
|
76622
|
+
try {
|
|
76623
|
+
resolved = new URL(raw, RETURN_TO_BASE);
|
|
76624
|
+
} catch {
|
|
76625
|
+
return { ok: false };
|
|
76626
|
+
}
|
|
76627
|
+
if (resolved.origin !== RETURN_TO_BASE) return { ok: false };
|
|
76628
|
+
if (resolved.pathname.startsWith("//")) return { ok: false };
|
|
76629
|
+
return {
|
|
76630
|
+
ok: true,
|
|
76631
|
+
value: `${resolved.pathname}${resolved.search}${resolved.hash}`
|
|
76632
|
+
};
|
|
76633
|
+
}
|
|
76634
|
+
let parsed;
|
|
76635
|
+
try {
|
|
76636
|
+
parsed = new URL(raw);
|
|
76637
|
+
} catch {
|
|
76638
|
+
return { ok: false };
|
|
76639
|
+
}
|
|
76640
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return { ok: false };
|
|
76641
|
+
if (parsed.username || parsed.password) return { ok: false };
|
|
76642
|
+
if (!allowedOrigins.includes(parsed.origin)) return { ok: false };
|
|
76643
|
+
return {
|
|
76644
|
+
ok: true,
|
|
76645
|
+
value: parsed.href
|
|
76646
|
+
};
|
|
76647
|
+
}
|
|
76648
|
+
function connectionScope(connection) {
|
|
76649
|
+
return connection.resourceId == null ? "global" : "user";
|
|
76650
|
+
}
|
|
76651
|
+
/**
|
|
76652
|
+
* Builds the mountable end-user connect surface as `ApiRoute[]` for
|
|
76653
|
+
* `server.apiRoutes`. The routes do no authentication of their own — the
|
|
76654
|
+
* server's auth pipeline authenticates the request and
|
|
76655
|
+
* `server.auth.mapUserToResourceId` establishes the end-user resource key the
|
|
76656
|
+
* routes are gated on. Every platform call happens server-side with the
|
|
76657
|
+
* configured platform token; the browser only ever talks to these routes.
|
|
76658
|
+
*
|
|
76659
|
+
* ```ts
|
|
76660
|
+
* server: {
|
|
76661
|
+
* auth: myAuth,
|
|
76662
|
+
* apiRoutes: [...userConnectRoutes({ providers: ['linear', 'slack'] })],
|
|
76663
|
+
* }
|
|
76664
|
+
* ```
|
|
76665
|
+
*/
|
|
76666
|
+
function userConnectRoutes(options) {
|
|
76667
|
+
if (!Array.isArray(options.providers) || options.providers.length === 0) throw new MastraConnectError("invalid_options", "userConnectRoutes requires a non-empty providers allowlist: the end-user surface never defaults open.");
|
|
76668
|
+
const providers = options.providers.map((provider) => {
|
|
76669
|
+
const id = typeof provider === "string" ? provider.trim().toLowerCase() : "";
|
|
76670
|
+
if (!id) throw new MastraConnectError("invalid_options", "Invalid provider id in userConnectRoutes providers allowlist.");
|
|
76671
|
+
if (RESERVED_PROVIDER_IDS.has(id) || id.includes("/")) throw new MastraConnectError("invalid_options", `Invalid provider id '${id}': reserved route segment.`);
|
|
76672
|
+
return id;
|
|
76673
|
+
});
|
|
76674
|
+
const allowlist = new Set(providers);
|
|
76675
|
+
const basePath = options.basePath ?? "/connect";
|
|
76676
|
+
if (!basePath.startsWith("/") || basePath.endsWith("/") || basePath.includes(":") || basePath.includes("*")) throw new MastraConnectError("invalid_options", `Invalid basePath '${basePath}': expected an absolute path without a trailing slash or route parameters.`);
|
|
76677
|
+
const projectId = options.projectId?.trim() || process.env.MASTRA_PROJECT_ID?.trim();
|
|
76678
|
+
if (!projectId) throw new MastraConnectError("missing_project_id", "Missing project id: set MASTRA_PROJECT_ID or pass projectId.");
|
|
76679
|
+
if (options.client?.resourceId !== void 0) throw new MastraConnectError("invalid_options", "userConnectRoutes derives the resource key per request; do not set client.resourceId.");
|
|
76680
|
+
if (options.resourceId !== void 0 && typeof options.resourceId !== "function") throw new MastraConnectError("invalid_options", "Invalid resourceId: userConnectRoutes expects a callback deriving the key per request.");
|
|
76681
|
+
const allowedOrigins = options.returnTo?.allowedOrigins ?? [];
|
|
76682
|
+
/** Resolves the caller's resource key or null when the request carries no usable identity. */
|
|
76683
|
+
const identify = async (c) => {
|
|
76684
|
+
const requestContext = c.get?.("requestContext");
|
|
76685
|
+
let raw;
|
|
76686
|
+
if (options.resourceId) try {
|
|
76687
|
+
raw = await options.resourceId({
|
|
76688
|
+
requestContext,
|
|
76689
|
+
user: requestContext?.get("user")
|
|
76690
|
+
});
|
|
76691
|
+
} catch (error) {
|
|
76692
|
+
logRouteError(c, error);
|
|
76693
|
+
return null;
|
|
76694
|
+
}
|
|
76695
|
+
else raw = requestContext?.get(MASTRA_RESOURCE_ID_KEY);
|
|
76696
|
+
if (typeof raw !== "string" || raw.trim() === "") return null;
|
|
76697
|
+
try {
|
|
76698
|
+
return validateResourceId(raw);
|
|
76699
|
+
} catch (error) {
|
|
76700
|
+
logRouteError(c, error);
|
|
76701
|
+
return null;
|
|
76702
|
+
}
|
|
76703
|
+
};
|
|
76704
|
+
/**
|
|
76705
|
+
* Wraps a handler with the shared gate: resolve the caller identity (401
|
|
76706
|
+
* when absent), resolve a resource-scoped platform client, set no-store,
|
|
76707
|
+
* and map failures to safe responses.
|
|
76708
|
+
*/
|
|
76709
|
+
const gated = (handler) => async (c) => {
|
|
76710
|
+
c.header("cache-control", NO_STORE);
|
|
76711
|
+
const resourceId = await identify(c);
|
|
76712
|
+
if (resourceId === null) return errorResponse(c, UNAUTHORIZED);
|
|
76713
|
+
let client;
|
|
76714
|
+
try {
|
|
76715
|
+
client = resolveClient({
|
|
76716
|
+
...options.client,
|
|
76717
|
+
resourceId
|
|
76718
|
+
});
|
|
76719
|
+
} catch (error) {
|
|
76720
|
+
logRouteError(c, error);
|
|
76721
|
+
return errorResponse(c, toRouteError(error));
|
|
76722
|
+
}
|
|
76723
|
+
try {
|
|
76724
|
+
return await handler(c, client, resourceId);
|
|
76725
|
+
} catch (error) {
|
|
76726
|
+
logRouteError(c, error);
|
|
76727
|
+
return errorResponse(c, toRouteError(error));
|
|
76728
|
+
}
|
|
76729
|
+
};
|
|
76730
|
+
/** Lists the caller's visible connections (global + own), filtered to the allowlist. */
|
|
76731
|
+
const listVisibleConnections = async (client) => {
|
|
76732
|
+
return (await listProjectConnections(client, projectId)).filter((connection) => allowlist.has(connection.integrationId.toLowerCase()));
|
|
76733
|
+
};
|
|
76734
|
+
const providerParam = (c) => {
|
|
76735
|
+
const provider = String(c.req.param("provider") ?? "").trim().toLowerCase();
|
|
76736
|
+
return allowlist.has(provider) ? provider : null;
|
|
76737
|
+
};
|
|
76738
|
+
const notFound = (c, code, message) => errorResponse(c, {
|
|
76739
|
+
status: 404,
|
|
76740
|
+
code,
|
|
76741
|
+
message
|
|
76742
|
+
});
|
|
76743
|
+
return [
|
|
76744
|
+
{
|
|
76745
|
+
path: `${basePath}/providers`,
|
|
76746
|
+
method: "GET",
|
|
76747
|
+
requiresAuth: true,
|
|
76748
|
+
handler: gated(async (c, client) => {
|
|
76749
|
+
const catalog = await listIntegrations(client);
|
|
76750
|
+
const byId = new Map(catalog.map((entry) => [entry.id.toLowerCase(), entry]));
|
|
76751
|
+
const visible = providers.flatMap((id) => {
|
|
76752
|
+
const entry = byId.get(id);
|
|
76753
|
+
if (!entry) return [];
|
|
76754
|
+
return [{
|
|
76755
|
+
id: entry.id,
|
|
76756
|
+
displayName: entry.displayName ?? null,
|
|
76757
|
+
logoUrl: entry.logoUrl ?? null,
|
|
76758
|
+
authType: entry.authType ?? null,
|
|
76759
|
+
authFields: entry.authFields ?? null
|
|
76760
|
+
}];
|
|
76761
|
+
});
|
|
76762
|
+
return c.json({ providers: visible });
|
|
76763
|
+
})
|
|
76764
|
+
},
|
|
76765
|
+
{
|
|
76766
|
+
path: `${basePath}/connections`,
|
|
76767
|
+
method: "GET",
|
|
76768
|
+
requiresAuth: true,
|
|
76769
|
+
handler: gated(async (c, client) => {
|
|
76770
|
+
const connections = await listVisibleConnections(client);
|
|
76771
|
+
return c.json({ connections: connections.map((connection) => ({
|
|
76772
|
+
id: connection.id,
|
|
76773
|
+
integrationId: connection.integrationId,
|
|
76774
|
+
status: connection.status,
|
|
76775
|
+
accountLabel: connection.accountLabel ?? null,
|
|
76776
|
+
scope: connectionScope(connection)
|
|
76777
|
+
})) });
|
|
76778
|
+
})
|
|
76779
|
+
},
|
|
76780
|
+
{
|
|
76781
|
+
path: `${basePath}/connections/:id/status`,
|
|
76782
|
+
method: "GET",
|
|
76783
|
+
requiresAuth: true,
|
|
76784
|
+
handler: gated(async (c, client) => {
|
|
76785
|
+
const id = String(c.req.param("id") ?? "");
|
|
76786
|
+
const connection = (await listVisibleConnections(client)).find((candidate) => candidate.id === id);
|
|
76787
|
+
if (!connection) return notFound(c, "connection_not_found", "Connection not found.");
|
|
76788
|
+
return c.json({
|
|
76789
|
+
id: connection.id,
|
|
76790
|
+
status: connection.status,
|
|
76791
|
+
scope: connectionScope(connection)
|
|
76792
|
+
});
|
|
76793
|
+
})
|
|
76794
|
+
},
|
|
76795
|
+
{
|
|
76796
|
+
path: `${basePath}/connections/:id`,
|
|
76797
|
+
method: "DELETE",
|
|
76798
|
+
requiresAuth: true,
|
|
76799
|
+
handler: gated(async (c, client) => {
|
|
76800
|
+
const id = String(c.req.param("id") ?? "");
|
|
76801
|
+
const connection = (await listVisibleConnections(client)).find((candidate) => candidate.id === id);
|
|
76802
|
+
if (!connection) return notFound(c, "connection_not_found", "Connection not found.");
|
|
76803
|
+
if (connectionScope(connection) === "global") return errorResponse(c, {
|
|
76804
|
+
status: 403,
|
|
76805
|
+
code: "forbidden",
|
|
76806
|
+
message: "Global connections cannot be disconnected from the end-user surface."
|
|
76807
|
+
});
|
|
76808
|
+
await disconnectConnection(client, connection.id);
|
|
76809
|
+
return c.body(null, 204);
|
|
76810
|
+
})
|
|
76811
|
+
},
|
|
76812
|
+
{
|
|
76813
|
+
path: `${basePath}/:provider/sessions`,
|
|
76814
|
+
method: "POST",
|
|
76815
|
+
requiresAuth: true,
|
|
76816
|
+
handler: gated(async (c, client) => {
|
|
76817
|
+
const provider = providerParam(c);
|
|
76818
|
+
if (!provider) return notFound(c, "provider_not_found", "Provider not available.");
|
|
76819
|
+
const session = await createProjectConnectSession(client, projectId, provider);
|
|
76820
|
+
return c.json({
|
|
76821
|
+
connectionId: session.connectionId,
|
|
76822
|
+
integrationId: session.integrationId,
|
|
76823
|
+
connectUrl: session.connectUrl,
|
|
76824
|
+
sessionToken: session.sessionToken,
|
|
76825
|
+
expiresAt: session.expiresAt
|
|
76826
|
+
}, 201);
|
|
76827
|
+
})
|
|
76828
|
+
},
|
|
76829
|
+
{
|
|
76830
|
+
path: `${basePath}/:provider`,
|
|
76831
|
+
method: "GET",
|
|
76832
|
+
requiresAuth: true,
|
|
76833
|
+
handler: gated(async (c, client) => {
|
|
76834
|
+
const provider = providerParam(c);
|
|
76835
|
+
if (!provider) return notFound(c, "provider_not_found", "Provider not available.");
|
|
76836
|
+
const returnTo = validateReturnTo(c.req.query("returnTo") ?? void 0, allowedOrigins);
|
|
76837
|
+
if (!returnTo.ok) return errorResponse(c, {
|
|
76838
|
+
status: 400,
|
|
76839
|
+
code: "invalid_return_to",
|
|
76840
|
+
message: "Invalid returnTo: use a relative path, or allowlist the origin via returnTo.allowedOrigins."
|
|
76841
|
+
});
|
|
76842
|
+
const entry = (await listIntegrations(client)).find((candidate) => candidate.id.toLowerCase() === provider);
|
|
76843
|
+
if (!entry) return notFound(c, "provider_not_found", "Provider not available.");
|
|
76844
|
+
c.header("content-security-policy", [
|
|
76845
|
+
"default-src 'none'",
|
|
76846
|
+
"script-src 'unsafe-inline'",
|
|
76847
|
+
"style-src 'unsafe-inline'",
|
|
76848
|
+
"img-src https:",
|
|
76849
|
+
"connect-src 'self'",
|
|
76850
|
+
"base-uri 'none'",
|
|
76851
|
+
"form-action 'none'",
|
|
76852
|
+
"frame-ancestors 'none'"
|
|
76853
|
+
].join("; "));
|
|
76854
|
+
return c.html(renderConnectPage({
|
|
76855
|
+
basePath,
|
|
76856
|
+
provider: {
|
|
76857
|
+
id: entry.id,
|
|
76858
|
+
displayName: entry.displayName,
|
|
76859
|
+
logoUrl: entry.logoUrl
|
|
76860
|
+
},
|
|
76861
|
+
returnTo: returnTo.value
|
|
76862
|
+
}));
|
|
76863
|
+
})
|
|
76864
|
+
}
|
|
76865
|
+
];
|
|
76098
76866
|
}
|
|
76099
76867
|
//#endregion
|
|
76100
|
-
export { CHANNELS, MastraConnectError, PROVIDERS, channels, credential, tools };
|
|
76868
|
+
export { CHANNELS, MastraConnectError, PROVIDERS, channels, credential, tools, userConnectRoutes };
|
|
76101
76869
|
|
|
76102
76870
|
//# sourceMappingURL=index.js.map
|