matrix-nodejs-homeserver 0.3.3 → 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 (59) hide show
  1. package/README.md +89 -44
  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/longpoll.js +61 -0
  7. package/dist/api/misc.js +3 -3
  8. package/dist/api/oauthserver.js +620 -0
  9. package/dist/api/slidingsync.js +28 -30
  10. package/dist/api/sync.js +181 -120
  11. package/dist/api/threepid.js +1 -1
  12. package/dist/api/uia.js +20 -8
  13. package/dist/api/user.js +6 -3
  14. package/dist/core/admin.js +1 -0
  15. package/dist/core/auth.d.ts +33 -2
  16. package/dist/core/auth.js +84 -21
  17. package/dist/core/changes.d.ts +79 -0
  18. package/dist/core/changes.js +332 -0
  19. package/dist/core/cluster.d.ts +19 -3
  20. package/dist/core/cluster.js +44 -4
  21. package/dist/core/keys.js +4 -2
  22. package/dist/core/notifier.d.ts +49 -3
  23. package/dist/core/notifier.js +123 -26
  24. package/dist/core/oauth.js +2 -1
  25. package/dist/core/oauthserver.d.ts +141 -0
  26. package/dist/core/oauthserver.js +603 -0
  27. package/dist/core/push.d.ts +12 -11
  28. package/dist/core/push.js +6 -4
  29. package/dist/core/ratelimit.d.ts +1 -1
  30. package/dist/core/ratelimit.js +4 -0
  31. package/dist/core/rooms.js +7 -1
  32. package/dist/core/semaphore.js +1 -1
  33. package/dist/core/sso.d.ts +51 -15
  34. package/dist/core/sso.js +86 -26
  35. package/dist/homeserver.d.ts +19 -2
  36. package/dist/homeserver.js +33 -9
  37. package/dist/index.d.ts +3 -2
  38. package/dist/index.js +2 -1
  39. package/dist/storage/schema.js +20 -2
  40. package/dist/storage/sql.js +2 -2
  41. package/dist/vendor/zod/v4/classic/errors.js +1 -1
  42. package/dist/vendor/zod/v4/classic/parse.js +1 -1
  43. package/dist/vendor/zod/v4/classic/schemas.js +1 -1
  44. package/dist/vendor/zod/v4/core/api.js +1 -1
  45. package/dist/vendor/zod/v4/core/checks.js +1 -1
  46. package/dist/vendor/zod/v4/core/core.js +1 -1
  47. package/dist/vendor/zod/v4/core/doc.js +1 -1
  48. package/dist/vendor/zod/v4/core/errors.js +1 -1
  49. package/dist/vendor/zod/v4/core/json-schema-processors.js +1 -1
  50. package/dist/vendor/zod/v4/core/memoizer.js +1 -1
  51. package/dist/vendor/zod/v4/core/parse.js +1 -1
  52. package/dist/vendor/zod/v4/core/regexes.js +1 -1
  53. package/dist/vendor/zod/v4/core/registries.js +1 -1
  54. package/dist/vendor/zod/v4/core/schemas.js +1 -1
  55. package/dist/vendor/zod/v4/core/to-json-schema.js +1 -1
  56. package/dist/vendor/zod/v4/core/util.js +1 -1
  57. package/dist/vendor/zod/v4/core/versions.js +1 -1
  58. package/dist/vendor/zod/v4/locales/en.js +1 -1
  59. package/package.json +12 -15
@@ -80,6 +80,11 @@ function localCluster() {
80
80
  }
81
81
  const WRITER_TTL_MS = 3e4;
82
82
  /**
83
+ * A stream position held longer than this holds back /sync for everyone: it is logged with where
84
+ * it was allocated (https://github.com/k18k/matrix-nodejs-homeserver/issues/48).
85
+ */
86
+ const SLOW_LEASE_MS = 5e3;
87
+ /**
83
88
  * Stream positions with multiple writers. Positions are allocated from a global
84
89
  * sequence; readers only see positions up to which every writer has finished, so
85
90
  * an event committed late with a lower position can never be skipped by /sync.
@@ -99,11 +104,16 @@ var StreamManager = class {
99
104
  lastPos = 0;
100
105
  heartbeat;
101
106
  positionCache = null;
107
+ highestAllocated = 0;
108
+ /** Called after a write of this instance completed, when the position may have moved (#50). */
109
+ onAdvance = () => {};
102
110
  publishing = Promise.resolve();
