@vercel/connect 0.0.5-alpha.1 → 0.1.1

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.
@@ -28,13 +28,13 @@
28
28
  * `@vercel/connect/ash` requires Ash to be installed in the consumer
29
29
  * project, but the rest of `@vercel/connect` works without it.
30
30
  */
31
- import { type InteractiveAuthorizationDefinition, type JsonValue, type NonInteractiveAuthorizationDefinition } from "experimental-ash/connections";
32
- import type { ConnectOptions, ConnectTokenParams } from "../token.js";
31
+ import { type InteractiveAuthorizationDefinition, type JsonValue, type NonInteractiveAuthorizationDefinition } from 'experimental-ash/connections';
32
+ import type { ConnectOptions, ConnectTokenParams } from '../token.js';
33
33
  /**
34
34
  * Authorization phase passed to {@link AshAuthorizationOptions.onError}
35
35
  * so consumers can branch their error translation per callback.
36
36
  */
37
- export type ConnectAuthorizationPhase = "getToken" | "startAuthorization" | "completeAuthorization";
37
+ export type ConnectAuthorizationPhase = 'getToken' | 'startAuthorization' | 'completeAuthorization';
38
38
  /**
39
39
  * State journaled by Ash between
40
40
  * {@link InteractiveAuthorizationDefinition.startAuthorization} and
@@ -72,7 +72,7 @@ export interface AshAuthorizationOptions {
72
72
  * failure so the channel can prompt an operator to install the
73
73
  * Vercel Connect app.
74
74
  */
75
- readonly principalType?: "app" | "user";
75
+ readonly principalType?: 'app' | 'user';
76
76
  /**
77
77
  * Extra parameters forwarded to every `getTokenResponse` /
78
78
  * `startAuthorization` call, minus `subject` — the helper derives
@@ -81,7 +81,7 @@ export interface AshAuthorizationOptions {
81
81
  * Use this to pin `scopes`, `audience`, `resources`, or
82
82
  * `authorizationDetails`. Passed through verbatim.
83
83
  */
84
- readonly tokenParams?: Omit<ConnectTokenParams, "subject">;
84
+ readonly tokenParams?: Omit<ConnectTokenParams, 'subject'>;
85
85
  /**
86
86
  * Low-level Vercel Connect SDK options (currently `vercelToken`
87
87
  * for overriding the OIDC bearer). Most callers leave this unset
@@ -158,9 +158,9 @@ export type AshConnectAuthorizationDefinition<TAuthorization extends Interactive
158
158
  */
159
159
  export declare function connect(connector: string): AshConnectAuthorizationDefinition<InteractiveAuthorizationDefinition<ConnectAuthorizationState>>;
160
160
  export declare function connect(options: AshAuthorizationOptions & {
161
- readonly principalType?: "user";
161
+ readonly principalType?: 'user';
162
162
  }): AshConnectAuthorizationDefinition<InteractiveAuthorizationDefinition<ConnectAuthorizationState>>;
163
163
  export declare function connect(options: AshAuthorizationOptions & {
164
- readonly principalType: "app";
164
+ readonly principalType: 'app';
165
165
  }): AshConnectAuthorizationDefinition<NonInteractiveAuthorizationDefinition>;
166
166
  export declare function connect(options: AshAuthorizationInput): AshConnectAuthorizationDefinition<InteractiveAuthorizationDefinition<ConnectAuthorizationState> | NonInteractiveAuthorizationDefinition>;
@@ -28,33 +28,33 @@
28
28
  * `@vercel/connect/ash` requires Ash 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 "experimental-ash/connections";
