@vercel/connect 0.9.0 → 2.0.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
@@ -5,7 +5,7 @@ SDK for obtaining scoped tokens for third-party services on behalf of apps or us
5
5
  Seven entrypoints, all ESM:
6
6
 
7
7
  - `@vercel/connect` — core token / authorization SDK
8
- - `@vercel/connect/chat` — adapter helpers for the [Chat SDK](https://chat-sdk.dev) (`chat`): `connectSlackAdapter`, `connectDiscordAdapter`, `connectGitHubAdapter`, `connectLinearAdapter`, `connectNotionAdapter`, `connectTelegramAdapter` (no Chat SDK dependency — returns structural config)
8
+ - `@vercel/connect/chat` — adapter helpers for the [Chat SDK](https://chat-sdk.dev) (`chat`): `connectSlackAdapter`, `connectDiscordAdapter`, `connectGitHubAdapter`, `connectLinearAdapter`, `connectNotionAdapter`, `connectTelegramAdapter`, `connectSendblueAdapter` (no Chat SDK dependency — returns structural config)
9
9
  - `@vercel/connect/ai-sdk` — [Vercel AI SDK](https://ai-sdk.dev) glue: re-exports `connectAuthProvider` for MCP transports (optional peers: `ai`, `@ai-sdk/mcp`)
10
10
  - `@vercel/connect/mcp` — canonical MCP-spec `OAuthClientProvider` for any MCP client (optional peer: `@ai-sdk/mcp`)
11
11
  - `@vercel/connect/eve` — adapter helpers for [Eve](https://github.com/vercel/eve) connections (optional peer: `eve`)
@@ -64,10 +64,12 @@ createSlackAdapter({
64
64
 
65
65
  `connectDiscordAdapter` (`botToken` and `applicationId`),
66
66
  `connectGitHubAdapter` (`installationToken`), and `connectLinearAdapter`
67
- (`accessToken`) follow the same shape. `connectNotionAdapter` supplies only the
68
- outbound `token`; native Notion webhooks still require
69
- `NOTION_VERIFICATION_TOKEN`. `connectTelegramAdapter` supplies only
70
- `botToken`; Telegram retains native webhook verification or polling. See the
67
+ (`accessToken`) follow the same shape. `connectSendblueAdapter` supplies a
68
+ lazy Sendblue `accessToken` and Connect OIDC webhook verifier.
69
+ `connectNotionAdapter` supplies only the outbound `token`; native Notion
70
+ webhooks still require `NOTION_VERIFICATION_TOKEN`. `connectTelegramAdapter`
71
+ supplies only `botToken`; Telegram retains native webhook verification or
72
+ polling. See the
71
73
  [Chat SDK integration guide](https://github.com/vercel/vercel/blob/main/packages/connect/docs/chat-integration.md)
72
74
  for connector setup, trigger forwarding, and per-platform examples.
73
75
 
@@ -110,7 +112,21 @@ SDK's `toolApproval` option or `wrapMcpTools` from `@ai-sdk/policy-opa`.
110
112
  Non-AI-SDK MCP clients (the official MCP TypeScript SDK, Mastra, etc.)
111
113
  can import the same `connectAuthProvider` from `@vercel/connect/mcp`.
112
114
 
113
- ### eve
115
+ ### Eve
116
+
117
+ Use `connectSendblueCredentials` with Eve's native Sendblue channel. It
118
+ resolves the app-scoped bearer token and managed sending line together, and
119
+ includes Vercel OIDC webhook verification. When the connector has multiple
120
+ lines, pass `fromNumber` to choose one.
121
+
122
+ ```ts
123
+ import { connectSendblueCredentials } from '@vercel/connect/eve';
124
+ import { sendblueChannel } from 'eve/channels/sendblue';
125
+
126
+ export default sendblueChannel({
127
+ credentials: connectSendblueCredentials('sendblue/my-agent'),
128
+ });
129
+ ```
114
130
 
115
131
  ```ts
116
132
  import { defineMcpClientConnection } from 'eve/connections';
@@ -118,13 +134,15 @@ import { connect } from '@vercel/connect/eve';
118
134
 
119
135
  export default defineMcpClientConnection({
120
136
  url: 'https://mcp.linear.app/sse',
121
- auth: connect('linear'),
137
+ auth: connect({ connector: 'linear', autoProvision: true }),
122
138
  });
123
139
  ```
124
140
 
125
- By default, `connect()` provisions or links the connector for the deploying
126
- Vercel project on first use. Pass `autoProvision: false` when the connector is
127
- managed elsewhere.
141
+ By default, `connect()` only uses connectors already linked to the project and
142
+ does not provision or modify connectors at runtime. Pass `autoProvision: true`
143
+ to opt in. When enabled, `connect()` first tries the token or authorization
144
+ request; if the connector is missing or not linked, it provisions or links the
145
+ connector and retries the request once.
128
146
 
129
147
  ### Better Auth
130
148
 
@@ -1,4 +1,4 @@
1
- import type { ConnectTokenParams } from './token.js';
1
+ import { type ConnectTokenParams } from './token.js';
2
2
  export interface ConnectAuthorizationOptions {
3
3
  vercelToken?: string;
4
4
  callbackUrl?: string;
@@ -7,6 +7,12 @@ export interface ConnectAuthorizationOptions {
7
7
  expiresInMs?: number;
8
8
  /** OAuth prompt value to pass to the connector's authorization server. */
9
9
  prompt?: string;
10
+ /**
11
+ * Region to send the request to, e.g. `sfo1`. Defaults to the
12
+ * `VERCEL_REGION` environment variable; override it to target a
13
+ * different region.
14
+ */
15
+ region?: string;
10
16
  }
11
17
  export interface ConnectAuthorizationResponse {
12
18
  request: string;
@@ -1,5 +1,8 @@
1
1
  import { getVercelOidcToken } from '@vercel/oidc';
2
2
  import { isDetachedInteractiveAuth, validateCallbackUrl, validateWebhookUrl, } from './internal/url-validation.js';
3
+ import { withDefaultScopes } from './internal/default-scopes.js';
4
+ import { resolveBaseUrl } from './internal/base-url.js';
5
+ import { createConnectErrorFromResponse, } from './token.js';
3
6
  export async function startAuthorization(connector, params, options) {
4
7
  if (!connector) {
5
8
  throw new Error('connector is required');
@@ -12,13 +15,14 @@ export async function startAuthorization(connector, params, options) {
12
15
  validateWebhookUrl(options.webhook);
13
16
  }
14
17
  const vercelToken = options?.vercelToken ?? (await getVercelOidcToken());
15
- const endpoint = `https://api.vercel.com/v1/connect/authorize/${encodeURIComponent(connector)}`;
18
+ const baseUrl = resolveBaseUrl(options);
19
+ const endpoint = `${baseUrl}/v1/connect/authorize/${encodeURIComponent(connector)}`;
16
20
  const deviceCode = options?.deviceCode ?? (detachedInteractiveAuth ? true : undefined);
17
21
  const returnUrl = !detachedInteractiveAuth && options?.callbackUrl !== undefined
18
22
  ? { returnUrl: options.callbackUrl }
19
23
  : {};
20
24
  const body = {
21
- ...params,
25
+ ...withDefaultScopes(params),
22
26
  ...returnUrl,
23
27
  ...(options?.webhook !== undefined && { webhook: options.webhook }),
24
28
  ...(deviceCode !== undefined && {
@@ -39,12 +43,7 @@ export async function startAuthorization(connector, params, options) {
39
43
  body: JSON.stringify(body),
40
44
  });
41
45
  if (!response.ok) {
42
- let errorText;
43
- try {
44
- errorText = await response.text();
45
- }
46
- catch { }
47
- throw new Error(`Failed to start authorization: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ''}`);
46
+ throw await createConnectErrorFromResponse(response, 'Failed to start authorization');
48
47
  }
49
48
  const data = await response.json();
50
49
  return data;
@@ -21,7 +21,7 @@
21
21
  * ```
22
22
  */
23
23
  export { createConnectWebhookVerifier, type ConnectWebhookVerifier, type ConnectWebhookVerifierOptions, } from './webhook-verifier.js';
24
- export type { ConnectDiscordAdapterConfig, ConnectGitHubAdapterConfig, ConnectLinearAdapterConfig, ConnectLinqAdapterConfig, ConnectLinqCredentials, ConnectNotionAdapterConfig, ConnectTelegramAdapterConfig, ConnectTokenResolver, } from './types.js';
24
+ export type { ConnectDiscordAdapterConfig, ConnectGitHubAdapterConfig, ConnectLinearAdapterConfig, ConnectLinqAdapterConfig, ConnectLinqCredentials, ConnectNotionAdapterConfig, ConnectSendblueAdapterConfig, ConnectTelegramAdapterConfig, ConnectTokenResolver, } from './types.js';
25
25
  export { connectDiscordAdapter, type ConnectDiscordAdapterParams, } from './discord-adapter.js';
26
26
  export { connectSlackAdapter, type ConnectSlackAdapterConfig, type ConnectSlackAdapterParams, } from './slack-adapter.js';
27
27
  export { connectGitHubAdapter, type ConnectGitHubAdapterParams, } from './github-adapter.js';
@@ -29,3 +29,4 @@ export { connectLinearAdapter, type ConnectLinearAdapterParams, } from './linear
29
29
  export { connectLinqAdapter, type ConnectLinqAdapterParams, } from './linq-adapter.js';
30
30
  export { connectNotionAdapter, type ConnectNotionAdapterParams, } from './notion-adapter.js';
31
31
  export { connectTelegramAdapter, type ConnectTelegramAdapterParams, } from './telegram-adapter.js';
32
+ export { connectSendblueAdapter, type ConnectSendblueAdapterParams, } from './sendblue-adapter.js';
@@ -28,3 +28,4 @@ export { connectLinearAdapter, } from './linear-adapter.js';
28
28
  export { connectLinqAdapter, } from './linq-adapter.js';
29
29
  export { connectNotionAdapter, } from './notion-adapter.js';
30
30
  export { connectTelegramAdapter, } from './telegram-adapter.js';
31
+ export { connectSendblueAdapter, } from './sendblue-adapter.js';
@@ -0,0 +1,25 @@
1
+ import type { ConnectOptions } from '../index.js';
2
+ import { type ConnectSendblueParams } from '../internal/sendblue-lines.js';
3
+ import type { ConnectSendblueAdapterConfig } from './types.js';
4
+ /**
5
+ * Token parameters accepted by {@link connectSendblueAdapter}.
6
+ *
7
+ * Mirrors {@link ConnectTokenParams} from `@vercel/connect`, minus `subject`.
8
+ * Sendblue account credentials are app-scoped, so this helper pins the subject
9
+ * to `{ type: 'app' }`.
10
+ */
11
+ export type ConnectSendblueAdapterParams = ConnectSendblueParams;
12
+ /**
13
+ * Build a Sendblue adapter config fragment backed by a Vercel Connect
14
+ * connector.
15
+ *
16
+ * Sendblue connectors issue a short-lived, line-scoped bearer token. The
17
+ * returned `accessToken` resolver fetches that token for each adapter API
18
+ * operation so Connect token rotation takes effect without rebuilding the
19
+ * adapter. When the connector has one line, that line is selected
20
+ * automatically; select a line explicitly when it has more than one. The
21
+ * webhook verifier accepts Connect trigger-forwarded webhooks using Vercel
22
+ * OIDC, replacing Sendblue's provider-native verification at the deployment
23
+ * boundary.
24
+ */
25
+ export declare function connectSendblueAdapter(connector: string, params?: ConnectSendblueAdapterParams, options?: ConnectOptions): ConnectSendblueAdapterConfig;
@@ -0,0 +1,21 @@
1
+ import { createConnectSendblueConfig, } from '../internal/sendblue-lines.js';
2
+ import { createConnectWebhookVerifier } from './webhook-verifier.js';
3
+ /**
4
+ * Build a Sendblue adapter config fragment backed by a Vercel Connect
5
+ * connector.
6
+ *
7
+ * Sendblue connectors issue a short-lived, line-scoped bearer token. The
8
+ * returned `accessToken` resolver fetches that token for each adapter API
9
+ * operation so Connect token rotation takes effect without rebuilding the
10
+ * adapter. When the connector has one line, that line is selected
11
+ * automatically; select a line explicitly when it has more than one. The
12
+ * webhook verifier accepts Connect trigger-forwarded webhooks using Vercel
13
+ * OIDC, replacing Sendblue's provider-native verification at the deployment
14
+ * boundary.
15
+ */
16
+ export function connectSendblueAdapter(connector, params = {}, options) {
17
+ return {
18
+ ...createConnectSendblueConfig(connector, params, options),
19
+ webhookVerifier: createConnectWebhookVerifier(),
20
+ };
21
+ }
@@ -76,3 +76,15 @@ export interface ConnectNotionAdapterConfig {
76
76
  export interface ConnectTelegramAdapterConfig {
77
77
  botToken: ConnectTokenResolver;
78
78
  }
79
+ /**
80
+ * Partial Sendblue adapter config backed by Vercel Connect.
81
+ *
82
+ * Structurally matches the `accessToken` and `webhookVerifier` options of
83
+ * `createSendblueAdapter` from `chat-adapter-sendblue`.
84
+ */
85
+ export interface ConnectSendblueAdapterConfig {
86
+ accessToken: ConnectTokenResolver;
87
+ defaultFromNumber: string | (() => Promise<string>);
88
+ allowedFromNumbers: readonly string[] | (() => Promise<readonly string[]>);
89
+ webhookVerifier: ConnectWebhookVerifier;
90
+ }
package/dist/connector.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { getVercelOidcToken } from '@vercel/oidc';
2
2
  import { createConnectErrorFromResponse, } from './token.js';
3
+ import { resolveBaseUrl } from './internal/base-url.js';
3
4
  /**
4
5
  * Fetch a connector's metadata from Vercel Connect.
5
6
  *
@@ -11,7 +12,8 @@ import { createConnectErrorFromResponse, } from './token.js';
11
12
  */
12
13
  export async function getConnectorMetadata(connector, options) {
13
14
  const vercelToken = options?.vercelToken ?? (await getVercelOidcToken());
14
- const endpoint = `https://api.vercel.com/v1/connect/connectors/${encodeURIComponent(connector)}`;
15
+ const baseUrl = resolveBaseUrl(options);
16
+ const endpoint = `${baseUrl}/v1/connect/connectors/${encodeURIComponent(connector)}`;
15
17
  const response = await fetch(endpoint, {
16
18
  method: 'GET',
17
19
  headers: {
@@ -117,17 +117,18 @@ export interface EveAuthorizationOptions {
117
117
  readonly connectOptions?: ConnectOptions;
118
118
  /**
119
119
  * Create or link the declared connector against the deploying Vercel
120
- * project before the first token / authorization call. Defaults to
121
- * `true`.
120
+ * project when a token / authorization call reports that the connector
121
+ * is missing or not linked. Defaults to `false`; set this to `true` to
122
+ * opt in to runtime provisioning.
122
123
  *
123
124
  * The provision request is authenticated with the deployment OIDC token
124
125
  * and carries the eve connection's `url` plus this connector UID. Connect
125
126
  * creates the managed OAuth connector when missing, links an existing
126
127
  * 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
+ * link to the OIDC token's authenticated environment.
128
129
  *
129
- * Set this to `false` for callers that intentionally manage the connector
130
- * linkage elsewhere. Opaque connector ids (`scl_...`) and connections
130
+ * Existing linked connectors are always used directly without a
131
+ * provisioning request. Opaque connector ids (`scl_...`) and connections
131
132
  * without a URL are skipped automatically.
132
133
  */
133
134
  readonly autoProvision?: boolean;
@@ -30,7 +30,7 @@
30
30
  */
31
31
  import { ConnectionAuthorizationFailedError, ConnectionAuthorizationRequiredError, } from 'eve/connections';
32
32
  import { startAuthorization } from '../authorization.js';
33
- import { ConnectorInstallationRequiredError, deleteTokenCacheEntry, getTokenResponse, NoValidTokenError, revokeToken, UserAuthorizationRequiredError, } from '../token.js';
33
+ import { ConnectError, ConnectorInstallationRequiredError, deleteTokenCacheEntry, getTokenResponse, NoValidTokenError, revokeToken, UserAuthorizationRequiredError, } from '../token.js';
34
34
  import { provisionEveOAuthConnector } from './provision-oauth-connector.js';
35
35
  export function connect(input) {
36
36
  const options = normalizeAuthorizationOptions(input);
@@ -85,8 +85,8 @@ function buildInteractiveDefinition(options) {
85
85
  principalType: 'user',
86
86
  async getToken({ principal, connection, }) {
87
87
  try {
88
- await autoProvisionConnectorIfEnabled(options, connection);
89
- const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal, connection), getTokenConnectOptions(options));
88
+ const params = await buildTokenParams(options, principal, connection);
89
+ const response = await withConnectorProvisioningFallback(options, connection, () => getTokenResponse(options.connector, params, getTokenConnectOptions(options)));
90
90
  return { token: response.token, expiresAt: response.expiresAt };
91
91
  }
92
92
  catch (error) {
@@ -95,7 +95,6 @@ function buildInteractiveDefinition(options) {
95
95
  },
96
96
  async startAuthorization({ principal, connection, callbackUrl, webhook, }) {
97
97
  try {
98
- await autoProvisionConnectorIfEnabled(options, connection);
99
98
  // eve's `webhook` parameter is also the browser-redirect
100
99
  // target when `callbackUrl` is absent — the orchestrator mints
101
100
  // it via `createWebhook({ respondWith:
@@ -115,12 +114,13 @@ function buildInteractiveDefinition(options) {
115
114
  // OIDC, which is what lets per-workflow dynamic webhook URLs
116
115
  // work without an OAuth-style redirect-URI allowlist.
117
116
  const completionWebhook = connectCompletionWebhook(webhook);
118
- const response = await startAuthorization(options.connector, await buildTokenParams(options, principal, connection), {
117
+ const params = await buildTokenParams(options, principal, connection);
118
+ const response = await withConnectorProvisioningFallback(options, connection, () => startAuthorization(options.connector, params, {
119
119
  ...options.connectOptions,
120
120
  callbackUrl: callbackUrl ?? webhook,
121
121
  ...(completionWebhook ? { webhook: completionWebhook } : null),
122
122
  deviceCode: true,
123
- });
123
+ }));
124
124
  // Sign-in buttons name the destination service ("Sign in with
125
125
  // Salesforce"), matching the OAuth idiom — the consent screen
126
126
  // names the specific requesting app. The connector's own name is
@@ -149,8 +149,8 @@ function buildInteractiveDefinition(options) {
149
149
  },
150
150
  async completeAuthorization({ principal, connection, }) {
151
151
  try {
152
- await autoProvisionConnectorIfEnabled(options, connection);
153
- const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal, connection), options.connectOptions);
152
+ const params = await buildTokenParams(options, principal, connection);
153
+ const response = await withConnectorProvisioningFallback(options, connection, () => getTokenResponse(options.connector, params, options.connectOptions));
154
154
  return { token: response.token, expiresAt: response.expiresAt };
155
155
  }
156
156
  catch (error) {
@@ -175,8 +175,8 @@ function buildNonInteractiveDefinition(options) {
175
175
  principalType: 'app',
176
176
  async getToken({ principal, connection, }) {
177
177
  try {
178
- await autoProvisionConnectorIfEnabled(options, connection);
179
- const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal, connection), getTokenConnectOptions(options));
178
+ const params = await buildTokenParams(options, principal, connection);
179
+ const response = await withConnectorProvisioningFallback(options, connection, () => getTokenResponse(options.connector, params, getTokenConnectOptions(options)));
180
180
  return { token: response.token, expiresAt: response.expiresAt };
181
181
  }
182
182
  catch (error) {
@@ -185,15 +185,33 @@ function buildNonInteractiveDefinition(options) {
185
185
  },
186
186
  };
187
187
  }
188
- async function autoProvisionConnectorIfEnabled(options, connection) {
189
- if (options.autoProvision === false) {
190
- return;
188
+ async function withConnectorProvisioningFallback(options, connection, operation) {
189
+ try {
190
+ return await operation();
191
+ }
192
+ catch (error) {
193
+ if (options.autoProvision !== true ||
194
+ !isMissingConnectorOrProjectLink(error)) {
195
+ throw error;
196
+ }
191
197
  }
192
198
  await provisionEveOAuthConnector({
193
199
  connector: options.connector,
194
200
  connection,
195
201
  connectOptions: options.connectOptions,
196
202
  });
203
+ return operation();
204
+ }
205
+ function isMissingConnectorOrProjectLink(error) {
206
+ if (!(error instanceof ConnectError)) {
207
+ return false;
208
+ }
209
+ if (error.status === 404 && error.code === 'not_found') {
210
+ return true;
211
+ }
212
+ return (error.status === 403 &&
213
+ error.code === 'forbidden' &&
214
+ /connector is not linked to this project/i.test(error.message));
197
215
  }
198
216
  /**
199
217
  * Connect SDK options for `getToken` calls. When {@link
@@ -14,3 +14,4 @@ export { connectLinearCredentials, type ConnectLinearCredentialsParams, } from '
14
14
  export { connectLinqCredentials, type ConnectLinqCredentials, type ConnectLinqCredentialsParams, } from './linq-credentials.js';
15
15
  export { connectPhotonCredentials, type ConnectPhotonCredentialProvider, type PhotonCredentials, type ConnectPhotonCredentialsParams, } from './photon-credentials.js';
16
16
  export { connectSlackCredentials, type ConnectSlackCredentialsParams, } from './slack-credentials.js';
17
+ export { connectSendblueCredentials, type ConnectSendblueCredentials, type ConnectSendblueCredentialsParams, } from './sendblue-credentials.js';
package/dist/eve/index.js CHANGED
@@ -14,3 +14,4 @@ export { connectLinearCredentials, } from './linear-credentials.js';
14
14
  export { connectLinqCredentials, } from './linq-credentials.js';
15
15
  export { connectPhotonCredentials, } from './photon-credentials.js';
16
16
  export { connectSlackCredentials, } from './slack-credentials.js';
17
+ export { connectSendblueCredentials, } from './sendblue-credentials.js';
@@ -1,6 +1,7 @@
1
1
  import { getVercelOidcToken } from '@vercel/oidc';
2
2
  import { ConnectError, createConnectErrorFromResponse } from '../token.js';
3
- const MANAGED_OAUTH_CONNECTOR_ENDPOINT = 'https://api.vercel.com/v1/connect/connectors/managed/oauth';
3
+ import { resolveBaseUrl } from '../internal/base-url.js';
4
+ const MANAGED_OAUTH_CONNECTOR_PATH = '/v1/connect/connectors/managed/oauth';
4
5
  const RESERVED_UID_PATTERN = /^(vc\/|[^/]*\.vercel\.com\/)/;
5
6
  const ALLOWED_RESERVED_UID_PREFIXES = ['mcp.vercel.com/'];
6
7
  const RESERVED_ID_PREFIXES = ['scl_', 'sca_', 'store_', 'ir_'];
@@ -12,9 +13,11 @@ export async function provisionEveOAuthConnector({ connector, connection, connec
12
13
  return;
13
14
  }
14
15
  const vercelToken = connectOptions?.vercelToken ?? (await getVercelOidcToken());
16
+ const endpoint = `${resolveBaseUrl(connectOptions)}${MANAGED_OAUTH_CONNECTOR_PATH}`;
15
17
  const cacheKey = JSON.stringify({
16
18
  connector,
17
19
  serverUrl,
20
+ endpoint,
18
21
  token: await tokenCacheKeyPart(vercelToken),
19
22
  });
20
23
  let promise = provisionCache.get(cacheKey);
@@ -23,6 +26,7 @@ export async function provisionEveOAuthConnector({ connector, connection, connec
23
26
  connector,
24
27
  serverUrl,
25
28
  vercelToken,
29
+ endpoint,
26
30
  }).catch(error => {
27
31
  if (isNonOAuthConnectorConflict(error)) {
28
32
  return;
@@ -80,8 +84,8 @@ function isNonOAuthConnectorConflict(error) {
80
84
  error.status === 409 &&
81
85
  /not an OAuth connector/i.test(error.message));
82
86
  }
83
- async function provisionManagedOAuthConnector({ connector, serverUrl, vercelToken, }) {
84
- const response = await fetch(MANAGED_OAUTH_CONNECTOR_ENDPOINT, {
87
+ async function provisionManagedOAuthConnector({ connector, serverUrl, vercelToken, endpoint, }) {
88
+ const response = await fetch(endpoint, {
85
89
  method: 'POST',
86
90
  headers: {
87
91
  Accept: 'application/json',
@@ -0,0 +1,23 @@
1
+ import type { ConnectOptions } from '../index.js';
2
+ import { type ConnectSendblueParams } from '../internal/sendblue-lines.js';
3
+ import { vercelOidc } from 'eve/channels/auth';
4
+ /** Managed Sendblue channel credentials returned by {@link connectSendblueCredentials}. */
5
+ export interface ConnectSendblueCredentials {
6
+ /** Short-lived bearer token for outbound Sendblue API calls. */
7
+ accessToken: () => Promise<string>;
8
+ /** Default managed Sendblue line. */
9
+ defaultFromNumber: () => Promise<string>;
10
+ /** Every Sendblue line authorized for this connector. */
11
+ allowedFromNumbers: () => Promise<readonly string[]>;
12
+ /** Verifies webhooks forwarded by Vercel Connect. */
13
+ webhookVerifier: ReturnType<typeof vercelOidc>;
14
+ }
15
+ /** Token parameters accepted by {@link connectSendblueCredentials}. */
16
+ export type ConnectSendblueCredentialsParams = ConnectSendblueParams;
17
+ /**
18
+ * Builds managed Sendblue channel credentials backed by a Vercel Connect
19
+ * connector. The access-token resolver and managed line resolvers keep token
20
+ * rotation and line selection delegated to Connect. Vercel OIDC verifies
21
+ * Connect-forwarded webhooks.
22
+ */
23
+ export declare function connectSendblueCredentials(connector: string, params?: ConnectSendblueCredentialsParams, options?: ConnectOptions): ConnectSendblueCredentials;
@@ -0,0 +1,14 @@
1
+ import { createConnectSendblueConfig, } from '../internal/sendblue-lines.js';
2
+ import { vercelOidc } from 'eve/channels/auth';
3
+ /**
4
+ * Builds managed Sendblue channel credentials backed by a Vercel Connect
5
+ * connector. The access-token resolver and managed line resolvers keep token
6
+ * rotation and line selection delegated to Connect. Vercel OIDC verifies
7
+ * Connect-forwarded webhooks.
8
+ */
9
+ export function connectSendblueCredentials(connector, params = {}, options) {
10
+ return {
11
+ ...createConnectSendblueConfig(connector, params, options),
12
+ webhookVerifier: vercelOidc(),
13
+ };
14
+ }
@@ -7,6 +7,12 @@ export interface ConnectInstallationOptions {
7
7
  tenantId?: string;
8
8
  deviceCode?: boolean;
9
9
  expiresInMs?: number;
10
+ /**
11
+ * Region to send the request to, e.g. `sfo1`. Defaults to the
12
+ * `VERCEL_REGION` environment variable; override it to target a
13
+ * different region.
14
+ */
15
+ region?: string;
10
16
  }
11
17
  export interface ConnectInstallationResponse {
12
18
  request: string;
@@ -1,6 +1,7 @@
1
1
  import { getVercelOidcToken } from '@vercel/oidc';
2
2
  import { isDetachedInteractiveAuth, validateCallbackUrl, validateWebhookUrl, } from './internal/url-validation.js';
3
3
  import { createConnectErrorFromResponse } from './token.js';
4
+ import { resolveBaseUrl } from './internal/base-url.js';
4
5
  /**
5
6
  * Create an operator installation request for an app-scoped connector.
6
7
  *
@@ -19,7 +20,8 @@ export async function experimental_startInstallation(connector, params = {}, opt
19
20
  validateWebhookUrl(options.webhook);
20
21
  }
21
22
  const vercelToken = options?.vercelToken ?? (await getVercelOidcToken());
22
- const endpoint = `https://api.vercel.com/v1/connect/install/${encodeURIComponent(connector)}`;
23
+ const baseUrl = resolveBaseUrl(options);
24
+ const endpoint = `${baseUrl}/v1/connect/install/${encodeURIComponent(connector)}`;
23
25
  const deviceCode = options?.deviceCode ?? (detachedInteractiveAuth ? true : undefined);
24
26
  const returnUrl = !detachedInteractiveAuth && options?.returnUrl !== undefined
25
27
  ? { returnUrl: options.returnUrl }
@@ -0,0 +1,10 @@
1
+ /** Base URL for the Vercel API when no region is available. */
2
+ export declare const API_BASE_URL = "https://api.vercel.com";
3
+ /**
4
+ * Resolves the Vercel API base URL for a region. The region comes from the
5
+ * `region` argument, then the `VERCEL_REGION` environment variable; without
6
+ * either, requests go to the global {@link API_BASE_URL}.
7
+ */
8
+ export declare function resolveBaseUrl({ region }?: {
9
+ region?: string;
10
+ }): string;
@@ -0,0 +1,12 @@
1
+ import { readNonEmptyEnv } from './env.js';
2
+ /** Base URL for the Vercel API when no region is available. */
3
+ export const API_BASE_URL = 'https://api.vercel.com';
4
+ /**
5
+ * Resolves the Vercel API base URL for a region. The region comes from the
6
+ * `region` argument, then the `VERCEL_REGION` environment variable; without
7
+ * either, requests go to the global {@link API_BASE_URL}.
8
+ */
9
+ export function resolveBaseUrl({ region } = {}) {
10
+ const resolved = region ?? readNonEmptyEnv('VERCEL_REGION');
11
+ return resolved ? `https://api-${resolved}.vercel.com` : API_BASE_URL;
12
+ }
@@ -0,0 +1,2 @@
1
+ import type { ConnectTokenParams } from '../token.js';
2
+ export declare function withDefaultScopes(params: ConnectTokenParams): ConnectTokenParams;
@@ -0,0 +1,3 @@
1
+ export function withDefaultScopes(params) {
2
+ return params.scopes === undefined ? { ...params, scopes: ['*'] } : params;
3
+ }
@@ -0,0 +1,33 @@
1
+ import { type ConnectOptions, type ConnectTokenParams } from '../token.js';
2
+ export type SendblueFromNumber = string | (() => Promise<string>);
3
+ /** Shared token parameters accepted by Sendblue Connect helpers. */
4
+ export type ConnectSendblueParams = Omit<ConnectTokenParams, 'subject'> & {
5
+ /** Select a Sendblue line when the connector owns more than one. */
6
+ fromNumber?: SendblueFromNumber;
7
+ };
8
+ /** Shared Connect-backed Sendblue credentials and line resolvers. */
9
+ export interface ConnectSendblueConfig {
10
+ accessToken: () => Promise<string>;
11
+ defaultFromNumber: () => Promise<string>;
12
+ allowedFromNumbers: () => Promise<readonly string[]>;
13
+ }
14
+ /** Reads non-empty Sendblue line numbers from Connect connector metadata. */
15
+ export declare function readPhoneNumbers(vendor: Record<string, unknown>): string[];
16
+ /** Resolvers for a selected Sendblue line and every line managed by Connect. */
17
+ export interface SendblueLineResolvers {
18
+ fromNumber: () => Promise<string>;
19
+ allowedFromNumbers: () => Promise<readonly string[]>;
20
+ }
21
+ /**
22
+ * Builds shared lazy resolvers for the connector's managed Sendblue lines.
23
+ * An explicit selected line must be one of the managed lines; when no line is
24
+ * selected, Connect's sole line is used automatically.
25
+ */
26
+ export declare function createSendblueLineResolvers(connector: string, fromNumber: SendblueFromNumber | undefined, options?: ConnectOptions): SendblueLineResolvers;
27
+ /** Resolves an explicit Sendblue line, or the connector's sole managed line. */
28
+ export declare function createSendblueFromNumberResolver(connector: string, fromNumber: SendblueFromNumber | undefined, options?: ConnectOptions): () => Promise<string>;
29
+ /**
30
+ * Builds the shared credential and line configuration for Sendblue Connect
31
+ * integrations. Callers add the webhook verifier required by their runtime.
32
+ */
33
+ export declare function createConnectSendblueConfig(connector: string, params?: ConnectSendblueParams, options?: ConnectOptions): ConnectSendblueConfig;
@@ -0,0 +1,80 @@
1
+ import { getConnectorMetadata } from '../connector.js';
2
+ import { getToken, } from '../token.js';
3
+ /** Reads non-empty Sendblue line numbers from Connect connector metadata. */
4
+ export function readPhoneNumbers(vendor) {
5
+ const lines = vendor.lines;
6
+ if (!Array.isArray(lines))
7
+ return [];
8
+ const phoneNumbers = [];
9
+ for (const line of lines) {
10
+ if (typeof line !== 'object' || line === null)
11
+ return [];
12
+ const phoneNumber = line.phone_number;
13
+ if (typeof phoneNumber !== 'string' || phoneNumber.length === 0)
14
+ return [];
15
+ phoneNumbers.push(phoneNumber);
16
+ }
17
+ return phoneNumbers;
18
+ }
19
+ /**
20
+ * Builds shared lazy resolvers for the connector's managed Sendblue lines.
21
+ * An explicit selected line must be one of the managed lines; when no line is
22
+ * selected, Connect's sole line is used automatically.
23
+ */
24
+ export function createSendblueLineResolvers(connector, fromNumber, options) {
25
+ let pending;
26
+ let resolved;
27
+ function resolveAllowedFromNumbers() {
28
+ if (resolved !== undefined)
29
+ return Promise.resolve(resolved);
30
+ if (!pending) {
31
+ pending = getConnectorMetadata(connector, options)
32
+ .then(metadata => {
33
+ const phoneNumbers = readPhoneNumbers(metadata.vendor);
34
+ if (phoneNumbers.length === 0) {
35
+ throw new Error(`Vercel Connect connector ${connector} has no Sendblue lines.`);
36
+ }
37
+ resolved = phoneNumbers;
38
+ return phoneNumbers;
39
+ })
40
+ .finally(() => {
41
+ pending = undefined;
42
+ });
43
+ }
44
+ return pending;
45
+ }
46
+ async function resolveFromNumber() {
47
+ const phoneNumbers = await resolveAllowedFromNumbers();
48
+ const selected = typeof fromNumber === 'function' ? await fromNumber() : fromNumber;
49
+ if (selected === undefined) {
50
+ if (phoneNumbers.length === 1)
51
+ return phoneNumbers[0];
52
+ throw new Error(`Vercel Connect connector ${connector} has multiple Sendblue lines. Pass fromNumber to select one.`);
53
+ }
54
+ if (!phoneNumbers.includes(selected)) {
55
+ throw new Error(`Vercel Connect connector ${connector} did not authorize Sendblue line ${selected}.`);
56
+ }
57
+ return selected;
58
+ }
59
+ return {
60
+ fromNumber: resolveFromNumber,
61
+ allowedFromNumbers: resolveAllowedFromNumbers,
62
+ };
63
+ }
64
+ /** Resolves an explicit Sendblue line, or the connector's sole managed line. */
65
+ export function createSendblueFromNumberResolver(connector, fromNumber, options) {
66
+ return createSendblueLineResolvers(connector, fromNumber, options).fromNumber;
67
+ }
68
+ /**
69
+ * Builds the shared credential and line configuration for Sendblue Connect
70
+ * integrations. Callers add the webhook verifier required by their runtime.
71
+ */
72
+ export function createConnectSendblueConfig(connector, params = {}, options) {
73
+ const { fromNumber, ...tokenParams } = params;
74
+ const { fromNumber: defaultFromNumber, allowedFromNumbers } = createSendblueLineResolvers(connector, fromNumber, options);
75
+ return {
76
+ accessToken: () => getToken(connector, { ...tokenParams, subject: { type: 'app' } }, options),
77
+ defaultFromNumber,
78
+ allowedFromNumbers,
79
+ };
80
+ }
package/dist/token.d.ts CHANGED
@@ -27,8 +27,8 @@ export interface ConnectTokenParams {
27
27
  installationId?: string;
28
28
  audience?: string[];
29
29
  /**
30
- * Access scopes to request. Use `['*']` to request the default scopes for
31
- * the specified subject type.
30
+ * Access scopes to request. Defaults to `['*']`, which requests the default
31
+ * scopes for the specified subject type.
32
32
  */
33
33
  scopes?: string[];
34
34
  resources?: string[];
@@ -103,6 +103,12 @@ export interface ConnectOptions {
103
103
  * a stale bearer.
104
104
  */
105
105
  forceRefresh?: boolean;
106
+ /**
107
+ * Region to send the request to, e.g. `sfo1`. Defaults to the
108
+ * `VERCEL_REGION` environment variable; override it to target a
109
+ * different region.
110
+ */
111
+ region?: string;
106
112
  }
107
113
  export declare function getToken(connector: string, params: ConnectTokenParams, options?: ConnectOptions): Promise<string>;
108
114
  export declare function getTokenResponse(connector: string, params: ConnectTokenParams, options?: ConnectOptions): Promise<ConnectTokenResponse>;
package/dist/token.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { getVercelOidcToken } from '@vercel/oidc';
2
+ import { resolveBaseUrl } from './internal/base-url.js';
3
+ import { withDefaultScopes } from './internal/default-scopes.js';
2
4
  export class ConnectError extends Error {
3
5
  code;
4
6
  status;
@@ -36,8 +38,9 @@ export async function getToken(connector, params, options) {
36
38
  return token;
37
39
  }
38
40
  export async function getTokenResponse(connector, params, options) {
39
- const bufferMs = params.validityBufferMs ?? DEFAULT_VALIDITY_BUFFER_MS;
40
- const cacheKey = tokenCacheKey(connector, params);
41
+ const requestParams = withDefaultScopes(params);
42
+ const bufferMs = requestParams.validityBufferMs ?? DEFAULT_VALIDITY_BUFFER_MS;
43
+ const cacheKey = tokenCacheKey(connector, requestParams);
41
44
  if (options?.forceRefresh) {
42
45
  cache.delete(cacheKey);
43
46
  }
@@ -53,7 +56,8 @@ export async function getTokenResponse(connector, params, options) {
53
56
  }
54
57
  }
55
58
  const vercelToken = options?.vercelToken ?? (await getVercelOidcToken());
56
- const endpoint = `https://api.vercel.com/v1/connect/token/${encodeURIComponent(connector)}`;
59
+ const baseUrl = resolveBaseUrl(options);
60
+ const endpoint = `${baseUrl}/v1/connect/token/${encodeURIComponent(connector)}`;
57
61
  const response = await fetch(endpoint, {
58
62
  method: 'POST',
59
63
  headers: {
@@ -61,7 +65,7 @@ export async function getTokenResponse(connector, params, options) {
61
65
  'Content-Type': 'application/json',
62
66
  Authorization: `Bearer ${vercelToken}`,
63
67
  },
64
- body: JSON.stringify(params),
68
+ body: JSON.stringify(requestParams),
65
69
  });
66
70
  if (!response.ok) {
67
71
  throw await createConnectErrorFromResponse(response, 'Failed to get token');
@@ -75,7 +79,8 @@ export async function getTokenResponse(connector, params, options) {
75
79
  }
76
80
  export async function revokeToken(connector, params, options) {
77
81
  const vercelToken = options?.vercelToken ?? (await getVercelOidcToken());
78
- const endpoint = `https://api.vercel.com/v1/connect/connectors/${encodeURIComponent(connector)}/tokens`;
82
+ const baseUrl = resolveBaseUrl(options);
83
+ const endpoint = `${baseUrl}/v1/connect/connectors/${encodeURIComponent(connector)}/tokens`;
79
84
  const response = await fetch(endpoint, {
80
85
  method: 'DELETE',
81
86
  headers: {
@@ -108,7 +113,7 @@ export async function revokeToken(connector, params, options) {
108
113
  * call. No-op when no matching entry exists.
109
114
  */
110
115
  export function deleteTokenCacheEntry(connector, params) {
111
- cache.delete(tokenCacheKey(connector, params));
116
+ cache.delete(tokenCacheKey(connector, withDefaultScopes(params)));
112
117
  }
113
118
  const DEFAULT_VALIDITY_BUFFER_MS = 30_000;
114
119
  const MAX_CACHE_SIZE = 100;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/connect",
3
- "version": "0.9.0",
3
+ "version": "2.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "repository": {