matrix-nodejs-homeserver 0.3.4 → 0.3.5

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.
Files changed (44) hide show
  1. package/README.md +43 -9
  2. package/dist/api/account.js +109 -64
  3. package/dist/api/admin.js +9 -0
  4. package/dist/api/federation.js +1 -1
  5. package/dist/api/index.js +2 -0
  6. package/dist/api/misc.js +3 -3
  7. package/dist/api/oauthserver.js +620 -0
  8. package/dist/api/sync.js +1 -1
  9. package/dist/api/threepid.js +1 -1
  10. package/dist/api/uia.js +20 -8
  11. package/dist/api/user.js +6 -3
  12. package/dist/core/auth.d.ts +33 -2
  13. package/dist/core/auth.js +84 -21
  14. package/dist/core/oauth.js +2 -1
  15. package/dist/core/oauthserver.d.ts +141 -0
  16. package/dist/core/oauthserver.js +603 -0
  17. package/dist/core/ratelimit.d.ts +1 -1
  18. package/dist/core/ratelimit.js +4 -0
  19. package/dist/core/sso.d.ts +51 -15
  20. package/dist/core/sso.js +86 -26
  21. package/dist/homeserver.d.ts +12 -1
  22. package/dist/homeserver.js +17 -2
  23. package/dist/index.d.ts +3 -2
  24. package/dist/index.js +2 -1
  25. package/dist/storage/schema.js +20 -2
  26. package/dist/vendor/zod/v4/classic/errors.js +1 -1
  27. package/dist/vendor/zod/v4/classic/parse.js +1 -1
  28. package/dist/vendor/zod/v4/classic/schemas.js +1 -1
  29. package/dist/vendor/zod/v4/core/api.js +1 -1
  30. package/dist/vendor/zod/v4/core/checks.js +1 -1
  31. package/dist/vendor/zod/v4/core/core.js +1 -1
  32. package/dist/vendor/zod/v4/core/doc.js +1 -1
  33. package/dist/vendor/zod/v4/core/errors.js +1 -1
  34. package/dist/vendor/zod/v4/core/json-schema-processors.js +1 -1
  35. package/dist/vendor/zod/v4/core/memoizer.js +1 -1
  36. package/dist/vendor/zod/v4/core/parse.js +1 -1
  37. package/dist/vendor/zod/v4/core/regexes.js +1 -1
  38. package/dist/vendor/zod/v4/core/registries.js +1 -1
  39. package/dist/vendor/zod/v4/core/schemas.js +1 -1
  40. package/dist/vendor/zod/v4/core/to-json-schema.js +1 -1
  41. package/dist/vendor/zod/v4/core/util.js +1 -1
  42. package/dist/vendor/zod/v4/core/versions.js +1 -1
  43. package/dist/vendor/zod/v4/locales/en.js +1 -1
  44. package/package.json +12 -15
package/dist/api/uia.js CHANGED
@@ -2,15 +2,26 @@ import { isObject } from "../core/json.js";
2
2
  import { randomString } from "../core/crypto.js";
3
3
  import { M, UNSTABLE } from "../core/m.js";
4
4
  import { MatrixError, errors } from "../core/errors.js";
5
+ import { crossSigningResetAllowed } from "../core/oauthserver.js";
5
6
  //#region src/api/uia.ts
6
7
  const UIA_TTL_MS = 6e5;
7
8
  /**
8
9
  * User-interactive authentication. Resolves when one of `flows` has been
9
10
  * completed; otherwise throws the 401 response describing what is required.
10
- * `userId` binds password stages to an already-authenticated user.
11
+ * `userId` binds password stages to an already-authenticated user. `session` is the caller's,
12
+ * and `crossSigningReset` marks `/keys/device_signing/upload`, the one endpoint where tokens of
13
+ * the OAuth 2.0 API may use UIA (the `m.oauth` stage).
11
14
  */
