@marktiderman/genesis-connect 0.1.3 → 0.3.0
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/README.md +80 -7
- package/dist/index.cjs +151 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -3
- package/dist/index.d.ts +54 -3
- package/dist/index.js +151 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ interface OAuthProvider {
|
|
|
13
13
|
readonly authorizeUrl: string;
|
|
14
14
|
/** The token endpoint (code and refresh-token grants). */
|
|
15
15
|
readonly tokenUrl: string;
|
|
16
|
+
/**
|
|
17
|
+
* The OpenID Connect issuer(s) its ID tokens name in `iss`. Without it, no `identity` is read
|
|
18
|
+
* from an ID token.
|
|
19
|
+
*/
|
|
20
|
+
readonly issuer?: string | readonly string[] | undefined;
|
|
16
21
|
/** The revocation endpoint (RFC 7009), when the provider has one. */
|
|
17
22
|
readonly revokeUrl?: string | undefined;
|
|
18
23
|
/** Scopes asked for when neither the connector nor `start` names any. */
|
|
@@ -53,8 +58,14 @@ declare const GOOGLE_SCOPES: {
|
|
|
53
58
|
readonly driveFile: "https://www.googleapis.com/auth/drive.file";
|
|
54
59
|
/** Every spreadsheet the person can reach. Prefer `driveFile`. */
|
|
55
60
|
readonly spreadsheets: "https://www.googleapis.com/auth/spreadsheets";
|
|
56
|
-
/**
|
|
61
|
+
/** Read and change events on the person's calendars. Prefer `calendarEventsReadonly` to only read. */
|
|
57
62
|
readonly calendarEvents: "https://www.googleapis.com/auth/calendar.events";
|
|
63
|
+
/** Read events on every calendar the person can see, and nothing else. */
|
|
64
|
+
readonly calendarEventsReadonly: "https://www.googleapis.com/auth/calendar.events.readonly";
|
|
65
|
+
/** The list of calendars the person has (names, colors, which are shown), not their events. */
|
|
66
|
+
readonly calendarListReadonly: "https://www.googleapis.com/auth/calendar.calendarlist.readonly";
|
|
67
|
+
/** Read everything on the person's calendars (calendars, events, settings). Prefer the two above. */
|
|
68
|
+
readonly calendarReadonly: "https://www.googleapis.com/auth/calendar.readonly";
|
|
58
69
|
/** Send mail as the person. */
|
|
59
70
|
readonly gmailSend: "https://www.googleapis.com/auth/gmail.send";
|
|
60
71
|
};
|
|
@@ -162,14 +173,35 @@ interface ConnectedAccount<D extends StateData = StateData> {
|
|
|
162
173
|
scope: string;
|
|
163
174
|
/** The granted scopes. */
|
|
164
175
|
scopes: string[];
|
|
165
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* The OpenID Connect ID token, when the provider sent one (an `openid` scope), as it came.
|
|
178
|
+
* Its claims are NOT checked here: read who the account is from `identity`, which is.
|
|
179
|
+
*/
|
|
166
180
|
idToken?: string | undefined;
|
|
181
|
+
/**
|
|
182
|
+
* Who the account is, read from the ID token: to show "connected as …" and to tell two
|
|
183
|
+
* accounts of one person apart. Only with an `openid` scope (add `email` for the address), a
|
|
184
|
+
* provider with an `issuer` and an https token endpoint; left out when the token's issuer,
|
|
185
|
+
* audience, authorized party or expiry don't check out (the connection itself still succeeds).
|
|
186
|
+
*/
|
|
187
|
+
identity?: AccountIdentity | undefined;
|
|
167
188
|
/** From `start`. */
|
|
168
189
|
subject: string;
|
|
169
190
|
intent: string;
|
|
170
191
|
returnTo: string;
|
|
171
192
|
data: D;
|
|
172
193
|
}
|
|
194
|
+
/** The account an ID token names. `subject` is the provider's stable id for it; the rest may change. */
|
|
195
|
+
interface AccountIdentity {
|
|
196
|
+
/** The provider's id for the account (`sub`): stable, unique per provider. */
|
|
197
|
+
subject: string;
|
|
198
|
+
/** Only when the provider says it's verified. Never key an account on it: use `subject`. */
|
|
199
|
+
email?: string | undefined;
|
|
200
|
+
emailVerified?: boolean | undefined;
|
|
201
|
+
/** As the person set them: untrusted text and an untrusted URL. Escape or check before use. */
|
|
202
|
+
name?: string | undefined;
|
|
203
|
+
picture?: string | undefined;
|
|
204
|
+
}
|
|
173
205
|
/** What `store` may add to the way back, as query parameters (a grant id to finish with). */
|
|
174
206
|
type StoreResult = void | undefined | Readonly<Record<string, string>>;
|
|
175
207
|
/** A safe log line: never a token, a code, a secret or a provider's error text. */
|
|
@@ -225,6 +257,25 @@ interface ConnectorConfig<D extends StateData = StateData> extends HttpOptions {
|
|
|
225
257
|
aad?: ((id: string) => string) | undefined;
|
|
226
258
|
/** Fail with `failed` when the provider sends no refresh token. Default true. */
|
|
227
259
|
requireRefreshToken?: boolean | undefined;
|
|
260
|
+
/**
|
|
261
|
+
* Fail with `failed` when no `identity` can be read (no ID token, or one whose issuer,
|
|
262
|
+
* audience or expiry don't check out). Default false: the connection succeeds without one,
|
|
263
|
+
* and the reason is logged.
|
|
264
|
+
*/
|
|
265
|
+
requireIdentity?: boolean | undefined;
|
|
266
|
+
/**
|
|
267
|
+
* Audiences other than `clientId` an ID token's `aud` may also name (another client of the
|
|
268
|
+
* same app). Default none: a token naming any other audience gives no `identity`.
|
|
269
|
+
*/
|
|
270
|
+
trustedAudiences?: readonly string[] | undefined;
|
|
271
|
+
/**
|
|
272
|
+
* Revoke the new grant (best effort, through the provider's `revokeUrl`) when the callback
|
|
273
|
+
* turns it down after the code was traded: a missing scope or, with `requireIdentity`, no
|
|
274
|
+
* identity. Default false: Google ends every token of the person's grant across the project
|
|
275
|
+
* on a revoke, so a returning person who unticks a scope would also lose the connection they
|
|
276
|
+
* already have. Set it true only when each grant is isolated (one connection per client).
|
|
277
|
+
*/
|
|
278
|
+
revokeFailedGrants?: boolean | undefined;
|
|
228
279
|
/**
|
|
229
280
|
* Keeps the new account: the app's row, under `account.id`, for
|
|
230
281
|
* `account.subject` (the user who started). Use that, never a session read
|
|
@@ -304,4 +355,4 @@ declare function clearAccessTokenCache(): void;
|
|
|
304
355
|
/** Sets up a connector for one provider. Checks its config now, so a mistake shows at boot. */
|
|
305
356
|
declare function createConnector<D extends StateData = StateData>(config: ConnectorConfig<D>): Connector<D>;
|
|
306
357
|
|
|
307
|
-
export { CONNECT_RESULTS, ConnectError, type ConnectErrorCode, type ConnectLogEvent, type ConnectResult, type ConnectedAccount, type Connector, type ConnectorConfig, GOOGLE_SCOPES, type HttpOptions, type OAuthProvider, type RefreshedToken, Secrets, type StartOptions, type StartResult, type StateData, type StoreResult, type TokenCallOptions, clearAccessTokenCache, createConnector, googleProvider, isConnectError, isSafeReturnOrigin };
|
|
358
|
+
export { type AccountIdentity, CONNECT_RESULTS, ConnectError, type ConnectErrorCode, type ConnectLogEvent, type ConnectResult, type ConnectedAccount, type Connector, type ConnectorConfig, GOOGLE_SCOPES, type HttpOptions, type OAuthProvider, type RefreshedToken, Secrets, type StartOptions, type StartResult, type StateData, type StoreResult, type TokenCallOptions, clearAccessTokenCache, createConnector, googleProvider, isConnectError, isSafeReturnOrigin };
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ var googleProvider = {
|
|
|
7
7
|
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
8
8
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
9
9
|
revokeUrl: "https://oauth2.googleapis.com/revoke",
|
|
10
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
10
11
|
defaultScopes: [],
|
|
11
12
|
authorizeParams: {
|
|
12
13
|
access_type: "offline",
|
|
@@ -33,8 +34,14 @@ var GOOGLE_SCOPES = {
|
|
|
33
34
|
driveFile: "https://www.googleapis.com/auth/drive.file",
|
|
34
35
|
/** Every spreadsheet the person can reach. Prefer `driveFile`. */
|
|
35
36
|
spreadsheets: "https://www.googleapis.com/auth/spreadsheets",
|
|
36
|
-
/**
|
|
37
|
+
/** Read and change events on the person's calendars. Prefer `calendarEventsReadonly` to only read. */
|
|
37
38
|
calendarEvents: "https://www.googleapis.com/auth/calendar.events",
|
|
39
|
+
/** Read events on every calendar the person can see, and nothing else. */
|
|
40
|
+
calendarEventsReadonly: "https://www.googleapis.com/auth/calendar.events.readonly",
|
|
41
|
+
/** The list of calendars the person has (names, colors, which are shown), not their events. */
|
|
42
|
+
calendarListReadonly: "https://www.googleapis.com/auth/calendar.calendarlist.readonly",
|
|
43
|
+
/** Read everything on the person's calendars (calendars, events, settings). Prefer the two above. */
|
|
44
|
+
calendarReadonly: "https://www.googleapis.com/auth/calendar.readonly",
|
|
38
45
|
/** Send mail as the person. */
|
|
39
46
|
gmailSend: "https://www.googleapis.com/auth/gmail.send"
|
|
40
47
|
};
|
|
@@ -79,6 +86,50 @@ var CONNECT_ERROR_CODES = [
|
|
|
79
86
|
function isConnectError(error) {
|
|
80
87
|
return error instanceof Error && error.name === "ConnectError" && CONNECT_ERROR_CODES.includes(error.code);
|
|
81
88
|
}
|
|
89
|
+
function identityFromTokenEndpoint(idToken, expect) {
|
|
90
|
+
if (expect.issuers.length === 0)
|
|
91
|
+
return { reason: "the provider names no issuer" };
|
|
92
|
+
const part = idToken.split(".")[1];
|
|
93
|
+
if (!part) return { reason: "not a JWT" };
|
|
94
|
+
let claims;
|
|
95
|
+
try {
|
|
96
|
+
const value = JSON.parse(
|
|
97
|
+
new TextDecoder().decode(fromBase64(part))
|
|
98
|
+
);
|
|
99
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
100
|
+
return { reason: "unreadable claims" };
|
|
101
|
+
claims = value;
|
|
102
|
+
} catch {
|
|
103
|
+
return { reason: "unreadable claims" };
|
|
104
|
+
}
|
|
105
|
+
if (typeof claims["iss"] !== "string" || !expect.issuers.includes(claims["iss"]))
|
|
106
|
+
return { reason: "issuer mismatch" };
|
|
107
|
+
const aud = claims["aud"];
|
|
108
|
+
const audiences = typeof aud === "string" ? [aud] : Array.isArray(aud) ? aud : [];
|
|
109
|
+
if (!audiences.includes(expect.clientId))
|
|
110
|
+
return { reason: "audience mismatch" };
|
|
111
|
+
if (audiences.some(
|
|
112
|
+
(a) => a !== expect.clientId && !(typeof a === "string" && expect.trustedAudiences.includes(a))
|
|
113
|
+
))
|
|
114
|
+
return { reason: "untrusted audience" };
|
|
115
|
+
const azp = claims["azp"];
|
|
116
|
+
if (azp !== void 0 && azp !== expect.clientId || audiences.length > 1 && azp === void 0)
|
|
117
|
+
return { reason: "authorized party mismatch" };
|
|
118
|
+
const exp = claims["exp"];
|
|
119
|
+
if (typeof exp !== "number" || exp * 1e3 <= expect.now)
|
|
120
|
+
return { reason: "expired" };
|
|
121
|
+
const text = (key) => typeof claims[key] === "string" && claims[key] ? claims[key] : void 0;
|
|
122
|
+
const subject = text("sub");
|
|
123
|
+
if (!subject) return { reason: "no subject" };
|
|
124
|
+
const verified = claims["email_verified"] === true || claims["email_verified"] === "true";
|
|
125
|
+
return {
|
|
126
|
+
subject,
|
|
127
|
+
// The address only when the provider says it's verified: never key an account on it.
|
|
128
|
+
...verified && text("email") ? { email: text("email"), emailVerified: true } : {},
|
|
129
|
+
...text("name") ? { name: text("name") } : {},
|
|
130
|
+
...text("picture") ? { picture: text("picture") } : {}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
82
133
|
var PUBLIC_SUFFIXES = /* @__PURE__ */ new Set([
|
|
83
134
|
"vercel.app",
|
|
84
135
|
"netlify.app",
|
|
@@ -137,6 +188,21 @@ function checkOrigin(value, what) {
|
|
|
137
188
|
}
|
|
138
189
|
return origin;
|
|
139
190
|
}
|
|
191
|
+
function checkEndpoint(value, what) {
|
|
192
|
+
let url;
|
|
193
|
+
try {
|
|
194
|
+
url = new URL(value);
|
|
195
|
+
} catch {
|
|
196
|
+
throw new ConnectError("config", `The provider's ${what} is not a URL`);
|
|
197
|
+
}
|
|
198
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocal(url.hostname))) {
|
|
199
|
+
throw new ConnectError(
|
|
200
|
+
"config",
|
|
201
|
+
`The provider's ${what} must be https (http only on localhost)`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
140
206
|
function checkPath(value, what) {
|
|
141
207
|
if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || /[\\\s\x00-\x1f\x7f]/.test(value) || value.length > 512) {
|
|
142
208
|
throw new ConnectError(
|
|
@@ -173,6 +239,7 @@ function scopeList(scopes, separator) {
|
|
|
173
239
|
return list;
|
|
174
240
|
}
|
|
175
241
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
242
|
+
var ABANDON_TIMEOUT_MS = 3e3;
|
|
176
243
|
async function timedFetch(deps, url, init, signal) {
|
|
177
244
|
const timeout = AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
178
245
|
const combined = signal ? anySignal([timeout, signal]) : timeout;
|
|
@@ -242,6 +309,8 @@ function createConnector(config) {
|
|
|
242
309
|
"config",
|
|
243
310
|
"The provider needs an id, an authorize URL and a token URL"
|
|
244
311
|
);
|
|
312
|
+
checkEndpoint(provider.tokenUrl, "token URL");
|
|
313
|
+
if (provider.revokeUrl) checkEndpoint(provider.revokeUrl, "revoke URL");
|
|
245
314
|
if (!config.clientId || !config.clientSecret)
|
|
246
315
|
throw new ConnectError(
|
|
247
316
|
"config",
|
|
@@ -311,6 +380,21 @@ function createConnector(config) {
|
|
|
311
380
|
);
|
|
312
381
|
const aliases = provider.scopeAliases ?? {};
|
|
313
382
|
const normalScope = (scope) => aliases[scope] ?? scope;
|
|
383
|
+
const asksOpenid = (scopes) => scopes.some((scope) => normalScope(scope) === "openid");
|
|
384
|
+
const issuers = (typeof provider.issuer === "string" ? [provider.issuer] : provider.issuer ?? []).filter((issuer) => typeof issuer === "string" && issuer.trim() !== "");
|
|
385
|
+
if (config.requireIdentity) {
|
|
386
|
+
if (issuers.length === 0)
|
|
387
|
+
throw new ConnectError(
|
|
388
|
+
"config",
|
|
389
|
+
`requireIdentity needs the ${provider.id} provider's issuer`
|
|
390
|
+
);
|
|
391
|
+
if (!asksOpenid(defaultScopes))
|
|
392
|
+
throw new ConnectError(
|
|
393
|
+
"config",
|
|
394
|
+
"requireIdentity needs the openid scope in the connector's scopes"
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const trustedAudiences = [...config.trustedAudiences ?? []];
|
|
314
398
|
const now = () => config.now?.() ?? Date.now();
|
|
315
399
|
const log = (event) => {
|
|
316
400
|
try {
|
|
@@ -561,6 +645,46 @@ ${refreshToken}`);
|
|
|
561
645
|
tokenCache.set(await cacheKey(plain), entry);
|
|
562
646
|
}
|
|
563
647
|
}
|
|
648
|
+
async function revokeToken(token, hint, signal) {
|
|
649
|
+
if (!provider.revokeUrl) return false;
|
|
650
|
+
const body = new URLSearchParams({ token, token_type_hint: hint });
|
|
651
|
+
const headers = {
|
|
652
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
653
|
+
accept: "application/json"
|
|
654
|
+
};
|
|
655
|
+
if (provider.revokeWithClientAuth) clientAuth(body, headers);
|
|
656
|
+
try {
|
|
657
|
+
const response = await timedFetch(
|
|
658
|
+
config,
|
|
659
|
+
provider.revokeUrl,
|
|
660
|
+
{ method: "POST", headers, body: body.toString() },
|
|
661
|
+
signal
|
|
662
|
+
);
|
|
663
|
+
let gone = response.ok;
|
|
664
|
+
if (!gone && response.status === 400) {
|
|
665
|
+
gone = errorCode(await jsonBody(response)) === "invalid_token";
|
|
666
|
+
} else {
|
|
667
|
+
discard(response);
|
|
668
|
+
}
|
|
669
|
+
log({
|
|
670
|
+
step: "revoke",
|
|
671
|
+
result: gone ? "revoked" : "failed",
|
|
672
|
+
...gone ? {} : { detail: `revoke endpoint answered ${response.status}` }
|
|
673
|
+
});
|
|
674
|
+
return gone;
|
|
675
|
+
} catch {
|
|
676
|
+
log({ step: "revoke", result: "unavailable" });
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function abandonGrant(refreshToken, accessToken) {
|
|
681
|
+
if (config.revokeFailedGrants !== true) return;
|
|
682
|
+
const [token, hint] = typeof refreshToken === "string" && refreshToken ? [refreshToken, "refresh_token"] : [accessToken, "access_token"];
|
|
683
|
+
revokeToken(token, hint, AbortSignal.timeout(ABANDON_TIMEOUT_MS)).catch(
|
|
684
|
+
() => {
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
}
|
|
564
688
|
return {
|
|
565
689
|
provider,
|
|
566
690
|
redirectUri,
|
|
@@ -593,6 +717,11 @@ ${refreshToken}`);
|
|
|
593
717
|
"usage",
|
|
594
718
|
"No scopes to ask for: give the connector or start some"
|
|
595
719
|
);
|
|
720
|
+
if (config.requireIdentity && !asksOpenid(scopes))
|
|
721
|
+
throw new ConnectError(
|
|
722
|
+
"usage",
|
|
723
|
+
"requireIdentity needs the openid scope: start's scopes leave it out"
|
|
724
|
+
);
|
|
596
725
|
const nonce = randomToken(16);
|
|
597
726
|
const verifier = randomToken(32);
|
|
598
727
|
const state = await signState({
|
|
@@ -696,8 +825,10 @@ ${refreshToken}`);
|
|
|
696
825
|
return back(request, state, "failed", {}, "no access token");
|
|
697
826
|
}
|
|
698
827
|
const { scope, scopes } = granted(body, state.scopes);
|
|
699
|
-
if (!state.scopes.every((s) => scopes.includes(normalScope(s))))
|
|
828
|
+
if (!state.scopes.every((s) => scopes.includes(normalScope(s)))) {
|
|
829
|
+
abandonGrant(refreshToken, accessToken);
|
|
700
830
|
return back(request, state, "scope");
|
|
831
|
+
}
|
|
701
832
|
if ((config.requireRefreshToken ?? true) && (typeof refreshToken !== "string" || !refreshToken)) {
|
|
702
833
|
return back(
|
|
703
834
|
request,
|
|
@@ -707,6 +838,20 @@ ${refreshToken}`);
|
|
|
707
838
|
"no refresh token (is offline access asked for?)"
|
|
708
839
|
);
|
|
709
840
|
}
|
|
841
|
+
const idToken = typeof body["id_token"] === "string" ? body["id_token"] : void 0;
|
|
842
|
+
const wantsIdentity = config.requireIdentity === true || asksOpenid(state.scopes);
|
|
843
|
+
const read = !wantsIdentity ? void 0 : idToken ? identityFromTokenEndpoint(idToken, {
|
|
844
|
+
issuers,
|
|
845
|
+
clientId: config.clientId,
|
|
846
|
+
trustedAudiences,
|
|
847
|
+
now: now()
|
|
848
|
+
}) : { reason: "no ID token" };
|
|
849
|
+
const identity = read && "subject" in read ? read : void 0;
|
|
850
|
+
const identityNote = read && "reason" in read ? `no identity: ${read.reason}` : void 0;
|
|
851
|
+
if (!identity && config.requireIdentity) {
|
|
852
|
+
abandonGrant(refreshToken, accessToken);
|
|
853
|
+
return back(request, state, "failed", {}, identityNote);
|
|
854
|
+
}
|
|
710
855
|
let extra;
|
|
711
856
|
try {
|
|
712
857
|
const id = config.randomId?.() ?? crypto.randomUUID();
|
|
@@ -721,7 +866,8 @@ ${refreshToken}`);
|
|
|
721
866
|
expiresIn: expiresIn(body),
|
|
722
867
|
scope,
|
|
723
868
|
scopes,
|
|
724
|
-
...
|
|
869
|
+
...idToken ? { idToken } : {},
|
|
870
|
+
...identity ? { identity } : {},
|
|
725
871
|
subject: state.subject,
|
|
726
872
|
intent: state.intent,
|
|
727
873
|
returnTo: state.returnTo,
|
|
@@ -732,7 +878,7 @@ ${refreshToken}`);
|
|
|
732
878
|
} catch {
|
|
733
879
|
return back(request, state, "failed", {}, "store threw");
|
|
734
880
|
}
|
|
735
|
-
return back(request, state, "connected", extra ?? {});
|
|
881
|
+
return back(request, state, "connected", extra ?? {}, identityNote);
|
|
736
882
|
},
|
|
737
883
|
async refresh(sealedRefreshToken, aad, options = {}) {
|
|
738
884
|
const refreshToken = await config.secrets.open(sealedRefreshToken, aad);
|
|
@@ -761,39 +907,7 @@ ${refreshToken}`);
|
|
|
761
907
|
async revoke(sealedRefreshToken, aad, options = {}) {
|
|
762
908
|
const refreshToken = await config.secrets.open(sealedRefreshToken, aad);
|
|
763
909
|
tokenCache.delete(await cacheKey(refreshToken));
|
|
764
|
-
|
|
765
|
-
const body = new URLSearchParams({
|
|
766
|
-
token: refreshToken,
|
|
767
|
-
token_type_hint: "refresh_token"
|
|
768
|
-
});
|
|
769
|
-
const headers = {
|
|
770
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
771
|
-
accept: "application/json"
|
|
772
|
-
};
|
|
773
|
-
if (provider.revokeWithClientAuth) clientAuth(body, headers);
|
|
774
|
-
try {
|
|
775
|
-
const response = await timedFetch(
|
|
776
|
-
config,
|
|
777
|
-
provider.revokeUrl,
|
|
778
|
-
{ method: "POST", headers, body: body.toString() },
|
|
779
|
-
options.signal
|
|
780
|
-
);
|
|
781
|
-
let gone = response.ok;
|
|
782
|
-
if (!gone && response.status === 400) {
|
|
783
|
-
gone = errorCode(await jsonBody(response)) === "invalid_token";
|
|
784
|
-
} else {
|
|
785
|
-
discard(response);
|
|
786
|
-
}
|
|
787
|
-
log({
|
|
788
|
-
step: "revoke",
|
|
789
|
-
result: gone ? "revoked" : "failed",
|
|
790
|
-
...gone ? {} : { detail: `revoke endpoint answered ${response.status}` }
|
|
791
|
-
});
|
|
792
|
-
return gone;
|
|
793
|
-
} catch {
|
|
794
|
-
log({ step: "revoke", result: "unavailable" });
|
|
795
|
-
return false;
|
|
796
|
-
}
|
|
910
|
+
return revokeToken(refreshToken, "refresh_token", options.signal);
|
|
797
911
|
}
|
|
798
912
|
};
|
|
799
913
|
}
|