@vercel/connect 0.2.1 → 0.2.3
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/dist/eve/connection-authorization.d.ts +46 -0
- package/dist/eve/connection-authorization.js +51 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/token.d.ts +30 -0
- package/dist/token.js +41 -8
- package/package.json +1 -1
|
@@ -82,6 +82,25 @@ export interface EveAuthorizationOptions {
|
|
|
82
82
|
* and rely on `@vercel/oidc` auto-discovery.
|
|
83
83
|
*/
|
|
84
84
|
readonly connectOptions?: ConnectOptions;
|
|
85
|
+
/**
|
|
86
|
+
* Re-validate the grant against Vercel Connect on every `getToken`
|
|
87
|
+
* instead of trusting the in-process token cache.
|
|
88
|
+
*
|
|
89
|
+
* By default `getToken` returns a cached token as long as it has not
|
|
90
|
+
* expired, so a grant the user revoked server-side keeps reaching the
|
|
91
|
+
* tool until the bearer's natural expiry. With `validate: true` each
|
|
92
|
+
* `getToken` bypasses the local cache and re-checks Connect; a revoked
|
|
93
|
+
* grant comes back as `no_token` / `user_authorization_required`, which
|
|
94
|
+
* the adapter maps to {@link ConnectionAuthorizationRequiredError} so
|
|
95
|
+
* Eve re-runs the consent flow rather than calling the tool with a dead
|
|
96
|
+
* token.
|
|
97
|
+
*
|
|
98
|
+
* This trades a Connect round trip per call for freshness — leave it
|
|
99
|
+
* off for high-frequency tools where a short revocation window is
|
|
100
|
+
* acceptable, and turn it on for sensitive actions (payments, deletes,
|
|
101
|
+
* privileged writes) that must never run on a revoked grant.
|
|
102
|
+
*/
|
|
103
|
+
readonly validate?: boolean;
|
|
85
104
|
/**
|
|
86
105
|
* Custom call-to-action rendered on the
|
|
87
106
|
* `connection.authorization_required` event. When omitted, Eve
|
|
@@ -132,6 +151,33 @@ export interface VercelConnectMetadata {
|
|
|
132
151
|
*/
|
|
133
152
|
export type EveConnectAuthorizationDefinition<TAuthorization extends InteractiveAuthorizationDefinition | NonInteractiveAuthorizationDefinition> = TAuthorization & {
|
|
134
153
|
readonly vercelConnect: VercelConnectMetadata;
|
|
154
|
+
/**
|
|
155
|
+
* Drops the in-process Vercel Connect token cache entry for
|
|
156
|
+
* `principal` so the next `getToken` re-fetches instead of re-serving a
|
|
157
|
+
* rejected bearer. Eve's runtime calls this from its shared eviction
|
|
158
|
+
* path when a resolved token is rejected (a downstream `401` mapped to
|
|
159
|
+
* `requireAuth()`, or an MCP server rejecting the bearer), cascading
|
|
160
|
+
* invalidation from Eve's per-step cache down into this adapter's cache.
|
|
161
|
+
*
|
|
162
|
+
* By default this is a local-cache-only operation: it preserves the
|
|
163
|
+
* underlying Connect grant (and its refresh token), so the next
|
|
164
|
+
* `getToken` can refresh a merely-expired access token without forcing
|
|
165
|
+
* a new consent flow. That is the right default for the automatic
|
|
166
|
+
* `401` cascade, where the rejected bearer is usually just stale.
|
|
167
|
+
*
|
|
168
|
+
* Pass `revoke: true` only when the grant itself is known to be dead
|
|
169
|
+
* and you want it torn down at Vercel Connect (refresh token included)
|
|
170
|
+
* so the next `getToken` surfaces `user_authorization_required` and
|
|
171
|
+
* re-runs consent — e.g. a user-initiated "disconnect this
|
|
172
|
+
* integration" action. Revocation is destructive and best-effort: a
|
|
173
|
+
* failed or duplicate revoke is swallowed so it never masks the error
|
|
174
|
+
* that triggered eviction, and the local cache entry is dropped either
|
|
175
|
+
* way.
|
|
176
|
+
*/
|
|
177
|
+
readonly evict: (opts: {
|
|
178
|
+
readonly principal: ConnectionPrincipal;
|
|
179
|
+
readonly revoke?: boolean;
|
|
180
|
+
}) => Promise<void>;
|
|
135
181
|
};
|
|
136
182
|
/**
|
|
137
183
|
* Builds an Eve {@link AuthorizationDefinition} backed by Vercel
|
|
@@ -30,14 +30,48 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { ConnectionAuthorizationFailedError, ConnectionAuthorizationRequiredError, } from 'eve/connections';
|
|
32
32
|
import { startAuthorization } from '../authorization.js';
|
|
33
|
-
import { ConnectorInstallationRequiredError, getTokenResponse, NoValidTokenError, UserAuthorizationRequiredError, } from '../token.js';
|
|
33
|
+
import { ConnectorInstallationRequiredError, deleteTokenCacheEntry, getTokenResponse, NoValidTokenError, revokeToken, UserAuthorizationRequiredError, } from '../token.js';
|
|
34
34
|
export function connect(input) {
|
|
35
35
|
const options = normalizeAuthorizationOptions(input);
|
|
36
36
|
const vercelConnect = { connector: options.connector };
|
|
37
|
+
const evict = makeEvict(options);
|
|
37
38
|
if (options.principalType === 'app') {
|
|
38
|
-
return { ...buildNonInteractiveDefinition(options), vercelConnect };
|
|
39
|
+
return { ...buildNonInteractiveDefinition(options), vercelConnect, evict };
|
|
39
40
|
}
|
|
40
|
-
return { ...buildInteractiveDefinition(options), vercelConnect };
|
|
41
|
+
return { ...buildInteractiveDefinition(options), vercelConnect, evict };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Builds the {@link EveConnectAuthorizationDefinition.evict} callback for
|
|
45
|
+
* a connector. Resolves the same token params {@link getToken} uses for
|
|
46
|
+
* `principal`, then drops exactly that cache entry — leaving every other
|
|
47
|
+
* principal's cached token intact.
|
|
48
|
+
*
|
|
49
|
+
* When called with `revoke: true` it instead tears the grant down at
|
|
50
|
+
* Vercel Connect via {@link revokeToken} (best-effort, falling back to a
|
|
51
|
+
* local cache drop if the revoke request fails).
|
|
52
|
+
*/
|
|
53
|
+
function makeEvict(options) {
|
|
54
|
+
return async ({ principal, revoke }) => {
|
|
55
|
+
const params = await buildTokenParams(options, principal);
|
|
56
|
+
if (revoke) {
|
|
57
|
+
try {
|
|
58
|
+
// Destructive: tears down the grant at Vercel Connect (refresh
|
|
59
|
+
// token included) and clears the in-process cache. Best-effort —
|
|
60
|
+
// a failed or duplicate revoke must not mask the auth error that
|
|
61
|
+
// triggered eviction.
|
|
62
|
+
await revokeToken(options.connector, {
|
|
63
|
+
subject: params.subject,
|
|
64
|
+
installationId: params.installationId,
|
|
65
|
+
}, options.connectOptions);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Fall through to the local cache drop so the rejected bearer is
|
|
70
|
+
// gone even when the server-side revoke failed.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
deleteTokenCacheEntry(options.connector, params);
|
|
74
|
+
};
|
|
41
75
|
}
|
|
42
76
|
function normalizeAuthorizationOptions(input) {
|
|
43
77
|
if (typeof input === 'string') {
|
|
@@ -50,7 +84,7 @@ function buildInteractiveDefinition(options) {
|
|
|
50
84
|
principalType: 'user',
|
|
51
85
|
async getToken({ principal }) {
|
|
52
86
|
try {
|
|
53
|
-
const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal), options
|
|
87
|
+
const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal), getTokenConnectOptions(options));
|
|
54
88
|
return { token: response.token, expiresAt: response.expiresAt };
|
|
55
89
|
}
|
|
56
90
|
catch (error) {
|
|
@@ -118,7 +152,7 @@ function buildNonInteractiveDefinition(options) {
|
|
|
118
152
|
principalType: 'app',
|
|
119
153
|
async getToken({ principal }) {
|
|
120
154
|
try {
|
|
121
|
-
const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal), options
|
|
155
|
+
const response = await getTokenResponse(options.connector, await buildTokenParams(options, principal), getTokenConnectOptions(options));
|
|
122
156
|
return { token: response.token, expiresAt: response.expiresAt };
|
|
123
157
|
}
|
|
124
158
|
catch (error) {
|
|
@@ -127,6 +161,18 @@ function buildNonInteractiveDefinition(options) {
|
|
|
127
161
|
},
|
|
128
162
|
};
|
|
129
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Connect SDK options for `getToken` calls. When {@link
|
|
166
|
+
* EveAuthorizationOptions.validate} is set, forces a cache-bypassing
|
|
167
|
+
* re-fetch so a revoked-but-unexpired grant is caught instead of served
|
|
168
|
+
* from the in-process token cache.
|
|
169
|
+
*/
|
|
170
|
+
function getTokenConnectOptions(options) {
|
|
171
|
+
if (!options.validate) {
|
|
172
|
+
return options.connectOptions;
|
|
173
|
+
}
|
|
174
|
+
return { ...options.connectOptions, forceRefresh: true };
|
|
175
|
+
}
|
|
130
176
|
async function buildTokenParams(options, principal) {
|
|
131
177
|
const toSubject = options.principalToSubject ?? principalToSubject;
|
|
132
178
|
return {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { getToken, getTokenResponse, revokeToken, ConnectError, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, type ConnectErrorOptions, type ConnectOptions, type ConnectTokenParams, type ConnectTokenResponse, type ConnectTokenSubject, type ConnectVendorErrorPayload, } from './token.js';
|
|
1
|
+
export { deleteTokenCacheEntry, getToken, getTokenResponse, revokeToken, ConnectError, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, type ConnectErrorOptions, type ConnectOptions, type ConnectTokenParams, type ConnectTokenResponse, type ConnectTokenSubject, type ConnectVendorErrorPayload, } from './token.js';
|
|
2
2
|
export { startAuthorization, type ConnectAuthorizationOptions, type ConnectAuthorizationResponse, } from './authorization.js';
|
|
3
3
|
export type { ConnectAuthorizationDetail } from './authorization-details.js';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { getToken, getTokenResponse, revokeToken, ConnectError, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, } from './token.js';
|
|
1
|
+
export { deleteTokenCacheEntry, getToken, getTokenResponse, revokeToken, ConnectError, NoValidTokenError, UserAuthorizationRequiredError, ConnectorInstallationRequiredError, } from './token.js';
|
|
2
2
|
export { startAuthorization, } from './authorization.js';
|
package/dist/token.d.ts
CHANGED
|
@@ -80,6 +80,18 @@ export declare class ConnectorInstallationRequiredError extends ConnectError {
|
|
|
80
80
|
}
|
|
81
81
|
export interface ConnectOptions {
|
|
82
82
|
vercelToken?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Bypass the in-process token cache and re-fetch from Vercel Connect.
|
|
85
|
+
*
|
|
86
|
+
* The cache normally serves any token that is not within
|
|
87
|
+
* {@link ConnectTokenParams.validityBufferMs} of expiry. That means a
|
|
88
|
+
* grant the user revoked server-side keeps being handed back from the
|
|
89
|
+
* local cache until it expires. Set `forceRefresh` when the caller
|
|
90
|
+
* needs Connect to re-validate the grant on this call — a revoked grant
|
|
91
|
+
* then surfaces as `no_token` / `user_authorization_required` instead of
|
|
92
|
+
* a stale bearer.
|
|
93
|
+
*/
|
|
94
|
+
forceRefresh?: boolean;
|
|
83
95
|
}
|
|
84
96
|
export declare function getToken(connector: string, params: ConnectTokenParams, options?: ConnectOptions): Promise<string>;
|
|
85
97
|
export declare function getTokenResponse(connector: string, params: ConnectTokenParams, options?: ConnectOptions): Promise<ConnectTokenResponse>;
|
|
@@ -87,4 +99,22 @@ export declare function revokeToken(connector: string, params: {
|
|
|
87
99
|
subject: ConnectTokenSubject;
|
|
88
100
|
installationId?: string;
|
|
89
101
|
}, options?: ConnectOptions): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Remove a single cached token entry for `(connector, params)` from the
|
|
104
|
+
* in-process cache.
|
|
105
|
+
*
|
|
106
|
+
* Targeted counterpart to {@link revokeToken}'s `cache.clear()`: it drops
|
|
107
|
+
* exactly the entry {@link getTokenResponse} would serve for these
|
|
108
|
+
* arguments, leaving every other connector/principal untouched. Use it
|
|
109
|
+
* when a credential is known to be bad (the resource server rejected the
|
|
110
|
+
* bearer with a `401`) so the next {@link getTokenResponse} re-fetches
|
|
111
|
+
* instead of re-serving the rejected token — without paying for a Connect
|
|
112
|
+
* round trip on every call the way {@link ConnectOptions.forceRefresh}
|
|
113
|
+
* does.
|
|
114
|
+
*
|
|
115
|
+
* The cache key is derived from `connector` plus every field of `params`,
|
|
116
|
+
* so pass the same `params` used for the original {@link getTokenResponse}
|
|
117
|
+
* call. No-op when no matching entry exists.
|
|
118
|
+
*/
|
|
119
|
+
export declare function deleteTokenCacheEntry(connector: string, params: ConnectTokenParams): void;
|
|
90
120
|
export declare function createConnectErrorFromResponse(response: Response, fallbackMessage: string): Promise<ConnectError>;
|
package/dist/token.js
CHANGED
|
@@ -37,16 +37,21 @@ export async function getToken(connector, params, options) {
|
|
|
37
37
|
}
|
|
38
38
|
export async function getTokenResponse(connector, params, options) {
|
|
39
39
|
const bufferMs = params.validityBufferMs ?? DEFAULT_VALIDITY_BUFFER_MS;
|
|
40
|
-
const cacheKey =
|
|
41
|
-
|
|
42
|
-
if (cached) {
|
|
43
|
-
const now = Date.now();
|
|
44
|
-
if (cached.response.expiresAt - now > bufferMs) {
|
|
45
|
-
cached.lastUsed = now;
|
|
46
|
-
return cached.response;
|
|
47
|
-
}
|
|
40
|
+
const cacheKey = tokenCacheKey(connector, params);
|
|
41
|
+
if (options?.forceRefresh) {
|
|
48
42
|
cache.delete(cacheKey);
|
|
49
43
|
}
|
|
44
|
+
else {
|
|
45
|
+
const cached = cache.get(cacheKey);
|
|
46
|
+
if (cached) {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
if (cached.response.expiresAt - now > bufferMs) {
|
|
49
|
+
cached.lastUsed = now;
|
|
50
|
+
return cached.response;
|
|
51
|
+
}
|
|
52
|
+
cache.delete(cacheKey);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
50
55
|
const vercelToken = options?.vercelToken ?? (await getVercelOidcToken());
|
|
51
56
|
const endpoint = `https://api.vercel.com/v1/connect/token/${encodeURIComponent(connector)}`;
|
|
52
57
|
const response = await fetch(endpoint, {
|
|
@@ -85,9 +90,37 @@ export async function revokeToken(connector, params, options) {
|
|
|
85
90
|
}
|
|
86
91
|
cache.clear();
|
|
87
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Remove a single cached token entry for `(connector, params)` from the
|
|
95
|
+
* in-process cache.
|
|
96
|
+
*
|
|
97
|
+
* Targeted counterpart to {@link revokeToken}'s `cache.clear()`: it drops
|
|
98
|
+
* exactly the entry {@link getTokenResponse} would serve for these
|
|
99
|
+
* arguments, leaving every other connector/principal untouched. Use it
|
|
100
|
+
* when a credential is known to be bad (the resource server rejected the
|
|
101
|
+
* bearer with a `401`) so the next {@link getTokenResponse} re-fetches
|
|
102
|
+
* instead of re-serving the rejected token — without paying for a Connect
|
|
103
|
+
* round trip on every call the way {@link ConnectOptions.forceRefresh}
|
|
104
|
+
* does.
|
|
105
|
+
*
|
|
106
|
+
* The cache key is derived from `connector` plus every field of `params`,
|
|
107
|
+
* so pass the same `params` used for the original {@link getTokenResponse}
|
|
108
|
+
* call. No-op when no matching entry exists.
|
|
109
|
+
*/
|
|
110
|
+
export function deleteTokenCacheEntry(connector, params) {
|
|
111
|
+
cache.delete(tokenCacheKey(connector, params));
|
|
112
|
+
}
|
|
88
113
|
const DEFAULT_VALIDITY_BUFFER_MS = 30_000;
|
|
89
114
|
const MAX_CACHE_SIZE = 100;
|
|
90
115
|
const cache = new Map();
|
|
116
|
+
/**
|
|
117
|
+
* Cache key for a `(connector, params)` pair. Stable across calls with
|
|
118
|
+
* equal arguments so {@link getTokenResponse} and
|
|
119
|
+
* {@link deleteTokenCacheEntry} address the same entry.
|
|
120
|
+
*/
|
|
121
|
+
function tokenCacheKey(connector, params) {
|
|
122
|
+
return JSON.stringify({ connector, ...params });
|
|
123
|
+
}
|
|
91
124
|
function evictLru() {
|
|
92
125
|
let oldestKey;
|
|
93
126
|
let oldestTime = Infinity;
|