@vercel/connect 2.5.7-snapshot-c4f338842961 → 2.6.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/README.md CHANGED
@@ -237,8 +237,14 @@ export default defineMcpClientConnection({
237
237
  By default, `connect()` only uses connectors already linked to the project and
238
238
  does not provision or modify connectors at runtime. Pass `autoProvision: true`
239
239
  to opt in. When enabled, `connect()` first tries the token or authorization
240
- request; if the connector is missing or not linked, it provisions or links the
241
- connector and retries the request once.
240
+ request; if the connector is missing or not linked, it asks Vercel Connect to
241
+ resolve the connector by its project-local UID (`'linear'` above) and retries
242
+ the request once with the returned connector id. Vercel Connect reuses a
243
+ connector already mapped to that UID or linked to the project, or creates one
244
+ linked to the deploying project. It never widens an existing link: when setup
245
+ is required (for example, enabling the connector for another environment), the
246
+ call fails with a non-retryable `connector_setup_required` authorization error.
247
+ Teams without project-local UIDs provision the connector by team UID instead.
242
248
 
243
249
  ### Better Auth
244
250
 
@@ -58,9 +58,10 @@ export type EveConnectionAuthorizationContext = Parameters<NonInteractiveAuthori
58
58
  export interface EveAuthorizationOptions {
59
59
  /**
60
60
  * Vercel Connect OAuth connector identifier. Accepts either the
61
- * opaque service connector key (`scl_...`) or the human-readable
62
- * UID (`oauth/mcp-linear-app`) — both resolve to the same
63
- * connector on the Vercel Connect side.
61
+ * opaque service connector key (`scl_...`) or a human-readable UID.
62
+ * When the team has project-local UIDs enabled, a UID is resolved first
63
+ * as the project-local UID of a connector linked to the deploying
64
+ * project, then as the connector's team UID.
64
65
  */
65
66
  readonly connector: string;
66
67
  /**
@@ -130,14 +131,23 @@ export interface EveAuthorizationOptions {
130
131
  * opt in to runtime provisioning.
131
132
  *
132
133
  * The provision request is authenticated with the deployment OIDC token
133
- * and carries the eve connection's `url` plus this connector UID. Connect
134
- * creates the managed OAuth connector when missing, links an existing
135
- * OAuth connector when the UID already exists, and scopes the new project
136
- * link to the OIDC token's authenticated environment.
134
+ * and carries the eve connection's `url` plus this connector UID as the
135
+ * project-local UID (`projectUid`). Connect returns the connector already
136
+ * mapped to that UID in the deploying project, reuses a compatible
137
+ * connector already linked to the project and environment, or creates a
138
+ * new connector linked to the project under that UID. Existing links are
139
+ * never widened; cases that need user-authorized setup fail with a
140
+ * non-retryable `connector_setup_required` authorization error. Teams
141
+ * without project-local UIDs fall back to provisioning the connector by
142
+ * team UID.
137
143
  *
138
- * Existing linked connectors are always used directly without a
139
- * provisioning request. Opaque connector ids (`scl_...`) and connections
140
- * without a URL are skipped automatically.
144
+ * For a few minutes after provisioning, token, authorization, revocation,
145
+ * and eviction calls address the connector by the id Connect returned,
146
+ * because a newly created connector may not resolve by UID until Connect's
147
+ * indexes catch up; afterwards they use the UID again. Existing linked
148
+ * connectors are used directly without a provisioning request. Opaque
149
+ * connector ids (`scl_...`) and connections without a URL are skipped
150
+ * automatically.
141
151
  */
142
152
  readonly autoProvision?: boolean;
143
153
  /**
@@ -224,7 +234,7 @@ export interface Experimental_EveEventsBackend {
224
234
  * Token-fetching identity continues to be driven by the
225
235
  * `getToken` / `startAuthorization` / `completeAuthorization`
226
236
  * callbacks. When auto-provisioning is enabled, the same connector value is
227
- * also sent as the managed OAuth UID.
237
+ * also sent as the managed OAuth project-local UID.
228
238
  */
229
239
  export interface VercelConnectMetadata {
230
240
  /** Live capability; compilers must omit this from serializable metadata. */
@@ -33,7 +33,7 @@ import { startAuthorization } from '../authorization.js';
33
33
  import { experimental_connectEventsAdapter, } from '../ai-sdk/connect-events-adapter.js';
34
34
  import { verifyMcpEvent } from '../mcp/verify-event.js';
35
35
  import { ConnectError, ConnectorInstallationRequiredError, deleteTokenCacheEntry, getTokenResponse, NoValidTokenError, revokeToken, UserAuthorizationRequiredError, } from '../token.js';
36
- import { provisionEveOAuthConnector } from './provision-oauth-connector.js';
36
+ import { ConnectorProvisioningInProgressError, ConnectorSetupRequiredError, findProvisionedConnector, forgetProvisionedConnector, getProvisionedConnector, getProvisionedConnectorIds, provisionEveOAuthConnector, resolveProvisionScope, } from './provision-oauth-connector.js';
37
37
  export function connect(input) {
38
38
  const options = normalizeAuthorizationOptions(input);
39
39
  const requirement = options.service === undefined
@@ -83,7 +83,7 @@ function makeEvict(options) {
83
83
  // token included) and clears the in-process cache. Best-effort —
84
84
  // a failed or duplicate revoke must not mask the auth error that
85
85
  // triggered eviction.
86
- await revokeToken(options.connector, {
86
+ await revokeToken(resolveRevocationConnector(options, connection), {
87
87
  subject: params.subject,
88
88
  installationId: params.installationId,
89
89
  }, options.connectOptions);
@@ -94,9 +94,33 @@ function makeEvict(options) {
94
94
  // gone even when the server-side revoke failed.
95
95
  }
96
96
  }
97
+ // Tokens fetched shortly after provisioning are cached under the
98
+ // provisioned connector id, possibly by another `connect()` definition
99
+ // for the same connector, so drop those entries too.
97
100
  deleteTokenCacheEntry(options.connector, params);
101
+ for (const id of getProvisionedConnectorIds(options.connector)) {
102
+ deleteTokenCacheEntry(id, params);
103
+ }
98
104
  };
99
105
  }
106
+ /**
107
+ * Picks the connector reference revocation should address, using the same
108
+ * policy as token retrieval: only `autoProvision` definitions address a
109
+ * recently provisioned id, so an opted-out definition never revokes a grant
110
+ * on a connector another definition provisioned. Without a connection, the
111
+ * id is looked up across server URLs and used only when unambiguous.
112
+ */
113
+ function resolveRevocationConnector(options, connection) {
114
+ if (options.autoProvision !== true) {
115
+ return options.connector;
116
+ }
117
+ if (connection === undefined) {
118
+ return (findProvisionedConnector(options.connector, options.connectOptions) ??
119
+ options.connector);
120
+ }
121
+ const scope = resolveProvisionScope(options.connector, connection, options.connectOptions);
122
+ return (scope && getProvisionedConnector(scope)) ?? options.connector;
123
+ }
100
124
  function buildEventsBackend(options) {
101
125
  return {
102
126
  verify: verifyMcpEvent,
@@ -138,7 +162,7 @@ function buildInteractiveDefinition(options) {
138
162
  async getToken({ principal, connection, }) {
139
163
  try {
140
164
  const params = await buildTokenParams(options, principal, connection);
141
- const response = await withConnectorProvisioningFallback(options, connection, () => getTokenResponse(options.connector, params, getTokenConnectOptions(options)));
165
+ const response = await withConnectorProvisioningFallback(options, connection, connector => getTokenResponse(connector, params, getTokenConnectOptions(options)));
142
166
  return { token: response.token, expiresAt: response.expiresAt };
143
167
  }
144
168
  catch (error) {
@@ -167,7 +191,7 @@ function buildInteractiveDefinition(options) {
167
191
  // work without an OAuth-style redirect-URI allowlist.
168
192
  const completionWebhook = connectCompletionWebhook(webhook);
169
193
  const params = await buildTokenParams(options, principal, connection);
170
- const response = await withConnectorProvisioningFallback(options, connection, () => startAuthorization(options.connector, params, {
194
+ const response = await withConnectorProvisioningFallback(options, connection, connector => startAuthorization(connector, params, {
171
195
  ...options.connectOptions,
172
196
  callbackUrl: callbackUrl ?? webhook,
173
197
  ...(completionWebhook ? { webhook: completionWebhook } : null),
@@ -202,7 +226,7 @@ function buildInteractiveDefinition(options) {
202
226
  async completeAuthorization({ principal, connection, }) {
203
227
  try {
204
228
  const params = await buildTokenParams(options, principal, connection);
205
- const response = await withConnectorProvisioningFallback(options, connection, () => getTokenResponse(options.connector, params, options.connectOptions));
229
+ const response = await withConnectorProvisioningFallback(options, connection, connector => getTokenResponse(connector, params, options.connectOptions));
206
230
  return { token: response.token, expiresAt: response.expiresAt };
207
231
  }
208
232
  catch (error) {
@@ -228,7 +252,7 @@ function buildNonInteractiveDefinition(options) {
228
252
  async getToken({ principal, connection, }) {
229
253
  try {
230
254
  const params = await buildTokenParams(options, principal, connection);
231
- const response = await withConnectorProvisioningFallback(options, connection, () => getTokenResponse(options.connector, params, getTokenConnectOptions(options)));
255
+ const response = await withConnectorProvisioningFallback(options, connection, connector => getTokenResponse(connector, params, getTokenConnectOptions(options)));
232
256
  return { token: response.token, expiresAt: response.expiresAt };
233
257
  }
234
258
  catch (error) {
@@ -238,21 +262,29 @@ function buildNonInteractiveDefinition(options) {
238
262
  };
239
263
  }
240
264
  async function withConnectorProvisioningFallback(options, connection, operation) {
265
+ const scope = options.autoProvision === true
266
+ ? resolveProvisionScope(options.connector, connection, options.connectOptions)
267
+ : undefined;
268
+ if (scope === undefined) {
269
+ return operation(options.connector);
270
+ }
271
+ const attempted = getProvisionedConnector(scope) ?? options.connector;
241
272
  try {
242
- return await operation();
273
+ return await operation(attempted);
243
274
  }
244
275
  catch (error) {
245
- if (options.autoProvision !== true ||
246
- !isMissingConnectorOrProjectLink(error)) {
276
+ if (!isMissingConnectorOrProjectLink(error)) {
247
277
  throw error;
248
278
  }
249
279
  }
250
- await provisionEveOAuthConnector({
251
- connector: options.connector,
252
- connection,
253
- connectOptions: options.connectOptions,
254
- });
255
- return operation();
280
+ forgetProvisionedConnector(scope, attempted);
281
+ // A concurrent caller may already have provisioned a replacement.
282
+ const current = getProvisionedConnector(scope);
283
+ if (current !== undefined && current !== attempted) {
284
+ return operation(current);
285
+ }
286
+ const provisioned = await provisionEveOAuthConnector(scope, options.connectOptions);
287
+ return operation(provisioned ?? options.connector);
256
288
  }
257
289
  function isMissingConnectorOrProjectLink(error) {
258
290
  if (!(error instanceof ConnectError)) {
@@ -354,6 +386,20 @@ function translate(error, phase, options) {
354
386
  message: error.message,
355
387
  });
356
388
  }
389
+ if (error instanceof ConnectorSetupRequiredError) {
390
+ return new ConnectionAuthorizationFailedError('connect', {
391
+ message: error.message,
392
+ reason: 'connector_setup_required',
393
+ retryable: false,
394
+ });
395
+ }
396
+ if (error instanceof ConnectorProvisioningInProgressError) {
397
+ return new ConnectionAuthorizationFailedError('connect', {
398
+ message: error.message,
399
+ reason: 'connector_provisioning_in_progress',
400
+ retryable: true,
401
+ });
402
+ }
357
403
  // `ConnectorInstallationRequiredError` is terminal in both modes:
358
404
  // the operator has not installed the Vercel Connect connector for
359
405
  // this team.
@@ -7,6 +7,7 @@
7
7
  * everything from `@vercel/connect/eve`.
8
8
  */
9
9
  export { connect, type EveAuthorizationInput, type EveAuthorizationOptions, type EveConnectAuthorizationDefinition, type EveConnectionAuthorizationContext, type ConnectAuthorizationPhase, type VercelConnectMetadata, type Experimental_EveEventsBackend, } from './connection-authorization.js';
10
+ export { ConnectorProvisioningInProgressError, ConnectorSetupRequiredError, } from './provision-oauth-connector.js';
10
11
  export { CONNECT_OAUTH_ISSUER, connectOAuth, type ConnectOAuthAudienceEnvironment, type ConnectOAuthEnvironment, type ConnectOAuthOptions, } from './connect-oauth.js';
11
12
  export { connectDiscordCredentials, type ConnectDiscordCredentialsParams, } from './discord-credentials.js';
12
13
  export { connectGitHubCredentials, type ConnectGitHubCredentialsParams, } from './github-credentials.js';
package/dist/eve/index.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * everything from `@vercel/connect/eve`.
8
8
  */
9
9
  export { connect, } from './connection-authorization.js';
10
+ export { ConnectorProvisioningInProgressError, ConnectorSetupRequiredError, } from './provision-oauth-connector.js';
10
11
  export { CONNECT_OAUTH_ISSUER, connectOAuth, } from './connect-oauth.js';
11
12
  export { connectDiscordCredentials, } from './discord-credentials.js';
12
13
  export { connectGitHubCredentials, } from './github-credentials.js';
@@ -1,9 +1,83 @@
1
- import type { ConnectOptions } from '../token.js';
1
+ import type { ConnectErrorOptions, ConnectOptions } from '../token.js';
2
+ import { ConnectError } from '../token.js';
2
3
  import type { EveConnectionAuthorizationContext } from './connection-authorization.js';
3
- interface ProvisionEveOAuthConnectorOptions {
4
+ /**
5
+ * Vercel Connect could not resolve the connection's project UID to a
6
+ * connector without user-authorized setup, such as enabling the connector
7
+ * for this environment or choosing between ambiguous connectors. Retrying
8
+ * the request does not help until the setup is done.
9
+ */
10
+ export declare class ConnectorSetupRequiredError extends ConnectError {
11
+ /** Machine-readable cause reported by Vercel Connect. */
12
+ readonly reason?: string;
13
+ /**
14
+ * Connector the setup applies to, when Vercel Connect identifies one,
15
+ * such as a connector that was removed from the project and can be
16
+ * reconnected.
17
+ */
18
+ readonly connectorId?: string;
19
+ constructor(message: string, options?: ConnectErrorOptions & {
20
+ readonly reason?: string;
21
+ readonly connectorId?: string;
22
+ });
23
+ }
24
+ /** Another request is provisioning the same connector; retry shortly. */
25
+ export declare class ConnectorProvisioningInProgressError extends ConnectError {
26
+ constructor(message: string, options?: ConnectErrorOptions);
27
+ }
28
+ /**
29
+ * Identifies one provisionable connector reference: the authored UID, the
30
+ * connection's server URL, the Vercel API it is provisioned against, and an
31
+ * explicitly configured Vercel token. The ambient deployment OIDC token is
32
+ * not part of the scope: it always belongs to the same project, and leaving
33
+ * it out keeps lookups synchronous so cached token reads need no OIDC token.
34
+ */
35
+ export interface ProvisionScope {
36
+ readonly key: string;
4
37
  readonly connector: string;
5
- readonly connection: EveConnectionAuthorizationContext;
6
- readonly connectOptions?: ConnectOptions;
38
+ readonly serverUrl: string;
39
+ readonly endpoint: string;
40
+ readonly vercelToken: string | undefined;
7
41
  }
8
- export declare function provisionEveOAuthConnector({ connector, connection, connectOptions, }: ProvisionEveOAuthConnectorOptions): Promise<void>;
9
- export {};
42
+ /**
43
+ * Returns the provisioning scope for a connector reference, or `undefined`
44
+ * when it cannot be provisioned (opaque ids, reserved UIDs, connections
45
+ * without a URL).
46
+ */
47
+ export declare function resolveProvisionScope(connector: string, connection: EveConnectionAuthorizationContext | undefined, connectOptions: ConnectOptions | undefined): ProvisionScope | undefined;
48
+ /**
49
+ * Returns the connector id provisioning recently resolved for `scope`, if
50
+ * any. Synchronous and free of network or OIDC lookups.
51
+ */
52
+ export declare function getProvisionedConnector(scope: ProvisionScope): string | undefined;
53
+ /**
54
+ * Returns the connector id recently provisioned for `connector` against the
55
+ * same Vercel API and explicitly configured Vercel token, when exactly one
56
+ * server URL has one. Used to target revocation when the caller did not pass
57
+ * the connection; ambiguous matches return `undefined` so the caller falls
58
+ * back to the UID rather than guessing.
59
+ */
60
+ export declare function findProvisionedConnector(connector: string, connectOptions: ConnectOptions | undefined): string | undefined;
61
+ /**
62
+ * Returns every live connector id provisioned for the authored `connector`
63
+ * across scopes, so eviction can drop token cache entries stored under them
64
+ * without knowing the connection.
65
+ */
66
+ export declare function getProvisionedConnectorIds(connector: string): string[];
67
+ /**
68
+ * Drops `id` for `scope` after Vercel Connect rejected it. Leaves the entry
69
+ * alone when another caller has already replaced it.
70
+ */
71
+ export declare function forgetProvisionedConnector(scope: ProvisionScope, id: string): void;
72
+ /**
73
+ * Resolves or provisions the managed OAuth connector `scope` refers to and
74
+ * returns its connector id. The authored connector is sent as the
75
+ * project-local UID (`projectUid`), so Vercel Connect resolves it within the
76
+ * deployment's project and environment rather than across the team. Teams
77
+ * without project UIDs fall back to provisioning by team-global UID.
78
+ *
79
+ * The returned id is remembered for {@link getProvisionedConnector}.
80
+ * Returns `undefined` for non-OAuth connectors or when Vercel Connect does
81
+ * not report an id.
82
+ */
83
+ export declare function provisionEveOAuthConnector(scope: ProvisionScope, connectOptions: ConnectOptions | undefined): Promise<string | undefined>;
@@ -6,37 +6,186 @@ const RESERVED_UID_PATTERN = /^(vc\/|[^/]*\.vercel\.com\/)/;
6
6
  const ALLOWED_RESERVED_UID_PREFIXES = ['mcp.vercel.com/'];
7
7
  const RESERVED_ID_PREFIXES = ['scl_', 'sca_', 'store_', 'ir_'];
8
8
  const INVALID_UID_CHARS = /[\s%#]/;
9
- const provisionCache = new Map();
10
- export async function provisionEveOAuthConnector({ connector, connection, connectOptions, }) {
11
- const serverUrl = resolveServerUrl(connection);
9
+ /**
10
+ * Vercel Connect could not resolve the connection's project UID to a
11
+ * connector without user-authorized setup, such as enabling the connector
12
+ * for this environment or choosing between ambiguous connectors. Retrying
13
+ * the request does not help until the setup is done.
14
+ */
15
+ export class ConnectorSetupRequiredError extends ConnectError {
16
+ /** Machine-readable cause reported by Vercel Connect. */
17
+ reason;
18
+ /**
19
+ * Connector the setup applies to, when Vercel Connect identifies one,
20
+ * such as a connector that was removed from the project and can be
21
+ * reconnected.
22
+ */
23
+ connectorId;
24
+ constructor(message, options = {}) {
25
+ super(message, options);
26
+ this.name = 'ConnectorSetupRequiredError';
27
+ this.reason = options.reason;
28
+ this.connectorId = options.connectorId;
29
+ }
30
+ }
31
+ /** Another request is provisioning the same connector; retry shortly. */
32
+ export class ConnectorProvisioningInProgressError extends ConnectError {
33
+ constructor(message, options) {
34
+ super(message, options);
35
+ this.name = 'ConnectorProvisioningInProgressError';
36
+ }
37
+ }
38
+ /**
39
+ * How long a provisioned connector id is preferred over its UID. Vercel
40
+ * Connect resolves UIDs through eventually consistent indexes, so a connector
41
+ * created moments ago may not resolve by UID yet; the opaque id does. Once
42
+ * the indexes have caught up the UID resolves to the same connector, so
43
+ * dropping back to it picks up later changes such as a user remapping the
44
+ * UID to another connector.
45
+ */
46
+ const PROVISIONED_CONNECTOR_TTL_MS = 5 * 60 * 1000;
47
+ const MAX_PROVISIONED_CONNECTORS = 100;
48
+ /** Provisioned connector ids by {@link ProvisionScope.key}. */
49
+ const provisionedConnectors = new Map();
50
+ /**
51
+ * In-flight provisioning requests by {@link ProvisionScope.key}. Concurrent
52
+ * callers share one request; entries are dropped as soon as the request
53
+ * settles.
54
+ */
55
+ const inFlightProvisioning = new Map();
56
+ /**
57
+ * Returns the provisioning scope for a connector reference, or `undefined`
58
+ * when it cannot be provisioned (opaque ids, reserved UIDs, connections
59
+ * without a URL).
60
+ */
61
+ export function resolveProvisionScope(connector, connection, connectOptions) {
62
+ const serverUrl = connection === undefined ? undefined : resolveServerUrl(connection);
12
63
  if (serverUrl === undefined || !isProvisionableConnectorUid(connector)) {
13
- return;
64
+ return undefined;
14
65
  }
15
- const vercelToken = connectOptions?.vercelToken ?? (await getVercelOidcToken());
16
66
  const endpoint = `${resolveBaseUrl(connectOptions)}${MANAGED_OAUTH_CONNECTOR_PATH}`;
17
- const cacheKey = JSON.stringify({
67
+ const key = JSON.stringify({
18
68
  connector,
19
69
  serverUrl,
20
70
  endpoint,
21
- token: await tokenCacheKeyPart(vercelToken),
71
+ token: connectOptions?.vercelToken ?? null,
22
72
  });
23
- let promise = provisionCache.get(cacheKey);
24
- if (promise === undefined) {
25
- promise = provisionManagedOAuthConnector({
26
- connector,
27
- serverUrl,
28
- vercelToken,
29
- endpoint,
30
- }).catch(error => {
31
- if (isNonOAuthConnectorConflict(error)) {
32
- return;
33
- }
34
- provisionCache.delete(cacheKey);
35
- throw error;
36
- });
37
- provisionCache.set(cacheKey, promise);
73
+ return {
74
+ key,
75
+ connector,
76
+ serverUrl,
77
+ endpoint,
78
+ vercelToken: connectOptions?.vercelToken,
79
+ };
80
+ }
81
+ /**
82
+ * Returns the connector id provisioning recently resolved for `scope`, if
83
+ * any. Synchronous and free of network or OIDC lookups.
84
+ */
85
+ export function getProvisionedConnector(scope) {
86
+ const entry = provisionedConnectors.get(scope.key);
87
+ if (entry === undefined) {
88
+ return undefined;
89
+ }
90
+ if (entry.expiresAt <= Date.now()) {
91
+ provisionedConnectors.delete(scope.key);
92
+ return undefined;
93
+ }
94
+ return entry.id;
95
+ }
96
+ /**
97
+ * Returns the connector id recently provisioned for `connector` against the
98
+ * same Vercel API and explicitly configured Vercel token, when exactly one
99
+ * server URL has one. Used to target revocation when the caller did not pass
100
+ * the connection; ambiguous matches return `undefined` so the caller falls
101
+ * back to the UID rather than guessing.
102
+ */
103
+ export function findProvisionedConnector(connector, connectOptions) {
104
+ const endpoint = `${resolveBaseUrl(connectOptions)}${MANAGED_OAUTH_CONNECTOR_PATH}`;
105
+ const vercelToken = connectOptions?.vercelToken;
106
+ const now = Date.now();
107
+ const ids = new Set();
108
+ for (const entry of provisionedConnectors.values()) {
109
+ if (entry.connector === connector &&
110
+ entry.endpoint === endpoint &&
111
+ entry.vercelToken === vercelToken &&
112
+ entry.expiresAt > now) {
113
+ ids.add(entry.id);
114
+ }
115
+ }
116
+ return ids.size === 1 ? ids.values().next().value : undefined;
117
+ }
118
+ /**
119
+ * Returns every live connector id provisioned for the authored `connector`
120
+ * across scopes, so eviction can drop token cache entries stored under them
121
+ * without knowing the connection.
122
+ */
123
+ export function getProvisionedConnectorIds(connector) {
124
+ const now = Date.now();
125
+ const ids = new Set();
126
+ for (const entry of provisionedConnectors.values()) {
127
+ if (entry.connector === connector && entry.expiresAt > now) {
128
+ ids.add(entry.id);
129
+ }
130
+ }
131
+ return [...ids];
132
+ }
133
+ /**
134
+ * Drops `id` for `scope` after Vercel Connect rejected it. Leaves the entry
135
+ * alone when another caller has already replaced it.
136
+ */
137
+ export function forgetProvisionedConnector(scope, id) {
138
+ if (provisionedConnectors.get(scope.key)?.id === id) {
139
+ provisionedConnectors.delete(scope.key);
140
+ }
141
+ }
142
+ /**
143
+ * Resolves or provisions the managed OAuth connector `scope` refers to and
144
+ * returns its connector id. The authored connector is sent as the
145
+ * project-local UID (`projectUid`), so Vercel Connect resolves it within the
146
+ * deployment's project and environment rather than across the team. Teams
147
+ * without project UIDs fall back to provisioning by team-global UID.
148
+ *
149
+ * The returned id is remembered for {@link getProvisionedConnector}.
150
+ * Returns `undefined` for non-OAuth connectors or when Vercel Connect does
151
+ * not report an id.
152
+ */
153
+ export function provisionEveOAuthConnector(scope, connectOptions) {
154
+ const existing = inFlightProvisioning.get(scope.key);
155
+ if (existing !== undefined) {
156
+ return existing;
157
+ }
158
+ const promise = (async () => {
159
+ const vercelToken = connectOptions?.vercelToken ?? (await getVercelOidcToken());
160
+ const id = await provisionManagedOAuthConnector(scope, vercelToken);
161
+ if (id !== undefined) {
162
+ rememberProvisionedConnector(scope, id);
163
+ }
164
+ return id;
165
+ })().finally(() => {
166
+ if (inFlightProvisioning.get(scope.key) === promise) {
167
+ inFlightProvisioning.delete(scope.key);
168
+ }
169
+ });
170
+ inFlightProvisioning.set(scope.key, promise);
171
+ return promise;
172
+ }
173
+ function rememberProvisionedConnector(scope, id) {
174
+ provisionedConnectors.delete(scope.key);
175
+ provisionedConnectors.set(scope.key, {
176
+ connector: scope.connector,
177
+ endpoint: scope.endpoint,
178
+ vercelToken: scope.vercelToken,
179
+ id,
180
+ expiresAt: Date.now() + PROVISIONED_CONNECTOR_TTL_MS,
181
+ });
182
+ while (provisionedConnectors.size > MAX_PROVISIONED_CONNECTORS) {
183
+ const oldest = provisionedConnectors.keys().next().value;
184
+ if (oldest === undefined) {
185
+ break;
186
+ }
187
+ provisionedConnectors.delete(oldest);
38
188
  }
39
- await promise;
40
189
  }
41
190
  function resolveServerUrl(connection) {
42
191
  const url = connection.url;
@@ -66,35 +215,107 @@ function isProvisionableConnectorUid(connector) {
66
215
  }
67
216
  return true;
68
217
  }
69
- async function tokenCacheKeyPart(token) {
70
- const subtle = globalThis.crypto?.subtle;
71
- if (subtle) {
72
- const digest = await subtle.digest('SHA-256', new TextEncoder().encode(token));
73
- return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
218
+ async function provisionManagedOAuthConnector(scope, vercelToken) {
219
+ const response = await postManagedOAuthConnector(scope, vercelToken, {
220
+ serverUrl: scope.serverUrl,
221
+ projectUid: scope.connector,
222
+ });
223
+ if (response.ok) {
224
+ return connectorIdFromResponse(response);
74
225
  }
75
- let hash = 2166136261;
76
- for (let index = 0; index < token.length; index++) {
77
- hash ^= token.charCodeAt(index);
78
- hash = Math.imul(hash, 16777619);
226
+ const setup = await setupDetailsFromResponse(response.clone());
227
+ const error = await createConnectErrorFromResponse(response, 'Failed to provision connector');
228
+ if (isProjectUidUnsupported(error)) {
229
+ return provisionByTeamUid(scope, vercelToken);
79
230
  }
80
- return `${token.length}:${hash >>> 0}`;
231
+ throw toProvisioningError(error, setup);
232
+ }
233
+ /**
234
+ * Teams without project-local connector UIDs reject `projectUid` with
235
+ * `404 project_uid_unavailable`; Connect versions that predate OIDC
236
+ * project UIDs reject it with `403`. Explicit user or team API tokens carry
237
+ * no deployment project, so Connect rejects `projectUid` with `400` for
238
+ * them. Provision by team-global UID as before in all of these cases.
239
+ */
240
+ function isProjectUidUnsupported(error) {
241
+ return ((error.status === 404 && error.code === 'project_uid_unavailable') ||
242
+ (error.status === 400 &&
243
+ error.code === 'bad_request' &&
244
+ /"projectUid" requires "projectId"/.test(error.message)) ||
245
+ (error.status === 403 &&
246
+ error.code === 'forbidden' &&
247
+ /"projectUid" is not allowed/.test(error.message)));
81
248
  }
82
- function isNonOAuthConnectorConflict(error) {
83
- return (error instanceof ConnectError &&
84
- error.status === 409 &&
85
- /not an OAuth connector/i.test(error.message));
249
+ async function provisionByTeamUid(scope, vercelToken) {
250
+ const response = await postManagedOAuthConnector(scope, vercelToken, {
251
+ serverUrl: scope.serverUrl,
252
+ uid: scope.connector,
253
+ });
254
+ if (response.ok) {
255
+ return connectorIdFromResponse(response);
256
+ }
257
+ const error = await createConnectErrorFromResponse(response, 'Failed to provision connector');
258
+ if (error.status === 409 && /not an OAuth connector/i.test(error.message)) {
259
+ return undefined;
260
+ }
261
+ throw error;
86
262
  }
87
- async function provisionManagedOAuthConnector({ connector, serverUrl, vercelToken, endpoint, }) {
88
- const response = await fetch(endpoint, {
263
+ function postManagedOAuthConnector(scope, vercelToken, body) {
264
+ return fetch(scope.endpoint, {
89
265
  method: 'POST',
90
266
  headers: {
91
267
  Accept: 'application/json',
92
268
  'Content-Type': 'application/json',
93
269
  Authorization: `Bearer ${vercelToken}`,
94
270
  },
95
- body: JSON.stringify({ serverUrl, uid: connector }),
271
+ body: JSON.stringify(body),
96
272
  });
97
- if (!response.ok) {
98
- throw await createConnectErrorFromResponse(response, 'Failed to provision connector');
273
+ }
274
+ async function connectorIdFromResponse(response) {
275
+ try {
276
+ const body = await response.json();
277
+ return isRecord(body) && typeof body.id === 'string' && body.id !== ''
278
+ ? body.id
279
+ : undefined;
280
+ }
281
+ catch {
282
+ return undefined;
283
+ }
284
+ }
285
+ function toProvisioningError(error, setup) {
286
+ const options = {
287
+ code: error.code,
288
+ status: error.status,
289
+ statusText: error.statusText,
290
+ vendor: error.vendor,
291
+ };
292
+ if (error.code === 'connector_setup_required') {
293
+ return new ConnectorSetupRequiredError(error.message, {
294
+ ...options,
295
+ ...setup,
296
+ });
99
297
  }
298
+ if (error.code === 'connector_provisioning_in_progress') {
299
+ return new ConnectorProvisioningInProgressError(error.message, options);
300
+ }
301
+ return error;
302
+ }
303
+ async function setupDetailsFromResponse(response) {
304
+ try {
305
+ const body = await response.json();
306
+ if (!isRecord(body) || !isRecord(body.error)) {
307
+ return {};
308
+ }
309
+ const { reason, connectorId } = body.error;
310
+ return {
311
+ ...(typeof reason === 'string' ? { reason } : {}),
312
+ ...(typeof connectorId === 'string' ? { connectorId } : {}),
313
+ };
314
+ }
315
+ catch {
316
+ return {};
317
+ }
318
+ }
319
+ function isRecord(value) {
320
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
100
321
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/connect",
3
- "version": "2.5.7-snapshot-c4f338842961",
3
+ "version": "2.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "repository": {
@@ -50,7 +50,7 @@
50
50
  "dist"
51
51
  ],
52
52
  "dependencies": {
53
- "@vercel/oidc": "4.0.5-snapshot-c4f338842961"
53
+ "@vercel/oidc": "4.0.5"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@ai-sdk/mcp": "^1 || ^2",