@mastra/connect 0.3.1-alpha.0 → 0.4.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -343,6 +343,106 @@ async function proxyRequestWithResponse(client, connectionId, options) {
343
343
  }
344
344
  }
345
345
  //#endregion
346
+ //#region src/providers/channels.ts
347
+ function credentialToken$1(credential) {
348
+ return credential.type === "oauth2" ? credential.accessToken : credential.apiKey;
349
+ }
350
+ function missingPeerError(integrationId, packageName, error) {
351
+ return new MastraConnectError("invalid_options", `channels() cannot build '${integrationId}' provider: install '${packageName}' as a dependency of your app (${error instanceof Error ? error.message : String(error)}).`);
352
+ }
353
+ const RESERVED_OPTION_KEYS = {
354
+ slack: [
355
+ "baseUrl",
356
+ "refreshToken",
357
+ "token",
358
+ "encryptionKey"
359
+ ],
360
+ telegram: [
361
+ "baseUrl",
362
+ "apiBaseUrl",
363
+ "botToken",
364
+ "encryptionKey"
365
+ ],
366
+ discord: ["baseUrl", "encryptionKey"]
367
+ };
368
+ function stripReservedOptions(integrationId, options) {
369
+ if (!options) return options;
370
+ const reserved = RESERVED_OPTION_KEYS[integrationId];
371
+ if (!reserved) return options;
372
+ const stripped = [];
373
+ const filtered = {};
374
+ for (const [key, value] of Object.entries(options)) {
375
+ if (reserved.includes(key)) {
376
+ stripped.push(key);
377
+ continue;
378
+ }
379
+ filtered[key] = value;
380
+ }
381
+ if (stripped.length > 0) console.warn(`[@mastra/connect] ${integrationId} channel: ignoring reserved providerOptions field(s): ${stripped.map((k) => `'${k}'`).join(", ")}. These are managed by the connection credential or the Mastra server config.`);
382
+ return filtered;
383
+ }
384
+ const CHANNELS$1 = [
385
+ {
386
+ integrationId: "slack",
387
+ async build(credential, options) {
388
+ const refreshToken = credentialToken$1(credential);
389
+ let mod;
390
+ try {
391
+ mod = await import("@mastra/slack");
392
+ } catch (error) {
393
+ throw missingPeerError("slack", "@mastra/slack", error);
394
+ }
395
+ const safeOptions = stripReservedOptions("slack", options);
396
+ return new mod.SlackProvider({
397
+ refreshToken,
398
+ ...safeOptions ?? {}
399
+ });
400
+ }
401
+ },
402
+ {
403
+ integrationId: "telegram",
404
+ async build(credential, options) {
405
+ const botToken = credentialToken$1(credential);
406
+ let mod;
407
+ try {
408
+ mod = await import("@mastra/telegram");
409
+ } catch (error) {
410
+ throw missingPeerError("telegram", "@mastra/telegram", error);
411
+ }
412
+ const safeOptions = stripReservedOptions("telegram", options);
413
+ return new mod.TelegramProvider({
414
+ botToken,
415
+ ...safeOptions ?? {}
416
+ });
417
+ }
418
+ },
419
+ {
420
+ integrationId: "discord",
421
+ async build(credential, options, context) {
422
+ const botToken = credentialToken$1(credential);
423
+ const metadata = context?.context?.metadata ?? {};
424
+ const applicationId = options?.applicationId ?? (typeof metadata.applicationId === "string" ? metadata.applicationId : void 0) ?? (typeof metadata.application_id === "string" ? metadata.application_id : void 0);
425
+ const publicKey = options?.publicKey ?? (typeof metadata.publicKey === "string" ? metadata.publicKey : void 0) ?? (typeof metadata.public_key === "string" ? metadata.public_key : void 0);
426
+ let mod;
427
+ try {
428
+ mod = await import("@mastra/discord");
429
+ } catch (error) {
430
+ throw missingPeerError("discord", "@mastra/discord", error);
431
+ }
432
+ if (!applicationId || !publicKey) console.warn(`[@mastra/connect] discord channel: missing ${[!applicationId && "applicationId", !publicKey && "publicKey"].filter(Boolean).join(" + ")} on the connection. Store them on the connection's metadata or pass them via integrations.discord.providerOptions.`);
433
+ const { applicationId: _optAppId, publicKey: _optPubKey, ...rest } = options ?? {};
434
+ return new mod.DiscordProvider({
435
+ app: {
436
+ botToken,
437
+ applicationId,
438
+ publicKey
439
+ },
440
+ ...rest
441
+ });
442
+ }
443
+ }
444
+ ];
445
+ //#endregion
346
446
  //#region src/runtime/platform-proxy.ts