103
111
  nextPublish = null;
104
- constructor(cluster, instanceId) {
112
+ warn;
113
+ constructor(cluster, instanceId, warn = () => {}) {
105
114
  this.cluster = cluster;
106
115
  this.instanceId = instanceId;
116
+ this.warn = warn;
107
117
  this.heartbeat = setInterval(() => void this.flush().catch(() => {}), WRITER_TTL_MS / 3);
108
118
  this.heartbeat.unref?.();
109
119
  }
@@ -146,10 +156,14 @@ var StreamManager = class {
146
156
  await this.flush();
147
157
  id = await this.cluster.nextId("stream");
148
158
  this.inflight.add(id);
159
+ if (id > this.highestAllocated) this.highestAllocated = id;
149
160
  } finally {
150
161
  this.allocating--;
151
162
  }
152
163
  let finished = false;
164
+ const site = /* @__PURE__ */ new Error("stream position allocated here");
165
+ const slow = setTimeout(() => this.warn(`Stream position ${id} held for over ${SLOW_LEASE_MS} ms; /sync waits for it`, site), SLOW_LEASE_MS);
166
+ slow.unref?.();
153
167
  const timer = setTimeout(() => void lease.done(), 6e4);
154
168
  timer.unref?.();
155
169
  const lease = {
@@ -158,9 +172,11 @@ var StreamManager = class {
158
172
  if (finished) return;
159
173
  finished = true;
160
174
  clearTimeout(timer);
175
+ clearTimeout(slow);
161
176
  this.inflight.delete(id);
162
177
  await this.flush();
163
178
  this.positionCache = null;
179
+ this.onAdvance();
164
180
  }
165
181
  };
166
182
  return lease;
@@ -178,9 +194,16 @@ var StreamManager = class {
178
194
  async backfillId() {
179
195
  return -await this.cluster.nextId("backfill");
180
196
  }
181
- /** The highest position below which everything is persisted, cluster-wide. */
182
- async position() {
183
- if (this.positionCache && Date.now() - this.positionCache.at < 20) return this.positionCache.value;
197
+ /** The highest position this instance has allocated: an upper bound of its changes so far. */
198
+ get lastAllocated() {
199
+ return this.highestAllocated;
200
+ }
201
+ /**
202
+ * The highest position below which everything is persisted, cluster-wide. `fresh` skips the
203
+ * 20 ms cache, for a caller that knows a change has completed elsewhere.
204
+ */
205
+ async position(fresh = false) {
206
+ if (!fresh && this.positionCache && Date.now() - this.positionCache.at < 20) return this.positionCache.value;
184
207
  let pos = await this.cluster.currentId("stream");
185
208
  for (const [, w] of await this.cluster.entries("writer:")) if (w?.busy) pos = Math.min(pos, w.pos);
186
209
  if (this.inflight.size) pos = Math.min(pos, Math.min(...this.inflight) - 1);
@@ -190,6 +213,23 @@ var StreamManager = class {
190
213
  };
191
214
  return pos;
192
215
  }
216
+ /**
217
+ * Resolve once the position has reached `pos` (true), or false after `timeoutMs` or on abort.
218
+ * For a request woken by a change that another write in flight still keeps out of sight
219
+ * (https://github.com/k18k/matrix-nodejs-homeserver/issues/50). It checks again at growing
220
+ * intervals: writes finish in milliseconds, and the position may be held by another instance.
221
+ */
222
+ async waitFor(pos, timeoutMs, signal) {
223
+ const deadline = Date.now() + timeoutMs;
224
+ let delay = 5;
225
+ for (;;) {
226
+ if (await this.position(true) >= pos) return true;
227
+ const left = deadline - Date.now();
228
+ if (left <= 0 || signal?.aborted) return false;
229
+ await new Promise((resolve) => setTimeout(resolve, Math.min(delay, left)));
230
+ delay = Math.min(delay * 2, 200);
231
+ }
232
+ }
193
233
  async advanceTo(value) {
194
234
  await this.cluster.advance("stream", value);
195
235
  }
package/dist/core/keys.js CHANGED
@@ -438,8 +438,10 @@ var KeyService = class {
438
438
  if (m.membership === "join") {
439
439
  for (const u of members) if (u !== userId) changed.add(u);
440
440
  } else for (const u of members) if (!sharing.has(u)) left.add(u);
441
- } else if (m.membership === "join") changed.add(m.user_id);
442
- else if (!sharing.has(m.user_id)) left.add(m.user_id);
441
+ } else if (m.membership === "join") {
442
+ const ev = await this.hs.rooms.getEvent(m.event_id);
443
+ if (ev && ev.stream > since) changed.add(m.user_id);
444
+ } else if (!sharing.has(m.user_id)) left.add(m.user_id);
443
445
  }
444
446
  for (const u of left) changed.delete(u);
445
447
  return {
@@ -1,17 +1,63 @@
1
1
  import { ClusterAdapter } from "./cluster.js";
2
2
  //#region src/core/notifier.d.ts
3
+ /** A registration for wake-ups from the moment it was made (see `Notifier.listen`). */
4
+ export interface Listener {
5
+ /**
6
+ * The stream position of this user's latest wake-up before the listener was registered (0 if
7
+ * none is remembered): the change may not have been visible to the build that preceded it.
8
+ */
9
+ readonly recent: number;
10
+ /**
11
+ * Resolve with the position of the first wake-up since registration (at once if there was one
12
+ * already), or null after `timeoutMs` or on abort.
13
+ */
14
+ wait(timeoutMs: number, signal?: AbortSignal): Promise<number | null>;
15
+ /** Stop listening; a pending `wait` resolves with null. */
16
+ cancel(): void;
17
+ }
3
18
  /**
4
19
  * Wakes long-polling `/sync` requests when something relevant happens. Notifications
5
20
  * are broadcast through the cluster adapter so waiters on other instances wake too.
21
+ *
22
+ * https://github.com/k18k/matrix-nodejs-homeserver/issues/50: every wake-up carries the stream
23
+ * position of its change, or an upper bound of it. Writes can complete out of order, so a change
24
+ * may be committed while an earlier write still holds the position below it. As in Synapse
25
+ * (`Notifier._notify_pending_new_room_events`), such a wake-up is held here until the position
26
+ * reaches it: a request is only woken for changes it can see.
6
27
  */
7
28
  export declare class Notifier {
8
- private waiters;
29
+ private listeners;
30
+ /** Each user's latest wake-up position, oldest first (re-inserted on update). */
31
+ private recent;
9
32
  private cluster;
10
33
  private instanceId;
11
- constructor(cluster: ClusterAdapter, instanceId: string);
12
- notify(userIds: Iterable<string>): void;
34
+ private lastAllocated;
35
+ private position;
36
+ /** Wake-ups whose change is beyond the position known to be visible. */
37
+ private held;
38
+ /** The highest position known to be visible. */
39
+ private visible;
40
+ private release;
41
+ /**
42
+ * `lastAllocated`: the highest stream position this instance has allocated (an upper bound of
43
+ * its changes); `position`: the position below which everything is visible.
44
+ */
45
+ constructor(cluster: ClusterAdapter, instanceId: string, lastAllocated?: () => number, position?: (fresh: boolean) => Promise<number>);
46
+ /**
47
+ * Wake `userIds`. `pos` is the stream position of the change; without it, the highest position
48
+ * this instance has allocated stands in, which is never below it.
49
+ */
50
+ notify(userIds: Iterable<string>, pos?: number): void;
51
+ /** The position may have moved (a write of this instance completed): release what it covers. */
52
+ advanced(): void;
53
+ /** Wake now if the change is visible, otherwise once the position reaches it. */
54
+ private hold;
55
+ private scheduleRelease;
56
+ private releaseHeld;
13
57
  private wakeLocal;
14
58
  notifyAll(): void;
59
+ listen(userId: string): Listener;
60
+ /** Wait for a wake-up of `userId` (for callers that do not track positions). */
15
61
  wait(userId: string, timeoutMs: number, signal?: AbortSignal): Promise<void>;
16
62
  }
17
63
  //#endregion
@@ -1,53 +1,150 @@
1
1
  //#region src/core/notifier.ts
2
+ /** Users whose latest wake-up position is remembered (#50); the oldest are forgotten first. */
3
+ const RECENT_USERS = 1e5;
4
+ /** How soon held wake-ups are looked at again while the position has not reached them. */
5
+ const RELEASE_RETRY_MS = 10;
2
6
  /**
3
7
  * Wakes long-polling `/sync` requests when something relevant happens. Notifications
4
8
  * are broadcast through the cluster adapter so waiters on other instances wake too.
9
+ *
10
+ * https://github.com/k18k/matrix-nodejs-homeserver/issues/50: every wake-up carries the stream
11
+ * position of its change, or an upper bound of it. Writes can complete out of order, so a change
12
+ * may be committed while an earlier write still holds the position below it. As in Synapse
13
+ * (`Notifier._notify_pending_new_room_events`), such a wake-up is held here until the position
14
+ * reaches it: a request is only woken for changes it can see.
5
15
  */
6
16
  var Notifier = class {
7
- waiters = /* @__PURE__ */ new Map();
17
+ listeners = /* @__PURE__ */ new Map();
18
+ /** Each user's latest wake-up position, oldest first (re-inserted on update). */
19
+ recent = /* @__PURE__ */ new Map();
8
20
  cluster;
9
21
  instanceId;
10
- constructor(cluster, instanceId) {
22
+ lastAllocated;
23
+ position;
24
+ /** Wake-ups whose change is beyond the position known to be visible. */
25
+ held = [];
26
+ /** The highest position known to be visible. */
27
+ visible = 0;
28
+ release = null;
29
+ /**
30
+ * `lastAllocated`: the highest stream position this instance has allocated (an upper bound of
31
+ * its changes); `position`: the position below which everything is visible.
32
+ */
33
+ constructor(cluster, instanceId, lastAllocated = () => 0, position = async () => Number.POSITIVE_INFINITY) {
11
34
  this.cluster = cluster;
12
35
  this.instanceId = instanceId;
36
+ this.lastAllocated = lastAllocated;
37
+ this.position = position;
13
38
  cluster.subscribe("notify", (msg) => {
14
- if (msg?.from !== this.instanceId && Array.isArray(msg?.users)) this.wakeLocal(msg.users);
39
+ if (msg?.from !== this.instanceId && Array.isArray(msg?.users)) this.hold(msg.users, Number(msg.pos) || 0);
15
40
  });
16
41
  }
17
- notify(userIds) {
42
+ /**
43
+ * Wake `userIds`. `pos` is the stream position of the change; without it, the highest position
44
+ * this instance has allocated stands in, which is never below it.
45
+ */
46
+ notify(userIds, pos) {
18
47
  const users = [...userIds];
19
48
  if (!users.length) return;
20
- this.wakeLocal(users);
49
+ const at = pos ?? this.lastAllocated();
50
+ this.hold(users, at);
21
51
  this.cluster.publish("notify", {
22
52
  from: this.instanceId,
23
- users
53
+ users,
54
+ pos: at
24
55
  }).catch(() => {});
25
56
  }
26
- wakeLocal(userIds) {
57
+ /** The position may have moved (a write of this instance completed): release what it covers. */
58
+ advanced() {
59
+ if (this.held.length) this.scheduleRelease(0);
60
+ }
61
+ /** Wake now if the change is visible, otherwise once the position reaches it. */
62
+ hold(users, pos) {
63
+ if (pos <= this.visible) return this.wakeLocal(users, pos);
64
+ this.held.push({
65
+ users,
66
+ pos
67
+ });
68
+ this.scheduleRelease(0);
69
+ }
70
+ scheduleRelease(delay) {
71
+ if (this.release) return;
72
+ this.release = setTimeout(() => {
73
+ this.release = null;
74
+ this.releaseHeld().catch(() => this.scheduleRelease(RELEASE_RETRY_MS));
75
+ }, delay);
76
+ this.release.unref?.();
77
+ }
78
+ async releaseHeld() {
79
+ const highest = Math.max(...this.held.map((h) => h.pos));
80
+ let pos = await this.position(false);
81
+ if (pos < highest) pos = await this.position(true);
82
+ if (pos > this.visible) this.visible = pos;
83
+ const due = this.held.filter((h) => h.pos <= this.visible);
84
+ this.held = this.held.filter((h) => h.pos > this.visible);
85
+ for (const h of due) this.wakeLocal(h.users, h.pos);
86
+ if (this.held.length) this.scheduleRelease(RELEASE_RETRY_MS);
87
+ }
88
+ wakeLocal(userIds, pos) {
27
89
  for (const u of userIds) {
28
- const set = this.waiters.get(u);
90
+ const previous = this.recent.get(u) ?? 0;
91
+ this.recent.delete(u);
92
+ this.recent.set(u, Math.max(previous, pos));
93
+ if (this.recent.size > RECENT_USERS) this.recent.delete(this.recent.keys().next().value);
94
+ const set = this.listeners.get(u);
29
95
  if (!set) continue;
30
- this.waiters.delete(u);
31
- for (const wake of set) wake();
96
+ this.listeners.delete(u);
97
+ for (const wake of set) wake(pos);
32
98
  }
33
99
  }
34
100
  notifyAll() {
35
- this.wakeLocal([...this.waiters.keys()]);
36
- }
37
- wait(userId, timeoutMs, signal) {
38
- return new Promise((resolve) => {
39
- let set = this.waiters.get(userId);
40
- if (!set) this.waiters.set(userId, set = /* @__PURE__ */ new Set());
41
- const done = () => {
42
- clearTimeout(timer);
43
- set.delete(done);
44
- signal?.removeEventListener("abort", done);
45
- resolve();
46
- };
47
- const timer = setTimeout(done, timeoutMs);
48
- signal?.addEventListener("abort", done, { once: true });
49
- set.add(done);
50
- });
101
+ for (const [u, set] of this.listeners) {
102
+ this.listeners.delete(u);
103
+ for (const wake of set) wake(0);
104
+ }
105
+ }
106
+ listen(userId) {
107
+ let woken = null;
108
+ let resolveWait = null;
109
+ let set = this.listeners.get(userId);
110
+ if (!set) this.listeners.set(userId, set = /* @__PURE__ */ new Set());
111
+ const onWake = (pos) => {
112
+ woken = Math.max(woken ?? 0, pos);
113
+ resolveWait?.(woken);
114
+ };
115
+ set.add(onWake);
116
+ const remove = () => {
117
+ const current = this.listeners.get(userId);
118
+ current?.delete(onWake);
119
+ if (current && current.size === 0) this.listeners.delete(userId);
120
+ };
121
+ return {
122
+ recent: this.recent.get(userId) ?? 0,
123
+ wait: (timeoutMs, signal) => {
124
+ if (woken !== null) return Promise.resolve(woken);
125
+ return new Promise((resolve) => {
126
+ const done = (result) => {
127
+ clearTimeout(timer);
128
+ signal?.removeEventListener("abort", aborted);
129
+ resolveWait = null;
130
+ remove();
131
+ resolve(result);
132
+ };
133
+ const aborted = () => done(null);
134
+ const timer = setTimeout(() => done(null), timeoutMs);
135
+ signal?.addEventListener("abort", aborted, { once: true });
136
+ resolveWait = (pos) => done(pos);
137
+ });
138
+ },
139
+ cancel: () => {
140
+ remove();
141
+ resolveWait?.(null);
142
+ }
143
+ };
144
+ }
145
+ /** Wait for a wake-up of `userId` (for callers that do not track positions). */
146
+ async wait(userId, timeoutMs, signal) {
147
+ await this.listen(userId).wait(timeoutMs, signal);
51
148
  }
52
149
  };
53
150
  //#endregion
@@ -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