@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
|
@@ -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
|
|
32
|
-
import type { ConnectOptions, ConnectTokenParams } from
|
|
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 =
|
|
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?:
|
|
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,
|
|
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?:
|
|
161
|
+
readonly principalType?: 'user';
|
|
162
162
|
}): AshConnectAuthorizationDefinition<InteractiveAuthorizationDefinition<ConnectAuthorizationState>>;
|
|
163
163
|
export declare function connect(options: AshAuthorizationOptions & {
|
|
164
|
-
readonly principalType:
|
|
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
|
|
32
|
-
import { startAuthorization } from
|
|
33
|
-
import { ConnectorInstallationRequiredError, getTokenResponse, NoValidTokenError, UserAuthorizationRequiredError, } from
|
|
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 ===
|
|
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 ===
|
|
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:
|
|
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,
|
|
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), {
|
|
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:
|
|
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,
|
|
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,
|
|
112
|
+
throw translate(error, 'completeAuthorization', options);
|
|
100
113
|
}
|
|
101
114
|
},
|
|
102
115
|
};
|
|
103
116
|
}
|
|
104
117
|
function buildNonInteractiveDefinition(options) {
|
|
105
118
|
return {
|
|
106
|
-
principalType:
|
|
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,
|
|
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 ===
|
|
126
|
-
return { type:
|
|
138
|
+
if (principal.type === 'app') {
|
|
139
|
+
return { type: 'app' };
|
|
127
140
|
}
|
|
128
|
-
return { type:
|
|
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 ===
|
|
158
|
-
return new ConnectionAuthorizationFailedError(
|
|
164
|
+
if (options.principalType === 'app') {
|
|
165
|
+
return new ConnectionAuthorizationFailedError('connect', {
|
|
159
166
|
message: error.message,
|
|
160
|
-
reason:
|
|
167
|
+
reason: 'app_not_installed',
|
|
161
168
|
retryable: false,
|
|
162
169
|
});
|
|
163
170
|
}
|
|
164
|
-
if (phase ===
|
|
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(
|
|
171
|
-
message:
|
|
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(
|
|
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(
|
|
189
|
+
return new ConnectionAuthorizationFailedError('connect', {
|
|
183
190
|
message: error.message,
|
|
184
|
-
reason:
|
|
191
|
+
reason: 'connector_installation_required',
|
|
185
192
|
retryable: false,
|
|
186
193
|
});
|
|
187
194
|
}
|
package/dist/ash/index.d.ts
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, type AshAuthorizationInput, type AshAuthorizationOptions, type AshConnectAuthorizationDefinition, type ConnectAuthorizationPhase, type ConnectAuthorizationState, type VercelConnectMetadata, } from
|
|
10
|
-
export { CONNECT_OAUTH_ISSUER, connectOAuth, type ConnectOAuthAudienceEnvironment, type ConnectOAuthEnvironment, type ConnectOAuthOptions, } from
|
|
11
|
-
export { connectSlackCredentials } from
|
|
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
|
|
10
|
-
export { CONNECT_OAUTH_ISSUER, connectOAuth, } from
|
|
11
|
-
export { connectSlackCredentials } from
|
|
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
|
|
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
|
|
2
|
-
import { vercelOidc } from
|
|
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:
|
|
28
|
+
botToken: () => getToken(connector, { subject: { type: 'app' } }),
|
|
29
29
|
webhookVerifier: vercelOidc(),
|
|
30
30
|
};
|
|
31
31
|
}
|
|
@@ -23,29 +23,29 @@
|
|
|
23
23
|
* };
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
|
-
import { customFetch } from
|
|
27
|
-
import { getVercelOidcToken } from
|
|
28
|
-
import { tryGetVercelTeamId, withTeamId } from
|
|
29
|
-
const CONNECT_AUTHORIZATION_URL =
|
|
30
|
-
const CONNECT_TOKEN_URL =
|
|
31
|
-
const CONNECT_USERINFO_URL =
|
|
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 ?? [
|
|
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:
|
|
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:
|
|
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: [
|
|
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 ===
|
|
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(
|
|
78
|
+
headers.set('authorization', `Basic ${btoa(credentials)}`);
|
|
79
79
|
return fetch(input, { ...init, headers });
|
|
80
80
|
}
|
|
81
81
|
return fetch(input, init);
|
package/dist/authjs/index.d.ts
CHANGED
|
@@ -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
|
|
10
|
+
export { connect, type AuthJsConnectOptions, type ConnectProfile, } from './connect-provider.js';
|
package/dist/authjs/index.js
CHANGED
package/dist/authorization.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import type { ConnectTokenParams } from
|
|
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>;
|
package/dist/authorization.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getVercelOidcToken } from
|
|
1
|
+
import { getVercelOidcToken } from '@vercel/oidc';
|
|
2
2
|
export async function startAuthorization(connector, params, options) {
|
|
3
3
|
if (!connector) {
|
|
4
|
-
throw new Error(
|
|
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:
|
|
28
|
+
method: 'POST',
|
|
23
29
|
headers: {
|
|
24
|
-
Accept:
|
|
25
|
-
|
|
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
|
|
39
|
-
return
|
|
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 ===
|
|
55
|
+
if (url.protocol === 'https:')
|
|
50
56
|
return;
|
|
51
|
-
if (url.protocol ===
|
|
52
|
-
(url.hostname ===
|
|
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 !==
|
|
71
|
+
if (url.protocol !== 'https:') {
|
|
66
72
|
throw new Error(`webhook must be https://, got: ${value}`);
|
|
67
73
|
}
|
|
68
74
|
}
|
|
@@ -27,19 +27,20 @@
|
|
|
27
27
|
* });
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
|
-
import { getVercelOidcToken } from
|
|
31
|
-
import { getOAuth2Tokens } from
|
|
32
|
-
import { tryGetVercelTeamId, withTeamId } from
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
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 ?? [
|
|
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:
|
|
60
|
+
grant_type: 'authorization_code',
|
|
60
61
|
code,
|
|
61
62
|
redirect_uri: redirectURI,
|
|
62
63
|
});
|
|
63
64
|
if (codeVerifier)
|
|
64
|
-
body.set(
|
|
65
|
+
body.set('code_verifier', codeVerifier);
|
|
65
66
|
if (deviceId)
|
|
66
|
-
body.set(
|
|
67
|
+
body.set('device_id', deviceId);
|
|
67
68
|
const response = await fetch(CONNECT_TOKEN_URL, {
|
|
68
|
-
method:
|
|
69
|
+
method: 'POST',
|
|
69
70
|
headers: {
|
|
70
|
-
|
|
71
|
-
Accept:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
10
|
+
export { connect, type BetterAuthConnectOptions } from './connect-provider.js';
|
|
11
|
+
export { ConnectError, type ConnectErrorOptions, type ConnectVendorErrorPayload, } from '../token.js';
|
package/dist/betterauth/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { getToken, getTokenResponse, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, type ConnectOptions, type ConnectTokenParams, type ConnectTokenResponse, } from
|
|
2
|
-
export { startAuthorization, type ConnectAuthorizationOptions, type ConnectAuthorizationResponse, } from
|
|
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
|
|
2
|
-
export { startAuthorization, } from
|
|
1
|
+
export { getToken, getTokenResponse, ConnectError, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, } from './token.js';
|
|
2
|
+
export { startAuthorization, } from './authorization.js';
|
package/dist/internal/team-id.js
CHANGED
|
@@ -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
|
|
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,
|
|
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 +
|
|
35
|
-
return typeof claims.owner_id ===
|
|
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(
|
|
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:
|
|
3
|
+
type: 'app';
|
|
4
4
|
} | {
|
|
5
|
-
type:
|
|
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
|
|
43
|
-
|
|
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
|
|
46
|
-
|
|
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
|
|
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>;
|