@vercel/connect 0.2.8 → 0.2.10

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
@@ -68,7 +68,7 @@ SDK's `toolApproval` option or `wrapMcpTools` from `@ai-sdk/policy-opa`.
68
68
  Non-AI-SDK MCP clients (the official MCP TypeScript SDK, Mastra, etc.)
69
69
  can import the same `connectAuthProvider` from `@vercel/connect/mcp`.
70
70
 
71
- ### Eve
71
+ ### eve
72
72
 
73
73
  ```ts
74
74
  import { defineMcpClientConnection } from 'eve/connections';
@@ -80,6 +80,10 @@ export default defineMcpClientConnection({
80
80
  });
81
81
  ```
82
82
 
83
+ By default, `connect()` provisions or links the connector for the deploying
84
+ Vercel project on first use. Pass `autoProvision: false` when the connector is
85
+ managed elsewhere.
86
+
83
87
  ### Better Auth
84
88
 
85
89
  ```ts
@@ -115,6 +115,22 @@ export interface EveAuthorizationOptions {
115
115
  * and rely on `@vercel/oidc` auto-discovery.
116
116
  */
117
117
  readonly connectOptions?: ConnectOptions;
118
+ /**
119
+ * Create or link the declared connector against the deploying Vercel
120
+ * project before the first token / authorization call. Defaults to
121
+ * `true`.
122
+ *
123
+ * The provision request is authenticated with the deployment OIDC token
124
+ * and carries the eve connection's `url` plus this connector UID. Connect
125
+ * creates the managed OAuth connector when missing, links an existing
126
+ * OAuth connector when the UID already exists, and scopes the new project
127
+ * link to the OIDC token's environment and higher promotion targets.
128
+ *
129
+ * Set this to `false` for callers that intentionally manage the connector
130
+ * linkage elsewhere. Opaque connector ids (`scl_...`) and connections
131
+ * without a URL are skipped automatically.
132
+ */
133
+ readonly autoProvision?: boolean;
118
134
  /**
119
135
  * Re-validate the grant against Vercel Connect on every `getToken`
120
136
  * instead of trusting the in-process token cache.
@@ -170,9 +186,10 @@ export type EveAuthorizationInput = string | EveAuthorizationOptions;
170
186
  * both forms address the same connector.
171
187
  *
172
188
  * The marker is purely metadata — it does not influence the runtime
173
- * token-fetching behaviour, which continues to be driven by the
189
+ * token-fetching identity, which continues to be driven by the
174
190
  * `getToken` / `startAuthorization` / `completeAuthorization`
175
- * callbacks.
191
+ * callbacks. When auto-provisioning is enabled, the same connector value is
192
+ * also sent as the managed OAuth UID.
176
193
  */
177
194
  export interface VercelConnectMetadata {
178
195
  readonly connector: string;
@@ -31,6 +31,7 @@
31
31
  import { ConnectionAuthorizationFailedError, ConnectionAuthorizationRequiredError, } from 'eve/connections';
32
32
  import { startAuthorization } from '../authorization.js';
33
33
  import { ConnectorInstallationRequiredError, deleteTokenCacheEntry, getTokenResponse, NoValidTokenError, revokeToken, UserAuthorizationRequiredError, } from '../token.js';
34
+ import { provisionEveOAuthConnector } from './provision-oauth-connector.js';
34
35
  export function connect(input) {
35
36
  const options = normalizeAuthorizationOptions(input);
36
37
  const vercelConnect = { connector: options.connector };
@@ -84,6 +85,7 @@ function buildInteractiveDefinition(options) {
84
85
  principalType: 'user',
85
86
  async getToken({ principal, connection, }) {
86
87
  try {
88
+ await autoProvisionConnectorIfEnabled(options, connection);
87
89
  const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal, connection), getTokenConnectOptions(options));
88
90
  return { token: response.token, expiresAt: response.expiresAt };
89
91
  }
@@ -93,30 +95,30 @@ function buildInteractiveDefinition(options) {
93
95
  },
94
96
  async startAuthorization({ principal, connection, callbackUrl, webhook, }) {
95
97
  try {
96
- // Eve's `webhook` parameter is semantically a browser-redirect
97
- // target — the orchestrator mints it via `createWebhook({
98
- // respondWith: buildAuthorizationCompletePage() })` so the
99
- // user lands on a friendly "you can close this tab" page after
100
- // consent. That maps to Vercel Connect's `callbackUrl:`
101
- // semantics, which accepts both `https://` (prod) and
102
- // `http://localhost` (vercel dev) — one field covers both.
98
+ await autoProvisionConnectorIfEnabled(options, connection);
99
+ // eve's `webhook` parameter is also the browser-redirect
100
+ // target when `callbackUrl` is absent — the orchestrator mints
101
+ // it via `createWebhook({ respondWith:
102
+ // buildAuthorizationCompletePage() })` so the user lands on a
103
+ // friendly "you can close this tab" page after consent. That
104
+ // maps to Vercel Connect's `callbackUrl:` semantics, which
105
+ // accepts both `https://` (prod) and `http://localhost`
106
+ // (vercel dev).
107
+ //
108
+ // When the eve webhook is HTTPS, also pass it as Vercel
109
+ // Connect's server-side completion webhook. That lets Connect
110
+ // resume the eve session even when the OAuth callback fails
111
+ // before the browser can be redirected back, such as a token
112
+ // exchange error after provider consent.
113
+ //
103
114
  // Vercel Connect authenticates the calling Vercel project via
104
115
  // OIDC, which is what lets per-workflow dynamic webhook URLs
105
116
  // work without an OAuth-style redirect-URI allowlist.
106
- //
107
- // We don't route `https://` URLs into Vercel Connect's
108
- // `webhook:` (server-POST) field, even though it would
109
- // survive the user closing the consent tab right after IdP
110
- // callback. That mode shows the user Vercel Connect's
111
- // generic "close this window" page instead of Eve's branded
112
- // landing page, and the helper would need to grow
113
- // protocol-aware logic that diverges from the simple "Eve
114
- // mints one URL, Vercel Connect redirects there" mental
115
- // model. Revisit if tab-close timeouts become a real problem
116
- // in production.
117
+ const completionWebhook = connectCompletionWebhook(webhook);
117
118
  const response = await startAuthorization(options.connector, await buildTokenParams(options, principal, connection), {
118
119
  ...options.connectOptions,
119
120
  callbackUrl: callbackUrl ?? webhook,
121
+ ...(completionWebhook ? { webhook: completionWebhook } : null),
120
122
  deviceCode: true,
121
123
  });
122
124
  return {
@@ -138,6 +140,7 @@ function buildInteractiveDefinition(options) {
138
140
  },
139
141
  async completeAuthorization({ principal, connection, }) {
140
142
  try {
143
+ await autoProvisionConnectorIfEnabled(options, connection);
141
144
  const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal, connection), options.connectOptions);
142
145
  return { token: response.token, expiresAt: response.expiresAt };
143
146
  }
@@ -147,11 +150,23 @@ function buildInteractiveDefinition(options) {
147
150
  },
148
151
  };
149
152
  }
