matrix-js-sdk 42.3.0 → 42.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/lib/@types/json.d.ts.map +1 -1
  3. package/lib/@types/json.js.map +1 -1
  4. package/lib/client.d.ts +74 -5
  5. package/lib/client.d.ts.map +1 -1
  6. package/lib/client.js +83 -7
  7. package/lib/client.js.map +1 -1
  8. package/lib/common-crypto/CryptoBackend.d.ts +42 -23
  9. package/lib/common-crypto/CryptoBackend.d.ts.map +1 -1
  10. package/lib/common-crypto/CryptoBackend.js +7 -0
  11. package/lib/common-crypto/CryptoBackend.js.map +1 -1
  12. package/lib/embedded.d.ts +35 -1
  13. package/lib/embedded.d.ts.map +1 -1
  14. package/lib/embedded.js +49 -1
  15. package/lib/embedded.js.map +1 -1
  16. package/lib/http-api/fetch.d.ts +0 -1
  17. package/lib/http-api/fetch.d.ts.map +1 -1
  18. package/lib/http-api/fetch.js +3 -22
  19. package/lib/http-api/fetch.js.map +1 -1
  20. package/lib/http-api/logging.d.ts +10 -0
  21. package/lib/http-api/logging.d.ts.map +1 -0
  22. package/lib/http-api/logging.js +46 -0
  23. package/lib/http-api/logging.js.map +1 -0
  24. package/lib/matrixrtc/EncryptionManager.d.ts +5 -0
  25. package/lib/matrixrtc/EncryptionManager.d.ts.map +1 -1
  26. package/lib/matrixrtc/EncryptionManager.js.map +1 -1
  27. package/lib/matrixrtc/LivekitTransport.d.ts +82 -0
  28. package/lib/matrixrtc/LivekitTransport.d.ts.map +1 -1
  29. package/lib/matrixrtc/LivekitTransport.js +25 -0
  30. package/lib/matrixrtc/LivekitTransport.js.map +1 -1
  31. package/lib/matrixrtc/MatrixRTCSession.d.ts +41 -2
  32. package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
  33. package/lib/matrixrtc/MatrixRTCSession.js +59 -1
  34. package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
  35. package/lib/matrixrtc/MembershipManager.js +7 -4
  36. package/lib/matrixrtc/MembershipManager.js.map +1 -1
  37. package/lib/matrixrtc/RTCEncryptionManager.d.ts +17 -0
  38. package/lib/matrixrtc/RTCEncryptionManager.d.ts.map +1 -1
  39. package/lib/matrixrtc/RTCEncryptionManager.js +68 -29
  40. package/lib/matrixrtc/RTCEncryptionManager.js.map +1 -1
  41. package/lib/models/room-receipts.d.ts +9 -3
  42. package/lib/models/room-receipts.d.ts.map +1 -1
  43. package/lib/models/room-receipts.js +51 -11
  44. package/lib/models/room-receipts.js.map +1 -1
  45. package/lib/oauth/authorize.d.ts +7 -2
  46. package/lib/oauth/authorize.d.ts.map +1 -1
  47. package/lib/oauth/authorize.js +10 -4
  48. package/lib/oauth/authorize.js.map +1 -1
  49. package/lib/oauth/fetch.d.ts +17 -0
  50. package/lib/oauth/fetch.d.ts.map +1 -0
  51. package/lib/oauth/fetch.js +47 -0
  52. package/lib/oauth/fetch.js.map +1 -0
  53. package/lib/oauth/index.d.ts +15 -2
  54. package/lib/oauth/index.d.ts.map +1 -1
  55. package/lib/oauth/index.js +33 -12
  56. package/lib/oauth/index.js.map +1 -1
  57. package/lib/rust-crypto/index.d.ts +17 -0
  58. package/lib/rust-crypto/index.d.ts.map +1 -1
  59. package/lib/rust-crypto/index.js +4 -2
  60. package/lib/rust-crypto/index.js.map +1 -1
  61. package/lib/rust-crypto/rust-crypto.d.ts +6 -26
  62. package/lib/rust-crypto/rust-crypto.d.ts.map +1 -1
  63. package/lib/rust-crypto/rust-crypto.js +19 -54
  64. package/lib/rust-crypto/rust-crypto.js.map +1 -1
  65. package/lib/sliding-sync-sdk.d.ts.map +1 -1
  66. package/lib/sliding-sync-sdk.js +65 -27
  67. package/lib/sliding-sync-sdk.js.map +1 -1
  68. package/lib/sliding-sync.d.ts +17 -0
  69. package/lib/sliding-sync.d.ts.map +1 -1
  70. package/lib/sliding-sync.js +21 -9
  71. package/lib/sliding-sync.js.map +1 -1
  72. package/lib/sync.d.ts +9 -1
  73. package/lib/sync.d.ts.map +1 -1
  74. package/lib/sync.js +54 -29
  75. package/lib/sync.js.map +1 -1
  76. package/package.json +5 -5
  77. package/src/@types/json.ts +11 -2
  78. package/src/client.ts +126 -11
  79. package/src/common-crypto/CryptoBackend.ts +45 -23
  80. package/src/embedded.ts +67 -4
  81. package/src/http-api/fetch.ts +2 -23
  82. package/src/http-api/logging.ts +46 -0
  83. package/src/matrixrtc/EncryptionManager.ts +6 -0
  84. package/src/matrixrtc/LivekitTransport.ts +86 -0
  85. package/src/matrixrtc/MatrixRTCSession.ts +81 -1
  86. package/src/matrixrtc/MembershipManager.ts +7 -7
  87. package/src/matrixrtc/RTCEncryptionManager.ts +80 -31
  88. package/src/models/room-receipts.ts +54 -10
  89. package/src/oauth/authorize.ts +10 -2
  90. package/src/oauth/fetch.ts +54 -0
  91. package/src/oauth/index.ts +29 -10
  92. package/src/rust-crypto/index.ts +23 -0
  93. package/src/rust-crypto/rust-crypto.ts +25 -67
  94. package/src/sliding-sync-sdk.ts +73 -33
  95. package/src/sliding-sync.ts +31 -14
  96. package/src/sync.ts +58 -35
