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.
- package/README.md +89 -44
- package/dist/api/account.js +109 -64
- package/dist/api/admin.js +9 -0
- package/dist/api/federation.js +1 -1
- package/dist/api/index.js +2 -0
- package/dist/api/longpoll.js +61 -0
- package/dist/api/misc.js +3 -3
- package/dist/api/oauthserver.js +620 -0
- package/dist/api/slidingsync.js +28 -30
- package/dist/api/sync.js +181 -120
- package/dist/api/threepid.js +1 -1
- package/dist/api/uia.js +20 -8
- package/dist/api/user.js +6 -3
- package/dist/core/admin.js +1 -0
- package/dist/core/auth.d.ts +33 -2
- package/dist/core/auth.js +84 -21
- package/dist/core/changes.d.ts +79 -0
- package/dist/core/changes.js +332 -0
- package/dist/core/cluster.d.ts +19 -3
- package/dist/core/cluster.js +44 -4
- package/dist/core/keys.js +4 -2
- package/dist/core/notifier.d.ts +49 -3
- package/dist/core/notifier.js +123 -26
- 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/push.d.ts +12 -11
- package/dist/core/push.js +6 -4
- package/dist/core/ratelimit.d.ts +1 -1
- package/dist/core/ratelimit.js +4 -0
- package/dist/core/rooms.js +7 -1
- package/dist/core/semaphore.js +1 -1
- package/dist/core/sso.d.ts +51 -15
- package/dist/core/sso.js +86 -26
- package/dist/homeserver.d.ts +19 -2
- package/dist/homeserver.js +33 -9
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/storage/schema.js +20 -2
- package/dist/storage/sql.js +2 -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 +12 -15
package/dist/core/cluster.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
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")
|
|
442
|
-
|
|
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 {
|
package/dist/core/notifier.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
12
|
-
|
|
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
|
package/dist/core/notifier.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
39
|
+
if (msg?.from !== this.instanceId && Array.isArray(msg?.users)) this.hold(msg.users, Number(msg.pos) || 0);
|
|
15
40
|
});
|
|
16
41
|
}
|
|
17
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
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
|