Merge branch 'develop' into t3chguy/safari-compat

This commit is contained in:
Michael Telatynski
2025-01-23 09:53:58 +00:00
committed by GitHub
65 changed files with 1775 additions and 1282 deletions

View File

@@ -96,3 +96,4 @@ jobs:
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }} projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
directory: _deploy directory: _deploy
gitHubToken: ${{ secrets.GITHUB_TOKEN }} gitHubToken: ${{ secrets.GITHUB_TOKEN }}
branch: main

View File

@@ -51,7 +51,7 @@ jobs:
- name: Build and push - name: Build and push
id: build-and-push id: build-and-push
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6 uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6
with: with:
context: . context: .
push: true push: true

View File

@@ -104,7 +104,7 @@ jobs:
- name: Skip SonarCloud in merge queue - name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61 uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: success state: success

View File

@@ -8,11 +8,13 @@
#### develop #### develop
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable. It corresponds to the develop.element.io CD platform. The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable.
It is auto-deployed on every commit to element-web or matrix-js-sdk to develop.element.io via GitHub Actions `build_develop.yml`.
#### staging #### staging
The staging branch corresponds to the very latest release regardless of whether it is an RC or not. Deployed to staging.element.io manually. The staging branch corresponds to the very latest release regardless of whether it is an RC or not. Deployed to staging.element.io manually.
It is auto-deployed on every release of element-web to staging.element.io via GitHub Actions `deploy.yml`.
#### master #### master
@@ -215,7 +217,7 @@ We ship Element Web to dockerhub, `*.element.io`, and packages.element.io.
We ship Element Desktop to packages.element.io. We ship Element Desktop to packages.element.io.
- [ ] Check that element-web has shipped to dockerhub - [ ] Check that element-web has shipped to dockerhub
- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) - [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
- [ ] Test staging.element.io - [ ] Test staging.element.io
For final releases additionally do these steps: For final releases additionally do these steps:
@@ -225,6 +227,9 @@ For final releases additionally do these steps:
- [ ] Ensure Element Web package has shipped to packages.element.io - [ ] Ensure Element Web package has shipped to packages.element.io
- [ ] Ensure Element Desktop packages have shipped to packages.element.io - [ ] Ensure Element Desktop packages have shipped to packages.element.io
If you need to roll back a deployment to staging.element.io,
you can run the `deploy.yml` automation choosing an older tag which you wish to deploy.
# Housekeeping # Housekeeping
We have some manual housekeeping to do in order to prepare for the next release. We have some manual housekeeping to do in order to prepare for the next release.

View File

@@ -74,7 +74,7 @@
"@types/react-dom": "18.3.5", "@types/react-dom": "18.3.5",
"oidc-client-ts": "3.1.0", "oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0", "jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001690", "caniuse-lite": "1.0.30001692",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0" "wrap-ansi": "npm:wrap-ansi@^7.0.0"
}, },
@@ -178,7 +178,7 @@
"@peculiar/webcrypto": "^1.4.3", "@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.40.1", "@playwright/test": "^1.40.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7", "@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@sentry/webpack-plugin": "^2.7.1", "@sentry/webpack-plugin": "^3.0.0",
"@stylistic/eslint-plugin": "^2.9.0", "@stylistic/eslint-plugin": "^2.9.0",
"@svgr/webpack": "^8.0.0", "@svgr/webpack": "^8.0.0",
"@testcontainers/postgresql": "^10.16.0", "@testcontainers/postgresql": "^10.16.0",
@@ -230,7 +230,7 @@
"dotenv": "^16.0.2", "dotenv": "^16.0.2",
"eslint": "8.57.1", "eslint": "8.57.1",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^10.0.0",
"eslint-plugin-deprecate": "0.8.5", "eslint-plugin-deprecate": "0.8.5",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^28.0.0", "eslint-plugin-jest": "^28.0.0",
@@ -287,7 +287,7 @@
"terser-webpack-plugin": "^5.3.9", "terser-webpack-plugin": "^5.3.9",
"testcontainers": "^10.16.0", "testcontainers": "^10.16.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "5.7.2", "typescript": "5.7.3",
"util": "^0.12.5", "util": "^0.12.5",
"web-streams-polyfill": "^4.0.0", "web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0", "webpack": "^5.89.0",

View File

@@ -42,7 +42,7 @@ test.describe("Memberlist", () => {
await app.viewRoomByName(ROOM_NAME); await app.viewRoomByName(ROOM_NAME);
const memberlist = await app.toggleMemberlistPanel(); const memberlist = await app.toggleMemberlistPanel();
await expect(memberlist.locator(".mx_MemberTileView")).toHaveCount(4); await expect(memberlist.locator(".mx_MemberTileView")).toHaveCount(4);
await expect(memberlist.getByText("(Invited)")).toHaveCount(1); await expect(memberlist.getByText("Invited")).toHaveCount(1);
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png"); await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
}); });
}); });

View File

@@ -69,11 +69,6 @@ const test = base.extend<{
}); });
test.describe("Sliding Sync", () => { test.describe("Sliding Sync", () => {
test.skip(
({ homeserverType }) => homeserverType === "pinecone",
"due to a bug in Pinecone https://github.com/element-hq/dendrite/issues/3490",
);
const checkOrder = async (wantOrder: string[], page: Page) => { const checkOrder = async (wantOrder: string[], page: Page) => {
await expect(page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile_title")).toHaveText(wantOrder); await expect(page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile_title")).toHaveText(wantOrder);
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -19,7 +19,7 @@ import { HomeserverContainer, StartedHomeserverContainer } from "./HomeserverCon
import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
import { Api, ClientServerApi, Verb } from "../plugins/utils/api.ts"; import { Api, ClientServerApi, Verb } from "../plugins/utils/api.ts";
const TAG = "develop@sha256:e48308d68dec00af6ce43a05785d475de21a37bc2afaabb440d3a575bcc3d57d"; const TAG = "develop@sha256:3594fba0d21ad44f407225baed4be0542da8abcb6e1a7e2e16d3be35c278a7cb";
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
server_name: "localhost", server_name: "localhost",

View File

@@ -283,6 +283,7 @@
@import "./views/rooms/_EventTile.pcss"; @import "./views/rooms/_EventTile.pcss";
@import "./views/rooms/_HistoryTile.pcss"; @import "./views/rooms/_HistoryTile.pcss";
@import "./views/rooms/_IRCLayout.pcss"; @import "./views/rooms/_IRCLayout.pcss";
@import "./views/rooms/_InvitedIconView.pcss";
@import "./views/rooms/_JumpToBottomButton.pcss"; @import "./views/rooms/_JumpToBottomButton.pcss";
@import "./views/rooms/_LinkPreviewGroup.pcss"; @import "./views/rooms/_LinkPreviewGroup.pcss";
@import "./views/rooms/_LinkPreviewWidget.pcss"; @import "./views/rooms/_LinkPreviewWidget.pcss";

View File

@@ -26,7 +26,8 @@ Please see LICENSE files in the repository root for full details.
} }
&.mx_UserPill_me, &.mx_UserPill_me,
&.mx_AtRoomPill { &.mx_AtRoomPill,
&.mx_KeywordPill {
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */ background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
} }
@@ -45,7 +46,8 @@ Please see LICENSE files in the repository root for full details.
} }
/* We don't want to indicate clickability */ /* We don't want to indicate clickability */
&.mx_AtRoomPill:hover { &.mx_AtRoomPill:hover,
&.mx_KeywordPill:hover {
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */ background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
cursor: unset; cursor: unset;
} }

View File

@@ -35,6 +35,8 @@ Please see LICENSE files in the repository root for full details.
.mx_DisambiguatedProfile_mxid { .mx_DisambiguatedProfile_mxid {
margin-inline-start: 0; margin-inline-start: 0;
font: var(--cpd-font-body-sm-regular); font: var(--cpd-font-body-sm-regular);
text-overflow: ellipsis;
overflow: hidden;
} }
span:not(.mx_DisambiguatedProfile_mxid) { span:not(.mx_DisambiguatedProfile_mxid) {

View File

@@ -135,12 +135,6 @@ $left-gutter: 64px;
} }
} }
&.mx_EventTile_highlight,
&.mx_EventTile_highlight .markdown-body,
&.mx_EventTile_highlight .mx_EventTile_edited {
color: $alert;
}
&.mx_EventTile_bubbleContainer { &.mx_EventTile_bubbleContainer {
display: grid; display: grid;
grid-template-columns: 1fr 100px; grid-template-columns: 1fr 100px;

View File

@@ -0,0 +1,10 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
.mx_InvitedIconView {
color: var(--cpd-color-icon-tertiary);
}

View File

@@ -14,4 +14,10 @@ Please see LICENSE files in the repository root for full details.
.mx_MemberListView_container { .mx_MemberListView_container {
height: 100%; height: 100%;
} }
.mx_MemberListView_separator {
margin: 0;
border: none;
border-top: 2px solid var(--cpd-color-bg-subtle-primary);
}
} }

View File

@@ -31,9 +31,11 @@ Please see LICENSE files in the repository root for full details.
min-width: 0; min-width: 0;
} }
.mx_MemberTileView_user_label { .mx_MemberTileView_userLabel {
font: var(--cpd-font-body-sm-regular); font: var(--cpd-font-body-sm-regular);
font-size: 13px; font-size: 13px;
color: var(--cpd-color-text-secondary);
margin-left: var(--cpd-space-4x);
} }
.mx_MemberTileView_avatar { .mx_MemberTileView_avatar {
@@ -41,18 +43,4 @@ Please see LICENSE files in the repository root for full details.
height: 32px; height: 32px;
width: 32px; width: 32px;
} }
.mx_E2EIconView {
display: flex;
justify-content: center;
align-items: center;
}
.mx_E2EIconView_warning {
color: var(--cpd-color-icon-critical-primary);
}
.mx_E2EIconView_verified {
color: var(--cpd-color-icon-success-primary);
}
} }

View File

