matrix-js-sdk 41.4.0 → 41.5.0
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/CHANGELOG.md +12 -0
- package/README.md +1 -0
- package/lib/@types/json.d.ts +7 -0
- package/lib/@types/json.d.ts.map +1 -0
- package/lib/@types/json.js +1 -0
- package/lib/@types/json.js.map +1 -0
- package/lib/@types/requests.d.ts +6 -9
- package/lib/@types/requests.d.ts.map +1 -1
- package/lib/@types/requests.js.map +1 -1
- package/lib/client.d.ts +17 -2
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +27 -12
- package/lib/client.js.map +1 -1
- package/lib/filter.d.ts +20 -5
- package/lib/filter.d.ts.map +1 -1
- package/lib/filter.js +21 -0
- package/lib/filter.js.map +1 -1
- package/lib/models/user.d.ts +5 -0
- package/lib/models/user.d.ts.map +1 -1
- package/lib/models/user.js +5 -0
- package/lib/models/user.js.map +1 -1
- package/lib/oidc/authorize.d.ts +60 -0
- package/lib/oidc/authorize.d.ts.map +1 -1
- package/lib/oidc/authorize.js +115 -2
- package/lib/oidc/authorize.js.map +1 -1
- package/lib/oidc/register.d.ts.map +1 -1
- package/lib/oidc/register.js +5 -0
- package/lib/oidc/register.js.map +1 -1
- package/lib/rendezvous/MSC4108SignInWithQR.d.ts +19 -2
- package/lib/rendezvous/MSC4108SignInWithQR.d.ts.map +1 -1
- package/lib/rendezvous/MSC4108SignInWithQR.js +126 -36
- package/lib/rendezvous/MSC4108SignInWithQR.js.map +1 -1
- package/lib/rendezvous/channels/MSC4108SecureChannel.d.ts.map +1 -1
- package/lib/rendezvous/channels/MSC4108SecureChannel.js +4 -2
- package/lib/rendezvous/channels/MSC4108SecureChannel.js.map +1 -1
- package/lib/rendezvous/index.d.ts +36 -0
- package/lib/rendezvous/index.d.ts.map +1 -1
- package/lib/rendezvous/index.js +115 -0
- package/lib/rendezvous/index.js.map +1 -1
- package/lib/rendezvous/transports/MSC4108RendezvousSession.d.ts +1 -1
- package/lib/rendezvous/transports/MSC4108RendezvousSession.d.ts.map +1 -1
- package/lib/rendezvous/transports/MSC4108RendezvousSession.js.map +1 -1
- package/lib/rust-crypto/rust-crypto.d.ts.map +1 -1
- package/lib/rust-crypto/rust-crypto.js +2 -2
- package/lib/rust-crypto/rust-crypto.js.map +1 -1
- package/lib/store/index.d.ts +17 -1
- package/lib/store/index.d.ts.map +1 -1
- package/lib/store/index.js.map +1 -1
- package/lib/store/indexeddb-backend.d.ts +4 -0
- package/lib/store/indexeddb-backend.d.ts.map +1 -1
- package/lib/store/indexeddb-backend.js.map +1 -1
- package/lib/store/indexeddb-local-backend.d.ts +4 -1
- package/lib/store/indexeddb-local-backend.d.ts.map +1 -1
- package/lib/store/indexeddb-local-backend.js +45 -3
- package/lib/store/indexeddb-local-backend.js.map +1 -1
- package/lib/store/indexeddb-remote-backend.d.ts +4 -0
- package/lib/store/indexeddb-remote-backend.d.ts.map +1 -1
- package/lib/store/indexeddb-remote-backend.js +21 -3
- package/lib/store/indexeddb-remote-backend.js.map +1 -1
- package/lib/store/indexeddb-store-worker.d.ts.map +1 -1
- package/lib/store/indexeddb-store-worker.js +10 -1
- package/lib/store/indexeddb-store-worker.js.map +1 -1
- package/lib/store/indexeddb.d.ts +4 -0
- package/lib/store/indexeddb.d.ts.map +1 -1
- package/lib/store/indexeddb.js +18 -0
- package/lib/store/indexeddb.js.map +1 -1
- package/lib/store/memory.d.ts +5 -1
- package/lib/store/memory.d.ts.map +1 -1
- package/lib/store/memory.js +19 -0
- package/lib/store/memory.js.map +1 -1
- package/lib/store/stub.d.ts +3 -0
- package/lib/store/stub.d.ts.map +1 -1
- package/lib/store/stub.js +15 -0
- package/lib/store/stub.js.map +1 -1
- package/lib/sync-accumulator.d.ts +15 -0
- package/lib/sync-accumulator.d.ts.map +1 -1
- package/lib/sync-accumulator.js +4 -0
- package/lib/sync-accumulator.js.map +1 -1
- package/lib/sync.d.ts +9 -1
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +51 -9
- package/lib/sync.js.map +1 -1
- package/lib/webrtc/call.d.ts.map +1 -1
- package/lib/webrtc/call.js +1 -2
- package/lib/webrtc/call.js.map +1 -1
- package/package.json +7 -7
- package/src/@types/json.ts +16 -0
- package/src/@types/requests.ts +6 -9
- package/src/client.ts +40 -12
- package/src/filter.ts +31 -5
- package/src/models/user.ts +6 -0
- package/src/oidc/authorize.ts +135 -2
- package/src/oidc/register.ts +5 -0
- package/src/rendezvous/MSC4108SignInWithQR.ts +117 -4
- package/src/rendezvous/channels/MSC4108SecureChannel.ts +10 -2
- package/src/rendezvous/index.ts +115 -0
- package/src/rendezvous/transports/MSC4108RendezvousSession.ts +1 -1
- package/src/rust-crypto/rust-crypto.ts +6 -3
- package/src/store/index.ts +20 -1
- package/src/store/indexeddb-backend.ts +4 -0
- package/src/store/indexeddb-local-backend.ts +32 -1
- package/src/store/indexeddb-remote-backend.ts +13 -0
- package/src/store/indexeddb-store-worker.ts +9 -0
- package/src/store/indexeddb.ts +13 -0
- package/src/store/memory.ts +14 -1
- package/src/store/stub.ts +12 -0
- package/src/sync-accumulator.ts +16 -1
- package/src/sync.ts +48 -4
- package/src/webrtc/call.ts +1 -2
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matrix-js-sdk",
|
|
3
|
-
"version": "41.
|
|
3
|
+
"version": "41.5.0",
|
|
4
4
|
"description": "Matrix Client-Server SDK for Javascript",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"prepare": "pnpm build",
|
|
10
|
-
"start": "
|
|
10
|
+
"start": "concurrently 'pnpm run build:compile --watch' 'pnpm run build:types --watch'",
|
|
11
11
|
"build": "pnpm build:compile && pnpm build:types",
|
|
12
12
|
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
|
13
13
|
"build:compile": "babel --delete-dir-on-start -d lib --verbose --extensions \".ts,.js\" src",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@babel/runtime": "^7.12.5",
|
|
51
|
-
"@matrix-org/matrix-sdk-crypto-wasm": "^18.
|
|
51
|
+
"@matrix-org/matrix-sdk-crypto-wasm": "^18.2.0",
|
|
52
52
|
"another-json": "^0.2.0",
|
|
53
53
|
"bs58": "^6.0.0",
|
|
54
54
|
"content-type": "^1.0.4",
|
|
@@ -59,8 +59,7 @@
|
|
|
59
59
|
"oidc-client-ts": "^3.0.1",
|
|
60
60
|
"p-retry": "8",
|
|
61
61
|
"sdp-transform": "^3.0.0",
|
|
62
|
-
"unhomoglyph": "^1.0.6"
|
|
63
|
-
"uuid": "13"
|
|
62
|
+
"unhomoglyph": "^1.0.6"
|
|
64
63
|
},
|
|
65
64
|
"devDependencies": {
|
|
66
65
|
"@action-validator/cli": "^0.6.0",
|
|
@@ -91,6 +90,7 @@
|
|
|
91
90
|
"@vitest/eslint-plugin": "^1.6.6",
|
|
92
91
|
"@vitest/ui": "^4.0.17",
|
|
93
92
|
"babel-plugin-search-and-replace": "^1.1.1",
|
|
93
|
+
"concurrently": "^9.2.1",
|
|
94
94
|
"debug": "^4.3.4",
|
|
95
95
|
"eslint": "8.57.1",
|
|
96
96
|
"eslint-config-google": "^0.14.0",
|
|
@@ -132,8 +132,8 @@
|
|
|
132
132
|
"flatted@<=3.4.1": "^3.4.2",
|
|
133
133
|
"picomatch@>=4.0.0 <4.0.4": "^4.0.4",
|
|
134
134
|
"yaml@>=2.0.0 <2.8.3": "^2.8.3",
|
|
135
|
-
"vite": "
|
|
135
|
+
"vite": "8.0.8"
|
|
136
136
|
}
|
|
137
137
|
},
|
|
138
|
-
"packageManager": "pnpm@10.33.
|
|
138
|
+
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8"
|
|
139
139
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 New Vector Ltd.
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
5
|
+
Please see LICENSE files in the repository root for full details.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Types for JSON and JSON objects, copied from element-web (left in both places as I don't think we
|
|
9
|
+
// want this as a part of js-sdk's interface and I don't think it's worth it being its own package)
|
|
10
|
+
|
|
11
|
+
export type JsonValue = null | string | number | boolean;
|
|
12
|
+
export type JsonArray = Array<JsonValue | JsonObject | JsonArray>;
|
|
13
|
+
export interface JsonObject {
|
|
14
|
+
[key: string]: JsonObject | JsonArray | JsonValue;
|
|
15
|
+
}
|
|
16
|
+
export type Json = JsonArray | JsonObject;
|
package/src/@types/requests.ts
CHANGED
|
@@ -40,10 +40,10 @@ export interface IJoinRoomOpts {
|
|
|
40
40
|
viaServers?: string[];
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* Previously, configured whether to accept encrypted history shared by the inviter. This is now always enabled,
|
|
44
|
+
* and the setting is only retained to avoid a breaking change to the API. It has no effect.
|
|
45
45
|
*
|
|
46
|
-
* @
|
|
46
|
+
* @deprecated
|
|
47
47
|
*/
|
|
48
48
|
acceptSharedHistory?: boolean;
|
|
49
49
|
}
|
|
@@ -56,13 +56,10 @@ export interface InviteOpts {
|
|
|
56
56
|
reason?: string;
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* support for [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268). If the room's current
|
|
62
|
-
* history visibility setting is neither `shared` nor `world_readable`, history sharing will be disabled to prevent
|
|
63
|
-
* exposing keys for messages sent prior to the visibility restriction.
|
|
59
|
+
* Previously, configured whether to send encrypted history if the visibility settings allow it.
|
|
60
|
+
* This is now always enabled, and the setting is only retained to avoid a breaking change to the API. It has no effect.
|
|
64
61
|
*
|
|
65
|
-
* @
|
|
62
|
+
* @deprecated
|
|
66
63
|
*/
|
|
67
64
|
shareEncryptedHistory?: boolean;
|
|
68
65
|
}
|
package/src/client.ts
CHANGED
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
type UploadOpts,
|
|
79
79
|
type UploadResponse,
|
|
80
80
|
} from "./http-api/index.ts";
|
|
81
|
-
import { User, UserEvent, type UserEventHandlerMap } from "./models/user.ts";
|
|
81
|
+
import { type SyncUserProfile, User, UserEvent, type UserEventHandlerMap } from "./models/user.ts";
|
|
82
82
|
import { getHttpUriForMxc } from "./content-repo.ts";
|
|
83
83
|
import { SearchResult } from "./models/search-result.ts";
|
|
84
84
|
import { type IIdentityServerProvider } from "./@types/IIdentityServerProvider.ts";
|
|
@@ -539,6 +539,12 @@ export interface IStartClientOpts {
|
|
|
539
539
|
* @experimental
|
|
540
540
|
*/
|
|
541
541
|
slidingSync?: SlidingSync;
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Include user profiles in sync responses.
|
|
545
|
+
* Will only work if the server supports MSC4429.
|
|
546
|
+
*/
|
|
547
|
+
unstableMSC4429SyncUserProfileFields?: string[];
|
|
542
548
|
}
|
|
543
549
|
|
|
544
550
|
export interface IStoredClientOpts extends IStartClientOpts {}
|
|
@@ -1102,6 +1108,7 @@ export enum ClientEvent {
|
|
|
1102
1108
|
ReceivedVoipEvent = "received_voip_event",
|
|
1103
1109
|
TurnServers = "turnServers",
|
|
1104
1110
|
TurnServersError = "turnServers.error",
|
|
1111
|
+
UserProfileUpdate = "userProfileUpdate",
|
|
1105
1112
|
}
|
|
1106
1113
|
|
|
1107
1114
|
type RoomEvents =
|
|
@@ -1172,6 +1179,12 @@ export type ClientEventHandlerMap = {
|
|
|
1172
1179
|
[ClientEvent.ReceivedVoipEvent]: (event: MatrixEvent) => void;
|
|
1173
1180
|
[ClientEvent.TurnServers]: (servers: ITurnServer[]) => void;
|
|
1174
1181
|
[ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void;
|
|
1182
|
+
/**
|
|
1183
|
+
*
|
|
1184
|
+
* @param userId - the user ID of the profile which was updated
|
|
1185
|
+
* @param profile - the updated profile information
|
|
1186
|
+
*/
|
|
1187
|
+
[ClientEvent.UserProfileUpdate]: (userId: string, profile: Record<string, unknown> | null) => void;
|
|
1175
1188
|
} & RoomEventHandlerMap &
|
|
1176
1189
|
RoomStateEventHandlerMap &
|
|
1177
1190
|
CryptoEventHandlerMap &
|
|
@@ -2428,7 +2441,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
2428
2441
|
const res = await this.http.authedRequest<{ room_id: string }>(Method.Post, path, queryParams, data);
|
|
2429
2442
|
|
|
2430
2443
|
const roomId = res.room_id;
|
|
2431
|
-
if (
|
|
2444
|
+
if (inviter && this.cryptoBackend) {
|
|
2432
2445
|
// Flag upfront that we are waiting for a key bundle, so that if we crash mid-import, we can try again.
|
|
2433
2446
|
await this.cryptoBackend.markRoomAsPendingKeyBundle(roomId, inviter);
|
|
2434
2447
|
// Try to accept the room key bundle specified in a `m.room_key_bundle` to-device message we (might have) already received.
|
|
@@ -4086,14 +4099,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
4086
4099
|
opts = { reason: opts };
|
|
4087
4100
|
}
|
|
4088
4101
|
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
await this.cryptoBackend?.shareRoomHistoryWithUser(roomId, userId);
|
|
4096
|
-
}
|
|
4102
|
+
const historyVisibility = this.getRoom(roomId)?.getHistoryVisibility() ?? HistoryVisibility.Shared;
|
|
4103
|
+
// We should only share room history if the *current* visibility allows it.
|
|
4104
|
+
if ([HistoryVisibility.Invited, HistoryVisibility.Joined].includes(historyVisibility)) {
|
|
4105
|
+
this.logger.debug("Not sharing message history as the room history visibility is currently unshared");
|
|
4106
|
+
} else {
|
|
4107
|
+
await this.cryptoBackend?.shareRoomHistoryWithUser(roomId, userId);
|
|
4097
4108
|
}
|
|
4098
4109
|
|
|
4099
4110
|
return await this.membershipChange(roomId, userId, KnownMembership.Invite, opts.reason);
|
|
@@ -7364,6 +7375,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
7364
7375
|
|
|
7365
7376
|
/**
|
|
7366
7377
|
* Fetch a user's *extended* profile, which may include additional keys.
|
|
7378
|
+
* Always returns all available profile fields, irrespective of what profile fields are set
|
|
7379
|
+
* in the sync filter.
|
|
7367
7380
|
*
|
|
7368
7381
|
* @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md
|
|
7369
7382
|
* @param userId The user ID to fetch the profile of.
|
|
@@ -7376,6 +7389,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
7376
7389
|
if (!(await this.doesServerSupportExtendedProfiles())) {
|
|
7377
7390
|
throw new Error("Server does not support extended profiles");
|
|
7378
7391
|
}
|
|
7392
|
+
|
|
7393
|
+
// Note that this does not look at the profile cache as this will only contain keys
|
|
7394
|
+
// that we included in the sync filter and this function's purpose is to return the whole profile.
|
|
7395
|
+
|
|
7379
7396
|
return this.http.authedRequest(
|
|
7380
7397
|
Method.Get,
|
|
7381
7398
|
utils.encodeUri("/profile/$userId", { $userId: userId }),
|
|
@@ -7388,7 +7405,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
7388
7405
|
}
|
|
7389
7406
|
|
|
7390
7407
|
/**
|
|
7391
|
-
* Fetch a specific key from the user's *extended* profile
|
|
7408
|
+
* Fetch a specific key from the user's *extended* profile by checking local cache (which is updated from
|
|
7409
|
+
* the sync) and querying the server if no data is cached locally.
|
|
7392
7410
|
*
|
|
7393
7411
|
* @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md
|
|
7394
7412
|
* @param userId The user ID to fetch the profile of.
|
|
@@ -7402,6 +7420,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
7402
7420
|
if (!(await this.doesServerSupportExtendedProfiles())) {
|
|
7403
7421
|
throw new Error("Server does not support extended profiles");
|
|
7404
7422
|
}
|
|
7423
|
+
// NOTE: We only read individual keys from a cached profile as we don't have the full profile
|
|
7424
|
+
// cached, only the keys that the user has configured via their sync filter.
|
|
7425
|
+
const storedProfile = await this.store.getUserProfile(userId);
|
|
7426
|
+
if (storedProfile?.[key] !== undefined) {
|
|
7427
|
+
return storedProfile[key];
|
|
7428
|
+
}
|
|
7405
7429
|
const profile = (await this.http.authedRequest(
|
|
7406
7430
|
Method.Get,
|
|
7407
7431
|
utils.encodeUri("/profile/$userId/$key", { $userId: userId, $key: key }),
|
|
@@ -7410,7 +7434,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|
|
7410
7434
|
{
|
|
7411
7435
|
prefix: await this.getExtendedProfileRequestPrefix(),
|
|
7412
7436
|
},
|
|
7413
|
-
)) as
|
|
7437
|
+
)) as SyncUserProfile;
|
|
7438
|
+
|
|
7439
|
+
// write through to the cache
|
|
7440
|
+
await this.store.storeUserProfiles(new Map([[userId, profile]]));
|
|
7441
|
+
|
|
7414
7442
|
return profile[key];
|
|
7415
7443
|
}
|
|
7416
7444
|
|
package/src/filter.ts
CHANGED
|
@@ -18,6 +18,9 @@ import { type EventType, type RelationType } from "./@types/event.ts";
|
|
|
18
18
|
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync.ts";
|
|
19
19
|
import { FilterComponent, type IFilterComponent } from "./filter-component.ts";
|
|
20
20
|
import { type MatrixEvent } from "./models/event.ts";
|
|
21
|
+
import { NamespacedValue } from "./NamespacedValue.ts";
|
|
22
|
+
|
|
23
|
+
const profileFieldsFilterName = new NamespacedValue("profile_fields", "org.matrix.msc4429.profile_fields");
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
*/
|
|
@@ -35,11 +38,13 @@ function setProp(obj: Record<string, any>, keyNesting: string, val: any): void {
|
|
|
35
38
|
|
|
36
39
|
/* eslint-disable camelcase */
|
|
37
40
|
export interface IFilterDefinition {
|
|
38
|
-
event_fields?: string[];
|
|
39
|
-
event_format?: "client" | "federation";
|
|
40
|
-
presence?: IFilterComponent;
|
|
41
|
-
account_data?: IFilterComponent;
|
|
42
|
-
room?: IRoomFilter;
|
|
41
|
+
"event_fields"?: string[];
|
|
42
|
+
"event_format"?: "client" | "federation";
|
|
43
|
+
"presence"?: IFilterComponent;
|
|
44
|
+
"account_data"?: IFilterComponent;
|
|
45
|
+
"room"?: IRoomFilter;
|
|
46
|
+
"profile_fields"?: ProfileFieldsFilter;
|
|
47
|
+
"org.matrix.msc4429.profile_fields"?: ProfileFieldsFilter;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export interface IRoomEventFilter extends IFilterComponent {
|
|
@@ -67,6 +72,14 @@ interface IRoomFilter {
|
|
|
67
72
|
timeline?: IRoomEventFilter;
|
|
68
73
|
account_data?: IRoomEventFilter;
|
|
69
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Filter section used for requesting a set of extended profile fields that will be sent down the sync stream.
|
|
78
|
+
*/
|
|
79
|
+
interface ProfileFieldsFilter {
|
|
80
|
+
ids: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
/* eslint-enable camelcase */
|
|
71
84
|
|
|
72
85
|
export class Filter {
|
|
@@ -242,4 +255,17 @@ export class Filter {
|
|
|
242
255
|
public setIncludeLeaveRooms(includeLeave: boolean): void {
|
|
243
256
|
setProp(this.definition, "room.include_leave", includeLeave);
|
|
244
257
|
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Set the list of fields to be included in the profile information sent down the sync stream.
|
|
261
|
+
* @param ids The field IDs to sync.
|
|
262
|
+
* @param stable Whether to use the stable or unstable versions of this filter.
|
|
263
|
+
* @experimental
|
|
264
|
+
*/
|
|
265
|
+
public setUnstableMSC4429SyncUserProfiles(ids: string[], stable: boolean): void {
|
|
266
|
+
const field = stable
|
|
267
|
+
? profileFieldsFilterName.name
|
|
268
|
+
: (profileFieldsFilterName.unstable ?? profileFieldsFilterName.name);
|
|
269
|
+
this.definition[field] = { ids };
|
|
270
|
+
}
|
|
245
271
|
}
|
package/src/models/user.ts
CHANGED
|
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { type Json, type JsonValue } from "../@types/json.ts";
|
|
17
18
|
import { type MatrixClient } from "../matrix.ts";
|
|
18
19
|
import { type MatrixEvent } from "./event.ts";
|
|
19
20
|
import { TypedEventEmitter } from "./typed-event-emitter.ts";
|
|
@@ -26,6 +27,11 @@ export enum UserEvent {
|
|
|
26
27
|
LastPresenceTs = "User.lastPresenceTs",
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* An object of extended profile attributes for a user as it arrives down the sync stream.
|
|
32
|
+
*/
|
|
33
|
+
export type SyncUserProfile = Record<string, Json | JsonValue>;
|
|
34
|
+
|
|
29
35
|
export type UserEventHandlerMap = {
|
|
30
36
|
/**
|
|
31
37
|
* Fires whenever any user's display name changes.
|
package/src/oidc/authorize.ts
CHANGED
|
@@ -37,6 +37,9 @@ import {
|
|
|
37
37
|
} from "./validate.ts";
|
|
38
38
|
import { sha256 } from "../digest.ts";
|
|
39
39
|
import { encodeUnpaddedBase64Url } from "../base64.ts";
|
|
40
|
+
import { OAuthGrantType } from "./register.ts";
|
|
41
|
+
import { sleep } from "../utils.ts";
|
|
42
|
+
import { Method } from "../http-api/index.ts";
|
|
40
43
|
|
|
41
44
|
// reexport for backwards compatibility
|
|
42
45
|
export type { BearerTokenResponse };
|
|
@@ -271,8 +274,16 @@ export const completeAuthorizationCodeGrant = async (
|
|
|
271
274
|
|
|
272
275
|
// throws when response is invalid
|
|
273
276
|
validateBearerTokenResponse(signinResponse);
|
|
274
|
-
|
|
275
|
-
|
|
277
|
+
if (signinResponse.id_token) {
|
|
278
|
+
// The token is not yet in the Matrix spec so consider it optional
|
|
279
|
+
// throws when token is invalid
|
|
280
|
+
validateIdToken(
|
|
281
|
+
signinResponse.id_token,
|
|
282
|
+
client.settings.authority,
|
|
283
|
+
client.settings.client_id,
|
|
284
|
+
userState.nonce,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
276
287
|
const normalizedTokenResponse = normalizeBearerTokenResponseTokenType(signinResponse);
|
|
277
288
|
|
|
278
289
|
return {
|
|
@@ -296,3 +307,125 @@ export const completeAuthorizationCodeGrant = async (
|
|
|
296
307
|
throw new Error(OidcError.CodeExchangeFailed);
|
|
297
308
|
}
|
|
298
309
|
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Response from the OIDC token endpoint when exchanging a token for grant_type device_code.
|
|
313
|
+
*/
|
|
314
|
+
export interface DeviceAccessTokenResponse {
|
|
315
|
+
id_token?: string;
|
|
316
|
+
access_token: string;
|
|
317
|
+
token_type: string;
|
|
318
|
+
refresh_token?: string;
|
|
319
|
+
scope?: string;
|
|
320
|
+
expires_in?: number;
|
|
321
|
+
session_state?: string;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Error from the OIDC token endpoint when exchanging a token for grant_type device_code.
|
|
326
|
+
*/
|
|
327
|
+
export interface DeviceAccessTokenError {
|
|
328
|
+
error: string;
|
|
329
|
+
error_description?: string;
|
|
330
|
+
error_uri?: string;
|
|
331
|
+
session_state?: string;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Response from the OIDC device authorization endpoint.
|
|
336
|
+
*/
|
|
337
|
+
export interface DeviceAuthorizationResponse {
|
|
338
|
+
device_code: string;
|
|
339
|
+
user_code: string;
|
|
340
|
+
verification_uri: string;
|
|
341
|
+
verification_uri_complete?: string;
|
|
342
|
+
expires_in: number;
|
|
343
|
+
interval?: number;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Begin OIDC device authorization flow.
|
|
348
|
+
* @param options - The device authorization parameters.
|
|
349
|
+
* @param options.clientId - the client ID returned from client registration.
|
|
350
|
+
* @param options.scope - the scope to request for authorization.
|
|
351
|
+
* @param options.metadata - the validated OIDC metadata for the Identity Provider.
|
|
352
|
+
* @returns a promise that resolves to a device access token response,
|
|
353
|
+
* or an error response if the user denies authorization or the device code expires.
|
|
354
|
+
*/
|
|
355
|
+
export const startDeviceAuthorization = async ({
|
|
356
|
+
clientId,
|
|
357
|
+
scope,
|
|
358
|
+
metadata,
|
|
359
|
+
}: {
|
|
360
|
+
clientId: string;
|
|
361
|
+
scope: string;
|
|
362
|
+
metadata: ValidatedAuthMetadata;
|
|
363
|
+
}): Promise<DeviceAuthorizationResponse> => {
|
|
364
|
+
const body = new URLSearchParams({ client_id: clientId, scope: scope }).toString();
|
|
365
|
+
|
|
366
|
+
const url = metadata.device_authorization_endpoint;
|
|
367
|
+
if (!url) {
|
|
368
|
+
throw new Error("No device_authorization_endpoint given");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const response = await fetch(url, {
|
|
372
|
+
method: Method.Post,
|
|
373
|
+
headers: {
|
|
374
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
375
|
+
},
|
|
376
|
+
body,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return (await response.json()) as DeviceAuthorizationResponse;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Polls the OIDC token endpoint until we get a device access token response, or encounter an unrecoverable error.
|
|
384
|
+
* @param options - The device authorization parameters.
|
|
385
|
+
* @param options.session - The session returned from a previous call to {@link startDeviceAuthorization}.
|
|
386
|
+
* @param options.metadata - The validated OIDC metadata for the Identity Provider.
|
|
387
|
+
* @param options.clientId - The client ID returned from client registration.
|
|
388
|
+
* @returns a promise that resolves to a device access token response,
|
|
389
|
+
* or an error response if the user denies authorization or the device code expires.
|
|
390
|
+
*/
|
|
391
|
+
export const waitForDeviceAuthorization = async ({
|
|
392
|
+
session,
|
|
393
|
+
metadata,
|
|
394
|
+
clientId,
|
|
395
|
+
}: {
|
|
396
|
+
session: DeviceAuthorizationResponse;
|
|
397
|
+
metadata: ValidatedAuthMetadata;
|
|
398
|
+
clientId: string;
|
|
399
|
+
}): Promise<DeviceAccessTokenResponse | DeviceAccessTokenError> => {
|
|
400
|
+
let interval = (session.interval ?? 5) * 1000; // poll interval
|
|
401
|
+
const expiration = Date.now() + session.expires_in * 1000;
|
|
402
|
+
do {
|
|
403
|
+
const body = new URLSearchParams({
|
|
404
|
+
device_code: session.device_code,
|
|
405
|
+
grant_type: OAuthGrantType.DeviceAuthorization,
|
|
406
|
+
client_id: clientId,
|
|
407
|
+
}).toString();
|
|
408
|
+
const response = await fetch(metadata.token_endpoint, {
|
|
409
|
+
method: Method.Post,
|
|
410
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
411
|
+
body,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (response.ok) {
|
|
415
|
+
return (await response.json()) as DeviceAccessTokenResponse;
|
|
416
|
+
}
|
|
417
|
+
const errorResponse = (await response.json()) as DeviceAccessTokenError;
|
|
418
|
+
switch (errorResponse.error) {
|
|
419
|
+
case "authorization_pending":
|
|
420
|
+
break;
|
|
421
|
+
case "slow_down":
|
|
422
|
+
interval += 5000;
|
|
423
|
+
break;
|
|
424
|
+
case "access_denied":
|
|
425
|
+
case "expired_token":
|
|
426
|
+
return errorResponse;
|
|
427
|
+
}
|
|
428
|
+
await sleep(interval);
|
|
429
|
+
} while (Date.now() < expiration);
|
|
430
|
+
return { error: "expired" };
|
|
431
|
+
};
|
package/src/oidc/register.ts
CHANGED
|
@@ -111,6 +111,11 @@ export const registerOidcClient = async (
|
|
|
111
111
|
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// ask for device authorization grant if supported
|
|
115
|
+
if (delegatedAuthConfig.grant_types_supported.includes(OAuthGrantType.DeviceAuthorization)) {
|
|
116
|
+
grantTypes.push(OAuthGrantType.DeviceAuthorization);
|
|
117
|
+
}
|
|
118
|
+
|
|
114
119
|
const commonBase = new URL(clientMetadata.clientUri);
|
|
115
120
|
|
|
116
121
|
// https://openid.net/specs/openid-connect-registration-1_0.html
|
|
@@ -27,7 +27,16 @@ import { logger } from "../logger.ts";
|
|
|
27
27
|
import { type MSC4108SecureChannel } from "./channels/MSC4108SecureChannel.ts";
|
|
28
28
|
import { MatrixError } from "../http-api/index.ts";
|
|
29
29
|
import { sleep } from "../utils.ts";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
type DeviceAccessTokenResponse,
|
|
32
|
+
type DeviceAuthorizationResponse,
|
|
33
|
+
generateScope,
|
|
34
|
+
OAuthGrantType,
|
|
35
|
+
startDeviceAuthorization,
|
|
36
|
+
type ValidatedAuthMetadata,
|
|
37
|
+
waitForDeviceAuthorization,
|
|
38
|
+
type OidcClientConfig,
|
|
39
|
+
} from "../oidc/index.ts";
|
|
31
40
|
import { type CryptoApi } from "../crypto-api/index.ts";
|
|
32
41
|
|
|
33
42
|
/**
|
|
@@ -111,6 +120,8 @@ export class MSC4108SignInWithQR {
|
|
|
111
120
|
private readonly ourIntent: QrCodeIntent;
|
|
112
121
|
private _code?: Uint8Array;
|
|
113
122
|
private expectingNewDeviceId?: string;
|
|
123
|
+
private metadata?: ValidatedAuthMetadata;
|
|
124
|
+
private grantInProgress?: DeviceAuthorizationResponse;
|
|
114
125
|
|
|
115
126
|
/**
|
|
116
127
|
* Returns the check code for the secure channel or undefined if not generated yet.
|
|
@@ -241,14 +252,53 @@ export class MSC4108SignInWithQR {
|
|
|
241
252
|
/**
|
|
242
253
|
* The second & third step in the OIDC QR login process.
|
|
243
254
|
* To be called after `negotiateProtocols` for the existing device.
|
|
244
|
-
* To be called after OIDC negotiation for the new device.
|
|
255
|
+
* To be called after OIDC negotiation for the new device.
|
|
256
|
+
*
|
|
257
|
+
* @param input - Required for the new device to start the device authorization grant, not required for the existing device reciprocating the login
|
|
245
258
|
*/
|
|
246
|
-
public async deviceAuthorizationGrant(
|
|
259
|
+
public async deviceAuthorizationGrant(input?: {
|
|
260
|
+
metadata: ValidatedAuthMetadata;
|
|
261
|
+
clientId: string;
|
|
262
|
+
deviceId: string;
|
|
263
|
+
}): Promise<{
|
|
247
264
|
verificationUri?: string;
|
|
248
265
|
userCode?: string;
|
|
249
266
|
}> {
|
|
250
267
|
if (this.isNewDevice) {
|
|
251
|
-
|
|
268
|
+
if (!input) {
|
|
269
|
+
throw new Error("Input must be provided for new device");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const { metadata, clientId, deviceId } = input;
|
|
273
|
+
|
|
274
|
+
const scope = generateScope(deviceId);
|
|
275
|
+
|
|
276
|
+
// MSC4108-Flow: NewDevice - start device authorization grant
|
|
277
|
+
const dagResponse = await startDeviceAuthorization({
|
|
278
|
+
clientId,
|
|
279
|
+
scope,
|
|
280
|
+
metadata,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
this.metadata = metadata;
|
|
284
|
+
this.grantInProgress = dagResponse;
|
|
285
|
+
|
|
286
|
+
const protocol: DeviceAuthorizationGrantProtocolPayload = {
|
|
287
|
+
type: PayloadType.Protocol,
|
|
288
|
+
protocol: "device_authorization_grant",
|
|
289
|
+
device_id: deviceId,
|
|
290
|
+
device_authorization_grant: {
|
|
291
|
+
verification_uri: dagResponse.verification_uri,
|
|
292
|
+
verification_uri_complete: dagResponse.verification_uri_complete,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
await this.send(protocol);
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
verificationUri: dagResponse.verification_uri_complete ?? dagResponse.verification_uri,
|
|
300
|
+
userCode: dagResponse.user_code,
|
|
301
|
+
};
|
|
252
302
|
} else {
|
|
253
303
|
// The user needs to do step 7 for the out-of-band confirmation
|
|
254
304
|
// but, first we receive the protocol chosen by the other device so that
|
|
@@ -311,6 +361,69 @@ export class MSC4108SignInWithQR {
|
|
|
311
361
|
}
|
|
312
362
|
}
|
|
313
363
|
|
|
364
|
+
/**
|
|
365
|
+
* The fourth step in the OIDC QR login process.
|
|
366
|
+
* The reciprocating device must perform step 5 for this method to resolve.
|
|
367
|
+
* To be called after {@link deviceAuthorizationGrant} only on the new device.
|
|
368
|
+
*/
|
|
369
|
+
public async completeLoginOnNewDevice({
|
|
370
|
+
clientId,
|
|
371
|
+
}: {
|
|
372
|
+
clientId: string;
|
|
373
|
+
}): Promise<DeviceAccessTokenResponse | undefined> {
|
|
374
|
+
if (!this.isNewDevice || !this.grantInProgress || !this.metadata) {
|
|
375
|
+
throw new Error("Can only complete login on new device");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
logger.info("Waiting for protocol accepted message");
|
|
379
|
+
// wait for accepted message
|
|
380
|
+
const payload = await this.receive<AcceptedPayload | FailurePayload>();
|
|
381
|
+
|
|
382
|
+
if (!payload) {
|
|
383
|
+
throw new RendezvousError(
|
|
384
|
+
"No response from existing device",
|
|
385
|
+
MSC4108FailureReason.UnexpectedMessageReceived,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (payload.type === PayloadType.Failure) {
|
|
389
|
+
throw new RendezvousError("Failed", (payload as FailurePayload).reason);
|
|
390
|
+
}
|
|
391
|
+
if (payload.type !== PayloadType.ProtocolAccepted) {
|
|
392
|
+
throw new RendezvousError("Unexpected message received", MSC4108FailureReason.UnexpectedMessageReceived);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// poll for DAG
|
|
396
|
+
const res = await waitForDeviceAuthorization({
|
|
397
|
+
session: this.grantInProgress,
|
|
398
|
+
metadata: this.metadata,
|
|
399
|
+
clientId,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
if (!res) {
|
|
403
|
+
throw new RendezvousError(
|
|
404
|
+
"No response from device authorization endpoint",
|
|
405
|
+
ClientRendezvousFailureReason.Unknown,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if ("error" in res) {
|
|
410
|
+
let reason: MSC4108FailureReason = MSC4108FailureReason.UnexpectedMessageReceived;
|
|
411
|
+
if (res.error === "expired_token") {
|
|
412
|
+
reason = MSC4108FailureReason.AuthorizationExpired;
|
|
413
|
+
} else if (res.error === "access_denied") {
|
|
414
|
+
reason = MSC4108FailureReason.UserCancelled;
|
|
415
|
+
}
|
|
416
|
+
const payload: FailurePayload = {
|
|
417
|
+
type: PayloadType.Failure,
|
|
418
|
+
reason,
|
|
419
|
+
};
|
|
420
|
+
await this.send(payload);
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return res;
|
|
425
|
+
}
|
|
426
|
+
|
|
314
427
|
/**
|
|
315
428
|
* The fifth (and final) step in the OIDC QR login process.
|
|
316
429
|
* To be called after the new device has completed authentication.
|
|
@@ -164,7 +164,10 @@ export class MSC4108SecureChannel {
|
|
|
164
164
|
logger.info("Waiting for LoginInitiateMessage");
|
|
165
165
|
const loginInitiateMessage = await this.rendezvousSession.receive();
|
|
166
166
|
if (!loginInitiateMessage) {
|
|
167
|
-
throw new
|
|
167
|
+
throw new RendezvousError(
|
|
168
|
+
"No response from other device",
|
|
169
|
+
MSC4108FailureReason.UnexpectedMessageReceived,
|
|
170
|
+
);
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
const { channel, message: candidateLoginInitiateMessage } =
|
|
@@ -257,7 +260,12 @@ export class MSC4108SecureChannel {
|
|
|
257
260
|
await this.rendezvousSession.cancel(reason);
|
|
258
261
|
this.onFailure?.(reason);
|
|
259
262
|
} finally {
|
|
260
|
-
|
|
263
|
+
if (
|
|
264
|
+
reason !== ClientRendezvousFailureReason.UserDeclined &&
|
|
265
|
+
reason !== MSC4108FailureReason.UserCancelled
|
|
266
|
+
) {
|
|
267
|
+
await this.close();
|
|
268
|
+
}
|
|
261
269
|
}
|
|
262
270
|
}
|
|
263
271
|
|