From 00c41264b57cd90c7cdad269d09d2f4033aec1c2 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Thu, 16 Apr 2026 13:54:43 +0100 Subject: [PATCH] PoC Sign in with QR on new EW using generated QR for MSC4108 v2024 --- apps/web/package.json | 2 +- apps/web/src/Lifecycle.ts | 46 ++++++-- apps/web/src/Login.ts | 37 +++++- .../src/components/structures/MatrixChat.tsx | 6 +- .../src/components/structures/auth/Login.tsx | 100 +++++++++++++--- .../src/components/views/auth/LoginWithQR.tsx | 111 +++++++++++++++--- .../settings/devices/LoginWithQRSection.tsx | 16 ++- pnpm-lock.yaml | 22 ++-- 8 files changed, 270 insertions(+), 70 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 655c44ab11..1fb58c885f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -81,7 +81,7 @@ "lodash": "npm:lodash-es@^4.17.21", "maplibre-gl": "^5.0.0", "matrix-encrypt-attachment": "^1.0.3", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#hughns/msc4108-2024-signin-on-new-device", "matrix-widget-api": "^1.16.1", "memoize-one": "^6.0.0", "mime": "^4.0.4", diff --git a/apps/web/src/Lifecycle.ts b/apps/web/src/Lifecycle.ts index 78c6cc2de6..19bca3dffd 100644 --- a/apps/web/src/Lifecycle.ts +++ b/apps/web/src/Lifecycle.ts @@ -297,7 +297,43 @@ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise const { accessToken, refreshToken, homeserverUrl, identityServerUrl, idToken, clientId, issuer } = await completeOidcLogin(queryParams); - const { + await configureFromCompletedOAuthLogin({ + accessToken, + refreshToken, + homeserverUrl, + identityServerUrl, + clientId, + issuer, + idToken, + }); + + return true; + } catch (error) { + logger.error("Failed to login via OIDC", error); + + onFailedDelegatedAuthLogin(getOidcErrorMessage(error as Error)); + return false; + } +} + +export async function configureFromCompletedOAuthLogin({ + accessToken, + refreshToken, + homeserverUrl, + identityServerUrl, + clientId, + issuer, + idToken, +}: { + accessToken: string; + refreshToken?: string; + homeserverUrl: string; + identityServerUrl?: string; + clientId: string; + issuer: string; + idToken: string; +}): Promise { + const { user_id: userId, device_id: deviceId, is_guest: isGuest, @@ -317,14 +353,8 @@ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise await onSuccessfulDelegatedAuthLogin(credentials); // this needs to happen after success handler which clears storages persistOidcAuthenticatedSettings(clientId, issuer, idToken); - return true; - } catch (error) { - logger.error("Failed to login via OIDC", error); - - onFailedDelegatedAuthLogin(getOidcErrorMessage(error as Error)); - return false; + return credentials; } -} /** * Gets information about the owner of a given access token. diff --git a/apps/web/src/Login.ts b/apps/web/src/Login.ts index 34b6513ad1..7693420713 100644 --- a/apps/web/src/Login.ts +++ b/apps/web/src/Login.ts @@ -18,6 +18,7 @@ import { type ISSOFlow, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; +import { isSignInWithQRAvailable } from "matrix-js-sdk/src/rendezvous"; import { type IMatrixClientCreds } from "./MatrixClientPeg"; import { ModuleRunner } from "./modules/ModuleRunner"; @@ -31,7 +32,12 @@ import { isUserRegistrationSupported } from "./utils/oidc/isUserRegistrationSupp * LoginFlow type use the client API /login endpoint * OidcNativeFlow is specific to this client */ -export type ClientLoginFlow = LoginFlow | OidcNativeFlow; +export type ClientLoginFlow = LoginFlow | OidcNativeFlow | LoginWithQrFlow; + +export interface LoginWithQrFlow { + type: "loginWithQrFlow"; + clientId: string; +} interface ILoginOptions { defaultDeviceDisplayName?: string; @@ -116,7 +122,17 @@ export default class Login { SdkConfig.get().oidc_static_clients, isRegistration, ); - return [oidcFlow]; + let possibleQrFlow: LoginWithQrFlow | undefined; + try { + // TODO: this seems wasteful + const tempClient = this.createTemporaryClient(); + // we reuse the clientId from the oidcFlow for QR login + // it might be that we later find that the homeserver is different and we initialise a new client + possibleQrFlow = await tryInitLoginWithQRFlow(tempClient, oidcFlow.clientId); + } catch (e) { + logger.warn("Could not fetch server versions for login with QR support, assuming unsupported", e); + } + return possibleQrFlow ? [possibleQrFlow, oidcFlow] : [oidcFlow]; } catch (error) { logger.error("Failed to get oidc native flow", error); } @@ -288,3 +304,20 @@ export async function sendLoginRequest( return creds; } + +const tryInitLoginWithQRFlow = async ( + tempClient: MatrixClient, + clientId: string, +): Promise => { + // This could fail because the server doesn't support the API or it requires authentication + const canUseServer = await isSignInWithQRAvailable(tempClient); + + if (!canUseServer) return undefined; + + const flow = { + type: "loginWithQrFlow", + clientId, + } satisfies LoginWithQrFlow; + + return flow; +}; diff --git a/apps/web/src/components/structures/MatrixChat.tsx b/apps/web/src/components/structures/MatrixChat.tsx index ce5bfd4598..7de12415ff 100644 --- a/apps/web/src/components/structures/MatrixChat.tsx +++ b/apps/web/src/components/structures/MatrixChat.tsx @@ -2130,9 +2130,11 @@ export default class MatrixChat extends React.PureComponent { * Note: SSO users (and any others using token login) currently do not pass through * this, as they instead jump straight into the app after `attemptTokenLogin`. */ - private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds): Promise => { + private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, alreadySignedIn = false): Promise => { // Create and start the client - await Lifecycle.setLoggedIn(credentials); + if (!alreadySignedIn) { + await Lifecycle.setLoggedIn(credentials); + } await this.postLoginSetup(); PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN); diff --git a/apps/web/src/components/structures/auth/Login.tsx b/apps/web/src/components/structures/auth/Login.tsx index 582473a028..2b385f4dfe 100644 --- a/apps/web/src/components/structures/auth/Login.tsx +++ b/apps/web/src/components/structures/auth/Login.tsx @@ -9,11 +9,13 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX, memo, type ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; -import { type SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix"; +import { type SSOFlow, type MatrixClient, SSOAction } from "matrix-js-sdk/src/matrix"; import { Button } from "@vector-im/compound-web"; +import { QrCodeIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { secureRandomString } from "matrix-js-sdk/src/randomstring"; import { _t, UserFriendlyError } from "../../../languageHandler"; -import Login, { type ClientLoginFlow, type OidcNativeFlow } from "../../../Login"; +import Login, { type LoginWithQrFlow, type ClientLoginFlow, type OidcNativeFlow } from "../../../Login"; import { messageForConnectionError, messageForLoginError } from "../../../utils/ErrorUtils"; import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; import AuthPage from "../../views/auth/AuthPage"; @@ -33,6 +35,9 @@ import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig import { filterBoolean } from "../../../utils/arrays"; import { startOidcLogin } from "../../../utils/oidc/authorize"; import { ModuleApi } from "../../../modules/Api.ts"; +import LoginWithQR from "../../views/auth/LoginWithQR.tsx"; +import { Mode } from "../../views/auth/LoginWithQR-types.ts"; +import createMatrixClient from "../../../utils/createMatrixClient.ts"; interface IProps { serverConfig: ValidatedServerConfig; @@ -51,7 +56,8 @@ interface IProps { // Called when the user has logged in. Params: // - The object returned by the login API - onLoggedIn(data: IMatrixClientCreds): void; + // - alreadySignedIn: true if the user was already signed in (e.g. QR login) and only the post login setup is needed + onLoggedIn(data: IMatrixClientCreds, alreadySignedIn?: boolean): void; // login shouldn't know or care how registration, password recovery, etc is done. onRegisterClick?(): void; @@ -79,6 +85,9 @@ interface IState { serverIsAlive: boolean; serverErrorIsFatal: boolean; serverDeadError?: ReactNode; + + loginWithQrInProgress: boolean; + loginWithQrClient?: MatrixClient; } type OnPasswordLogin = { @@ -110,6 +119,8 @@ class LoginComponent extends React.PureComponent { serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", + + loginWithQrInProgress: false, }; // map from login step type to a function which will render a control @@ -123,6 +134,7 @@ class LoginComponent extends React.PureComponent { // eslint-disable-next-line @typescript-eslint/naming-convention "m.login.sso": () => this.renderSsoStep("sso"), "oidcNativeFlow": () => this.renderOidcNativeStep(), + "loginWithQrFlow": () => this.renderLoginWithQRStep(), }; } @@ -402,7 +414,7 @@ class LoginComponent extends React.PureComponent { if (!this.state.flows) return null; // this is the ideal order we want to show the flows in - const order = ["oidcNativeFlow", "m.login.password", "m.login.sso"]; + const order = ["loginWithQrFlow", "oidcNativeFlow", "m.login.password", "m.login.sso"]; const flows = filterBoolean(order.map((type) => this.state.flows?.find((flow) => flow.type === type))); return ( @@ -451,7 +463,7 @@ class LoginComponent extends React.PureComponent { ); }} > - {_t("action|continue")} + {_t("Sign in manually")} ); }; @@ -472,6 +484,42 @@ class LoginComponent extends React.PureComponent { ); }; + private startLoginWithQR = (): void => { + if (this.state.loginWithQrInProgress) return; + // pick our device ID + const deviceId = secureRandomString(10); + const loginWithQrClient = createMatrixClient({ + baseUrl: this.loginLogic.getHomeserverUrl(), + idBaseUrl: this.loginLogic.getIdentityServerUrl(), + deviceId, + }); + this.setState({ loginWithQrInProgress: true, loginWithQrClient }); + }; + + private renderLoginWithQRStep = (): JSX.Element => { + return ( + <> + + + ); + }; + + private onLoginWithQRFinished = (success: boolean, credentials?: IMatrixClientCreds): void => { + if (credentials) { + this.props.onLoggedIn(credentials, true); + } else if (!success) { + this.state.loginWithQrClient?.stopClient(); + this.setState({ loginWithQrInProgress: false, loginWithQrClient: undefined }); + } + }; + + private get qrClientId(): string { + return (this.state.flows?.find((flow) => flow.type === "loginWithQrFlow") as LoginWithQrFlow).clientId ?? ""; + } + public render(): React.ReactNode { const loader = this.isBusy() && !this.state.busyLoggingIn ? ( @@ -532,20 +580,34 @@ class LoginComponent extends React.PureComponent { -