32
- import { startAuthorization } from "../authorization.js";
33
- import { ConnectorInstallationRequiredError, getTokenResponse, NoValidTokenError, UserAuthorizationRequiredError, } from "../token.js";
31
+ import { ConnectionAuthorizationFailedError, ConnectionAuthorizationRequiredError, } from 'experimental-ash/connections';
32
+ import { startAuthorization } from '../authorization.js';
33
+ import { ConnectorInstallationRequiredError, getTokenResponse, NoValidTokenError, UserAuthorizationRequiredError, } from '../token.js';
34
34
  export function connect(input) {
35
35
  const options = normalizeAuthorizationOptions(input);
36
36
  const vercelConnect = { connector: options.connector };
37
- if (options.principalType === "app") {
37
+ if (options.principalType === 'app') {
38
38
  return { ...buildNonInteractiveDefinition(options), vercelConnect };
39
39
  }
40
40
  return { ...buildInteractiveDefinition(options), vercelConnect };
41
41
  }
42
42
  function normalizeAuthorizationOptions(input) {
43
- if (typeof input === "string") {
43
+ if (typeof input === 'string') {
44
44
  return { connector: input };
45
45
  }
46
46
  return input;
47
47
  }
48
48
  function buildInteractiveDefinition(options) {
49
49
  return {
50
- principalType: "user",
50
+ principalType: 'user',
51
51
  async getToken({ principal }) {
52
52
  try {
53
53
  const response = await getTokenResponse(options.connector, buildTokenParams(options, principal), options.connectOptions);
54
54
  return { token: response.token, expiresAt: response.expiresAt };
55
55
  }
56
56
  catch (error) {
57
- throw translate(error, "getToken", options);
57
+ throw translate(error, 'getToken', options);
58
58
  }
59
59
  },
60
60
  async startAuthorization({ principal, callbackUrl, webhook, }) {
@@ -80,14 +80,27 @@ function buildInteractiveDefinition(options) {
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), { ...options.connectOptions, callbackUrl: callbackUrl ?? webhook });
83
+ const response = await startAuthorization(options.connector, buildTokenParams(options, principal), {
84
+ ...options.connectOptions,
85
+ callbackUrl: callbackUrl ?? webhook,
86
+ deviceCode: true,
87
+ });
84
88
  return {
85
- challenge: buildChallenge(options, response.url),
89
+ challenge: {
90
+ url: response.url,
91
+ ...(response.deviceCode ? { userCode: response.deviceCode } : null),
92
+ ...(response.expiresAt
93
+ ? { expiresAt: new Date(response.expiresAt).toISOString() }
94
+ : null),
95
+ ...(options.instructions
96
+ ? { instructions: options.instructions }
97
+ : null),
98
+ },
86
99
  state: { verifier: response.verifier },
87
100
  };
88
101
  }
89
102
  catch (error) {
90
- throw translate(error, "startAuthorization", options);
103
+ throw translate(error, 'startAuthorization', options);
91
104
  }
92
105
  },
93
106
  async completeAuthorization({ principal, }) {
@@ -96,21 +109,21 @@ function buildInteractiveDefinition(options) {
96
109
  return { token: response.token, expiresAt: response.expiresAt };
97
110
  }
98
111
  catch (error) {
99
- throw translate(error, "completeAuthorization", options);
112
+ throw translate(error, 'completeAuthorization', options);
100
113
  }
101
114
  },
102
115
  };
103
116
  }
104
117
  function buildNonInteractiveDefinition(options) {
105
118
  return {
106
- principalType: "app",
119
+ principalType: 'app',
107
120
  async getToken({ principal }) {
108
121
  try {
109
122
  const response = await getTokenResponse(options.connector, buildTokenParams(options, principal), options.connectOptions);
110
123
  return { token: response.token, expiresAt: response.expiresAt };
111
124
  }
112
125
  catch (error) {
113
- throw translate(error, "getToken", options);
126
+ throw translate(error, 'getToken', options);
114
127
  }
115
128
  },
116
129
  };
@@ -122,16 +135,10 @@ function buildTokenParams(options, principal) {
122
135
  };
123
136
  }
