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
@@ -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 deadline = Date.now() + timeout;
108
- for (;;) {
109
- const to = await hs.streams.position();
110
- const { response, empty, next } = await buildResponse(hs, s, body, conn, since, to);
111
- const remaining = deadline - Date.now();
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
- for (const chunk of chunks(joined)) {
341
- queries.push({
342
- model: "events",
343
- opts: {
344
- where: [inRooms(chunk), ...range],
345
- fields: ["room_id"]
346
- }
347
- });
348
- queries.push({
349
- model: "receipts",
350
- opts: { where: [inRooms(chunk), ...range] }
351
- });
352
- queries.push({
353
- model: "partial_state_rooms",
354
- opts: {
355
- where: [inRooms(chunk), {
356
- field: "completed_stream",
357
- operator: "gt",
358
- value: since
359
- }],
360
- fields: ["room_id"]
361
- }
362
- });
363
- }
364
- for (const chunk of chunks(anyMembership)) queries.push({
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 joinedChunks = Math.ceil(joined.length / 500);
376
- for (let c = 0; c < joinedChunks; c++) {
377
- const [events, receiptRows, completed] = results.slice(c * 3, c * 3 + 3);
378
- for (const r of events) touched.add(r.room_id);
379
- for (const r of receiptRows) {
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
- for (const r of completed) touched.add(r.room_id);
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 [memberships, ignoredRows, toDeviceRows, oneTimeKeyRows, fallbackRows, accountRows = []] = await readBatch(hs.db, [
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
- model: "account_data",
441
- opts: { where: [
442
- {
439
+ query: {
440
+ model: "memberships",
441
+ opts: { where: [{
443
442
  field: "user_id",
444
443
  value: userId
445
- },
446
- {
447
- field: "room_id",
448
- value: ""
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
- model: "to_device",
458
- opts: {
459
- where: [...deviceWhere, {
460
- field: "stream",
461
- operator: "gt",
462
- value: p.since?.toDevice ?? 0
463
- }],
464
- sortBy: {
465
- field: "stream",
466
- direction: "asc"
467
- },
468
- limit: 100
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
- model: "one_time_keys",
473
- opts: {
474
- where: [...deviceWhere, {
475
- field: "fallback",
476
- value: false
477
- }],
478
- fields: ["algorithm"]
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
- model: "fallback_keys",
483
- opts: {
484
- where: [...deviceWhere, {
485
- field: "used",
486
- value: false
487
- }],
488
- fields: ["algorithm"]
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
- model: "account_data",
493
- opts: { where: [{
494
- field: "user_id",
495
- value: userId
496
- }, {
497
- field: "stream",
498
- operator: "gt",
499
- value: since
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 readBatch(hs.db, [
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 ? [recent("device_list_changes")] : []
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 deadline = Date.now() + timeout;
749
- for (;;) {
750
- const to = await hs.streams.position();
751
- const ignored = /* @__PURE__ */ new Set();
752
- const stable = ctx.query.get("use_state_after") === "true";
753
- const unstable = ctx.query.get(UNSTABLE.ORG.MATRIX.MSC4222.USE_STATE_AFTER) === "true";
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
- const remaining = deadline - Date.now();
767
- if (!since || !empty || remaining <= 0 || fullState) return body;
768
- if (await hs.streams.position() > to) continue;
769
- await hs.notifier.wait(session.userId, remaining, ctx.request.signal);
770
- if (ctx.request.signal?.aborted) return body;
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 row = await hs.db.findOne("presence", [{
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) => hs.db.upsert("presence", {
788
- user_id: userId,
789
- presence: state,
790
- status_msg: row?.status_msg ?? null,
791
- last_active_ts: Date.now(),
792
- stream
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) {
@@ -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
- 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 {};
@@ -301,6 +301,7 @@ var AdminService = class {
301
301
  content: { tags: { [M.SERVER_NOTICE]: { order: .5 } } },
302
302
  stream
303
303
  }));
304
+ this.hs.notifier.notify([userId]);
304
305
  return room_id;
305
306
  }
306
307
  };
@@ -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). */