12
- async function requireUia(hs, body, flows, userId) {
13
- if (hs.oauth && flows.some((f) => f.includes(M.LOGIN.PASSWORD))) flows = [[M.OAUTH], [UNSTABLE.ORG.MATRIX.CROSS_SIGNING_RESET]];
15
+ async function requireUia(hs, body, flows, userId, opts = {}) {
16
+ const viaOAuth = !!opts.session?.oauth || !!hs.oauth && flows.some((f) => f.includes(M.LOGIN.PASSWORD));
17
+ let oauthUrl;
18
+ if (viaOAuth) {
19
+ const accountUri = hs.oauthServer?.accountManagementUri ?? hs.oauth?.options.accountManagementUrl;
20
+ if (!opts.crossSigningReset) throw new MatrixError(403, "M_FORBIDDEN", "Do this on the account management page", accountUri ? { account_management_uri: accountUri } : {});
21
+ if (userId && await crossSigningResetAllowed(hs, userId)) return { threepids: [] };
22
+ flows = [[M.OAUTH], [UNSTABLE.ORG.MATRIX.CROSS_SIGNING_RESET]];
23
+ oauthUrl = hs.oauthServer ? `${hs.oauthServer.accountManagementUri}?action=org.matrix.cross_signing_reset` : hs.oauth.options.accountManagementUrl ?? hs.oauth.options.issuer;
24
+ }
14
25
  const auth = isObject(body.auth) ? body.auth : null;
15
26
  let sessionId = typeof auth?.session === "string" ? auth.session : void 0;
16
27
  const stored = sessionId ? await hs.cluster.get(`uia:${sessionId}`) : null;
@@ -25,8 +36,7 @@ async function requireUia(hs, body, flows, userId) {
25
36
  userId: session.userId,
26
37
  threepids: session.threepids
27
38
  }, UIA_TTL_MS);
28
- const oauthUrl = hs.oauth?.options.accountManagementUrl ?? hs.oauth?.options.issuer;
29
- const params = hs.oauth ? {
39
+ const params = oauthUrl ? {
30
40
  [M.OAUTH]: { url: oauthUrl },
31
41
  [UNSTABLE.ORG.MATRIX.CROSS_SIGNING_RESET]: { url: oauthUrl }
32
42
  } : {};
@@ -49,12 +59,14 @@ async function requireUia(hs, body, flows, userId) {
49
59
  break;
50
60
  case M.OAUTH:
51
61
  case UNSTABLE.ORG.MATRIX.CROSS_SIGNING_RESET:
52
- if (!hs.oauth) throw challenge({
62
+ if (!viaOAuth) throw challenge({
53
63
  errcode: "M_UNRECOGNIZED",
54
64
  error: "OAuth is not enabled"
55
65
  });
56
- session.completed.add(auth.type);
57
- break;
66
+ throw challenge({
67
+ errcode: "M_FORBIDDEN",
68
+ error: "The reset has not been approved on the account page"
69
+ });
58
70
  case M.LOGIN.REGISTRATION_TOKEN: {
59
71
  const { registrationTokenValid } = await import("./account.js");
60
72
  if (typeof auth.token !== "string" || !await registrationTokenValid(hs, auth.token)) throw challenge({
package/dist/api/user.js CHANGED
@@ -185,7 +185,7 @@ function registerUserRoutes(r) {
185
185
  value: ctx.params.deviceId
186
186
  }];
187
187
  if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
188
- await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
188
+ await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId, { session: s });
189
189
  await ctx.hs.auth.deleteDevice(s.userId, ctx.params.deviceId);
190
190
  return {};
191
191
  }
@@ -196,7 +196,7 @@ function registerUserRoutes(r) {
196
196
  handler: async (ctx) => {
197
197
  const s = ctx.session;
198
198
  const body = ctx.input;
199
- await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
199
+ await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId, { session: s });
200
200
  for (const d of body.devices) if (typeof d === "string") await ctx.hs.auth.deleteDevice(s.userId, d);
201
201
  return {};
202
202
  }
@@ -275,7 +275,10 @@ function registerUserRoutes(r) {
275
275
  "self_signing",
276
276
  "user_signing"
277
277
  ].filter((type) => body[`${type}_key`] !== void 0).every((type) => stored.get(type) === canonicalJson(body[`${type}_key`]))) return {};
278
- await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
278
+ await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId, {
279
+ session: s,
280
+ crossSigningReset: true
281
+ });
279
282
  }
280
283
  await ctx.hs.keys.uploadCrossSigning(s.userId, body);
281
284
  return {};
@@ -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, s] of this.cache) if (s.userId === msg.userId) this.cache.delete(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.expires_ts === null) this.cache.set(token, session);
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
- async refresh(refreshToken) {
119
- const row = await this.hs.db.findOne("access_tokens", [{
120
- field: "refresh_token",
121
- value: refreshToken
122
- }]);
123
- if (!row) throw errors.unknownToken("Unknown refresh token");
124
- await this.hs.db.delete("access_tokens", [{
125
- field: "token",
126
- value: row.token
127
- }]);
128
- this.cache.delete(row.token);
129
- return this.issueToken(row.user_id, row.device_id, true);
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, s] of this.cache) if (s.userId === userId) this.cache.delete(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
@@ -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