153
+ function connectCompletionWebhook(webhook) {
154
+ if (!webhook) {
155
+ return null;
156
+ }
157
+ try {
158
+ return new URL(webhook).protocol === 'https:' ? webhook : null;
159
+ }
160
+ catch {
161
+ return null;
162
+ }
163
+ }
150
164
  function buildNonInteractiveDefinition(options) {
151
165
  return {
152
166
  principalType: 'app',
153
167
  async getToken({ principal, connection, }) {
154
168
  try {
169
+ await autoProvisionConnectorIfEnabled(options, connection);
155
170
  const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal, connection), getTokenConnectOptions(options));
156
171
  return { token: response.token, expiresAt: response.expiresAt };
157
172
  }
@@ -161,6 +176,16 @@ function buildNonInteractiveDefinition(options) {
161
176
  },
162
177
  };
163
178
  }
179
+ async function autoProvisionConnectorIfEnabled(options, connection) {
180
+ if (options.autoProvision === false) {
181
+ return;
182
+ }
183
+ await provisionEveOAuthConnector({
184
+ connector: options.connector,
185
+ connection,
186
+ connectOptions: options.connectOptions,
187
+ });
188
+ }
164
189
  /**
165
190
  * Connect SDK options for `getToken` calls. When {@link
166
191
  * EveAuthorizationOptions.validate} is set, forces a cache-bypassing
@@ -0,0 +1,9 @@
1
+ import type { ConnectOptions } from '../token.js';
2
+ import type { EveConnectionAuthorizationContext } from './connection-authorization.js';
3
+ interface ProvisionEveOAuthConnectorOptions {
4
+ readonly connector: string;
5
+ readonly connection: EveConnectionAuthorizationContext;
6
+ readonly connectOptions?: ConnectOptions;
7
+ }
8
+ export declare function provisionEveOAuthConnector({ connector, connection, connectOptions, }: ProvisionEveOAuthConnectorOptions): Promise<void>;
9
+ export {};
@@ -0,0 +1,96 @@
1
+ import { getVercelOidcToken } from '@vercel/oidc';
2
+ import { ConnectError, createConnectErrorFromResponse } from '../token.js';
3
+ const MANAGED_OAUTH_CONNECTOR_ENDPOINT = 'https://api.vercel.com/v1/connect/connectors/managed/oauth';
4
+ const RESERVED_UID_PATTERN = /^(vc\/|[^/]*\.vercel\.com\/)/;
5
+ const ALLOWED_RESERVED_UID_PREFIXES = ['mcp.vercel.com/'];
6
+ const RESERVED_ID_PREFIXES = ['scl_', 'sca_', 'store_', 'ir_'];
7
+ const INVALID_UID_CHARS = /[\s%#]/;
8
+ const provisionCache = new Map();
9
+ export async function provisionEveOAuthConnector({ connector, connection, connectOptions, }) {
10
+ const serverUrl = resolveServerUrl(connection);
11
+ if (serverUrl === undefined || !isProvisionableConnectorUid(connector)) {
12
+ return;
13
+ }
14
+ const vercelToken = connectOptions?.vercelToken ?? (await getVercelOidcToken());
15
+ const cacheKey = JSON.stringify({
16
+ connector,
17
+ serverUrl,
18
+ token: await tokenCacheKeyPart(vercelToken),
19
+ });
20
+ let promise = provisionCache.get(cacheKey);
21
+ if (promise === undefined) {
22
+ promise = provisionManagedOAuthConnector({
23
+ connector,
24
+ serverUrl,
25
+ vercelToken,
26
+ }).catch(error => {
27
+ if (isNonOAuthConnectorConflict(error)) {
28
+ return;
29
+ }
30
+ provisionCache.delete(cacheKey);
31
+ throw error;
32
+ });
33
+ provisionCache.set(cacheKey, promise);
34
+ }
35
+ await promise;
36
+ }
37
+ function resolveServerUrl(connection) {
38
+ const url = connection.url;
39
+ if (typeof url !== 'string') {
40
+ return undefined;
41
+ }
42
+ const trimmed = url.trim();
43
+ return trimmed === '' ? undefined : trimmed;
44
+ }
45
+ function isProvisionableConnectorUid(connector) {
46
+ if (connector === '' || INVALID_UID_CHARS.test(connector)) {
47
+ return false;
48
+ }
49
+ for (let index = 0; index < connector.length; index++) {
50
+ const code = connector.charCodeAt(index);
51
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
52
+ return false;
53
+ }
54
+ }
55
+ const normalized = connector.toLowerCase();
56
+ if (RESERVED_ID_PREFIXES.some(prefix => normalized.startsWith(prefix))) {
57
+ return false;
58
+ }
59
+ if (RESERVED_UID_PATTERN.test(normalized) &&
60
+ !ALLOWED_RESERVED_UID_PREFIXES.some(prefix => normalized.startsWith(prefix))) {
61
+ return false;
62
+ }
63
+ return true;
64
+ }
65
+ async function tokenCacheKeyPart(token) {
66
+ const subtle = globalThis.crypto?.subtle;
67
+ if (subtle) {
68
+ const digest = await subtle.digest('SHA-256', new TextEncoder().encode(token));
69
+ return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
70
+ }
71
+ let hash = 2166136261;
72
+ for (let index = 0; index < token.length; index++) {
73
+ hash ^= token.charCodeAt(index);
74
+ hash = Math.imul(hash, 16777619);
75
+ }
76
+ return `${token.length}:${hash >>> 0}`;
77
+ }
78
+ function isNonOAuthConnectorConflict(error) {
79
+ return (error instanceof ConnectError &&
80
+ error.status === 409 &&
81
+ /not an OAuth connector/i.test(error.message));
82
+ }
83
+ async function provisionManagedOAuthConnector({ connector, serverUrl, vercelToken, }) {
84
+ const response = await fetch(MANAGED_OAUTH_CONNECTOR_ENDPOINT, {
85
+ method: 'POST',
86
+ headers: {
87
+ Accept: 'application/json',
88
+ 'Content-Type': 'application/json',
89
+ Authorization: `Bearer ${vercelToken}`,
90
+ },
91
+ body: JSON.stringify({ serverUrl, uid: connector }),
92
+ });
93
+ if (!response.ok) {
94
+ throw await createConnectErrorFromResponse(response, 'Failed to provision connector');
95
+ }
96
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/connect",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "repository": {
@@ -38,14 +38,14 @@
38
38
  "dist"
39
39
  ],
40
40
  "dependencies": {
41
- "@vercel/oidc": "3.7.0"
41
+ "@vercel/oidc": "3.7.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@ai-sdk/mcp": "^1 || ^2",
45
45
  "@auth/core": ">=0.37.0",
46
46
  "ai": "^6 || ^7.0.0-beta.0",
47
47
  "better-auth": ">=1.5.0",
48
- "eve": ">=0.6.0-beta.1"
48
+ "eve": ">=0.13.7"
49
49
  },
50
50
  "peerDependenciesMeta": {
51
51
  "@ai-sdk/mcp": {