124
137
  function principalToSubject(principal) {
125
- if (principal.type === "app") {
126
- return { type: "app" };
138
+ if (principal.type === 'app') {
139
+ return { type: 'app' };
127
140
  }
128
- return { type: "user", id: principal.id, issuer: principal.issuer };
129
- }
130
- function buildChallenge(options, url) {
131
- if (options.instructions === undefined) {
132
- return { url };
133
- }
134
- return { url, instructions: options.instructions };
141
+ return { type: 'user', id: principal.id, issuer: principal.issuer };
135
142
  }
136
143
  /**
137
144
  * Translates raw Vercel Connect errors into Ash's public
@@ -154,24 +161,24 @@ function translate(error, phase, options) {
154
161
  // `retryable: false`.
155
162
  if (error instanceof UserAuthorizationRequiredError ||
156
163
  error instanceof NoValidTokenError) {
157
- if (options.principalType === "app") {
158
- return new ConnectionAuthorizationFailedError("connect", {
164
+ if (options.principalType === 'app') {
165
+ return new ConnectionAuthorizationFailedError('connect', {
159
166
  message: error.message,
160
- reason: "app_not_installed",
167
+ reason: 'app_not_installed',
161
168
  retryable: false,
162
169
  });
163
170
  }
164
- if (phase === "completeAuthorization") {
171
+ if (phase === 'completeAuthorization') {
165
172
  // The consent leg reported success (Ash would not call us
166
173
  // otherwise) but Vercel Connect still says the user is
167
174
  // unauthorized. Default to retryable so the model can
168
175
  // re-prompt; most cases (network blip, replay) resolve on
169
176
  // retry.
170
- return new ConnectionAuthorizationFailedError("connect", {
171
- message: "Authorization did not complete. Vercel Connect still reports the user as unauthorized.",
177
+ return new ConnectionAuthorizationFailedError('connect', {
178
+ message: 'Authorization did not complete. Vercel Connect still reports the user as unauthorized.',
172
179
  });
173
180
  }
174
- return new ConnectionAuthorizationRequiredError("connect", {
181
+ return new ConnectionAuthorizationRequiredError('connect', {
175
182
  message: error.message,
176
183
  });
177
184
  }
@@ -179,9 +186,9 @@ function translate(error, phase, options) {
179
186
  // the operator has not installed the Vercel Connect connector for
180
187
  // this team.
181
188
  if (error instanceof ConnectorInstallationRequiredError) {
182
- return new ConnectionAuthorizationFailedError("connect", {
189
+ return new ConnectionAuthorizationFailedError('connect', {
183
190
  message: error.message,
184
- reason: "connector_installation_required",
191
+ reason: 'connector_installation_required',
185
192
  retryable: false,
186
193
  });
187
194
  }
@@ -6,6 +6,6 @@
6
6
  * this barrel re-exports the public API so consumers import
7
7
  * everything from `@vercel/connect/ash`.
8
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";
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';
package/dist/ash/index.js CHANGED
@@ -6,6 +6,6 @@
6
6
  * this barrel re-exports the public API so consumers import
7
7
  * everything from `@vercel/connect/ash`.
8
8
  */
9
- export { connect, } from "./connection-authorization.js";
10
- export { CONNECT_OAUTH_ISSUER, connectOAuth, } from "./connect-oauth.js";
11
- export { connectSlackCredentials } from "./slack-credentials.js";
9
+ export { connect, } from './connection-authorization.js';
10
+ export { CONNECT_OAUTH_ISSUER, connectOAuth, } from './connect-oauth.js';
11
+ export { connectSlackCredentials } from './slack-credentials.js';
@@ -1,4 +1,4 @@
1
- import type { SlackChannelCredentials } from "experimental-ash/channels/slack";
1
+ import type { SlackChannelCredentials } from 'experimental-ash/channels/slack';
2
2
  /**
3
3
  * Build {@link SlackChannelCredentials} backed by a Vercel Connect
4
4
  * connector that stores a Slack workspace's bot token.
@@ -1,5 +1,5 @@
1
- import { getToken } from "../index.js";
2
- import { vercelOidc } from "experimental-ash/channels/auth";
1
+ import { getToken } from '../index.js';
2
+ import { vercelOidc } from 'experimental-ash/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.
@@ -25,7 +25,7 @@ import { vercelOidc } from "experimental-ash/channels/auth";
25
25
  */
26
26
  export function connectSlackCredentials(connector) {
27
27
  return {
28
- botToken: () => getToken(connector, { subject: { type: "app" } }),
28
+ botToken: () => getToken(connector, { subject: { type: 'app' } }),
29
29
  webhookVerifier: vercelOidc(),
30
30
  };
31
31
  }
@@ -1,4 +1,4 @@
1
- import type { OAuth2Config } from "@auth/core/providers";
1
+ import type { OAuth2Config } from '@auth/core/providers';
2
2
  /** Userinfo payload returned by Vercel Connect's OIDC userinfo endpoint. */
3
3
  export interface ConnectProfile {
4
4
  sub: string;
@@ -23,29 +23,29 @@
23
23
  * };
24
24
  * ```
25
25
  */
26
- import { customFetch } from "@auth/core";
27
- import { getVercelOidcToken } from "@vercel/oidc";
28
- import { tryGetVercelTeamId, withTeamId } from "../internal/team-id.js";
29
- const CONNECT_AUTHORIZATION_URL = "https://connect.vercel.com/oauth/authorize";
30
- const CONNECT_TOKEN_URL = "https://connect.vercel.com/oauth/token";
31
- const CONNECT_USERINFO_URL = "https://connect.vercel.com/oauth/userinfo";
26
+ import { customFetch } from '@auth/core';
27
+ import { getVercelOidcToken } from '@vercel/oidc';
28
+ import { tryGetVercelTeamId, withTeamId } from '../internal/team-id.js';
29
+ const CONNECT_AUTHORIZATION_URL = 'https://connect.vercel.com/oauth/authorize';
30
+ const CONNECT_TOKEN_URL = 'https://connect.vercel.com/oauth/token';
31
+ const CONNECT_USERINFO_URL = 'https://connect.vercel.com/oauth/userinfo';
32
32
  /**
33
33
  * Builds an Auth.js `OAuth2Config` for Vercel Connect. Push the
34
34
  * result into `AuthConfig.providers`.
35
35
  */
36
36
  export function connect(options) {
37
37
  const fetchVercelToken = options.getVercelOidcToken ?? getVercelOidcToken;
38
- const scope = Array.from(new Set([...(options.scopes ?? ["openid"]), "offline_access"])).join(" ");
38
+ const scope = Array.from(new Set([...(options.scopes ?? ['openid']), 'offline_access'])).join(' ');
39
39
  return {
40
40
  id: options.id,
41
41
  name: options.name,
42
- type: "oauth",
42
+ type: 'oauth',
43
43
  clientId: options.connector,
44
44
  // The token endpoint expects client_secret_basic, but the secret
45
45
  // is a Vercel OIDC token that has to be fetched per-request. We
46
46
  // rely on the customFetch hook below to inject the token endpoint
47
47
  // Authorization header.
48
- client: { token_endpoint_auth_method: "none" },
48
+ client: { token_endpoint_auth_method: 'none' },
49
49
  authorization: {
50
50
  // Best-effort: scope the Vercel Connect consent UI to the
51
51
  // deployment's Vercel team when we can read it from the OIDC
@@ -56,7 +56,7 @@ export function connect(options) {
56
56
  },
57
57
  token: CONNECT_TOKEN_URL,
58
58
  userinfo: CONNECT_USERINFO_URL,
59
- checks: ["pkce"],
59
+ checks: ['pkce'],
60
60
  profile(profile) {
61
61
  return {
62
62
  id: profile.sub,
@@ -66,7 +66,7 @@ export function connect(options) {
66
66
  };
67
67
  },
68
68
  [customFetch]: async (input, init) => {
69
- const url = typeof input === "string"
69
+ const url = typeof input === 'string'
70
70
  ? input
71
71
  : input instanceof URL
72
72
  ? input.href
@@ -75,7 +75,7 @@ export function connect(options) {
75
75
  const vercelToken = await fetchVercelToken();
76
76
  const credentials = `${encodeURIComponent(options.connector)}:${encodeURIComponent(vercelToken)}`;
77
77
  const headers = new Headers(init?.headers);
78
- headers.set("authorization", `Basic ${btoa(credentials)}`);
78
+ headers.set('authorization', `Basic ${btoa(credentials)}`);
79
79
  return fetch(input, { ...init, headers });
80
80
  }
81
81
  return fetch(input, init);
@@ -7,4 +7,4 @@
7
7
  * installed in the consumer project, but the rest of
8
8
  * `@vercel/connect` works without it.
9
9
  */
10
- export { connect, type AuthJsConnectOptions, type ConnectProfile, } from "./connect-provider.js";
10
+ export { connect, type AuthJsConnectOptions, type ConnectProfile, } from './connect-provider.js';
@@ -7,4 +7,4 @@
7
7
  * installed in the consumer project, but the rest of
8
8
  * `@vercel/connect` works without it.
9
9
  */
10
- export { connect, } from "./connect-provider.js";
10
+ export { connect, } from './connect-provider.js';
@@ -1,12 +1,16 @@
1
- import type { ConnectTokenParams } from "./token.js";
1
+ import type { ConnectTokenParams } from './token.js';
2
2
  export interface ConnectAuthorizationOptions {
3
3
  vercelToken?: string;
4
4
  callbackUrl?: string;
5
5
  webhook?: string;
6
+ deviceCode?: boolean;
7
+ expiresInMs?: number;
6
8
  }
7
9
  export interface ConnectAuthorizationResponse {
8
10
  request: string;
9
11
  verifier: string;
10
12
  url: string;
13
+ deviceCode?: string;
14
+ expiresAt?: number;
11
15
  }
12
16
  export declare function startAuthorization(connector: string, params: ConnectTokenParams, options?: ConnectAuthorizationOptions): Promise<ConnectAuthorizationResponse>;
@@ -1,7 +1,7 @@
1
- import { getVercelOidcToken } from "@vercel/oidc";
1
+ import { getVercelOidcToken } from '@vercel/oidc';
2
2
  export async function startAuthorization(connector, params, options) {
3
3
  if (!connector) {
4
- throw new Error("connector is required");
4
+ throw new Error('connector is required');
5
5
  }
6
6
  if (options?.callbackUrl !== undefined) {
7
7
  validateCallbackUrl(options.callbackUrl);
@@ -17,12 +17,18 @@ export async function startAuthorization(connector, params, options) {
17
17
  returnUrl: options.callbackUrl,
18
18
  }),
19
19
  ...(options?.webhook !== undefined && { webhook: options.webhook }),
20
+ ...(options?.deviceCode !== undefined && {
21
+ deviceCode: options.deviceCode,
22
+ }),
23
+ ...(options?.expiresInMs !== undefined && {
24
+ expiresInMs: options.expiresInMs,
25
+ }),
20
26
  };
21
27
  const response = await fetch(endpoint, {
22
- method: "POST",
28
+ method: 'POST',
23
29
  headers: {
24
- Accept: "application/json",
25
- "Content-Type": "application/json",
30
+ Accept: 'application/json',
31
+ 'Content-Type': 'application/json',
26
32
  Authorization: `Bearer ${vercelToken}`,
27
33
  },
28
34
  body: JSON.stringify(body),
@@ -33,10 +39,10 @@ export async function startAuthorization(connector, params, options) {
33
39
  errorText = await response.text();
34
40
  }
35
41
  catch { }
36
- throw new Error(`Failed to start authorization: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
42
+ throw new Error(`Failed to start authorization: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ''}`);
37
43
  }
