matrix-nodejs-homeserver 0.3.4 → 0.3.6
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/AUDITBOT.md +67 -0
- package/README.md +55 -9
- package/dist/api/account.js +109 -64
- package/dist/api/admin.js +14 -0
- package/dist/api/federation.js +1 -1
- package/dist/api/index.js +2 -0
- package/dist/api/misc.js +3 -3
- package/dist/api/oauthserver.js +620 -0
- package/dist/api/sync.js +1 -1
- package/dist/api/threepid.js +1 -1
- package/dist/api/uia.js +20 -8
- package/dist/api/user.js +6 -3
- package/dist/auditbot/bot.d.ts +37 -0
- package/dist/auditbot/bot.js +208 -0
- package/dist/auditbot/client.d.ts +41 -0
- package/dist/auditbot/client.js +156 -0
- package/dist/auditbot/index.d.ts +4 -0
- package/dist/auditbot/index.js +4 -0
- package/dist/auditbot/sinks.d.ts +37 -0
- package/dist/auditbot/sinks.js +33 -0
- package/dist/core/admin.d.ts +6 -0
- package/dist/core/admin.js +25 -0
- package/dist/core/auth.d.ts +33 -2
- package/dist/core/auth.js +84 -21
- package/dist/core/oauth.js +2 -1
- package/dist/core/oauthserver.d.ts +141 -0
- package/dist/core/oauthserver.js +603 -0
- package/dist/core/ratelimit.d.ts +1 -1
- package/dist/core/ratelimit.js +4 -0
- package/dist/core/sso.d.ts +51 -15
- package/dist/core/sso.js +86 -26
- package/dist/homeserver.d.ts +20 -9
- package/dist/homeserver.js +17 -2
- package/dist/index.d.ts +15 -14
- package/dist/index.js +2 -1
- package/dist/storage/schema.js +20 -2
- package/dist/vendor/zod/v4/classic/errors.js +1 -1
- package/dist/vendor/zod/v4/classic/parse.js +1 -1
- package/dist/vendor/zod/v4/classic/schemas.js +1 -1
- package/dist/vendor/zod/v4/core/api.js +1 -1
- package/dist/vendor/zod/v4/core/checks.js +1 -1
- package/dist/vendor/zod/v4/core/core.js +1 -1
- package/dist/vendor/zod/v4/core/doc.js +1 -1
- package/dist/vendor/zod/v4/core/errors.js +1 -1
- package/dist/vendor/zod/v4/core/json-schema-processors.js +1 -1
- package/dist/vendor/zod/v4/core/memoizer.js +1 -1
- package/dist/vendor/zod/v4/core/parse.js +1 -1
- package/dist/vendor/zod/v4/core/regexes.js +1 -1
- package/dist/vendor/zod/v4/core/registries.js +1 -1
- package/dist/vendor/zod/v4/core/schemas.js +1 -1
- package/dist/vendor/zod/v4/core/to-json-schema.js +1 -1
- package/dist/vendor/zod/v4/core/util.js +1 -1
- package/dist/vendor/zod/v4/core/versions.js +1 -1
- package/dist/vendor/zod/v4/locales/en.js +1 -1
- package/package.json +28 -17
package/dist/core/auth.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface Session {
|
|
|
13
13
|
suspended?: boolean;
|
|
14
14
|
/** Set when the request was made with an application service token. */
|
|
15
15
|
appservice?: AppServiceRegistration;
|
|
16
|
+
/**
|
|
17
|
+
* The token comes from the OAuth 2.0 API (this server's or a delegated one), so endpoints that
|
|
18
|
+
* need User-Interactive Authentication send the user to account management instead.
|
|
19
|
+
*/
|
|
20
|
+
oauth?: boolean;
|
|
16
21
|
}
|
|
17
22
|
export interface CreatedSession {
|
|
18
23
|
user_id: string;
|
|
@@ -20,6 +25,8 @@ export interface CreatedSession {
|
|
|
20
25
|
access_token: string;
|
|
21
26
|
refresh_token?: string;
|
|
22
27
|
expires_in_ms?: number;
|
|
28
|
+
/** OAuth 2.0 API: the scope the tokens were issued for. */
|
|
29
|
+
scope?: string;
|
|
23
30
|
}
|
|
24
31
|
export declare class AuthService {
|
|
25
32
|
private hs;
|
|
@@ -31,9 +38,33 @@ export declare class AuthService {
|
|
|
31
38
|
deviceId?: string;
|
|
32
39
|
displayName?: string;
|
|
33
40
|
refreshToken?: boolean;
|
|
41
|
+
/** OAuth 2.0 API: the client the tokens are issued to. */
|
|
42
|
+
clientId?: string;
|
|
43
|
+
/** Access token lifetime; default `accessTokenLifetimeMs` (for tokens with a refresh token). */
|
|
44
|
+
lifetimeMs?: number;
|
|
45
|
+
/** OAuth 2.0 API: the scope granted. */
|
|
46
|
+
scope?: string;
|
|
47
|
+
}): Promise<CreatedSession>;
|
|
48
|
+
issueToken(userId: string, deviceId: string, withRefresh: boolean, opts?: {
|
|
49
|
+
clientId?: string | null;
|
|
50
|
+
lifetimeMs?: number;
|
|
51
|
+
parent?: string;
|
|
52
|
+
scope?: string | null;
|
|
53
|
+
}): Promise<CreatedSession>;
|
|
54
|
+
/**
|
|
55
|
+
* Exchange a refresh token for new tokens.
|
|
56
|
+
*
|
|
57
|
+
* https://github.com/k18k/matrix-nodejs-homeserver/issues/66: the old row used to be deleted
|
|
58
|
+
* at once, so a client whose response was lost could not retry and was logged out, and a
|
|
59
|
+
* refresh token used twice went unnoticed. As the OAuth 2.0 API asks (and the legacy API
|
|
60
|
+
* allows): the old refresh token keeps working until the new access token is first used; a
|
|
61
|
+
* retry before that replaces the unused tokens. Once the new token was used, the old refresh
|
|
62
|
+
* token is spent, and presenting it again means it leaked, so the device is logged out.
|
|
63
|
+
*/
|
|
64
|
+
refresh(refreshToken: string, opts?: {
|
|
65
|
+
clientId?: string | null;
|
|
66
|
+
lifetimeMs?: number;
|
|
34
67
|
}): Promise<CreatedSession>;
|
|
35
|
-
issueToken(userId: string, deviceId: string, withRefresh: boolean): Promise<CreatedSession>;
|
|
36
|
-
refresh(refreshToken: string): Promise<CreatedSession>;
|
|
37
68
|
/** Drop cached sessions of a user (after admin/deactivation changes). */
|
|
38
69
|
invalidateUser(userId: string): Promise<void>;
|
|
39
70
|
/** Issue a short-lived single-use `m.login.token` for a user (SSO, or host-app sign-in). */
|
package/dist/core/auth.js
CHANGED
|
@@ -11,13 +11,13 @@ var AuthService = class {
|
|
|
11
11
|
if (msg?.from === hs.instanceId) return;
|
|
12
12
|
if (msg.token) this.cache.delete(msg.token);
|
|
13
13
|
if (msg.userId) {
|
|
14
|
-
for (const [t,
|
|
14
|
+
for (const [t, e] of this.cache) if (e.session.userId === msg.userId) this.cache.delete(t);
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
async authenticate(token, ip) {
|
|
19
19
|
const cached = this.cache.get(token);
|
|
20
|
-
if (cached) return cached;
|
|
20
|
+
if (cached && cached.expires > Date.now()) return cached.session;
|
|
21
21
|
const row = await this.hs.db.findOne("access_tokens", [{
|
|
22
22
|
field: "token",
|
|
23
23
|
value: token
|
|
@@ -39,9 +39,24 @@ var AuthService = class {
|
|
|
39
39
|
isGuest: !!user.is_guest,
|
|
40
40
|
isAdmin: !!user.is_admin,
|
|
41
41
|
locked: !!user.locked,
|
|
42
|
-
suspended: !!user.suspended
|
|
42
|
+
suspended: !!user.suspended,
|
|
43
|
+
...row.client_id ? { oauth: true } : {}
|
|
43
44
|
};
|
|
44
|
-
if (row.
|
|
45
|
+
if (row.refresh_parent) {
|
|
46
|
+
await this.hs.db.update("access_tokens", [{
|
|
47
|
+
field: "token",
|
|
48
|
+
value: row.refresh_parent
|
|
49
|
+
}], { refresh_used: true });
|
|
50
|
+
await this.hs.db.update("access_tokens", [{
|
|
51
|
+
field: "token",
|
|
52
|
+
value: token
|
|
53
|
+
}], { refresh_parent: null });
|
|
54
|
+
}
|
|
55
|
+
if (this.cache.size > 1e5) this.cache.clear();
|
|
56
|
+
this.cache.set(token, {
|
|
57
|
+
session,
|
|
58
|
+
expires: row.expires_ts ?? Infinity
|
|
59
|
+
});
|
|
45
60
|
await this.hs.db.update("devices", [{
|
|
46
61
|
field: "user_id",
|
|
47
62
|
value: row.user_id
|
|
@@ -91,46 +106,94 @@ var AuthService = class {
|
|
|
91
106
|
target: deviceId
|
|
92
107
|
});
|
|
93
108
|
}
|
|
94
|
-
return this.issueToken(userId, deviceId, !!opts.refreshToken
|
|
109
|
+
return this.issueToken(userId, deviceId, !!opts.refreshToken, {
|
|
110
|
+
clientId: opts.clientId,
|
|
111
|
+
lifetimeMs: opts.lifetimeMs,
|
|
112
|
+
scope: opts.scope
|
|
113
|
+
});
|
|
95
114
|
}
|
|
96
|
-
async issueToken(userId, deviceId, withRefresh) {
|
|
115
|
+
async issueToken(userId, deviceId, withRefresh, opts = {}) {
|
|
97
116
|
const token = `mat_${randomToken()}`;
|
|
98
|
-
const lifetime = this.hs.config.accessTokenLifetimeMs;
|
|
117
|
+
const lifetime = opts.lifetimeMs ?? this.hs.config.accessTokenLifetimeMs;
|
|
99
118
|
const refresh = withRefresh ? `mar_${randomToken()}` : null;
|
|
100
119
|
await this.hs.db.create("access_tokens", {
|
|
101
120
|
token,
|
|
102
121
|
user_id: userId,
|
|
103
122
|
device_id: deviceId,
|
|
104
123
|
refresh_token: refresh,
|
|
105
|
-
expires_ts: refresh && lifetime ? Date.now() + lifetime : null
|
|
124
|
+
expires_ts: refresh && lifetime ? Date.now() + lifetime : null,
|
|
125
|
+
client_id: opts.clientId ?? null,
|
|
126
|
+
scope: opts.scope ?? null,
|
|
127
|
+
refresh_parent: opts.parent ?? null,
|
|
128
|
+
refresh_used: false
|
|
106
129
|
});
|
|
107
130
|
const out = {
|
|
108
131
|
user_id: userId,
|
|
109
132
|
device_id: deviceId,
|
|
110
133
|
access_token: token
|
|
111
134
|
};
|
|
135
|
+
if (opts.scope) out.scope = opts.scope;
|
|
112
136
|
if (refresh) {
|
|
113
137
|
out.refresh_token = refresh;
|
|
114
138
|
if (lifetime) out.expires_in_ms = lifetime;
|
|
115
139
|
}
|
|
116
140
|
return out;
|
|
117
141
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return this.
|
|
142
|
+
/**
|
|
143
|
+
* Exchange a refresh token for new tokens.
|
|
144
|
+
*
|
|
145
|
+
* https://github.com/k18k/matrix-nodejs-homeserver/issues/66: the old row used to be deleted
|
|
146
|
+
* at once, so a client whose response was lost could not retry and was logged out, and a
|
|
147
|
+
* refresh token used twice went unnoticed. As the OAuth 2.0 API asks (and the legacy API
|
|
148
|
+
* allows): the old refresh token keeps working until the new access token is first used; a
|
|
149
|
+
* retry before that replaces the unused tokens. Once the new token was used, the old refresh
|
|
150
|
+
* token is spent, and presenting it again means it leaked, so the device is logged out.
|
|
151
|
+
*/
|
|
152
|
+
async refresh(refreshToken, opts = {}) {
|
|
153
|
+
return this.hs.cluster.lock(`refresh:${refreshToken}`, async () => {
|
|
154
|
+
const row = await this.hs.db.findOne("access_tokens", [{
|
|
155
|
+
field: "refresh_token",
|
|
156
|
+
value: refreshToken
|
|
157
|
+
}]);
|
|
158
|
+
if (!row) throw errors.unknownToken("Unknown refresh token");
|
|
159
|
+
if (opts.clientId !== void 0 && (row.client_id ?? null) !== opts.clientId) throw errors.unknownToken("Unknown refresh token");
|
|
160
|
+
if (row.refresh_used) {
|
|
161
|
+
this.hs.logger.warn("A spent refresh token was used again; logging out", row.user_id, row.device_id);
|
|
162
|
+
await this.hs.audit.record({
|
|
163
|
+
action: "logout",
|
|
164
|
+
actor: row.user_id,
|
|
165
|
+
target: row.device_id,
|
|
166
|
+
details: { refresh_token_reuse: true }
|
|
167
|
+
});
|
|
168
|
+
await this.deleteDevice(row.user_id, row.device_id);
|
|
169
|
+
throw errors.unknownToken("Refresh token was already used");
|
|
170
|
+
}
|
|
171
|
+
for (const unused of await this.hs.db.findMany("access_tokens", { where: [{
|
|
172
|
+
field: "refresh_parent",
|
|
173
|
+
value: row.token
|
|
174
|
+
}] })) await this.revokeToken(unused.token);
|
|
175
|
+
if (row.expires_ts === null || row.expires_ts > Date.now()) {
|
|
176
|
+
await this.hs.db.update("access_tokens", [{
|
|
177
|
+
field: "token",
|
|
178
|
+
value: row.token
|
|
179
|
+
}], { expires_ts: Date.now() });
|
|
180
|
+
this.cache.delete(row.token);
|
|
181
|
+
await this.hs.cluster.publish("auth", {
|
|
182
|
+
from: this.hs.instanceId,
|
|
183
|
+
token: row.token
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return this.issueToken(row.user_id, row.device_id, true, {
|
|
187
|
+
clientId: row.client_id,
|
|
188
|
+
lifetimeMs: opts.lifetimeMs,
|
|
189
|
+
parent: row.token,
|
|
190
|
+
scope: row.scope
|
|
191
|
+
});
|
|
192
|
+
});
|
|
130
193
|
}
|
|
131
194
|
/** Drop cached sessions of a user (after admin/deactivation changes). */
|
|
132
195
|
async invalidateUser(userId) {
|
|
133
|
-
for (const [token,
|
|
196
|
+
for (const [token, e] of this.cache) if (e.session.userId === userId) this.cache.delete(token);
|
|
134
197
|
await this.hs.cluster.publish("auth", {
|
|
135
198
|
from: this.hs.instanceId,
|
|
136
199
|
userId
|
package/dist/core/oauth.js
CHANGED
|
@@ -89,7 +89,8 @@ var OAuthDelegation = class {
|
|
|
89
89
|
deviceId,
|
|
90
90
|
accessToken: token,
|
|
91
91
|
isGuest: false,
|
|
92
|
-
isAdmin: !!user.is_admin || scopes.includes(this.options.adminScope ?? "urn:synapse:admin:*")
|
|
92
|
+
isAdmin: !!user.is_admin || scopes.includes(this.options.adminScope ?? "urn:synapse:admin:*"),
|
|
93
|
+
oauth: true
|
|
93
94
|
};
|
|
94
95
|
const ttl = this.options.cacheTtlMs ?? 6e4;
|
|
95
96
|
const exp = typeof claims.exp === "number" ? claims.exp * 1e3 : Infinity;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
//#region src/core/oauthserver.d.ts
|
|
3
|
+
export interface OAuthServerOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Keep the legacy login API (`/login`, `/register`) next to the OAuth 2.0 API. Its `m.login.sso`
|
|
6
|
+
* flow then goes through the same web login and is marked `oauth_aware_preferred`. Default: true.
|
|
7
|
+
*/
|
|
8
|
+
legacyApi?: boolean;
|
|
9
|
+
/** Lifetime of access tokens issued through the OAuth 2.0 API, in ms. Default: 5 minutes. */
|
|
10
|
+
accessTokenLifetimeMs?: number;
|
|
11
|
+
/** Shown on the login and registration pages: the server's terms of service and privacy policy. */
|
|
12
|
+
termsUrl?: string;
|
|
13
|
+
policyUrl?: string;
|
|
14
|
+
/** How long a browser stays logged in to the web pages, in ms. Default: 7 days. */
|
|
15
|
+
webSessionLifetimeMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/** An OAuth error response (RFC 6749 section 5.2, RFC 7591 section 3.2.2). */
|
|
18
|
+
export declare class OAuthError extends Error {
|
|
19
|
+
readonly error: string;
|
|
20
|
+
readonly description: string;
|
|
21
|
+
readonly status: number;
|
|
22
|
+
constructor(error: string, description: string, status?: number);
|
|
23
|
+
}
|
|
24
|
+
export interface ClientMetadata {
|
|
25
|
+
client_id: string;
|
|
26
|
+
client_uri: string;
|
|
27
|
+
client_name?: string;
|
|
28
|
+
logo_uri?: string;
|
|
29
|
+
tos_uri?: string;
|
|
30
|
+
policy_uri?: string;
|
|
31
|
+
redirect_uris: string[];
|
|
32
|
+
response_types: string[];
|
|
33
|
+
grant_types: string[];
|
|
34
|
+
token_endpoint_auth_method: "none";
|
|
35
|
+
application_type: "web" | "native";
|
|
36
|
+
}
|
|
37
|
+
/** A validated authorization request, waiting for the user to log in and consent. */
|
|
38
|
+
export interface PendingGrant {
|
|
39
|
+
clientId: string;
|
|
40
|
+
redirectUri: string;
|
|
41
|
+
responseMode: "query" | "fragment";
|
|
42
|
+
state?: string;
|
|
43
|
+
scope: string;
|
|
44
|
+
deviceId: string;
|
|
45
|
+
codeChallenge: string;
|
|
46
|
+
nonce?: string;
|
|
47
|
+
/** The client asked for the registration page (`prompt=create`). */
|
|
48
|
+
create: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface DeviceGrant {
|
|
51
|
+
clientId: string;
|
|
52
|
+
scope: string;
|
|
53
|
+
deviceId: string;
|
|
54
|
+
userCode: string;
|
|
55
|
+
status: "pending" | "approved" | "denied";
|
|
56
|
+
userId?: string;
|
|
57
|
+
lastPoll: number;
|
|
58
|
+
expires: number;
|
|
59
|
+
}
|
|
60
|
+
export interface TokenResponse {
|
|
61
|
+
access_token: string;
|
|
62
|
+
token_type: "Bearer";
|
|
63
|
+
expires_in: number;
|
|
64
|
+
refresh_token: string;
|
|
65
|
+
scope: string;
|
|
66
|
+
id_token?: string;
|
|
67
|
+
}
|
|
68
|
+
export declare class OAuthServer {
|
|
69
|
+
private hs;
|
|
70
|
+
readonly options: OAuthServerOptions;
|
|
71
|
+
/** The issuer identifier: the public base URL with a trailing slash. */
|
|
72
|
+
readonly issuer: string;
|
|
73
|
+
constructor(hs: Homeserver, options: OAuthServerOptions, publicBaseUrl: string);
|
|
74
|
+
get legacyApi(): boolean;
|
|
75
|
+
get accessTokenLifetimeMs(): number;
|
|
76
|
+
/** Absolute URL of a path under the OAuth prefix. */
|
|
77
|
+
url(path: string): string;
|
|
78
|
+
get accountManagementUri(): string;
|
|
79
|
+
/** Authorization server metadata (RFC 8414), served at `/_matrix/client/v1/auth_metadata`. */
|
|
80
|
+
metadata(): Record<string, unknown>;
|
|
81
|
+
/** The server's signing key as a JWK set (RFC 8037), for ID tokens. */
|
|
82
|
+
jwks(): {
|
|
83
|
+
keys: Record<string, string>[];
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Register a client. Each login registers again (the spec asks clients to), so identical
|
|
87
|
+
* metadata gets the same client ID, derived from it, instead of a new row every time.
|
|
88
|
+
*/
|
|
89
|
+
registerClient(raw: Record<string, unknown>): Promise<ClientMetadata>;
|
|
90
|
+
client(clientId: string | null): Promise<ClientMetadata | null>;
|
|
91
|
+
/** Delete old registrations that no token uses (the spec forbids deleting ones in use). */
|
|
92
|
+
prune(): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Check an authorization request. Problems with the client or the redirect URI are thrown as
|
|
95
|
+
* `OAuthError` with `redirect: false` semantics (shown to the user: RFC 6749 section 4.1.2.1
|
|
96
|
+
* forbids redirecting to an unverified URI); the rest come back as `{ error }` to redirect.
|
|
97
|
+
*/
|
|
98
|
+
checkAuthorizationRequest(params: URLSearchParams): Promise<{
|
|
99
|
+
client: ClientMetadata;
|
|
100
|
+
grant: PendingGrant;
|
|
101
|
+
} | {
|
|
102
|
+
client: ClientMetadata;
|
|
103
|
+
redirect: string;
|
|
104
|
+
error: OAuthError;
|
|
105
|
+
}>;
|
|
106
|
+
/** The client's redirect URI with the response parameters (and RFC 9207 `iss`). */
|
|
107
|
+
callbackUrl(redirectUri: string, mode: "query" | "fragment", params: Record<string, string | undefined>): string;
|
|
108
|
+
saveGrant(grant: PendingGrant): Promise<string>;
|
|
109
|
+
grant(id: string): Promise<PendingGrant | null>;
|
|
110
|
+
/** The user approved: issue a code and return where the browser goes next. */
|
|
111
|
+
approveGrant(id: string, grant: PendingGrant, userId: string, authTime: number): Promise<string>;
|
|
112
|
+
denyGrant(id: string, grant: PendingGrant): Promise<string>;
|
|
113
|
+
token(params: URLSearchParams, clientId: string | null): Promise<TokenResponse>;
|
|
114
|
+
private exchangeCode;
|
|
115
|
+
private issue;
|
|
116
|
+
private tokenResponse;
|
|
117
|
+
/** An OpenID Connect ID token (EdDSA with the server's signing key). */
|
|
118
|
+
private idToken;
|
|
119
|
+
startDeviceGrant(clientId: string | null, scope: string | null): Promise<{
|
|
120
|
+
device_code: string;
|
|
121
|
+
user_code: string;
|
|
122
|
+
verification_uri: string;
|
|
123
|
+
verification_uri_complete: string;
|
|
124
|
+
expires_in: number;
|
|
125
|
+
interval: number;
|
|
126
|
+
}>;
|
|
127
|
+
/** The device grant a user code belongs to (typed in any case, with or without the dash). */
|
|
128
|
+
deviceGrantByUserCode(raw: string): Promise<{
|
|
129
|
+
deviceCode: string;
|
|
130
|
+
grant: DeviceGrant;
|
|
131
|
+
} | null>;
|
|
132
|
+
decideDeviceGrant(deviceCode: string, grant: DeviceGrant, userId: string, approve: boolean): Promise<void>;
|
|
133
|
+
private pollDevice;
|
|
134
|
+
/**
|
|
135
|
+
* Revoke an access or refresh token (RFC 7009). The spec revokes both tokens of the pair; a
|
|
136
|
+
* logout through this endpoint ends the device's session like `/logout` does. Unknown tokens
|
|
137
|
+
* are not an error.
|
|
138
|
+
*/
|
|
139
|
+
revoke(token: string | null): Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|