347
447
  /** Maps the platform credential to the field names templates are written against. */
348
448
  function toTemplateCredentials(credential) {
@@ -65718,7 +65818,7 @@ function createWorkosTools(options) {
65718
65818
  * Providers with checked-in HTTP toolsets. MCP-backed providers are discovered
65719
65819
  * from the Platform integration catalog at runtime.
65720
65820
  */
65721
- const PROVIDERS = [
65821
+ const TOOLS = [
65722
65822
  anthropicProvider,
65723
65823
  clerkProvider,
65724
65824
  discordProvider,
@@ -65745,18 +65845,84 @@ const PROVIDERS = [
65745
65845
  createTools: createWorkosTools
65746
65846
  }
65747
65847
  ];
65848
+ /**
65849
+ * @deprecated Use `TOOLS`. This alias is retained for one release cycle so
65850
+ * downstream consumers keep working; it will be removed in the next minor.
65851
+ */
65852
+ const PROVIDERS = TOOLS;
65748
65853
  function findRegistration(integrationId) {
65749
- return PROVIDERS.find((p) => p.integrationId === integrationId);
65854
+ return TOOLS.find((p) => p.integrationId === integrationId);
65855
+ }
65856
+ /**
65857
+ * Channel-capable providers with a hand-maintained `channels()` registration.
65858
+ * The launch ships three (Slack, Telegram, Discord); the provider generator
65859
+ * grows to cover new entries when a fourth lands.
65860
+ */
65861
+ const CHANNELS = CHANNELS$1;
65862
+ function findChannelRegistration(integrationId) {
65863
+ return CHANNELS.find((c) => c.integrationId === integrationId);
65750
65864
  }
65751
65865
  //#endregion
65752
- //#region src/connect.ts
65753
- const DEFAULT_TTL_MS = 3e4;
65866
+ //#region src/resolution.ts
65867
+ const INTEGRATION_ID_PATTERN = /^[a-zA-Z0-9_-]{1,128}$/;
65868
+ function validateIntegrationOverrides(integrations) {
65869
+ for (const integrationId of Object.keys(integrations ?? {})) if (!INTEGRATION_ID_PATTERN.test(integrationId)) throw new MastraConnectError("invalid_options", `Invalid provider '${integrationId}' in integrations option: expected 1-128 letters, numbers, underscores, or hyphens.`);
65870
+ }
65871
+ function connectionIdEnvVar(integrationId) {
65872
+ return `MASTRA_${integrationId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_CONNECTION_ID`;
65873
+ }
65874
+ function groupByIntegrationId(connections) {
65875
+ const byIntegrationId = /* @__PURE__ */ new Map();
65876
+ for (const connection of connections) {
65877
+ const list = byIntegrationId.get(connection.integrationId) ?? [];
65878
+ list.push(connection);
65879
+ byIntegrationId.set(connection.integrationId, list);
65880
+ }
65881
+ return byIntegrationId;
65882
+ }
65883
+ /**
65884
+ * Resolves the connection to use for one provider, per the contract:
65885
+ * option/env var wins; else a single active connection; anything else
65886
+ * (ambiguity, needs_reauth, no usable candidate) warns and skips so one bad
65887
+ * integration never takes down the whole resolution. A needs_reauth
65888
+ * connection is never silently mapped.
65889
+ */
65890
+ function resolveConnection(request, candidates) {
65891
+ const integrationId = request.integrationId;
65892
+ const directed = request.connectionId?.trim() || process.env[request.envVar]?.trim() || void 0;
65893
+ if (directed) {
65894
+ const match = candidates.find((connection) => connection.id === directed);
65895
+ if (!match) {
65896
+ console.warn(`[@mastra/connect] Skipping ${integrationId}: pinned connection ${directed} is not attached to this project.`);
65897
+ return;
65898
+ }
65899
+ if (match.status === "needs_reauth") {
65900
+ console.warn(`[@mastra/connect] Skipping ${integrationId}: connection ${directed} needs re-auth.`);
65901
+ return;
65902
+ }
65903
+ if (match.status !== "active") {
65904
+ console.warn(`[@mastra/connect] Skipping ${integrationId}: connection ${directed} is not active (status '${match.status}').`);
65905
+ return;
65906
+ }
65907
+ return directed;
65908
+ }
65909
+ const active = candidates.filter((connection) => connection.status === "active");
65910
+ if (active.length === 1) return active[0].id;
65911
+ if (active.length === 0) {
65912
+ console.warn(`[@mastra/connect] Skipping ${integrationId}: no active connections (found ${candidates.length} in other states).`);
65913
+ return;
65914
+ }
65915
+ console.warn(`[@mastra/connect] Skipping ${integrationId}: ${active.length} active connections; pin one with connectionId or ${request.envVar}.`);
65916
+ }
65917
+ //#endregion
65918
+ //#region src/tools.ts
65919
+ const DEFAULT_TTL_MS$1 = 3e4;
65754
65920
  /** Minimum wait after a failed platform fetch before another background revalidation. */
65755
- const FAILURE_COOLDOWN_MS = 3e4;
65921
+ const FAILURE_COOLDOWN_MS$1 = 3e4;
65756
65922
  let nextResolverId = 0;
65757
65923
  /**
65758
65924
  * Returns a live toolset resolver over the project's Platform connections.
65759
- * HTTP providers are loaded from the shipped `PROVIDERS` registry. MCP providers
65925
+ * HTTP providers are loaded from the shipped `TOOLS` registry. MCP providers
65760
65926
  * are discovered from the Platform integration catalog. Tools from every supported
65761
65927
  * provider with a matching project connection are merged into one flat record
65762
65928
  * (matched by `integrationId`). The resolver serves a cached snapshot,
@@ -65770,11 +65936,11 @@ let nextResolverId = 0;
65770
65936
  * problems during resolution (needs re-auth or ambiguity) are downgraded to
65771
65937
  * warn-and-skip so one bad integration never takes down the whole toolset.
65772
65938
  */
65773
- function connect(options = {}) {
65939
+ function tools(options = {}) {
65774
65940
  const projectId = options.projectId?.trim() || process.env.MASTRA_PROJECT_ID?.trim();
65775
65941
  if (!projectId) throw new MastraConnectError("missing_project_id", "Missing project id: set MASTRA_PROJECT_ID or pass projectId.");
65776
65942
  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.`);
65777
- const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
65943
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS$1;
65778
65944
  const client = resolveClient(options.client);
65779
65945
  const resolverId = ++nextResolverId;
65780
65946
  const mcpClients = /* @__PURE__ */ new Map();
@@ -65796,7 +65962,7 @@ function connect(options = {}) {
65796
65962
  connections,
65797
65963
  catalog: catalogResult.value
65798
65964
  };
65799
- const checkedIn = new Set(PROVIDERS.map((registration) => registration.integrationId));
65965
+ const checkedIn = new Set(TOOLS.map((registration) => registration.integrationId));
65800
65966
  if (connections.some((connection) => connection.status === "active" && !checkedIn.has(connection.integrationId) && !options.integrations?.[connection.integrationId]?.disabled)) throw catalogResult.reason;
65801
65967
  const reason = catalogResult.reason;
65802
65968
  console.warn(`[@mastra/connect] Platform catalog unavailable (${reason instanceof Error ? reason.message : String(reason)}); resolving checked-in providers only.`);
@@ -65834,7 +66000,7 @@ function connect(options = {}) {
65834
66000
  if (cache && Date.now() - cache.fetchedAt < ttlMs) return cache.snapshot;
65835
66001
  if (cache) {
65836
66002
  const staleSnapshot = cache.snapshot;
65837
- if (!(lastFailureAt !== void 0 && Date.now() - lastFailureAt < FAILURE_COOLDOWN_MS) && !inflight && !closing) {
66003
+ if (!(lastFailureAt !== void 0 && Date.now() - lastFailureAt < FAILURE_COOLDOWN_MS$1) && !inflight && !closing) {
65838
66004
  const staleFetchedAt = cache.fetchedAt;
65839
66005
  refresh().catch((error) => {
65840
66006
  console.warn(`[@mastra/connect] Keeping cached tools (fetched ${Date.now() - staleFetchedAt}ms ago); platform refresh failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -65865,16 +66031,9 @@ function connect(options = {}) {
65865
66031
  }
65866
66032
  });
65867
66033
  }
65868
- function validateIntegrationOverrides(integrations) {
65869
- const integrationIdPattern = /^[a-zA-Z0-9_-]{1,128}$/;
65870
- for (const integrationId of Object.keys(integrations ?? {})) if (!integrationIdPattern.test(integrationId)) throw new MastraConnectError("invalid_options", `Invalid provider '${integrationId}' in integrations option: expected 1-128 letters, numbers, underscores, or hyphens.`);
65871
- }
65872
- function connectionIdEnvVar(integrationId) {
65873
- return `MASTRA_${integrationId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_CONNECTION_ID`;
65874
- }
65875
66034
  function buildRequests(integrations, catalog) {
65876
66035
  const overrides = integrations ?? {};
65877
- const registrations = new Map(PROVIDERS.map((registration) => [registration.integrationId, registration]));
66036
+ const registrations = new Map(TOOLS.map((registration) => [registration.integrationId, registration]));
65878
66037
  const catalogIds = new Set(catalog.map((integration) => integration.id));
65879
66038
  for (const integration of catalog) {
65880
66039
  if (!integration.capabilities.mcp) continue;
@@ -65908,7 +66067,11 @@ async function mapTools(connections, requests, options, client, mcpClients, reso
65908
66067
  try {
65909
66068
  const candidates = byIntegrationId.get(integrationId) ?? [];
65910
66069
  if (candidates.length === 0) continue;
65911
- const connectionId = resolveProviderConnection(request, candidates);
66070
+ const connectionId = resolveConnection({
66071
+ integrationId,
66072
+ envVar: request.registration.envVar,
66073
+ connectionId: request.options.connectionId
66074
+ }, candidates);
65912
66075
  if (!connectionId) continue;
65913
66076
  if (request.registration.transport === "mcp") {
65914
66077
  activeMcpIntegrations.add(integrationId);
@@ -65968,51 +66131,192 @@ async function discoverMcpTools(input) {
65968
66131
  if (unknown.length > 0) throw new MastraConnectError("invalid_options", `Unknown tool name(s) in autoApproveTools for '${registration.integrationId}': ${unknown.join(", ")}. Known tools: ${Object.keys(discovery.tools).join(", ")}.`);
65969
66132
  return applyAllowTools(discovery.tools, allowTools);
65970
66133
  }
65971
- function groupByIntegrationId(connections) {
65972
- const byIntegrationId = /* @__PURE__ */ new Map();
65973
- for (const connection of connections) {
65974
- const list = byIntegrationId.get(connection.integrationId) ?? [];
65975
- list.push(connection);
65976
- byIntegrationId.set(connection.integrationId, list);
65977
- }
65978
- return byIntegrationId;
66134
+ //#endregion
66135
+ //#region src/connect.ts
66136
+ /**
66137
+ * @deprecated Use `tools` from `@mastra/connect` instead. This module is a
66138
+ * thin re-export retained for one release cycle so downstream consumers keep
66139
+ * working; it will be removed in the next minor.
66140
+ */
66141
+ /**
66142
+ * @deprecated Renamed to {@link tools}.
66143
+ *
66144
+ * `connect(options)` is a drop-in alias for `tools(options)`. Prefer `tools`
66145
+ * from `@mastra/connect` in new code; this export will be removed in the next
66146
+ * minor version.
66147
+ */
66148
+ function connect(options = {}) {
66149
+ return tools(options);
65979
66150
  }
65980
- function readEnvConnectionId(registration) {
65981
- return process.env[registration.envVar]?.trim() || void 0;
66151
+ //#endregion
66152
+ //#region src/channels.ts
66153
+ const DEFAULT_TTL_MS = 3e4;
66154
+ /** Minimum wait after a failed platform fetch before another background revalidation. */
66155
+ const FAILURE_COOLDOWN_MS = 3e4;
66156
+ /**
66157
+ * Returns a live channels resolver over the project's Platform connections.
66158
+ * Channel-capable integrations (Slack, Telegram, Discord) with an active
66159
+ * project connection are materialized into `ChannelProvider` instances ready
66160
+ * to hand to `new Mastra({ channels: await channels({...}) })`.
66161
+ *
66162
+ * Configuration errors (missing project id, bad ttlMs, malformed integration
66163
+ * id) throw at call time so they surface at startup. Actionable per-integration
66164
+ * problems during resolution (needs re-auth, ambiguity, missing peer package)
66165
+ * are downgraded to warn-and-skip so one bad integration never takes down the
66166
+ * whole map.
66167
+ *
66168
+ * @example
66169
+ * ```ts
66170
+ * import { Mastra } from '@mastra/core/mastra';
66171
+ * import { channels } from '@mastra/connect';
66172
+ *
66173
+ * export const mastra = new Mastra({
66174
+ * agents: { pat },
66175
+ * channels: await channels({ projectId }),
66176
+ * });
66177
+ * ```
66178
+ */
66179
+ function channels(options = {}) {
66180
+ const projectId = options.projectId?.trim() || process.env.MASTRA_PROJECT_ID?.trim();
66181
+ if (!projectId) throw new MastraConnectError("missing_project_id", "Missing project id: set MASTRA_PROJECT_ID or pass projectId.");
66182
+ 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.`);
66183
+ validateIntegrationOverrides(options.integrations);
66184
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
66185
+ const client = resolveClient(options.client);
66186
+ let cache;
66187
+ let inflight;
66188
+ let lastFailureAt;
66189
+ const buildSnapshot = async () => {
66190
+ let connections;
66191
+ try {
66192
+ connections = await listProjectConnections(client, projectId);
66193
+ } catch (error) {
66194
+ lastFailureAt = Date.now();
66195
+ throw error;
66196
+ }
66197
+ const byIntegrationId = groupByIntegrationId(connections);
66198
+ const providers = {};
66199
+ for (const registration of CHANNELS) {
66200
+ const integrationId = registration.integrationId;
66201
+ const overrides = options.integrations?.[integrationId] ?? {};
66202
+ if (overrides.disabled) continue;
66203
+ const candidates = byIntegrationId.get(integrationId) ?? [];
66204
+ if (candidates.length === 0) continue;
66205
+ const connectionId = selectChannelConnection(integrationId, overrides.connectionId, candidates);
66206
+ if (!connectionId) continue;
66207
+ const provider = await buildProvider(registration, connectionId, overrides, client);
66208
+ if (!provider) continue;
66209
+ providers[integrationId] = provider;
66210
+ }
66211
+ lastFailureAt = void 0;
66212
+ return providers;
66213
+ };
66214
+ const refresh = () => {
66215
+ if (!inflight) inflight = (async () => {
66216
+ try {
66217
+ const providers = await buildSnapshot();
66218
+ cache = {
66219
+ providers,
66220
+ fetchedAt: Date.now()
66221
+ };
66222
+ return providers;
66223
+ } finally {
66224
+ inflight = void 0;
66225
+ }
66226
+ })();
66227
+ return inflight;
66228
+ };
66229
+ const resolve = async () => {
66230
+ if (cache && Date.now() - cache.fetchedAt < ttlMs) return cache.providers;
66231
+ if (cache) {
66232
+ const stale = cache.providers;
66233
+ if (!(lastFailureAt !== void 0 && Date.now() - lastFailureAt < FAILURE_COOLDOWN_MS) && !inflight) {
66234
+ const staleFetchedAt = cache.fetchedAt;
66235
+ refresh().catch((error) => {
66236
+ console.warn(`[@mastra/connect] Keeping cached channels (fetched ${Date.now() - staleFetchedAt}ms ago); platform refresh failed: ${error instanceof Error ? error.message : String(error)}`);
66237
+ });
66238
+ }
66239
+ return stale;
66240
+ }
66241
+ return refresh();
66242
+ };
66243
+ const invocable = ((_context) => resolve());
66244
+ invocable.invalidate = () => {
66245
+ cache = void 0;
66246
+ };
66247
+ invocable.refresh = refresh;
66248
+ invocable.disconnect = async () => {
66249
+ cache = void 0;
66250
+ };
66251
+ invocable.then = ((onfulfilled, onrejected) => resolve().then(onfulfilled, onrejected));
66252
+ return invocable;
66253
+ }
66254
+ async function buildProvider(registration, connectionId, overrides, client) {
66255
+ let credential;
66256
+ try {
66257
+ credential = await getCredential(client, connectionId);
66258
+ } catch (error) {
66259
+ console.warn(`[@mastra/connect] Skipping ${registration.integrationId} channel: ${error instanceof Error ? error.message : String(error)}`);
66260
+ return;
66261
+ }
66262
+ let context = {
66263
+ connectionId,
66264
+ client
66265
+ };
66266
+ try {
66267
+ context = {
66268
+ connectionId,
66269
+ client,
66270
+ context: await getConnectionContext(client, connectionId)
66271
+ };
66272
+ } catch {}
66273
+ try {
66274
+ return await registration.build(credential, overrides.providerOptions ?? {}, context);
66275
+ } catch (error) {
66276
+ console.warn(`[@mastra/connect] Skipping ${registration.integrationId} channel: ${error instanceof Error ? error.message : String(error)}`);
66277
+ return;
66278
+ }
65982
66279
  }
65983
66280
  /**
65984
- * Resolves the connection to use for one provider, per the contract:
65985
- * option/env var wins; else a single active connection; anything else
65986
- * (ambiguity, needs_reauth, no usable candidate) warns and skips so one bad
65987
- * integration never takes down the whole toolset resolution. A needs_reauth
65988
- * connection is never silently mapped.
66281
+ * Selects the connection `channels()` should use for one provider.
66282
+ *
66283
+ * - Pinned `connectionId` (from `integrations.<id>.connectionId`) wins, but is
66284
+ * skipped if the pinned id isn't attached to the project or the connection
66285
+ * isn't `active`.
66286
+ * - Otherwise, the single active connection is used.
66287
+ * - When more than one active connection exists, this warns naming the chosen
66288
+ * id and picks the first active connection. There is no env-var fallback;
66289
+ * pin explicitly with `integrations.<id>.connectionId` when the deterministic
66290
+ * choice matters.
65989
66291
  */
65990
- function resolveProviderConnection(request, candidates) {
65991
- const integrationId = request.registration.integrationId;
65992
- const directed = request.options.connectionId?.trim() || readEnvConnectionId(request.registration);
66292
+ function selectChannelConnection(integrationId, pinnedId, candidates) {
66293
+ const directed = pinnedId?.trim() || void 0;
65993
66294
  if (directed) {
65994
66295
  const match = candidates.find((connection) => connection.id === directed);
65995
66296
  if (!match) {
65996
- console.warn(`[@mastra/connect] Skipping ${integrationId}: pinned connection ${directed} is not attached to this project.`);
66297
+ console.warn(`[@mastra/connect] Skipping ${integrationId} channel: pinned connection ${directed} is not attached to this project.`);
65997
66298
  return;
65998
66299
  }
65999
66300
  if (match.status === "needs_reauth") {
66000
- console.warn(`[@mastra/connect] Skipping ${integrationId}: connection ${directed} needs re-auth.`);
66301
+ console.warn(`[@mastra/connect] Skipping ${integrationId} channel: connection ${directed} needs re-auth.`);
66001
66302
  return;
66002
66303
  }
66003
66304
  if (match.status !== "active") {
66004
- console.warn(`[@mastra/connect] Skipping ${integrationId}: connection ${directed} is not active (status '${match.status}').`);
66305
+ console.warn(`[@mastra/connect] Skipping ${integrationId} channel: connection ${directed} is not active (status '${match.status}').`);
66005
66306
  return;
66006
66307
  }
66007
66308
  return directed;
66008
66309
  }
66009
66310
  const active = candidates.filter((connection) => connection.status === "active");
66010
- if (active.length === 1) return active[0].id;
66011
66311
  if (active.length === 0) {
66012
- console.warn(`[@mastra/connect] Skipping ${integrationId}: no active connections (found ${candidates.length} in other states).`);
66312
+ console.warn(`[@mastra/connect] Skipping ${integrationId} channel: no active connections (found ${candidates.length} in other states).`);
66013
66313
  return;
66014
66314
  }
66015
- console.warn(`[@mastra/connect] Skipping ${integrationId}: ${active.length} active connections; pin one with connectionId or ${request.registration.envVar}.`);
66315
+ if (active.length === 1) return active[0].id;
66316
+ const chosen = active[0].id;
66317
+ const others = active.slice(1).map((connection) => connection.id).join(", ");
66318
+ console.warn(`[@mastra/connect] ${integrationId} channel: found ${active.length} active connections; using ${chosen}. Ignoring ${others}. Pin one with integrations.${integrationId}.connectionId to silence this warning.`);
66319
+ return chosen;
66016
66320
  }
66017
66321
  //#endregion
66018
66322
  //#region src/credential.ts
@@ -66024,13 +66328,121 @@ async function credential(connectionId, options) {
66024
66328
  return getCredential(resolveClient(options?.client), connectionId);
66025
66329
  }
66026
66330
  //#endregion
66331
+ //#region src/environment.ts
66332
+ function credentialToken(credential) {
66333
+ return credential.type === "oauth2" ? credential.accessToken : credential.apiKey;
66334
+ }
66335
+ /**
66336
+ * Wires git's credential helper to read the token from the environment at use
66337
+ * time, so the secret lives in the process env rather than in git config or
66338
+ * remote URLs. `gh` needs no setup: it honors GH_TOKEN directly.
66339
+ */
66340
+ const GITHUB_GIT_CREDENTIAL_HELPER = "!f() { echo username=x-access-token; echo \"password=$GH_TOKEN\"; }; f";
66341
+ /** Providers that know how to materialize sandbox credentials. */
66342
+ const ENVIRONMENT_CONTRIBUTORS = [{
66343
+ integrationId: "github",
66344
+ envVar: "MASTRA_GITHUB_CONNECTION_ID",
66345
+ build(credential) {
66346
+ const token = credentialToken(credential);
66347
+ return {
66348
+ env: {
66349
+ GH_TOKEN: token,
66350
+ GITHUB_TOKEN: token
66351
+ },
66352
+ onStart: async ({ sandbox }) => {
66353
+ if (!sandbox.executeCommand) {
66354
+ console.warn("[@mastra/connect] github environment: sandbox does not support executeCommand; skipping git credential setup (gh still works via GH_TOKEN).");
66355
+ return;
66356
+ }
66357
+ await sandbox.executeCommand("git", [
66358
+ "config",
66359
+ "--global",
66360
+ "credential.https://github.com.helper",
66361
+ GITHUB_GIT_CREDENTIAL_HELPER
66362
+ ]);
66363
+ }
66364
+ };
66365
+ }
66366
+ }];
66367
+ /**
66368
+ * Resolves the project's Platform connections into sandbox credentials:
66369
+ * environment variables plus a start hook that finishes in-sandbox setup.
66370
+ * Currently GitHub-aware (`GH_TOKEN`/`GITHUB_TOKEN` + git credential helper);
66371
+ * more providers plug in over time.
66372
+ *
66373
+ * Connection resolution matches `connect()`: a per-integration `connectionId`
66374
+ * or the provider's `MASTRA_*_CONNECTION_ID` env var wins, else a single
66375
+ * active connection; ambiguity, needs_reauth, and credential-fetch failures
66376
+ * warn and skip that provider. Configuration errors (missing project id,
66377
+ * malformed integration id) throw here so they surface at startup.
66378
+ *
66379
+ * The returned `env` is a snapshot: call `environment()` again for a fresh
66380
+ * token when starting new sandboxes.
66381
+ *
66382
+ * @example
66383
+ * const { env, onStart } = await environment({ projectId });
66384
+ * const sandbox = new E2BSandbox({ env, onStart });
66385
+ * // in the sandbox: `gh pr list`, `git clone https://github.com/org/repo` just work
66386
+ */
66387
+ async function environment(options = {}) {
66388
+ const projectId = options.projectId?.trim() || process.env.MASTRA_PROJECT_ID?.trim();
66389
+ if (!projectId) throw new MastraConnectError("missing_project_id", "Missing project id: set MASTRA_PROJECT_ID or pass projectId.");
66390
+ validateIntegrationOverrides(options.integrations);
66391
+ const client = resolveClient(options.client);
66392
+ const byIntegrationId = groupByIntegrationId(await listProjectConnections(client, projectId));
66393
+ const env = {};
66394
+ const hooks = [];
66395
+ for (const contributor of ENVIRONMENT_CONTRIBUTORS) {
66396
+ const integrationId = contributor.integrationId;
66397
+ const overrides = options.integrations?.[integrationId] ?? {};
66398
+ if (overrides.disabled) continue;
66399
+ const candidates = byIntegrationId.get(integrationId) ?? [];
66400
+ if (candidates.length === 0) continue;
66401
+ const connectionId = resolveConnection({
66402
+ integrationId,
66403
+ envVar: contributor.envVar,
66404
+ connectionId: overrides.connectionId
66405
+ }, candidates);
66406
+ if (!connectionId) continue;
66407
+ let credential;
66408
+ try {
66409
+ credential = await getCredential(client, connectionId);
66410
+ } catch (error) {
66411
+ console.warn(`[@mastra/connect] Skipping ${integrationId} environment: ${error instanceof Error ? error.message : String(error)}`);
66412
+ continue;
66413
+ }
66414
+ const contribution = contributor.build(credential);
66415
+ for (const [key, value] of Object.entries(contribution.env)) {
66416
+ if (key in env) console.warn(`[@mastra/connect] ${integrationId} environment overwrites env var '${key}' set by an earlier provider.`);
66417
+ env[key] = value;
66418
+ }
66419
+ if (contribution.onStart) hooks.push({
66420
+ integrationId,
66421
+ hook: contribution.onStart
66422
+ });
66423
+ }
66424
+ const onStart = async (args) => {
66425
+ for (const { hook } of hooks) await hook(args);
66426
+ };
66427
+ return {
66428
+ env,
66429
+ onStart
66430
+ };
66431
+ }
66432
+ //#endregion
66433
+ exports.CHANNELS = CHANNELS;
66027
66434
  exports.MastraConnectError = MastraConnectError;
66028
66435
  exports.PROVIDERS = PROVIDERS;
66436
+ exports.TOOLS = TOOLS;
66029
66437
  exports.applyAllowTools = applyAllowTools;
66438
+ exports.channels = channels;
66030
66439
  exports.connect = connect;
66031
66440
  exports.credential = credential;
66032
66441
  exports.defineProxyTool = defineProxyTool;
66442
+ exports.environment = environment;
66443
+ exports.findChannelRegistration = findChannelRegistration;
66033
66444
  exports.findRegistration = findRegistration;
66034
66445
  exports.resolveConnectionId = resolveConnectionId;
66446
+ exports.tools = tools;
66035
66447
 
66036
66448
  //# sourceMappingURL=index.cjs.map