@nekm/sveltekit-armor 0.2.2 → 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/dist/browser/index.d.ts +8 -0
- package/dist/contracts.d.ts +5 -2
- package/dist/errors.d.ts +2 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.esm.js +164 -24
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +161 -23
- package/dist/index.js.map +1 -1
- package/dist/routes/refresh.d.ts +3 -0
- package/dist/routes/routes.d.ts +2 -1
- package/dist/utils/jwt.d.ts +1 -1
- package/dist/utils/refresh.d.ts +3 -0
- package/dist/utils/utils.d.ts +3 -1
- package/package.json +25 -14
- package/src/browser/index.ts +32 -0
- package/src/contracts.ts +7 -2
- package/src/errors.ts +1 -0
- package/src/index.ts +25 -11
- package/src/routes/login.ts +3 -1
- package/src/routes/logout.ts +1 -0
- package/src/routes/redirect-login.ts +6 -2
- package/src/routes/redirect-logout.ts +1 -0
- package/src/routes/refresh.ts +39 -0
- package/src/routes/routes.ts +6 -2
- package/src/session/cookie.ts +5 -3
- package/src/utils/jwt.ts +37 -5
- package/src/utils/refresh.ts +87 -0
- package/src/utils/utils.ts +13 -1
package/src/contracts.ts
CHANGED
|
@@ -26,7 +26,8 @@ export interface ArmorAccessToken extends JWTPayload {
|
|
|
26
26
|
export interface ArmorTokens {
|
|
27
27
|
readonly exchange: ArmorTokenExchange;
|
|
28
28
|
readonly idToken: ArmorIdToken;
|
|
29
|
-
readonly accessToken: ArmorAccessToken;
|
|
29
|
+
readonly accessToken: ArmorAccessToken | string;
|
|
30
|
+
readonly expiresAt: Date;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
interface OauthBaseUrl {
|
|
@@ -36,6 +37,7 @@ interface OauthBaseUrl {
|
|
|
36
37
|
readonly authorizeEndpoint?: never;
|
|
37
38
|
readonly logoutEndpoint?: never;
|
|
38
39
|
readonly tokenEndpoint?: never;
|
|
40
|
+
readonly refreshEndpoint?: never;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
interface OauthEndpoints {
|
|
@@ -45,18 +47,21 @@ interface OauthEndpoints {
|
|
|
45
47
|
readonly authorizeEndpoint: string;
|
|
46
48
|
readonly logoutEndpoint?: string;
|
|
47
49
|
readonly tokenEndpoint: string;
|
|
50
|
+
readonly refreshEndpoint: string;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
type OauthEndpointsOrBaseUrl = OauthBaseUrl | OauthEndpoints;
|
|
51
54
|
|
|
52
55
|
export interface ArmorConfig {
|
|
53
56
|
readonly session: {
|
|
54
|
-
readonly exists: (event: RequestEvent) => Promise<boolean> | boolean;
|
|
55
57
|
readonly login: (
|
|
56
58
|
event: RequestEvent,
|
|
57
59
|
tokens: ArmorTokens,
|
|
58
60
|
) => Promise<void> | void;
|
|
59
61
|
readonly logout: (event: RequestEvent) => Promise<void> | void;
|
|
62
|
+
readonly getTokens: (
|
|
63
|
+
event: RequestEvent,
|
|
64
|
+
) => Promise<ArmorTokens | undefined> | ArmorTokens | undefined;
|
|
60
65
|
};
|
|
61
66
|
readonly oauth: OauthEndpointsOrBaseUrl & {
|
|
62
67
|
readonly clientId: string;
|
package/src/errors.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,32 +1,45 @@
|
|
|
1
1
|
import { redirect, type Handle } from "@sveltejs/kit";
|
|
2
2
|
import { ROUTE_PATH_LOGIN } from "./routes/login";
|
|
3
3
|
import type { ArmorConfig, ArmorOpenIdConfig, ArmorTokens } from "./contracts";
|
|
4
|
-
import { ROUTE_PATH_LOGOUT } from "./routes/logout";
|
|
5
4
|
import { routeCreate } from "./routes/routes";
|
|
6
|
-
import { ArmorOpenIdConfigError } from "./errors";
|
|
5
|
+
import { ArmorOpenIdConfigError, ArmorRefreshError } from "./errors";
|
|
6
|
+
import { shouldRefresh } from "./utils/utils";
|
|
7
|
+
import { createRefresh } from "./utils/refresh";
|
|
7
8
|
|
|
8
9
|
export type { ArmorConfig, ArmorTokens };
|
|
9
10
|
export { armorCookieSession, armorCookieSessionGet } from "./session/cookie";
|
|
10
11
|
|
|
11
|
-
export const ARMOR_LOGIN = ROUTE_PATH_LOGIN;
|
|
12
|
-
export const ARMOR_LOGOUT = ROUTE_PATH_LOGOUT;
|
|
13
|
-
|
|
14
12
|
export function armor(config: ArmorConfig): Handle {
|
|
15
|
-
const
|
|
13
|
+
const routeByPath = routeCreate(config);
|
|
14
|
+
const refresh = createRefresh(config);
|
|
16
15
|
|
|
17
16
|
return async ({ event, resolve }) => {
|
|
18
|
-
const
|
|
17
|
+
const route = routeByPath.get(event.url.pathname);
|
|
19
18
|
|
|
20
|
-
if (
|
|
21
|
-
return
|
|
19
|
+
if (route && route.method === event.request.method) {
|
|
20
|
+
return route.handle({ event, resolve });
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
const
|
|
23
|
+
const tokens = await config.session.getTokens(event);
|
|
25
24
|
|
|
26
|
-
if (!
|
|
25
|
+
if (!tokens) {
|
|
27
26
|
throw redirect(302, ROUTE_PATH_LOGIN);
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
try {
|
|
30
|
+
if (shouldRefresh(tokens)) {
|
|
31
|
+
console.log("Refreshing token...");
|
|
32
|
+
await refresh(event, tokens);
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof ArmorRefreshError) {
|
|
36
|
+
console.error("Could not refresh token. Redirect user to login...");
|
|
37
|
+
throw redirect(302, ROUTE_PATH_LOGIN);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
return resolve(event);
|
|
31
44
|
};
|
|
32
45
|
}
|
|
@@ -65,6 +78,7 @@ export async function armorConfigFromOpenId(
|
|
|
65
78
|
issuer: body.issuer,
|
|
66
79
|
jwksEndpoint: body.jwks_uri,
|
|
67
80
|
logoutEndpoint: body.end_session_endpoint ?? undefined,
|
|
81
|
+
refreshEndpoint: body.token_endpoint,
|
|
68
82
|
},
|
|
69
83
|
};
|
|
70
84
|
}
|
package/src/routes/login.ts
CHANGED
|
@@ -6,8 +6,9 @@ import { randomUUID } from "node:crypto";
|
|
|
6
6
|
import type { RouteFactory } from "./routes";
|
|
7
7
|
import { COOKIE_STATE, cookieSet } from "../utils/cookie";
|
|
8
8
|
import { urlConcat } from "../utils/utils";
|
|
9
|
+
import { ARMOR_LOGIN } from "../browser";
|
|
9
10
|
|
|
10
|
-
export const ROUTE_PATH_LOGIN =
|
|
11
|
+
export const ROUTE_PATH_LOGIN = ARMOR_LOGIN;
|
|
11
12
|
|
|
12
13
|
export const routeLoginFactory: RouteFactory = (config: ArmorConfig) => {
|
|
13
14
|
const authorizeEndpoint =
|
|
@@ -18,6 +19,7 @@ export const routeLoginFactory: RouteFactory = (config: ArmorConfig) => {
|
|
|
18
19
|
|
|
19
20
|
return {
|
|
20
21
|
path: ROUTE_PATH_LOGIN,
|
|
22
|
+
method: "GET",
|
|
21
23
|
async handle({ event }) {
|
|
22
24
|
const state = randomUUID();
|
|
23
25
|
cookieSet(event.cookies, COOKIE_STATE, state);
|
package/src/routes/logout.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
import { queryParamsCreate, throwIfUndefined } from "@nekm/core";
|
|
8
8
|
import { createRemoteJWKSet } from "jose";
|
|
9
9
|
import type { RouteFactory } from "./routes";
|
|
10
|
-
import { urlConcat, isTokenExchange } from "../utils/utils";
|
|
10
|
+
import { urlConcat, isTokenExchange, createExpiresAt } from "../utils/utils";
|
|
11
11
|
import { jwtVerifyAccessToken, jwtVerifyIdToken } from "../utils/jwt";
|
|
12
12
|
import { eventStateValidOrThrow } from "../utils/event";
|
|
13
13
|
|
|
@@ -70,6 +70,7 @@ export const routeRedirectLoginFactory: RouteFactory = (
|
|
|
70
70
|
|
|
71
71
|
return {
|
|
72
72
|
path: ROUTE_PATH_REDIRECT_LOGIN,
|
|
73
|
+
method: "GET",
|
|
73
74
|
async handle({ event }) {
|
|
74
75
|
eventStateValidOrThrow(event);
|
|
75
76
|
|
|
@@ -113,7 +114,10 @@ export const routeRedirectLoginFactory: RouteFactory = (
|
|
|
113
114
|
await config.session.login(event, {
|
|
114
115
|
exchange,
|
|
115
116
|
idToken: idToken as ArmorIdToken,
|
|
116
|
-
|
|
117
|
+
// Generally, IdP's require an audience to get a JWT
|
|
118
|
+
// access token. Most cases, this doesn't matter.
|
|
119
|
+
accessToken: accessToken ?? exchange.access_token,
|
|
120
|
+
expiresAt: createExpiresAt(exchange.expires_in),
|
|
117
121
|
});
|
|
118
122
|
|
|
119
123
|
throw redirect(302, "/");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { error, json } from "@sveltejs/kit";
|
|
2
|
+
import type { ArmorConfig } from "../contracts";
|
|
3
|
+
import type { RouteFactory } from "./routes";
|
|
4
|
+
import { createRefresh } from "../utils/refresh";
|
|
5
|
+
import { ARMOR_REFRESH } from "../browser";
|
|
6
|
+
import { ArmorRefreshError } from "../errors";
|
|
7
|
+
|
|
8
|
+
export const ROUTE_PATH_REFRESH = ARMOR_REFRESH;
|
|
9
|
+
|
|
10
|
+
export const routeRefreshFactory: RouteFactory = (config: ArmorConfig) => {
|
|
11
|
+
const refresh = createRefresh(config);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
path: ROUTE_PATH_REFRESH,
|
|
15
|
+
method: "POST",
|
|
16
|
+
async handle({ event }) {
|
|
17
|
+
try {
|
|
18
|
+
const tokens = await config.session.getTokens(event);
|
|
19
|
+
|
|
20
|
+
if (!tokens) {
|
|
21
|
+
return error(401, "Unauthorized");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { idToken, expiresAt, accessToken } = await refresh(
|
|
25
|
+
event,
|
|
26
|
+
tokens,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return json({ idToken, expiresAt, accessToken });
|
|
30
|
+
} catch (ex) {
|
|
31
|
+
if (ex instanceof ArmorRefreshError) {
|
|
32
|
+
return error(401, "Unauthorized");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw ex;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
};
|
package/src/routes/routes.ts
CHANGED
|
@@ -4,10 +4,12 @@ import { routeLoginFactory } from "./login";
|
|
|
4
4
|
import { routeLogoutFactory } from "./logout";
|
|
5
5
|
import { routeRedirectLogoutFactory } from "./redirect-logout";
|
|
6
6
|
import { routeRedirectLoginFactory } from "./redirect-login";
|
|
7
|
+
import { routeRefreshFactory } from "./refresh";
|
|
7
8
|
|
|
8
9
|
export interface Route {
|
|
9
10
|
readonly path: string;
|
|
10
11
|
readonly handle: Handle;
|
|
12
|
+
readonly method: "GET" | "POST";
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export type RouteFactory = (config: ArmorConfig) => Route | undefined;
|
|
@@ -17,14 +19,16 @@ const routeFactories = Object.freeze([
|
|
|
17
19
|
routeLogoutFactory,
|
|
18
20
|
routeRedirectLoginFactory,
|
|
19
21
|
routeRedirectLogoutFactory,
|
|
22
|
+
routeRefreshFactory,
|
|
20
23
|
]);
|
|
21
24
|
|
|
22
|
-
export function routeCreate(config: ArmorConfig): Map<string,
|
|
25
|
+
export function routeCreate(config: ArmorConfig): Map<string, Route> {
|
|
26
|
+
// @ts-expect-error Incorrect typing error.
|
|
23
27
|
return new Map(
|
|
24
28
|
routeFactories
|
|
25
29
|
.map((routeFactory) => routeFactory(config))
|
|
26
30
|
.filter((route) => Boolean(route))
|
|
27
31
|
// @ts-expect-error Incorrect typing error.
|
|
28
|
-
.map((route) => [route.path, route
|
|
32
|
+
.map((route) => [route.path, route]),
|
|
29
33
|
);
|
|
30
34
|
}
|
package/src/session/cookie.ts
CHANGED
|
@@ -8,8 +8,10 @@ import {
|
|
|
8
8
|
import { ArmorConfig, ArmorTokens } from "../contracts";
|
|
9
9
|
import { ArmorAuthMissingError } from "../errors";
|
|
10
10
|
|
|
11
|
-
function
|
|
12
|
-
|
|
11
|
+
function cookieSessionGetTokens({
|
|
12
|
+
cookies,
|
|
13
|
+
}: RequestEvent): ArmorTokens | undefined {
|
|
14
|
+
return cookies.get(COOKIE_TOKENS) as ArmorTokens | undefined;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export function cookieSessionLogin(
|
|
@@ -34,7 +36,7 @@ export function armorCookieSessionGet({ cookies }: RequestEvent): ArmorTokens {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export const armorCookieSession: ArmorConfig["session"] = {
|
|
37
|
-
|
|
39
|
+
getTokens: cookieSessionGetTokens,
|
|
38
40
|
login: cookieSessionLogin,
|
|
39
41
|
logout: cookieSessionLogout,
|
|
40
42
|
};
|
package/src/utils/jwt.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { ArmorConfig } from "../contracts";
|
|
2
2
|
import { JWTPayload, jwtVerify, JWTVerifyGetKey, JWTVerifyOptions } from "jose";
|
|
3
|
+
import { throwIfUndefined } from "@nekm/core";
|
|
4
|
+
|
|
5
|
+
function jwtIsCompactJwt(token: string): boolean {
|
|
6
|
+
// Must be three base64url segments
|
|
7
|
+
const parts = token.trim().split(".");
|
|
8
|
+
return parts.length === 3 && parts.every((p) => p.length > 0);
|
|
9
|
+
}
|
|
3
10
|
|
|
4
11
|
export function jwtVerifyIdToken(
|
|
5
12
|
config: ArmorConfig,
|
|
6
13
|
jwks: JWTVerifyGetKey,
|
|
7
14
|
idToken: string,
|
|
8
15
|
): Promise<JWTPayload> {
|
|
9
|
-
|
|
16
|
+
const payload = jwtVerifyToken(
|
|
10
17
|
jwks,
|
|
11
18
|
{
|
|
12
19
|
issuer: config.oauth.issuer,
|
|
@@ -14,13 +21,16 @@ export function jwtVerifyIdToken(
|
|
|
14
21
|
},
|
|
15
22
|
idToken,
|
|
16
23
|
);
|
|
24
|
+
throwIfUndefined(payload);
|
|
25
|
+
// @ts-expect-error We're already verifying non-null above.
|
|
26
|
+
return payload;
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
export function jwtVerifyAccessToken(
|
|
20
30
|
config: ArmorConfig,
|
|
21
31
|
jwks: JWTVerifyGetKey,
|
|
22
32
|
accessToken: string,
|
|
23
|
-
): Promise<JWTPayload> {
|
|
33
|
+
): Promise<JWTPayload | undefined> {
|
|
24
34
|
const opts: JWTVerifyOptions = { issuer: config.oauth.issuer };
|
|
25
35
|
|
|
26
36
|
if (config.oauth.audience) {
|
|
@@ -30,11 +40,33 @@ export function jwtVerifyAccessToken(
|
|
|
30
40
|
return jwtVerifyToken(jwks, opts, accessToken);
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
function isInvalidCompactJwt(error: unknown): boolean {
|
|
44
|
+
return Boolean(
|
|
45
|
+
typeof error === "object" &&
|
|
46
|
+
error &&
|
|
47
|
+
"message" in error &&
|
|
48
|
+
typeof error.message === "string" &&
|
|
49
|
+
/invalid compact jws/gi.test(error.message),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
async function jwtVerifyToken(
|
|
34
54
|
jwks: JWTVerifyGetKey,
|
|
35
55
|
opts: JWTVerifyOptions,
|
|
36
56
|
token: string,
|
|
37
|
-
): Promise<JWTPayload> {
|
|
38
|
-
|
|
39
|
-
|
|
57
|
+
): Promise<JWTPayload | undefined> {
|
|
58
|
+
try {
|
|
59
|
+
if (!jwtIsCompactJwt(token)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { payload } = await jwtVerify(token, jwks, opts);
|
|
64
|
+
return payload;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (isInvalidCompactJwt(error)) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
40
72
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createRemoteJWKSet } from "jose";
|
|
2
|
+
import {
|
|
3
|
+
ArmorConfig,
|
|
4
|
+
ArmorIdToken,
|
|
5
|
+
ArmorTokenExchange,
|
|
6
|
+
ArmorTokens,
|
|
7
|
+
} from "../contracts";
|
|
8
|
+
import { ArmorRefreshError } from "../errors";
|
|
9
|
+
import { createExpiresAt, urlConcat } from "./utils";
|
|
10
|
+
import { jwtVerifyAccessToken, jwtVerifyIdToken } from "./jwt";
|
|
11
|
+
import { RequestEvent } from "@sveltejs/kit";
|
|
12
|
+
|
|
13
|
+
export function createRefresh(config: ArmorConfig) {
|
|
14
|
+
const refreshEndpoint =
|
|
15
|
+
config.oauth.refreshEndpoint ??
|
|
16
|
+
urlConcat(config.oauth.baseUrl, "oauth2/token");
|
|
17
|
+
|
|
18
|
+
const jwksUrl = new URL(
|
|
19
|
+
config.oauth.jwksEndpoint ??
|
|
20
|
+
urlConcat(config.oauth.baseUrl, ".well-known/jwks.json"),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const refresh = async (
|
|
24
|
+
fetch: typeof global.fetch,
|
|
25
|
+
refreshToken: string,
|
|
26
|
+
): Promise<ArmorTokenExchange> => {
|
|
27
|
+
const body = new URLSearchParams({
|
|
28
|
+
grant_type: "refresh_token",
|
|
29
|
+
client_id: config.oauth.clientId,
|
|
30
|
+
client_secret: config.oauth.clientSecret,
|
|
31
|
+
refresh_token: refreshToken,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (config.oauth.scope) {
|
|
35
|
+
body.set("scope", config.oauth.scope);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const response = await fetch(refreshEndpoint, {
|
|
39
|
+
headers: {
|
|
40
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
41
|
+
Accept: "application/json",
|
|
42
|
+
},
|
|
43
|
+
body: body.toString(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const error = await response.text();
|
|
48
|
+
throw new ArmorRefreshError(`Could not refresh token: ${error}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const json: ArmorTokenExchange = await response.json();
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...json,
|
|
55
|
+
refresh_token: json.refresh_token ?? refreshToken,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return async (
|
|
60
|
+
event: RequestEvent,
|
|
61
|
+
tokens: ArmorTokens,
|
|
62
|
+
): Promise<ArmorTokens> => {
|
|
63
|
+
const refreshToken = tokens.exchange?.refresh_token;
|
|
64
|
+
|
|
65
|
+
if (!refreshToken) {
|
|
66
|
+
throw new ArmorRefreshError("Could not find refresh token");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const newExchange = await refresh(event.fetch, refreshToken);
|
|
70
|
+
|
|
71
|
+
const jwks = createRemoteJWKSet(jwksUrl);
|
|
72
|
+
|
|
73
|
+
const [idToken, accessToken] = await Promise.all([
|
|
74
|
+
jwtVerifyIdToken(config, jwks, newExchange.id_token),
|
|
75
|
+
jwtVerifyAccessToken(config, jwks, newExchange.access_token),
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
exchange: newExchange,
|
|
80
|
+
idToken: idToken as ArmorIdToken,
|
|
81
|
+
// Generally, IdP's require an audience to get a JWT
|
|
82
|
+
// access token. Most cases, this doesn't matter.
|
|
83
|
+
accessToken: accessToken ?? newExchange.access_token,
|
|
84
|
+
expiresAt: createExpiresAt(newExchange.expires_in),
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/utils/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { strTrimEnd, strTrimStart } from "@nekm/core";
|
|
2
|
-
import type { ArmorTokenExchange } from "../contracts";
|
|
2
|
+
import type { ArmorTokenExchange, ArmorTokens } from "../contracts";
|
|
3
3
|
|
|
4
4
|
export function urlConcat(origin: string, path: string): string {
|
|
5
5
|
return [strTrimEnd(origin, "/"), strTrimStart(path, "/")].join("/");
|
|
@@ -21,3 +21,15 @@ export function isTokenExchange(value: unknown): value is ArmorTokenExchange {
|
|
|
21
21
|
(typeof obj.scope === "string" || obj.scope === undefined)
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
const MINUTES_MS = 60 * 1000;
|
|
26
|
+
|
|
27
|
+
export function shouldRefresh(tokens: ArmorTokens) {
|
|
28
|
+
return tokens.expiresAt.getTime() < Date.now() + 5 * MINUTES_MS;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createExpiresAt(seconds: number): Date {
|
|
32
|
+
const now = new Date();
|
|
33
|
+
now.setSeconds(now.getSeconds() + seconds);
|
|
34
|
+
return now;
|
|
35
|
+
}
|