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