38
- const { request, verifier, url } = await response.json();
39
- return { request, verifier, url };
44
+ const data = await response.json();
45
+ return data;
40
46
  }
41
47
  function validateCallbackUrl(value) {
42
48
  let url;
@@ -46,10 +52,10 @@ function validateCallbackUrl(value) {
46
52
  catch {
47
53
  throw new Error(`Invalid callbackUrl: ${value}`);
48
54
  }
49
- if (url.protocol === "https:")
55
+ if (url.protocol === 'https:')
50
56
  return;
51
- if (url.protocol === "http:" &&
52
- (url.hostname === "localhost" || url.hostname === "127.0.0.1")) {
57
+ if (url.protocol === 'http:' &&
58
+ (url.hostname === 'localhost' || url.hostname === '127.0.0.1')) {
53
59
  return;
54
60
  }
55
61
  throw new Error(`callbackUrl must be https:// or http://localhost, got: ${value}`);
@@ -62,7 +68,7 @@ function validateWebhookUrl(value) {
62
68
  catch {
63
69
  throw new Error(`Invalid webhook URL: ${value}`);
64
70
  }
65
- if (url.protocol !== "https:") {
71
+ if (url.protocol !== 'https:') {
66
72
  throw new Error(`webhook must be https://, got: ${value}`);
67
73
  }
68
74
  }
@@ -1,4 +1,4 @@
1
- import type { GenericOAuthConfig } from "better-auth/plugins/generic-oauth";
1
+ import type { GenericOAuthConfig } from 'better-auth/plugins/generic-oauth';
2
2
  /** Options accepted by {@link connect}. */
3
3
  export interface BetterAuthConnectOptions {
4
4
  /**
@@ -27,19 +27,20 @@
27
27
  * });
28
28
  * ```
29
29
  */
30
- import { getVercelOidcToken } from "@vercel/oidc";
31
- import { getOAuth2Tokens } from "better-auth/oauth2";
32
- import { tryGetVercelTeamId, withTeamId } from "../internal/team-id.js";
33
- const CONNECT_AUTHORIZATION_URL = "https://connect.vercel.com/oauth/authorize";
34
- const CONNECT_TOKEN_URL = "https://connect.vercel.com/oauth/token";
35
- const CONNECT_USERINFO_URL = "https://connect.vercel.com/oauth/userinfo";
30
+ import { getVercelOidcToken } from '@vercel/oidc';
31
+ import { getOAuth2Tokens } from 'better-auth/oauth2';
32
+ import { tryGetVercelTeamId, withTeamId } from '../internal/team-id.js';
33
+ import { ConnectError, createConnectErrorFromResponse } from '../token.js';
34
+ const CONNECT_AUTHORIZATION_URL = 'https://connect.vercel.com/oauth/authorize';
35
+ const CONNECT_TOKEN_URL = 'https://connect.vercel.com/oauth/token';
36
+ const CONNECT_USERINFO_URL = 'https://connect.vercel.com/oauth/userinfo';
36
37
  /**
37
38
  * Builds a `GenericOAuthConfig` entry for Vercel Connect. Pass the
38
39
  * result into `genericOAuth({ config: [...] })`.
39
40
  */
40
41
  export function connect(options) {
41
42
  const fetchVercelToken = options.getVercelOidcToken ?? getVercelOidcToken;
42
- const scopes = Array.from(new Set([...(options.scopes ?? ["openid"]), "offline_access"]));
43
+ const scopes = Array.from(new Set([...(options.scopes ?? ['openid']), 'offline_access']));
43
44
  return {
44
45
  providerId: options.providerId,
45
46
  clientId: options.connector,
@@ -56,37 +57,39 @@ export function connect(options) {
56
57
  const credentials = `${encodeURIComponent(options.connector)}:${encodeURIComponent(vercelToken)}`;
57
58
  const basicAuth = btoa(credentials);
58
59
  const body = new URLSearchParams({
59
- grant_type: "authorization_code",
60
+ grant_type: 'authorization_code',
60
61
  code,
61
62
  redirect_uri: redirectURI,
62
63
  });
63
64
  if (codeVerifier)
64
- body.set("code_verifier", codeVerifier);
65
+ body.set('code_verifier', codeVerifier);
65
66
  if (deviceId)
66
- body.set("device_id", deviceId);
67
+ body.set('device_id', deviceId);
67
68
  const response = await fetch(CONNECT_TOKEN_URL, {
68
- method: "POST",
69
+ method: 'POST',
69
70
  headers: {
70
- "Content-Type": "application/x-www-form-urlencoded",
71
- Accept: "application/json",
71
+ 'Content-Type': 'application/x-www-form-urlencoded',
72
+ Accept: 'application/json',
72
73
  Authorization: `Basic ${basicAuth}`,
73
74
  },
74
75
  body: body.toString(),
75
76
  });
76
77
  if (!response.ok) {
77
- throw new Error(`Failed to exchange Vercel Connect authorization code: ${response.status} ${response.statusText}: ${await response.text()}`);
78
+ throw await createConnectErrorFromResponse(response, 'Failed to exchange Vercel Connect authorization code');
78
79
  }
79
80
  return getOAuth2Tokens(await response.json());
80
81
  },
81
82
  getUserInfo: async (tokens) => {
82
83
  if (!tokens.accessToken) {
83
- throw new Error("Missing access token for Vercel Connect userinfo request");
84
+ throw new ConnectError('Missing access token for Vercel Connect userinfo request', {
85
+ code: 'missing_access_token',
86
+ });
84
87
  }
85
88
  const response = await fetch(CONNECT_USERINFO_URL, {
86
89
  headers: { Authorization: `Bearer ${tokens.accessToken}` },
87
90
  });
88
91
  if (!response.ok) {
89
- throw new Error(`Failed to fetch Vercel Connect user info: ${response.status} ${response.statusText}: ${await response.text()}`);
92
+ throw await createConnectErrorFromResponse(response, 'Failed to fetch Vercel Connect user info');
90
93
  }
91
94
  const user = (await response.json());
92
95
  return {
@@ -7,4 +7,5 @@
7
7
  * in the consumer project, but the rest of `@vercel/connect` works
8
8
  * without it.
9
9
  */
10
- export { connect, type BetterAuthConnectOptions } from "./connect-provider.js";
10
+ export { connect, type BetterAuthConnectOptions } from './connect-provider.js';
11
+ export { ConnectError, type ConnectErrorOptions, type ConnectVendorErrorPayload, } from '../token.js';
@@ -7,4 +7,5 @@
7
7
  * in the consumer project, but the rest of `@vercel/connect` works
8
8
  * without it.
9
9
  */
10
- export { connect } from "./connect-provider.js";
10
+ export { connect } from './connect-provider.js';
11
+ export { ConnectError, } from '../token.js';
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { getToken, getTokenResponse, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, type ConnectOptions, type ConnectTokenParams, type ConnectTokenResponse, } from "./token.js";
2
- export { startAuthorization, type ConnectAuthorizationOptions, type ConnectAuthorizationResponse, } from "./authorization.js";
1
+ export { getToken, getTokenResponse, ConnectError, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, type ConnectErrorOptions, type ConnectOptions, type ConnectTokenParams, type ConnectTokenResponse, type ConnectVendorErrorPayload, } from './token.js';
2
+ export { startAuthorization, type ConnectAuthorizationOptions, type ConnectAuthorizationResponse, } from './authorization.js';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { getToken, getTokenResponse, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, } from "./token.js";
2
- export { startAuthorization, } from "./authorization.js";
1
+ export { getToken, getTokenResponse, ConnectError, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, } from './token.js';
2
+ export { startAuthorization, } from './authorization.js';
@@ -13,7 +13,7 @@
13
13
  * be decoded. Adapters then fall back to an unqualified
14
14
  * authorization URL.
15
15
  */
16
- import { getVercelOidcTokenSync } from "@vercel/oidc";
16
+ import { getVercelOidcTokenSync } from '@vercel/oidc';
17
17
  export function tryGetVercelTeamId() {
18
18
  let token;
19
19
  try {
@@ -26,13 +26,13 @@ export function tryGetVercelTeamId() {
26
26
  }
27
27
  export function extractTeamIdFromOidcToken(token) {
28
28
  try {
29
- const parts = token.split(".");
29
+ const parts = token.split('.');
30
30
  if (parts.length < 2)
31
31
  return undefined;
32
- const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
32
+ const padded = parts[1].replace(/-/g, '+').replace(/_/g, '/');
33
33
  const padLen = (4 - (padded.length % 4)) % 4;
34
- const claims = JSON.parse(atob(padded + "=".repeat(padLen)));
35
- return typeof claims.owner_id === "string" ? claims.owner_id : undefined;
34
+ const claims = JSON.parse(atob(padded + '='.repeat(padLen)));
35
+ return typeof claims.owner_id === 'string' ? claims.owner_id : undefined;
36
36
  }
37
37
  catch {
38
38
  return undefined;
@@ -42,6 +42,6 @@ export function withTeamId(baseUrl, teamId) {
42
42
  if (!teamId)
43
43
  return baseUrl;
44
44
  const url = new URL(baseUrl);
45
- url.searchParams.set("teamId", teamId);
45
+ url.searchParams.set('teamId', teamId);
46
46
  return url.toString();
47
47
  }
package/dist/token.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export interface ConnectTokenParams {
2
2
  subject: {
3
- type: "app";
3
+ type: 'app';
4
4
  } | {
5
- type: "user";
5
+ type: 'user';
6
6
  id: string;
7
7
  issuer?: string;
8
8
  };
@@ -39,17 +39,32 @@ export interface ConnectTokenResponse {
39
39
  /** Driver-specific metadata stored during OAuth */
40
40
  metadata?: Record<string, unknown>;
41
41
  }
42
- export declare class NoValidTokenError extends Error {
43
- constructor(message: string);
42
+ export type ConnectVendorErrorPayload = Record<string, unknown>;
43
+ export interface ConnectErrorOptions {
44
+ code?: string;
45
+ status?: number;
46
+ statusText?: string;
47
+ vendor?: ConnectVendorErrorPayload;
44
48
  }
45
- export declare class UserAuthorizationRequiredError extends Error {
46
- constructor(message: string);
49
+ export declare class ConnectError extends Error {
50
+ readonly code?: string;
51
+ readonly status?: number;
52
+ readonly statusText?: string;
53
+ readonly vendor?: ConnectVendorErrorPayload;
54
+ constructor(message: string, options?: ConnectErrorOptions);
47
55
  }
48
- export declare class ConnectorInstallationRequiredError extends Error {
49
- constructor(message: string);
56
+ export declare class NoValidTokenError extends ConnectError {
57
+ constructor(message: string, options?: ConnectErrorOptions);
58
+ }
59
+ export declare class UserAuthorizationRequiredError extends ConnectError {
60
+ constructor(message: string, options?: ConnectErrorOptions);
61
+ }
62
+ export declare class ConnectorInstallationRequiredError extends ConnectError {
63
+ constructor(message: string, options?: ConnectErrorOptions);
50
64
  }
51
65
  export interface ConnectOptions {
52
66
  vercelToken?: string;
53
67
  }
54
68
  export declare function getToken(connector: string, params: ConnectTokenParams, options?: ConnectOptions): Promise<string>;
55
69
  export declare function getTokenResponse(connector: string, params: ConnectTokenParams, options?: ConnectOptions): Promise<ConnectTokenResponse>;
70
+ export declare function createConnectErrorFromResponse(response: Response, fallbackMessage: string): Promise<ConnectError>;