@vercel/connect 0.0.4-alpha.2
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 +351 -0
- package/dist/ash/connection-authorization.d.ts +131 -0
- package/dist/ash/connection-authorization.js +191 -0
- package/dist/ash/index.d.ts +10 -0
- package/dist/ash/index.js +10 -0
- package/dist/ash/slack-credentials.d.ts +25 -0
- package/dist/ash/slack-credentials.js +31 -0
- package/dist/authjs/connect-provider.d.ts +43 -0
- package/dist/authjs/connect-provider.js +95 -0
- package/dist/authjs/index.d.ts +10 -0
- package/dist/authjs/index.js +10 -0
- package/dist/authorization.d.ts +12 -0
- package/dist/authorization.js +68 -0
- package/dist/betterauth/connect-provider.d.ts +32 -0
- package/dist/betterauth/connect-provider.js +107 -0
- package/dist/betterauth/index.d.ts +10 -0
- package/dist/betterauth/index.js +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/internal/team-id.d.ts +3 -0
- package/dist/internal/team-id.js +47 -0
- package/dist/token.d.ts +47 -0
- package/dist/token.js +102 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# `@vercel/connect`
|
|
2
|
+
|
|
3
|
+
SDK for obtaining scoped tokens for third-party services on behalf of apps or users. The runtime authenticates the calling Vercel project via [`@vercel/oidc`](https://www.npmjs.com/package/@vercel/oidc) and exchanges the OIDC token for a Vercel Connect-issued credential through the Vercel API.
|
|
4
|
+
|
|
5
|
+
The package ships four entrypoints:
|
|
6
|
+
|
|
7
|
+
- `@vercel/connect` — the core token / authorization SDK (no peer deps beyond `@vercel/oidc`).
|
|
8
|
+
- `@vercel/connect/ash` — adapter helpers for the [Ash](https://github.com/vercel/ash) connection runtime. `experimental-ash` is an _optional_ peer dependency.
|
|
9
|
+
- `@vercel/connect/betterauth` — a [Better Auth](https://www.better-auth.com/) `genericOAuth` provider. `better-auth` is an _optional_ peer dependency.
|
|
10
|
+
- `@vercel/connect/authjs` — an [Auth.js](https://authjs.dev/) (NextAuth core) `OAuth2Config` provider. `@auth/core` is an _optional_ peer dependency.
|
|
11
|
+
|
|
12
|
+
Consumers that don't use a given integration never load it.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
pnpm add @vercel/connect
|
|
18
|
+
# or
|
|
19
|
+
npm install @vercel/connect
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The package is ESM-only (`"type": "module"`).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## `@vercel/connect`
|
|
27
|
+
|
|
28
|
+
### `getToken(connector, params, options?)`
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
function getToken(
|
|
32
|
+
connector: string,
|
|
33
|
+
params: ConnectTokenParams,
|
|
34
|
+
options?: ConnectOptions,
|
|
35
|
+
): Promise<string>;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Returns a token string for the given Vercel Connect connector and subject. Wraps `getTokenResponse` and discards the metadata.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { getToken } from "@vercel/connect";
|
|
42
|
+
|
|
43
|
+
const token = await getToken(process.env.CONNECTOR_LINEAR!, {
|
|
44
|
+
subject: { type: "user", id: "user_123" },
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `getTokenResponse(connector, params, options?)`
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
function getTokenResponse(
|
|
52
|
+
connector: string,
|
|
53
|
+
params: ConnectTokenParams,
|
|
54
|
+
options?: ConnectOptions,
|
|
55
|
+
): Promise<ConnectTokenResponse>;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Returns the full token response including `expiresAt`, `installationId`, `tenantId`, and `externalSubject`. Use this when you need the expiration or any of the metadata fields.
|
|
59
|
+
|
|
60
|
+
Both functions share an in-process LRU cache (max 100 entries) keyed by `connector` plus the full `params` payload. Cached entries are reused until they fall inside the `validityBufferMs` window before expiration, after which they're refetched.
|
|
61
|
+
|
|
62
|
+
`connector` accepts either the opaque service connector key (`scl_...`) or the human-readable UID (`oauth/mcp-linear-app`) — both resolve to the same connector on the Vercel Connect side.
|
|
63
|
+
|
|
64
|
+
### `startAuthorization(connector, params, options?)`
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
function startAuthorization(
|
|
68
|
+
connector: string,
|
|
69
|
+
params: ConnectTokenParams,
|
|
70
|
+
options?: ConnectAuthorizationOptions,
|
|
71
|
+
): Promise<ConnectAuthorizationResponse>;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Begins an interactive (user-facing) authorization flow. Returns a `{ request, verifier, url }` triple; redirect the end user to `url`, then call `getToken` / `getTokenResponse` once Vercel Connect signals completion (via the configured `webhook` or `callbackUrl`). The `verifier` is the PKCE verifier — store it alongside the in-flight request so you can correlate the callback.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { startAuthorization } from "@vercel/connect";
|
|
78
|
+
|
|
79
|
+
const { url, verifier } = await startAuthorization(
|
|
80
|
+
process.env.CONNECTOR_LINEAR!,
|
|
81
|
+
{ subject: { type: "user", id: "user_123" } },
|
|
82
|
+
{ callbackUrl: "https://app.example.com/connect/callback" },
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`callbackUrl` must be `https://` or `http://localhost` (for `vercel dev`). `webhook` must be `https://`. Both are validated client-side before the request is sent.
|
|
87
|
+
|
|
88
|
+
### Types
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
interface ConnectTokenParams {
|
|
92
|
+
subject: { type: "app" } | { type: "user"; id: string; issuer?: string };
|
|
93
|
+
installationId?: string;
|
|
94
|
+
audience?: string[];
|
|
95
|
+
scopes?: string[];
|
|
96
|
+
resources?: string[];
|
|
97
|
+
authorizationDetails?: Array<{ type: string } & Record<string, unknown>>;
|
|
98
|
+
/** Buffer (ms) before expiration to consider the token invalid. Default: 30_000. */
|
|
99
|
+
validityBufferMs?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface ConnectTokenResponse {
|
|
103
|
+
token: string;
|
|
104
|
+
/** Expiration timestamp in ms since epoch. */
|
|
105
|
+
expiresAt: number;
|
|
106
|
+
name?: string;
|
|
107
|
+
installationId?: string;
|
|
108
|
+
tenantId?: string;
|
|
109
|
+
externalSubject?: string;
|
|
110
|
+
metadata?: Record<string, unknown>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface ConnectOptions {
|
|
114
|
+
/** Override the OIDC bearer used to authenticate to the Vercel API. */
|
|
115
|
+
vercelToken?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface ConnectAuthorizationOptions {
|
|
119
|
+
vercelToken?: string;
|
|
120
|
+
/** Browser-redirect target after consent. Must be https:// or http://localhost. */
|
|
121
|
+
callbackUrl?: string;
|
|
122
|
+
/** Server-POST callback. Must be https://. */
|
|
123
|
+
webhook?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface ConnectAuthorizationResponse {
|
|
127
|
+
request: string;
|
|
128
|
+
verifier: string;
|
|
129
|
+
url: string;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Errors
|
|
134
|
+
|
|
135
|
+
All exchange / authorization calls reject with typed errors. Catch them by class to distinguish recoverable consent-required cases from terminal failures.
|
|
136
|
+
|
|
137
|
+
| Class | Meaning |
|
|
138
|
+
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
|
139
|
+
| `NoValidTokenError` | Vercel Connect has no valid credential for this subject. For user subjects, recoverable via `startAuthorization`. |
|
|
140
|
+
| `UserAuthorizationRequiredError` | The user has not consented yet (or the previous grant was revoked). Recoverable via `startAuthorization`. |
|
|
141
|
+
| `ConnectorInstallationRequiredError` | The Vercel Connect connector is not installed for this team. Terminal — an operator must install the connector. |
|
|
142
|
+
|
|
143
|
+
Any other failure (network, 5xx, etc.) is thrown as a plain `Error` with the HTTP status and response body in the message.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import {
|
|
147
|
+
getToken,
|
|
148
|
+
NoValidTokenError,
|
|
149
|
+
UserAuthorizationRequiredError,
|
|
150
|
+
ConnectorInstallationRequiredError,
|
|
151
|
+
} from "@vercel/connect";
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await getToken(connector, { subject: { type: "user", id } });
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err instanceof UserAuthorizationRequiredError) {
|
|
157
|
+
// Redirect the user through startAuthorization().
|
|
158
|
+
} else if (err instanceof ConnectorInstallationRequiredError) {
|
|
159
|
+
// Surface "install this Vercel Connect connector" to an operator.
|
|
160
|
+
} else {
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## `@vercel/connect/ash`
|
|
169
|
+
|
|
170
|
+
Adapter helpers for the [Ash](https://github.com/vercel/ash) connection runtime. Importing this subpath requires `experimental-ash >= 0.3.0-alpha.19` to be installed in the consumer project.
|
|
171
|
+
|
|
172
|
+
### `connect(input)`
|
|
173
|
+
|
|
174
|
+
Builds an Ash `AuthorizationDefinition` backed by Vercel Connect, collapsing the `getToken` / `startAuthorization` / `completeAuthorization` boilerplate that each Vercel Connect-backed connection would otherwise repeat.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
function connect(
|
|
178
|
+
connector: string,
|
|
179
|
+
): InteractiveAuthorizationDefinition<ConnectAuthorizationState>;
|
|
180
|
+
function connect(
|
|
181
|
+
options: AshAuthorizationOptions & { principalType?: "user" },
|
|
182
|
+
): InteractiveAuthorizationDefinition<ConnectAuthorizationState>;
|
|
183
|
+
function connect(
|
|
184
|
+
options: AshAuthorizationOptions & { principalType: "app" },
|
|
185
|
+
): NonInteractiveAuthorizationDefinition;
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The shorthand form `connect("linear")` is equivalent to `connect({ connector: "linear" })` — `principalType` defaults to `"user"`, so the helper returns an interactive definition unless you opt into `"app"` explicitly.
|
|
189
|
+
|
|
190
|
+
The return type narrows on `principalType`:
|
|
191
|
+
|
|
192
|
+
- `"user"` (default) → interactive definition. Ash drives the consent flow through its framework-owned webhook; failures map to `ConnectionAuthorizationRequiredError` so the runtime can prompt the user.
|
|
193
|
+
- `"app"` → non-interactive definition (`getToken` only). Failures map to `ConnectionAuthorizationFailedError` with `retryable: false` — there's nobody to consent for an app-scoped connector.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { defineMcpClientConnection } from "experimental-ash/connections";
|
|
197
|
+
import { connect } from "@vercel/connect/ash";
|
|
198
|
+
|
|
199
|
+
export default defineMcpClientConnection({
|
|
200
|
+
url: "https://mcp.linear.app/sse",
|
|
201
|
+
description: "Linear workspace — issues, projects, cycles, and comments.",
|
|
202
|
+
auth: connect("oauth/mcp-linear-app"),
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `AshAuthorizationOptions`
|
|
207
|
+
|
|
208
|
+
| Field | Type | Notes |
|
|
209
|
+
| ---------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
210
|
+
| `connector` | `string` | Vercel Connect connector identifier — accepts the opaque `scl_...` key or the human-readable UID (`oauth/mcp-linear-app`). |
|
|
211
|
+
| `principalType` | `"app" \| "user"` | Defaults to `"user"` when omitted. Selects which definition shape is returned. |
|
|
212
|
+
| `tokenParams` | `Omit<ConnectTokenParams, "subject">` | Forwarded verbatim to every token / authorization call. Use to pin `scopes`, `audience`, `resources`, etc. `subject` is derived from the framework-resolved principal. |
|
|
213
|
+
| `connectOptions` | `ConnectOptions` | Low-level overrides (e.g. `vercelToken`). Most callers leave this unset. |
|
|
214
|
+
| `instructions` | `string` | Custom call-to-action shown on `connection.authorization_required`. Defaults to a message derived from the connection's filename. |
|
|
215
|
+
| `onError` | `(error: unknown, phase: ConnectAuthorizationPhase) => Error \| undefined` | Escape hatch for translating raw errors. Return a replacement `Error`, or `undefined` to fall back to the default mapping. |
|
|
216
|
+
|
|
217
|
+
#### Supporting types
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
type AshAuthorizationInput = string | AshAuthorizationOptions;
|
|
221
|
+
|
|
222
|
+
type ConnectAuthorizationPhase =
|
|
223
|
+
| "getToken"
|
|
224
|
+
| "startAuthorization"
|
|
225
|
+
| "completeAuthorization";
|
|
226
|
+
|
|
227
|
+
type ConnectAuthorizationState = {
|
|
228
|
+
readonly verifier: string;
|
|
229
|
+
readonly [key: string]: JsonValue;
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`ConnectAuthorizationState` is what Ash journals between `startAuthorization` and `completeAuthorization`. The index signature satisfies Ash's `State extends JsonValue` constraint; in practice only `verifier` is set.
|
|
234
|
+
|
|
235
|
+
### `connectSlackCredentials(connector)`
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
function connectSlackCredentials(connector: string): SlackChannelCredentials;
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Builds `SlackChannelCredentials` for the Ash Slack channel adapter, backed by a Vercel Connect connector that stores the workspace's bot token. The returned `botToken` is a function form, invoked once per inbound webhook so the adapter always picks up a fresh token (rotation, refresh, multi-workspace tenancy are all handled server-side).
|
|
242
|
+
|
|
243
|
+
Slack bot tokens are app-scoped — one token per workspace install, shared across every end-user — so this helper calls Vercel Connect with `subject: { type: "app" }`. Per-user Slack OAuth is a separate concern.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { slackRoute } from "experimental-ash/channels/slack";
|
|
247
|
+
import { connectSlackCredentials } from "@vercel/connect/ash";
|
|
248
|
+
|
|
249
|
+
export default slackRoute({
|
|
250
|
+
credentials: connectSlackCredentials("scl_..."),
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The `webhookVerifier` is set to `vercelOidc()`, which authenticates inbound webhooks using the Vercel OIDC token issued for the calling project.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## `@vercel/connect/betterauth`
|
|
259
|
+
|
|
260
|
+
Better Auth provider for Vercel Connect's OAuth2 endpoints. Importing this subpath requires `better-auth >= 1.5.0` to be installed in the consumer project.
|
|
261
|
+
|
|
262
|
+
### `connect(options)`
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
function connect(options: BetterAuthConnectOptions): GenericOAuthConfig;
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Returns a `GenericOAuthConfig` entry that drops into `genericOAuth({ config: [...] })` from `better-auth/plugins/generic-oauth`. Vercel Connect's OAuth client authentication is non-standard — the "client secret" is a Vercel OIDC token that has to be fetched per-request — so the helper overrides `getToken` and `getUserInfo` to inject the right `Authorization` headers.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { betterAuth } from "better-auth";
|
|
272
|
+
import { genericOAuth } from "better-auth/plugins/generic-oauth";
|
|
273
|
+
import { connect } from "@vercel/connect/betterauth";
|
|
274
|
+
|
|
275
|
+
export const auth = betterAuth({
|
|
276
|
+
plugins: [
|
|
277
|
+
genericOAuth({
|
|
278
|
+
config: [
|
|
279
|
+
connect({
|
|
280
|
+
providerId: "slack",
|
|
281
|
+
connector: process.env.CONNECTOR_SLACK!,
|
|
282
|
+
}),
|
|
283
|
+
],
|
|
284
|
+
}),
|
|
285
|
+
],
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### `BetterAuthConnectOptions`
|
|
290
|
+
|
|
291
|
+
| Field | Type | Notes |
|
|
292
|
+
| -------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
293
|
+
| `id` | `string` | Provider id used by Better Auth's generic OAuth plugin. Surfaces in the sign-in URL and account rows. |
|
|
294
|
+
| `connector` | `string` | Vercel Connect connector identifier — accepts `scl_...` or the human-readable UID. |
|
|
295
|
+
| `scopes` | `readonly string[]` | Scopes to request. `offline_access` is added automatically. Defaults to `["*"]`. |
|
|
296
|
+
| `getVercelOidcToken` | `() => Promise<string>` | Override the OIDC token fetcher. Defaults to `getVercelOidcToken` from `@vercel/oidc`. |
|
|
297
|
+
|
|
298
|
+
When the OIDC token is readable, the helper scopes the Vercel Connect consent UI to the deployment's Vercel team (`?teamId=...` on the authorization URL); outside Vercel it falls back to the unqualified URL.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## `@vercel/connect/authjs`
|
|
303
|
+
|
|
304
|
+
Auth.js (NextAuth core) provider for Vercel Connect's OAuth2 endpoints. Importing this subpath requires `@auth/core >= 0.37.0` to be installed in the consumer project.
|
|
305
|
+
|
|
306
|
+
### `connect(options)`
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
function connect(options: AuthJsConnectOptions): OAuth2Config<ConnectProfile>;
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Returns an `OAuth2Config` provider entry for `AuthConfig.providers`. Vercel Connect's token endpoint expects the Vercel OIDC token in place of a static client secret, so the helper declares `token_endpoint_auth_method: "none"` and injects the real `Authorization` headers via Auth.js's `customFetch` symbol on both the token and userinfo endpoints.
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import type { AuthConfig } from "@auth/core";
|
|
316
|
+
import { connect } from "@vercel/connect/authjs";
|
|
317
|
+
|
|
318
|
+
export const authConfig: AuthConfig = {
|
|
319
|
+
providers: [
|
|
320
|
+
connect({
|
|
321
|
+
id: "slack",
|
|
322
|
+
name: "Slack",
|
|
323
|
+
connector: process.env.CONNECTOR_SLACK!,
|
|
324
|
+
}),
|
|
325
|
+
],
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
#### `AuthJsConnectOptions`
|
|
330
|
+
|
|
331
|
+
| Field | Type | Notes |
|
|
332
|
+
| -------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
333
|
+
| `id` | `string` | Provider id surfaced by Auth.js. Becomes the path segment in callback URLs and the `provider` field on accounts. |
|
|
334
|
+
| `name` | `string` | Human-readable display name used by Auth.js sign-in UIs. |
|
|
335
|
+
| `connector` | `string` | Vercel Connect connector identifier — accepts `scl_...` or the human-readable UID. |
|
|
336
|
+
| `scopes` | `readonly string[]` | Scopes to request. `offline_access` is added automatically. Defaults to `["*"]`. |
|
|
337
|
+
| `getVercelOidcToken` | `() => Promise<string>` | Override the OIDC token fetcher. Defaults to `getVercelOidcToken` from `@vercel/oidc`. |
|
|
338
|
+
|
|
339
|
+
#### `ConnectProfile`
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
interface ConnectProfile {
|
|
343
|
+
sub: string;
|
|
344
|
+
email?: string;
|
|
345
|
+
email_verified?: boolean;
|
|
346
|
+
name?: string;
|
|
347
|
+
picture?: string;
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Userinfo payload returned by Vercel Connect's OIDC userinfo endpoint. The helper maps it to an Auth.js user via `profile()`; override that callback in the provider config if you need to surface additional fields.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ash adapter helper for `@vercel/connect`.
|
|
3
|
+
*
|
|
4
|
+
* {@link connect} turns a Vercel Connect OAuth connector id plus a
|
|
5
|
+
* principal type into a ready-made Ash
|
|
6
|
+
* {@link AuthorizationDefinition} that the connection runtime
|
|
7
|
+
* consumes directly. The helper collapses the ~100 lines of
|
|
8
|
+
* `getToken` / `startAuthorization` / `completeAuthorization`
|
|
9
|
+
* boilerplate each Connect-backed connection normally writes down
|
|
10
|
+
* to a single call:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { defineMcpClientConnection } from "experimental-ash/connections";
|
|
14
|
+
* import { connect } from "@vercel/connect/ash";
|
|
15
|
+
*
|
|
16
|
+
* export default defineMcpClientConnection({
|
|
17
|
+
* url: "https://mcp.linear.app/sse",
|
|
18
|
+
* description: "Linear workspace — issues, projects, cycles, and comments.",
|
|
19
|
+
* auth: connect("linear"),
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* # Module layout
|
|
24
|
+
*
|
|
25
|
+
* This entrypoint is exposed at the `@vercel/connect/ash` subpath so
|
|
26
|
+
* consumers that don't use Ash never load it. `experimental-ash` is
|
|
27
|
+
* declared as an optional peer dependency: importing
|
|
28
|
+
* `@vercel/connect/ash` requires Ash to be installed in the consumer
|
|
29
|
+
* project, but the rest of `@vercel/connect` works without it.
|
|
30
|
+
*/
|
|
31
|
+
import { type InteractiveAuthorizationDefinition, type JsonValue, type NonInteractiveAuthorizationDefinition } from "experimental-ash/connections";
|
|
32
|
+
import type { ConnectOptions, ConnectTokenParams } from "../token.js";
|
|
33
|
+
/**
|
|
34
|
+
* Authorization phase passed to {@link AshAuthorizationOptions.onError}
|
|
35
|
+
* so consumers can branch their error translation per callback.
|
|
36
|
+
*/
|
|
37
|
+
export type ConnectAuthorizationPhase = "getToken" | "startAuthorization" | "completeAuthorization";
|
|
38
|
+
/**
|
|
39
|
+
* State journaled by Ash between
|
|
40
|
+
* {@link InteractiveAuthorizationDefinition.startAuthorization} and
|
|
41
|
+
* {@link InteractiveAuthorizationDefinition.completeAuthorization}.
|
|
42
|
+
*
|
|
43
|
+
* Currently just the PKCE verifier. The index signature exists to
|
|
44
|
+
* satisfy Ash's `State extends JsonValue` constraint; in practice
|
|
45
|
+
* the only key the helper produces or reads is `verifier`.
|
|
46
|
+
*/
|
|
47
|
+
export type ConnectAuthorizationState = {
|
|
48
|
+
readonly verifier: string;
|
|
49
|
+
readonly [key: string]: JsonValue;
|
|
50
|
+
};
|
|
51
|
+
/** Options accepted by {@link connect}. */
|
|
52
|
+
export interface AshAuthorizationOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Vercel Connect OAuth connector identifier. Accepts either the
|
|
55
|
+
* opaque service connector key (`scl_...`) or the human-readable
|
|
56
|
+
* UID (`oauth/mcp-linear-app`) — both resolve to the same
|
|
57
|
+
* connector on the Vercel Connect side.
|
|
58
|
+
*/
|
|
59
|
+
readonly connector: string;
|
|
60
|
+
/**
|
|
61
|
+
* Whether this connection authenticates as the agent itself
|
|
62
|
+
* (`"app"`) or on behalf of the end-user (`"user"`). Defaults to
|
|
63
|
+
* `"user"` when omitted, and the returned definition shape depends
|
|
64
|
+
* on this choice:
|
|
65
|
+
*
|
|
66
|
+
* - `"user"` → full interactive OAuth definition with `getToken`,
|
|
67
|
+
* `startAuthorization`, and `completeAuthorization`. Ash will
|
|
68
|
+
* drive a consent flow through its framework-owned webhook.
|
|
69
|
+
* - `"app"` → non-interactive definition with `getToken` only. Ash
|
|
70
|
+
* never runs a consent flow for app-scoped connectors; a failure
|
|
71
|
+
* to fetch the token surfaces as a terminal authorization
|
|
72
|
+
* failure so the channel can prompt an operator to install the
|
|
73
|
+
* Vercel Connect app.
|
|
74
|
+
*/
|
|
75
|
+
readonly principalType?: "app" | "user";
|
|
76
|
+
/**
|
|
77
|
+
* Extra parameters forwarded to every `getTokenResponse` /
|
|
78
|
+
* `startAuthorization` call, minus `subject` — the helper derives
|
|
79
|
+
* `subject` from the framework-resolved principal.
|
|
80
|
+
*
|
|
81
|
+
* Use this to pin `scopes`, `audience`, `resources`, or
|
|
82
|
+
* `authorizationDetails`. Passed through verbatim.
|
|
83
|
+
*/
|
|
84
|
+
readonly tokenParams?: Omit<ConnectTokenParams, "subject">;
|
|
85
|
+
/**
|
|
86
|
+
* Low-level Vercel Connect SDK options (currently `vercelToken`
|
|
87
|
+
* for overriding the OIDC bearer). Most callers leave this unset
|
|
88
|
+
* and rely on `@vercel/oidc` auto-discovery.
|
|
89
|
+
*/
|
|
90
|
+
readonly connectOptions?: ConnectOptions;
|
|
91
|
+
/**
|
|
92
|
+
* Custom call-to-action rendered on the
|
|
93
|
+
* `connection.authorization_required` event. When omitted, Ash
|
|
94
|
+
* fills in `Authorize <ConnectionName> in your browser to continue.`
|
|
95
|
+
* from the connection's filename.
|
|
96
|
+
*/
|
|
97
|
+
readonly instructions?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Escape hatch for turning an unexpected Vercel Connect / network
|
|
100
|
+
* error into an Ash-recognizable error. Called once per failure
|
|
101
|
+
* with the raw error and the phase that produced it.
|
|
102
|
+
*
|
|
103
|
+
* Return a new `Error` to replace the helper's default translation,
|
|
104
|
+
* or `undefined` to let the helper use its own mapping. Useful
|
|
105
|
+
* when a deployment has stricter error reporting requirements
|
|
106
|
+
* (custom `reason` codes, structured logging, etc.).
|
|
107
|
+
*/
|
|
108
|
+
readonly onError?: (error: unknown, phase: ConnectAuthorizationPhase) => Error | undefined;
|
|
109
|
+
}
|
|
110
|
+
/** Input accepted by {@link connect}. */
|
|
111
|
+
export type AshAuthorizationInput = string | AshAuthorizationOptions;
|
|
112
|
+
/**
|
|
113
|
+
* Builds an Ash {@link AuthorizationDefinition} backed by Vercel
|
|
114
|
+
* Connect. The return type narrows based on
|
|
115
|
+
* {@link AshAuthorizationOptions.principalType}:
|
|
116
|
+
*
|
|
117
|
+
* - omitted or `principalType: "user"` returns an
|
|
118
|
+
* {@link InteractiveAuthorizationDefinition}; Ash drives a consent
|
|
119
|
+
* flow through its framework-owned webhook.
|
|
120
|
+
* - `principalType: "app"` returns a
|
|
121
|
+
* {@link NonInteractiveAuthorizationDefinition}; Ash never runs a
|
|
122
|
+
* consent flow for app-scoped connectors.
|
|
123
|
+
*/
|
|
124
|
+
export declare function connect(connector: string): InteractiveAuthorizationDefinition<ConnectAuthorizationState>;
|
|
125
|
+
export declare function connect(options: AshAuthorizationOptions & {
|
|
126
|
+
readonly principalType?: "user";
|
|
127
|
+
}): InteractiveAuthorizationDefinition<ConnectAuthorizationState>;
|
|
128
|
+
export declare function connect(options: AshAuthorizationOptions & {
|
|
129
|
+
readonly principalType: "app";
|
|
130
|
+
}): NonInteractiveAuthorizationDefinition;
|
|
131
|
+
export declare function connect(options: AshAuthorizationInput): InteractiveAuthorizationDefinition<ConnectAuthorizationState> | NonInteractiveAuthorizationDefinition;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ash adapter helper for `@vercel/connect`.
|
|
3
|
+
*
|
|
4
|
+
* {@link connect} turns a Vercel Connect OAuth connector id plus a
|
|
5
|
+
* principal type into a ready-made Ash
|
|
6
|
+
* {@link AuthorizationDefinition} that the connection runtime
|
|
7
|
+
* consumes directly. The helper collapses the ~100 lines of
|
|
8
|
+
* `getToken` / `startAuthorization` / `completeAuthorization`
|
|
9
|
+
* boilerplate each Connect-backed connection normally writes down
|
|
10
|
+
* to a single call:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { defineMcpClientConnection } from "experimental-ash/connections";
|
|
14
|
+
* import { connect } from "@vercel/connect/ash";
|
|
15
|
+
*
|
|
16
|
+
* export default defineMcpClientConnection({
|
|
17
|
+
* url: "https://mcp.linear.app/sse",
|
|
18
|
+
* description: "Linear workspace — issues, projects, cycles, and comments.",
|
|
19
|
+
* auth: connect("linear"),
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* # Module layout
|
|
24
|
+
*
|
|
25
|
+
* This entrypoint is exposed at the `@vercel/connect/ash` subpath so
|
|
26
|
+
* consumers that don't use Ash never load it. `experimental-ash` is
|
|
27
|
+
* declared as an optional peer dependency: importing
|
|
28
|
+
* `@vercel/connect/ash` requires Ash to be installed in the consumer
|
|
29
|
+
* project, but the rest of `@vercel/connect` works without it.
|
|
30
|
+
*/
|
|
31
|
+
import { ConnectionAuthorizationFailedError, ConnectionAuthorizationRequiredError, } from "experimental-ash/connections";
|
|
32
|
+
import { startAuthorization } from "../authorization.js";
|
|
33
|
+
import { ConnectorInstallationRequiredError, getTokenResponse, NoValidTokenError, UserAuthorizationRequiredError, } from "../token.js";
|
|
34
|
+
export function connect(input) {
|
|
35
|
+
const options = normalizeAuthorizationOptions(input);
|
|
36
|
+
if (options.principalType === "app") {
|
|
37
|
+
return buildNonInteractiveDefinition(options);
|
|
38
|
+
}
|
|
39
|
+
return buildInteractiveDefinition(options);
|
|
40
|
+
}
|
|
41
|
+
function normalizeAuthorizationOptions(input) {
|
|
42
|
+
if (typeof input === "string") {
|
|
43
|
+
return { connector: input };
|
|
44
|
+
}
|
|
45
|
+
return input;
|
|
46
|
+
}
|
|
47
|
+
function buildInteractiveDefinition(options) {
|
|
48
|
+
return {
|
|
49
|
+
principalType: "user",
|
|
50
|
+
async getToken({ principal }) {
|
|
51
|
+
try {
|
|
52
|
+
const response = await getTokenResponse(options.connector, buildTokenParams(options, principal), options.connectOptions);
|
|
53
|
+
return { token: response.token, expiresAt: response.expiresAt };
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw translate(error, "getToken", options);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
async startAuthorization({ principal, callbackUrl, webhook, }) {
|
|
60
|
+
try {
|
|
61
|
+
// Ash's `webhook` parameter is semantically a browser-redirect
|
|
62
|
+
// target — the orchestrator mints it via `createWebhook({
|
|
63
|
+
// respondWith: buildAuthorizationCompletePage() })` so the
|
|
64
|
+
// user lands on a friendly "you can close this tab" page after
|
|
65
|
+
// consent. That maps to Vercel Connect's `callbackUrl:`
|
|
66
|
+
// semantics, which accepts both `https://` (prod) and
|
|
67
|
+
// `http://localhost` (vercel dev) — one field covers both.
|
|
68
|
+
// Vercel Connect authenticates the calling Vercel project via
|
|
69
|
+
// OIDC, which is what lets per-workflow dynamic webhook URLs
|
|
70
|
+
// work without an OAuth-style redirect-URI allowlist.
|
|
71
|
+
//
|
|
72
|
+
// We don't route `https://` URLs into Vercel Connect's
|
|
73
|
+
// `webhook:` (server-POST) field, even though it would
|
|
74
|
+
// survive the user closing the consent tab right after IdP
|
|
75
|
+
// callback. That mode shows the user Vercel Connect's
|
|
76
|
+
// generic "close this window" page instead of Ash's branded
|
|
77
|
+
// landing page, and the helper would need to grow
|
|
78
|
+
// protocol-aware logic that diverges from the simple "Ash
|
|
79
|
+
// mints one URL, Vercel Connect redirects there" mental
|
|
80
|
+
// model. Revisit if tab-close timeouts become a real problem
|
|
81
|
+
// in production.
|
|
82
|
+
const response = await startAuthorization(options.connector, buildTokenParams(options, principal), { ...options.connectOptions, callbackUrl: callbackUrl ?? webhook });
|
|
83
|
+
return {
|
|
84
|
+
challenge: buildChallenge(options, response.url),
|
|
85
|
+
state: { verifier: response.verifier },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw translate(error, "startAuthorization", options);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
async completeAuthorization({ principal, }) {
|
|
93
|
+
try {
|
|
94
|
+
const response = await getTokenResponse(options.connector, buildTokenParams(options, principal), options.connectOptions);
|
|
95
|
+
return { token: response.token, expiresAt: response.expiresAt };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
throw translate(error, "completeAuthorization", options);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function buildNonInteractiveDefinition(options) {
|
|
104
|
+
return {
|
|
105
|
+
principalType: "app",
|
|
106
|
+
async getToken({ principal }) {
|
|
107
|
+
try {
|
|
108
|
+
const response = await getTokenResponse(options.connector, buildTokenParams(options, principal), options.connectOptions);
|
|
109
|
+
return { token: response.token, expiresAt: response.expiresAt };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
throw translate(error, "getToken", options);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function buildTokenParams(options, principal) {
|
|
118
|
+
return {
|
|
119
|
+
...options.tokenParams,
|
|
120
|
+
subject: principalToSubject(principal),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function principalToSubject(principal) {
|
|
124
|
+
if (principal.type === "app") {
|
|
125
|
+
return { type: "app" };
|
|
126
|
+
}
|
|
127
|
+
return { type: "user", id: principal.id, issuer: principal.issuer };
|
|
128
|
+
}
|
|
129
|
+
function buildChallenge(options, url) {
|
|
130
|
+
if (options.instructions === undefined) {
|
|
131
|
+
return { url };
|
|
132
|
+
}
|
|
133
|
+
return { url, instructions: options.instructions };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Translates raw Vercel Connect errors into Ash's public
|
|
137
|
+
* {@link ConnectionAuthorizationRequiredError} /
|
|
138
|
+
* {@link ConnectionAuthorizationFailedError} classes. Ash discriminates
|
|
139
|
+
* on `err.name` (not `instanceof`), so even if the consumer's bundle
|
|
140
|
+
* loads a different copy of `experimental-ash` than this helper, the
|
|
141
|
+
* runtime still recognizes the throw.
|
|
142
|
+
*/
|
|
143
|
+
function translate(error, phase, options) {
|
|
144
|
+
const override = options.onError?.(error, phase);
|
|
145
|
+
if (override !== undefined)
|
|
146
|
+
return override;
|
|
147
|
+
// `UserAuthorizationRequiredError` and `NoValidTokenError` both
|
|
148
|
+
// mean "Vercel Connect has no valid credential for this principal
|
|
149
|
+
// yet". For interactive (user) connectors that's recoverable via
|
|
150
|
+
// a consent flow; Ash will see the `Required` throw and drive
|
|
151
|
+
// `startAuthorization`. For app connectors it's terminal — there
|
|
152
|
+
// is nobody to consent — so we surface `Failed` with
|
|
153
|
+
// `retryable: false`.
|
|
154
|
+
if (error instanceof UserAuthorizationRequiredError ||
|
|
155
|
+
error instanceof NoValidTokenError) {
|
|
156
|
+
if (options.principalType === "app") {
|
|
157
|
+
return new ConnectionAuthorizationFailedError("connect", {
|
|
158
|
+
message: error.message,
|
|
159
|
+
reason: "app_not_installed",
|
|
160
|
+
retryable: false,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (phase === "completeAuthorization") {
|
|
164
|
+
// The consent leg reported success (Ash would not call us
|
|
165
|
+
// otherwise) but Vercel Connect still says the user is
|
|
166
|
+
// unauthorized. Default to retryable so the model can
|
|
167
|
+
// re-prompt; most cases (network blip, replay) resolve on
|
|
168
|
+
// retry.
|
|
169
|
+
return new ConnectionAuthorizationFailedError("connect", {
|
|
170
|
+
message: "Authorization did not complete. Vercel Connect still reports the user as unauthorized.",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return new ConnectionAuthorizationRequiredError("connect", {
|
|
174
|
+
message: error.message,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// `ConnectorInstallationRequiredError` is terminal in both modes:
|
|
178
|
+
// the operator has not installed the Vercel Connect connector for
|
|
179
|
+
// this team.
|
|
180
|
+
if (error instanceof ConnectorInstallationRequiredError) {
|
|
181
|
+
return new ConnectionAuthorizationFailedError("connect", {
|
|
182
|
+
message: error.message,
|
|
183
|
+
reason: "connector_installation_required",
|
|
184
|
+
retryable: false,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// Every other error is re-thrown verbatim. Ash treats an unknown
|
|
188
|
+
// throw from `completeAuthorization` as a retryable failure, so the
|
|
189
|
+
// default behavior stays intuitive.
|
|
190
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
191
|
+
}
|