@vercel/connect 0.0.6-alpha.1 → 0.1.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/LICENSE +25 -1
- package/README.md +23 -364
- package/dist/ash/connect-oauth.d.ts +4 -4
- package/dist/ash/connect-oauth.js +38 -39
- package/dist/ash/connection-authorization.d.ts +7 -7
- package/dist/ash/connection-authorization.js +38 -31
- package/dist/ash/index.d.ts +3 -3
- package/dist/ash/index.js +3 -3
- package/dist/ash/slack-credentials.d.ts +1 -1
- package/dist/ash/slack-credentials.js +3 -3
- package/dist/authjs/connect-provider.d.ts +1 -1
- package/dist/authjs/connect-provider.js +12 -12
- package/dist/authjs/index.d.ts +1 -1
- package/dist/authjs/index.js +1 -1
- package/dist/authorization.d.ts +5 -1
- package/dist/authorization.js +18 -12
- package/dist/betterauth/connect-provider.d.ts +1 -1
- package/dist/betterauth/connect-provider.js +19 -16
- package/dist/betterauth/index.d.ts +2 -1
- package/dist/betterauth/index.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/internal/team-id.js +6 -6
- package/dist/token.d.ts +23 -8
- package/dist/token.js +116 -46
- package/package.json +8 -3
package/LICENSE
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
Apache License
|
|
2
3
|
Version 2.0, January 2004
|
|
3
4
|
http://www.apache.org/licenses/
|
|
@@ -175,4 +176,27 @@
|
|
|
175
176
|
|
|
176
177
|
END OF TERMS AND CONDITIONS
|
|
177
178
|
|
|
178
|
-
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2017 Vercel, Inc.
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,400 +1,59 @@
|
|
|
1
1
|
# `@vercel/connect`
|
|
2
2
|
|
|
3
|
-
SDK for obtaining scoped tokens for third-party services on behalf of apps or users.
|
|
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
|
+
Four entrypoints, all ESM:
|
|
6
6
|
|
|
7
|
-
- `@vercel/connect` —
|
|
8
|
-
- `@vercel/connect/ash` — adapter helpers for
|
|
9
|
-
- `@vercel/connect/betterauth` —
|
|
10
|
-
- `@vercel/connect/authjs` —
|
|
11
|
-
|
|
12
|
-
Consumers that don't use a given integration never load it.
|
|
7
|
+
- `@vercel/connect` — core token / authorization SDK
|
|
8
|
+
- `@vercel/connect/ash` — adapter helpers for [Ash](https://github.com/vercel/ash) connections (optional peer: `experimental-ash`)
|
|
9
|
+
- `@vercel/connect/betterauth` — [Better Auth](https://www.better-auth.com/) `genericOAuth` provider (optional peer: `better-auth`)
|
|
10
|
+
- `@vercel/connect/authjs` — [Auth.js](https://authjs.dev/) `OAuth2Config` provider (optional peer: `@auth/core`)
|
|
13
11
|
|
|
14
12
|
## Install
|
|
15
13
|
|
|
16
14
|
```sh
|
|
17
15
|
pnpm add @vercel/connect
|
|
18
|
-
# or
|
|
19
|
-
npm install @vercel/connect
|
|
20
16
|
```
|
|
21
17
|
|
|
22
|
-
|
|
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
|
-
```
|
|
18
|
+
## Usage
|
|
37
19
|
|
|
38
|
-
|
|
20
|
+
### Core SDK
|
|
39
21
|
|
|
40
22
|
```ts
|
|
41
|
-
import { getToken } from
|
|
23
|
+
import { getToken } from '@vercel/connect';
|
|
42
24
|
|
|
43
25
|
const token = await getToken(process.env.CONNECTOR_LINEAR!, {
|
|
44
|
-
subject: { type:
|
|
26
|
+
subject: { type: 'user', id: 'user_123' },
|
|
45
27
|
});
|
|
46
28
|
```
|
|
47
29
|
|
|
48
|
-
###
|
|
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`, connector identity (`connector`), `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
|
|
30
|
+
### Ash
|
|
89
31
|
|
|
90
32
|
```ts
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
connector: {
|
|
107
|
-
/** Opaque Vercel Connect connector id. */
|
|
108
|
-
id: string;
|
|
109
|
-
/** Human-readable Vercel Connect connector UID. */
|
|
110
|
-
uid: string;
|
|
111
|
-
/** Vercel Connect connector type identifier. */
|
|
112
|
-
type: string;
|
|
113
|
-
};
|
|
114
|
-
name?: string;
|
|
115
|
-
installationId?: string;
|
|
116
|
-
tenantId?: string;
|
|
117
|
-
externalSubject?: string;
|
|
118
|
-
metadata?: Record<string, unknown>;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface ConnectOptions {
|
|
122
|
-
/** Override the OIDC bearer used to authenticate to the Vercel API. */
|
|
123
|
-
vercelToken?: string;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
interface ConnectAuthorizationOptions {
|
|
127
|
-
vercelToken?: string;
|
|
128
|
-
/** Browser-redirect target after consent. Must be https:// or http://localhost. */
|
|
129
|
-
callbackUrl?: string;
|
|
130
|
-
/** Server-POST callback. Must be https://. */
|
|
131
|
-
webhook?: string;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
interface ConnectAuthorizationResponse {
|
|
135
|
-
request: string;
|
|
136
|
-
verifier: string;
|
|
137
|
-
url: string;
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### Errors
|
|
142
|
-
|
|
143
|
-
All exchange / authorization calls reject with typed errors. Catch them by class to distinguish recoverable consent-required cases from terminal failures.
|
|
144
|
-
|
|
145
|
-
| Class | Meaning |
|
|
146
|
-
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
|
147
|
-
| `NoValidTokenError` | Vercel Connect has no valid credential for this subject. For user subjects, recoverable via `startAuthorization`. |
|
|
148
|
-
| `UserAuthorizationRequiredError` | The user has not consented yet (or the previous grant was revoked). Recoverable via `startAuthorization`. |
|
|
149
|
-
| `ConnectorInstallationRequiredError` | The Vercel Connect connector is not installed for this team. Terminal — an operator must install the connector. |
|
|
150
|
-
|
|
151
|
-
Any other failure (network, 5xx, etc.) is thrown as a plain `Error` with the HTTP status and response body in the message.
|
|
152
|
-
|
|
153
|
-
```ts
|
|
154
|
-
import {
|
|
155
|
-
getToken,
|
|
156
|
-
NoValidTokenError,
|
|
157
|
-
UserAuthorizationRequiredError,
|
|
158
|
-
ConnectorInstallationRequiredError,
|
|
159
|
-
} from "@vercel/connect";
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
await getToken(connector, { subject: { type: "user", id } });
|
|
163
|
-
} catch (err) {
|
|
164
|
-
if (err instanceof UserAuthorizationRequiredError) {
|
|
165
|
-
// Redirect the user through startAuthorization().
|
|
166
|
-
} else if (err instanceof ConnectorInstallationRequiredError) {
|
|
167
|
-
// Surface "install this Vercel Connect connector" to an operator.
|
|
168
|
-
} else {
|
|
169
|
-
throw err;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
## `@vercel/connect/ash`
|
|
177
|
-
|
|
178
|
-
Adapter helpers for the [Ash](https://github.com/vercel/ash) connection runtime. Importing this subpath requires `experimental-ash >= 0.8.2` to be installed in the consumer project.
|
|
179
|
-
|
|
180
|
-
### `connect(input)`
|
|
181
|
-
|
|
182
|
-
Builds an Ash `AuthorizationDefinition` backed by Vercel Connect, collapsing the `getToken` / `startAuthorization` / `completeAuthorization` boilerplate that each Vercel Connect-backed connection would otherwise repeat.
|
|
183
|
-
|
|
184
|
-
```ts
|
|
185
|
-
function connect(
|
|
186
|
-
connector: string,
|
|
187
|
-
): AshConnectAuthorizationDefinition<
|
|
188
|
-
InteractiveAuthorizationDefinition<ConnectAuthorizationState>
|
|
189
|
-
>;
|
|
190
|
-
function connect(
|
|
191
|
-
options: AshAuthorizationOptions & { principalType?: "user" },
|
|
192
|
-
): AshConnectAuthorizationDefinition<
|
|
193
|
-
InteractiveAuthorizationDefinition<ConnectAuthorizationState>
|
|
194
|
-
>;
|
|
195
|
-
function connect(
|
|
196
|
-
options: AshAuthorizationOptions & { principalType: "app" },
|
|
197
|
-
): AshConnectAuthorizationDefinition<NonInteractiveAuthorizationDefinition>;
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
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.
|
|
201
|
-
|
|
202
|
-
The return type narrows on `principalType`:
|
|
203
|
-
|
|
204
|
-
- `"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.
|
|
205
|
-
- `"app"` → non-interactive definition (`getToken` only). Failures map to `ConnectionAuthorizationFailedError` with `retryable: false` — there's nobody to consent for an app-scoped connector.
|
|
206
|
-
|
|
207
|
-
```ts
|
|
208
|
-
import { defineMcpClientConnection } from "experimental-ash/connections";
|
|
209
|
-
import { connect } from "@vercel/connect/ash";
|
|
33
|
+
import { defineMcpClientConnection } from 'experimental-ash/connections';
|
|
34
|
+
import { connect } from '@vercel/connect/ash';
|
|
210
35
|
|
|
211
36
|
export default defineMcpClientConnection({
|
|
212
|
-
url:
|
|
213
|
-
|
|
214
|
-
auth: connect("oauth/mcp-linear-app"),
|
|
215
|
-
});
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
#### `AshAuthorizationOptions`
|
|
219
|
-
|
|
220
|
-
| Field | Type | Notes |
|
|
221
|
-
| ---------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
222
|
-
| `connector` | `string` | Vercel Connect connector identifier — accepts the opaque `scl_...` key or the human-readable UID (`oauth/mcp-linear-app`). |
|
|
223
|
-
| `principalType` | `"app" \| "user"` | Defaults to `"user"` when omitted. Selects which definition shape is returned. |
|
|
224
|
-
| `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. |
|
|
225
|
-
| `connectOptions` | `ConnectOptions` | Low-level overrides (e.g. `vercelToken`). Most callers leave this unset. |
|
|
226
|
-
| `instructions` | `string` | Custom call-to-action shown on `connection.authorization_required`. Defaults to a message derived from the connection's filename. |
|
|
227
|
-
| `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. |
|
|
228
|
-
|
|
229
|
-
#### Supporting types
|
|
230
|
-
|
|
231
|
-
```ts
|
|
232
|
-
type AshAuthorizationInput = string | AshAuthorizationOptions;
|
|
233
|
-
|
|
234
|
-
interface VercelConnectMetadata {
|
|
235
|
-
readonly connector: string;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
type AshConnectAuthorizationDefinition<
|
|
239
|
-
TAuthorization extends
|
|
240
|
-
| InteractiveAuthorizationDefinition<ConnectAuthorizationState>
|
|
241
|
-
| NonInteractiveAuthorizationDefinition,
|
|
242
|
-
> = TAuthorization & {
|
|
243
|
-
readonly vercelConnect: VercelConnectMetadata;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
type ConnectAuthorizationPhase =
|
|
247
|
-
| "getToken"
|
|
248
|
-
| "startAuthorization"
|
|
249
|
-
| "completeAuthorization";
|
|
250
|
-
|
|
251
|
-
type ConnectAuthorizationState = {
|
|
252
|
-
readonly verifier: string;
|
|
253
|
-
readonly [key: string]: JsonValue;
|
|
254
|
-
};
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
`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.
|
|
258
|
-
|
|
259
|
-
### `connectSlackCredentials(connector)`
|
|
260
|
-
|
|
261
|
-
```ts
|
|
262
|
-
function connectSlackCredentials(connector: string): SlackChannelCredentials;
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
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).
|
|
266
|
-
|
|
267
|
-
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.
|
|
268
|
-
|
|
269
|
-
```ts
|
|
270
|
-
import { slackRoute } from "experimental-ash/channels/slack";
|
|
271
|
-
import { connectSlackCredentials } from "@vercel/connect/ash";
|
|
272
|
-
|
|
273
|
-
export default slackRoute({
|
|
274
|
-
credentials: connectSlackCredentials("scl_..."),
|
|
37
|
+
url: 'https://mcp.linear.app/sse',
|
|
38
|
+
auth: connect('linear'),
|
|
275
39
|
});
|
|
276
40
|
```
|
|
277
41
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
### `connectOAuth(options?)`
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
function connectOAuth(options?: ConnectOAuthOptions): AuthFn<Request>;
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
Returns an Ash route auth callback for Vercel Connect OAuth gateway access tokens. It verifies bearer JWTs issued by `https://connect.vercel.com`, requires the gateway access-token claim `typ: "at"`, and by default accepts Connect OAuth audiences shaped `teamId:VERCEL_PROJECT_ID:VERCEL_ENV`; the team id segment is ignored because project ids are globally unique.
|
|
42
|
+
### Better Auth
|
|
287
43
|
|
|
288
44
|
```ts
|
|
289
|
-
import {
|
|
290
|
-
import {
|
|
45
|
+
import { genericOAuth } from 'better-auth/plugins';
|
|
46
|
+
import { connect } from '@vercel/connect/betterauth';
|
|
291
47
|
|
|
292
|
-
|
|
293
|
-
auth: connectOAuth({
|
|
294
|
-
connectors: ["slack/mybot"],
|
|
295
|
-
}),
|
|
296
|
-
});
|
|
48
|
+
genericOAuth({ config: [connect({ connector: 'linear' })] });
|
|
297
49
|
```
|
|
298
50
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
---
|
|
302
|
-
|
|
303
|
-
## `@vercel/connect/betterauth`
|
|
304
|
-
|
|
305
|
-
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.
|
|
306
|
-
|
|
307
|
-
### `connect(options)`
|
|
51
|
+
### Auth.js
|
|
308
52
|
|
|
309
53
|
```ts
|
|
310
|
-
|
|
311
|
-
```
|
|
54
|
+
import { connect } from '@vercel/connect/authjs';
|
|
312
55
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
```ts
|
|
316
|
-
import { betterAuth } from "better-auth";
|
|
317
|
-
import { genericOAuth } from "better-auth/plugins/generic-oauth";
|
|
318
|
-
import { connect } from "@vercel/connect/betterauth";
|
|
319
|
-
|
|
320
|
-
export const auth = betterAuth({
|
|
321
|
-
plugins: [
|
|
322
|
-
genericOAuth({
|
|
323
|
-
config: [
|
|
324
|
-
connect({
|
|
325
|
-
providerId: "slack",
|
|
326
|
-
connector: process.env.CONNECTOR_SLACK!,
|
|
327
|
-
}),
|
|
328
|
-
],
|
|
329
|
-
}),
|
|
330
|
-
],
|
|
331
|
-
});
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
#### `BetterAuthConnectOptions`
|
|
335
|
-
|
|
336
|
-
| Field | Type | Notes |
|
|
337
|
-
| -------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
338
|
-
| `id` | `string` | Provider id used by Better Auth's generic OAuth plugin. Surfaces in the sign-in URL and account rows. |
|
|
339
|
-
| `connector` | `string` | Vercel Connect connector identifier — accepts `scl_...` or the human-readable UID. |
|
|
340
|
-
| `scopes` | `readonly string[]` | Scopes to request. `offline_access` is added automatically. Defaults to `["openid"]`. |
|
|
341
|
-
| `getVercelOidcToken` | `() => Promise<string>` | Override the OIDC token fetcher. Defaults to `getVercelOidcToken` from `@vercel/oidc`. |
|
|
342
|
-
|
|
343
|
-
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.
|
|
344
|
-
|
|
345
|
-
---
|
|
346
|
-
|
|
347
|
-
## `@vercel/connect/authjs`
|
|
348
|
-
|
|
349
|
-
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.
|
|
350
|
-
|
|
351
|
-
### `connect(options)`
|
|
352
|
-
|
|
353
|
-
```ts
|
|
354
|
-
function connect(options: AuthJsConnectOptions): OAuth2Config<ConnectProfile>;
|
|
56
|
+
const providers = [connect({ connector: 'linear' })];
|
|
355
57
|
```
|
|
356
58
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
```ts
|
|
360
|
-
import type { AuthConfig } from "@auth/core";
|
|
361
|
-
import { connect } from "@vercel/connect/authjs";
|
|
362
|
-
|
|
363
|
-
export const authConfig: AuthConfig = {
|
|
364
|
-
providers: [
|
|
365
|
-
connect({
|
|
366
|
-
id: "slack",
|
|
367
|
-
name: "Slack",
|
|
368
|
-
connector: process.env.CONNECTOR_SLACK!,
|
|
369
|
-
}),
|
|
370
|
-
],
|
|
371
|
-
};
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
#### `AuthJsConnectOptions`
|
|
375
|
-
|
|
376
|
-
| Field | Type | Notes |
|
|
377
|
-
| -------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
378
|
-
| `id` | `string` | Provider id surfaced by Auth.js. Becomes the path segment in callback URLs and the `provider` field on accounts. |
|
|
379
|
-
| `name` | `string` | Human-readable display name used by Auth.js sign-in UIs. |
|
|
380
|
-
| `connector` | `string` | Vercel Connect connector identifier — accepts `scl_...` or the human-readable UID. |
|
|
381
|
-
| `scopes` | `readonly string[]` | Scopes to request. `offline_access` is added automatically. Defaults to `["openid"]`. |
|
|
382
|
-
| `getVercelOidcToken` | `() => Promise<string>` | Override the OIDC token fetcher. Defaults to `getVercelOidcToken` from `@vercel/oidc`. |
|
|
383
|
-
|
|
384
|
-
#### `ConnectProfile`
|
|
385
|
-
|
|
386
|
-
```ts
|
|
387
|
-
interface ConnectProfile {
|
|
388
|
-
sub: string;
|
|
389
|
-
email?: string;
|
|
390
|
-
email_verified?: boolean;
|
|
391
|
-
name?: string;
|
|
392
|
-
picture?: string;
|
|
393
|
-
}
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
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.
|
|
397
|
-
|
|
398
|
-
## License
|
|
399
|
-
|
|
400
|
-
Apache-2.0. See [LICENSE](LICENSE) for details.
|
|
59
|
+
See the source under `src/` for the full API (additional helpers like `getTokenResponse`, `startAuthorization`, typed error classes, and per-adapter options).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type AuthFn, type VerifyOidcConfig } from
|
|
1
|
+
import { type AuthFn, type VerifyOidcConfig } from 'experimental-ash/channels/auth';
|
|
2
2
|
export declare const CONNECT_OAUTH_ISSUER = "https://connect.vercel.com";
|
|
3
|
-
export type ConnectOAuthEnvironment =
|
|
4
|
-
export type ConnectOAuthAudienceEnvironment = ConnectOAuthEnvironment |
|
|
3
|
+
export type ConnectOAuthEnvironment = 'production' | 'preview' | 'development';
|
|
4
|
+
export type ConnectOAuthAudienceEnvironment = ConnectOAuthEnvironment | '*';
|
|
5
5
|
export interface ConnectOAuthOptions {
|
|
6
6
|
/**
|
|
7
7
|
* Exact `aud` claim values to accept. When omitted, the helper
|
|
@@ -43,7 +43,7 @@ export interface ConnectOAuthOptions {
|
|
|
43
43
|
* Additional exact-match string claims to require. `typ` is always
|
|
44
44
|
* constrained to `"at"` for Connect OAuth gateway access tokens.
|
|
45
45
|
*/
|
|
46
|
-
readonly claims?: VerifyOidcConfig[
|
|
46
|
+
readonly claims?: VerifyOidcConfig['claims'];
|
|
47
47
|
/**
|
|
48
48
|
* Override the OIDC discovery URL. Defaults to
|
|
49
49
|
* `https://connect.vercel.com/.well-known/openid-configuration`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { extractBearerToken, verifyOidc, } from
|
|
2
|
-
export const CONNECT_OAUTH_ISSUER =
|
|
1
|
+
import { extractBearerToken, verifyOidc, } from 'experimental-ash/channels/auth';
|
|
2
|
+
export const CONNECT_OAUTH_ISSUER = 'https://connect.vercel.com';
|
|
3
3
|
/**
|
|
4
4
|
* Returns an Ash route auth callback for Vercel Connect OAuth gateway
|
|
5
5
|
* access tokens.
|
|
@@ -17,7 +17,7 @@ export function connectOAuth(opts = {}) {
|
|
|
17
17
|
const connectorPolicy = resolveConnectorPolicy(opts);
|
|
18
18
|
const claims = buildClaimMatchers(opts);
|
|
19
19
|
return async (request) => {
|
|
20
|
-
const token = extractBearerToken(request.headers.get(
|
|
20
|
+
const token = extractBearerToken(request.headers.get('authorization'));
|
|
21
21
|
if (token === null) {
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
@@ -26,8 +26,7 @@ export function connectOAuth(opts = {}) {
|
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
28
|
const audiences = resolveRequestAudiences(payload, audiencePolicy);
|
|
29
|
-
if (audiences === null ||
|
|
30
|
-
!isConnectorAccepted(payload, connectorPolicy)) {
|
|
29
|
+
if (audiences === null || !isConnectorAccepted(payload, connectorPolicy)) {
|
|
31
30
|
return null;
|
|
32
31
|
}
|
|
33
32
|
const result = await verifyOidc(token, {
|
|
@@ -44,29 +43,29 @@ export function connectOAuth(opts = {}) {
|
|
|
44
43
|
function resolveAudiencePolicy(opts) {
|
|
45
44
|
if (opts.audiences !== undefined) {
|
|
46
45
|
if (opts.audiences.length === 0) {
|
|
47
|
-
throw new Error(
|
|
46
|
+
throw new Error('connectOAuth: audiences must not be empty.');
|
|
48
47
|
}
|
|
49
|
-
return { kind:
|
|
48
|
+
return { kind: 'exact', audiences: opts.audiences };
|
|
50
49
|
}
|
|
51
|
-
const projectId = opts.projectId ?? readNonEmptyEnv(
|
|
50
|
+
const projectId = opts.projectId ?? readNonEmptyEnv('VERCEL_PROJECT_ID');
|
|
52
51
|
const environment = opts.environment ?? inferVercelEnvironment();
|
|
53
52
|
if (projectId === undefined) {
|
|
54
|
-
throw new Error(
|
|
53
|
+
throw new Error('connectOAuth: could not infer projectId from VERCEL_PROJECT_ID; pass audiences or projectId explicitly.');
|
|
55
54
|
}
|
|
56
55
|
if (environment === undefined) {
|
|
57
|
-
throw new Error(
|
|
56
|
+
throw new Error('connectOAuth: could not infer environment from VERCEL_TARGET_ENV or VERCEL_ENV; pass audiences or environment explicitly.');
|
|
58
57
|
}
|
|
59
|
-
const environments = environment ===
|
|
60
|
-
? [
|
|
58
|
+
const environments = environment === '*'
|
|
59
|
+
? ['production', 'preview', 'development']
|
|
61
60
|
: [environment];
|
|
62
|
-
return { kind:
|
|
61
|
+
return { kind: 'project', environments, projectId };
|
|
63
62
|
}
|
|
64
63
|
function resolveRequestAudiences(payload, policy) {
|
|
65
|
-
if (policy.kind ===
|
|
64
|
+
if (policy.kind === 'exact') {
|
|
66
65
|
return policy.audiences;
|
|
67
66
|
}
|
|
68
67
|
const audiences = extractAudiences(payload);
|
|
69
|
-
const accepted = audiences.filter(
|
|
68
|
+
const accepted = audiences.filter(audience => {
|
|
70
69
|
const parsed = parseConnectOAuthAudience(audience);
|
|
71
70
|
return (parsed !== null &&
|
|
72
71
|
parsed.projectId === policy.projectId &&
|
|
@@ -76,26 +75,26 @@ function resolveRequestAudiences(payload, policy) {
|
|
|
76
75
|
}
|
|
77
76
|
function resolveConnectorPolicy(opts) {
|
|
78
77
|
if (opts.connectors === undefined) {
|
|
79
|
-
return { kind:
|
|
78
|
+
return { kind: 'any' };
|
|
80
79
|
}
|
|
81
80
|
if (opts.connectors.length === 0) {
|
|
82
|
-
throw new Error(
|
|
81
|
+
throw new Error('connectOAuth: connectors must not be empty.');
|
|
83
82
|
}
|
|
84
|
-
return { kind:
|
|
83
|
+
return { kind: 'connectors', connectors: opts.connectors };
|
|
85
84
|
}
|
|
86
85
|
function isConnectorAccepted(payload, policy) {
|
|
87
|
-
if (policy.kind ===
|
|
86
|
+
if (policy.kind === 'any') {
|
|
88
87
|
return true;
|
|
89
88
|
}
|
|
90
|
-
const clientId = stringClaim(payload,
|
|
89
|
+
const clientId = stringClaim(payload, 'clientId');
|
|
91
90
|
if (clientId !== undefined && policy.connectors.includes(clientId)) {
|
|
92
91
|
return true;
|
|
93
92
|
}
|
|
94
|
-
const clientUid = stringClaim(payload,
|
|
93
|
+
const clientUid = stringClaim(payload, 'clientUid');
|
|
95
94
|
return clientUid !== undefined && policy.connectors.includes(clientUid);
|
|
96
95
|
}
|
|
97
96
|
function inferVercelEnvironment() {
|
|
98
|
-
const value = readNonEmptyEnv(
|
|
97
|
+
const value = readNonEmptyEnv('VERCEL_TARGET_ENV') ?? readNonEmptyEnv('VERCEL_ENV');
|
|
99
98
|
if (value === undefined)
|
|
100
99
|
return undefined;
|
|
101
100
|
assertEnvironment(value);
|
|
@@ -108,58 +107,58 @@ function buildClaimMatchers(opts) {
|
|
|
108
107
|
...(opts.installationIds === undefined
|
|
109
108
|
? {}
|
|
110
109
|
: { installationId: opts.installationIds }),
|
|
111
|
-
typ: [
|
|
110
|
+
typ: ['at'],
|
|
112
111
|
};
|
|
113
112
|
}
|
|
114
113
|
function readNonEmptyEnv(name) {
|
|
115
|
-
const value = typeof process ===
|
|
114
|
+
const value = typeof process === 'undefined' ? undefined : process.env?.[name]?.trim();
|
|
116
115
|
return value === undefined || value.length === 0 ? undefined : value;
|
|
117
116
|
}
|
|
118
117
|
function extractAudiences(payload) {
|
|
119
118
|
const aud = payload.aud;
|
|
120
|
-
if (typeof aud ===
|
|
119
|
+
if (typeof aud === 'string') {
|
|
121
120
|
return [aud];
|
|
122
121
|
}
|
|
123
122
|
if (Array.isArray(aud)) {
|
|
124
|
-
return aud.filter((value) => typeof value ===
|
|
123
|
+
return aud.filter((value) => typeof value === 'string');
|
|
125
124
|
}
|
|
126
125
|
return [];
|
|
127
126
|
}
|
|
128
127
|
function stringClaim(payload, claim) {
|
|
129
128
|
const value = payload[claim];
|
|
130
|
-
return typeof value ===
|
|
129
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
131
130
|
}
|
|
132
131
|
function decodeJwtPayload(token) {
|
|
133
132
|
try {
|
|
134
|
-
const payload = token.split(
|
|
133
|
+
const payload = token.split('.')[1];
|
|
135
134
|
if (payload === undefined)
|
|
136
135
|
return null;
|
|
137
|
-
const base64 = payload.replace(/-/g,
|
|
138
|
-
const padded = base64 +
|
|
136
|
+
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
137
|
+
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
|
|
139
138
|
const decoded = JSON.parse(atob(padded));
|
|
140
|
-
return typeof decoded ===
|
|
139
|
+
return typeof decoded === 'object' && decoded !== null ? decoded : null;
|
|
141
140
|
}
|
|
142
141
|
catch {
|
|
143
142
|
return null;
|
|
144
143
|
}
|
|
145
144
|
}
|
|
146
145
|
function parseConnectOAuthAudience(audience) {
|
|
147
|
-
const [teamId, projectId, environment, extra] = audience.split(
|
|
146
|
+
const [teamId, projectId, environment, extra] = audience.split(':');
|
|
148
147
|
if (!teamId || !projectId || !environment || extra !== undefined) {
|
|
149
148
|
return null;
|
|
150
149
|
}
|
|
151
|
-
if (environment !==
|
|
152
|
-
environment !==
|
|
153
|
-
environment !==
|
|
150
|
+
if (environment !== 'production' &&
|
|
151
|
+
environment !== 'preview' &&
|
|
152
|
+
environment !== 'development') {
|
|
154
153
|
return null;
|
|
155
154
|
}
|
|
156
155
|
return { projectId, environment };
|
|
157
156
|
}
|
|
158
157
|
function assertEnvironment(value) {
|
|
159
|
-
if (value !==
|
|
160
|
-
value !==
|
|
161
|
-
value !==
|
|
162
|
-
value !==
|
|
158
|
+
if (value !== 'production' &&
|
|
159
|
+
value !== 'preview' &&
|
|
160
|
+
value !== 'development' &&
|
|
161
|
+
value !== '*') {
|
|
163
162
|
throw new Error(`connectOAuth: invalid environment ${JSON.stringify(value)}; expected "production", "preview", "development", or "*".`);
|
|
164
163
|
}
|
|
165
164
|
}
|