@@ -99,8 +99,12 @@ export function sdkRoomMemberToRoomMember(member: SdkRoomMember): Member {
}; };
} }
export const SEPARATOR = "SEPARATOR";
export type MemberWithSeparator = Member | typeof SEPARATOR;
export interface MemberListViewState { export interface MemberListViewState {
members: Member[]; members: MemberWithSeparator[];
memberCount: number;
search: (searchQuery: string) => void; search: (searchQuery: string) => void;
isPresenceEnabled: boolean; isPresenceEnabled: boolean;
shouldShowInvite: boolean; shouldShowInvite: boolean;
@@ -118,10 +122,16 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
} }
const sdkContext = useContext(SDKContext); const sdkContext = useContext(SDKContext);
const [memberMap, setMemberMap] = useState<Map<string, Member>>(new Map()); const [memberMap, setMemberMap] = useState<Map<string, MemberWithSeparator>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
// This is the last known total number of members in this room. // This is the last known total number of members in this room.
const [totalMemberCount, setTotalMemberCount] = useState(0); const [totalMemberCount, setTotalMemberCount] = useState(0);
/**
* This is the current number of members in the list.
* This number will be less than the total number of members
* in the room when the search functionality is used.
*/
const [memberCount, setMemberCount] = useState(0);
const loadMembers = useMemo( const loadMembers = useMemo(
() => () =>
@@ -131,24 +141,34 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
roomId, roomId,
searchQuery, searchQuery,
); );
const newMemberMap = new Map<string, Member>();
// First add the invited room members
for (const member of invitedSdk) {
const roomMember = sdkRoomMemberToRoomMember(member);
newMemberMap.set(member.userId, roomMember);
}
// Then add the third party invites
const threePidInvited = getPending3PidInvites(room, searchQuery); const threePidInvited = getPending3PidInvites(room, searchQuery);
for (const invited of threePidInvited) {
const key = invited.threePidInvite!.event.getContent().display_name; const newMemberMap = new Map<string, MemberWithSeparator>();
newMemberMap.set(key, invited);
} // First add the joined room members
// Finally add the joined room members
for (const member of joinedSdk) { for (const member of joinedSdk) {
const roomMember = sdkRoomMemberToRoomMember(member); const roomMember = sdkRoomMemberToRoomMember(member);
newMemberMap.set(member.userId, roomMember); newMemberMap.set(member.userId, roomMember);
} }
// Then a separator if needed
if (joinedSdk.length > 0 && (invitedSdk.length > 0 || threePidInvited.length > 0))
newMemberMap.set(SEPARATOR, SEPARATOR);
// Then add the invited room members
for (const member of invitedSdk) {
const roomMember = sdkRoomMemberToRoomMember(member);
newMemberMap.set(member.userId, roomMember);
}
// Finally add the third party invites
for (const invited of threePidInvited) {
const key = invited.threePidInvite!.event.getContent().display_name;
newMemberMap.set(key, invited);
}
setMemberMap(newMemberMap); setMemberMap(newMemberMap);
setMemberCount(joinedSdk.length + invitedSdk.length + threePidInvited.length);
if (!searchQuery) { if (!searchQuery) {
/** /**
* Since searching for members only gives you the relevant * Since searching for members only gives you the relevant
@@ -241,6 +261,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
return { return {
members: Array.from(memberMap.values()), members: Array.from(memberMap.values()),
memberCount,
search: loadMembers, search: loadMembers,
shouldShowInvite, shouldShowInvite,
isPresenceEnabled, isPresenceEnabled,

View File

@@ -145,7 +145,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
userLabel = _t(PowerLabel[powerStatus]); userLabel = _t(PowerLabel[powerStatus]);
} }
if (props.member.isInvite) { if (props.member.isInvite) {
userLabel = `(${_t("member_list|invited_label")})`; userLabel = _t("member_list|invited_label");
} }
return { return {

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import dis from "../../../../dispatcher/dispatcher"; import dis from "../../../../dispatcher/dispatcher";
import { Action } from "../../../../dispatcher/actions"; import { Action } from "../../../../dispatcher/actions";
import { ThreePIDInvite } from "../../../../models/rooms/ThreePIDInvite"; import { ThreePIDInvite } from "../../../../models/rooms/ThreePIDInvite";
import { _t } from "../../../../languageHandler";
interface ThreePidTileViewModelProps { interface ThreePidTileViewModelProps {
threePidInvite: ThreePIDInvite; threePidInvite: ThreePIDInvite;
@@ -16,6 +17,7 @@ interface ThreePidTileViewModelProps {
export interface ThreePidTileViewState { export interface ThreePidTileViewState {
name: string; name: string;
onClick: () => void; onClick: () => void;
userLabel?: string;
} }
export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): ThreePidTileViewState { export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): ThreePidTileViewState {
@@ -28,8 +30,11 @@ export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): Thr
}); });
}; };
const userLabel = _t("member_list|invited_label");
return { return {
name, name,
onClick, onClick,
userLabel,
}; };
} }

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import ToggleSwitch from "./ToggleSwitch"; import ToggleSwitch from "./ToggleSwitch";
import { Caption } from "../typography/Caption"; import { Caption } from "../typography/Caption";
@@ -36,7 +36,7 @@ interface IProps {
} }
export default class LabelledToggleSwitch extends React.PureComponent<IProps> { export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
private readonly id = `mx_LabelledToggleSwitch_${randomString(12)}`; private readonly id = `mx_LabelledToggleSwitch_${secureRandomString(12)}`;
public render(): React.ReactNode { public render(): React.ReactNode {
// This is a minimal version of a SettingsFlag // This is a minimal version of a SettingsFlag

View File

@@ -25,6 +25,7 @@ export enum PillType {
AtRoomMention = "TYPE_AT_ROOM_MENTION", // '@room' mention AtRoomMention = "TYPE_AT_ROOM_MENTION", // '@room' mention
EventInSameRoom = "TYPE_EVENT_IN_SAME_ROOM", EventInSameRoom = "TYPE_EVENT_IN_SAME_ROOM",
EventInOtherRoom = "TYPE_EVENT_IN_OTHER_ROOM", EventInOtherRoom = "TYPE_EVENT_IN_OTHER_ROOM",
Keyword = "TYPE_KEYWORD", // Used to highlight keywords that triggered a notification rule
} }
export const pillRoomNotifPos = (text: string | null): number => { export const pillRoomNotifPos = (text: string | null): number => {
@@ -76,14 +77,32 @@ export interface PillProps {
room?: Room; room?: Room;
// Whether to include an avatar in the pill // Whether to include an avatar in the pill
shouldShowPillAvatar?: boolean; shouldShowPillAvatar?: boolean;
// Explicitly-provided text to display in the pill
text?: string;
} }
export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar = true }) => { export const Pill: React.FC<PillProps> = ({
const { event, member, onClick, resourceId, targetRoom, text, type } = usePermalink({ type: propType,
url,
inMessage,
room,
shouldShowPillAvatar = true,
text: customPillText,
}) => {
const {
event,
member,
onClick,
resourceId,
targetRoom,
text: linkText,
type,
} = usePermalink({
room, room,
type: propType, type: propType,
url, url,
}); });
const text = customPillText ?? linkText;
if (!type || !text) { if (!type || !text) {
return null; return null;
@@ -96,6 +115,7 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
mx_UserPill: type === PillType.UserMention, mx_UserPill: type === PillType.UserMention,
mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(), mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(),
mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom, mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom,
mx_KeywordPill: type === PillType.Keyword,
}); });
let avatar: ReactElement | null = null; let avatar: ReactElement | null = null;
@@ -131,6 +151,8 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
case PillType.UserMention: case PillType.UserMention:
avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />; avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />;
break; break;
case PillType.Keyword:
break;
default: default:
return null; return null;
} }

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React from "react";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@@ -35,7 +35,7 @@ interface IState {
} }
export default class SettingsFlag extends React.Component<IProps, IState> { export default class SettingsFlag extends React.Component<IProps, IState> {
private readonly id = `mx_SettingsFlag_${randomString(12)}`; private readonly id = `mx_SettingsFlag_${secureRandomString(12)}`;
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { Ref } from "react"; import React, { Ref } from "react";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import classnames from "classnames"; import classnames from "classnames";
export enum CheckboxStyle { export enum CheckboxStyle {
@@ -33,7 +33,7 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
// 56^10 so unlikely chance of collision. // 56^10 so unlikely chance of collision.
this.id = this.props.id || "checkbox_" + randomString(10); this.id = this.props.id || "checkbox_" + secureRandomString(10);
} }
public render(): React.ReactNode { public render(): React.ReactNode {

View File

@@ -18,7 +18,7 @@ import {
ContentHelpers, ContentHelpers,
M_BEACON, M_BEACON,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import classNames from "classnames"; import classNames from "classnames";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
@@ -81,10 +81,10 @@ const useBeaconState = (
// eg thread and main timeline, reply // eg thread and main timeline, reply
// maplibregl needs a unique id to attach the map instance to // maplibregl needs a unique id to attach the map instance to
const useUniqueId = (eventId: string): string => { const useUniqueId = (eventId: string): string => {
const [id, setId] = useState(`${eventId}_${randomString(8)}`); const [id, setId] = useState(`${eventId}_${secureRandomString(8)}`);
useEffect(() => { useEffect(() => {
setId(`${eventId}_${randomString(8)}`); setId(`${eventId}_${secureRandomString(8)}`);
}, [eventId]); }, [eventId]);
return id; return id;

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react"; import React from "react";
import { MatrixEvent, ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { Tooltip } from "@vector-im/compound-web"; import { Tooltip } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@@ -41,7 +41,7 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> {
// multiple instances of same map might be in document // multiple instances of same map might be in document
// eg thread and main timeline, reply // eg thread and main timeline, reply
const idSuffix = `${props.mxEvent.getId()}_${randomString(8)}`; const idSuffix = `${props.mxEvent.getId()}_${secureRandomString(8)}`;
this.mapId = `mx_MLocationBody_${idSuffix}`; this.mapId = `mx_MLocationBody_${idSuffix}`;
this.reconnectedListener = createReconnectedListener(this.clearError); this.reconnectedListener = createReconnectedListener(this.clearError);

View File

@@ -7,8 +7,9 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react"; import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react";
import { MsgType } from "matrix-js-sdk/src/matrix"; import { MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix";
import { TooltipProvider } from "@vector-im/compound-web"; import { TooltipProvider } from "@vector-im/compound-web";
import { globToRegexp } from "matrix-js-sdk/src/utils";
import * as HtmlUtils from "../../../HtmlUtils"; import * as HtmlUtils from "../../../HtmlUtils";
import { formatDate } from "../../../DateUtils"; import { formatDate } from "../../../DateUtils";
@@ -35,6 +36,7 @@ import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
import { IEventTileOps } from "../rooms/EventTile"; import { IEventTileOps } from "../rooms/EventTile";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import CodeBlock from "./CodeBlock"; import CodeBlock from "./CodeBlock";
import { Pill, PillType } from "../elements/Pill";
import { ReactRootManager } from "../../../utils/react"; import { ReactRootManager } from "../../../utils/react";
interface IState { interface IState {
@@ -100,6 +102,16 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
} }
} }
} }
// Highlight notification keywords using pills
const pushDetails = this.props.mxEvent.getPushDetails();
if (
pushDetails.rule?.enabled &&
pushDetails.rule.kind === PushRuleKind.ContentSpecific &&
pushDetails.rule.pattern
) {
this.pillifyNotificationKeywords([content], this.regExpForKeywordPattern(pushDetails.rule.pattern));
}
} }
private addCodeElement(pre: HTMLPreElement): void { private addCodeElement(pre: HTMLPreElement): void {
@@ -210,6 +222,55 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
} }
} }
/**
* Marks the text that activated a push-notification keyword pattern.
*/
private pillifyNotificationKeywords(nodes: ArrayLike<Element>, exp: RegExp): void {
let node: Node | null = nodes[0];
while (node) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.nodeValue;
if (!text) {
node = node.nextSibling;
continue;
}
const match = text.match(exp);
if (!match || match.length < 3) {
node = node.nextSibling;
continue;
}
const keywordText = match[2];
const idx = match.index! + match[1].length;
const before = text.substring(0, idx);
const after = text.substring(idx + keywordText.length);
const container = document.createElement("span");
const newContent = (
<>
{before}
<TooltipProvider>
<Pill text={keywordText} type={PillType.Keyword} />
</TooltipProvider>
{after}
</>
);
this.reactRoots.render(newContent, container, node);
node.parentNode?.replaceChild(container, node);
} else if (node.childNodes && node.childNodes.length) {
this.pillifyNotificationKeywords(node.childNodes as NodeListOf<Element>, exp);
}
node = node.nextSibling;
}
}
private regExpForKeywordPattern(pattern: string): RegExp {
// Reflects the push notification pattern-matching implementation at
// https://github.com/matrix-org/matrix-js-sdk/blob/dbd7d26968b94700827bac525c39afff2c198e61/src/pushprocessor.ts#L570
return new RegExp("(^|\\W)(" + globToRegexp(pattern) + ")(\\W|$)", "i");
}
private findLinks(nodes: ArrayLike<Element>): string[] { private findLinks(nodes: ArrayLike<Element>): string[] {
let links: string[] = []; let links: string[] = [];

View File

@@ -88,12 +88,10 @@ function getHeaderLabelJSX(vm: MemberListViewState): React.ReactNode {
</Flex> </Flex>
); );
} }
if (vm.memberCount === 0) {
const filteredMemberCount = vm.members.length;
if (filteredMemberCount === 0) {
return _t("member_list|no_matches"); return _t("member_list|no_matches");
} }
return _t("member_list|count", { count: filteredMemberCount }); return _t("member_list|count", { count: vm.memberCount });
} }
export const MemberListHeaderView: React.FC<Props> = (props: Props) => { export const MemberListHeaderView: React.FC<Props> = (props: Props) => {

View File

@@ -11,7 +11,11 @@ import { List, ListRowProps } from "react-virtualized/dist/commonjs/List";
import { AutoSizer } from "react-virtualized"; import { AutoSizer } from "react-virtualized";
import { Flex } from "../../../utils/Flex"; import { Flex } from "../../../utils/Flex";
import { useMemberListViewModel } from "../../../viewmodels/memberlist/MemberListViewModel"; import {
MemberWithSeparator,
SEPARATOR,
useMemberListViewModel,
} from "../../../viewmodels/memberlist/MemberListViewModel";
import { RoomMemberTileView } from "./tiles/RoomMemberTileView"; import { RoomMemberTileView } from "./tiles/RoomMemberTileView";
import { ThreePidInviteTileView } from "./tiles/ThreePidInviteTileView"; import { ThreePidInviteTileView } from "./tiles/ThreePidInviteTileView";
import { MemberListHeaderView } from "./MemberListHeaderView"; import { MemberListHeaderView } from "./MemberListHeaderView";
@@ -26,10 +30,41 @@ interface IProps {
const MemberListView: React.FC<IProps> = (props: IProps) => { const MemberListView: React.FC<IProps> = (props: IProps) => {
const vm = useMemberListViewModel(props.roomId); const vm = useMemberListViewModel(props.roomId);
const memberCount = vm.members.length; const totalRows = vm.members.length;
const getRowComponent = (item: MemberWithSeparator): React.JSX.Element => {
if (item === SEPARATOR) {
return <hr className="mx_MemberListView_separator" />;
} else if (item.member) {
return <RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />;
} else {
return <ThreePidInviteTileView threePidInvite={item.threePidInvite} />;
}
};
const getRowHeight = ({ index }: { index: number }): number => {
if (vm.members[index] === SEPARATOR) {
/**
* This is a separator of 2px height rendered between
* joined and invited members.
*/
return 2;
} else if (totalRows && index === totalRows) {
/**
* The empty spacer div rendered at the bottom should
* have a height of 32px.
*/
return 32;
} else {
/**
* The actual member tiles have a height of 56px.
*/
return 56;
}
};
const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => { const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => {
if (index === memberCount) { if (index === totalRows) {
// We've rendered all the members, // We've rendered all the members,
// now we render an empty div to add some space to the end of the list. // now we render an empty div to add some space to the end of the list.
return <div key={key} style={style} />; return <div key={key} style={style} />;
@@ -37,11 +72,7 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
const item = vm.members[index]; const item = vm.members[index];
return ( return (
<div key={key} style={style}> <div key={key} style={style}>
{item.member ? ( {getRowComponent(item)}
<RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />
) : (
<ThreePidInviteTileView threePidInvite={item.threePidInvite} />
)}
</div> </div>
); );
}; };
@@ -63,11 +94,9 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
{({ height, width }) => ( {({ height, width }) => (
<List <List
rowRenderer={rowRenderer} rowRenderer={rowRenderer}
// All the member tiles will have a height of 56px. rowHeight={getRowHeight}
// The additional empty div at the end of the list should have a height of 32px.
rowHeight={({ index }) => (index === memberCount ? 32 : 56)}
// The +1 refers to the additional empty div that we render at the end of the list. // The +1 refers to the additional empty div that we render at the end of the list.
rowCount={memberCount + 1} rowCount={totalRows + 1}
// Subtract the height of MemberlistHeaderView so that the parent div does not overflow. // Subtract the height of MemberlistHeaderView so that the parent div does not overflow.
height={height - 113} height={height - 113}
width={width} width={width}

View File

@@ -14,7 +14,8 @@ import { E2EIconView } from "./common/E2EIconView";
import AvatarPresenceIconView from "./common/PresenceIconView"; import AvatarPresenceIconView from "./common/PresenceIconView";
import BaseAvatar from "../../../avatars/BaseAvatar"; import BaseAvatar from "../../../avatars/BaseAvatar";
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import { MemberTileLayout } from "./common/MemberTileLayout"; import { MemberTileView } from "./common/MemberTileView";
import { InvitedIconView } from "./common/InvitedIconView";
interface IProps { interface IProps {
member: RoomMember; member: RoomMember;
@@ -43,25 +44,23 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
presenceJSX = <AvatarPresenceIconView presenceState={presenceState} />; presenceJSX = <AvatarPresenceIconView presenceState={presenceState} />;
} }
let userLabelJSX; let iconJsx;
if (vm.userLabel) {
userLabelJSX = <div className="mx_MemberTileView_user_label">{vm.userLabel}</div>;
}
let e2eIcon;
if (vm.e2eStatus) { if (vm.e2eStatus) {
e2eIcon = <E2EIconView status={vm.e2eStatus} />; iconJsx = <E2EIconView status={vm.e2eStatus} />;
}
if (member.isInvite) {
iconJsx = <InvitedIconView isThreePid={false} />;
} }
return ( return (
<MemberTileLayout <MemberTileView
title={vm.title} title={vm.title}
onClick={vm.onClick} onClick={vm.onClick}
avatarJsx={av} avatarJsx={av}
presenceJsx={presenceJSX} presenceJsx={presenceJSX}
nameJsx={nameJSX} nameJsx={nameJSX}
userLabelJsx={userLabelJSX} userLabel={vm.userLabel}
e2eIconJsx={e2eIcon} iconJsx={iconJsx}
/> />
); );
} }

View File

@@ -10,7 +10,8 @@ import React from "react";
import { useThreePidTileViewModel } from "../../../../viewmodels/memberlist/tiles/ThreePidTileViewModel"; import { useThreePidTileViewModel } from "../../../../viewmodels/memberlist/tiles/ThreePidTileViewModel";
import { ThreePIDInvite } from "../../../../../models/rooms/ThreePIDInvite"; import { ThreePIDInvite } from "../../../../../models/rooms/ThreePIDInvite";
import BaseAvatar from "../../../avatars/BaseAvatar"; import BaseAvatar from "../../../avatars/BaseAvatar";
import { MemberTileLayout } from "./common/MemberTileLayout"; import { MemberTileView } from "./common/MemberTileView";
import { InvitedIconView } from "./common/InvitedIconView";
interface Props { interface Props {
threePidInvite: ThreePIDInvite; threePidInvite: ThreePIDInvite;
@@ -19,5 +20,15 @@ interface Props {
export function ThreePidInviteTileView(props: Props): JSX.Element { export function ThreePidInviteTileView(props: Props): JSX.Element {
const vm = useThreePidTileViewModel(props); const vm = useThreePidTileViewModel(props);
const av = <BaseAvatar name={vm.name} size="32px" aria-hidden="true" />; const av = <BaseAvatar name={vm.name} size="32px" aria-hidden="true" />;
return <MemberTileLayout nameJsx={vm.name} avatarJsx={av} onClick={vm.onClick} />; const iconJsx = <InvitedIconView isThreePid={true} />;
return (
<MemberTileView
nameJsx={vm.name}
avatarJsx={av}
onClick={vm.onClick}
userLabel={vm.userLabel}
iconJsx={iconJsx}
/>
);
} }

View File

@@ -0,0 +1,25 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add-solid";
import { Flex } from "../../../../../utils/Flex";
interface Props {
isThreePid: boolean;
}
export function InvitedIconView({ isThreePid }: Props): JSX.Element {
const Icon = isThreePid ? EmailIcon : UserAddIcon;
return (
<Flex align="center" className="mx_InvitedIconView">
<Icon height="16px" width="16px" />
</Flex>
);
}

View File

@@ -15,11 +15,16 @@ interface Props {
onClick: () => void; onClick: () => void;
title?: string; title?: string;
presenceJsx?: JSX.Element; presenceJsx?: JSX.Element;
userLabelJsx?: JSX.Element; userLabel?: React.ReactNode;
e2eIconJsx?: JSX.Element; iconJsx?: JSX.Element;
}
export function MemberTileView(props: Props): JSX.Element {
let userLabelJsx: React.ReactNode;
if (props.userLabel) {
userLabelJsx = <div className="mx_MemberTileView_userLabel">{props.userLabel}</div>;
} }
export function MemberTileLayout(props: Props): JSX.Element {
return ( return (
// The wrapping div is required to make the magic mouse listener work, for some reason. // The wrapping div is required to make the magic mouse listener work, for some reason.
<div> <div>
@@ -31,8 +36,8 @@ export function MemberTileLayout(props: Props): JSX.Element {
<div className="mx_MemberTileView_name">{props.nameJsx}</div> <div className="mx_MemberTileView_name">{props.nameJsx}</div>
</div> </div>
<div className="mx_MemberTileView_right"> <div className="mx_MemberTileView_right">
{props.userLabelJsx} {userLabelJsx}
{props.e2eIconJsx} {props.iconJsx}
</div> </div>
</AccessibleButton> </AccessibleButton>
</div> </div>

View File

@@ -31,8 +31,7 @@ export function shouldShowQr(
): boolean { ): boolean {
const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"]; const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"];
const deviceAuthorizationGrantSupported = const deviceAuthorizationGrantSupported = oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
oidcClientConfig?.metadata?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
return ( return (
!!deviceAuthorizationGrantSupported && !!deviceAuthorizationGrantSupported &&

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { lazy, Suspense, useCallback, useContext, useEffect, useRef, useState } from "react"; import React, { lazy, Suspense, useCallback, useContext, useEffect, useRef, useState } from "react";
import { discoverAndValidateOIDCIssuerWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix"; import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { defer } from "matrix-js-sdk/src/utils"; import { defer } from "matrix-js-sdk/src/utils";
@@ -163,10 +163,7 @@ const SessionManagerTab: React.FC<{
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]); const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
const oidcClientConfig = useAsyncMemo(async () => { const oidcClientConfig = useAsyncMemo(async () => {
try { try {
const authIssuer = await matrixClient?.getAuthIssuer(); return await matrixClient?.getAuthMetadata();
if (authIssuer) {
return discoverAndValidateOIDCIssuerWellKnown(authIssuer.issuer);
}
} catch (e) { } catch (e) {
logger.error("Failed to discover OIDC metadata", e); logger.error("Failed to discover OIDC metadata", e);
} }

View File

@@ -18,7 +18,7 @@ import {
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { KnownMembership, Membership } from "matrix-js-sdk/src/types"; import { KnownMembership, Membership } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue"; import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue";
import { IWidgetApiRequest } from "matrix-widget-api"; import { IWidgetApiRequest } from "matrix-widget-api";
@@ -743,7 +743,7 @@ export class ElementCall extends Call {
const url = ElementCall.generateWidgetUrl(client, roomId); const url = ElementCall.generateWidgetUrl(client, roomId);
return WidgetStore.instance.addVirtualWidget( return WidgetStore.instance.addVirtualWidget(
{ {
id: randomString(24), // So that it's globally unique id: secureRandomString(24), // So that it's globally unique
creatorUserId: client.getUserId()!, creatorUserId: client.getUserId()!,
name: "Element Call", name: "Element Call",
type: WidgetType.CALL.preferred, type: WidgetType.CALL.preferred,

View File

@@ -31,7 +31,7 @@ Please see LICENSE files in the repository root for full details.
// the frequency with which we flush to indexeddb // the frequency with which we flush to indexeddb
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { getCircularReplacer } from "../utils/JSON"; import { getCircularReplacer } from "../utils/JSON";
@@ -135,7 +135,7 @@ export class IndexedDBLogStore {
private indexedDB: IDBFactory, private indexedDB: IDBFactory,
private logger: ConsoleLogger, private logger: ConsoleLogger,
) { ) {
this.id = "instance-" + randomString(16); this.id = "instance-" + secureRandomString(16);
} }
/** /**

View File

@@ -50,11 +50,8 @@ export class OidcClientStore {
} else { } else {
// We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC. // We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC.
try { try {
const authIssuer = await this.matrixClient.getAuthIssuer(); const authMetadata = await this.matrixClient.getAuthMetadata();
const { accountManagementEndpoint, metadata } = await discoverAndValidateOIDCIssuerWellKnown( this.setAccountManagementEndpoint(authMetadata.account_management_uri, authMetadata.issuer);
authIssuer.issuer,
);
this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer);
} catch (e) { } catch (e) {
console.log("Auth issuer not found", e); console.log("Auth issuer not found", e);
} }
@@ -153,14 +150,11 @@ export class OidcClientStore {
try { try {
const clientId = getStoredOidcClientId(); const clientId = getStoredOidcClientId();
const { accountManagementEndpoint, metadata, signingKeys } = await discoverAndValidateOIDCIssuerWellKnown( const authMetadata = await discoverAndValidateOIDCIssuerWellKnown(this.authenticatedIssuer);
this.authenticatedIssuer, this.setAccountManagementEndpoint(authMetadata.account_management_uri, authMetadata.issuer);
);
this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer);
this.oidcClient = new OidcClient({ this.oidcClient = new OidcClient({
...metadata, authority: authMetadata.issuer,
authority: metadata.issuer, signingKeys: authMetadata.signingKeys ?? undefined,
signingKeys,
redirect_uri: PlatformPeg.get()!.getOidcCallbackUrl().href, redirect_uri: PlatformPeg.get()!.getOidcCallbackUrl().href,
client_id: clientId, client_id: clientId,
}); });

View File

@@ -6,7 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
import { Room, MatrixEvent, MatrixEventEvent, MatrixClient, ClientEvent } from "matrix-js-sdk/src/matrix"; import {
Room,
MatrixEvent,
MatrixEventEvent,
MatrixClient,
ClientEvent,
RoomStateEvent,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";
import { import {
ClientWidgetApi, ClientWidgetApi,
@@ -26,7 +33,6 @@ import {
WidgetApiFromWidgetAction, WidgetApiFromWidgetAction,
WidgetKind, WidgetKind,
} from "matrix-widget-api"; } from "matrix-widget-api";
import { Optional } from "matrix-events-sdk";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@@ -56,6 +62,7 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import Modal from "../../Modal"; import Modal from "../../Modal";
import ErrorDialog from "../../components/views/dialogs/ErrorDialog"; import ErrorDialog from "../../components/views/dialogs/ErrorDialog";
import { SdkContextClass } from "../../contexts/SDKContext"; import { SdkContextClass } from "../../contexts/SDKContext";
import { UPDATE_EVENT } from "../AsyncStore";
// TODO: Destroy all of this code // TODO: Destroy all of this code
@@ -151,6 +158,9 @@ export class StopGapWidget extends EventEmitter {
private mockWidget: ElementWidget; private mockWidget: ElementWidget;
private scalarToken?: string; private scalarToken?: string;
private roomId?: string; private roomId?: string;
// The room that we're currently allowing the widget to interact with. Only
// used for account widgets, which may follow the user to different rooms.
private viewedRoomId: string | null = null;
private kind: WidgetKind; private kind: WidgetKind;
private readonly virtual: boolean; private readonly virtual: boolean;
private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID
@@ -177,17 +187,6 @@ export class StopGapWidget extends EventEmitter {
this.stickyPromise = appTileProps.stickyPromise; this.stickyPromise = appTileProps.stickyPromise;
} }
private get eventListenerRoomId(): Optional<string> {
// When widgets are listening to events, we need to make sure they're only
// receiving events for the right room. In particular, room widgets get locked
// to the room they were added in while account widgets listen to the currently
// active room.
if (this.roomId) return this.roomId;
return SdkContextClass.instance.roomViewStore.getRoomId();
}
public get widgetApi(): ClientWidgetApi | null { public get widgetApi(): ClientWidgetApi | null {
return this.messaging; return this.messaging;
} }
@@ -259,6 +258,17 @@ export class StopGapWidget extends EventEmitter {
}); });
} }
}; };
// This listener is only active for account widgets, which may follow the
// user to different rooms
private onRoomViewStoreUpdate = (): void => {
const roomId = SdkContextClass.instance.roomViewStore.getRoomId() ?? null;
if (roomId !== this.viewedRoomId) {
this.messaging!.setViewedRoomId(roomId);
this.viewedRoomId = roomId;
}
};
/** /**
* This starts the messaging for the widget if it is not in the state `started` yet. * This starts the messaging for the widget if it is not in the state `started` yet.
* @param iframe the iframe the widget should use * @param iframe the iframe the widget should use
@@ -285,6 +295,17 @@ export class StopGapWidget extends EventEmitter {
this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified")); this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified"));
this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal); this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
// When widgets are listening to events, we need to make sure they're only
// receiving events for the right room
if (this.roomId === undefined) {
// Account widgets listen to the currently active room
this.messaging.setViewedRoomId(SdkContextClass.instance.roomViewStore.getRoomId() ?? null);
SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
} else {
// Room widgets get locked to the room they were added in
this.messaging.setViewedRoomId(this.roomId);
}
// Always attach a handler for ViewRoom, but permission check it internally // Always attach a handler for ViewRoom, but permission check it internally
this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => { this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => {
ev.preventDefault(); // stop the widget API from auto-rejecting this ev.preventDefault(); // stop the widget API from auto-rejecting this
@@ -329,6 +350,7 @@ export class StopGapWidget extends EventEmitter {
// Attach listeners for feeding events - the underlying widget classes handle permissions for us // Attach listeners for feeding events - the underlying widget classes handle permissions for us
this.client.on(ClientEvent.Event, this.onEvent); this.client.on(ClientEvent.Event, this.onEvent);
this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
this.client.on(RoomStateEvent.Events, this.onStateUpdate);
this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
this.messaging.on( this.messaging.on(
@@ -457,8 +479,11 @@ export class StopGapWidget extends EventEmitter {
WidgetMessagingStore.instance.stopMessaging(this.mockWidget, this.roomId); WidgetMessagingStore.instance.stopMessaging(this.mockWidget, this.roomId);
this.messaging = null; this.messaging = null;
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.client.off(ClientEvent.Event, this.onEvent); this.client.off(ClientEvent.Event, this.onEvent);
this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted); this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted);
this.client.off(RoomStateEvent.Events, this.onStateUpdate);
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
} }
@@ -471,6 +496,14 @@ export class StopGapWidget extends EventEmitter {
this.feedEvent(ev); this.feedEvent(ev);
}; };
private onStateUpdate = (ev: MatrixEvent): void => {
if (this.messaging === null) return;
const raw = ev.getEffectiveEvent();
this.messaging.feedStateUpdate(raw as IRoomEvent).catch((e) => {
logger.error("Error sending state update to widget: ", e);
});
};
private onToDeviceEvent = async (ev: MatrixEvent): Promise<void> => { private onToDeviceEvent = async (ev: MatrixEvent): Promise<void> => {
await this.client.decryptEventIfNeeded(ev); await this.client.decryptEventIfNeeded(ev);
if (ev.isDecryptionFailure()) return; if (ev.isDecryptionFailure()) return;
@@ -570,7 +603,7 @@ export class StopGapWidget extends EventEmitter {
this.eventsToFeed.add(ev); this.eventsToFeed.add(ev);
} else { } else {
const raw = ev.getEffectiveEvent(); const raw = ev.getEffectiveEvent();
this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => { this.messaging.feedEvent(raw as IRoomEvent).catch((e) => {
logger.error("Error sending event to widget: ", e); logger.error("Error sending event to widget: ", e);
}); });
} }

View File

@@ -19,7 +19,6 @@ import {
MatrixCapabilities, MatrixCapabilities,
OpenIDRequestState, OpenIDRequestState,
SimpleObservable, SimpleObservable,
Symbols,
Widget, Widget,
WidgetDriver, WidgetDriver,
WidgetEventCapability, WidgetEventCapability,
@@ -36,7 +35,6 @@ import {
IContent, IContent,
MatrixError, MatrixError,
MatrixEvent, MatrixEvent,
Room,
Direction, Direction,
THREAD_RELATION_TYPE, THREAD_RELATION_TYPE,
SendDelayedEventResponse, SendDelayedEventResponse,
@@ -469,70 +467,69 @@ export class StopGapWidgetDriver extends WidgetDriver {
} }
} }
private pickRooms(roomIds?: (string | Symbols.AnyRoom)[]): Room[] { /**
const client = MatrixClientPeg.get(); * Reads all events of the given type, and optionally `msgtype` (if applicable/defined),
if (!client) throw new Error("Not attached to a client"); * the user has access to. The widget API will have already verified that the widget is
* capable of receiving the events. Less events than the limit are allowed to be returned,
const targetRooms = roomIds * but not more.
? roomIds.includes(Symbols.AnyRoom) * @param roomId The ID of the room to look within.
? client.getVisibleRooms(SettingsStore.getValue("feature_dynamic_room_predecessors")) * @param eventType The event type to be read.
: roomIds.map((r) => client.getRoom(r)) * @param msgtype The msgtype of the events to be read, if applicable/defined.
: [client.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()!)]; * @param stateKey The state key of the events to be read, if applicable/defined.
return targetRooms.filter((r) => !!r) as Room[]; * @param limit The maximum number of events to retrieve. Will be zero to denote "as many as
} * possible".
* @param since When null, retrieves the number of events specified by the "limit" parameter.
public async readRoomEvents( * Otherwise, the event ID at which only subsequent events will be returned, as many as specified
* in "limit".
* @returns {Promise<IRoomEvent[]>} Resolves to the room events, or an empty array.
*/
public async readRoomTimeline(
roomId: string,
eventType: string, eventType: string,
msgtype: string | undefined, msgtype: string | undefined,
limitPerRoom: number, stateKey: string | undefined,
roomIds?: (string | Symbols.AnyRoom)[], limit: number,
since: string | undefined,
): Promise<IRoomEvent[]> { ): Promise<IRoomEvent[]> {
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary limit = limit > 0 ? Math.min(limit, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary
const rooms = this.pickRooms(roomIds); const room = MatrixClientPeg.safeGet().getRoom(roomId);
const allResults: IRoomEvent[] = []; if (room === null) return [];
for (const room of rooms) {
const results: MatrixEvent[] = []; const results: MatrixEvent[] = [];
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
for (let i = events.length - 1; i > 0; i--) { for (let i = events.length - 1; i >= 0; i--) {
if (results.length >= limitPerRoom) break;
const ev = events[i]; const ev = events[i];
if (results.length >= limit) break;
if (since !== undefined && ev.getId() === since) break;
if (ev.getType() !== eventType || ev.isState()) continue; if (ev.getType() !== eventType || ev.isState()) continue;
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()["msgtype"]) continue; if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()["msgtype"]) continue;
if (ev.getStateKey() !== undefined && stateKey !== undefined && ev.getStateKey() !== stateKey) continue;
results.push(ev); results.push(ev);
} }
results.forEach((e) => allResults.push(e.getEffectiveEvent() as IRoomEvent)); return results.map((e) => e.getEffectiveEvent() as IRoomEvent);
}
return allResults;
} }
public async readStateEvents( /**
eventType: string, * Reads the current values of all matching room state entries.
stateKey: string | undefined, * @param roomId The ID of the room.
limitPerRoom: number, * @param eventType The event type of the entries to be read.
roomIds?: (string | Symbols.AnyRoom)[], * @param stateKey The state key of the entry to be read. If undefined,
): Promise<IRoomEvent[]> { * all room state entries with a matching event type should be returned.
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary * @returns {Promise<IRoomEvent[]>} Resolves to the events representing the
* current values of the room state entries.
*/
public async readRoomState(roomId: string, eventType: string, stateKey: string | undefined): Promise<IRoomEvent[]> {
const room = MatrixClientPeg.safeGet().getRoom(roomId);
if (room === null) return [];
const state = room.getLiveTimeline().getState(Direction.Forward);
if (state === undefined) return [];
const rooms = this.pickRooms(roomIds); if (stateKey === undefined)
const allResults: IRoomEvent[] = []; return state.getStateEvents(eventType).map((e) => e.getEffectiveEvent() as IRoomEvent);
for (const room of rooms) { const event = state.getStateEvents(eventType, stateKey);
const results: MatrixEvent[] = []; return event === null ? [] : [event.getEffectiveEvent() as IRoomEvent];
const state = room.currentState.events.get(eventType);
if (state) {
if (stateKey === "" || !!stateKey) {
const forKey = state.get(stateKey);
if (forKey) results.push(forKey);
} else {
results.push(...Array.from(state.values()));
}
}
results.slice(0, limitPerRoom).forEach((e) => allResults.push(e.getEffectiveEvent() as IRoomEvent));
}
return allResults;
} }
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>): Promise<void> { public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>): Promise<void> {
@@ -693,6 +690,17 @@ export class StopGapWidgetDriver extends WidgetDriver {
return { file: blob }; return { file: blob };
} }
/**
* Gets the IDs of all joined or invited rooms currently known to the
* client.
* @returns The room IDs.
*/
public getKnownRooms(): string[] {
return MatrixClientPeg.safeGet()
.getVisibleRooms(SettingsStore.getValue("feature_dynamic_room_predecessors"))
.map((r) => r.roomId);
}
/** /**
* Expresses a {@link MatrixError} as a JSON payload * Expresses a {@link MatrixError} as a JSON payload
* for use by Widget API error responses. * for use by Widget API error responses.

View File

@@ -11,7 +11,6 @@ import {
AutoDiscovery, AutoDiscovery,
AutoDiscoveryError, AutoDiscoveryError,
ClientConfig, ClientConfig,
discoverAndValidateOIDCIssuerWellKnown,
IClientWellKnown, IClientWellKnown,
MatrixClient, MatrixClient,
MatrixError, MatrixError,
@@ -293,8 +292,7 @@ export default class AutoDiscoveryUtils {
let delegatedAuthenticationError: Error | undefined; let delegatedAuthenticationError: Error | undefined;
try { try {
const tempClient = new MatrixClient({ baseUrl: preferredHomeserverUrl }); const tempClient = new MatrixClient({ baseUrl: preferredHomeserverUrl });
const { issuer } = await tempClient.getAuthIssuer(); delegatedAuthentication = await tempClient.getAuthMetadata();
delegatedAuthentication = await discoverAndValidateOIDCIssuerWellKnown(issuer);
} catch (e) { } catch (e) {
if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_UNRECOGNIZED") { if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_UNRECOGNIZED") {
// 404 M_UNRECOGNIZED means the server does not support OIDC // 404 M_UNRECOGNIZED means the server does not support OIDC

View File

@@ -9,12 +9,13 @@ Please see LICENSE files in the repository root for full details.
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { base32 } from "rfc4648"; import { base32 } from "rfc4648";
import { capitalize } from "lodash";
import { IWidget, IWidgetData } from "matrix-widget-api"; import { IWidget, IWidgetData } from "matrix-widget-api";
import { Room, ClientEvent, MatrixClient, RoomStateEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Room, ClientEvent, MatrixClient, RoomStateEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { randomString, randomLowercaseString, randomUppercaseString } from "matrix-js-sdk/src/randomstring"; import { LOWERCASE, secureRandomString, secureRandomStringFrom } from "matrix-js-sdk/src/randomstring";
import PlatformPeg from "../PlatformPeg"; import PlatformPeg from "../PlatformPeg";
import SdkConfig from "../SdkConfig"; import SdkConfig from "../SdkConfig";
@@ -427,7 +428,10 @@ export default class WidgetUtils {
): Promise<void> { ): Promise<void> {
const domain = Jitsi.getInstance().preferredDomain; const domain = Jitsi.getInstance().preferredDomain;
const auth = (await Jitsi.getInstance().getJitsiAuth()) ?? undefined; const auth = (await Jitsi.getInstance().getJitsiAuth()) ?? undefined;
const widgetId = randomString(24); // Must be globally unique
// Must be globally unique, although predicatablity is not important, the js-sdk has functions to generate
// secure ranom strings, and speed is not important here.
const widgetId = secureRandomString(24);
let confId: string; let confId: string;
if (auth === "openidtoken-jwt") { if (auth === "openidtoken-jwt") {
@@ -437,8 +441,8 @@ export default class WidgetUtils {
// https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
confId = base32.stringify(new TextEncoder().encode(roomId), { pad: false }); confId = base32.stringify(new TextEncoder().encode(roomId), { pad: false });
} else { } else {
// Create a random conference ID // Create a random conference ID (capitalised so the name looks sensible in Jitsi)
confId = `Jitsi${randomUppercaseString(1)}${randomLowercaseString(23)}`; confId = `Jitsi${capitalize(secureRandomStringFrom(24, LOWERCASE))}`;
} }
// TODO: Remove URL hacks when the mobile clients eventually support v2 widgets // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import { completeAuthorizationCodeGrant, generateOidcAuthorizationUrl } from "matrix-js-sdk/src/oidc/authorize"; import { completeAuthorizationCodeGrant, generateOidcAuthorizationUrl } from "matrix-js-sdk/src/oidc/authorize";
import { QueryDict } from "matrix-js-sdk/src/utils"; import { QueryDict } from "matrix-js-sdk/src/utils";
import { OidcClientConfig } from "matrix-js-sdk/src/matrix"; import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { IdTokenClaims } from "oidc-client-ts"; import { IdTokenClaims } from "oidc-client-ts";
import { OidcClientError } from "./error"; import { OidcClientError } from "./error";
@@ -34,12 +34,12 @@ export const startOidcLogin = async (
): Promise<void> => { ): Promise<void> => {
const redirectUri = PlatformPeg.get()!.getOidcCallbackUrl().href; const redirectUri = PlatformPeg.get()!.getOidcCallbackUrl().href;
const nonce = randomString(10); const nonce = secureRandomString(10);
const prompt = isRegistration ? "create" : undefined; const prompt = isRegistration ? "create" : undefined;
const authorizationUrl = await generateOidcAuthorizationUrl({ const authorizationUrl = await generateOidcAuthorizationUrl({
metadata: delegatedAuthConfig.metadata, metadata: delegatedAuthConfig,
redirectUri, redirectUri,
clientId, clientId,
homeserverUrl, homeserverUrl,

View File

@@ -15,8 +15,6 @@ import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
* @returns whether user registration is supported * @returns whether user registration is supported
*/ */
export const isUserRegistrationSupported = (delegatedAuthConfig: OidcClientConfig): boolean => { export const isUserRegistrationSupported = (delegatedAuthConfig: OidcClientConfig): boolean => {
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported` const supportedPrompts = delegatedAuthConfig.prompt_values_supported;
// even though it is part of the OIDC spec, so cheat TS here to access it
const supportedPrompts = (delegatedAuthConfig.metadata as Record<string, unknown>)["prompt_values_supported"];
return Array.isArray(supportedPrompts) && supportedPrompts?.includes("create"); return Array.isArray(supportedPrompts) && supportedPrompts?.includes("create");
}; };

View File

@@ -40,9 +40,9 @@ export const getOidcClientId = async (
delegatedAuthConfig: OidcClientConfig, delegatedAuthConfig: OidcClientConfig,
staticOidcClients?: IConfigOptions["oidc_static_clients"], staticOidcClients?: IConfigOptions["oidc_static_clients"],
): Promise<string> => { ): Promise<string> => {
const staticClientId = getStaticOidcClientId(delegatedAuthConfig.metadata.issuer, staticOidcClients); const staticClientId = getStaticOidcClientId(delegatedAuthConfig.issuer, staticOidcClients);
if (staticClientId) { if (staticClientId) {
logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.metadata.issuer}`); logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.issuer}`);
return staticClientId; return staticClientId;
} }
return await registerOidcClient(delegatedAuthConfig, await PlatformPeg.get()!.getOidcClientMetadata()); return await registerOidcClient(delegatedAuthConfig, await PlatformPeg.get()!.getOidcClientMetadata());

View File

@@ -12,7 +12,7 @@ Please see LICENSE files in the repository root for full details.
import { MatrixClient, Room, MatrixEvent, OidcRegistrationClientMetadata } from "matrix-js-sdk/src/matrix"; import { MatrixClient, Room, MatrixEvent, OidcRegistrationClientMetadata } from "matrix-js-sdk/src/matrix";
import React from "react"; import React from "react";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import BasePlatform, { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform"; import BasePlatform, { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform";
@@ -93,7 +93,7 @@ export default class ElectronPlatform extends BasePlatform {
private readonly ipc = new IPCManager("ipcCall", "ipcReply"); private readonly ipc = new IPCManager("ipcCall", "ipcReply");
private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager(); private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager();
// this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile // this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile
private readonly ssoID: string = randomString(32); private readonly ssoID: string = secureRandomString(32);
public constructor() { public constructor() {
super(); super();

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import "blob-polyfill"; import "blob-polyfill";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { PredictableRandom } from "./test-utils/predictableRandom"; // https://github.com/jsdom/jsdom/issues/2555 import { PredictableRandom } from "./test-utils/predictableRandom"; // https://github.com/jsdom/jsdom/issues/2555
@@ -25,7 +25,8 @@ jest.mock("matrix-js-sdk/src/randomstring");
beforeEach(() => { beforeEach(() => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const mockRandom = new PredictableRandom(); const mockRandom = new PredictableRandom();
mocked(randomString).mockImplementation((len) => { // needless to say, the mock is not cryptographically secure
mocked(secureRandomString).mockImplementation((len) => {
let ret = ""; let ret = "";
for (let i = 0; i < len; ++i) { for (let i = 0; i < len; ++i) {
const v = mockRandom.get() * chars.length; const v = mockRandom.get() * chars.length;

View File

@@ -6,41 +6,4 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { OidcClientConfig } from "matrix-js-sdk/src/matrix"; export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "matrix-js-sdk/src/testing";
import { ValidatedIssuerMetadata } from "matrix-js-sdk/src/oidc/validate";
/**
* Makes a valid OidcClientConfig with minimum valid values
* @param issuer used as the base for all other urls
* @returns OidcClientConfig
*/
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
const metadata = mockOpenIdConfiguration(issuer);
return {
accountManagementEndpoint: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
metadata,
};
};
/**
* Useful for mocking <issuer>/.well-known/openid-configuration
* @param issuer used as the base for all other urls
* @returns ValidatedIssuerMetadata
*/
export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): ValidatedIssuerMetadata => ({
issuer,
revocation_endpoint: issuer + "revoke",
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"],
account_management_uri: issuer + "account",
});

View File

@@ -18,7 +18,7 @@ import {
M_POLL_RESPONSE, M_POLL_RESPONSE,
M_TEXT, M_TEXT,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { flushPromises } from "./utilities"; import { flushPromises } from "./utilities";
@@ -67,7 +67,7 @@ export const makePollEndEvent = (
id?: string, id?: string,
): MatrixEvent => { ): MatrixEvent => {
return new MatrixEvent({ return new MatrixEvent({
event_id: id || randomString(16), event_id: id || secureRandomString(16),
room_id: roomId, room_id: roomId,
origin_server_ts: ts, origin_server_ts: ts,
type: M_POLL_END.name, type: M_POLL_END.name,
@@ -91,7 +91,7 @@ export const makePollResponseEvent = (
ts = 0, ts = 0,
): MatrixEvent => ): MatrixEvent =>
new MatrixEvent({ new MatrixEvent({
event_id: randomString(16), event_id: secureRandomString(16),
room_id: roomId, room_id: roomId,
origin_server_ts: ts, origin_server_ts: ts,
type: M_POLL_RESPONSE.name, type: M_POLL_RESPONSE.name,

View File

@@ -749,11 +749,8 @@ describe("Lifecycle", () => {
"eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJzdWIiOiIwMUhQUDJGU0JZREU5UDlFTU04REQ3V1pIUiIsImlzcyI6Imh0dHBzOi8vYXV0aC1vaWRjLmxhYi5lbGVtZW50LmRldi8iLCJpYXQiOjE3MTUwNzE5ODUsImF1dGhfdGltZSI6MTcwNzk5MDMxMiwiY19oYXNoIjoidGt5R1RhUjU5aTk3YXoyTU4yMGdidyIsImV4cCI6MTcxNTA3NTU4NSwibm9uY2UiOiJxaXhwM0hFMmVaIiwiYXVkIjoiMDFIWDk0Mlg3QTg3REgxRUs2UDRaNjI4WEciLCJhdF9oYXNoIjoiNFlFUjdPRlVKTmRTeEVHV2hJUDlnZyJ9.HxODneXvSTfWB5Vc4cf7b8GiN2gdwUuTiyVqZuupWske2HkZiJZUt5Lsxg9BW3gz28POkE0Ln17snlkmy02B_AD3DQxKOOxQCzIIARHdfFvZxgGWsMdFcVQZDW7rtXcqgj-SpVaUQ_8acsgxSrz_DF2o0O4tto0PT6wVUiw8KlBmgWTscWPeAWe-39T-8EiQ8Wi16h6oSPcz2NzOQ7eOM_S9fDkOorgcBkRGLl1nrahrPSdWJSGAeruk5mX4YxN714YThFDyEA2t9YmKpjaiSQ2tT-Xkd7tgsZqeirNs2ni9mIiFX3bRX6t2AhUNzA7MaX9ZyizKGa6go3BESO_oDg"; "eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJzdWIiOiIwMUhQUDJGU0JZREU5UDlFTU04REQ3V1pIUiIsImlzcyI6Imh0dHBzOi8vYXV0aC1vaWRjLmxhYi5lbGVtZW50LmRldi8iLCJpYXQiOjE3MTUwNzE5ODUsImF1dGhfdGltZSI6MTcwNzk5MDMxMiwiY19oYXNoIjoidGt5R1RhUjU5aTk3YXoyTU4yMGdidyIsImV4cCI6MTcxNTA3NTU4NSwibm9uY2UiOiJxaXhwM0hFMmVaIiwiYXVkIjoiMDFIWDk0Mlg3QTg3REgxRUs2UDRaNjI4WEciLCJhdF9oYXNoIjoiNFlFUjdPRlVKTmRTeEVHV2hJUDlnZyJ9.HxODneXvSTfWB5Vc4cf7b8GiN2gdwUuTiyVqZuupWske2HkZiJZUt5Lsxg9BW3gz28POkE0Ln17snlkmy02B_AD3DQxKOOxQCzIIARHdfFvZxgGWsMdFcVQZDW7rtXcqgj-SpVaUQ_8acsgxSrz_DF2o0O4tto0PT6wVUiw8KlBmgWTscWPeAWe-39T-8EiQ8Wi16h6oSPcz2NzOQ7eOM_S9fDkOorgcBkRGLl1nrahrPSdWJSGAeruk5mX4YxN714YThFDyEA2t9YmKpjaiSQ2tT-Xkd7tgsZqeirNs2ni9mIiFX3bRX6t2AhUNzA7MaX9ZyizKGa6go3BESO_oDg";
beforeAll(() => { beforeAll(() => {
fetchMock.get( fetchMock.get(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`, delegatedAuthConfig);
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`, fetchMock.get(`${delegatedAuthConfig.issuer}jwks`, {
delegatedAuthConfig.metadata,
);
fetchMock.get(`${delegatedAuthConfig.metadata.issuer}jwks`, {
status: 200, status: 200,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -772,9 +769,7 @@ describe("Lifecycle", () => {
await setLoggedIn(credentials); await setLoggedIn(credentials);
// didn't try to initialise token refresher // didn't try to initialise token refresher
expect(fetchMock).not.toHaveFetched( expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
);
}); });
it("should not try to create a token refresher without a deviceId", async () => { it("should not try to create a token refresher without a deviceId", async () => {
@@ -785,9 +780,7 @@ describe("Lifecycle", () => {
}); });
// didn't try to initialise token refresher // didn't try to initialise token refresher
expect(fetchMock).not.toHaveFetched( expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
);
}); });
it("should not try to create a token refresher without an issuer in session storage", async () => { it("should not try to create a token refresher without an issuer in session storage", async () => {
@@ -803,9 +796,7 @@ describe("Lifecycle", () => {
}); });
// didn't try to initialise token refresher // didn't try to initialise token refresher
expect(fetchMock).not.toHaveFetched( expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`);
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
);
}); });
it("should create a client with a tokenRefreshFunction", async () => { it("should create a client with a tokenRefreshFunction", async () => {

View File

@@ -384,7 +384,7 @@ describe("Login", function () {
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// didn't try to register // didn't try to register
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registrationEndpoint); expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registration_endpoint);
// continued with normal setup // continued with normal setup
expect(mockClient.loginFlows).toHaveBeenCalled(); expect(mockClient.loginFlows).toHaveBeenCalled();
// normal password login rendered // normal password login rendered
@@ -394,25 +394,25 @@ describe("Login", function () {
it("should attempt to register oidc client", async () => { it("should attempt to register oidc client", async () => {
// dont mock, spy so we can check config values were correctly passed // dont mock, spy so we can check config values were correctly passed
jest.spyOn(registerClientUtils, "getOidcClientId"); jest.spyOn(registerClientUtils, "getOidcClientId");
fetchMock.post(delegatedAuth.registrationEndpoint!, { status: 500 }); fetchMock.post(delegatedAuth.registration_endpoint!, { status: 500 });
getComponent(hsUrl, isUrl, delegatedAuth); getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// tried to register // tried to register
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registrationEndpoint, expect.any(Object)); expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registration_endpoint, expect.any(Object));
// called with values from config // called with values from config
expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(delegatedAuth, oidcStaticClientsConfig); expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(delegatedAuth, oidcStaticClientsConfig);
}); });
it("should fallback to normal login when client registration fails", async () => { it("should fallback to normal login when client registration fails", async () => {
fetchMock.post(delegatedAuth.registrationEndpoint!, { status: 500 }); fetchMock.post(delegatedAuth.registration_endpoint!, { status: 500 });
getComponent(hsUrl, isUrl, delegatedAuth); getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// tried to register // tried to register
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registrationEndpoint, expect.any(Object)); expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registration_endpoint, expect.any(Object));
expect(logger.error).toHaveBeenCalledWith(new Error(OidcError.DynamicRegistrationFailed)); expect(logger.error).toHaveBeenCalledWith(new Error(OidcError.DynamicRegistrationFailed));
// continued with normal setup // continued with normal setup
@@ -423,7 +423,7 @@ describe("Login", function () {
// short term during active development, UI will be added in next PRs // short term during active development, UI will be added in next PRs
it("should show continue button when oidc native flow is correctly configured", async () => { it("should show continue button when oidc native flow is correctly configured", async () => {
fetchMock.post(delegatedAuth.registrationEndpoint!, { client_id: "abc123" }); fetchMock.post(delegatedAuth.registration_endpoint!, { client_id: "abc123" });
getComponent(hsUrl, isUrl, delegatedAuth); getComponent(hsUrl, isUrl, delegatedAuth);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
@@ -455,7 +455,7 @@ describe("Login", function () {
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…")); await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
// didn't try to register // didn't try to register
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registrationEndpoint); expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registration_endpoint);
// continued with normal setup // continued with normal setup
expect(mockClient.loginFlows).toHaveBeenCalled(); expect(mockClient.loginFlows).toHaveBeenCalled();
// oidc-aware 'continue' button displayed // oidc-aware 'continue' button displayed

View File

@@ -158,24 +158,26 @@ describe("Registration", function () {
describe("when delegated authentication is configured and enabled", () => { describe("when delegated authentication is configured and enabled", () => {
const authConfig = makeDelegatedAuthConfig(); const authConfig = makeDelegatedAuthConfig();
const clientId = "test-client-id"; const clientId = "test-client-id";
// @ts-ignore authConfig.prompt_values_supported = ["create"];
authConfig.metadata["prompt_values_supported"] = ["create"];
beforeEach(() => { beforeEach(() => {
// mock a statically registered client to avoid dynamic registration // mock a statically registered client to avoid dynamic registration
SdkConfig.put({ SdkConfig.put({
oidc_static_clients: { oidc_static_clients: {
[authConfig.metadata.issuer]: { [authConfig.issuer]: {
client_id: clientId, client_id: clientId,
}, },
}, },
}); });
fetchMock.get(`${defaultHsUrl}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`, { fetchMock.get(`${defaultHsUrl}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`, {
issuer: authConfig.metadata.issuer, issuer: authConfig.issuer,
}); });
fetchMock.get("https://auth.org/.well-known/openid-configuration", authConfig.metadata); fetchMock.get("https://auth.org/.well-known/openid-configuration", {
fetchMock.get(authConfig.metadata.jwks_uri!, { keys: [] }); ...authConfig,
signingKeys: undefined,
});
fetchMock.get(authConfig.jwks_uri!, { keys: [] });
}); });
it("should display oidc-native continue button", async () => { it("should display oidc-native continue button", async () => {

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React from "react";
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MatrixClient, MatrixEvent, PushRuleKind } from "matrix-js-sdk/src/matrix";
import { mocked, MockedObject } from "jest-mock"; import { mocked, MockedObject } from "jest-mock";
import { render, waitFor } from "jest-matrix-react"; import { render, waitFor } from "jest-matrix-react";
@@ -228,6 +228,23 @@ describe("<TextualBody />", () => {
const content = container.querySelector(".mx_EventTile_body"); const content = container.querySelector(".mx_EventTile_body");
expect(content.innerHTML.replace(defaultEvent.getId(), "%event_id%")).toMatchSnapshot(); expect(content.innerHTML.replace(defaultEvent.getId(), "%event_id%")).toMatchSnapshot();
}); });
it("should pillify a keyword responsible for triggering a notification", () => {
const ev = mkRoomTextMessage("foo bar baz");
ev.setPushDetails(undefined, {
actions: [],
pattern: "bar",
rule_id: "bar",
default: false,
enabled: true,
kind: PushRuleKind.ContentSpecific,
});
const { container } = getComponent({ mxEvent: ev });
const content = container.querySelector(".mx_EventTile_body");
expect(content.innerHTML).toMatchInlineSnapshot(
`"<span>foo <bdi><span tabindex="0"><span class="mx_Pill mx_KeywordPill"><span class="mx_Pill_text">bar</span></span></span></bdi> baz</span>"`,
);
});
}); });
describe("renders formatted m.text correctly", () => { describe("renders formatted m.text correctly", () => {

View File

@@ -224,7 +224,29 @@ exports[`MemberTileView ThreePidInviteTileView renders ThreePidInvite correctly
</div> </div>
<div <div
class="mx_MemberTileView_right" class="mx_MemberTileView_right"
>
<div
class="mx_MemberTileView_userLabel"
>
Invited
</div>
<div
class="mx_Flex mx_InvitedIconView"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<svg
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm0 5.111a1 1 0 0 0 .514.874l7 3.89a1 1 0 0 0 .972 0l7-3.89a1 1 0 1 0-.972-1.748L12 11.856 5.486 8.237A1 1 0 0 0 4 9.111Z"
/> />
</svg>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -23,7 +23,7 @@ import {
IThreepid, IThreepid,
ThreepidMedium, ThreepidMedium,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { import {
act, act,
fireEvent, fireEvent,
@@ -287,7 +287,7 @@ describe("<Notifications />", () => {
beforeEach(async () => { beforeEach(async () => {
let i = 0; let i = 0;
mocked(randomString).mockImplementation(() => { mocked(secureRandomString).mockImplementation(() => {
return "testid_" + i++; return "testid_" + i++;
}); });

View File

@@ -57,7 +57,7 @@ import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { getClientInformationEventType } from "../../../../../../../src/utils/device/clientInformation"; import { getClientInformationEventType } from "../../../../../../../src/utils/device/clientInformation";
import { SDKContext, SdkContextClass } from "../../../../../../../src/contexts/SDKContext"; import { SDKContext, SdkContextClass } from "../../../../../../../src/contexts/SDKContext";
import { OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore"; import { OidcClientStore } from "../../../../../../../src/stores/oidc/OidcClientStore";
import { mockOpenIdConfiguration } from "../../../../../../test-utils/oidc"; import { makeDelegatedAuthConfig } from "../../../../../../test-utils/oidc";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext"; import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
mockPlatformPeg(); mockPlatformPeg();
@@ -215,7 +215,7 @@ describe("<SessionManagerTab />", () => {
getPushers: jest.fn(), getPushers: jest.fn(),
setPusher: jest.fn(), setPusher: jest.fn(),
setLocalNotificationSettings: jest.fn(), setLocalNotificationSettings: jest.fn(),
getAuthIssuer: jest.fn().mockReturnValue(new Promise(() => {})), getAuthMetadata: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404)),
}); });
jest.clearAllMocks(); jest.clearAllMocks();
jest.spyOn(logger, "error").mockRestore(); jest.spyOn(logger, "error").mockRestore();
@@ -1615,7 +1615,6 @@ describe("<SessionManagerTab />", () => {
describe("MSC4108 QR code login", () => { describe("MSC4108 QR code login", () => {
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue"); const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
const issuer = "https://issuer.org"; const issuer = "https://issuer.org";
const openIdConfiguration = mockOpenIdConfiguration(issuer);
beforeEach(() => { beforeEach(() => {
settingsValueSpy.mockClear().mockReturnValue(true); settingsValueSpy.mockClear().mockReturnValue(true);
@@ -1631,16 +1630,16 @@ describe("<SessionManagerTab />", () => {
enabled: true, enabled: true,
}, },
}); });
mockClient.getAuthIssuer.mockResolvedValue({ issuer }); const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
mockCrypto.exportSecretsBundle = jest.fn(); mockClient.getAuthMetadata.mockResolvedValue({
fetchMock.mock(`${issuer}/.well-known/openid-configuration`, { ...delegatedAuthConfig,
...openIdConfiguration,
grant_types_supported: [ grant_types_supported: [
...openIdConfiguration.grant_types_supported, ...delegatedAuthConfig.grant_types_supported,
"urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:device_code",
], ],
}); });
fetchMock.mock(openIdConfiguration.jwks_uri!, { mockCrypto.exportSecretsBundle = jest.fn();
fetchMock.mock(delegatedAuthConfig.jwks_uri!, {
status: 200, status: 200,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react"; import React from "react";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { act, fireEvent, render, RenderResult } from "jest-matrix-react"; import { act, fireEvent, render, RenderResult } from "jest-matrix-react";
import { EventType, MatrixClient, Room, GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/matrix"; import { EventType, MatrixClient, Room, GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/matrix";
@@ -92,7 +92,7 @@ describe("<SpaceSettingsVisibilityTab />", () => {
beforeEach(() => { beforeEach(() => {
let i = 0; let i = 0;
mocked(randomString).mockImplementation(() => { mocked(secureRandomString).mockImplementation(() => {
return "testid_" + i++; return "testid_" + i++;
}); });

View File

@@ -15,7 +15,7 @@ import { OidcError } from "matrix-js-sdk/src/oidc/error";
import { OidcClientStore } from "../../../../src/stores/oidc/OidcClientStore"; import { OidcClientStore } from "../../../../src/stores/oidc/OidcClientStore";
import { flushPromises, getMockClientWithEventEmitter, mockPlatformPeg } from "../../../test-utils"; import { flushPromises, getMockClientWithEventEmitter, mockPlatformPeg } from "../../../test-utils";
import { mockOpenIdConfiguration } from "../../../test-utils/oidc"; import { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
jest.mock("matrix-js-sdk/src/matrix", () => ({ jest.mock("matrix-js-sdk/src/matrix", () => ({
...jest.requireActual("matrix-js-sdk/src/matrix"), ...jest.requireActual("matrix-js-sdk/src/matrix"),
@@ -24,28 +24,30 @@ jest.mock("matrix-js-sdk/src/matrix", () => ({
describe("OidcClientStore", () => { describe("OidcClientStore", () => {
const clientId = "test-client-id"; const clientId = "test-client-id";
const metadata = mockOpenIdConfiguration(); const authConfig = makeDelegatedAuthConfig();
const account = metadata.issuer + "account"; const account = authConfig.issuer + "account";
const mockClient = getMockClientWithEventEmitter({ const mockClient = getMockClientWithEventEmitter({
getAuthIssuer: jest.fn(), getAuthMetadata: jest.fn(),
}); });
beforeEach(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
localStorage.setItem("mx_oidc_client_id", clientId); localStorage.setItem("mx_oidc_client_id", clientId);
localStorage.setItem("mx_oidc_token_issuer", metadata.issuer); localStorage.setItem("mx_oidc_token_issuer", authConfig.issuer);
mocked(discoverAndValidateOIDCIssuerWellKnown).mockClear().mockResolvedValue({ mocked(discoverAndValidateOIDCIssuerWellKnown)
metadata, .mockClear()
accountManagementEndpoint: account, .mockResolvedValue({
authorizationEndpoint: "authorization-endpoint", ...authConfig,
tokenEndpoint: "token-endpoint", account_management_uri: account,
authorization_endpoint: "authorization-endpoint",
token_endpoint: "token-endpoint",
}); });
jest.spyOn(logger, "error").mockClear(); jest.spyOn(logger, "error").mockClear();
fetchMock.get(`${metadata.issuer}.well-known/openid-configuration`, metadata); fetchMock.get(`${authConfig.issuer}.well-known/openid-configuration`, authConfig);
fetchMock.get(`${metadata.issuer}jwks`, { keys: [] }); fetchMock.get(`${authConfig.issuer}jwks`, { keys: [] });
mockPlatformPeg(); mockPlatformPeg();
}); });
@@ -116,7 +118,7 @@ describe("OidcClientStore", () => {
const client = await store.getOidcClient(); const client = await store.getOidcClient();
expect(client?.settings.client_id).toEqual(clientId); expect(client?.settings.client_id).toEqual(clientId);
expect(client?.settings.authority).toEqual(metadata.issuer); expect(client?.settings.authority).toEqual(authConfig.issuer);
}); });
it("should set account management endpoint when configured", async () => { it("should set account management endpoint when configured", async () => {
@@ -129,17 +131,19 @@ describe("OidcClientStore", () => {
}); });
it("should set account management endpoint to issuer when not configured", async () => { it("should set account management endpoint to issuer when not configured", async () => {
mocked(discoverAndValidateOIDCIssuerWellKnown).mockClear().mockResolvedValue({ mocked(discoverAndValidateOIDCIssuerWellKnown)
metadata, .mockClear()
accountManagementEndpoint: undefined, .mockResolvedValue({
authorizationEndpoint: "authorization-endpoint", ...authConfig,
tokenEndpoint: "token-endpoint", account_management_uri: undefined,
authorization_endpoint: "authorization-endpoint",
token_endpoint: "token-endpoint",
}); });
const store = new OidcClientStore(mockClient); const store = new OidcClientStore(mockClient);
await store.readyPromise; await store.readyPromise;
expect(store.accountManagementEndpoint).toEqual(metadata.issuer); expect(store.accountManagementEndpoint).toEqual(authConfig.issuer);
}); });
it("should reuse initialised oidc client", async () => { it("should reuse initialised oidc client", async () => {
@@ -175,7 +179,7 @@ describe("OidcClientStore", () => {
fetchMock.resetHistory(); fetchMock.resetHistory();
fetchMock.post( fetchMock.post(
metadata.revocation_endpoint, authConfig.revocation_endpoint,
{ {
status: 200, status: 200,
}, },
@@ -197,7 +201,7 @@ describe("OidcClientStore", () => {
await store.revokeTokens(accessToken, refreshToken); await store.revokeTokens(accessToken, refreshToken);
expect(fetchMock).toHaveFetchedTimes(2, metadata.revocation_endpoint); expect(fetchMock).toHaveFetchedTimes(2, authConfig.revocation_endpoint);
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token"); expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(refreshToken, "refresh_token"); expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(refreshToken, "refresh_token");
}); });
@@ -206,14 +210,14 @@ describe("OidcClientStore", () => {
// fail once, then succeed // fail once, then succeed
fetchMock fetchMock
.postOnce( .postOnce(
metadata.revocation_endpoint, authConfig.revocation_endpoint,
{ {
status: 404, status: 404,
}, },
{ overwriteRoutes: true, sendAsJson: true }, { overwriteRoutes: true, sendAsJson: true },
) )
.post( .post(
metadata.revocation_endpoint, authConfig.revocation_endpoint,
{ {
status: 200, status: 200,
}, },
@@ -226,7 +230,7 @@ describe("OidcClientStore", () => {
"Failed to revoke tokens", "Failed to revoke tokens",
); );
expect(fetchMock).toHaveFetchedTimes(2, metadata.revocation_endpoint); expect(fetchMock).toHaveFetchedTimes(2, authConfig.revocation_endpoint);
expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token"); expect(OidcClient.prototype.revokeToken).toHaveBeenCalledWith(accessToken, "access_token");
}); });
}); });
@@ -237,7 +241,10 @@ describe("OidcClientStore", () => {
}); });
it("should resolve account management endpoint", async () => { it("should resolve account management endpoint", async () => {
mockClient.getAuthIssuer.mockResolvedValue({ issuer: metadata.issuer }); mockClient.getAuthMetadata.mockResolvedValue({
...authConfig,
account_management_uri: account,
});
const store = new OidcClientStore(mockClient); const store = new OidcClientStore(mockClient);
await store.readyPromise; await store.readyPromise;
expect(store.accountManagementEndpoint).toBe(account); expect(store.accountManagementEndpoint).toBe(account);

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { mocked, MockedObject } from "jest-mock"; import { mocked, MockedFunction, MockedObject } from "jest-mock";
import { last } from "lodash"; import { last } from "lodash";
import { import {
MatrixEvent, MatrixEvent,
@@ -15,15 +15,20 @@ import {
EventTimeline, EventTimeline,
EventType, EventType,
MatrixEventEvent, MatrixEventEvent,
RoomStateEvent,
RoomState,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { ClientWidgetApi, WidgetApiFromWidgetAction } from "matrix-widget-api"; import { ClientWidgetApi, WidgetApiFromWidgetAction } from "matrix-widget-api";
import { waitFor } from "jest-matrix-react"; import { waitFor } from "jest-matrix-react";
import { Optional } from "matrix-events-sdk";
import { stubClient, mkRoom, mkEvent } from "../../../test-utils"; import { stubClient, mkRoom, mkEvent } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget"; import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget";
import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore"; import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore";
import SettingsStore from "../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../src/settings/SettingsStore";
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore";
jest.mock("matrix-widget-api/lib/ClientWidgetApi"); jest.mock("matrix-widget-api/lib/ClientWidgetApi");
@@ -53,6 +58,7 @@ describe("StopGapWidget", () => {
// Start messaging without an iframe, since ClientWidgetApi is mocked // Start messaging without an iframe, since ClientWidgetApi is mocked
widget.startMessaging(null as unknown as HTMLIFrameElement); widget.startMessaging(null as unknown as HTMLIFrameElement);
messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!); messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!);
messaging.feedStateUpdate.mockResolvedValue();
}); });
afterEach(() => { afterEach(() => {
@@ -84,6 +90,20 @@ describe("StopGapWidget", () => {
expect(messaging.feedToDevice).toHaveBeenCalledWith(event.getEffectiveEvent(), false); expect(messaging.feedToDevice).toHaveBeenCalledWith(event.getEffectiveEvent(), false);
}); });
it("feeds incoming state updates to the widget", () => {
const event = mkEvent({
event: true,
type: "org.example.foo",
skey: "",
user: "@alice:example.org",
content: { hello: "world" },
room: "!1:example.org",
});
client.emit(RoomStateEvent.Events, event, {} as unknown as RoomState, null);
expect(messaging.feedStateUpdate).toHaveBeenCalledWith(event.getEffectiveEvent());
});
describe("feed event", () => { describe("feed event", () => {
let event1: MatrixEvent; let event1: MatrixEvent;
let event2: MatrixEvent; let event2: MatrixEvent;
@@ -118,24 +138,24 @@ describe("StopGapWidget", () => {
it("feeds incoming event to the widget", async () => { it("feeds incoming event to the widget", async () => {
client.emit(ClientEvent.Event, event1); client.emit(ClientEvent.Event, event1);
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org"); expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
client.emit(ClientEvent.Event, event2); client.emit(ClientEvent.Event, event2);
expect(messaging.feedEvent).toHaveBeenCalledTimes(2); expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org"); expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
}); });
it("should not feed incoming event to the widget if seen already", async () => { it("should not feed incoming event to the widget if seen already", async () => {
client.emit(ClientEvent.Event, event1); client.emit(ClientEvent.Event, event1);
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org"); expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
client.emit(ClientEvent.Event, event2); client.emit(ClientEvent.Event, event2);
expect(messaging.feedEvent).toHaveBeenCalledTimes(2); expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org"); expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
client.emit(ClientEvent.Event, event1); client.emit(ClientEvent.Event, event1);
expect(messaging.feedEvent).toHaveBeenCalledTimes(2); expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent(), "!1:example.org"); expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2.getEffectiveEvent());
}); });
it("feeds decrypted events asynchronously", async () => { it("feeds decrypted events asynchronously", async () => {
@@ -165,7 +185,7 @@ describe("StopGapWidget", () => {
decryptingSpy2.mockReturnValue(false); decryptingSpy2.mockReturnValue(false);
client.emit(MatrixEventEvent.Decrypted, event2Encrypted); client.emit(MatrixEventEvent.Decrypted, event2Encrypted);
expect(messaging.feedEvent).toHaveBeenCalledTimes(1); expect(messaging.feedEvent).toHaveBeenCalledTimes(1);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2Encrypted.getEffectiveEvent(), "!1:example.org"); expect(messaging.feedEvent).toHaveBeenLastCalledWith(event2Encrypted.getEffectiveEvent());
// …then event 1 // …then event 1
event1Encrypted.event.type = event1.getType(); event1Encrypted.event.type = event1.getType();
event1Encrypted.event.content = event1.getContent(); event1Encrypted.event.content = event1.getContent();
@@ -175,7 +195,7 @@ describe("StopGapWidget", () => {
// doesn't have to be blocked on the decryption of event 1 (or // doesn't have to be blocked on the decryption of event 1 (or
// worse, dropped) // worse, dropped)
expect(messaging.feedEvent).toHaveBeenCalledTimes(2); expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event1Encrypted.getEffectiveEvent(), "!1:example.org"); expect(messaging.feedEvent).toHaveBeenLastCalledWith(event1Encrypted.getEffectiveEvent());
}); });
it("should not feed incoming event if not in timeline", () => { it("should not feed incoming event if not in timeline", () => {
@@ -191,7 +211,7 @@ describe("StopGapWidget", () => {
}); });
client.emit(ClientEvent.Event, event); client.emit(ClientEvent.Event, event);
expect(messaging.feedEvent).toHaveBeenCalledWith(event.getEffectiveEvent(), "!1:example.org"); expect(messaging.feedEvent).toHaveBeenCalledWith(event.getEffectiveEvent());
}); });
it("feeds incoming event that is not in timeline but relates to unknown parent to the widget", async () => { it("feeds incoming event that is not in timeline but relates to unknown parent to the widget", async () => {
@@ -211,18 +231,19 @@ describe("StopGapWidget", () => {
}); });
client.emit(ClientEvent.Event, event1); client.emit(ClientEvent.Event, event1);
expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent(), "!1:example.org"); expect(messaging.feedEvent).toHaveBeenCalledWith(event1.getEffectiveEvent());
client.emit(ClientEvent.Event, event); client.emit(ClientEvent.Event, event);
expect(messaging.feedEvent).toHaveBeenCalledTimes(2); expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org"); expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent());
client.emit(ClientEvent.Event, event1); client.emit(ClientEvent.Event, event1);
expect(messaging.feedEvent).toHaveBeenCalledTimes(2); expect(messaging.feedEvent).toHaveBeenCalledTimes(2);
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org"); expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent());
}); });
}); });
}); });
describe("StopGapWidget with stickyPromise", () => { describe("StopGapWidget with stickyPromise", () => {
let client: MockedObject<MatrixClient>; let client: MockedObject<MatrixClient>;
let widget: StopGapWidget; let widget: StopGapWidget;
@@ -288,3 +309,49 @@ describe("StopGapWidget with stickyPromise", () => {
waitFor(() => expect(setPersistenceSpy).toHaveBeenCalled(), { interval: 5 }); waitFor(() => expect(setPersistenceSpy).toHaveBeenCalled(), { interval: 5 });
}); });
}); });
describe("StopGapWidget as an account widget", () => {
let widget: StopGapWidget;
let messaging: MockedObject<ClientWidgetApi>;
let getRoomId: MockedFunction<() => Optional<string>>;
beforeEach(() => {
stubClient();
// I give up, getting the return type of spyOn right is hopeless
getRoomId = jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId") as unknown as MockedFunction<
() => Optional<string>
>;
getRoomId.mockReturnValue("!1:example.org");
widget = new StopGapWidget({
app: {
id: "test",
creatorUserId: "@alice:example.org",
type: "example",
url: "https://example.org?user-id=$matrix_user_id&device-id=$org.matrix.msc3819.matrix_device_id&base-url=$org.matrix.msc4039.matrix_base_url&theme=$org.matrix.msc2873.client_theme",
roomId: "!1:example.org",
},
userId: "@alice:example.org",
creatorUserId: "@alice:example.org",
waitForIframeLoad: true,
userWidget: false,
});
// Start messaging without an iframe, since ClientWidgetApi is mocked
widget.startMessaging(null as unknown as HTMLIFrameElement);
messaging = mocked(last(mocked(ClientWidgetApi).mock.instances)!);
});
afterEach(() => {
widget.stopMessaging();
getRoomId.mockRestore();
});
it("updates viewed room", () => {
expect(messaging.setViewedRoomId).toHaveBeenCalledTimes(1);
expect(messaging.setViewedRoomId).toHaveBeenLastCalledWith("!1:example.org");
getRoomId.mockReturnValue("!2:example.org");
SdkContextClass.instance.roomViewStore.emit(UPDATE_EVENT);
expect(messaging.setViewedRoomId).toHaveBeenCalledTimes(2);
expect(messaging.setViewedRoomId).toHaveBeenLastCalledWith("!2:example.org");
});
});

View File

@@ -17,6 +17,7 @@ import {
MatrixEvent, MatrixEvent,
MsgType, MsgType,
RelationType, RelationType,
Room,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { import {
Widget, Widget,
@@ -38,7 +39,7 @@ import {
import { SdkContextClass } from "../../../../src/contexts/SDKContext"; import { SdkContextClass } from "../../../../src/contexts/SDKContext";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { StopGapWidgetDriver } from "../../../../src/stores/widgets/StopGapWidgetDriver"; import { StopGapWidgetDriver } from "../../../../src/stores/widgets/StopGapWidgetDriver";
import { stubClient } from "../../../test-utils"; import { mkEvent, stubClient } from "../../../test-utils";
import { ModuleRunner } from "../../../../src/modules/ModuleRunner"; import { ModuleRunner } from "../../../../src/modules/ModuleRunner";
import dis from "../../../../src/dispatcher/dispatcher"; import dis from "../../../../src/dispatcher/dispatcher";
import Modal from "../../../../src/Modal"; import Modal from "../../../../src/Modal";
@@ -569,7 +570,7 @@ describe("StopGapWidgetDriver", () => {
it("passes the flag through to getVisibleRooms", () => { it("passes the flag through to getVisibleRooms", () => {
const driver = mkDefaultDriver(); const driver = mkDefaultDriver();
driver.readRoomEvents(EventType.CallAnswer, "", 0, ["*"]); driver.getKnownRooms();
expect(client.getVisibleRooms).toHaveBeenCalledWith(false); expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
}); });
}); });
@@ -584,7 +585,7 @@ describe("StopGapWidgetDriver", () => {
it("passes the flag through to getVisibleRooms", () => { it("passes the flag through to getVisibleRooms", () => {
const driver = mkDefaultDriver(); const driver = mkDefaultDriver();
driver.readRoomEvents(EventType.CallAnswer, "", 0, ["*"]); driver.getKnownRooms();
expect(client.getVisibleRooms).toHaveBeenCalledWith(true); expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
}); });
}); });
@@ -692,4 +693,107 @@ describe("StopGapWidgetDriver", () => {
await expect(file.text()).resolves.toEqual("test contents"); await expect(file.text()).resolves.toEqual("test contents");
}); });
}); });
describe("readRoomTimeline", () => {
const event1 = mkEvent({
event: true,
id: "$event-id1",
type: "org.example.foo",
user: "@alice:example.org",
content: { hello: "world" },
room: "!1:example.org",
});
const event2 = mkEvent({
event: true,
id: "$event-id2",
type: "org.example.foo",
user: "@alice:example.org",
content: { hello: "world" },
room: "!1:example.org",
});
let driver: WidgetDriver;
beforeEach(() => {
driver = mkDefaultDriver();
client.getRoom.mockReturnValue({
getLiveTimeline: () => ({ getEvents: () => [event1, event2] }),
} as unknown as Room);
});
it("reads all events", async () => {
expect(
await driver.readRoomTimeline("!1:example.org", "org.example.foo", undefined, undefined, 10, undefined),
).toEqual([event2, event1].map((e) => e.getEffectiveEvent()));
});
it("reads up to a limit", async () => {
expect(
await driver.readRoomTimeline("!1:example.org", "org.example.foo", undefined, undefined, 1, undefined),
).toEqual([event2.getEffectiveEvent()]);
});
it("reads up to a specific event", async () => {
expect(
await driver.readRoomTimeline(
"!1:example.org",
"org.example.foo",
undefined,
undefined,
10,
event1.getId(),
),
).toEqual([event2.getEffectiveEvent()]);
});
});
describe("readRoomState", () => {
const event1 = mkEvent({
event: true,
id: "$event-id1",
type: "org.example.foo",
user: "@alice:example.org",
content: { hello: "world" },
skey: "1",
room: "!1:example.org",
});
const event2 = mkEvent({
event: true,
id: "$event-id2",
type: "org.example.foo",
user: "@alice:example.org",
content: { hello: "world" },
skey: "2",
room: "!1:example.org",
});
let driver: WidgetDriver;
let getStateEvents: jest.Mock;
beforeEach(() => {
driver = mkDefaultDriver();
getStateEvents = jest.fn();
client.getRoom.mockReturnValue({
getLiveTimeline: () => ({ getState: () => ({ getStateEvents }) }),
} as unknown as Room);
});
it("reads a specific state key", async () => {
getStateEvents.mockImplementation((eventType, stateKey) => {
if (eventType === "org.example.foo" && stateKey === "1") return event1;
return undefined;
});
expect(await driver.readRoomState("!1:example.org", "org.example.foo", "1")).toEqual([
event1.getEffectiveEvent(),
]);
});
it("reads all state keys", async () => {
getStateEvents.mockImplementation((eventType, stateKey) => {
if (eventType === "org.example.foo" && stateKey === undefined) return [event1, event2];
return [];
});
expect(await driver.readRoomState("!1:example.org", "org.example.foo", undefined)).toEqual(
[event1, event2].map((e) => e.getEffectiveEvent()),
);
});
});
}); });

View File

@@ -355,21 +355,19 @@ describe("AutoDiscoveryUtils", () => {
hsNameIsDifferent: true, hsNameIsDifferent: true,
hsName: serverName, hsName: serverName,
delegatedAuthentication: expect.objectContaining({ delegatedAuthentication: expect.objectContaining({
accountManagementActionsSupported: [ issuer,
account_management_actions_supported: [
"org.matrix.profile", "org.matrix.profile",
"org.matrix.sessions_list", "org.matrix.sessions_list",
"org.matrix.session_view", "org.matrix.session_view",
"org.matrix.session_end", "org.matrix.session_end",
"org.matrix.cross_signing_reset", "org.matrix.cross_signing_reset",
], ],
accountManagementEndpoint: "https://auth.matrix.org/account/", account_management_uri: "https://auth.matrix.org/account/",
authorizationEndpoint: "https://auth.matrix.org/auth", authorization_endpoint: "https://auth.matrix.org/auth",
metadata: expect.objectContaining({ registration_endpoint: "https://auth.matrix.org/registration",
issuer,
}),
registrationEndpoint: "https://auth.matrix.org/registration",
signingKeys: [], signingKeys: [],
tokenEndpoint: "https://auth.matrix.org/token", token_endpoint: "https://auth.matrix.org/token",
}), }),
warning: null, warning: null,
}); });

View File

@@ -38,7 +38,7 @@ describe("TokenRefresher", () => {
}; };
beforeEach(() => { beforeEach(() => {
fetchMock.get(`${issuer}.well-known/openid-configuration`, authConfig.metadata); fetchMock.get(`${issuer}.well-known/openid-configuration`, authConfig);
fetchMock.get(`${issuer}jwks`, { fetchMock.get(`${issuer}jwks`, {
status: 200, status: 200,
headers: { headers: {

View File

@@ -49,7 +49,7 @@ describe("OIDC authorization", () => {
origin: baseUrl, origin: baseUrl,
}; };
jest.spyOn(randomStringUtils, "randomString").mockRestore(); jest.spyOn(randomStringUtils, "secureRandomString").mockRestore();
mockPlatformPeg(); mockPlatformPeg();
Object.defineProperty(window, "crypto", { Object.defineProperty(window, "crypto", {
value: { value: {
@@ -61,10 +61,7 @@ describe("OIDC authorization", () => {
}); });
beforeAll(() => { beforeAll(() => {
fetchMock.get( fetchMock.get(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`, delegatedAuthConfig);
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
delegatedAuthConfig.metadata,
);
}); });
afterAll(() => { afterAll(() => {

View File

@@ -58,7 +58,7 @@ describe("getOidcClientId()", () => {
const authConfigWithoutRegistration: OidcClientConfig = makeDelegatedAuthConfig( const authConfigWithoutRegistration: OidcClientConfig = makeDelegatedAuthConfig(
"https://issuerWithoutStaticClientId.org/", "https://issuerWithoutStaticClientId.org/",
); );
authConfigWithoutRegistration.registrationEndpoint = undefined; authConfigWithoutRegistration.registration_endpoint = undefined;
await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow( await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow(
OidcError.DynamicRegistrationNotSupported, OidcError.DynamicRegistrationNotSupported,
); );
@@ -69,7 +69,7 @@ describe("getOidcClientId()", () => {
it("should handle when staticOidcClients object is falsy", async () => { it("should handle when staticOidcClients object is falsy", async () => {
const authConfigWithoutRegistration: OidcClientConfig = { const authConfigWithoutRegistration: OidcClientConfig = {
...delegatedAuthConfig, ...delegatedAuthConfig,
registrationEndpoint: undefined, registration_endpoint: undefined,
}; };
await expect(getOidcClientId(authConfigWithoutRegistration)).rejects.toThrow( await expect(getOidcClientId(authConfigWithoutRegistration)).rejects.toThrow(
OidcError.DynamicRegistrationNotSupported, OidcError.DynamicRegistrationNotSupported,
@@ -79,14 +79,14 @@ describe("getOidcClientId()", () => {
}); });
it("should make correct request to register client", async () => { it("should make correct request to register client", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, { fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 200, status: 200,
body: JSON.stringify({ client_id: dynamicClientId }), body: JSON.stringify({ client_id: dynamicClientId }),
}); });
expect(await getOidcClientId(delegatedAuthConfig)).toEqual(dynamicClientId); expect(await getOidcClientId(delegatedAuthConfig)).toEqual(dynamicClientId);
// didn't try to register // didn't try to register
expect(fetchMockJest).toHaveBeenCalledWith( expect(fetchMockJest).toHaveBeenCalledWith(
delegatedAuthConfig.registrationEndpoint!, delegatedAuthConfig.registration_endpoint!,
expect.objectContaining({ expect.objectContaining({
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",
@@ -111,14 +111,14 @@ describe("getOidcClientId()", () => {
}); });
it("should throw when registration request fails", async () => { it("should throw when registration request fails", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, { fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 500, status: 500,
}); });
await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationFailed); await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationFailed);
}); });
it("should throw when registration response is invalid", async () => { it("should throw when registration response is invalid", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, { fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 200, status: 200,
// no clientId in response // no clientId in response
body: "{}", body: "{}",

1960
yarn.lock

File diff suppressed because it is too large Load Diff