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/api/slidingsync.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getState } from "../core/authrules.js";
|
|
|
6
6
|
import { record, string, unknown } from "../vendor/zod/v4/classic/schemas.js";
|
|
7
7
|
import { body, lenient } from "../http/input.js";
|
|
8
8
|
import { procedure } from "../http/procedure.js";
|
|
9
|
+
import { longPoll } from "./longpoll.js";
|
|
9
10
|
import { roomEphemeral, roomTimeline, toStripped } from "./sync.js";
|
|
10
11
|
//#region src/api/slidingsync.ts
|
|
11
12
|
const slidingSyncBody = body({
|
|
@@ -104,37 +105,34 @@ async function slidingSync(ctx) {
|
|
|
104
105
|
rooms: {}
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (since === null || !empty || remaining <= 0) {
|
|
113
|
-
await hs.db.upsert("sliding_sync_connections", {
|
|
114
|
-
user_id: s.userId,
|
|
115
|
-
device_id: s.deviceId,
|
|
116
|
-
conn_id: connId,
|
|
117
|
-
pos: to,
|
|
118
|
-
state: {
|
|
119
|
-
...next,
|
|
120
|
-
prevPos: since
|
|
121
|
-
},
|
|
122
|
-
updated_ts: Date.now()
|
|
123
|
-
});
|
|
124
|
-
return {
|
|
125
|
-
pos: String(to),
|
|
126
|
-
...response
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
if (await hs.streams.position() > to) continue;
|
|
130
|
-
await hs.notifier.wait(s.userId, remaining, ctx.request.signal);
|
|
131
|
-
if (ctx.request.signal?.aborted) return {
|
|
132
|
-
pos: String(since),
|
|
133
|
-
lists: {},
|
|
134
|
-
rooms: {},
|
|
135
|
-
extensions: {}
|
|
108
|
+
const { value, to, aborted } = await longPoll(hs, s.userId, timeout, ctx.request.signal, async (to) => {
|
|
109
|
+
const built = await buildResponse(hs, s, body, conn, since, to);
|
|
110
|
+
return {
|
|
111
|
+
value: built,
|
|
112
|
+
done: since === null || !built.empty
|
|
136
113
|
};
|
|
137
|
-
}
|
|
114
|
+
});
|
|
115
|
+
if (aborted && value.empty) return {
|
|
116
|
+
pos: String(since),
|
|
117
|
+
lists: {},
|
|
118
|
+
rooms: {},
|
|
119
|
+
extensions: {}
|
|
120
|
+
};
|
|
121
|
+
await hs.db.upsert("sliding_sync_connections", {
|
|
122
|
+
user_id: s.userId,
|
|
123
|
+
device_id: s.deviceId,
|
|
124
|
+
conn_id: connId,
|
|
125
|
+
pos: to,
|
|
126
|
+
state: {
|
|
127
|
+
...value.next,
|
|
128
|
+
prevPos: since
|
|
129
|
+
},
|
|
130
|
+
updated_ts: Date.now()
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
pos: String(to),
|
|
134
|
+
...value.response
|
|
135
|
+
};
|
|
138
136
|
}
|
|
139
137
|
async function buildResponse(hs, s, body, conn, since, to) {
|
|
140
138
|
const userId = s.userId;
|
package/dist/api/sync.js
CHANGED
|
@@ -8,9 +8,11 @@ import { deviceListChangesQuery } from "../core/keys.js";
|
|
|
8
8
|
import { body } from "../http/input.js";
|
|
9
9
|
import { procedure } from "../http/procedure.js";
|
|
10
10
|
import { readBatch } from "../storage/batch.js";
|
|
11
|
+
import { trackedReads } from "../core/changes.js";
|
|
12
|
+
import { fullProfile } from "../core/profile.js";
|
|
11
13
|
import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
|
|
14
|
+
import { longPoll } from "./longpoll.js";
|
|
12
15
|
import { Semaphore } from "../core/semaphore.js";
|
|
13
|
-
import { fullProfile } from "../core/profile.js";
|
|
14
16
|
//#region src/api/sync.ts
|
|
15
17
|
function parseSyncToken(raw) {
|
|
16
18
|
if (!raw) return null;
|
|
@@ -330,6 +332,8 @@ async function changesSince(hs, p, memberships, since, accountRows) {
|
|
|
330
332
|
operator: "lte",
|
|
331
333
|
value: p.to
|
|
332
334
|
}];
|
|
335
|
+
const logs = hs.changes?.logs;
|
|
336
|
+
const mayHaveChanged = (log, rooms) => logs ? logs[log].filterChanged(rooms, since) : rooms;
|
|
333
337
|
const chunks = (list) => Array.from({ length: Math.ceil(list.length / 500) }, (_, i) => list.slice(i * 500, i * 500 + 500));
|
|
334
338
|
const inRooms = (chunk) => ({
|
|
335
339
|
field: "room_id",
|
|
@@ -337,31 +341,34 @@ async function changesSince(hs, p, memberships, since, accountRows) {
|
|
|
337
341
|
value: chunk
|
|
338
342
|
});
|
|
339
343
|
const queries = [];
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
344
|
+
const kinds = [];
|
|
345
|
+
const add = (kind, query) => {
|
|
346
|
+
kinds.push(kind);
|
|
347
|
+
queries.push(query);
|
|
348
|
+
};
|
|
349
|
+
for (const chunk of chunks(mayHaveChanged("roomEvents", joined))) add("events", {
|
|
350
|
+
model: "events",
|
|
351
|
+
opts: {
|
|
352
|
+
where: [inRooms(chunk), ...range],
|
|
353
|
+
fields: ["room_id"]
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
for (const chunk of chunks(mayHaveChanged("roomReceipts", joined))) add("receipts", {
|
|
357
|
+
model: "receipts",
|
|
358
|
+
opts: { where: [inRooms(chunk), ...range] }
|
|
359
|
+
});
|
|
360
|
+
for (const chunk of chunks(mayHaveChanged("roomPartialState", joined))) add("completed", {
|
|
361
|
+
model: "partial_state_rooms",
|
|
362
|
+
opts: {
|
|
363
|
+
where: [inRooms(chunk), {
|
|
364
|
+
field: "completed_stream",
|
|
365
|
+
operator: "gt",
|
|
366
|
+
value: since
|
|
367
|
+
}],
|
|
368
|
+
fields: ["room_id"]
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
for (const chunk of chunks(mayHaveChanged("roomMembers", anyMembership))) add("members", {
|
|
365
372
|
model: "memberships",
|
|
366
373
|
opts: { where: [inRooms(chunk), {
|
|
367
374
|
field: "stream",
|
|
@@ -369,20 +376,18 @@ async function changesSince(hs, p, memberships, since, accountRows) {
|
|
|
369
376
|
value: since
|
|
370
377
|
}] }
|
|
371
378
|
});
|
|
372
|
-
const results = await readBatch(hs.db, queries);
|
|
379
|
+
const results = queries.length ? await readBatch(hs.db, queries) : [];
|
|
373
380
|
const touched = /* @__PURE__ */ new Set();
|
|
374
381
|
const receipts = /* @__PURE__ */ new Map();
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
for (const r of
|
|
379
|
-
|
|
380
|
-
touched.add(r.room_id);
|
|
381
|
-
receipts.set(r.room_id, [...receipts.get(r.room_id) ?? [], r]);
|
|
382
|
+
const memberChanges = [];
|
|
383
|
+
results.forEach((rows, i) => {
|
|
384
|
+
const kind = kinds[i];
|
|
385
|
+
for (const r of rows) {
|
|
386
|
+
if (kind === "members") memberChanges.push(r);
|
|
387
|
+
else touched.add(r.room_id);
|
|
388
|
+
if (kind === "receipts") receipts.set(r.room_id, [...receipts.get(r.room_id) ?? [], r]);
|
|
382
389
|
}
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
const memberChanges = results.slice(joinedChunks * 3).flat();
|
|
390
|
+
});
|
|
386
391
|
const accountData = /* @__PURE__ */ new Map();
|
|
387
392
|
for (const r of accountRows) {
|
|
388
393
|
if (r.room_id) touched.add(r.room_id);
|
|
@@ -428,76 +433,115 @@ async function buildSync(hs, p) {
|
|
|
428
433
|
operator: "lte",
|
|
429
434
|
value: p.since.toDevice
|
|
430
435
|
}]) : Promise.resolve(0);
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
model: "memberships",
|
|
434
|
-
opts: { where: [{
|
|
435
|
-
field: "user_id",
|
|
436
|
-
value: userId
|
|
437
|
-
}] }
|
|
438
|
-
},
|
|
436
|
+
const device = `${userId}|${p.session.deviceId}`;
|
|
437
|
+
const [memberships, ignoredRows, toDeviceRows, oneTimeKeyRows, fallbackRows, accountRows = []] = await trackedReads(hs.db, hs.changes, [
|
|
439
438
|
{
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
{
|
|
439
|
+
query: {
|
|
440
|
+
model: "memberships",
|
|
441
|
+
opts: { where: [{
|
|
443
442
|
field: "user_id",
|
|
444
443
|
value: userId
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
field: "type",
|
|
452
|
-
value: M.IGNORED_USER_LIST
|
|
453
|
-
}
|
|
454
|
-
] }
|
|
444
|
+
}] }
|
|
445
|
+
},
|
|
446
|
+
cached: {
|
|
447
|
+
name: "userMemberships",
|
|
448
|
+
key: userId
|
|
449
|
+
}
|
|
455
450
|
},
|
|
456
451
|
{
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
where: [
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
452
|
+
query: {
|
|
453
|
+
model: "account_data",
|
|
454
|
+
opts: { where: [
|
|
455
|
+
{
|
|
456
|
+
field: "user_id",
|
|
457
|
+
value: userId
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
field: "room_id",
|
|
461
|
+
value: ""
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
field: "type",
|
|
465
|
+
value: M.IGNORED_USER_LIST
|
|
466
|
+
}
|
|
467
|
+
] }
|
|
468
|
+
},
|
|
469
|
+
cached: {
|
|
470
|
+
name: "ignoredUsers",
|
|
471
|
+
key: userId
|
|
469
472
|
}
|
|
470
473
|
},
|
|
471
474
|
{
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
475
|
+
query: {
|
|
476
|
+
model: "to_device",
|
|
477
|
+
opts: {
|
|
478
|
+
where: [...deviceWhere, {
|
|
479
|
+
field: "stream",
|
|
480
|
+
operator: "gt",
|
|
481
|
+
value: p.since?.toDevice ?? 0
|
|
482
|
+
}],
|
|
483
|
+
sortBy: {
|
|
484
|
+
field: "stream",
|
|
485
|
+
direction: "asc"
|
|
486
|
+
},
|
|
487
|
+
limit: 100
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
unlessChanged: {
|
|
491
|
+
log: "toDevice",
|
|
492
|
+
key: device,
|
|
493
|
+
after: p.since?.toDevice ?? 0
|
|
479
494
|
}
|
|
480
495
|
},
|
|
481
496
|
{
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
497
|
+
query: {
|
|
498
|
+
model: "one_time_keys",
|
|
499
|
+
opts: {
|
|
500
|
+
where: [...deviceWhere, {
|
|
501
|
+
field: "fallback",
|
|
502
|
+
value: false
|
|
503
|
+
}],
|
|
504
|
+
fields: ["algorithm"]
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
cached: {
|
|
508
|
+
name: "oneTimeKeys",
|
|
509
|
+
key: device
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
query: {
|
|
514
|
+
model: "fallback_keys",
|
|
515
|
+
opts: {
|
|
516
|
+
where: [...deviceWhere, {
|
|
517
|
+
field: "used",
|
|
518
|
+
value: false
|
|
519
|
+
}],
|
|
520
|
+
fields: ["algorithm"]
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
cached: {
|
|
524
|
+
name: "fallbackKeys",
|
|
525
|
+
key: device
|
|
489
526
|
}
|
|
490
527
|
},
|
|
491
528
|
...incremental ? [{
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
529
|
+
query: {
|
|
530
|
+
model: "account_data",
|
|
531
|
+
opts: { where: [{
|
|
532
|
+
field: "user_id",
|
|
533
|
+
value: userId
|
|
534
|
+
}, {
|
|
535
|
+
field: "stream",
|
|
536
|
+
operator: "gt",
|
|
537
|
+
value: since
|
|
538
|
+
}] }
|
|
539
|
+
},
|
|
540
|
+
unlessChanged: {
|
|
541
|
+
log: "accountData",
|
|
542
|
+
key: userId,
|
|
543
|
+
after: since
|
|
544
|
+
}
|
|
501
545
|
}] : []
|
|
502
546
|
]);
|
|
503
547
|
p = {
|
|
@@ -644,17 +688,31 @@ async function buildSync(hs, p) {
|
|
|
644
688
|
value: []
|
|
645
689
|
}] }
|
|
646
690
|
};
|
|
647
|
-
let [presenceRows = [], newlySharedPresence = [], deviceRows] = await
|
|
648
|
-
presenceQuery
|
|
649
|
-
|
|
691
|
+
let [presenceRows = [], newlySharedPresence = [], deviceRows] = await trackedReads(hs.db, hs.changes, [
|
|
692
|
+
since === null ? { query: presenceQuery } : {
|
|
693
|
+
query: presenceQuery,
|
|
694
|
+
unlessChanged: {
|
|
695
|
+
log: "presence",
|
|
696
|
+
key: "*",
|
|
697
|
+
after: since
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
{ query: {
|
|
650
701
|
model: "presence",
|
|
651
702
|
opts: { where: [{
|
|
652
703
|
field: "user_id",
|
|
653
704
|
operator: "in",
|
|
654
705
|
value: [...newlyShared]
|
|
655
706
|
}] }
|
|
656
|
-
},
|
|
657
|
-
...since !== null ? [
|
|
707
|
+
} },
|
|
708
|
+
...since !== null ? [{
|
|
709
|
+
query: recent("device_list_changes"),
|
|
710
|
+
unlessChanged: {
|
|
711
|
+
log: "deviceLists",
|
|
712
|
+
key: "*",
|
|
713
|
+
after: since
|
|
714
|
+
}
|
|
715
|
+
}] : []
|
|
658
716
|
]);
|
|
659
717
|
if (since !== null) {
|
|
660
718
|
if (presenceRows.length > RECENT_CHANGES) presenceRows = await hs.db.findMany("presence", { where: [{
|
|
@@ -745,14 +803,12 @@ async function sync(ctx) {
|
|
|
745
803
|
const timeout = Math.min(Math.max(Number.parseInt(ctx.query.get("timeout") ?? "0", 10) || 0, 0), hs.config.maxSyncTimeoutMs);
|
|
746
804
|
const setPresence = ctx.query.get("set_presence") ?? "online";
|
|
747
805
|
if (setPresence !== "offline") await touchPresence(hs, session.userId, setPresence);
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const useStateAfter = stable || unstable;
|
|
755
|
-
const stateAfterKey = stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER;
|
|
806
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
807
|
+
const stable = ctx.query.get("use_state_after") === "true";
|
|
808
|
+
const unstable = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4222.USE_STATE_AFTER) === "true";
|
|
809
|
+
const useStateAfter = stable || unstable;
|
|
810
|
+
const stateAfterKey = stable ? "state_after" : UNSTABLE.ORG.MATRIX.MSC4222.STATE_AFTER;
|
|
811
|
+
const { value } = await longPoll(hs, session.userId, timeout, ctx.request.signal, async (to) => {
|
|
756
812
|
const { body, empty } = await syncSlots(hs).run(() => buildSync(hs, {
|
|
757
813
|
session,
|
|
758
814
|
ignored,
|
|
@@ -763,20 +819,21 @@ async function sync(ctx) {
|
|
|
763
819
|
useStateAfter,
|
|
764
820
|
stateAfterKey
|
|
765
821
|
}));
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
822
|
+
return {
|
|
823
|
+
value: body,
|
|
824
|
+
done: !since || !empty || fullState
|
|
825
|
+
};
|
|
826
|
+
});
|
|
827
|
+
return value;
|
|
772
828
|
}
|
|
773
829
|
const LAST_ACTIVE_GRANULARITY_MS = 6e4;
|
|
774
830
|
async function touchPresence(hs, userId, presence) {
|
|
775
831
|
const state = presence === "unavailable" ? "unavailable" : "online";
|
|
776
|
-
const
|
|
832
|
+
const read = () => hs.db.findOne("presence", [{
|
|
777
833
|
field: "user_id",
|
|
778
834
|
value: userId
|
|
779
835
|
}]);
|
|
836
|
+
const row = hs.changes ? await hs.changes.values.presenceRow.get(userId, read) : await read();
|
|
780
837
|
if (row && row.presence === state) {
|
|
781
838
|
if (Date.now() - row.last_active_ts >= LAST_ACTIVE_GRANULARITY_MS) await hs.db.update("presence", [{
|
|
782
839
|
field: "user_id",
|
|
@@ -784,13 +841,17 @@ async function touchPresence(hs, userId, presence) {
|
|
|
784
841
|
}], { last_active_ts: Date.now() });
|
|
785
842
|
return;
|
|
786
843
|
}
|
|
787
|
-
await hs.streams.write((stream) =>
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
844
|
+
const stream = await hs.streams.write(async (stream) => {
|
|
845
|
+
await hs.db.upsert("presence", {
|
|
846
|
+
user_id: userId,
|
|
847
|
+
presence: state,
|
|
848
|
+
status_msg: row?.status_msg ?? null,
|
|
849
|
+
last_active_ts: Date.now(),
|
|
850
|
+
stream
|
|
851
|
+
});
|
|
852
|
+
return stream;
|
|
853
|
+
});
|
|
854
|
+
hs.notifier.notify([userId, ...await hs.rooms.usersSharingRooms(userId)], stream);
|
|
794
855
|
await hs.federation.onPresence(userId, state, row?.status_msg ?? null).catch(() => {});
|
|
795
856
|
}
|
|
796
857
|
function registerSyncRoutes(r) {
|
package/dist/api/threepid.js
CHANGED
|
@@ -89,7 +89,7 @@ function registerThreepidRoutes(r) {
|
|
|
89
89
|
notDelegated(ctx);
|
|
90
90
|
const s = await ctx.auth();
|
|
91
91
|
const body = ctx.input;
|
|
92
|
-
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
|
|
92
|
+
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId, { session: s });
|
|
93
93
|
const validated = await ctx.hs.threepids.validated({
|
|
94
94
|
sid: body.sid,
|
|
95
95
|
client_secret: body.client_secret
|
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
|
-
|
|
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
|
|
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 (!
|
|
62
|
+
if (!viaOAuth) throw challenge({
|
|
53
63
|
errcode: "M_UNRECOGNIZED",
|
|
54
64
|
error: "OAuth is not enabled"
|
|
55
65
|
});
|
|
56
|
-
|
|
57
|
-
|
|
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 {};
|
package/dist/core/admin.js
CHANGED
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). */
|