@@ -1 +1 @@
1
- {"version":3,"file":"RTCEncryptionManager.js","names":[],"sources":["../../src/matrixrtc/RTCEncryptionManager.ts"],"sourcesContent":["/*\nCopyright 2025-2026 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport {\n type CallMembershipIdentityParts,\n getEncryptionKeyMapKey,\n type IEncryptionManager,\n} from \"./EncryptionManager.ts\";\nimport { type EncryptionConfig, type MembershipConfig } from \"./MatrixRTCSession.ts\";\nimport type { CallMembership } from \"./CallMembership.ts\";\nimport { decodeBase64, encodeBase64 } from \"../base64.ts\";\nimport { type IKeyTransport, type KeyTransportEventListener, KeyTransportEvents } from \"./IKeyTransport.ts\";\nimport { type Logger } from \"../logger.ts\";\nimport { sleep } from \"../utils.ts\";\nimport {\n type EncryptionKeyMapKey,\n type InboundEncryptionSession,\n type OutboundEncryptionSession,\n type ParticipantDeviceInfo,\n} from \"./types.ts\";\nimport { OutdatedKeyFilter } from \"./utils.ts\";\nimport { computeRtcIdentityRaw } from \"./membershipData/rtc.ts\";\n\n/**\n * RTCEncryptionManager is used to manage the encryption keys for a call.\n *\n * It is responsible for distributing the keys to the other participants and rotating the keys if needed.\n *\n * This manager when used with to-device transport will share the existing key only to new joiners, and rotate\n * if there is a leaver.\n *\n * XXX In the future we want to distribute a ratcheted key not the current one for new joiners.\n */\nexport class RTCEncryptionManager implements IEncryptionManager {\n // This is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n private manageMediaKeys = false;\n\n private useHashedRtcBackendIdentity = false;\n private ownRtcBackendIdentityCache: string | undefined;\n\n /**\n * Store the key rings for each participant.\n * The encryption manager stores the keys because the application layer might not be ready yet to handle the keys.\n * The keys are stored and can be retrieved later when the application layer is ready {@link RTCEncryptionManager#getEncryptionKeys}.\n */\n private readonly participantKeyRings = new Map<\n EncryptionKeyMapKey,\n Array<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n rtcBackendIdentity: string;\n }>\n >();\n\n // The current per-sender media key for this device\n private outboundSession: OutboundEncryptionSession | null = null;\n\n /**\n * Ensures that there is only one distribute operation at a time for that call.\n */\n private currentKeyDistributionPromise: Promise<void> | null = null;\n\n /**\n * The time to wait before using the outbound session after it has been distributed.\n * This is to ensure that the key is delivered to all participants before it is used.\n * When creating the first key, this is set to 0 so that the key can be used immediately.\n */\n private useKeyDelay = 5000;\n\n /**\n * We want to avoid rolling out a new outbound key when the previous one was created less than `keyRotationGracePeriodMs` milliseconds ago.\n * This is to avoid expensive key rotations when users quickly join the call in a row.\n *\n * This must be higher than `useKeyDelay` to have an effect.\n * If it is lower, the current key will always be older than the grace period.\n * @private\n */\n private keyRotationGracePeriodMs = 10_000;\n\n /**\n * If a new key distribution is being requested while one is going on, we will set this flag to true.\n * This will ensure that a new round is started after the current one.\n * @private\n */\n private needToEnsureKeyAgain = false;\n\n /**\n * There is a possibility that keys arrive in the wrong order.\n * For example, after a quick join/leave/join, there will be 2 keys of index 0 distributed, and\n * if they are received in the wrong order, the stream won't be decryptable.\n * For that reason we keep a small buffer of keys for a limited time to disambiguate.\n * @private\n */\n private keyBuffer = new OutdatedKeyFilter();\n\n private logger: Logger | undefined = undefined;\n\n private readonly rtcIdentityProvider: (userId: string, deviceId: string, memberId: string) => Promise<string>;\n\n /**\n *\n * @param ownMembership - our own membership info\n * @param getMemberships - function to get current memberships\n * @param transport - key transport (room or to-device)\n * @param statistics - statistics collector\n * @param onEncryptionKeysChanged - callback to notify the media layer of new keys\n * @param parentLogger - optional parent logger\n * @param rtcBackendIdProvider - A function to compute the rtc backend identity, exposed for testing purposes\n */\n public constructor(\n private readonly ownMembership: CallMembershipIdentityParts,\n private getMemberships: () => CallMembership[],\n private transport: IKeyTransport,\n // Callback to notify the media layer of new keys\n private onEncryptionKeysChanged: (\n keyBin: Uint8Array<ArrayBuffer>,\n encryptionKeyIndex: number,\n membership: CallMembershipIdentityParts,\n rtcBackendIdentity: string,\n ) => void,\n parentLogger?: Logger,\n rtcBackendIdProvider?: (userId: string, deviceId: string, memberId: string) => Promise<string>,\n ) {\n this.logger = parentLogger?.getChild(`[EncryptionManager]`);\n this.rtcIdentityProvider = rtcBackendIdProvider ?? computeRtcIdentityRaw;\n }\n\n private async getOwnRtcBackendIdentity(): Promise<string> {\n if (this.ownRtcBackendIdentityCache) return this.ownRtcBackendIdentityCache;\n\n if (this.useHashedRtcBackendIdentity) {\n const { userId, deviceId, memberId } = this.ownMembership;\n this.logger?.info(\n // If we see this log multiple times, we need to reconsider the precompute call of getOwnRtcBackendIdentity\n `Computing RTC backend identity for ${userId}:${deviceId}:${memberId} (SHOULD ONLY BE CALLED ONCE)`,\n );\n this.ownRtcBackendIdentityCache = await this.rtcIdentityProvider(userId, deviceId, memberId);\n } else {\n this.ownRtcBackendIdentityCache = `${this.ownMembership.userId}:${this.ownMembership.deviceId}`;\n }\n return this.ownRtcBackendIdentityCache;\n }\n\n public getEncryptionKeys(): ReadonlyMap<\n EncryptionKeyMapKey,\n ReadonlyArray<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n rtcBackendIdentity: string;\n }>\n > {\n return new Map(this.participantKeyRings);\n }\n\n private keysWithoutMatchingRTCMembership: Array<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n }> = [];\n\n private checkKeysWithoutMatchingRTCMembership(): void {\n const keyInfoTemp = this.keysWithoutMatchingRTCMembership;\n this.keysWithoutMatchingRTCMembership = [];\n keyInfoTemp.forEach((keyInfo) => {\n this.addKeyToParticipant(keyInfo.key, keyInfo.keyIndex, keyInfo.membership);\n });\n }\n\n private addKeyToParticipant(\n key: Uint8Array<ArrayBuffer>,\n keyIndex: number,\n membership: CallMembershipIdentityParts,\n ): void {\n const knownRtcMembership = this.getMemberships();\n const fullMembership = knownRtcMembership.find(\n (member) => member.userId === membership.userId && member.deviceId === membership.deviceId,\n );\n if (!fullMembership) {\n this.logger?.info(\n `No matching RTC membership for key from ${membership.userId}:${membership.deviceId}, delaying key addition`,\n );\n this.keysWithoutMatchingRTCMembership.push({ key, keyIndex, membership });\n return;\n }\n this.addKeyToParticipantWithBackendIdentity(key, keyIndex, membership, fullMembership.rtcBackendIdentity);\n }\n\n private addKeyToParticipantWithBackendIdentity(\n key: Uint8Array<ArrayBuffer>,\n keyIndex: number,\n membership: CallMembershipIdentityParts,\n rtcBackendIdentity: string,\n ): void {\n const mapKey = getEncryptionKeyMapKey(membership);\n if (!this.participantKeyRings.has(mapKey)) {\n this.participantKeyRings.set(mapKey, []);\n }\n this.participantKeyRings.get(mapKey)!.push({ key, keyIndex, membership, rtcBackendIdentity });\n this.onEncryptionKeysChanged(key, keyIndex, membership, rtcBackendIdentity);\n }\n\n public join(joinConfig: (EncryptionConfig & MembershipConfig) | undefined): void {\n this.manageMediaKeys = joinConfig?.manageMediaKeys ?? true; // default to true\n this.useHashedRtcBackendIdentity = joinConfig?.unstableSendStickyEvents ?? false;\n this.useKeyDelay = joinConfig?.useKeyDelay ?? 1000;\n this.keyRotationGracePeriodMs = joinConfig?.keyRotationGracePeriodMs ?? 10_000;\n\n this.transport.on(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);\n void this.getOwnRtcBackendIdentity(); // precompute own identity\n\n this.logger?.info(`Joining room`);\n this.transport.start();\n }\n\n public leave(): void {\n this.transport.off(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);\n this.transport.stop();\n this.participantKeyRings.clear();\n }\n\n /**\n * Will ensure that a new key is distributed and used to encrypt our media.\n * If there is already a key distribution in progress, it will schedule a new distribution round just after the current one is completed.\n * If this function is called repeatedly while a distribution is in progress,\n * the calls will be coalesced to a single new distribution (that will start just after the current one has completed).\n */\n private ensureKeyDistribution(): void {\n // `manageMediaKeys` is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n if (!this.manageMediaKeys) return;\n if (this.currentKeyDistributionPromise == null) {\n this.logger?.debug(`No active rollout, start a new one`);\n // start a rollout\n this.currentKeyDistributionPromise = this.rolloutOutboundKey().then(() => {\n this.logger?.debug(`Rollout completed`);\n this.currentKeyDistributionPromise = null;\n if (this.needToEnsureKeyAgain) {\n this.logger?.debug(`New Rollout needed`);\n this.needToEnsureKeyAgain = false;\n // rollout a new one\n this.ensureKeyDistribution();\n }\n });\n } else {\n // There is a rollout in progress, but a key rotation is requested (could be caused by a ownMembership change)\n // Remember that a new rotation is needed after the current one.\n this.logger?.debug(`Rollout in progress, a new rollout will be started after the current one`);\n this.needToEnsureKeyAgain = true;\n }\n }\n\n public onNewKeyReceived: KeyTransportEventListener = (membership, keyBase64Encoded, index, timestamp) => {\n // `manageMediaKeys` is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n if (!this.manageMediaKeys) {\n this.logger?.warn(\n `Received key over transport ${membership.userId}:${membership.deviceId} at index ${index} but media keys are disabled`,\n );\n return;\n }\n this.logger?.debug(`Received key over transport ${membership.userId}:${membership.deviceId} at index ${index}`);\n\n // We received a new key, notify the video layer of this new key so that it can decrypt the frames properly.\n const keyBin = decodeBase64(keyBase64Encoded);\n const candidateInboundSession: InboundEncryptionSession = {\n key: keyBin,\n membership,\n keyIndex: index,\n creationTS: timestamp,\n };\n\n const outdated = this.keyBuffer.isOutdated(membership, candidateInboundSession);\n if (!outdated) {\n this.addKeyToParticipant(\n candidateInboundSession.key,\n candidateInboundSession.keyIndex,\n candidateInboundSession.membership,\n );\n } else {\n this.logger?.info(\n `Received an out of order key for ${membership.userId}:${membership.deviceId}, dropping it`,\n );\n }\n };\n\n /**\n * Called when the ownMembership of the call changes.\n * This encryption manager is very basic, it will rotate the key everytime this is called.\n * @param oldMemberships - This parameter is not used here, but it is kept for compatibility with the interface.\n */\n public onMembershipsUpdate(oldMemberships: CallMembership[] = []): void {\n this.logger?.trace(`onMembershipsUpdate`);\n\n // Ensure the key is distributed. This will be no-op if the key is already being distributed to everyone.\n // If there is an ongoing distribution, it will be completed before a new one is started.\n this.ensureKeyDistribution();\n // ensure key emission to the rtc backend\n this.checkKeysWithoutMatchingRTCMembership();\n }\n\n private async rolloutOutboundKey(): Promise<void> {\n const isFirstKey = this.outboundSession == null;\n if (isFirstKey) {\n // create the first key\n const firstKey = {\n key: this.generateRandomKey(),\n creationTS: Date.now(),\n sharedWith: [],\n keyId: 0,\n };\n this.outboundSession = firstKey;\n this.addKeyToParticipantWithBackendIdentity(\n firstKey.key,\n firstKey.keyId,\n this.ownMembership,\n await this.getOwnRtcBackendIdentity(),\n );\n }\n // get current memberships\n const toShareWith: ParticipantDeviceInfo[] = this.getMemberships()\n .filter((membership) => {\n return membership.sender != undefined;\n })\n .map((membership) => {\n return {\n userId: membership.sender,\n deviceId: membership.deviceId,\n membershipTs: membership.createdTs(),\n };\n });\n\n let alreadySharedWith = this.outboundSession?.sharedWith ?? [];\n\n // Some users might have rotate their ownMembership event (formally called fingerprint) meaning they might have\n // clear their key. Reset the `alreadySharedWith` flag for them.\n alreadySharedWith = alreadySharedWith.filter(\n (x) =>\n // If there was a member with same userId and deviceId but different membershipTs, we need to clear it\n !toShareWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs != o.membershipTs,\n ),\n );\n\n const anyLeft = alreadySharedWith.filter(\n (x) =>\n !toShareWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs == o.membershipTs,\n ),\n );\n const anyJoined = toShareWith.filter(\n (x) =>\n !alreadySharedWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs == o.membershipTs,\n ),\n );\n\n let toDistributeTo: ParticipantDeviceInfo[] = [];\n let outboundKey: OutboundEncryptionSession;\n let hasKeyChanged = false;\n if (anyLeft.length > 0) {\n // We need to rotate the key\n const newOutboundKey = this.createNewOutboundSession();\n hasKeyChanged = true;\n toDistributeTo = toShareWith;\n outboundKey = newOutboundKey;\n } else if (anyJoined.length > 0) {\n const now = Date.now();\n const keyAge = now - this.outboundSession!.creationTS;\n // If the current key is recently created (less than `keyRotationGracePeriodMs`), we can keep it and just distribute it to the new joiners.\n if (keyAge < this.keyRotationGracePeriodMs) {\n // keep the same key\n // XXX In the future we want to distribute a ratcheted key, not the current one\n this.logger?.debug(`New joiners detected, but the key is recent enough (age:${keyAge}), keeping it`);\n toDistributeTo = anyJoined;\n outboundKey = this.outboundSession!;\n } else {\n // We need to rotate the key\n this.logger?.debug(`New joiners detected, rotating the key`);\n const newOutboundKey = this.createNewOutboundSession();\n hasKeyChanged = true;\n toDistributeTo = toShareWith;\n outboundKey = newOutboundKey;\n }\n } else {\n // no changes\n return;\n }\n\n try {\n this.logger?.trace(`Sending key...`);\n await this.transport.sendKey(encodeBase64(outboundKey.key), outboundKey.keyId, toDistributeTo);\n outboundKey.sharedWith.push(...toDistributeTo);\n this.logger?.trace(\n `key index:${outboundKey.keyId} sent to ${outboundKey.sharedWith.map((m) => `${m.userId}:${m.deviceId}`).join(\",\")}`,\n );\n if (hasKeyChanged) {\n // Delay a bit before using this key\n // It is recommended not to start using a key immediately but instead wait for a short time to make sure it is delivered.\n this.logger?.trace(`Delay Rollout for key:${outboundKey.keyId}...`);\n await sleep(this.useKeyDelay);\n this.logger?.trace(`...Delayed rollout of index:${outboundKey.keyId} `);\n this.addKeyToParticipantWithBackendIdentity(\n outboundKey.key,\n outboundKey.keyId,\n this.ownMembership,\n await this.getOwnRtcBackendIdentity(),\n );\n }\n } catch (err) {\n this.logger?.error(`Failed to rollout key`, err);\n }\n }\n\n private createNewOutboundSession(): OutboundEncryptionSession {\n const newOutboundKey: OutboundEncryptionSession = {\n key: this.generateRandomKey(),\n creationTS: Date.now(),\n sharedWith: [],\n keyId: this.nextKeyIndex(),\n };\n\n this.logger?.info(`creating new outbound key index:${newOutboundKey.keyId}`);\n // Set this new key as the current one\n this.outboundSession = newOutboundKey;\n return newOutboundKey;\n }\n\n private nextKeyIndex(): number {\n if (this.outboundSession) {\n return (this.outboundSession.keyId + 1) % 256;\n }\n return 0;\n }\n\n private generateRandomKey(): Uint8Array<ArrayBuffer> {\n const key = new Uint8Array(16);\n globalThis.crypto.getRandomValues(key);\n return key;\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"RTCEncryptionManager.js","names":[],"sources":["../../src/matrixrtc/RTCEncryptionManager.ts"],"sourcesContent":["/*\nCopyright 2025-2026 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport {\n type CallMembershipIdentityParts,\n getEncryptionKeyMapKey,\n type IEncryptionManager,\n} from \"./EncryptionManager.ts\";\nimport { type EncryptionConfig, type MembershipConfig } from \"./MatrixRTCSession.ts\";\nimport type { CallMembership } from \"./CallMembership.ts\";\nimport { decodeBase64, encodeBase64 } from \"../base64.ts\";\nimport { type IKeyTransport, type KeyTransportEventListener, KeyTransportEvents } from \"./IKeyTransport.ts\";\nimport { type Logger } from \"../logger.ts\";\nimport { sleep } from \"../utils.ts\";\nimport {\n type EncryptionKeyMapKey,\n type InboundEncryptionSession,\n type OutboundEncryptionSession,\n type ParticipantDeviceInfo,\n} from \"./types.ts\";\nimport { OutdatedKeyFilter } from \"./utils.ts\";\nimport { computeRtcIdentityRaw } from \"./membershipData/rtc.ts\";\n\n/**\n * Default for {@link EncryptionConfig.keyRotationParticipantLimit}.\n *\n * Setting this to undefined implies that we do not have a limit and full rotations are always done.\n * This is the most secure and least performant option.\n * It is highly recommended to set this to < 50 for client deployments that are planned to be used for large calls.\n * But before setting this, make yourself familiar with the exact security implications.\n * Key, rotations will stop when reaching this user limit in a call. The call will still be encrypted.\n */\nconst DEFAULT_KEY_ROTATION_PARTICIPANT_LIMIT: number | undefined = undefined;\n\n/**\n * RTCEncryptionManager is used to manage the encryption keys for a call.\n *\n * It is responsible for distributing the keys to the other participants and rotating the keys if needed.\n *\n * This manager when used with to-device transport will share the existing key only to new joiners, and rotate\n * if there is a leaver.\n *\n * XXX In the future we want to distribute a ratcheted key not the current one for new joiners.\n */\nexport class RTCEncryptionManager implements IEncryptionManager {\n // This is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n private manageMediaKeys = false;\n\n private useHashedRtcBackendIdentity = false;\n private ownRtcBackendIdentityCache: string | undefined;\n\n /**\n * Store the key rings for each participant.\n * The encryption manager stores the keys because the application layer might not be ready yet to handle the keys.\n * The keys are stored and can be retrieved later when the application layer is ready {@link RTCEncryptionManager#getEncryptionKeys}.\n */\n private readonly participantKeyRings = new Map<\n EncryptionKeyMapKey,\n Array<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n rtcBackendIdentity: string;\n }>\n >();\n\n // The current per-sender media key for this device\n private outboundSession: OutboundEncryptionSession | null = null;\n\n /**\n * Ensures that there is only one distribute operation at a time for that call.\n */\n private currentKeyDistributionPromise: Promise<void> | null = null;\n\n /**\n * The time to wait before using the outbound session after it has been distributed.\n * This is to ensure that the key is delivered to all participants before it is used.\n * When creating the first key, this is set to 0 so that the key can be used immediately.\n */\n private useKeyDelay = 5000;\n\n /**\n * We want to avoid rolling out a new outbound key when the previous one was created less than `keyRotationGracePeriodMs` milliseconds ago.\n * This is to avoid expensive key rotations when users quickly join the call in a row.\n *\n * This must be higher than `useKeyDelay` to have an effect.\n * If it is lower, the current key will always be older than the grace period.\n * @private\n */\n private keyRotationGracePeriodMs = 10_000;\n\n /**\n * The number of participants at or above which we stop rotating the key altogether.\n * The current key is still distributed to new joiners, but no new key is generated.\n * @see EncryptionConfig.keyRotationParticipantLimit\n * @private\n */\n private keyRotationParticipantLimit = DEFAULT_KEY_ROTATION_PARTICIPANT_LIMIT;\n\n /**\n * If a new key distribution is being requested while one is going on, we will set this flag to true.\n * This will ensure that a new round is started after the current one.\n * @private\n */\n private needToEnsureKeyAgain = false;\n\n /**\n * There is a possibility that keys arrive in the wrong order.\n * For example, after a quick join/leave/join, there will be 2 keys of index 0 distributed, and\n * if they are received in the wrong order, the stream won't be decryptable.\n * For that reason we keep a small buffer of keys for a limited time to disambiguate.\n * @private\n */\n private keyBuffer = new OutdatedKeyFilter();\n\n private logger: Logger | undefined = undefined;\n\n private readonly rtcIdentityProvider: (userId: string, deviceId: string, memberId: string) => Promise<string>;\n\n /**\n *\n * @param ownMembership - our own membership info\n * @param getMemberships - function to get current memberships\n * @param transport - key transport (room or to-device)\n * @param statistics - statistics collector\n * @param onEncryptionKeysChanged - callback to notify the media layer of new keys\n * @param parentLogger - optional parent logger\n * @param rtcBackendIdProvider - A function to compute the rtc backend identity, exposed for testing purposes\n */\n public constructor(\n private readonly ownMembership: CallMembershipIdentityParts,\n private getMemberships: () => CallMembership[],\n private transport: IKeyTransport,\n // Callback to notify the media layer of new keys\n private onEncryptionKeysChanged: (\n keyBin: Uint8Array<ArrayBuffer>,\n encryptionKeyIndex: number,\n membership: CallMembershipIdentityParts,\n rtcBackendIdentity: string,\n ) => void,\n parentLogger?: Logger,\n rtcBackendIdProvider?: (userId: string, deviceId: string, memberId: string) => Promise<string>,\n ) {\n this.logger = parentLogger?.getChild(`[EncryptionManager]`);\n this.rtcIdentityProvider = rtcBackendIdProvider ?? computeRtcIdentityRaw;\n }\n\n /**\n * Whether the session currently has too many participants for the key to be rotated.\n *\n * This is computed by checking the participant count. If there are too many participants for efficient rotations,\n * the key rotation will be suppressed.\n * While this is true, the current key is still shared with new joiners and the current call is still fully encrypted,\n * but no new key is generated for joiners or leavers. Changes are signalled by {@link MatrixRTCSessionEvent.KeyRotationSuppressedChanged}.\n * @see EncryptionConfig.keyRotationParticipantLimit\n */\n public get isKeyRotationSuppressed(): boolean {\n if (!this.manageMediaKeys) return false;\n if (this.keyRotationParticipantLimit === undefined) return false;\n return this.getMemberships().length >= this.keyRotationParticipantLimit;\n }\n\n private async getOwnRtcBackendIdentity(): Promise<string> {\n if (this.ownRtcBackendIdentityCache) return this.ownRtcBackendIdentityCache;\n\n if (this.useHashedRtcBackendIdentity) {\n const { userId, deviceId, memberId } = this.ownMembership;\n this.logger?.info(\n // If we see this log multiple times, we need to reconsider the precompute call of getOwnRtcBackendIdentity\n `Computing RTC backend identity for ${userId}:${deviceId}:${memberId} (SHOULD ONLY BE CALLED ONCE)`,\n );\n this.ownRtcBackendIdentityCache = await this.rtcIdentityProvider(userId, deviceId, memberId);\n } else {\n this.ownRtcBackendIdentityCache = `${this.ownMembership.userId}:${this.ownMembership.deviceId}`;\n }\n return this.ownRtcBackendIdentityCache;\n }\n\n public getEncryptionKeys(): ReadonlyMap<\n EncryptionKeyMapKey,\n ReadonlyArray<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n rtcBackendIdentity: string;\n }>\n > {\n return new Map(this.participantKeyRings);\n }\n\n private keysWithoutMatchingRTCMembership: Array<{\n key: Uint8Array<ArrayBuffer>;\n keyIndex: number;\n membership: CallMembershipIdentityParts;\n }> = [];\n\n private checkKeysWithoutMatchingRTCMembership(): void {\n const keyInfoTemp = this.keysWithoutMatchingRTCMembership;\n this.keysWithoutMatchingRTCMembership = [];\n keyInfoTemp.forEach((keyInfo) => {\n this.addKeyToParticipant(keyInfo.key, keyInfo.keyIndex, keyInfo.membership);\n });\n }\n\n private addKeyToParticipant(\n key: Uint8Array<ArrayBuffer>,\n keyIndex: number,\n membership: CallMembershipIdentityParts,\n ): void {\n const knownRtcMembership = this.getMemberships();\n const fullMembership = knownRtcMembership.find(\n (member) => member.userId === membership.userId && member.deviceId === membership.deviceId,\n );\n if (!fullMembership) {\n this.logger?.info(\n `No matching RTC membership for key from ${membership.userId}:${membership.deviceId}, delaying key addition`,\n );\n this.keysWithoutMatchingRTCMembership.push({ key, keyIndex, membership });\n return;\n }\n this.addKeyToParticipantWithBackendIdentity(key, keyIndex, membership, fullMembership.rtcBackendIdentity);\n }\n\n private addKeyToParticipantWithBackendIdentity(\n key: Uint8Array<ArrayBuffer>,\n keyIndex: number,\n membership: CallMembershipIdentityParts,\n rtcBackendIdentity: string,\n ): void {\n const mapKey = getEncryptionKeyMapKey(membership);\n if (!this.participantKeyRings.has(mapKey)) {\n this.participantKeyRings.set(mapKey, []);\n }\n this.participantKeyRings.get(mapKey)!.push({ key, keyIndex, membership, rtcBackendIdentity });\n this.onEncryptionKeysChanged(key, keyIndex, membership, rtcBackendIdentity);\n }\n\n public join(joinConfig: (EncryptionConfig & MembershipConfig) | undefined): void {\n this.manageMediaKeys = joinConfig?.manageMediaKeys ?? true; // default to true\n this.useHashedRtcBackendIdentity = joinConfig?.unstableSendStickyEvents ?? false;\n this.useKeyDelay = joinConfig?.useKeyDelay ?? 1000;\n this.keyRotationGracePeriodMs = joinConfig?.keyRotationGracePeriodMs ?? 10_000;\n this.keyRotationParticipantLimit =\n joinConfig?.keyRotationParticipantLimit ?? DEFAULT_KEY_ROTATION_PARTICIPANT_LIMIT;\n\n this.transport.on(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);\n void this.getOwnRtcBackendIdentity(); // precompute own identity\n\n this.logger?.info(`Joining room`);\n this.transport.start();\n }\n\n public leave(): void {\n this.transport.off(KeyTransportEvents.ReceivedKeys, this.onNewKeyReceived);\n this.transport.stop();\n this.participantKeyRings.clear();\n }\n\n /**\n * Will ensure that a new key is distributed and used to encrypt our media.\n * If there is already a key distribution in progress, it will schedule a new distribution round just after the current one is completed.\n * If this function is called repeatedly while a distribution is in progress,\n * the calls will be coalesced to a single new distribution (that will start just after the current one has completed).\n */\n private ensureKeyDistribution(): void {\n // `manageMediaKeys` is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n if (!this.manageMediaKeys) return;\n if (this.currentKeyDistributionPromise == null) {\n this.logger?.debug(`No active rollout, start a new one`);\n // start a rollout\n this.currentKeyDistributionPromise = this.rolloutOutboundKey().then(() => {\n this.logger?.debug(`Rollout completed`);\n this.currentKeyDistributionPromise = null;\n if (this.needToEnsureKeyAgain) {\n this.logger?.debug(`New Rollout needed`);\n this.needToEnsureKeyAgain = false;\n // rollout a new one\n this.ensureKeyDistribution();\n }\n });\n } else {\n // There is a rollout in progress, but a key rotation is requested (could be caused by a ownMembership change)\n // Remember that a new rotation is needed after the current one.\n this.logger?.debug(`Rollout in progress, a new rollout will be started after the current one`);\n this.needToEnsureKeyAgain = true;\n }\n }\n\n public onNewKeyReceived: KeyTransportEventListener = (membership, keyBase64Encoded, index, timestamp) => {\n // `manageMediaKeys` is a stop-gap solution for now. The preferred way to handle this case would be instead\n // to create a NoOpEncryptionManager that does nothing and use it for the session.\n // This will be done when removing the legacy EncryptionManager.\n if (!this.manageMediaKeys) {\n this.logger?.warn(\n `Received key over transport ${membership.userId}:${membership.deviceId} at index ${index} but media keys are disabled`,\n );\n return;\n }\n this.logger?.debug(`Received key over transport ${membership.userId}:${membership.deviceId} at index ${index}`);\n\n // We received a new key, notify the video layer of this new key so that it can decrypt the frames properly.\n const keyBin = decodeBase64(keyBase64Encoded);\n const candidateInboundSession: InboundEncryptionSession = {\n key: keyBin,\n membership,\n keyIndex: index,\n creationTS: timestamp,\n };\n\n const outdated = this.keyBuffer.isOutdated(membership, candidateInboundSession);\n if (!outdated) {\n this.addKeyToParticipant(\n candidateInboundSession.key,\n candidateInboundSession.keyIndex,\n candidateInboundSession.membership,\n );\n } else {\n this.logger?.info(\n `Received an out of order key for ${membership.userId}:${membership.deviceId}, dropping it`,\n );\n }\n };\n\n /**\n * Called when the ownMembership of the call changes.\n * This encryption manager is very basic, it will rotate the key everytime this is called.\n * @param oldMemberships - This parameter is not used here, but it is kept for compatibility with the interface.\n */\n public onMembershipsUpdate(oldMemberships: CallMembership[] = []): void {\n this.logger?.trace(`onMembershipsUpdate`);\n\n // Ensure the key is distributed. This will be no-op if the key is already being distributed to everyone.\n // If there is an ongoing distribution, it will be completed before a new one is started.\n this.ensureKeyDistribution();\n // ensure key emission to the rtc backend\n this.checkKeysWithoutMatchingRTCMembership();\n }\n\n private async rolloutOutboundKey(): Promise<void> {\n const isFirstKey = this.outboundSession == null;\n if (isFirstKey) {\n // create the first key\n const firstKey = {\n key: this.generateRandomKey(),\n creationTS: Date.now(),\n sharedWith: [],\n keyId: 0,\n };\n this.outboundSession = firstKey;\n this.addKeyToParticipantWithBackendIdentity(\n firstKey.key,\n firstKey.keyId,\n this.ownMembership,\n await this.getOwnRtcBackendIdentity(),\n );\n }\n // get current memberships\n const toShareWith: ParticipantDeviceInfo[] = this.getMemberships()\n .filter((membership) => {\n return membership.sender != undefined;\n })\n .map((membership) => {\n return {\n userId: membership.sender,\n deviceId: membership.deviceId,\n membershipTs: membership.createdTs(),\n };\n });\n\n let alreadySharedWith = this.outboundSession?.sharedWith ?? [];\n\n // Some users might have rotate their ownMembership event (formally called fingerprint) meaning they might have\n // clear their key. Reset the `alreadySharedWith` flag for them.\n alreadySharedWith = alreadySharedWith.filter(\n (x) =>\n // If there was a member with same userId and deviceId but different membershipTs, we need to clear it\n !toShareWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs != o.membershipTs,\n ),\n );\n\n const anyLeft = alreadySharedWith.filter(\n (x) =>\n !toShareWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs == o.membershipTs,\n ),\n );\n const anyJoined = toShareWith.filter(\n (x) =>\n !alreadySharedWith.some(\n (o) => x.userId == o.userId && x.deviceId == o.deviceId && x.membershipTs == o.membershipTs,\n ),\n );\n\n let toDistributeTo: ParticipantDeviceInfo[] = [];\n //default to current session\n let newOutboundEncryptionSession: OutboundEncryptionSession = this.outboundSession!;\n let hasKeyChanged = false;\n\n // Rotating means sending the new key to every participant, this is expensive in large session.\n if (this.isKeyRotationSuppressed) {\n if (anyJoined.length > 0) {\n this.logger?.debug(\n `New joiners detected, but the session has ${toShareWith.length} participants (limit:${this.keyRotationParticipantLimit}), keeping the key`,\n );\n }\n toDistributeTo = anyJoined;\n } else {\n if (anyLeft.length > 0) {\n // We need to rotate the key\n newOutboundEncryptionSession = this.createNewOutboundSession();\n hasKeyChanged = true;\n toDistributeTo = toShareWith;\n } else if (anyJoined.length > 0) {\n const keyAge = Date.now() - this.outboundSession!.creationTS;\n if (keyAge < this.keyRotationGracePeriodMs) {\n this.logger?.debug(\n `New joiners detected, but the key is recent enough (age:${keyAge}), keeping it`,\n );\n toDistributeTo = anyJoined;\n } else {\n this.logger?.debug(`New joiners detected, rotating the key`);\n // We need to rotate the key\n newOutboundEncryptionSession = this.createNewOutboundSession();\n hasKeyChanged = true;\n toDistributeTo = toShareWith;\n }\n }\n }\n // return early if we dont have anything to distribute.\n if (toDistributeTo.length === 0) {\n return;\n }\n\n try {\n this.logger?.trace(`Sending key...`);\n await this.transport.sendKey(\n encodeBase64(newOutboundEncryptionSession.key),\n newOutboundEncryptionSession.keyId,\n toDistributeTo,\n );\n newOutboundEncryptionSession.sharedWith.push(...toDistributeTo);\n const outboundSessionList = newOutboundEncryptionSession.sharedWith\n .map((m) => `${m.userId}:${m.deviceId}`)\n .join(\",\");\n this.logger?.trace(`key index:${newOutboundEncryptionSession.keyId} sent to ${outboundSessionList}`);\n if (hasKeyChanged) {\n // Delay a bit before using this key\n // It is recommended not to start using a key immediately but instead wait for a short time to make sure it is delivered.\n this.logger?.trace(`Delay Rollout for key:${newOutboundEncryptionSession.keyId}...`);\n await sleep(this.useKeyDelay);\n this.logger?.trace(`...Delayed rollout of index:${newOutboundEncryptionSession.keyId} `);\n this.addKeyToParticipantWithBackendIdentity(\n newOutboundEncryptionSession.key,\n newOutboundEncryptionSession.keyId,\n this.ownMembership,\n await this.getOwnRtcBackendIdentity(),\n );\n }\n } catch (err) {\n this.logger?.error(`Failed to rollout key`, err);\n }\n }\n\n private createNewOutboundSession(): OutboundEncryptionSession {\n const newOutboundKey: OutboundEncryptionSession = {\n key: this.generateRandomKey(),\n creationTS: Date.now(),\n sharedWith: [],\n keyId: this.nextKeyIndex(),\n };\n\n this.logger?.info(`creating new outbound key index:${newOutboundKey.keyId}`);\n // Set this new key as the current one\n this.outboundSession = newOutboundKey;\n return newOutboundKey;\n }\n\n private nextKeyIndex(): number {\n if (this.outboundSession) {\n return (this.outboundSession.keyId + 1) % 256;\n }\n return 0;\n }\n\n private generateRandomKey(): Uint8Array<ArrayBuffer> {\n const key = new Uint8Array(16);\n globalThis.crypto.getRandomValues(key);\n return key;\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -31,9 +31,15 @@ export declare class RoomReceipts {
31
31
  private onTimelineEvent;
32
32
  hasUserReadEvent(userId: string, eventId: string): boolean;
33
33
  /**
34
- * @returns true if the thread with this ID can be found, and the supplied
35
- * user sent the latest message in it.
34
+ * @returns true if the timeline for this thread ID can be found, and the
35
+ * supplied user sent an event in it that is the same as, or after,
36
+ * the event with the supplied ID.
36
37
  */
37
- private userSentLatestEventInThread;
38
+ private userSentEventAfterOrSame;
39
+ /**
40
+ * @returns true if the thread rooted at the event with the supplied ID can
41
+ * be found, and the supplied user sent a reply in it.
42
+ */
43
+ private userRepliedInThread;
38
44
  }
39
45
  //# sourceMappingURL=room-receipts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"room-receipts.d.ts","sourceRoot":"","sources":["../../src/models/room-receipts.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAoC,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEnG,OAAO,EAAE,KAAK,IAAI,EAAa,MAAM,WAAW,CAAC;AAIjD;;GAEG;AACH,qBAAa,YAAY;IACrB,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,kBAAkB,CAAiB;IAC3C,OAAO,CAAC,gBAAgB,CAAmB;IAE3C,YAAmB,IAAI,EAAE,IAAI,EAO5B;IAED;;;;;;;;;;;;OAYG;IACI,GAAG,CAAC,cAAc,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CA0CnE;IAED;;;;OAIG;IACH,OAAO,CAAC,eAAe,CA2BrB;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAyChE;IAED;;;OAGG;IACH,OAAO,CAAC,2BAA2B;CAQtC"}
1
+ {"version":3,"file":"room-receipts.d.ts","sourceRoot":"","sources":["../../src/models/room-receipts.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAoC,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEnG,OAAO,EAAE,KAAK,IAAI,EAAa,MAAM,WAAW,CAAC;AAIjD;;GAEG;AACH,qBAAa,YAAY;IACrB,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,kBAAkB,CAAiB;IAC3C,OAAO,CAAC,gBAAgB,CAAmB;IAE3C,YAAmB,IAAI,EAAE,IAAI,EAO5B;IAED;;;;;;;;;;;;OAYG;IACI,GAAG,CAAC,cAAc,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CA0CnE;IAED;;;;OAIG;IACH,OAAO,CAAC,eAAe,CA2BrB;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAkDhE;IAED;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IAUhC;;;OAGG;IACH,OAAO,CAAC,mBAAmB;CAM9B"}
@@ -126,14 +126,21 @@ export class RoomReceipts {
126
126
  return true;
127
127
  }
128
128
  }
129
-
130
- // TODO: what if they sent the second-last event in the thread?
131
- if (this.userSentLatestEventInThread(threadId, userId)) {
132
- // The user sent the latest message in this event's thread, so we
133
- // consider everything in the thread to be read.
129
+ if (this.userSentEventAfterOrSame(threadId, userId, eventId)) {
130
+ // The user sent an event at or after this one, so they must have
131
+ // seen this one: we consider it read.
134
132
  //
135
- // Note: maybe we don't need this because synthetic receipts should
136
- // do this job for us?
133
+ // Note: the synthetic receipts we create when we see an event
134
+ // usually do this job for us, but not always - e.g. we don't create
135
+ // them for events we paginated in.
136
+ return true;
137
+ }
138
+ if (event.isThreadRoot && this.userRepliedInThread(eventId, userId)) {
139
+ // The user replied in the thread rooted at this event, and you
140
+ // can't reply to a thread without seeing its root. We have to check
141
+ // this separately because their reply is in the thread's timeline
142
+ // whereas the root is in the main one, so no receipt of theirs can
143
+ // cover the root.
137
144
  return true;
138
145
  }
139
146
 
@@ -142,13 +149,46 @@ export class RoomReceipts {
142
149
  }
143
150
 
144
151
  /**
145
- * @returns true if the thread with this ID can be found, and the supplied
146
- * user sent the latest message in it.
152
+ * @returns true if the timeline for this thread ID can be found, and the
153
+ * supplied user sent an event in it that is the same as, or after,
154
+ * the event with the supplied ID.
147
155
  */
148
- userSentLatestEventInThread(threadId, userId) {
156
+ userSentEventAfterOrSame(threadId, userId, eventId) {
149
157
  const timeline = threadId === MAIN_ROOM_TIMELINE ? this.room.getLiveTimeline().getEvents() : this.room.getThread(threadId)?.timeline;
150
- return !!(timeline && timeline.length > 0 && timeline[timeline.length - 1].getSender() === userId);
158
+ const latestEventId = latestEventIdSentByUser(timeline, userId);
159
+ return !!latestEventId && isAfterOrSame(latestEventId, eventId, this.room);
160
+ }
161
+
162
+ /**
163
+ * @returns true if the thread rooted at the event with the supplied ID can
164
+ * be found, and the supplied user sent a reply in it.
165
+ */
166
+ userRepliedInThread(rootEventId, userId) {
167
+ // The root is part of the thread's timeline, but it is not a reply to
168
+ // the thread, so leave it out.
169
+ const replies = this.room.getThread(rootEventId)?.timeline.filter(event => event.getId() !== rootEventId);
170
+ return !!latestEventIdSentByUser(replies, userId);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Find the last event in the supplied timeline that was sent by this user.
176
+ *
177
+ * Events that are still on their way to the server (or failed to get there) are
178
+ * ignored: they have no place in the timeline yet, and a failed send tells us
179
+ * nothing about what the user has seen.
180
+ *
181
+ * @returns the ID of the found event, or undefined if there is no such event.
182
+ */
183
+ function latestEventIdSentByUser(timeline, userId) {
184
+ if (!timeline) return undefined;
185
+ for (let index = timeline.length - 1; index >= 0; index--) {
186
+ const event = timeline[index];
187
+ if (event.getSender() === userId && !event.status) {
188
+ return event.getId();
189
+ }
151
190
  }
191
+ return undefined;
152
192
  }
153
193
 
154
194
  // --- implementation details ---
@@ -1 +1 @@
1
- {"version":3,"file":"room-receipts.js","names":[],"sources":["../../src/models/room-receipts.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { MAIN_ROOM_TIMELINE, type Receipt, type ReceiptContent } from \"../@types/read_receipts.ts\";\nimport { threadIdForReceipt } from \"../client.ts\";\nimport { type Room, RoomEvent } from \"./room.ts\";\nimport { type MatrixEvent } from \"./event.ts\";\nimport { logger } from \"../logger.ts\";\n\n/**\n * The latest receipts we have for a room.\n */\nexport class RoomReceipts {\n private room: Room;\n private threadedReceipts: ThreadedReceipts;\n private unthreadedReceipts: ReceiptsByUser;\n private danglingReceipts: DanglingReceipts;\n\n public constructor(room: Room) {\n this.room = room;\n this.threadedReceipts = new ThreadedReceipts(room);\n this.unthreadedReceipts = new ReceiptsByUser(room);\n this.danglingReceipts = new DanglingReceipts();\n // We listen for timeline events so we can process dangling receipts\n room.on(RoomEvent.Timeline, this.onTimelineEvent);\n }\n\n /**\n * Remember the receipt information supplied. For each receipt:\n *\n * If we don't have the event for this receipt, store it as \"dangling\" so we\n * can process it later.\n *\n * Otherwise store it per-user in either the threaded store for its\n * thread_id, or the unthreaded store if there is no thread_id.\n *\n * Ignores any receipt that is before an existing receipt for the same user\n * (in the same thread, if applicable). \"Before\" is defined by the\n * unfilteredTimelineSet of the room.\n */\n public add(receiptContent: ReceiptContent, synthetic: boolean): void {\n /*\n Transform this structure:\n {\n \"$EVENTID\": {\n \"m.read|m.read.private\": {\n \"@user:example.org\": {\n \"ts\": 1661,\n \"thread_id\": \"main|$THREAD_ROOT_ID\" // or missing/undefined for an unthreaded receipt\n }\n }\n },\n ...\n }\n into maps of:\n threaded :: threadid :: userId :: ReceiptInfo\n unthreaded :: userId :: ReceiptInfo\n dangling :: eventId :: DanglingReceipt\n */\n for (const [eventId, eventReceipt] of Object.entries(receiptContent)) {\n for (const [receiptType, receiptsByUser] of Object.entries(eventReceipt)) {\n for (const [userId, receipt] of Object.entries(receiptsByUser)) {\n const referencedEvent = this.room.findEventById(eventId);\n if (!referencedEvent) {\n this.danglingReceipts.add(\n new DanglingReceipt(eventId, receiptType, userId, receipt, synthetic),\n );\n } else if (receipt.thread_id) {\n this.threadedReceipts.set(\n receipt.thread_id,\n eventId,\n receiptType,\n userId,\n receipt.ts,\n synthetic,\n );\n } else {\n this.unthreadedReceipts.set(eventId, receiptType, userId, receipt.ts, synthetic);\n }\n }\n }\n }\n }\n\n /**\n * Look for dangling receipts for the given event ID,\n * and add them to the thread of unthread receipts if found.\n * @param event - the event to look for\n */\n private onTimelineEvent = (event: MatrixEvent): void => {\n const eventId = event.getId();\n if (!eventId) return;\n\n const danglingReceipts = this.danglingReceipts.remove(eventId);\n\n danglingReceipts?.forEach((danglingReceipt) => {\n // The receipt is a thread receipt\n if (danglingReceipt.receipt.thread_id) {\n this.threadedReceipts.set(\n danglingReceipt.receipt.thread_id,\n danglingReceipt.eventId,\n danglingReceipt.receiptType,\n danglingReceipt.userId,\n danglingReceipt.receipt.ts,\n danglingReceipt.synthetic,\n );\n } else {\n this.unthreadedReceipts.set(\n eventId,\n danglingReceipt.receiptType,\n danglingReceipt.userId,\n danglingReceipt.receipt.ts,\n danglingReceipt.synthetic,\n );\n }\n });\n };\n\n public hasUserReadEvent(userId: string, eventId: string): boolean {\n const unthreaded = this.unthreadedReceipts.get(userId);\n if (unthreaded) {\n if (isAfterOrSame(unthreaded.eventId, eventId, this.room)) {\n // The unthreaded receipt is after this event, so we have read it.\n return true;\n }\n }\n\n const event = this.room.findEventById(eventId);\n if (!event) {\n // We don't know whether the user has read it - default to caution and say no.\n // This shouldn't really happen and feels like it ought to be an exception: let's\n // log a warn for now.\n logger.warn(\n `hasUserReadEvent event ID ${eventId} not found in room ${this.room.roomId}: this shouldn't happen!`,\n );\n return false;\n }\n\n const threadId = threadIdForReceipt(event);\n const threaded = this.threadedReceipts.get(threadId, userId);\n if (threaded) {\n if (isAfterOrSame(threaded.eventId, eventId, this.room)) {\n // The threaded receipt is after this event, so we have read it.\n return true;\n }\n }\n\n // TODO: what if they sent the second-last event in the thread?\n if (this.userSentLatestEventInThread(threadId, userId)) {\n // The user sent the latest message in this event's thread, so we\n // consider everything in the thread to be read.\n //\n // Note: maybe we don't need this because synthetic receipts should\n // do this job for us?\n return true;\n }\n\n // Neither of the receipts were after the event, so it's unread.\n return false;\n }\n\n /**\n * @returns true if the thread with this ID can be found, and the supplied\n * user sent the latest message in it.\n */\n private userSentLatestEventInThread(threadId: string, userId: string): boolean {\n const timeline =\n threadId === MAIN_ROOM_TIMELINE\n ? this.room.getLiveTimeline().getEvents()\n : this.room.getThread(threadId)?.timeline;\n\n return !!(timeline && timeline.length > 0 && timeline[timeline.length - 1].getSender() === userId);\n }\n}\n\n// --- implementation details ---\n\n/**\n * The information \"inside\" a receipt once it has been stored inside\n * RoomReceipts - what eventId it refers to, its type, and its ts.\n *\n * Does not contain userId or threadId since these are stored as keys of the\n * maps in RoomReceipts.\n */\nclass ReceiptInfo {\n public constructor(\n public eventId: string,\n public receiptType: string,\n public ts: number,\n ) {}\n}\n\n/**\n * Everything we know about a receipt that is \"dangling\" because we can't find\n * the event to which it refers.\n */\nclass DanglingReceipt {\n public constructor(\n public eventId: string,\n public receiptType: string,\n public userId: string,\n public receipt: Receipt,\n public synthetic: boolean,\n ) {}\n}\n\nclass UserReceipts {\n private room: Room;\n\n /**\n * The real receipt for this user.\n */\n private real: ReceiptInfo | undefined;\n\n /**\n * The synthetic receipt for this user. If this is defined, it is later than real.\n */\n private synthetic: ReceiptInfo | undefined;\n\n public constructor(room: Room) {\n this.room = room;\n this.real = undefined;\n this.synthetic = undefined;\n }\n\n public set(synthetic: boolean, receiptInfo: ReceiptInfo): void {\n if (synthetic) {\n this.synthetic = receiptInfo;\n } else {\n this.real = receiptInfo;\n }\n\n // Preserve the invariant: synthetic is only defined if it's later than real\n if (this.synthetic && this.real) {\n if (isAfterOrSame(this.real.eventId, this.synthetic.eventId, this.room)) {\n this.synthetic = undefined;\n }\n }\n }\n\n /**\n * Return the latest receipt we have - synthetic if we have one (and it's\n * later), otherwise real.\n */\n public get(): ReceiptInfo | undefined {\n // Relies on the invariant that synthetic is only defined if it's later than real.\n return this.synthetic ?? this.real;\n }\n\n /**\n * Return the latest receipt we have of the specified type (synthetic or not).\n */\n public getByType(synthetic: boolean): ReceiptInfo | undefined {\n return synthetic ? this.synthetic : this.real;\n }\n}\n\n/**\n * The latest receipt info we have, either for a single thread, or all the\n * unthreaded receipts for a room.\n *\n * userId: ReceiptInfo\n */\nclass ReceiptsByUser {\n private room: Room;\n\n /** map of userId: UserReceipts */\n private data: Map<string, UserReceipts>;\n\n public constructor(room: Room) {\n this.room = room;\n this.data = new Map<string, UserReceipts>();\n }\n\n /**\n * Add the supplied receipt to our structure, if it is not earlier than the\n * one we already hold for this user.\n */\n public set(eventId: string, receiptType: string, userId: string, ts: number, synthetic: boolean): void {\n const userReceipts = getOrCreate(this.data, userId, () => new UserReceipts(this.room));\n\n const existingReceipt = userReceipts.getByType(synthetic);\n if (existingReceipt && isAfter(existingReceipt.eventId, eventId, this.room)) {\n // The new receipt is before the existing one - don't store it.\n return;\n }\n\n // Possibilities:\n //\n // 1. there was no existing receipt, or\n // 2. the existing receipt was before this one, or\n // 3. we were unable to compare the receipts.\n //\n // In the case of 3 it's difficult to decide what to do, so the\n // most-recently-received receipt wins.\n //\n // Case 3 can only happen if the events for these receipts have\n // disappeared, which is quite unlikely since the new one has just been\n // checked, and the old one was checked before it was inserted here.\n //\n // We go ahead and store this receipt (replacing the other if it exists)\n userReceipts.set(synthetic, new ReceiptInfo(eventId, receiptType, ts));\n }\n\n /**\n * Find the latest receipt we have for this user. (Note - there is only one\n * receipt per user, because we are already inside a specific thread or\n * unthreaded list.)\n *\n * If there is a later synthetic receipt for this user, return that.\n * Otherwise, return the real receipt.\n *\n * @returns the found receipt info, or undefined if we have no receipt for this user.\n */\n public get(userId: string): ReceiptInfo | undefined {\n return this.data.get(userId)?.get();\n }\n}\n\n/**\n * The latest threaded receipts we have for a room.\n */\nclass ThreadedReceipts {\n private room: Room;\n\n /** map of threadId: ReceiptsByUser */\n private data: Map<string, ReceiptsByUser>;\n\n public constructor(room: Room) {\n this.room = room;\n this.data = new Map<string, ReceiptsByUser>();\n }\n\n /**\n * Add the supplied receipt to our structure, if it is not earlier than one\n * we already hold for this user in this thread.\n */\n public set(\n threadId: string,\n eventId: string,\n receiptType: string,\n userId: string,\n ts: number,\n synthetic: boolean,\n ): void {\n const receiptsByUser = getOrCreate(this.data, threadId, () => new ReceiptsByUser(this.room));\n receiptsByUser.set(eventId, receiptType, userId, ts, synthetic);\n }\n\n /**\n * Find the latest threaded receipt for the supplied user in the supplied thread.\n *\n * @returns the found receipt info or undefined if we don't have one.\n */\n public get(threadId: string, userId: string): ReceiptInfo | undefined {\n return this.data.get(threadId)?.get(userId);\n }\n}\n\n/**\n * All the receipts that we have received but can't process because we can't\n * find the event they refer to.\n *\n * We hold on to them so we can process them if their event arrives later.\n */\nclass DanglingReceipts {\n /**\n * eventId: DanglingReceipt[]\n */\n private data = new Map<string, Array<DanglingReceipt>>();\n\n /**\n * Remember the supplied dangling receipt.\n */\n public add(danglingReceipt: DanglingReceipt): void {\n const danglingReceipts = getOrCreate(this.data, danglingReceipt.eventId, () => []);\n danglingReceipts.push(danglingReceipt);\n }\n\n /**\n * Remove and return the dangling receipts for the given event ID.\n * @param eventId - the event ID to look for\n * @returns the found dangling receipts, or undefined if we don't have one.\n */\n public remove(eventId: string): Array<DanglingReceipt> | undefined {\n const danglingReceipts = this.data.get(eventId);\n this.data.delete(eventId);\n return danglingReceipts;\n }\n}\n\nfunction getOrCreate<K, V>(m: Map<K, V>, key: K, createFn: () => V): V {\n const found = m.get(key);\n if (found) {\n return found;\n } else {\n const created = createFn();\n m.set(key, created);\n return created;\n }\n}\n\n/**\n * Is left after right (or the same)?\n *\n * Only returns true if both events can be found, and left is after or the same\n * as right.\n *\n * @returns left \\>= right\n */\nfunction isAfterOrSame(leftEventId: string, rightEventId: string, room: Room): boolean {\n const comparison = room.compareEventOrdering(leftEventId, rightEventId);\n return comparison !== null && comparison >= 0;\n}\n\n/**\n * Is left strictly after right?\n *\n * Only returns true if both events can be found, and left is strictly after right.\n *\n * @returns left \\> right\n */\nfunction isAfter(leftEventId: string, rightEventId: string, room: Room): boolean {\n const comparison = room.compareEventOrdering(leftEventId, rightEventId);\n return comparison !== null && comparison > 0;\n}\n"],"mappings":";AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAAS,kBAAkB,QAA2C,4BAA4B;AAClG,SAAS,kBAAkB,QAAQ,cAAc;AACjD,SAAoB,SAAS,QAAQ,WAAW;AAEhD,SAAS,MAAM,QAAQ,cAAc;;AAErC;AACA;AACA;AACA,OAAO,MAAM,YAAY,CAAC;EAMf,WAAW,CAAC,IAAU,EAAE;IAAA;IAAA;IAAA;IAAA;IAkE/B;AACJ;AACA;AACA;AACA;IAJI,yCAK2B,KAAkB,IAAW;MACpD,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;MAC7B,IAAI,CAAC,OAAO,EAAE;MAEd,MAAM,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC;MAE9D,gBAAgB,EAAE,OAAO,CAAE,eAAe,IAAK;QAC3C;QACA,IAAI,eAAe,CAAC,OAAO,CAAC,SAAS,EAAE;UACnC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CACrB,eAAe,CAAC,OAAO,CAAC,SAAS,EACjC,eAAe,CAAC,OAAO,EACvB,eAAe,CAAC,WAAW,EAC3B,eAAe,CAAC,MAAM,EACtB,eAAe,CAAC,OAAO,CAAC,EAAE,EAC1B,eAAe,CAAC,SACpB,CAAC;QACL,CAAC,MAAM;UACH,IAAI,CAAC,kBAAkB,CAAC,GAAG,CACvB,OAAO,EACP,eAAe,CAAC,WAAW,EAC3B,eAAe,CAAC,MAAM,EACtB,eAAe,CAAC,OAAO,CAAC,EAAE,EAC1B,eAAe,CAAC,SACpB,CAAC;QACL;MACJ,CAAC,CAAC;IACN,CAAC;IAjGG,IAAI,CAAC,IAAI,GAAG,IAAI;IAChB,IAAI,CAAC,gBAAgB,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC;IAClD,IAAI,CAAC,kBAAkB,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC;IAClD,IAAI,CAAC,gBAAgB,GAAG,IAAI,gBAAgB,CAAC,CAAC;IAC9C;IACA,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,CAAC;EACrD;;EAEA;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACW,GAAG,CAAC,cAA8B,EAAE,SAAkB,EAAQ;IACjE;AACR;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACQ,KAAK,MAAM,CAAC,OAAO,EAAE,YAAY,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE;MAClE,KAAK,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE;QACtE,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE;UAC5D,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC;UACxD,IAAI,CAAC,eAAe,EAAE;YAClB,IAAI,CAAC,gBAAgB,CAAC,GAAG,CACrB,IAAI,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,CACxE,CAAC;UACL,CAAC,MAAM,IAAI,OAAO,CAAC,SAAS,EAAE;YAC1B,IAAI,CAAC,gBAAgB,CAAC,GAAG,CACrB,OAAO,CAAC,SAAS,EACjB,OAAO,EACP,WAAW,EACX,MAAM,EACN,OAAO,CAAC,EAAE,EACV,SACJ,CAAC;UACL,CAAC,MAAM;YACH,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,CAAC;UACpF;QACJ;MACJ;IACJ;EACJ;EAoCO,gBAAgB,CAAC,MAAc,EAAE,OAAe,EAAW;IAC9D,MAAM,UAAU,GAAG,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC;IACtD,IAAI,UAAU,EAAE;MACZ,IAAI,aAAa,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;QACvD;QACA,OAAO,IAAI;MACf;IACJ;IAEA,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC;IAC9C,IAAI,CAAC,KAAK,EAAE;MACR;MACA;MACA;MACA,MAAM,CAAC,IAAI,CACP,6BAA6B,OAAO,sBAAsB,IAAI,CAAC,IAAI,CAAC,MAAM,0BAC9E,CAAC;MACD,OAAO,KAAK;IAChB;IAEA,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC5D,IAAI,QAAQ,EAAE;MACV,IAAI,aAAa,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;QACrD;QACA,OAAO,IAAI;MACf;IACJ;;IAEA;IACA,IAAI,IAAI,CAAC,2BAA2B,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE;MACpD;MACA;MACA;MACA;MACA;MACA,OAAO,IAAI;IACf;;IAEA;IACA,OAAO,KAAK;EAChB;;EAEA;AACJ;AACA;AACA;EACY,2BAA2B,CAAC,QAAgB,EAAE,MAAc,EAAW;IAC3E,MAAM,QAAQ,GACV,QAAQ,KAAK,kBAAkB,GACzB,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,GACvC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,QAAQ;IAEjD,OAAO,CAAC,EAAE,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,MAAM,CAAC;EACtG;AACJ;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,WAAW,CAAC;EACP,WAAW,CACP,OAAe,EACf,WAAmB,EACnB,EAAU,EACnB;IAAA,KAHS,OAAe,GAAf,OAAe;IAAA,KACf,WAAmB,GAAnB,WAAmB;IAAA,KACnB,EAAU,GAAV,EAAU;EAClB;AACP;;AAEA;AACA;AACA;AACA;AACA,MAAM,eAAe,CAAC;EACX,WAAW,CACP,OAAe,EACf,WAAmB,EACnB,MAAc,EACd,OAAgB,EAChB,SAAkB,EAC3B;IAAA,KALS,OAAe,GAAf,OAAe;IAAA,KACf,WAAmB,GAAnB,WAAmB;IAAA,KACnB,MAAc,GAAd,MAAc;IAAA,KACd,OAAgB,GAAhB,OAAgB;IAAA,KAChB,SAAkB,GAAlB,SAAkB;EAC1B;AACP;AAEA,MAAM,YAAY,CAAC;EAaR,WAAW,CAAC,IAAU,EAAE;IAAA;IAV/B;AACJ;AACA;IAFI;IAKA;AACJ;AACA;IAFI;IAMI,IAAI,CAAC,IAAI,GAAG,IAAI;IAChB,IAAI,CAAC,IAAI,GAAG,SAAS;IACrB,IAAI,CAAC,SAAS,GAAG,SAAS;EAC9B;EAEO,GAAG,CAAC,SAAkB,EAAE,WAAwB,EAAQ;IAC3D,IAAI,SAAS,EAAE;MACX,IAAI,CAAC,SAAS,GAAG,WAAW;IAChC,CAAC,MAAM;MACH,IAAI,CAAC,IAAI,GAAG,WAAW;IAC3B;;IAEA;IACA,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI,EAAE;MAC7B,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;QACrE,IAAI,CAAC,SAAS,GAAG,SAAS;MAC9B;IACJ;EACJ;;EAEA;AACJ;AACA;AACA;EACW,GAAG,GAA4B;IAClC;IACA,OAAO,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI;EACtC;;EAEA;AACJ;AACA;EACW,SAAS,CAAC,SAAkB,EAA2B;IAC1D,OAAO,SAAS,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI;EACjD;AACJ;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,cAAc,CAAC;EAMV,WAAW,CAAC,IAAU,EAAE;IAAA;IAH/B;IAAA;IAII,IAAI,CAAC,IAAI,GAAG,IAAI;IAChB,IAAI,CAAC,IAAI,GAAG,IAAI,GAAG,CAAuB,CAAC;EAC/C;;EAEA;AACJ;AACA;AACA;EACW,GAAG,CAAC,OAAe,EAAE,WAAmB,EAAE,MAAc,EAAE,EAAU,EAAE,SAAkB,EAAQ;IACnG,MAAM,YAAY,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEtF,MAAM,eAAe,GAAG,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC;IACzD,IAAI,eAAe,IAAI,OAAO,CAAC,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;MACzE;MACA;IACJ;;IAEA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;EAC1E;;EAEA;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACW,GAAG,CAAC,MAAc,EAA2B;IAChD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;EACvC;AACJ;;AAEA;AACA;AACA;AACA,MAAM,gBAAgB,CAAC;EAMZ,WAAW,CAAC,IAAU,EAAE;IAAA;IAH/B;IAAA;IAII,IAAI,CAAC,IAAI,GAAG,IAAI;IAChB,IAAI,CAAC,IAAI,GAAG,IAAI,GAAG,CAAyB,CAAC;EACjD;;EAEA;AACJ;AACA;AACA;EACW,GAAG,CACN,QAAgB,EAChB,OAAe,EACf,WAAmB,EACnB,MAAc,EACd,EAAU,EACV,SAAkB,EACd;IACJ,MAAM,cAAc,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5F,cAAc,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,CAAC;EACnE;;EAEA;AACJ;AACA;AACA;AACA;EACW,GAAG,CAAC,QAAgB,EAAE,MAAc,EAA2B;IAClE,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC;EAC/C;AACJ;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,gBAAgB,CAAC;EAAA;IACnB;AACJ;AACA;IAFI,8BAGe,IAAI,GAAG,CAAiC,CAAC;EAAA;EAExD;AACJ;AACA;EACW,GAAG,CAAC,eAAgC,EAAQ;IAC/C,MAAM,gBAAgB,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClF,gBAAgB,CAAC,IAAI,CAAC,eAAe,CAAC;EAC1C;;EAEA;AACJ;AACA;AACA;AACA;EACW,MAAM,CAAC,OAAe,EAAsC;IAC/D,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;IAC/C,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;IACzB,OAAO,gBAAgB;EAC3B;AACJ;AAEA,SAAS,WAAW,CAAO,CAAY,EAAE,GAAM,EAAE,QAAiB,EAAK;EACnE,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;EACxB,IAAI,KAAK,EAAE;IACP,OAAO,KAAK;EAChB,CAAC,MAAM;IACH,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC;IAC1B,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC;IACnB,OAAO,OAAO;EAClB;AACJ;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,aAAa,CAAC,WAAmB,EAAE,YAAoB,EAAE,IAAU,EAAW;EACnF,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,WAAW,EAAE,YAAY,CAAC;EACvE,OAAO,UAAU,KAAK,IAAI,IAAI,UAAU,IAAI,CAAC;AACjD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,OAAO,CAAC,WAAmB,EAAE,YAAoB,EAAE,IAAU,EAAW;EAC7E,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,WAAW,EAAE,YAAY,CAAC;EACvE,OAAO,UAAU,KAAK,IAAI,IAAI,UAAU,GAAG,CAAC;AAChD","ignoreList":[]}
1
+ {"version":3,"file":"room-receipts.js","names":[],"sources":["../../src/models/room-receipts.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { MAIN_ROOM_TIMELINE, type Receipt, type ReceiptContent } from \"../@types/read_receipts.ts\";\nimport { threadIdForReceipt } from \"../client.ts\";\nimport { type Room, RoomEvent } from \"./room.ts\";\nimport { type MatrixEvent } from \"./event.ts\";\nimport { logger } from \"../logger.ts\";\n\n/**\n * The latest receipts we have for a room.\n */\nexport class RoomReceipts {\n private room: Room;\n private threadedReceipts: ThreadedReceipts;\n private unthreadedReceipts: ReceiptsByUser;\n private danglingReceipts: DanglingReceipts;\n\n public constructor(room: Room) {\n this.room = room;\n this.threadedReceipts = new ThreadedReceipts(room);\n this.unthreadedReceipts = new ReceiptsByUser(room);\n this.danglingReceipts = new DanglingReceipts();\n // We listen for timeline events so we can process dangling receipts\n room.on(RoomEvent.Timeline, this.onTimelineEvent);\n }\n\n /**\n * Remember the receipt information supplied. For each receipt:\n *\n * If we don't have the event for this receipt, store it as \"dangling\" so we\n * can process it later.\n *\n * Otherwise store it per-user in either the threaded store for its\n * thread_id, or the unthreaded store if there is no thread_id.\n *\n * Ignores any receipt that is before an existing receipt for the same user\n * (in the same thread, if applicable). \"Before\" is defined by the\n * unfilteredTimelineSet of the room.\n */\n public add(receiptContent: ReceiptContent, synthetic: boolean): void {\n /*\n Transform this structure:\n {\n \"$EVENTID\": {\n \"m.read|m.read.private\": {\n \"@user:example.org\": {\n \"ts\": 1661,\n \"thread_id\": \"main|$THREAD_ROOT_ID\" // or missing/undefined for an unthreaded receipt\n }\n }\n },\n ...\n }\n into maps of:\n threaded :: threadid :: userId :: ReceiptInfo\n unthreaded :: userId :: ReceiptInfo\n dangling :: eventId :: DanglingReceipt\n */\n for (const [eventId, eventReceipt] of Object.entries(receiptContent)) {\n for (const [receiptType, receiptsByUser] of Object.entries(eventReceipt)) {\n for (const [userId, receipt] of Object.entries(receiptsByUser)) {\n const referencedEvent = this.room.findEventById(eventId);\n if (!referencedEvent) {\n this.danglingReceipts.add(\n new DanglingReceipt(eventId, receiptType, userId, receipt, synthetic),\n );\n } else if (receipt.thread_id) {\n this.threadedReceipts.set(\n receipt.thread_id,\n eventId,\n receiptType,\n userId,\n receipt.ts,\n synthetic,\n );\n } else {\n this.unthreadedReceipts.set(eventId, receiptType, userId, receipt.ts, synthetic);\n }\n }\n }\n }\n }\n\n /**\n * Look for dangling receipts for the given event ID,\n * and add them to the thread of unthread receipts if found.\n * @param event - the event to look for\n */\n private onTimelineEvent = (event: MatrixEvent): void => {\n const eventId = event.getId();\n if (!eventId) return;\n\n const danglingReceipts = this.danglingReceipts.remove(eventId);\n\n danglingReceipts?.forEach((danglingReceipt) => {\n // The receipt is a thread receipt\n if (danglingReceipt.receipt.thread_id) {\n this.threadedReceipts.set(\n danglingReceipt.receipt.thread_id,\n danglingReceipt.eventId,\n danglingReceipt.receiptType,\n danglingReceipt.userId,\n danglingReceipt.receipt.ts,\n danglingReceipt.synthetic,\n );\n } else {\n this.unthreadedReceipts.set(\n eventId,\n danglingReceipt.receiptType,\n danglingReceipt.userId,\n danglingReceipt.receipt.ts,\n danglingReceipt.synthetic,\n );\n }\n });\n };\n\n public hasUserReadEvent(userId: string, eventId: string): boolean {\n const unthreaded = this.unthreadedReceipts.get(userId);\n if (unthreaded) {\n if (isAfterOrSame(unthreaded.eventId, eventId, this.room)) {\n // The unthreaded receipt is after this event, so we have read it.\n return true;\n }\n }\n\n const event = this.room.findEventById(eventId);\n if (!event) {\n // We don't know whether the user has read it - default to caution and say no.\n // This shouldn't really happen and feels like it ought to be an exception: let's\n // log a warn for now.\n logger.warn(\n `hasUserReadEvent event ID ${eventId} not found in room ${this.room.roomId}: this shouldn't happen!`,\n );\n return false;\n }\n\n const threadId = threadIdForReceipt(event);\n const threaded = this.threadedReceipts.get(threadId, userId);\n if (threaded) {\n if (isAfterOrSame(threaded.eventId, eventId, this.room)) {\n // The threaded receipt is after this event, so we have read it.\n return true;\n }\n }\n\n if (this.userSentEventAfterOrSame(threadId, userId, eventId)) {\n // The user sent an event at or after this one, so they must have\n // seen this one: we consider it read.\n //\n // Note: the synthetic receipts we create when we see an event\n // usually do this job for us, but not always - e.g. we don't create\n // them for events we paginated in.\n return true;\n }\n\n if (event.isThreadRoot && this.userRepliedInThread(eventId, userId)) {\n // The user replied in the thread rooted at this event, and you\n // can't reply to a thread without seeing its root. We have to check\n // this separately because their reply is in the thread's timeline\n // whereas the root is in the main one, so no receipt of theirs can\n // cover the root.\n return true;\n }\n\n // Neither of the receipts were after the event, so it's unread.\n return false;\n }\n\n /**\n * @returns true if the timeline for this thread ID can be found, and the\n * supplied user sent an event in it that is the same as, or after,\n * the event with the supplied ID.\n */\n private userSentEventAfterOrSame(threadId: string, userId: string, eventId: string): boolean {\n const timeline =\n threadId === MAIN_ROOM_TIMELINE\n ? this.room.getLiveTimeline().getEvents()\n : this.room.getThread(threadId)?.timeline;\n\n const latestEventId = latestEventIdSentByUser(timeline, userId);\n return !!latestEventId && isAfterOrSame(latestEventId, eventId, this.room);\n }\n\n /**\n * @returns true if the thread rooted at the event with the supplied ID can\n * be found, and the supplied user sent a reply in it.\n */\n private userRepliedInThread(rootEventId: string, userId: string): boolean {\n // The root is part of the thread's timeline, but it is not a reply to\n // the thread, so leave it out.\n const replies = this.room.getThread(rootEventId)?.timeline.filter((event) => event.getId() !== rootEventId);\n return !!latestEventIdSentByUser(replies, userId);\n }\n}\n\n/**\n * Find the last event in the supplied timeline that was sent by this user.\n *\n * Events that are still on their way to the server (or failed to get there) are\n * ignored: they have no place in the timeline yet, and a failed send tells us\n * nothing about what the user has seen.\n *\n * @returns the ID of the found event, or undefined if there is no such event.\n */\nfunction latestEventIdSentByUser(timeline: Array<MatrixEvent> | undefined, userId: string): string | undefined {\n if (!timeline) return undefined;\n\n for (let index = timeline.length - 1; index >= 0; index--) {\n const event = timeline[index];\n if (event.getSender() === userId && !event.status) {\n return event.getId();\n }\n }\n\n return undefined;\n}\n\n// --- implementation details ---\n\n/**\n * The information \"inside\" a receipt once it has been stored inside\n * RoomReceipts - what eventId it refers to, its type, and its ts.\n *\n * Does not contain userId or threadId since these are stored as keys of the\n * maps in RoomReceipts.\n */\nclass ReceiptInfo {\n public constructor(\n public eventId: string,\n public receiptType: string,\n public ts: number,\n ) {}\n}\n\n/**\n * Everything we know about a receipt that is \"dangling\" because we can't find\n * the event to which it refers.\n */\nclass DanglingReceipt {\n public constructor(\n public eventId: string,\n public receiptType: string,\n public userId: string,\n public receipt: Receipt,\n public synthetic: boolean,\n ) {}\n}\n\nclass UserReceipts {\n private room: Room;\n\n /**\n * The real receipt for this user.\n */\n private real: ReceiptInfo | undefined;\n\n /**\n * The synthetic receipt for this user. If this is defined, it is later than real.\n */\n private synthetic: ReceiptInfo | undefined;\n\n public constructor(room: Room) {\n this.room = room;\n this.real = undefined;\n this.synthetic = undefined;\n }\n\n public set(synthetic: boolean, receiptInfo: ReceiptInfo): void {\n if (synthetic) {\n this.synthetic = receiptInfo;\n } else {\n this.real = receiptInfo;\n }\n\n // Preserve the invariant: synthetic is only defined if it's later than real\n if (this.synthetic && this.real) {\n if (isAfterOrSame(this.real.eventId, this.synthetic.eventId, this.room)) {\n this.synthetic = undefined;\n }\n }\n }\n\n /**\n * Return the latest receipt we have - synthetic if we have one (and it's\n * later), otherwise real.\n */\n public get(): ReceiptInfo | undefined {\n // Relies on the invariant that synthetic is only defined if it's later than real.\n return this.synthetic ?? this.real;\n }\n\n /**\n * Return the latest receipt we have of the specified type (synthetic or not).\n */\n public getByType(synthetic: boolean): ReceiptInfo | undefined {\n return synthetic ? this.synthetic : this.real;\n }\n}\n\n/**\n * The latest receipt info we have, either for a single thread, or all the\n * unthreaded receipts for a room.\n *\n * userId: ReceiptInfo\n */\nclass ReceiptsByUser {\n private room: Room;\n\n /** map of userId: UserReceipts */\n private data: Map<string, UserReceipts>;\n\n public constructor(room: Room) {\n this.room = room;\n this.data = new Map<string, UserReceipts>();\n }\n\n /**\n * Add the supplied receipt to our structure, if it is not earlier than the\n * one we already hold for this user.\n */\n public set(eventId: string, receiptType: string, userId: string, ts: number, synthetic: boolean): void {\n const userReceipts = getOrCreate(this.data, userId, () => new UserReceipts(this.room));\n\n const existingReceipt = userReceipts.getByType(synthetic);\n if (existingReceipt && isAfter(existingReceipt.eventId, eventId, this.room)) {\n // The new receipt is before the existing one - don't store it.\n return;\n }\n\n // Possibilities:\n //\n // 1. there was no existing receipt, or\n // 2. the existing receipt was before this one, or\n // 3. we were unable to compare the receipts.\n //\n // In the case of 3 it's difficult to decide what to do, so the\n // most-recently-received receipt wins.\n //\n // Case 3 can only happen if the events for these receipts have\n // disappeared, which is quite unlikely since the new one has just been\n // checked, and the old one was checked before it was inserted here.\n //\n // We go ahead and store this receipt (replacing the other if it exists)\n userReceipts.set(synthetic, new ReceiptInfo(eventId, receiptType, ts));\n }\n\n /**\n * Find the latest receipt we have for this user. (Note - there is only one\n * receipt per user, because we are already inside a specific thread or\n * unthreaded list.)\n *\n * If there is a later synthetic receipt for this user, return that.\n * Otherwise, return the real receipt.\n *\n * @returns the found receipt info, or undefined if we have no receipt for this user.\n */\n public get(userId: string): ReceiptInfo | undefined {\n return this.data.get(userId)?.get();\n }\n}\n\n/**\n * The latest threaded receipts we have for a room.\n */\nclass ThreadedReceipts {\n private room: Room;\n\n /** map of threadId: ReceiptsByUser */\n private data: Map<string, ReceiptsByUser>;\n\n public constructor(room: Room) {\n this.room = room;\n this.data = new Map<string, ReceiptsByUser>();\n }\n\n /**\n * Add the supplied receipt to our structure, if it is not earlier than one\n * we already hold for this user in this thread.\n */\n public set(\n threadId: string,\n eventId: string,\n receiptType: string,\n userId: string,\n ts: number,\n synthetic: boolean,\n ): void {\n const receiptsByUser = getOrCreate(this.data, threadId, () => new ReceiptsByUser(this.room));\n receiptsByUser.set(eventId, receiptType, userId, ts, synthetic);\n }\n\n /**\n * Find the latest threaded receipt for the supplied user in the supplied thread.\n *\n * @returns the found receipt info or undefined if we don't have one.\n */\n public get(threadId: string, userId: string): ReceiptInfo | undefined {\n return this.data.get(threadId)?.get(userId);\n }\n}\n\n/**\n * All the receipts that we have received but can't process because we can't\n * find the event they refer to.\n *\n * We hold on to them so we can process them if their event arrives later.\n */\nclass DanglingReceipts {\n /**\n * eventId: DanglingReceipt[]\n */\n private data = new Map<string, Array<DanglingReceipt>>();\n\n /**\n * Remember the supplied dangling receipt.\n */\n public add(danglingReceipt: DanglingReceipt): void {\n const danglingReceipts = getOrCreate(this.data, danglingReceipt.eventId, () => []);\n danglingReceipts.push(danglingReceipt);\n }\n\n /**\n * Remove and return the dangling receipts for the given event ID.\n * @param eventId - the event ID to look for\n * @returns the found dangling receipts, or undefined if we don't have one.\n */\n public remove(eventId: string): Array<DanglingReceipt> | undefined {\n const danglingReceipts = this.data.get(eventId);\n this.data.delete(eventId);\n return danglingReceipts;\n }\n}\n\nfunction getOrCreate<K, V>(m: Map<K, V>, key: K, createFn: () => V): V {\n const found = m.get(key);\n if (found) {\n return found;\n } else {\n const created = createFn();\n m.set(key, created);\n return created;\n }\n}\n\n/**\n * Is left after right (or the same)?\n *\n * Only returns true if both events can be found, and left is after or the same\n * as right.\n *\n * @returns left \\>= right\n */\nfunction isAfterOrSame(leftEventId: string, rightEventId: string, room: Room): boolean {\n const comparison = room.compareEventOrdering(leftEventId, rightEventId);\n return comparison !== null && comparison >= 0;\n}\n\n/**\n * Is left strictly after right?\n *\n * Only returns true if both events can be found, and left is strictly after right.\n *\n * @returns left \\> right\n */\nfunction isAfter(leftEventId: string, rightEventId: string, room: Room): boolean {\n const comparison = room.compareEventOrdering(leftEventId, rightEventId);\n return comparison !== null && comparison > 0;\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,5 +1,6 @@
1
1
  import { type OAuth2ErrorResponse } from "./error.ts";
2
2
  import { type ValidatedAuthMetadata } from "./discover.ts";
3
+ import { type Logger } from "../logger.ts";
3
4
  /**
4
5
  * The expected response type from the token endpoint during authorization code flow
5
6
  * Normalized to always use capitalized 'Bearer' for token_type
@@ -105,13 +106,15 @@ export declare function validateDeviceAuthorizationResponse(response: unknown):
105
106
  * @param options.clientId - the client ID returned from client registration.
106
107
  * @param options.scope - the scope to request for authorization.
107
108
  * @param options.metadata - the validated OAuth2 metadata for the Identity Provider.
109
+ * @param options.logger - optional logger to use for the request, defaults to the root logger of the js-sdk.
108
110
  * @returns a promise that resolves to a device access token response,
109
111
  * or an error response if the user denies authorization or the device code expires.
110
112
  */
111
- export declare const startDeviceAuthorization: ({ clientId, scope, metadata, }: {
113
+ export declare const startDeviceAuthorization: ({ clientId, scope, metadata, logger, }: {
112
114
  clientId: string;
113
115
  scope: string;
114
116
  metadata: ValidatedAuthMetadata;
117
+ logger?: Logger;
115
118
  }) => Promise<DeviceAuthorizationResponse>;
116
119
  /**
117
120
  * Polls the OAuth2 token endpoint until we get a device access token response, or encounter an unrecoverable error.
@@ -119,13 +122,15 @@ export declare const startDeviceAuthorization: ({ clientId, scope, metadata, }:
119
122
  * @param options.session - The session returned from a previous call to {@link startDeviceAuthorization}.
120
123
  * @param options.metadata - The validated OAuth2 metadata for the Identity Provider.
121
124
  * @param options.clientId - The client ID returned from client registration.
125
+ * @param options.logger - optional logger to use for the requests, defaults to the root logger of the js-sdk.
122
126
  * @returns a promise that resolves to a device access token response,
123
127
  * or an error response if the user denies authorization or the device code expires.
124
128
  */
125
- export declare const waitForDeviceAuthorization: ({ session, metadata, clientId, }: {
129
+ export declare const waitForDeviceAuthorization: ({ session, metadata, clientId, logger, }: {
126
130
  session: DeviceAuthorizationResponse;
127
131
  metadata: ValidatedAuthMetadata;
128
132
  clientId: string;
133
+ logger?: Logger;
129
134
  }) => Promise<DeviceAccessTokenResponse | DeviceAccessTokenError>;
130
135
  export {};
131
136
  //# sourceMappingURL=authorize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/oauth/authorize.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAe,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAY3D;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,kBAAkB,EAAE,YAAY,CAAC,GAAG;IACvE,UAAU,EAAE,QAAQ,CAAC;CACxB,CAAC;AAEF;;;;;;;GAOG;AACH,UAAU,kBAAkB;IACxB,UAAU,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,IAAI,kBAAkB,CAarG;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,cAAe,MAAM,KAAG,MAGjD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,qCAAqC,aAAc,kBAAkB,KAAG,mBAGnF,CAAC;AAEH;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,IAAI,yBAAyB,CASzG;AAED;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IACxC,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,mCAAmC,CAC/C,QAAQ,EAAE,OAAO,GAClB,OAAO,CAAC,QAAQ,IAAI,2BAA2B,CAYjD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,mCAIlC;IACC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,qBAAqB,CAAC;CACnC,KAAG,OAAO,CAAC,2BAA2B,CAmBtC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,qCAIpC;IACC,OAAO,EAAE,2BAA2B,CAAC;IACrC,QAAQ,EAAE,qBAAqB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;CACpB,KAAG,OAAO,CAAC,yBAAyB,GAAG,sBAAsB,CAkC7D,CAAC"}
1
+ {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/oauth/authorize.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAe,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAW3D,OAAO,EAAE,KAAK,MAAM,EAAwB,MAAM,cAAc,CAAC;AAGjE;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,kBAAkB,EAAE,YAAY,CAAC,GAAG;IACvE,UAAU,EAAE,QAAQ,CAAC;CACxB,CAAC;AAEF;;;;;;;GAOG;AACH,UAAU,kBAAkB;IACxB,UAAU,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,IAAI,kBAAkB,CAarG;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,cAAe,MAAM,KAAG,MAGjD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,qCAAqC,aAAc,kBAAkB,KAAG,mBAGnF,CAAC;AAEH;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,IAAI,yBAAyB,CASzG;AAED;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IACxC,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,mCAAmC,CAC/C,QAAQ,EAAE,OAAO,GAClB,OAAO,CAAC,QAAQ,IAAI,2BAA2B,CAYjD;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,2CAKlC;IACC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,qBAAqB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,KAAG,OAAO,CAAC,2BAA2B,CAmBtC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,6CAKpC;IACC,OAAO,EAAE,2BAA2B,CAAC;IACrC,QAAQ,EAAE,qBAAqB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,KAAG,OAAO,CAAC,yBAAyB,GAAG,sBAAsB,CAkC7D,CAAC"}
@@ -21,6 +21,8 @@ import { hasOptionalNumberProperty, hasOptionalStringProperty, hasRequiredNumber
21
21
  import { Method } from "../http-api/index.js";
22
22
  import { OAuthGrantType } from "./register.js";
23
23
  import { sleep } from "../utils.js";
24
+ import { logger as rootLogger } from "../logger.js";
25
+ import { fetchWithLogging } from "./fetch.js";
24
26
 
25
27
  /**
26
28
  * The expected response type from the token endpoint during authorization code flow
@@ -112,13 +114,15 @@ export function validateDeviceAuthorizationResponse(response) {
112
114
  * @param options.clientId - the client ID returned from client registration.
113
115
  * @param options.scope - the scope to request for authorization.
114
116
  * @param options.metadata - the validated OAuth2 metadata for the Identity Provider.
117
+ * @param options.logger - optional logger to use for the request, defaults to the root logger of the js-sdk.
115
118
  * @returns a promise that resolves to a device access token response,
116
119
  * or an error response if the user denies authorization or the device code expires.
117
120
  */
118
121
  export const startDeviceAuthorization = async ({
119
122
  clientId,
120
123
  scope,
121
- metadata
124
+ metadata,
125
+ logger = rootLogger
122
126
  }) => {
123
127
  const body = new URLSearchParams({
124
128
  client_id: clientId,
@@ -128,7 +132,7 @@ export const startDeviceAuthorization = async ({
128
132
  if (!url) {
129
133
  throw new Error("No device_authorization_endpoint given");
130
134
  }
131
- const response = await fetch(url, {
135
+ const response = await fetchWithLogging(logger, url, {
132
136
  method: Method.Post,
133
137
  headers: {
134
138
  "Content-Type": "application/x-www-form-urlencoded"
@@ -146,13 +150,15 @@ export const startDeviceAuthorization = async ({
146
150
  * @param options.session - The session returned from a previous call to {@link startDeviceAuthorization}.
147
151
  * @param options.metadata - The validated OAuth2 metadata for the Identity Provider.
148
152
  * @param options.clientId - The client ID returned from client registration.
153
+ * @param options.logger - optional logger to use for the requests, defaults to the root logger of the js-sdk.
149
154
  * @returns a promise that resolves to a device access token response,
150
155
  * or an error response if the user denies authorization or the device code expires.
151
156
  */
152
157
  export const waitForDeviceAuthorization = async ({
153
158
  session,
154
159
  metadata,
155
- clientId
160
+ clientId,
161
+ logger = rootLogger
156
162
  }) => {
157
163
  let interval = (session.interval ?? 5) * 1000; // poll interval
158
164
  const expiration = Date.now() + session.expires_in * 1000;
@@ -162,7 +168,7 @@ export const waitForDeviceAuthorization = async ({
162
168
  grant_type: OAuthGrantType.DeviceAuthorization,
163
169
  client_id: clientId
164
170
  }).toString();
165
- const response = await fetch(metadata.token_endpoint, {
171
+ const response = await fetchWithLogging(logger, metadata.token_endpoint, {
166
172
  method: Method.Post,
167
173
  headers: {
168
174
  "Content-Type": "application/x-www-form-urlencoded"
@@ -1 +1 @@
1
- {"version":3,"file":"authorize.js","names":[],"sources":["../../src/oauth/authorize.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { secureRandomString } from \"../randomstring.ts\";\nimport { OAuth2Error, type OAuth2ErrorResponse } from \"./error.ts\";\nimport { type ValidatedAuthMetadata } from \"./discover.ts\";\nimport {\n hasOptionalNumberProperty,\n hasOptionalStringProperty,\n hasRequiredNumberProperty,\n hasRequiredStringProperty,\n isRecord,\n} from \"../@types/type-guards.ts\";\nimport { Method } from \"../http-api/index.ts\";\nimport { OAuthGrantType } from \"./register.ts\";\nimport { sleep } from \"../utils.ts\";\n\n/**\n * The expected response type from the token endpoint during authorization code flow\n * Normalized to always use capitalized 'Bearer' for token_type\n *\n * See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4\n */\nexport type BearerTokenResponse = Omit<ValidTokenResponse, \"token_type\"> & {\n token_type: \"Bearer\";\n};\n\n/**\n * Metadata from OAuth 2.0 token_endpoint as per\n * https://datatracker.ietf.org/doc/html/rfc6749#section-5.1\n * With validated properties required in type\n *\n * This response is expected for the authorization code grant and refresh token grant,\n * as defined in the Matrix spec.\n */\ninterface ValidTokenResponse {\n token_type: \"Bearer\" | \"bearer\";\n access_token: string;\n expires_in?: number;\n refresh_token?: string;\n scope?: string;\n}\n\n/**\n * Validate the given response matches the format expected for a {@link ValidTokenResponse}\n * @param response - the response to validate\n * @throws if the response does not match the expected format\n */\nexport function validateBearerTokenResponse(response: unknown): asserts response is ValidTokenResponse {\n if (\n !isRecord(response) ||\n !hasRequiredStringProperty(response, \"token_type\") ||\n // token_type is case-insensitive, some OPs return `token_type: \"bearer\"`\n response[\"token_type\"].toLowerCase() !== \"bearer\" ||\n !hasRequiredStringProperty(response, \"access_token\") ||\n !hasOptionalNumberProperty(response, \"expires_in\") ||\n !hasOptionalStringProperty(response, \"refresh_token\") ||\n !hasOptionalStringProperty(response, \"scope\")\n ) {\n throw new Error(OAuth2Error.InvalidBearerTokenResponse);\n }\n}\n\n/**\n * Generate the scope used in authorization request with OAuth2 IdP\n * @returns scope\n */\nexport const generateScope = (deviceId?: string): string => {\n const safeDeviceId = deviceId ?? secureRandomString(10);\n return `urn:matrix:client:api:* urn:matrix:client:device:${safeDeviceId}`;\n};\n\n/**\n * Normalize token_type to use capital case to make consuming the token response easier\n * token_type is case insensitive, and it is spec-compliant for OPs to return token_type: \"bearer\"\n * Later, when used in auth headers it is case sensitive and must be Bearer\n * See: https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4\n *\n * @param response - validated token response\n * @returns response with token_type set to 'Bearer'\n */\nexport const normalizeBearerTokenResponseTokenType = (response: ValidTokenResponse): BearerTokenResponse => ({\n ...response,\n token_type: \"Bearer\",\n});\n\n/**\n * Response from the OAuth2 token endpoint when exchanging a token for grant_type device_code.\n */\nexport interface DeviceAccessTokenResponse {\n access_token: string;\n token_type: string;\n refresh_token?: string;\n scope?: string;\n expires_in?: number;\n}\n\n/**\n * Validate the given response matches the format expected for a {@link DeviceAccessTokenResponse}\n * @param response - the response to validate\n * @throws if the response does not match the expected format\n */\nexport function isValidDeviceAccessTokenResponse(response: unknown): response is DeviceAccessTokenResponse {\n return (\n isRecord(response) &&\n hasRequiredStringProperty(response, \"access_token\") &&\n hasRequiredStringProperty(response, \"token_type\") &&\n hasOptionalStringProperty(response, \"refresh_token\") &&\n hasOptionalStringProperty(response, \"scope\") &&\n hasOptionalNumberProperty(response, \"expires_in\")\n );\n}\n\n/**\n * Error from the OAuth2 token endpoint when exchanging a token for grant_type device_code.\n */\nexport interface DeviceAccessTokenError extends OAuth2ErrorResponse {\n session_state?: string;\n}\n\n/**\n * Response from the OAuth2 device authorization endpoint.\n * As specified in https://datatracker.ietf.org/doc/html/rfc8628#section-3.2\n */\nexport interface DeviceAuthorizationResponse {\n /** The device verification code. */\n device_code: string;\n /** The end-user verification code. */\n user_code: string;\n /**\n * The end-user verification URI on the authorization server.\n * The URI should be short and easy to remember as end users will be asked to manually type it into their user agent.\n */\n verification_uri: string;\n /**\n * The URI which doesn’t require the user to manually type the user_code, designed for non-textual transmission.\n */\n verification_uri_complete?: string;\n /** The lifetime in seconds of the \"device_code\" and \"user_code\". */\n expires_in: number;\n /**\n * The minimum amount of time in seconds that the client SHOULD wait between polling requests to the token endpoint.\n * If no value is provided, clients MUST use 5 as the default.\n */\n interval?: number;\n}\n\n/**\n * Validate the given response matches the format expected for a {@link DeviceAuthorizationResponse}\n * @param response - the response to validate\n * @throws if the response does not match the expected format\n */\nexport function validateDeviceAuthorizationResponse(\n response: unknown,\n): asserts response is DeviceAuthorizationResponse {\n if (\n !isRecord(response) ||\n !hasRequiredStringProperty(response, \"device_code\") ||\n !hasRequiredStringProperty(response, \"user_code\") ||\n !hasRequiredStringProperty(response, \"verification_uri\") ||\n !hasRequiredNumberProperty(response, \"expires_in\") ||\n !hasOptionalStringProperty(response, \"verification_uri_complete\") ||\n !hasOptionalNumberProperty(response, \"interval\")\n ) {\n throw new Error(OAuth2Error.InvalidDeviceAuthorizationResponse);\n }\n}\n\n/**\n * Begin OAuth2 device authorization flow.\n * @param options - The device authorization parameters.\n * @param options.clientId - the client ID returned from client registration.\n * @param options.scope - the scope to request for authorization.\n * @param options.metadata - the validated OAuth2 metadata for the Identity Provider.\n * @returns a promise that resolves to a device access token response,\n * or an error response if the user denies authorization or the device code expires.\n */\nexport const startDeviceAuthorization = async ({\n clientId,\n scope,\n metadata,\n}: {\n clientId: string;\n scope: string;\n metadata: ValidatedAuthMetadata;\n}): Promise<DeviceAuthorizationResponse> => {\n const body = new URLSearchParams({ client_id: clientId, scope: scope }).toString();\n\n const url = metadata.device_authorization_endpoint;\n if (!url) {\n throw new Error(\"No device_authorization_endpoint given\");\n }\n\n const response = await fetch(url, {\n method: Method.Post,\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body,\n });\n\n const data = await response.json();\n validateDeviceAuthorizationResponse(data);\n return data;\n};\n\n/**\n * Polls the OAuth2 token endpoint until we get a device access token response, or encounter an unrecoverable error.\n * @param options - The device authorization parameters.\n * @param options.session - The session returned from a previous call to {@link startDeviceAuthorization}.\n * @param options.metadata - The validated OAuth2 metadata for the Identity Provider.\n * @param options.clientId - The client ID returned from client registration.\n * @returns a promise that resolves to a device access token response,\n * or an error response if the user denies authorization or the device code expires.\n */\nexport const waitForDeviceAuthorization = async ({\n session,\n metadata,\n clientId,\n}: {\n session: DeviceAuthorizationResponse;\n metadata: ValidatedAuthMetadata;\n clientId: string;\n}): Promise<DeviceAccessTokenResponse | DeviceAccessTokenError> => {\n let interval = (session.interval ?? 5) * 1000; // poll interval\n const expiration = Date.now() + session.expires_in * 1000;\n do {\n const body = new URLSearchParams({\n device_code: session.device_code,\n grant_type: OAuthGrantType.DeviceAuthorization,\n client_id: clientId,\n }).toString();\n const response = await fetch(metadata.token_endpoint, {\n method: Method.Post,\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n });\n\n const data = await response.json();\n\n if (response.ok && isValidDeviceAccessTokenResponse(data)) {\n return data;\n }\n const errorResponse = data as DeviceAccessTokenError;\n switch (errorResponse.error) {\n case \"authorization_pending\":\n break;\n case \"slow_down\":\n interval += 5000;\n break;\n case \"access_denied\":\n case \"expired_token\":\n return errorResponse;\n }\n await sleep(interval);\n } while (Date.now() < expiration);\n return { error: \"expired\" };\n};\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"authorize.js","names":[],"sources":["../../src/oauth/authorize.ts"],"sourcesContent":["/*\nCopyright 2023 The Matrix.org Foundation C.I.C.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { secureRandomString } from \"../randomstring.ts\";\nimport { OAuth2Error, type OAuth2ErrorResponse } from \"./error.ts\";\nimport { type ValidatedAuthMetadata } from \"./discover.ts\";\nimport {\n hasOptionalNumberProperty,\n hasOptionalStringProperty,\n hasRequiredNumberProperty,\n hasRequiredStringProperty,\n isRecord,\n} from \"../@types/type-guards.ts\";\nimport { Method } from \"../http-api/index.ts\";\nimport { OAuthGrantType } from \"./register.ts\";\nimport { sleep } from \"../utils.ts\";\nimport { type Logger, logger as rootLogger } from \"../logger.ts\";\nimport { fetchWithLogging } from \"./fetch.ts\";\n\n/**\n * The expected response type from the token endpoint during authorization code flow\n * Normalized to always use capitalized 'Bearer' for token_type\n *\n * See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4\n */\nexport type BearerTokenResponse = Omit<ValidTokenResponse, \"token_type\"> & {\n token_type: \"Bearer\";\n};\n\n/**\n * Metadata from OAuth 2.0 token_endpoint as per\n * https://datatracker.ietf.org/doc/html/rfc6749#section-5.1\n * With validated properties required in type\n *\n * This response is expected for the authorization code grant and refresh token grant,\n * as defined in the Matrix spec.\n */\ninterface ValidTokenResponse {\n token_type: \"Bearer\" | \"bearer\";\n access_token: string;\n expires_in?: number;\n refresh_token?: string;\n scope?: string;\n}\n\n/**\n * Validate the given response matches the format expected for a {@link ValidTokenResponse}\n * @param response - the response to validate\n * @throws if the response does not match the expected format\n */\nexport function validateBearerTokenResponse(response: unknown): asserts response is ValidTokenResponse {\n if (\n !isRecord(response) ||\n !hasRequiredStringProperty(response, \"token_type\") ||\n // token_type is case-insensitive, some OPs return `token_type: \"bearer\"`\n response[\"token_type\"].toLowerCase() !== \"bearer\" ||\n !hasRequiredStringProperty(response, \"access_token\") ||\n !hasOptionalNumberProperty(response, \"expires_in\") ||\n !hasOptionalStringProperty(response, \"refresh_token\") ||\n !hasOptionalStringProperty(response, \"scope\")\n ) {\n throw new Error(OAuth2Error.InvalidBearerTokenResponse);\n }\n}\n\n/**\n * Generate the scope used in authorization request with OAuth2 IdP\n * @returns scope\n */\nexport const generateScope = (deviceId?: string): string => {\n const safeDeviceId = deviceId ?? secureRandomString(10);\n return `urn:matrix:client:api:* urn:matrix:client:device:${safeDeviceId}`;\n};\n\n/**\n * Normalize token_type to use capital case to make consuming the token response easier\n * token_type is case insensitive, and it is spec-compliant for OPs to return token_type: \"bearer\"\n * Later, when used in auth headers it is case sensitive and must be Bearer\n * See: https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4\n *\n * @param response - validated token response\n * @returns response with token_type set to 'Bearer'\n */\nexport const normalizeBearerTokenResponseTokenType = (response: ValidTokenResponse): BearerTokenResponse => ({\n ...response,\n token_type: \"Bearer\",\n});\n\n/**\n * Response from the OAuth2 token endpoint when exchanging a token for grant_type device_code.\n */\nexport interface DeviceAccessTokenResponse {\n access_token: string;\n token_type: string;\n refresh_token?: string;\n scope?: string;\n expires_in?: number;\n}\n\n/**\n * Validate the given response matches the format expected for a {@link DeviceAccessTokenResponse}\n * @param response - the response to validate\n * @throws if the response does not match the expected format\n */\nexport function isValidDeviceAccessTokenResponse(response: unknown): response is DeviceAccessTokenResponse {\n return (\n isRecord(response) &&\n hasRequiredStringProperty(response, \"access_token\") &&\n hasRequiredStringProperty(response, \"token_type\") &&\n hasOptionalStringProperty(response, \"refresh_token\") &&\n hasOptionalStringProperty(response, \"scope\") &&\n hasOptionalNumberProperty(response, \"expires_in\")\n );\n}\n\n/**\n * Error from the OAuth2 token endpoint when exchanging a token for grant_type device_code.\n */\nexport interface DeviceAccessTokenError extends OAuth2ErrorResponse {\n session_state?: string;\n}\n\n/**\n * Response from the OAuth2 device authorization endpoint.\n * As specified in https://datatracker.ietf.org/doc/html/rfc8628#section-3.2\n */\nexport interface DeviceAuthorizationResponse {\n /** The device verification code. */\n device_code: string;\n /** The end-user verification code. */\n user_code: string;\n /**\n * The end-user verification URI on the authorization server.\n * The URI should be short and easy to remember as end users will be asked to manually type it into their user agent.\n */\n verification_uri: string;\n /**\n * The URI which doesn’t require the user to manually type the user_code, designed for non-textual transmission.\n */\n verification_uri_complete?: string;\n /** The lifetime in seconds of the \"device_code\" and \"user_code\". */\n expires_in: number;\n /**\n * The minimum amount of time in seconds that the client SHOULD wait between polling requests to the token endpoint.\n * If no value is provided, clients MUST use 5 as the default.\n */\n interval?: number;\n}\n\n/**\n * Validate the given response matches the format expected for a {@link DeviceAuthorizationResponse}\n * @param response - the response to validate\n * @throws if the response does not match the expected format\n */\nexport function validateDeviceAuthorizationResponse(\n response: unknown,\n): asserts response is DeviceAuthorizationResponse {\n if (\n !isRecord(response) ||\n !hasRequiredStringProperty(response, \"device_code\") ||\n !hasRequiredStringProperty(response, \"user_code\") ||\n !hasRequiredStringProperty(response, \"verification_uri\") ||\n !hasRequiredNumberProperty(response, \"expires_in\") ||\n !hasOptionalStringProperty(response, \"verification_uri_complete\") ||\n !hasOptionalNumberProperty(response, \"interval\")\n ) {\n throw new Error(OAuth2Error.InvalidDeviceAuthorizationResponse);\n }\n}\n\n/**\n * Begin OAuth2 device authorization flow.\n * @param options - The device authorization parameters.\n * @param options.clientId - the client ID returned from client registration.\n * @param options.scope - the scope to request for authorization.\n * @param options.metadata - the validated OAuth2 metadata for the Identity Provider.\n * @param options.logger - optional logger to use for the request, defaults to the root logger of the js-sdk.\n * @returns a promise that resolves to a device access token response,\n * or an error response if the user denies authorization or the device code expires.\n */\nexport const startDeviceAuthorization = async ({\n clientId,\n scope,\n metadata,\n logger = rootLogger,\n}: {\n clientId: string;\n scope: string;\n metadata: ValidatedAuthMetadata;\n logger?: Logger;\n}): Promise<DeviceAuthorizationResponse> => {\n const body = new URLSearchParams({ client_id: clientId, scope: scope }).toString();\n\n const url = metadata.device_authorization_endpoint;\n if (!url) {\n throw new Error(\"No device_authorization_endpoint given\");\n }\n\n const response = await fetchWithLogging(logger, url, {\n method: Method.Post,\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body,\n });\n\n const data = await response.json();\n validateDeviceAuthorizationResponse(data);\n return data;\n};\n\n/**\n * Polls the OAuth2 token endpoint until we get a device access token response, or encounter an unrecoverable error.\n * @param options - The device authorization parameters.\n * @param options.session - The session returned from a previous call to {@link startDeviceAuthorization}.\n * @param options.metadata - The validated OAuth2 metadata for the Identity Provider.\n * @param options.clientId - The client ID returned from client registration.\n * @param options.logger - optional logger to use for the requests, defaults to the root logger of the js-sdk.\n * @returns a promise that resolves to a device access token response,\n * or an error response if the user denies authorization or the device code expires.\n */\nexport const waitForDeviceAuthorization = async ({\n session,\n metadata,\n clientId,\n logger = rootLogger,\n}: {\n session: DeviceAuthorizationResponse;\n metadata: ValidatedAuthMetadata;\n clientId: string;\n logger?: Logger;\n}): Promise<DeviceAccessTokenResponse | DeviceAccessTokenError> => {\n let interval = (session.interval ?? 5) * 1000; // poll interval\n const expiration = Date.now() + session.expires_in * 1000;\n do {\n const body = new URLSearchParams({\n device_code: session.device_code,\n grant_type: OAuthGrantType.DeviceAuthorization,\n client_id: clientId,\n }).toString();\n const response = await fetchWithLogging(logger, metadata.token_endpoint, {\n method: Method.Post,\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n });\n\n const data = await response.json();\n\n if (response.ok && isValidDeviceAccessTokenResponse(data)) {\n return data;\n }\n const errorResponse = data as DeviceAccessTokenError;\n switch (errorResponse.error) {\n case \"authorization_pending\":\n break;\n case \"slow_down\":\n interval += 5000;\n break;\n case \"access_denied\":\n case \"expired_token\":\n return errorResponse;\n }\n await sleep(interval);\n } while (Date.now() < expiration);\n return { error: \"expired\" };\n};\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,17 @@
1
+ import { type Logger } from "../logger.ts";
2
+ /**
3
+ * Perform a `fetch` request, logging the request and response in the same manner as
4
+ * `FetchHttpApi` does for Client-Server API requests.
5
+ *
6
+ * Neither the request nor the response body is logged, as they routinely contain credentials.
7
+ * Query parameter values are redacted for the same reason.
8
+ *
9
+ * @internal
10
+ * @param logger - the logger to write the request and response lines to.
11
+ * @param resource - the URL to request.
12
+ * @param options - the options to pass to `fetch`.
13
+ * @returns the `Response`, whatever its status code.
14
+ * @throws rethrows whatever `fetch` threw, having logged it.
15
+ */
16
+ export declare function fetchWithLogging(logger: Logger, resource: URL | string, options?: RequestInit): Promise<Response>;
17
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/oauth/fetch.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C;;;;;;;;;;;;;GAaG;AACH,wBAAsB,gBAAgB,CAClC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,GAAG,GAAG,MAAM,EACtB,OAAO,GAAE,WAAgB,GAC1B,OAAO,CAAC,QAAQ,CAAC,CAenB"}