@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 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
- Copyright 2026 Vercel, Inc.
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. 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.
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
- The package ships four entrypoints:
5
+ Four entrypoints, all ESM:
6
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.
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
- 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
- ```
18
+ ## Usage
37
19
 
38
- Returns a token string for the given Vercel Connect connector and subject. Wraps `getTokenResponse` and discards the metadata.
20
+ ### Core SDK
39
21
 
40
22
  ```ts
41
- import { getToken } from "@vercel/connect";
23
+ import { getToken } from '@vercel/connect';
42
24
 
43
25
  const token = await getToken(process.env.CONNECTOR_LINEAR!, {
44
- subject: { type: "user", id: "user_123" },
26
+ subject: { type: 'user', id: 'user_123' },
45
27
  });
46
28
  ```
47
29
 
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`, 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
- 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
- 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: "https://mcp.linear.app/sse",
213
- description: "Linear workspace — issues, projects, cycles, and comments.",
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
- The `webhookVerifier` is set to `vercelOidc()`, which authenticates inbound webhooks using the Vercel OIDC token issued for the calling project.
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 { httpRoute } from "experimental-ash/channels/http";
290
- import { connectOAuth } from "@vercel/connect/ash";
45
+ import { genericOAuth } from 'better-auth/plugins';
46
+ import { connect } from '@vercel/connect/betterauth';
291
47
 
292
- export default httpRoute({
293
- auth: connectOAuth({
294
- connectors: ["slack/mybot"],
295
- }),
296
- });
48
+ genericOAuth({ config: [connect({ connector: 'linear' })] });
297
49
  ```
298
50
 
299
- Pass `audiences` to accept explicit `aud` values, or pass `projectId` and `environment` to build the Connect OAuth audience manually. `connectors` accepts both `scl_...` connector ids and connector UIDs by checking each value against the `clientId` and `clientUid` claims; `tenantIds`, `installationIds`, `subjects`, and `claims` can further narrow accepted gateway tokens.
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
- function connect(options: BetterAuthConnectOptions): GenericOAuthConfig;
311
- ```
54
+ import { connect } from '@vercel/connect/authjs';
312
55
 
313
- 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` to inject the token endpoint `Authorization` header.
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
- 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 token endpoint `Authorization` header via Auth.js's `customFetch` symbol.
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 "experimental-ash/channels/auth";
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 = "production" | "preview" | "development";
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["claims"];
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 "experimental-ash/channels/auth";
2
- export const CONNECT_OAUTH_ISSUER = "https://connect.vercel.com";
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("authorization"));
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("connectOAuth: audiences must not be empty.");
46
+ throw new Error('connectOAuth: audiences must not be empty.');
48
47
  }
49
- return { kind: "exact", audiences: opts.audiences };
48
+ return { kind: 'exact', audiences: opts.audiences };
50
49
  }
51
- const projectId = opts.projectId ?? readNonEmptyEnv("VERCEL_PROJECT_ID");
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("connectOAuth: could not infer projectId from VERCEL_PROJECT_ID; pass audiences or projectId explicitly.");
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("connectOAuth: could not infer environment from VERCEL_TARGET_ENV or VERCEL_ENV; pass audiences or environment explicitly.");
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
- ? ["production", "preview", "development"]
58
+ const environments = environment === '*'
59
+ ? ['production', 'preview', 'development']
61
60
  : [environment];
62
- return { kind: "project", environments, projectId };
61
+ return { kind: 'project', environments, projectId };
63
62
  }
64
63
  function resolveRequestAudiences(payload, policy) {
65
- if (policy.kind === "exact") {
64
+ if (policy.kind === 'exact') {
66
65
  return policy.audiences;
67
66
  }
68
67
  const audiences = extractAudiences(payload);
69
- const accepted = audiences.filter((audience) => {
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: "any" };
78
+ return { kind: 'any' };
80
79
  }
81
80
  if (opts.connectors.length === 0) {
82
- throw new Error("connectOAuth: connectors must not be empty.");
81
+ throw new Error('connectOAuth: connectors must not be empty.');
83
82
  }
84
- return { kind: "connectors", connectors: opts.connectors };
83
+ return { kind: 'connectors', connectors: opts.connectors };
85
84
  }
86
85
  function isConnectorAccepted(payload, policy) {
87
- if (policy.kind === "any") {
86
+ if (policy.kind === 'any') {
88
87
  return true;
89
88
  }
90
- const clientId = stringClaim(payload, "clientId");
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, "clientUid");
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("VERCEL_TARGET_ENV") ?? readNonEmptyEnv("VERCEL_ENV");
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: ["at"],
110
+ typ: ['at'],
112
111
  };
113
112
  }
114
113
  function readNonEmptyEnv(name) {
115
- const value = typeof process === "undefined" ? undefined : process.env?.[name]?.trim();
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 === "string") {
119
+ if (typeof aud === 'string') {
121
120
  return [aud];
122
121
  }
123
122
  if (Array.isArray(aud)) {
124
- return aud.filter((value) => typeof value === "string");
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 === "string" && value.length > 0 ? value : undefined;
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(".")[1];
133
+ const payload = token.split('.')[1];
135
134
  if (payload === undefined)
136
135
  return null;
137
- const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
138
- const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
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 === "object" && decoded !== null ? decoded : null;
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 !== "production" &&
152
- environment !== "preview" &&
153
- environment !== "development") {
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 !== "production" &&
160
- value !== "preview" &&
161
- value !== "development" &&
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
  }