- {_t("action|sign_in")} - {loader} -

- {errorTextSection} - {serverDeadSection} - - {this.renderLoginComponentForFlows()} - {this.props.children} - {footer} + {this.state.loginWithQrInProgress ? ( + <> + + + ) : ( + <> + {" "} +

+ {_t("action|sign_in")} + {loader} +

+ {errorTextSection} + {serverDeadSection} + + {this.renderLoginComponentForFlows()} + {this.props.children} + {footer} + + )}
); diff --git a/apps/web/src/components/views/auth/LoginWithQR.tsx b/apps/web/src/components/views/auth/LoginWithQR.tsx index 1519a1fb45..da542d6eba 100644 --- a/apps/web/src/components/views/auth/LoginWithQR.tsx +++ b/apps/web/src/components/views/auth/LoginWithQR.tsx @@ -9,24 +9,27 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { ClientRendezvousFailureReason, + linkNewDeviceByGeneratingQR, MSC4108FailureReason, - MSC4108RendezvousSession, - MSC4108SecureChannel, MSC4108SignInWithQR, RendezvousError, type RendezvousFailureReason, RendezvousIntent, + signInByGeneratingQR, } from "matrix-js-sdk/src/rendezvous"; import { logger } from "matrix-js-sdk/src/logger"; -import { type MatrixClient } from "matrix-js-sdk/src/matrix"; +import { AutoDiscovery, type MatrixClient } from "matrix-js-sdk/src/matrix"; import { Click, Mode, Phase } from "./LoginWithQR-types"; import LoginWithQRFlow from "./LoginWithQRFlow"; +import { configureFromCompletedOAuthLogin, restoreSessionFromStorage } from "../../../Lifecycle"; +import { type IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg"; interface IProps { client: MatrixClient; + clientId: string; mode: Mode; - onFinished(...args: any): void; + onFinished(success: boolean, credentials?: IMatrixClientCreds): void; } interface IState { @@ -37,6 +40,8 @@ interface IState { userCode?: string; checkCode?: string; failureReason?: FailureReason; + homeserverName?: string; + newClient?: MatrixClient; } export enum LoginWithQRFailureReason { @@ -65,7 +70,9 @@ export default class LoginWithQR extends React.Component { } private get ourIntent(): RendezvousIntent { - return RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE; + return this.props.client.getUserId() + ? RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE + : RendezvousIntent.LOGIN_ON_NEW_DEVICE; } public componentDidMount(): void { @@ -99,23 +106,18 @@ export default class LoginWithQR extends React.Component { } } - private onFinished(success: boolean): void { + private onFinished(success: boolean, credentials?: IMatrixClientCreds): void { this.finished = true; - this.props.onFinished(success); + this.props.onFinished(success, credentials); } private generateAndShowCode = async (): Promise => { let rendezvous: MSC4108SignInWithQR; try { - const transport = new MSC4108RendezvousSession({ - onFailure: this.onFailure, - client: this.props.client, - }); - await transport.send(""); - const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); - rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); - - await rendezvous.generateCode(); + rendezvous = + this.ourIntent === RendezvousIntent.LOGIN_ON_NEW_DEVICE + ? await signInByGeneratingQR(this.props.client, this.onFailure) + : await linkNewDeviceByGeneratingQR(this.props.client, this.onFailure); this.setState({ phase: Phase.ShowingQR, rendezvous, @@ -136,6 +138,12 @@ export default class LoginWithQR extends React.Component { phase: Phase.OutOfBandConfirmation, verificationUri, }); + } else { + const { serverName } = await rendezvous.negotiateProtocols(); + this.setState({ + phase: Phase.OutOfBandConfirmation, + homeserverName: serverName, + }); } // we ask the user to confirm that the channel is secure @@ -175,8 +183,75 @@ export default class LoginWithQR extends React.Component { // done this.onFinished(true); } else { - this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown }); - throw new Error("New device flows around OIDC are not yet implemented"); + if (!this.state.homeserverName) { + this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown }); + throw new Error("Homeserver name not found in state"); + } + + // in the 2025 version we would check if the homeserver is on a different base URL, but for the 2024 version + // we can't do this as the temporary client doesn't know the server name. + + const metadata = await this.props.client.getAuthMetadata(); + const deviceId = this.props.client.getDeviceId()!; + const { userCode } = await this.state.rendezvous.deviceAuthorizationGrant({ + metadata, + clientId: this.props.clientId, + deviceId, + }); + this.setState({ phase: Phase.WaitingForDevice, userCode }); + + const datr = await this.state.rendezvous.completeLoginOnNewDevice({ + clientId: this.props.clientId, + }); + + if (datr) { + // the 2024 version of the spec only gives the server name, but the 2025 version will give the base URL + // so, we do a discovery for now. + const homeserverUrl = (await AutoDiscovery.findClientConfig(this.state.homeserverName))?.["m.homeserver"]?.base_url; + + if (!homeserverUrl) { + this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown }); + logger.error("Failed to discover homeserver URL"); + throw new Error("Failed to discover homeserver URL"); + } + + // TODO: this is not the right way to do this + + // store and use the new credentials + const credentials = await configureFromCompletedOAuthLogin({ + accessToken: datr.access_token, + refreshToken: datr.refresh_token, + homeserverUrl, + clientId: this.props.clientId, + idToken: datr.id_token ?? "", // I'm not sure the idToken is actually required + issuer: metadata!.issuer, + identityServerUrl: undefined, // PROTOTYPE: we should have stored this from before + }); + + const { secrets } = await this.state.rendezvous.shareSecrets(); + + await restoreSessionFromStorage(); + + if (secrets) { + const crypto = MatrixClientPeg.safeGet().getCrypto(); + if (crypto?.importSecretsBundle) { + await crypto.importSecretsBundle(secrets); + // it should be sufficient to just upload the device keys with the signature + // but this seems to do the job for now + await crypto.crossSignDevice(deviceId); + + // PROTOTYPE: this is a fudge to bypass the complete security step + window.location.reload(); + } else { + logger.warn("Crypto not initialised"); + logger.warn("Crypto not initialised or no importSecretsBundle() method, cannot import secrets from QR login"); + } } else { + logger.warn("No secrets received from QR login"); + } + + // done + this.onFinished(true, credentials); + } } } catch (e: RendezvousError | unknown) { logger.error("Error whilst approving sign in", e); diff --git a/apps/web/src/components/views/settings/devices/LoginWithQRSection.tsx b/apps/web/src/components/views/settings/devices/LoginWithQRSection.tsx index 523633c884..def791df32 100644 --- a/apps/web/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/apps/web/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -11,15 +11,16 @@ import { type IServerVersions, type OidcClientConfig, type MatrixClient, - DEVICE_CODE_SCOPE, } from "matrix-js-sdk/src/matrix"; import QrCodeIcon from "@vector-im/compound-design-tokens/assets/web/icons/qr-code"; import { Text } from "@vector-im/compound-web"; +import { isSignInWithQRAvailable } from "matrix-js-sdk/src/rendezvous"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../elements/AccessibleButton"; import { SettingsSubsection } from "../shared/SettingsSubsection"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; +import { useAsyncMemo } from "../../../../hooks/useAsyncMemo"; interface IProps { onShowQr: () => void; @@ -28,19 +29,16 @@ interface IProps { isCrossSigningReady?: boolean; } -export function shouldShowQr( +export async function shouldShowQrForLinkNewDevice( cli: MatrixClient, isCrossSigningReady: boolean, oidcClientConfig?: OidcClientConfig, versions?: IServerVersions, -): boolean { - const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"]; - - const deviceAuthorizationGrantSupported = oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE); +): Promise { + const doesServerHaveSupport = await isSignInWithQRAvailable(cli); return ( - !!deviceAuthorizationGrantSupported && - msc4108Supported && + doesServerHaveSupport && !!cli.getCrypto()?.exportSecretsBundle && isCrossSigningReady ); @@ -48,7 +46,7 @@ export function shouldShowQr( const LoginWithQRSection: React.FC = ({ onShowQr, versions, oidcClientConfig, isCrossSigningReady }) => { const cli = useMatrixClientContext(); - const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions); + const offerShowQr = useAsyncMemo(() => shouldShowQrForLinkNewDevice(cli, !!isCrossSigningReady, oidcClientConfig, versions), [cli, isCrossSigningReady, oidcClientConfig, versions], false); return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f90d985d16..e0fe01457e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -434,8 +434,8 @@ importers: specifier: ^1.0.3 version: 1.0.3 matrix-js-sdk: - specifier: github:matrix-org/matrix-js-sdk#develop - version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/aed74c5a7259497de590600e2f25af4b39b7734b + specifier: github:matrix-org/matrix-js-sdk#hughns/msc4108-2024-signin-on-new-device + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3fa77611104e0ae11217056cc5b0dd62177617e3 matrix-widget-api: specifier: ^1.17.0 version: 1.17.0 @@ -9754,9 +9754,9 @@ packages: matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/aed74c5a7259497de590600e2f25af4b39b7734b: - resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/aed74c5a7259497de590600e2f25af4b39b7734b} - version: 41.2.0 + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3fa77611104e0ae11217056cc5b0dd62177617e3: + resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3fa77611104e0ae11217056cc5b0dd62177617e3} + version: 41.3.0 engines: {node: '>=22.0.0'} matrix-web-i18n@3.6.0: @@ -10348,9 +10348,9 @@ packages: resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} engines: {node: '>=16.17'} - p-retry@7.1.1: - resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} - engines: {node: '>=20'} + p-retry@8.0.0: + resolution: {integrity: sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==} + engines: {node: '>=22'} p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} @@ -23349,7 +23349,7 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/aed74c5a7259497de590600e2f25af4b39b7734b: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3fa77611104e0ae11217056cc5b0dd62177617e3: dependencies: '@babel/runtime': 7.28.6 '@matrix-org/matrix-sdk-crypto-wasm': 18.0.0 @@ -23361,7 +23361,7 @@ snapshots: matrix-events-sdk: 0.0.1 matrix-widget-api: 1.17.0 oidc-client-ts: 3.5.0 - p-retry: 7.1.1 + p-retry: 8.0.0 sdp-transform: 3.0.0 unhomoglyph: 1.0.6 uuid: 13.0.0 @@ -24129,7 +24129,7 @@ snapshots: is-network-error: 1.3.0 retry: 0.13.1 - p-retry@7.1.1: + p-retry@8.0.0: dependencies: is-network-error: 1.3.0