@vercel/connect 0.1.2 → 0.2.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 +46 -5
- package/dist/ai-sdk/index.d.ts +52 -0
- package/dist/ai-sdk/index.js +52 -0
- package/dist/authorization-details.d.ts +11 -0
- package/dist/authorization-details.js +1 -0
- package/dist/{ash → eve}/connect-oauth.d.ts +4 -4
- package/dist/{ash → eve}/connect-oauth.js +2 -2
- package/dist/{ash → eve}/connection-authorization.d.ts +36 -42
- package/dist/{ash → eve}/connection-authorization.js +24 -24
- package/dist/eve/index.d.ts +11 -0
- package/dist/{ash → eve}/index.js +4 -4
- package/dist/eve/slack-credentials.d.ts +46 -0
- package/dist/{ash → eve}/slack-credentials.js +17 -6
- package/dist/index.d.ts +2 -1
- package/dist/mcp/connect-auth-provider.d.ts +99 -0
- package/dist/mcp/connect-auth-provider.js +148 -0
- package/dist/mcp/index.d.ts +33 -0
- package/dist/mcp/index.js +33 -0
- package/dist/token.d.ts +22 -10
- package/package.json +30 -9
- package/dist/ash/index.d.ts +0 -11
- package/dist/ash/slack-credentials.d.ts +0 -25
package/README.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
SDK for obtaining scoped tokens for third-party services on behalf of apps or users. 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.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Six entrypoints, all ESM:
|
|
6
6
|
|
|
7
7
|
- `@vercel/connect` — core token / authorization SDK
|
|
8
|
-
- `@vercel/connect/
|
|
8
|
+
- `@vercel/connect/ai-sdk` — [Vercel AI SDK](https://ai-sdk.dev) glue: re-exports `connectAuthProvider` for MCP transports (optional peers: `ai`, `@ai-sdk/mcp`)
|
|
9
|
+
- `@vercel/connect/mcp` — canonical MCP-spec `OAuthClientProvider` for any MCP client (optional peer: `@ai-sdk/mcp`)
|
|
10
|
+
- `@vercel/connect/eve` — adapter helpers for [Eve](https://github.com/vercel/eve) connections (optional peer: `eve`)
|
|
9
11
|
- `@vercel/connect/betterauth` — [Better Auth](https://www.better-auth.com/) `genericOAuth` provider (optional peer: `better-auth`)
|
|
10
12
|
- `@vercel/connect/authjs` — [Auth.js](https://authjs.dev/) `OAuth2Config` provider (optional peer: `@auth/core`)
|
|
11
13
|
|
|
@@ -27,11 +29,50 @@ const token = await getToken(process.env.CONNECTOR_LINEAR!, {
|
|
|
27
29
|
});
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
###
|
|
32
|
+
### Vercel AI SDK + MCP
|
|
31
33
|
|
|
32
34
|
```ts
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
+
import { createMCPClient } from '@ai-sdk/mcp';
|
|
36
|
+
import { streamText } from 'ai';
|
|
37
|
+
import {
|
|
38
|
+
connectAuthProvider,
|
|
39
|
+
ConsentRequiredError,
|
|
40
|
+
} from '@vercel/connect/ai-sdk';
|
|
41
|
+
|
|
42
|
+
const mcp = await createMCPClient({
|
|
43
|
+
transport: {
|
|
44
|
+
type: 'http',
|
|
45
|
+
url: 'https://mcp.linear.app',
|
|
46
|
+
authProvider: connectAuthProvider('oauth/linear', {
|
|
47
|
+
subject: { type: 'user', id: 'user_123' },
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = await streamText({
|
|
54
|
+
model: 'openai/gpt-5.4',
|
|
55
|
+
tools: await mcp.tools(),
|
|
56
|
+
prompt,
|
|
57
|
+
});
|
|
58
|
+
return result.toUIMessageStreamResponse();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err instanceof ConsentRequiredError) return Response.redirect(err.url);
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Tool-call approval (Human-in-the-Loop) is independent of Connect — use the AI
|
|
66
|
+
SDK's `toolApproval` option or `wrapMcpTools` from `@ai-sdk/policy-opa`.
|
|
67
|
+
|
|
68
|
+
Non-AI-SDK MCP clients (the official MCP TypeScript SDK, Mastra, etc.)
|
|
69
|
+
can import the same `connectAuthProvider` from `@vercel/connect/mcp`.
|
|
70
|
+
|
|
71
|
+
### Eve
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { defineMcpClientConnection } from 'eve/connections';
|
|
75
|
+
import { connect } from '@vercel/connect/eve';
|
|
35
76
|
|
|
36
77
|
export default defineMcpClientConnection({
|
|
37
78
|
url: 'https://mcp.linear.app/sse',
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the `@vercel/connect/ai-sdk` subpath.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the MCP-spec `connectAuthProvider` (and its consent
|
|
5
|
+
* types) so AI SDK users have a single, ergonomic import. The
|
|
6
|
+
* provider plugs straight into `@ai-sdk/mcp`'s `createMCPClient`
|
|
7
|
+
* `authProvider` and works with AI SDK v6 and v7.
|
|
8
|
+
*
|
|
9
|
+
* Tool-call approval (Human-in-the-Loop) is intentionally not
|
|
10
|
+
* provided here — it is independent of Connect and already covered by
|
|
11
|
+
* the AI SDK's own `toolApproval` primitive (and `wrapMcpTools` in
|
|
12
|
+
* `@ai-sdk/policy-opa`). See `docs/ai-sdk-mcp-integration.md`.
|
|
13
|
+
*
|
|
14
|
+
* Both `ai` and `@ai-sdk/mcp` are optional peer dependencies:
|
|
15
|
+
* importing this entrypoint requires them to be installed in the
|
|
16
|
+
* consumer project, but the rest of `@vercel/connect` works without
|
|
17
|
+
* them.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { createMCPClient } from '@ai-sdk/mcp';
|
|
21
|
+
* import { streamText } from 'ai';
|
|
22
|
+
* import {
|
|
23
|
+
* connectAuthProvider,
|
|
24
|
+
* ConsentRequiredError,
|
|
25
|
+
* } from '@vercel/connect/ai-sdk';
|
|
26
|
+
*
|
|
27
|
+
* const mcpClient = await createMCPClient({
|
|
28
|
+
* transport: {
|
|
29
|
+
* type: 'http',
|
|
30
|
+
* url: 'https://mcp.linear.app',
|
|
31
|
+
* authProvider: connectAuthProvider('oauth/linear', {
|
|
32
|
+
* subject: { type: 'user', id: userId },
|
|
33
|
+
* scopes: ['read'],
|
|
34
|
+
* }),
|
|
35
|
+
* },
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* try {
|
|
39
|
+
* const result = await streamText({
|
|
40
|
+
* model: 'openai/gpt-5.4',
|
|
41
|
+
* tools: await mcpClient.tools(),
|
|
42
|
+
* prompt,
|
|
43
|
+
* });
|
|
44
|
+
* return result.toUIMessageStreamResponse();
|
|
45
|
+
* } catch (err) {
|
|
46
|
+
* if (err instanceof ConsentRequiredError) return Response.redirect(err.url);
|
|
47
|
+
* throw err;
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export { connectAuthProvider, ConsentRequiredError, type ConnectAuthProviderOptions, type ConsentChallenge, } from '../mcp/connect-auth-provider.js';
|
|
52
|
+
export { ConnectError, ConnectorInstallationRequiredError, NoValidTokenError, UserAuthorizationRequiredError, type ConnectErrorOptions, type ConnectTokenParams, type ConnectTokenSubject, type ConnectVendorErrorPayload, } from '../token.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the `@vercel/connect/ai-sdk` subpath.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the MCP-spec `connectAuthProvider` (and its consent
|
|
5
|
+
* types) so AI SDK users have a single, ergonomic import. The
|
|
6
|
+
* provider plugs straight into `@ai-sdk/mcp`'s `createMCPClient`
|
|
7
|
+
* `authProvider` and works with AI SDK v6 and v7.
|
|
8
|
+
*
|
|
9
|
+
* Tool-call approval (Human-in-the-Loop) is intentionally not
|
|
10
|
+
* provided here — it is independent of Connect and already covered by
|
|
11
|
+
* the AI SDK's own `toolApproval` primitive (and `wrapMcpTools` in
|
|
12
|
+
* `@ai-sdk/policy-opa`). See `docs/ai-sdk-mcp-integration.md`.
|
|
13
|
+
*
|
|
14
|
+
* Both `ai` and `@ai-sdk/mcp` are optional peer dependencies:
|
|
15
|
+
* importing this entrypoint requires them to be installed in the
|
|
16
|
+
* consumer project, but the rest of `@vercel/connect` works without
|
|
17
|
+
* them.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { createMCPClient } from '@ai-sdk/mcp';
|
|
21
|
+
* import { streamText } from 'ai';
|
|
22
|
+
* import {
|
|
23
|
+
* connectAuthProvider,
|
|
24
|
+
* ConsentRequiredError,
|
|
25
|
+
* } from '@vercel/connect/ai-sdk';
|
|
26
|
+
*
|
|
27
|
+
* const mcpClient = await createMCPClient({
|
|
28
|
+
* transport: {
|
|
29
|
+
* type: 'http',
|
|
30
|
+
* url: 'https://mcp.linear.app',
|
|
31
|
+
* authProvider: connectAuthProvider('oauth/linear', {
|
|
32
|
+
* subject: { type: 'user', id: userId },
|
|
33
|
+
* scopes: ['read'],
|
|
34
|
+
* }),
|
|
35
|
+
* },
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* try {
|
|
39
|
+
* const result = await streamText({
|
|
40
|
+
* model: 'openai/gpt-5.4',
|
|
41
|
+
* tools: await mcpClient.tools(),
|
|
42
|
+
* prompt,
|
|
43
|
+
* });
|
|
44
|
+
* return result.toUIMessageStreamResponse();
|
|
45
|
+
* } catch (err) {
|
|
46
|
+
* if (err instanceof ConsentRequiredError) return Response.redirect(err.url);
|
|
47
|
+
* throw err;
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export { connectAuthProvider, ConsentRequiredError, } from '../mcp/connect-auth-provider.js';
|
|
52
|
+
export { ConnectError, ConnectorInstallationRequiredError, NoValidTokenError, UserAuthorizationRequiredError, } from '../token.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ConnectAuthorizationDetail = ConnectGitHubAppInstallationAuthorizationDetail | ConnectCustomAuthorizationDetail;
|
|
2
|
+
export interface ConnectCustomAuthorizationDetail {
|
|
3
|
+
type: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface ConnectGitHubAppInstallationAuthorizationDetail {
|
|
7
|
+
type: 'github_app_installation';
|
|
8
|
+
org?: string;
|
|
9
|
+
permissions?: string | string[];
|
|
10
|
+
repositories?: string | string[];
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AuthFn, type VerifyOidcConfig } from '
|
|
1
|
+
import { type AuthFn, type VerifyOidcConfig } from 'eve/channels/auth';
|
|
2
2
|
export declare const CONNECT_OAUTH_ISSUER = "https://connect.vercel.com";
|
|
3
3
|
export type ConnectOAuthEnvironment = 'production' | 'preview' | 'development';
|
|
4
4
|
export type ConnectOAuthAudienceEnvironment = ConnectOAuthEnvironment | '*';
|
|
@@ -23,7 +23,7 @@ export interface ConnectOAuthOptions {
|
|
|
23
23
|
*/
|
|
24
24
|
readonly environment?: ConnectOAuthAudienceEnvironment;
|
|
25
25
|
/**
|
|
26
|
-
* Optional gateway session id (`sub`) matchers. Patterns use
|
|
26
|
+
* Optional gateway session id (`sub`) matchers. Patterns use Eve's
|
|
27
27
|
* IAM-style `*` wildcard matching.
|
|
28
28
|
*/
|
|
29
29
|
readonly subjects?: readonly string[];
|
|
@@ -49,11 +49,11 @@ export interface ConnectOAuthOptions {
|
|
|
49
49
|
* `https://connect.vercel.com/.well-known/openid-configuration`.
|
|
50
50
|
*/
|
|
51
51
|
readonly discoveryUrl?: string;
|
|
52
|
-
/** Clock skew in seconds. Defaults to
|
|
52
|
+
/** Clock skew in seconds. Defaults to Eve's OIDC verifier default. */
|
|
53
53
|
readonly clockSkewSeconds?: number;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* Returns an
|
|
56
|
+
* Returns an Eve route auth callback for Vercel Connect OAuth gateway
|
|
57
57
|
* access tokens.
|
|
58
58
|
*
|
|
59
59
|
* The accepted token must be a bearer JWT issued by
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { extractBearerToken, verifyOidc, } from '
|
|
1
|
+
import { extractBearerToken, verifyOidc, } from 'eve/channels/auth';
|
|
2
2
|
export const CONNECT_OAUTH_ISSUER = 'https://connect.vercel.com';
|
|
3
3
|
/**
|
|
4
|
-
* Returns an
|
|
4
|
+
* Returns an Eve route auth callback for Vercel Connect OAuth gateway
|
|
5
5
|
* access tokens.
|
|
6
6
|
*
|
|
7
7
|
* The accepted token must be a bearer JWT issued by
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Eve adapter helper for `@vercel/connect`.
|
|
3
3
|
*
|
|
4
4
|
* {@link connect} turns a Vercel Connect OAuth connector id plus a
|
|
5
|
-
* principal type into a ready-made
|
|
5
|
+
* principal type into a ready-made Eve
|
|
6
6
|
* {@link AuthorizationDefinition} that the connection runtime
|
|
7
7
|
* consumes directly. The helper collapses the ~100 lines of
|
|
8
8
|
* `getToken` / `startAuthorization` / `completeAuthorization`
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* to a single call:
|
|
11
11
|
*
|
|
12
12
|
* ```ts
|
|
13
|
-
* import { defineMcpClientConnection } from "
|
|
14
|
-
* import { connect } from "@vercel/connect/
|
|
13
|
+
* import { defineMcpClientConnection } from "eve/connections";
|
|
14
|
+
* import { connect } from "@vercel/connect/eve";
|
|
15
15
|
*
|
|
16
16
|
* export default defineMcpClientConnection({
|
|
17
17
|
* url: "https://mcp.linear.app/sse",
|
|
@@ -22,34 +22,21 @@
|
|
|
22
22
|
*
|
|
23
23
|
* # Module layout
|
|
24
24
|
*
|
|
25
|
-
* This entrypoint is exposed at the `@vercel/connect/
|
|
26
|
-
* consumers that don't use
|
|
25
|
+
* This entrypoint is exposed at the `@vercel/connect/eve` subpath so
|
|
26
|
+
* consumers that don't use Eve never load it. `eve` is
|
|
27
27
|
* declared as an optional peer dependency: importing
|
|
28
|
-
* `@vercel/connect/
|
|
28
|
+
* `@vercel/connect/eve` requires Eve to be installed in the consumer
|
|
29
29
|
* project, but the rest of `@vercel/connect` works without it.
|
|
30
30
|
*/
|
|
31
|
-
import { type
|
|
32
|
-
import type { ConnectOptions, ConnectTokenParams } from '../token.js';
|
|
31
|
+
import { type ConnectionPrincipal, type InteractiveAuthorizationDefinition, type NonInteractiveAuthorizationDefinition } from 'eve/connections';
|
|
32
|
+
import type { ConnectOptions, ConnectTokenParams, ConnectTokenSubject } from '../token.js';
|
|
33
33
|
/**
|
|
34
|
-
* Authorization phase passed to {@link
|
|
34
|
+
* Authorization phase passed to {@link EveAuthorizationOptions.onError}
|
|
35
35
|
* so consumers can branch their error translation per callback.
|
|
36
36
|
*/
|
|
37
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
38
|
/** Options accepted by {@link connect}. */
|
|
52
|
-
export interface
|
|
39
|
+
export interface EveAuthorizationOptions {
|
|
53
40
|
/**
|
|
54
41
|
* Vercel Connect OAuth connector identifier. Accepts either the
|
|
55
42
|
* opaque service connector key (`scl_...`) or the human-readable
|
|
@@ -64,9 +51,9 @@ export interface AshAuthorizationOptions {
|
|
|
64
51
|
* on this choice:
|
|
65
52
|
*
|
|
66
53
|
* - `"user"` → full interactive OAuth definition with `getToken`,
|
|
67
|
-
* `startAuthorization`, and `completeAuthorization`.
|
|
54
|
+
* `startAuthorization`, and `completeAuthorization`. Eve will
|
|
68
55
|
* drive a consent flow through its framework-owned webhook.
|
|
69
|
-
* - `"app"` → non-interactive definition with `getToken` only.
|
|
56
|
+
* - `"app"` → non-interactive definition with `getToken` only. Eve
|
|
70
57
|
* never runs a consent flow for app-scoped connectors; a failure
|
|
71
58
|
* to fetch the token surfaces as a terminal authorization
|
|
72
59
|
* failure so the channel can prompt an operator to install the
|
|
@@ -82,6 +69,13 @@ export interface AshAuthorizationOptions {
|
|
|
82
69
|
* `authorizationDetails`. Passed through verbatim.
|
|
83
70
|
*/
|
|
84
71
|
readonly tokenParams?: Omit<ConnectTokenParams, 'subject'>;
|
|
72
|
+
/**
|
|
73
|
+
* Override how Eve's framework-resolved principal is mapped to a
|
|
74
|
+
* Vercel Connect token subject. When omitted, app principals map to
|
|
75
|
+
* `{ type: "app" }` and user principals map to
|
|
76
|
+
* `{ type: "user", id, issuer }`.
|
|
77
|
+
*/
|
|
78
|
+
readonly principalToSubject?: (principal: ConnectionPrincipal) => ConnectTokenSubject | Promise<ConnectTokenSubject>;
|
|
85
79
|
/**
|
|
86
80
|
* Low-level Vercel Connect SDK options (currently `vercelToken`
|
|
87
81
|
* for overriding the OIDC bearer). Most callers leave this unset
|
|
@@ -90,14 +84,14 @@ export interface AshAuthorizationOptions {
|
|
|
90
84
|
readonly connectOptions?: ConnectOptions;
|
|
91
85
|
/**
|
|
92
86
|
* Custom call-to-action rendered on the
|
|
93
|
-
* `connection.authorization_required` event. When omitted,
|
|
87
|
+
* `connection.authorization_required` event. When omitted, Eve
|
|
94
88
|
* fills in `Authorize <ConnectionName> in your browser to continue.`
|
|
95
89
|
* from the connection's filename.
|
|
96
90
|
*/
|
|
97
91
|
readonly instructions?: string;
|
|
98
92
|
/**
|
|
99
93
|
* Escape hatch for turning an unexpected Vercel Connect / network
|
|
100
|
-
* error into an
|
|
94
|
+
* error into an Eve-recognizable error. Called once per failure
|
|
101
95
|
* with the raw error and the phase that produced it.
|
|
102
96
|
*
|
|
103
97
|
* Return a new `Error` to replace the helper's default translation,
|
|
@@ -108,7 +102,7 @@ export interface AshAuthorizationOptions {
|
|
|
108
102
|
readonly onError?: (error: unknown, phase: ConnectAuthorizationPhase) => Error | undefined;
|
|
109
103
|
}
|
|
110
104
|
/** Input accepted by {@link connect}. */
|
|
111
|
-
export type
|
|
105
|
+
export type EveAuthorizationInput = string | EveAuthorizationOptions;
|
|
112
106
|
/**
|
|
113
107
|
* Structurally-readable marker exposed on every {@link connect} return
|
|
114
108
|
* value so downstream tooling can detect Vercel Connect-backed
|
|
@@ -132,35 +126,35 @@ export interface VercelConnectMetadata {
|
|
|
132
126
|
readonly connector: string;
|
|
133
127
|
}
|
|
134
128
|
/**
|
|
135
|
-
* Augments the standard
|
|
129
|
+
* Augments the standard Eve authorization shape with the
|
|
136
130
|
* {@link VercelConnectMetadata} marker, narrowed to whichever flavour
|
|
137
131
|
* `connect()` produces.
|
|
138
132
|
*/
|
|
139
|
-
export type
|
|
133
|
+
export type EveConnectAuthorizationDefinition<TAuthorization extends InteractiveAuthorizationDefinition | NonInteractiveAuthorizationDefinition> = TAuthorization & {
|
|
140
134
|
readonly vercelConnect: VercelConnectMetadata;
|
|
141
135
|
};
|
|
142
136
|
/**
|
|
143
|
-
* Builds an
|
|
137
|
+
* Builds an Eve {@link AuthorizationDefinition} backed by Vercel
|
|
144
138
|
* Connect. The return type narrows based on
|
|
145
|
-
* {@link
|
|
139
|
+
* {@link EveAuthorizationOptions.principalType}:
|
|
146
140
|
*
|
|
147
141
|
* - omitted or `principalType: "user"` returns an
|
|
148
|
-
* {@link InteractiveAuthorizationDefinition};
|
|
142
|
+
* {@link InteractiveAuthorizationDefinition}; Eve drives a consent
|
|
149
143
|
* flow through its framework-owned webhook.
|
|
150
144
|
* - `principalType: "app"` returns a
|
|
151
|
-
* {@link NonInteractiveAuthorizationDefinition};
|
|
145
|
+
* {@link NonInteractiveAuthorizationDefinition}; Eve never runs a
|
|
152
146
|
* consent flow for app-scoped connectors.
|
|
153
147
|
*
|
|
154
148
|
* Every returned definition also carries a {@link VercelConnectMetadata}
|
|
155
|
-
* marker on its `vercelConnect` field so downstream tooling (
|
|
149
|
+
* marker on its `vercelConnect` field so downstream tooling (Eve
|
|
156
150
|
* compiler, dashboards) can detect Vercel Connect-backed connections
|
|
157
151
|
* without inspecting closure state.
|
|
158
152
|
*/
|
|
159
|
-
export declare function connect(connector: string):
|
|
160
|
-
export declare function connect(options:
|
|
153
|
+
export declare function connect(connector: string): EveConnectAuthorizationDefinition<InteractiveAuthorizationDefinition>;
|
|
154
|
+
export declare function connect(options: EveAuthorizationOptions & {
|
|
161
155
|
readonly principalType?: 'user';
|
|
162
|
-
}):
|
|
163
|
-
export declare function connect(options:
|
|
156
|
+
}): EveConnectAuthorizationDefinition<InteractiveAuthorizationDefinition>;
|
|
157
|
+
export declare function connect(options: EveAuthorizationOptions & {
|
|
164
158
|
readonly principalType: 'app';
|
|
165
|
-
}):
|
|
166
|
-
export declare function connect(options:
|
|
159
|
+
}): EveConnectAuthorizationDefinition<NonInteractiveAuthorizationDefinition>;
|
|
160
|
+
export declare function connect(options: EveAuthorizationInput): EveConnectAuthorizationDefinition<InteractiveAuthorizationDefinition | NonInteractiveAuthorizationDefinition>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Eve adapter helper for `@vercel/connect`.
|
|
3
3
|
*
|
|
4
4
|
* {@link connect} turns a Vercel Connect OAuth connector id plus a
|
|
5
|
-
* principal type into a ready-made
|
|
5
|
+
* principal type into a ready-made Eve
|
|
6
6
|
* {@link AuthorizationDefinition} that the connection runtime
|
|
7
7
|
* consumes directly. The helper collapses the ~100 lines of
|
|
8
8
|
* `getToken` / `startAuthorization` / `completeAuthorization`
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* to a single call:
|
|
11
11
|
*
|
|
12
12
|
* ```ts
|
|
13
|
-
* import { defineMcpClientConnection } from "
|
|
14
|
-
* import { connect } from "@vercel/connect/
|
|
13
|
+
* import { defineMcpClientConnection } from "eve/connections";
|
|
14
|
+
* import { connect } from "@vercel/connect/eve";
|
|
15
15
|
*
|
|
16
16
|
* export default defineMcpClientConnection({
|
|
17
17
|
* url: "https://mcp.linear.app/sse",
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
*
|
|
23
23
|
* # Module layout
|
|
24
24
|
*
|
|
25
|
-
* This entrypoint is exposed at the `@vercel/connect/
|
|
26
|
-
* consumers that don't use
|
|
25
|
+
* This entrypoint is exposed at the `@vercel/connect/eve` subpath so
|
|
26
|
+
* consumers that don't use Eve never load it. `eve` is
|
|
27
27
|
* declared as an optional peer dependency: importing
|
|
28
|
-
* `@vercel/connect/
|
|
28
|
+
* `@vercel/connect/eve` requires Eve to be installed in the consumer
|
|
29
29
|
* project, but the rest of `@vercel/connect` works without it.
|
|
30
30
|
*/
|
|
31
|
-
import { ConnectionAuthorizationFailedError, ConnectionAuthorizationRequiredError, } from '
|
|
31
|
+
import { ConnectionAuthorizationFailedError, ConnectionAuthorizationRequiredError, } from 'eve/connections';
|
|
32
32
|
import { startAuthorization } from '../authorization.js';
|
|
33
33
|
import { ConnectorInstallationRequiredError, getTokenResponse, NoValidTokenError, UserAuthorizationRequiredError, } from '../token.js';
|
|
34
34
|
export function connect(input) {
|
|
@@ -50,7 +50,7 @@ function buildInteractiveDefinition(options) {
|
|
|
50
50
|
principalType: 'user',
|
|
51
51
|
async getToken({ principal }) {
|
|
52
52
|
try {
|
|
53
|
-
const response = await getTokenResponse(options.connector, buildTokenParams(options, principal), options.connectOptions);
|
|
53
|
+
const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal), options.connectOptions);
|
|
54
54
|
return { token: response.token, expiresAt: response.expiresAt };
|
|
55
55
|
}
|
|
56
56
|
catch (error) {
|
|
@@ -59,7 +59,7 @@ function buildInteractiveDefinition(options) {
|
|
|
59
59
|
},
|
|
60
60
|
async startAuthorization({ principal, callbackUrl, webhook, }) {
|
|
61
61
|
try {
|
|
62
|
-
//
|
|
62
|
+
// Eve's `webhook` parameter is semantically a browser-redirect
|
|
63
63
|
// target — the orchestrator mints it via `createWebhook({
|
|
64
64
|
// respondWith: buildAuthorizationCompletePage() })` so the
|
|
65
65
|
// user lands on a friendly "you can close this tab" page after
|
|
@@ -74,13 +74,13 @@ function buildInteractiveDefinition(options) {
|
|
|
74
74
|
// `webhook:` (server-POST) field, even though it would
|
|
75
75
|
// survive the user closing the consent tab right after IdP
|
|
76
76
|
// callback. That mode shows the user Vercel Connect's
|
|
77
|
-
// generic "close this window" page instead of
|
|
77
|
+
// generic "close this window" page instead of Eve's branded
|
|
78
78
|
// landing page, and the helper would need to grow
|
|
79
|
-
// protocol-aware logic that diverges from the simple "
|
|
79
|
+
// protocol-aware logic that diverges from the simple "Eve
|
|
80
80
|
// mints one URL, Vercel Connect redirects there" mental
|
|
81
81
|
// model. Revisit if tab-close timeouts become a real problem
|
|
82
82
|
// in production.
|
|
83
|
-
const response = await startAuthorization(options.connector, buildTokenParams(options, principal), {
|
|
83
|
+
const response = await startAuthorization(options.connector, await buildTokenParams(options, principal), {
|
|
84
84
|
...options.connectOptions,
|
|
85
85
|
callbackUrl: callbackUrl ?? webhook,
|
|
86
86
|
deviceCode: true,
|
|
@@ -96,7 +96,6 @@ function buildInteractiveDefinition(options) {
|
|
|
96
96
|
? { instructions: options.instructions }
|
|
97
97
|
: null),
|
|
98
98
|
},
|
|
99
|
-
state: { verifier: response.verifier },
|
|
100
99
|
};
|
|
101
100
|
}
|
|
102
101
|
catch (error) {
|
|
@@ -105,7 +104,7 @@ function buildInteractiveDefinition(options) {
|
|
|
105
104
|
},
|
|
106
105
|
async completeAuthorization({ principal, }) {
|
|
107
106
|
try {
|
|
108
|
-
const response = await getTokenResponse(options.connector, buildTokenParams(options, principal), options.connectOptions);
|
|
107
|
+
const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal), options.connectOptions);
|
|
109
108
|
return { token: response.token, expiresAt: response.expiresAt };
|
|
110
109
|
}
|
|
111
110
|
catch (error) {
|
|
@@ -119,7 +118,7 @@ function buildNonInteractiveDefinition(options) {
|
|
|
119
118
|
principalType: 'app',
|
|
120
119
|
async getToken({ principal }) {
|
|
121
120
|
try {
|
|
122
|
-
const response = await getTokenResponse(options.connector, buildTokenParams(options, principal), options.connectOptions);
|
|
121
|
+
const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal), options.connectOptions);
|
|
123
122
|
return { token: response.token, expiresAt: response.expiresAt };
|
|
124
123
|
}
|
|
125
124
|
catch (error) {
|
|
@@ -128,10 +127,11 @@ function buildNonInteractiveDefinition(options) {
|
|
|
128
127
|
},
|
|
129
128
|
};
|
|
130
129
|
}
|
|
131
|
-
function buildTokenParams(options, principal) {
|
|
130
|
+
async function buildTokenParams(options, principal) {
|
|
131
|
+
const toSubject = options.principalToSubject ?? principalToSubject;
|
|
132
132
|
return {
|
|
133
133
|
...options.tokenParams,
|
|
134
|
-
subject:
|
|
134
|
+
subject: await toSubject(principal),
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
function principalToSubject(principal) {
|
|
@@ -141,11 +141,11 @@ function principalToSubject(principal) {
|
|
|
141
141
|
return { type: 'user', id: principal.id, issuer: principal.issuer };
|
|
142
142
|
}
|
|
143
143
|
/**
|
|
144
|
-
* Translates raw Vercel Connect errors into
|
|
144
|
+
* Translates raw Vercel Connect errors into Eve's public
|
|
145
145
|
* {@link ConnectionAuthorizationRequiredError} /
|
|
146
|
-
* {@link ConnectionAuthorizationFailedError} classes.
|
|
146
|
+
* {@link ConnectionAuthorizationFailedError} classes. Eve discriminates
|
|
147
147
|
* on `err.name` (not `instanceof`), so even if the consumer's bundle
|
|
148
|
-
* loads a different copy of `
|
|
148
|
+
* loads a different copy of `eve` than this helper, the
|
|
149
149
|
* runtime still recognizes the throw.
|
|
150
150
|
*/
|
|
151
151
|
function translate(error, phase, options) {
|
|
@@ -155,7 +155,7 @@ function translate(error, phase, options) {
|
|
|
155
155
|
// `UserAuthorizationRequiredError` and `NoValidTokenError` both
|
|
156
156
|
// mean "Vercel Connect has no valid credential for this principal
|
|
157
157
|
// yet". For interactive (user) connectors that's recoverable via
|
|
158
|
-
// a consent flow;
|
|
158
|
+
// a consent flow; Eve will see the `Required` throw and drive
|
|
159
159
|
// `startAuthorization`. For app connectors it's terminal — there
|
|
160
160
|
// is nobody to consent — so we surface `Failed` with
|
|
161
161
|
// `retryable: false`.
|
|
@@ -169,7 +169,7 @@ function translate(error, phase, options) {
|
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
171
|
if (phase === 'completeAuthorization') {
|
|
172
|
-
// The consent leg reported success (
|
|
172
|
+
// The consent leg reported success (Eve would not call us
|
|
173
173
|
// otherwise) but Vercel Connect still says the user is
|
|
174
174
|
// unauthorized. Default to retryable so the model can
|
|
175
175
|
// re-prompt; most cases (network blip, replay) resolve on
|
|
@@ -192,7 +192,7 @@ function translate(error, phase, options) {
|
|
|
192
192
|
retryable: false,
|
|
193
193
|
});
|
|
194
194
|
}
|
|
195
|
-
// Every other error is re-thrown verbatim.
|
|
195
|
+
// Every other error is re-thrown verbatim. Eve treats an unknown
|
|
196
196
|
// throw from `completeAuthorization` as a retryable failure, so the
|
|
197
197
|
// default behavior stays intuitive.
|
|
198
198
|
return error instanceof Error ? error : new Error(String(error));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the `@vercel/connect/eve` subpath.
|
|
3
|
+
*
|
|
4
|
+
* Holds Eve-specific helpers that adapt the Vercel Connect SDK to
|
|
5
|
+
* Eve's connection runtime. Each helper lives in its own module;
|
|
6
|
+
* this barrel re-exports the public API so consumers import
|
|
7
|
+
* everything from `@vercel/connect/eve`.
|
|
8
|
+
*/
|
|
9
|
+
export { connect, type EveAuthorizationInput, type EveAuthorizationOptions, type EveConnectAuthorizationDefinition, type ConnectAuthorizationPhase, type VercelConnectMetadata, } from './connection-authorization.js';
|
|
10
|
+
export { CONNECT_OAUTH_ISSUER, connectOAuth, type ConnectOAuthAudienceEnvironment, type ConnectOAuthEnvironment, type ConnectOAuthOptions, } from './connect-oauth.js';
|
|
11
|
+
export { connectSlackCredentials } from './slack-credentials.js';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public surface of the `@vercel/connect/
|
|
2
|
+
* Public surface of the `@vercel/connect/eve` subpath.
|
|
3
3
|
*
|
|
4
|
-
* Holds
|
|
5
|
-
*
|
|
4
|
+
* Holds Eve-specific helpers that adapt the Vercel Connect SDK to
|
|
5
|
+
* Eve's connection runtime. Each helper lives in its own module;
|
|
6
6
|
* this barrel re-exports the public API so consumers import
|
|
7
|
-
* everything from `@vercel/connect/
|
|
7
|
+
* everything from `@vercel/connect/eve`.
|
|
8
8
|
*/
|
|
9
9
|
export { connect, } from './connection-authorization.js';
|
|
10
10
|
export { CONNECT_OAUTH_ISSUER, connectOAuth, } from './connect-oauth.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SlackChannelCredentials } from 'eve/channels/slack';
|
|
2
|
+
import { type ConnectOptions, type ConnectTokenParams } from '../index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Token parameters accepted by {@link connectSlackCredentials}.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors {@link ConnectTokenParams} from `@vercel/connect`, minus
|
|
7
|
+
* `subject` — Slack bot tokens are always app-scoped, so `subject`
|
|
8
|
+
* is pinned to `{ type: "app" }` by this helper and cannot be
|
|
9
|
+
* overridden.
|
|
10
|
+
*/
|
|
11
|
+
export type ConnectSlackCredentialsParams = Omit<ConnectTokenParams, 'subject'>;
|
|
12
|
+
/**
|
|
13
|
+
* Build {@link SlackChannelCredentials} backed by a Vercel Connect
|
|
14
|
+
* connector that stores a Slack workspace's bot token.
|
|
15
|
+
*
|
|
16
|
+
* The returned `botToken` is a function form, invoked once per
|
|
17
|
+
* inbound webhook so the chat adapter always picks up a fresh token
|
|
18
|
+
* from Vercel Connect (rotation, refresh, multi-workspace tenancy
|
|
19
|
+
* are all handled server-side).
|
|
20
|
+
*
|
|
21
|
+
* Slack bot tokens are app-scoped — one token per workspace install,
|
|
22
|
+
* shared across every end-user — so this helper calls Vercel Connect
|
|
23
|
+
* with `subject: { type: "app" }`. End-user identity (per-user OAuth
|
|
24
|
+
* into Slack) is a separate concern handled elsewhere.
|
|
25
|
+
*
|
|
26
|
+
* The optional `params` and `options` arguments mirror the signature
|
|
27
|
+
* of {@link getToken}, allowing callers to pass through fields like
|
|
28
|
+
* `installationId`, `scopes`, or `validityBufferMs`.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { slackRoute } from "eve/channels/slack";
|
|
32
|
+
* import { connectSlackCredentials } from "@vercel/connect/eve";
|
|
33
|
+
*
|
|
34
|
+
* export default slackRoute({
|
|
35
|
+
* credentials: connectSlackCredentials("scl_..."),
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Multi-workspace deployments can select a specific workspace install
|
|
40
|
+
* via `installationId`:
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* connectSlackCredentials("scl_...", { installationId: workspaceId });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function connectSlackCredentials(connector: string, params?: ConnectSlackCredentialsParams, options?: ConnectOptions): SlackChannelCredentials;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getToken } from '../index.js';
|
|
2
|
-
import { vercelOidc } from '
|
|
1
|
+
import { getToken, } from '../index.js';
|
|
2
|
+
import { vercelOidc } from 'eve/channels/auth';
|
|
3
3
|
/**
|
|
4
4
|
* Build {@link SlackChannelCredentials} backed by a Vercel Connect
|
|
5
5
|
* connector that stores a Slack workspace's bot token.
|
|
@@ -14,18 +14,29 @@ import { vercelOidc } from 'experimental-ash/channels/auth';
|
|
|
14
14
|
* with `subject: { type: "app" }`. End-user identity (per-user OAuth
|
|
15
15
|
* into Slack) is a separate concern handled elsewhere.
|
|
16
16
|
*
|
|
17
|
+
* The optional `params` and `options` arguments mirror the signature
|
|
18
|
+
* of {@link getToken}, allowing callers to pass through fields like
|
|
19
|
+
* `installationId`, `scopes`, or `validityBufferMs`.
|
|
20
|
+
*
|
|
17
21
|
* ```ts
|
|
18
|
-
* import { slackRoute } from "
|
|
19
|
-
* import { connectSlackCredentials } from "@vercel/connect/
|
|
22
|
+
* import { slackRoute } from "eve/channels/slack";
|
|
23
|
+
* import { connectSlackCredentials } from "@vercel/connect/eve";
|
|
20
24
|
*
|
|
21
25
|
* export default slackRoute({
|
|
22
26
|
* credentials: connectSlackCredentials("scl_..."),
|
|
23
27
|
* });
|
|
24
28
|
* ```
|
|
29
|
+
*
|
|
30
|
+
* Multi-workspace deployments can select a specific workspace install
|
|
31
|
+
* via `installationId`:
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* connectSlackCredentials("scl_...", { installationId: workspaceId });
|
|
35
|
+
* ```
|
|
25
36
|
*/
|
|
26
|
-
export function connectSlackCredentials(connector) {
|
|
37
|
+
export function connectSlackCredentials(connector, params = {}, options) {
|
|
27
38
|
return {
|
|
28
|
-
botToken: () => getToken(connector, { subject: { type: 'app' } }),
|
|
39
|
+
botToken: () => getToken(connector, { ...params, subject: { type: 'app' } }, options),
|
|
29
40
|
webhookVerifier: vercelOidc(),
|
|
30
41
|
};
|
|
31
42
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { getToken, getTokenResponse, ConnectError, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, type ConnectErrorOptions, type ConnectOptions, type ConnectTokenParams, type ConnectTokenResponse, type ConnectVendorErrorPayload, } from './token.js';
|
|
1
|
+
export { getToken, getTokenResponse, ConnectError, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, type ConnectErrorOptions, type ConnectOptions, type ConnectTokenParams, type ConnectTokenResponse, type ConnectTokenSubject, type ConnectVendorErrorPayload, } from './token.js';
|
|
2
2
|
export { startAuthorization, type ConnectAuthorizationOptions, type ConnectAuthorizationResponse, } from './authorization.js';
|
|
3
|
+
export type { ConnectAuthorizationDetail } from './authorization-details.js';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { OAuthClientProvider } from '@ai-sdk/mcp';
|
|
2
|
+
import { type ConnectTokenParams } from '../token.js';
|
|
3
|
+
/** Options accepted by {@link connectAuthProvider}. */
|
|
4
|
+
export interface ConnectAuthProviderOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Override the Vercel OIDC token used to authenticate against the
|
|
7
|
+
* Connect API. Defaults to the `@vercel/oidc` resolver used by the
|
|
8
|
+
* rest of `@vercel/connect`. Useful for tests and non-Vercel
|
|
9
|
+
* runtimes.
|
|
10
|
+
*
|
|
11
|
+
* Accepts a callback because the adapter is long-lived: `tokens()`
|
|
12
|
+
* is invoked across many turns and refreshes, so a captured string
|
|
13
|
+
* would go stale. When a function is provided it is resolved at
|
|
14
|
+
* call time.
|
|
15
|
+
*/
|
|
16
|
+
readonly vercelToken?: string | (() => string | Promise<string>);
|
|
17
|
+
/**
|
|
18
|
+
* Where to send the user after they finish granting access on
|
|
19
|
+
* Connect's hosted consent page. Forwarded to Connect's
|
|
20
|
+
* `startAuthorization` (as its `callbackUrl`) and also surfaced
|
|
21
|
+
* through the MCP-spec `OAuthClientProvider.redirectUrl` getter.
|
|
22
|
+
*
|
|
23
|
+
* Defaults to an empty string — Connect then falls back to the
|
|
24
|
+
* connector's server-side registered redirect.
|
|
25
|
+
*/
|
|
26
|
+
readonly redirectUrl?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Request the device-authorization flow when minting a consent
|
|
29
|
+
* challenge. When `true`, Connect returns a `deviceCode` (and
|
|
30
|
+
* `expiresAt`) on the {@link ConsentChallenge} for out-of-band /
|
|
31
|
+
* headless approval. Defaults to `false`.
|
|
32
|
+
*/
|
|
33
|
+
readonly deviceCode?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Called when Vercel Connect reports that the user has not yet
|
|
36
|
+
* authorized the connector. The caller decides how to surface the
|
|
37
|
+
* consent URL — `redirect()`, throw a typed error, emit a custom
|
|
38
|
+
* UI chunk, etc. If omitted, `connectAuthProvider` throws
|
|
39
|
+
* {@link ConsentRequiredError}.
|
|
40
|
+
*/
|
|
41
|
+
readonly onConsentRequired?: (challenge: ConsentChallenge) => void | Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returned to {@link ConnectAuthProviderOptions.onConsentRequired} (or
|
|
45
|
+
* raised inside {@link ConsentRequiredError}) when Connect has no
|
|
46
|
+
* cached grant for the configured subject and the caller must surface
|
|
47
|
+
* the consent URL.
|
|
48
|
+
*/
|
|
49
|
+
export interface ConsentChallenge {
|
|
50
|
+
readonly connector: string;
|
|
51
|
+
readonly subject: ConnectTokenParams['subject'];
|
|
52
|
+
/** Consent URL to redirect the user to. */
|
|
53
|
+
readonly url: string;
|
|
54
|
+
readonly request: string;
|
|
55
|
+
readonly verifier: string;
|
|
56
|
+
/**
|
|
57
|
+
* Device code to display to the user when Connect issues a
|
|
58
|
+
* device-flow authorization. Present only when the connector uses
|
|
59
|
+
* device flow.
|
|
60
|
+
*/
|
|
61
|
+
readonly deviceCode?: string;
|
|
62
|
+
/** Epoch-ms expiry of the consent challenge, when Connect reports one. */
|
|
63
|
+
readonly expiresAt?: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Thrown by the {@link connectAuthProvider} default
|
|
67
|
+
* `onConsentRequired` handler when the user has no Connect grant for
|
|
68
|
+
* the configured subject. Catch at the boundary
|
|
69
|
+
* (`route handler` / `streamText` call site) and redirect to
|
|
70
|
+
* `error.url`.
|
|
71
|
+
*/
|
|
72
|
+
export declare class ConsentRequiredError extends Error {
|
|
73
|
+
readonly name = "ConsentRequiredError";
|
|
74
|
+
readonly connector: string;
|
|
75
|
+
readonly subject: ConnectTokenParams['subject'];
|
|
76
|
+
readonly url: string;
|
|
77
|
+
readonly request: string;
|
|
78
|
+
readonly verifier: string;
|
|
79
|
+
readonly deviceCode?: string;
|
|
80
|
+
readonly expiresAt?: number;
|
|
81
|
+
constructor(challenge: ConsentChallenge);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Builds an MCP-spec {@link OAuthClientProvider} backed by Vercel
|
|
85
|
+
* Connect. The returned object delegates `tokens()` to
|
|
86
|
+
* {@link getTokenResponse} and `redirectToAuthorization` to
|
|
87
|
+
* {@link startAuthorization}. Connect owns client registration, PKCE,
|
|
88
|
+
* state, and the callback handshake server-side, so most of the
|
|
89
|
+
* `OAuthClientProvider` surface is intentionally a no-op.
|
|
90
|
+
*
|
|
91
|
+
* @param connector Vercel Connect connector UID (e.g. `oauth/linear`)
|
|
92
|
+
* or opaque connector id.
|
|
93
|
+
* @param params Token request parameters — always specify a
|
|
94
|
+
* `subject`; the three subject types
|
|
95
|
+
* (`'app'` / `'user'` / `'jwt-bearer'`) have distinct security
|
|
96
|
+
* semantics.
|
|
97
|
+
* @param options Optional adapter behavior overrides.
|
|
98
|
+
*/
|
|
99
|
+
export declare function connectAuthProvider(connector: string, params: ConnectTokenParams, options?: ConnectAuthProviderOptions): OAuthClientProvider;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { startAuthorization } from '../authorization.js';
|
|
2
|
+
import { ConnectorInstallationRequiredError, UserAuthorizationRequiredError, getTokenResponse, } from '../token.js';
|
|
3
|
+
/**
|
|
4
|
+
* Thrown by the {@link connectAuthProvider} default
|
|
5
|
+
* `onConsentRequired` handler when the user has no Connect grant for
|
|
6
|
+
* the configured subject. Catch at the boundary
|
|
7
|
+
* (`route handler` / `streamText` call site) and redirect to
|
|
8
|
+
* `error.url`.
|
|
9
|
+
*/
|
|
10
|
+
export class ConsentRequiredError extends Error {
|
|
11
|
+
name = 'ConsentRequiredError';
|
|
12
|
+
connector;
|
|
13
|
+
subject;
|
|
14
|
+
url;
|
|
15
|
+
request;
|
|
16
|
+
verifier;
|
|
17
|
+
deviceCode;
|
|
18
|
+
expiresAt;
|
|
19
|
+
constructor(challenge) {
|
|
20
|
+
super(`Vercel Connect: user authorization required for connector "${challenge.connector}". Redirect the user to challenge.url to grant access.`);
|
|
21
|
+
this.connector = challenge.connector;
|
|
22
|
+
this.subject = challenge.subject;
|
|
23
|
+
this.url = challenge.url;
|
|
24
|
+
this.request = challenge.request;
|
|
25
|
+
this.verifier = challenge.verifier;
|
|
26
|
+
this.deviceCode = challenge.deviceCode;
|
|
27
|
+
this.expiresAt = challenge.expiresAt;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const EMPTY_CLIENT_METADATA = {
|
|
31
|
+
redirect_uris: [],
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Builds an MCP-spec {@link OAuthClientProvider} backed by Vercel
|
|
35
|
+
* Connect. The returned object delegates `tokens()` to
|
|
36
|
+
* {@link getTokenResponse} and `redirectToAuthorization` to
|
|
37
|
+
* {@link startAuthorization}. Connect owns client registration, PKCE,
|
|
38
|
+
* state, and the callback handshake server-side, so most of the
|
|
39
|
+
* `OAuthClientProvider` surface is intentionally a no-op.
|
|
40
|
+
*
|
|
41
|
+
* @param connector Vercel Connect connector UID (e.g. `oauth/linear`)
|
|
42
|
+
* or opaque connector id.
|
|
43
|
+
* @param params Token request parameters — always specify a
|
|
44
|
+
* `subject`; the three subject types
|
|
45
|
+
* (`'app'` / `'user'` / `'jwt-bearer'`) have distinct security
|
|
46
|
+
* semantics.
|
|
47
|
+
* @param options Optional adapter behavior overrides.
|
|
48
|
+
*/
|
|
49
|
+
export function connectAuthProvider(connector, params, options) {
|
|
50
|
+
const redirectUrl = options?.redirectUrl ?? '';
|
|
51
|
+
async function resolveVercelToken() {
|
|
52
|
+
const { vercelToken } = options ?? {};
|
|
53
|
+
if (typeof vercelToken === 'function') {
|
|
54
|
+
return vercelToken();
|
|
55
|
+
}
|
|
56
|
+
return vercelToken;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
async tokens() {
|
|
60
|
+
try {
|
|
61
|
+
const vercelToken = await resolveVercelToken();
|
|
62
|
+
const response = await getTokenResponse(connector, params, vercelToken !== undefined ? { vercelToken } : undefined);
|
|
63
|
+
const expiresInSeconds = Math.max(1, Math.floor((response.expiresAt - Date.now()) / 1000));
|
|
64
|
+
return {
|
|
65
|
+
access_token: response.token,
|
|
66
|
+
token_type: 'Bearer',
|
|
67
|
+
expires_in: expiresInSeconds,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err instanceof UserAuthorizationRequiredError) {
|
|
72
|
+
// Returning undefined lets the MCP transport's auth()
|
|
73
|
+
// orchestrator trigger redirectToAuthorization below,
|
|
74
|
+
// where we surface the Connect consent URL.
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
if (err instanceof ConnectorInstallationRequiredError) {
|
|
78
|
+
// Configuration problem, not a per-user issue. Surface
|
|
79
|
+
// immediately rather than masking as a consent challenge.
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
async redirectToAuthorization(_authorizationUrl) {
|
|
86
|
+
// The URL argument is the MCP server's discovered authorization
|
|
87
|
+
// endpoint. We ignore it — Connect has its own consent URL
|
|
88
|
+
// backed by the connector's registered OAuth client.
|
|
89
|
+
const vercelToken = await resolveVercelToken();
|
|
90
|
+
const response = await startAuthorization(connector, params, {
|
|
91
|
+
...(vercelToken !== undefined && { vercelToken }),
|
|
92
|
+
// Forward the post-consent return URL so the user lands back
|
|
93
|
+
// in the app. Falsy (the default empty string) => Connect uses
|
|
94
|
+
// the connector's registered redirect.
|
|
95
|
+
...(redirectUrl && { callbackUrl: redirectUrl }),
|
|
96
|
+
...(options?.deviceCode && { deviceCode: true }),
|
|
97
|
+
});
|
|
98
|
+
const challenge = {
|
|
99
|
+
connector,
|
|
100
|
+
subject: params.subject,
|
|
101
|
+
url: response.url,
|
|
102
|
+
request: response.request,
|
|
103
|
+
verifier: response.verifier,
|
|
104
|
+
deviceCode: response.deviceCode,
|
|
105
|
+
expiresAt: response.expiresAt,
|
|
106
|
+
};
|
|
107
|
+
if (options?.onConsentRequired) {
|
|
108
|
+
await options.onConsentRequired(challenge);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
throw new ConsentRequiredError(challenge);
|
|
112
|
+
},
|
|
113
|
+
saveTokens(_tokens) {
|
|
114
|
+
// No-op: Connect owns token persistence server-side. The
|
|
115
|
+
// in-memory LRU cache in @vercel/connect's token module
|
|
116
|
+
// handles per-request reuse.
|
|
117
|
+
},
|
|
118
|
+
saveCodeVerifier(_codeVerifier) {
|
|
119
|
+
// No-op: Connect owns PKCE.
|
|
120
|
+
},
|
|
121
|
+
codeVerifier() {
|
|
122
|
+
// The MCP transport never reaches this when tokens() /
|
|
123
|
+
// redirectToAuthorization are wired correctly. If a future
|
|
124
|
+
// transport change starts calling it, throw loudly rather
|
|
125
|
+
// than fabricate a bogus verifier.
|
|
126
|
+
throw new Error('@vercel/connect: OAuthClientProvider.codeVerifier() is unsupported. Vercel Connect owns the PKCE flow server-side; this method should never be invoked by a correctly-configured MCP transport.');
|
|
127
|
+
},
|
|
128
|
+
get redirectUrl() {
|
|
129
|
+
// Same value forwarded to Connect as the post-consent return
|
|
130
|
+
// URL. Surfaced here to satisfy the OAuthClientProvider
|
|
131
|
+
// contract; the standard flow that reads it is never triggered.
|
|
132
|
+
return redirectUrl;
|
|
133
|
+
},
|
|
134
|
+
get clientMetadata() {
|
|
135
|
+
// Returned only during dynamic client registration, which we
|
|
136
|
+
// never trigger. A minimal but valid value satisfies the
|
|
137
|
+
// contract.
|
|
138
|
+
return EMPTY_CLIENT_METADATA;
|
|
139
|
+
},
|
|
140
|
+
clientInformation() {
|
|
141
|
+
// The connector UID stands in as the logical client id. The
|
|
142
|
+
// MCP transport uses this for cache keys and debug output;
|
|
143
|
+
// tying it to the Connect identifier is more useful than
|
|
144
|
+
// returning a synthetic value.
|
|
145
|
+
return { client_id: connector };
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the `@vercel/connect/mcp` subpath.
|
|
3
|
+
*
|
|
4
|
+
* Holds the MCP-spec `OAuthClientProvider` adapter for Vercel
|
|
5
|
+
* Connect. Plug the returned object into any MCP client that
|
|
6
|
+
* consumes the spec — `@ai-sdk/mcp`, the official
|
|
7
|
+
* `@modelcontextprotocol/typescript-sdk`, Mastra's MCP client,
|
|
8
|
+
* etc. AI SDK consumers should import the same function from
|
|
9
|
+
* `@vercel/connect/ai-sdk` instead; that subpath re-exports from
|
|
10
|
+
* here.
|
|
11
|
+
*
|
|
12
|
+
* `@ai-sdk/mcp` is an optional peer dependency: importing this
|
|
13
|
+
* entrypoint requires it to be installed in the consumer project,
|
|
14
|
+
* but the rest of `@vercel/connect` works without it.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { createMCPClient } from '@ai-sdk/mcp';
|
|
18
|
+
* import { connectAuthProvider } from '@vercel/connect/mcp';
|
|
19
|
+
*
|
|
20
|
+
* const mcpClient = await createMCPClient({
|
|
21
|
+
* transport: {
|
|
22
|
+
* type: 'http',
|
|
23
|
+
* url: 'https://mcp.linear.app',
|
|
24
|
+
* authProvider: connectAuthProvider('oauth/linear', {
|
|
25
|
+
* subject: { type: 'user', id: userId },
|
|
26
|
+
* scopes: ['read'],
|
|
27
|
+
* }),
|
|
28
|
+
* },
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export { connectAuthProvider, ConsentRequiredError, type ConnectAuthProviderOptions, type ConsentChallenge, } from './connect-auth-provider.js';
|
|
33
|
+
export { ConnectError, ConnectorInstallationRequiredError, NoValidTokenError, UserAuthorizationRequiredError, type ConnectErrorOptions, type ConnectTokenParams, type ConnectTokenSubject, type ConnectVendorErrorPayload, } from '../token.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the `@vercel/connect/mcp` subpath.
|
|
3
|
+
*
|
|
4
|
+
* Holds the MCP-spec `OAuthClientProvider` adapter for Vercel
|
|
5
|
+
* Connect. Plug the returned object into any MCP client that
|
|
6
|
+
* consumes the spec — `@ai-sdk/mcp`, the official
|
|
7
|
+
* `@modelcontextprotocol/typescript-sdk`, Mastra's MCP client,
|
|
8
|
+
* etc. AI SDK consumers should import the same function from
|
|
9
|
+
* `@vercel/connect/ai-sdk` instead; that subpath re-exports from
|
|
10
|
+
* here.
|
|
11
|
+
*
|
|
12
|
+
* `@ai-sdk/mcp` is an optional peer dependency: importing this
|
|
13
|
+
* entrypoint requires it to be installed in the consumer project,
|
|
14
|
+
* but the rest of `@vercel/connect` works without it.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { createMCPClient } from '@ai-sdk/mcp';
|
|
18
|
+
* import { connectAuthProvider } from '@vercel/connect/mcp';
|
|
19
|
+
*
|
|
20
|
+
* const mcpClient = await createMCPClient({
|
|
21
|
+
* transport: {
|
|
22
|
+
* type: 'http',
|
|
23
|
+
* url: 'https://mcp.linear.app',
|
|
24
|
+
* authProvider: connectAuthProvider('oauth/linear', {
|
|
25
|
+
* subject: { type: 'user', id: userId },
|
|
26
|
+
* scopes: ['read'],
|
|
27
|
+
* }),
|
|
28
|
+
* },
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export { connectAuthProvider, ConsentRequiredError, } from './connect-auth-provider.js';
|
|
33
|
+
export { ConnectError, ConnectorInstallationRequiredError, NoValidTokenError, UserAuthorizationRequiredError, } from '../token.js';
|
package/dist/token.d.ts
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
|
+
import type { ConnectAuthorizationDetail } from './authorization-details.js';
|
|
2
|
+
export type ConnectSubjectType = 'app' | 'user' | 'jwt-bearer';
|
|
3
|
+
export type ConnectTokenSubject = ConnectAppTokenSubject | ConnectUserTokenSubject | ConnectJwtBearerTokenSubject;
|
|
4
|
+
export interface ConnectAppTokenSubject {
|
|
5
|
+
type: 'app';
|
|
6
|
+
}
|
|
7
|
+
export interface ConnectUserTokenSubject {
|
|
8
|
+
type: 'user';
|
|
9
|
+
id: string;
|
|
10
|
+
issuer?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ConnectJwtBearerTokenSubject {
|
|
13
|
+
type: 'jwt-bearer';
|
|
14
|
+
sub: string;
|
|
15
|
+
/** Defaults to the connector's OAuth client id. */
|
|
16
|
+
iss?: string;
|
|
17
|
+
/** Defaults to the connector's OAuth token endpoint. */
|
|
18
|
+
aud?: string;
|
|
19
|
+
additionalClaims?: Record<string, unknown>;
|
|
20
|
+
}
|
|
1
21
|
export interface ConnectTokenParams {
|
|
2
|
-
subject:
|
|
3
|
-
type: 'app';
|
|
4
|
-
} | {
|
|
5
|
-
type: 'user';
|
|
6
|
-
id: string;
|
|
7
|
-
issuer?: string;
|
|
8
|
-
};
|
|
22
|
+
subject: ConnectTokenSubject;
|
|
9
23
|
installationId?: string;
|
|
10
24
|
audience?: string[];
|
|
11
25
|
scopes?: string[];
|
|
12
26
|
resources?: string[];
|
|
13
|
-
authorizationDetails?:
|
|
14
|
-
type: string;
|
|
15
|
-
} & Record<string, unknown>>;
|
|
27
|
+
authorizationDetails?: ConnectAuthorizationDetail[];
|
|
16
28
|
/**
|
|
17
29
|
* Buffer time in milliseconds before token expiration to consider it invalid.
|
|
18
30
|
* If the token expires within this buffer, it will be refreshed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/connect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"types": "./dist/index.d.ts",
|
|
14
14
|
"default": "./dist/index.js"
|
|
15
15
|
},
|
|
16
|
-
"./
|
|
17
|
-
"types": "./dist/
|
|
18
|
-
"default": "./dist/
|
|
16
|
+
"./eve": {
|
|
17
|
+
"types": "./dist/eve/index.d.ts",
|
|
18
|
+
"default": "./dist/eve/index.js"
|
|
19
19
|
},
|
|
20
20
|
"./betterauth": {
|
|
21
21
|
"types": "./dist/betterauth/index.d.ts",
|
|
@@ -24,34 +24,52 @@
|
|
|
24
24
|
"./authjs": {
|
|
25
25
|
"types": "./dist/authjs/index.d.ts",
|
|
26
26
|
"default": "./dist/authjs/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./mcp": {
|
|
29
|
+
"types": "./dist/mcp/index.d.ts",
|
|
30
|
+
"default": "./dist/mcp/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./ai-sdk": {
|
|
33
|
+
"types": "./dist/ai-sdk/index.d.ts",
|
|
34
|
+
"default": "./dist/ai-sdk/index.js"
|
|
27
35
|
}
|
|
28
36
|
},
|
|
29
37
|
"files": [
|
|
30
38
|
"dist"
|
|
31
39
|
],
|
|
32
40
|
"dependencies": {
|
|
33
|
-
"@vercel/oidc": "3.6.
|
|
41
|
+
"@vercel/oidc": "3.6.1"
|
|
34
42
|
},
|
|
35
43
|
"peerDependencies": {
|
|
44
|
+
"@ai-sdk/mcp": "^1 || ^2",
|
|
36
45
|
"@auth/core": ">=0.37.0",
|
|
46
|
+
"ai": "^6 || ^7",
|
|
37
47
|
"better-auth": ">=1.5.0",
|
|
38
|
-
"
|
|
48
|
+
"eve": ">=0.8.2"
|
|
39
49
|
},
|
|
40
50
|
"peerDependenciesMeta": {
|
|
51
|
+
"@ai-sdk/mcp": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
41
54
|
"@auth/core": {
|
|
42
55
|
"optional": true
|
|
43
56
|
},
|
|
57
|
+
"ai": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
44
60
|
"better-auth": {
|
|
45
61
|
"optional": true
|
|
46
62
|
},
|
|
47
|
-
"
|
|
63
|
+
"eve": {
|
|
48
64
|
"optional": true
|
|
49
65
|
}
|
|
50
66
|
},
|
|
51
67
|
"devDependencies": {
|
|
68
|
+
"@ai-sdk/mcp": "2.0.0-beta.37",
|
|
52
69
|
"@auth/core": "0.37.4",
|
|
70
|
+
"ai": "7.0.0-beta.116",
|
|
53
71
|
"better-auth": "1.5.5",
|
|
54
|
-
"
|
|
72
|
+
"eve": "file:./fixtures/eve",
|
|
55
73
|
"typescript": "^5"
|
|
56
74
|
},
|
|
57
75
|
"private": false,
|
|
@@ -60,6 +78,9 @@
|
|
|
60
78
|
},
|
|
61
79
|
"scripts": {
|
|
62
80
|
"build": "tsc",
|
|
63
|
-
"typecheck": "tsc --noEmit"
|
|
81
|
+
"typecheck": "tsc --noEmit",
|
|
82
|
+
"type-check": "tsc --noEmit",
|
|
83
|
+
"test": "vitest run --config ../../vitest.config.mts",
|
|
84
|
+
"vitest-unit": "vitest run --config ../../vitest.config.mts"
|
|
64
85
|
}
|
|
65
86
|
}
|
package/dist/ash/index.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public surface of the `@vercel/connect/ash` subpath.
|
|
3
|
-
*
|
|
4
|
-
* Holds Ash-specific helpers that adapt the Vercel Connect SDK to
|
|
5
|
-
* Ash's connection runtime. Each helper lives in its own module;
|
|
6
|
-
* this barrel re-exports the public API so consumers import
|
|
7
|
-
* everything from `@vercel/connect/ash`.
|
|
8
|
-
*/
|
|
9
|
-
export { connect, type AshAuthorizationInput, type AshAuthorizationOptions, type AshConnectAuthorizationDefinition, type ConnectAuthorizationPhase, type ConnectAuthorizationState, type VercelConnectMetadata, } from './connection-authorization.js';
|
|
10
|
-
export { CONNECT_OAUTH_ISSUER, connectOAuth, type ConnectOAuthAudienceEnvironment, type ConnectOAuthEnvironment, type ConnectOAuthOptions, } from './connect-oauth.js';
|
|
11
|
-
export { connectSlackCredentials } from './slack-credentials.js';
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { SlackChannelCredentials } from 'experimental-ash/channels/slack';
|
|
2
|
-
/**
|
|
3
|
-
* Build {@link SlackChannelCredentials} backed by a Vercel Connect
|
|
4
|
-
* connector that stores a Slack workspace's bot token.
|
|
5
|
-
*
|
|
6
|
-
* The returned `botToken` is a function form, invoked once per
|
|
7
|
-
* inbound webhook so the chat adapter always picks up a fresh token
|
|
8
|
-
* from Vercel Connect (rotation, refresh, multi-workspace tenancy
|
|
9
|
-
* are all handled server-side).
|
|
10
|
-
*
|
|
11
|
-
* Slack bot tokens are app-scoped — one token per workspace install,
|
|
12
|
-
* shared across every end-user — so this helper calls Vercel Connect
|
|
13
|
-
* with `subject: { type: "app" }`. End-user identity (per-user OAuth
|
|
14
|
-
* into Slack) is a separate concern handled elsewhere.
|
|
15
|
-
*
|
|
16
|
-
* ```ts
|
|
17
|
-
* import { slackRoute } from "experimental-ash/channels/slack";
|
|
18
|
-
* import { connectSlackCredentials } from "@vercel/connect/ash";
|
|
19
|
-
*
|
|
20
|
-
* export default slackRoute({
|
|
21
|
-
* credentials: connectSlackCredentials("scl_..."),
|
|
22
|
-
* });
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
export declare function connectSlackCredentials(connector: string): SlackChannelCredentials;
|