From 5e9d98ce4c0f5f1b8e8fff8790d303b218dfd471 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 15 Sep 2025 10:04:13 +0530 Subject: [PATCH 01/56] fix the tests --- packages/crypto/src/index.spec.ts | 3 + .../src/services/key.service.ts | 383 ++++++++++++++++++ 2 files changed, 386 insertions(+) create mode 100644 packages/federation-sdk/src/services/key.service.ts diff --git a/packages/crypto/src/index.spec.ts b/packages/crypto/src/index.spec.ts index c689ddcd7..fb6353580 100644 --- a/packages/crypto/src/index.spec.ts +++ b/packages/crypto/src/index.spec.ts @@ -1,11 +1,14 @@ import { describe, expect, it } from 'bun:test'; import { fromBase64ToBytes } from './utils/data-types'; import { + loadEd25519SignerFromSeed, + loadEd25519VerifierFromPublicKey, loadEd25519SignerFromSeed, loadEd25519VerifierFromPublicKey, signJson, verifyJsonSignature, } from './utils/keys'; +import { encodeCanonicalJson, fromBase64ToBytes } from './utils/data-types'; describe('signJson', () => { it('should sign a json object', async () => { diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts new file mode 100644 index 000000000..1ff938ca0 --- /dev/null +++ b/packages/federation-sdk/src/services/key.service.ts @@ -0,0 +1,383 @@ +import { singleton } from 'tsyringe'; + +import { type Signer } from '@hs/crypto'; +import { ConfigService } from './config.service'; +import { createLogger } from '../utils/logger'; +import { KeyRepository } from '../repositories/key.repository'; +import { type KeyV2ServerResponse, type ServerKey } from '@hs/core'; + +@singleton() +export class KeyService { + private key: Signer | undefined; + + private logger = createLogger('KeyService'); + + constructor( + private readonly configService: ConfigService, + private readonly keyRepository: KeyRepository, + ) { + this.configService.getSigningKey().then((key) => { + this.key = key; + }); + } + + private shouldRefetchKey( + serverName: string, // for logging + key: ServerKey['keys'][string], + validUntil?: number, // minimum_valid_until_ts + ) { + if (validUntil) { + if (key.expiresAt < validUntil) { + this.logger.debug( + `Key for ${serverName} is expired, ${key.expiresAt} < ${validUntil}, refetching keys`, + ); + return true; + } + + return false; + } + + if ((key._updatedAt.getTime() + key.expiresAt) / 2 < Date.now()) { + this.logger.debug(`Half life for key for ${serverName} is expired`); + return true; + } + } + + async validateKeySignature( + serverName: string, + serverkey: KeyV2ServerResponse, + ) { + const signatureKey = serverkey.signatures[serverName]; + if (!signatureKey) { + throw new Error(`No signature key found for origin server ${serverName}`); + } + + // validate the response first + for (const keyId of Object.keys(signatureKey)) { + const { key } = serverkey.verify_keys[keyId] ?? {}; + if (key) { + await verifySignaturesFromRemote( + serverkey, + serverName, + async () => new Uint8Array(Buffer.from(key, 'base64')), + ); + } + } + } + + // TODO: support using separate notary server, for now we use the same server + // since using the same server, we do not need to verify the signature, or rather we can not. + // because this is the only way we get fetch the key of the server we are using. + // TODO: once notary server is implemented, we need to verify the signature of the server we are using. + // one could say this implementation is not the most ideal. + // however, gotta use this path to get the tests to pass + async fetchKeysRemote(serverName: string): Promise { + // this doesn't need to be signed request + // notmal http is enough + + // 1. get the response from the server + const response = await fetch( + // TODO: move to federation-sdk + `https://${serverName}/_matrix/key/v2/server`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + if (!response.ok) { + throw new Error(`Failed to fetch keys from ${serverName}`); + } + + const data: KeyV2ServerResponse = await response.json(); // intentional throw + + // weird but to be sure + if (data.server_name !== serverName) { + throw new Error( + `Server name mismatch: ${data.server_name} !== ${serverName}`, + ); + } + + // make sure is signed by the originating server + await this.validateKeySignature(serverName, data); + + return data; + } + + convertKeyToKeyV2ServerResponse( + key: ServerKey, + ): Omit { + const { serverName, keys } = key; + + const verifyKeys = {} as KeyV2ServerResponse['verify_keys']; + const oldVerifyKeys = {} as KeyV2ServerResponse['old_verify_keys']; + + let validUntilTs = 0; + + for (const [keyId, { key, expiresAt, expiredTs }] of Object.entries(keys)) { + if (expiredTs) { + oldVerifyKeys[keyId] = { expired_ts: expiredTs, key }; + continue; + } + + // any other key should not be expired + verifyKeys[keyId] = { key }; + + // pick the largest - it should always be one + if (expiresAt > validUntilTs) { + validUntilTs = expiresAt; + } + } + + return { + server_name: serverName, + verify_keys: verifyKeys, + old_verify_keys: oldVerifyKeys, + valid_until_ts: validUntilTs, + }; + } + + convertKeyV2ServerResponseToKey(keyResponse: KeyV2ServerResponse) { + const keys = {} as ServerKey['keys']; + + for (const keyId of Object.keys(keyResponse.verify_keys)) { + const { key } = keyResponse.verify_keys[keyId]; + keys[keyId] = { + key, + _createdAt: new Date(), + expiresAt: keyResponse.valid_until_ts, + }; + } + + for (const keyId of Object.keys(keyResponse.old_verify_keys)) { + const { key, expired_ts } = keyResponse.old_verify_keys[keyId]; + keys[keyId] = { + key, + _createdAt: new Date(), + expiresAt: keyResponse.valid_until_ts, + expiredTs: expired_ts, + }; + } + return { + serverName: keyResponse.server_name, + keys, + }; + } + + // fetchKeys completes a query request essentially + async fetchAndStoreKey( + serverName: string, + { keyId, validUntil }: { keyId?: string; validUntil?: number }, + ): Promise { + // 1. check db + const { keys = {} } = + (await this.keyRepository.findAllKeyForServerName(serverName)) ?? {}; + + logger.log(`Key for ${serverName}: ${JSON.stringify(keys)}`); + + const validKeys = Object.entries(keys).filter(([id, key]) => { + return ( + keyId === id && !this.shouldRefetchKey(serverName, key, validUntil) + ); + }); + + logger.log( + `Found ${validKeys.length} keys in db for ${serverName}, ${JSON.stringify( + validKeys, + )}`, + ); + + // we return these valid keys + if (validKeys.length > 0) { + // const validKeyObj = validKeys.reduce((acc, [keyId, key]) => { + // acc[keyId] = key; + // return acc; + // }, {} as ServerKey["keys"]); + + // return this.convertKeyToKeyV2ServerResponse({ + // serverName, + // keys: validKeyObj, + // }); + return keys; + } + + logger.log( + `No valid keys found in db for ${serverName}, fetching remote keys`, + ); + + try { + const remoteKeys = await this.fetchKeysRemote(serverName); + + logger.log( + `Fetched keys for ${serverName}, JSON: ${JSON.stringify(remoteKeys)}`, + ); + + // if not expired store, irrespective of the keyId as we may use this later to validate requests or events + for (const keyId of Object.keys(remoteKeys.verify_keys)) { + const stored = await this.keyRepository.storeKey( + serverName, + keyId, + remoteKeys.verify_keys[keyId].key, + remoteKeys.valid_until_ts, + ); + + logger.log( + `Stored key for ${serverName} ${keyId}: ${JSON.stringify(stored)}`, + ); + } + + if (keyId && !Object.keys(remoteKeys.verify_keys).includes(keyId)) { + // was not asked about this key + logger.log( + `Was not asked about this key ${keyId}, returning empty array`, + ); + return null; + } + + // for /query controller yes this is a bit double work, however returning as ServerKey['keys'] makes the service feel more internal than external. + // coupled with the higher use it's a "meh" decision. + // TODO: maybe have a separate path ???? + return this.convertKeyV2ServerResponseToKey(remoteKeys).keys; + } catch (e) { + logger.error( + `Error fetching keys for ${serverName}: ${e}, returning cached keys`, + ); + return keys; + } + } + + async getValidVerifyKey( + serverName: string, + keyId: string, + ): Promise { + let depth = 0; + const _fetch = async () => { + if (depth) { + return null; + } + + const key = await this.keyRepository.findKey(serverName, keyId); + + if ( + !key || + key.keys[keyId].expiredTs || + key.keys[keyId].expiresAt < Date.now() + ) { + logger.error( + `Key for ${serverName} is expired, ${ + key?.keys[keyId].expiresAt + } < ${Date.now()}`, + ); + + // we try to refetch the key + await this.fetchAndStoreKey(serverName, { + keyId, + validUntil: Date.now(), + }); + + depth++; + + return _fetch(); + } + return key.keys[keyId].key; + }; + + return _fetch(); + } + + async getValidVerifyKeys(serverName: string, keyIds: string[]) { + const keys = await this.keyRepository.findAllKeyForServerName(serverName); + + const keysToFind = keys + ? Object.keys(keys.keys).filter((keyId) => !keyIds.includes(keyId)) + : keyIds; + + const foundKeys = await Promise.all( + keysToFind.map((keyToFind) => + this.fetchAndStoreKey(serverName, { + keyId: keyToFind, + validUntil: Date.now(), + }), + ), + ); + + if (keys) { + // ?/ + // + } + + return foundKeys; + } + + // async fetchKeysFromServer( + // notaryServerName: string, + // request: Record> + // ) { + // const foundkeys = []; + // const notaryQueryRequest = {} as V2KeyQueryBody["server_keys"]; + + // // FIXME: pointless + // for (const [serverName, filter] of Object.entries(request)) { + // for (const [keyId, { validUntil }] of Object.entries(filter)) { + // const keys = await this.keyRepository.findKeys( + // serverName, + // keyId, + // validUntil + // ); + // const keysArray = await keys.toArray(); + + // if (keysArray.length === 0) { + // notaryQueryRequest[serverName] = { + // [keyId]: { minimum_valid_until_ts: validUntil }, + // }; + // } else { + // foundkeys.push(...keysArray); + // } + // } + // } + + // if (Object.keys(notaryQueryRequest).length > 0) { + // // make sure we have the keys for the notary server + // const notaryKeys = await this.fetchKeys(notaryServerName, { + // keyId: undefined, + // validUntil: undefined, + // }); + + // const notaryQueryResponse = await fetch( + // `https://${notaryServerName}/_matrix/key/v2/query`, + // { + // method: "POST", + // body: JSON.stringify(notaryQueryRequest), + // } + // ); + + // if (!notaryQueryResponse.ok) { + // throw new Error( + // `Failed to fetch keys from notary server ${notaryServerName}` + // ); + // } + + // const notaryQueryResponseData: V2KeyQueryResponse = + // await notaryQueryResponse.json(); + + // for (const serverKey of notaryQueryResponseData.server_keys) { + // try { + // await this.validateKeySignature(notaryServerName, serverKey); + // } catch (e) { + // logger.error( + // `Error validating key signature for ${notaryServerName}: ${e}` + // ); + // continue; + // } + + // foundkeys.push(serverKey); + // } + // } + + // await Promise.all(foundkeys.map((key) => this.keyRepository.storeKey(key))); + + // return foundkeys; + // } +} From 8cbcc048db3936ff093e761743e3152ded937c1b Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 15 Sep 2025 10:22:30 +0530 Subject: [PATCH 02/56] test changes --- packages/crypto/src/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/crypto/src/index.spec.ts b/packages/crypto/src/index.spec.ts index fb6353580..ccb5c4f0b 100644 --- a/packages/crypto/src/index.spec.ts +++ b/packages/crypto/src/index.spec.ts @@ -8,7 +8,7 @@ import { signJson, verifyJsonSignature, } from './utils/keys'; -import { encodeCanonicalJson, fromBase64ToBytes } from './utils/data-types'; +import { fromBase64ToBytes } from './utils/data-types'; describe('signJson', () => { it('should sign a json object', async () => { From 453b8abe645b02c772c43cec1041192a7d6c95ad Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 15 Sep 2025 10:45:55 +0530 Subject: [PATCH 03/56] mistakenly committed --- .../src/services/key.service.ts | 383 ------------------ 1 file changed, 383 deletions(-) delete mode 100644 packages/federation-sdk/src/services/key.service.ts diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts deleted file mode 100644 index 1ff938ca0..000000000 --- a/packages/federation-sdk/src/services/key.service.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { singleton } from 'tsyringe'; - -import { type Signer } from '@hs/crypto'; -import { ConfigService } from './config.service'; -import { createLogger } from '../utils/logger'; -import { KeyRepository } from '../repositories/key.repository'; -import { type KeyV2ServerResponse, type ServerKey } from '@hs/core'; - -@singleton() -export class KeyService { - private key: Signer | undefined; - - private logger = createLogger('KeyService'); - - constructor( - private readonly configService: ConfigService, - private readonly keyRepository: KeyRepository, - ) { - this.configService.getSigningKey().then((key) => { - this.key = key; - }); - } - - private shouldRefetchKey( - serverName: string, // for logging - key: ServerKey['keys'][string], - validUntil?: number, // minimum_valid_until_ts - ) { - if (validUntil) { - if (key.expiresAt < validUntil) { - this.logger.debug( - `Key for ${serverName} is expired, ${key.expiresAt} < ${validUntil}, refetching keys`, - ); - return true; - } - - return false; - } - - if ((key._updatedAt.getTime() + key.expiresAt) / 2 < Date.now()) { - this.logger.debug(`Half life for key for ${serverName} is expired`); - return true; - } - } - - async validateKeySignature( - serverName: string, - serverkey: KeyV2ServerResponse, - ) { - const signatureKey = serverkey.signatures[serverName]; - if (!signatureKey) { - throw new Error(`No signature key found for origin server ${serverName}`); - } - - // validate the response first - for (const keyId of Object.keys(signatureKey)) { - const { key } = serverkey.verify_keys[keyId] ?? {}; - if (key) { - await verifySignaturesFromRemote( - serverkey, - serverName, - async () => new Uint8Array(Buffer.from(key, 'base64')), - ); - } - } - } - - // TODO: support using separate notary server, for now we use the same server - // since using the same server, we do not need to verify the signature, or rather we can not. - // because this is the only way we get fetch the key of the server we are using. - // TODO: once notary server is implemented, we need to verify the signature of the server we are using. - // one could say this implementation is not the most ideal. - // however, gotta use this path to get the tests to pass - async fetchKeysRemote(serverName: string): Promise { - // this doesn't need to be signed request - // notmal http is enough - - // 1. get the response from the server - const response = await fetch( - // TODO: move to federation-sdk - `https://${serverName}/_matrix/key/v2/server`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - - if (!response.ok) { - throw new Error(`Failed to fetch keys from ${serverName}`); - } - - const data: KeyV2ServerResponse = await response.json(); // intentional throw - - // weird but to be sure - if (data.server_name !== serverName) { - throw new Error( - `Server name mismatch: ${data.server_name} !== ${serverName}`, - ); - } - - // make sure is signed by the originating server - await this.validateKeySignature(serverName, data); - - return data; - } - - convertKeyToKeyV2ServerResponse( - key: ServerKey, - ): Omit { - const { serverName, keys } = key; - - const verifyKeys = {} as KeyV2ServerResponse['verify_keys']; - const oldVerifyKeys = {} as KeyV2ServerResponse['old_verify_keys']; - - let validUntilTs = 0; - - for (const [keyId, { key, expiresAt, expiredTs }] of Object.entries(keys)) { - if (expiredTs) { - oldVerifyKeys[keyId] = { expired_ts: expiredTs, key }; - continue; - } - - // any other key should not be expired - verifyKeys[keyId] = { key }; - - // pick the largest - it should always be one - if (expiresAt > validUntilTs) { - validUntilTs = expiresAt; - } - } - - return { - server_name: serverName, - verify_keys: verifyKeys, - old_verify_keys: oldVerifyKeys, - valid_until_ts: validUntilTs, - }; - } - - convertKeyV2ServerResponseToKey(keyResponse: KeyV2ServerResponse) { - const keys = {} as ServerKey['keys']; - - for (const keyId of Object.keys(keyResponse.verify_keys)) { - const { key } = keyResponse.verify_keys[keyId]; - keys[keyId] = { - key, - _createdAt: new Date(), - expiresAt: keyResponse.valid_until_ts, - }; - } - - for (const keyId of Object.keys(keyResponse.old_verify_keys)) { - const { key, expired_ts } = keyResponse.old_verify_keys[keyId]; - keys[keyId] = { - key, - _createdAt: new Date(), - expiresAt: keyResponse.valid_until_ts, - expiredTs: expired_ts, - }; - } - return { - serverName: keyResponse.server_name, - keys, - }; - } - - // fetchKeys completes a query request essentially - async fetchAndStoreKey( - serverName: string, - { keyId, validUntil }: { keyId?: string; validUntil?: number }, - ): Promise { - // 1. check db - const { keys = {} } = - (await this.keyRepository.findAllKeyForServerName(serverName)) ?? {}; - - logger.log(`Key for ${serverName}: ${JSON.stringify(keys)}`); - - const validKeys = Object.entries(keys).filter(([id, key]) => { - return ( - keyId === id && !this.shouldRefetchKey(serverName, key, validUntil) - ); - }); - - logger.log( - `Found ${validKeys.length} keys in db for ${serverName}, ${JSON.stringify( - validKeys, - )}`, - ); - - // we return these valid keys - if (validKeys.length > 0) { - // const validKeyObj = validKeys.reduce((acc, [keyId, key]) => { - // acc[keyId] = key; - // return acc; - // }, {} as ServerKey["keys"]); - - // return this.convertKeyToKeyV2ServerResponse({ - // serverName, - // keys: validKeyObj, - // }); - return keys; - } - - logger.log( - `No valid keys found in db for ${serverName}, fetching remote keys`, - ); - - try { - const remoteKeys = await this.fetchKeysRemote(serverName); - - logger.log( - `Fetched keys for ${serverName}, JSON: ${JSON.stringify(remoteKeys)}`, - ); - - // if not expired store, irrespective of the keyId as we may use this later to validate requests or events - for (const keyId of Object.keys(remoteKeys.verify_keys)) { - const stored = await this.keyRepository.storeKey( - serverName, - keyId, - remoteKeys.verify_keys[keyId].key, - remoteKeys.valid_until_ts, - ); - - logger.log( - `Stored key for ${serverName} ${keyId}: ${JSON.stringify(stored)}`, - ); - } - - if (keyId && !Object.keys(remoteKeys.verify_keys).includes(keyId)) { - // was not asked about this key - logger.log( - `Was not asked about this key ${keyId}, returning empty array`, - ); - return null; - } - - // for /query controller yes this is a bit double work, however returning as ServerKey['keys'] makes the service feel more internal than external. - // coupled with the higher use it's a "meh" decision. - // TODO: maybe have a separate path ???? - return this.convertKeyV2ServerResponseToKey(remoteKeys).keys; - } catch (e) { - logger.error( - `Error fetching keys for ${serverName}: ${e}, returning cached keys`, - ); - return keys; - } - } - - async getValidVerifyKey( - serverName: string, - keyId: string, - ): Promise { - let depth = 0; - const _fetch = async () => { - if (depth) { - return null; - } - - const key = await this.keyRepository.findKey(serverName, keyId); - - if ( - !key || - key.keys[keyId].expiredTs || - key.keys[keyId].expiresAt < Date.now() - ) { - logger.error( - `Key for ${serverName} is expired, ${ - key?.keys[keyId].expiresAt - } < ${Date.now()}`, - ); - - // we try to refetch the key - await this.fetchAndStoreKey(serverName, { - keyId, - validUntil: Date.now(), - }); - - depth++; - - return _fetch(); - } - return key.keys[keyId].key; - }; - - return _fetch(); - } - - async getValidVerifyKeys(serverName: string, keyIds: string[]) { - const keys = await this.keyRepository.findAllKeyForServerName(serverName); - - const keysToFind = keys - ? Object.keys(keys.keys).filter((keyId) => !keyIds.includes(keyId)) - : keyIds; - - const foundKeys = await Promise.all( - keysToFind.map((keyToFind) => - this.fetchAndStoreKey(serverName, { - keyId: keyToFind, - validUntil: Date.now(), - }), - ), - ); - - if (keys) { - // ?/ - // - } - - return foundKeys; - } - - // async fetchKeysFromServer( - // notaryServerName: string, - // request: Record> - // ) { - // const foundkeys = []; - // const notaryQueryRequest = {} as V2KeyQueryBody["server_keys"]; - - // // FIXME: pointless - // for (const [serverName, filter] of Object.entries(request)) { - // for (const [keyId, { validUntil }] of Object.entries(filter)) { - // const keys = await this.keyRepository.findKeys( - // serverName, - // keyId, - // validUntil - // ); - // const keysArray = await keys.toArray(); - - // if (keysArray.length === 0) { - // notaryQueryRequest[serverName] = { - // [keyId]: { minimum_valid_until_ts: validUntil }, - // }; - // } else { - // foundkeys.push(...keysArray); - // } - // } - // } - - // if (Object.keys(notaryQueryRequest).length > 0) { - // // make sure we have the keys for the notary server - // const notaryKeys = await this.fetchKeys(notaryServerName, { - // keyId: undefined, - // validUntil: undefined, - // }); - - // const notaryQueryResponse = await fetch( - // `https://${notaryServerName}/_matrix/key/v2/query`, - // { - // method: "POST", - // body: JSON.stringify(notaryQueryRequest), - // } - // ); - - // if (!notaryQueryResponse.ok) { - // throw new Error( - // `Failed to fetch keys from notary server ${notaryServerName}` - // ); - // } - - // const notaryQueryResponseData: V2KeyQueryResponse = - // await notaryQueryResponse.json(); - - // for (const serverKey of notaryQueryResponseData.server_keys) { - // try { - // await this.validateKeySignature(notaryServerName, serverKey); - // } catch (e) { - // logger.error( - // `Error validating key signature for ${notaryServerName}: ${e}` - // ); - // continue; - // } - - // foundkeys.push(serverKey); - // } - // } - - // await Promise.all(foundkeys.map((key) => this.keyRepository.storeKey(key))); - - // return foundkeys; - // } -} From 71af33169dc7eb8ed5ca14ac918c8db9df9db96e Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 15 Sep 2025 10:52:01 +0530 Subject: [PATCH 04/56] fix import order in test file --- packages/crypto/src/index.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/crypto/src/index.spec.ts b/packages/crypto/src/index.spec.ts index ccb5c4f0b..cbe7d33d5 100644 --- a/packages/crypto/src/index.spec.ts +++ b/packages/crypto/src/index.spec.ts @@ -8,7 +8,6 @@ import { signJson, verifyJsonSignature, } from './utils/keys'; -import { fromBase64ToBytes } from './utils/data-types'; describe('signJson', () => { it('should sign a json object', async () => { From fc13a07a9729fb13f7e6d04281730d4ac8b19972 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 15 Sep 2025 10:55:56 +0530 Subject: [PATCH 05/56] update lockfile --- bun.lock | 2911 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 2312 insertions(+), 599 deletions(-) diff --git a/bun.lock b/bun.lock index 5101ba395..248fa06d9 100644 --- a/bun.lock +++ b/bun.lock @@ -1,600 +1,2313 @@ { - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "homeserver", - "dependencies": { - "dotenv": "^16.5.0", - "memoize": "^10.1.0", - "reflect-metadata": "^0.2.2", - "rollup": "^4.52.4", - "rollup-plugin-dts": "^6.2.3", - "tsyringe": "^4.10.0", - "tweetnacl": "^1.0.3", - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@types/bun": "latest", - "@types/express": "^5.0.1", - "@types/node": "^22.15.18", - "@types/sinon": "^17.0.4", - "husky": "^9.1.7", - "lint-staged": "^16.1.2", - "sinon": "^20.0.0", - "tsconfig-paths": "^4.2.0", - "turbo": "~2.5.6", - "typescript": "~5.9.2", - }, - }, - "packages/core": { - "name": "@rocket.chat/federation-core", - "version": "1.0.50", - "dependencies": { - "@rocket.chat/federation-crypto": "workspace:*", - "@rocket.chat/federation-room": "workspace:*", - "pino": "^9.11.0", - }, - "devDependencies": { - "bun-types": "latest", - "pino-pretty": "^13.1.1", - "ts-node": "^10.9.2", - "ts-patch": "^3.1.2", - "typescript": "~5.9.2", - }, - }, - "packages/crypto": { - "name": "@rocket.chat/federation-crypto", - "version": "0.0.1", - "dependencies": { - "@noble/ed25519": "^3.0.0", - }, - "devDependencies": { - "bun-types": "latest", - }, - }, - "packages/federation-sdk": { - "name": "@rocket.chat/federation-sdk", - "version": "0.1.24", - "dependencies": { - "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-core": "workspace:*", - "@rocket.chat/federation-crypto": "workspace:*", - "@rocket.chat/federation-room": "workspace:*", - "mongodb": "^6.16.0", - "reflect-metadata": "^0.2.2", - "tsyringe": "^4.10.0", - "tweetnacl": "^1.0.3", - "zod": "^3.24.1", - }, - "peerDependencies": { - "typescript": "~5.9.2", - }, - }, - "packages/homeserver": { - "name": "@rocket.chat/homeserver", - "version": "1.0.50", - "dependencies": { - "@bogeychan/elysia-etag": "^0.0.6", - "@bogeychan/elysia-logger": "^0.1.4", - "@elysiajs/swagger": "^1.3.0", - "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-core": "workspace:*", - "@rocket.chat/federation-room": "workspace:*", - "@rocket.chat/federation-sdk": "workspace:*", - "elysia": "^1.1.26", - "mongodb": "^6.16.0", - "tsyringe": "^4.10.0", - }, - "devDependencies": { - "bun-types": "latest", - }, - }, - "packages/room": { - "name": "@rocket.chat/federation-room", - "version": "1.0.50", - "dependencies": { - "@datastructures-js/priority-queue": "^6.3.3", - "@rocket.chat/federation-crypto": "workspace:*", - "zod": "^3.22.4", - }, - "devDependencies": { - "bun-types": "latest", - }, - }, - }, - "packages": { - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], - - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - - "@bogeychan/elysia-etag": ["@bogeychan/elysia-etag@0.0.6", "", { "peerDependencies": { "elysia": ">= 1.0.22" } }, "sha512-DPHRQJLm4mR5zkQk+DYhDEX1YFh0S5M3ZTqLy/SJ+kdT68c3wxynkyZZtL2YRNhZ0LKHuDXzqAXd77eTZeKxMg=="], - - "@bogeychan/elysia-logger": ["@bogeychan/elysia-logger@0.1.8", "", { "dependencies": { "pino": "^9.6.0" }, "peerDependencies": { "elysia": ">= 1.2.10" } }, "sha512-TbCpMX+m68t0FbvpbBjMrCs4HQ9f1twkvTSGf6ShAkjash7zP9vGLGnEJ0iSG0ymgqLNN8Dgq0SdAEaMC6XLug=="], - - "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], - - "@datastructures-js/heap": ["@datastructures-js/heap@4.3.3", "", {}, "sha512-UcUu/DLh/aM4W3C8zZfwxxm6/6FIZUlm3mcAXuNOCa6Aj4iizNvNXQyb8DjZQH2jKSQbMRyNlngP6TPimuGjpQ=="], - - "@datastructures-js/priority-queue": ["@datastructures-js/priority-queue@6.3.3", "", { "dependencies": { "@datastructures-js/heap": "^4.3.3" } }, "sha512-CIbUf0h4TPu1tHJ2gV4OTjwPkh7XZNb37u44bwoW6Py2vPdgaRUhFh3qFET6jvhyMNq/+ChWfOBRh+9s5WUtvA=="], - - "@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - - "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.3.0", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ=="], - - "@noble/ed25519": ["@noble/ed25519@3.0.0", "", {}, "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg=="], - - "@rocket.chat/emitter": ["@rocket.chat/emitter@0.31.25", "", {}, "sha512-hw5BpDlNwpYSb+K5X3DNMNUVEVXxmXugUPetGZGCWvntSVFsOjYuVEypoKW6vBBXSfqCBb0kN1npYcKEb4NFBw=="], - - "@rocket.chat/federation-core": ["@rocket.chat/federation-core@workspace:packages/core"], - - "@rocket.chat/federation-crypto": ["@rocket.chat/federation-crypto@workspace:packages/crypto"], - - "@rocket.chat/federation-room": ["@rocket.chat/federation-room@workspace:packages/room"], - - "@rocket.chat/federation-sdk": ["@rocket.chat/federation-sdk@workspace:packages/federation-sdk"], - - "@rocket.chat/homeserver": ["@rocket.chat/homeserver@workspace:packages/homeserver"], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="], - - "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], - - "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], - - "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], - - "@sinclair/typebox": ["@sinclair/typebox@0.34.37", "", {}, "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw=="], - - "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], - - "@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - - "@sinonjs/samsam": ["@sinonjs/samsam@8.0.2", "", { "dependencies": { "@sinonjs/commons": "^3.0.1", "lodash.get": "^4.4.2", "type-detect": "^4.1.0" } }, "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw=="], - - "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], - - "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - - "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], - - "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], - - "@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="], - - "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], - - "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - - "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], - - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], - - "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="], - - "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], - - "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - - "@types/node": ["@types/node@22.16.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ=="], - - "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], - - "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - - "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], - - "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], - - "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], - - "@types/sinon": ["@types/sinon@17.0.4", "", { "dependencies": { "@types/sinonjs__fake-timers": "*" } }, "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew=="], - - "@types/sinonjs__fake-timers": ["@types/sinonjs__fake-timers@8.1.5", "", {}, "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ=="], - - "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], - - "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], - - "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], - - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], - - "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], - - "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="], - - "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], - - "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - - "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - - "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], - - "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], - - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], - - "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="], - - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - - "elysia": ["elysia@1.3.5", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw=="], - - "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], - - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - - "exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="], - - "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], - - "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], - - "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], - - "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], - - "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], - - "file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], - - "global-prefix": ["global-prefix@4.0.0", "", { "dependencies": { "ini": "^4.1.3", "kind-of": "^6.0.3", "which": "^4.0.0" } }, "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], - - "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], - - "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], - - "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], - - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "lint-staged": ["lint-staged@16.1.2", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0", "debug": "^4.4.1", "lilconfig": "^3.1.3", "listr2": "^8.3.3", "micromatch": "^4.0.8", "nano-spawn": "^1.0.2", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q=="], - - "listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="], - - "lodash.get": ["lodash.get@4.4.2", "", {}, "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="], - - "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], - - "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], - - "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], - - "memoize": ["memoize@10.1.0", "", { "dependencies": { "mimic-function": "^5.0.1" } }, "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg=="], - - "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mongodb": ["mongodb@6.17.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.4", "mongodb-connection-string-url": "^3.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA=="], - - "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nano-spawn": ["nano-spawn@1.0.2", "", {}, "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg=="], - - "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], - - "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - - "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], - - "pino": ["pino@9.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-+YIodBB9sxcWeR8PrXC2K3gEDyfkUuVEITOcbqrfcj+z5QW4ioIcqZfYFbrLTYLsmAwunbS7nfU/dpBB6PZc1g=="], - - "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], - - "pino-pretty": ["pino-pretty@13.1.1", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA=="], - - "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], - - "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], - - "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], - - "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], - - "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], - - "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], - - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - - "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - - "rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="], - - "rollup-plugin-dts": ["rollup-plugin-dts@6.2.3", "", { "dependencies": { "magic-string": "^0.30.17" }, "optionalDependencies": { "@babel/code-frame": "^7.27.1" }, "peerDependencies": { "rollup": "^3.29.4 || ^4", "typescript": "^4.5 || ^5.0" } }, "sha512-UgnEsfciXSPpASuOelix7m4DrmyQgiaWBnvI0TM4GxuDh5FkqW8E5hu57bCxXB90VvR1WNfLV80yEDN18UogSA=="], - - "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - - "secure-json-parse": ["secure-json-parse@4.0.0", "", {}, "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA=="], - - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "sinon": ["sinon@20.0.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^13.0.5", "@sinonjs/samsam": "^8.0.1", "diff": "^7.0.0", "supports-color": "^7.2.0" } }, "sha512-+FXOAbdnj94AQIxH0w1v8gzNxkawVvNqE3jUzRLptR71Oykeu2RrQXXl/VQjKay+Qnh73fDt/oDfMo6xMeDQbQ=="], - - "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], - - "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], - - "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], - - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - - "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], - - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - - "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - - "strtok3": ["strtok3@10.3.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "token-types": ["token-types@6.0.3", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ=="], - - "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], - - "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], - - "ts-patch": ["ts-patch@3.3.0", "", { "dependencies": { "chalk": "^4.1.2", "global-prefix": "^4.0.0", "minimist": "^1.2.8", "resolve": "^1.22.2", "semver": "^7.6.3", "strip-ansi": "^6.0.1" }, "bin": { "ts-patch": "bin/ts-patch.js", "tspc": "bin/tspc.js" } }, "sha512-zAOzDnd5qsfEnjd9IGy1IRuvA7ygyyxxdxesbhMdutt8AHFjD8Vw8hU2rMF89HX1BKRWFYqKHrO8Q6lw0NeUZg=="], - - "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], - - "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - - "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], - - "turbo": ["turbo@2.5.6", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.6", "turbo-darwin-arm64": "2.5.6", "turbo-linux-64": "2.5.6", "turbo-linux-arm64": "2.5.6", "turbo-windows-64": "2.5.6", "turbo-windows-arm64": "2.5.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w=="], - - "turbo-darwin-64": ["turbo-darwin-64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A=="], - - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA=="], - - "turbo-linux-64": ["turbo-linux-64@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA=="], - - "turbo-linux-arm64": ["turbo-linux-arm64@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ=="], - - "turbo-windows-64": ["turbo-windows-64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg=="], - - "turbo-windows-arm64": ["turbo-windows-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q=="], - - "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], - - "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], - - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - - "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - - "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], - - "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], - - "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], - - "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], - - "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], - - "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], - - "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], - - "zod": ["zod@3.25.71", "", {}, "sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q=="], - - "@bogeychan/elysia-logger/pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="], - - "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="], - - "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], - - "@sinonjs/samsam/type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], - - "@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], - - "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], - - "log-update/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "ts-node/diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], - - "ts-patch/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], - - "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], - - "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - } -} + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "homeserver", + "dependencies": { + "dotenv": "^16.5.0", + "memoize": "^10.1.0", + "reflect-metadata": "^0.2.2", + "rollup": "^4.52.4", + "rollup-plugin-dts": "^6.2.3", + "tsyringe": "^4.10.0", + "tweetnacl": "^1.0.3", + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/bun": "latest", + "@types/express": "^5.0.1", + "@types/node": "^22.15.18", + "@types/sinon": "^17.0.4", + "husky": "^9.1.7", + "lint-staged": "^16.1.2", + "sinon": "^20.0.0", + "tsconfig-paths": "^4.2.0", + "turbo": "~2.5.6", + "typescript": "~5.9.2", + }, + }, + "packages/core": { + "name": "@rocket.chat/federation-core", + "version": "1.0.50", + "dependencies": { + "@rocket.chat/federation-crypto": "workspace:*", + "@rocket.chat/federation-room": "workspace:*", + "pino": "^9.11.0", + }, + "devDependencies": { + "bun-types": "latest", + "pino-pretty": "^13.1.1", + "ts-node": "^10.9.2", + "ts-patch": "^3.1.2", + "typescript": "~5.9.2", + }, + }, + "packages/crypto": { + "name": "@rocket.chat/federation-crypto", + "version": "0.0.1", + "dependencies": { + "@noble/ed25519": "^3.0.0", + }, + "devDependencies": { + "bun-types": "latest", + }, + }, + "packages/federation-sdk": { + "name": "@rocket.chat/federation-sdk", + "version": "0.1.24", + "dependencies": { + "@rocket.chat/emitter": "^0.31.25", + "@rocket.chat/federation-core": "workspace:*", + "@rocket.chat/federation-crypto": "workspace:*", + "@rocket.chat/federation-room": "workspace:*", + "mongodb": "^6.16.0", + "reflect-metadata": "^0.2.2", + "tsyringe": "^4.10.0", + "tweetnacl": "^1.0.3", + "zod": "^3.24.1", + }, + "peerDependencies": { + "typescript": "~5.9.2", + }, + }, + "packages/homeserver": { + "name": "@rocket.chat/homeserver", + "version": "1.0.50", + "dependencies": { + "@bogeychan/elysia-etag": "^0.0.6", + "@bogeychan/elysia-logger": "^0.1.4", + "@elysiajs/swagger": "^1.3.0", + "@rocket.chat/emitter": "^0.31.25", + "@rocket.chat/federation-core": "workspace:*", + "@rocket.chat/federation-room": "workspace:*", + "@rocket.chat/federation-sdk": "workspace:*", + "elysia": "^1.1.26", + "mongodb": "^6.16.0", + "tsyringe": "^4.10.0", + }, + "devDependencies": { + "bun-types": "latest", + }, + }, + "packages/room": { + "name": "@rocket.chat/federation-room", + "version": "1.0.50", + "dependencies": { + "@datastructures-js/priority-queue": "^6.3.3", + "@rocket.chat/federation-crypto": "workspace:*", + "zod": "^3.22.4", + }, + "devDependencies": { + "bun-types": "latest", + }, + }, + }, + "packages": { + "@babel/code-frame": [ + "@babel/code-frame@7.27.1", + "", + { + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==" + ], + "@babel/helper-validator-identifier": [ + "@babel/helper-validator-identifier@7.27.1", + "", + {}, + "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" + ], + "@biomejs/biome": [ + "@biomejs/biome@1.9.4", + "", + { + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + }, + "bin": { + "biome": "bin/biome" + } + }, + "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==" + ], + "@biomejs/cli-darwin-arm64": [ + "@biomejs/cli-darwin-arm64@1.9.4", + "", + { + "os": "darwin", + "cpu": "arm64" + }, + "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==" + ], + "@biomejs/cli-darwin-x64": [ + "@biomejs/cli-darwin-x64@1.9.4", + "", + { + "os": "darwin", + "cpu": "x64" + }, + "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==" + ], + "@biomejs/cli-linux-arm64": [ + "@biomejs/cli-linux-arm64@1.9.4", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==" + ], + "@biomejs/cli-linux-arm64-musl": [ + "@biomejs/cli-linux-arm64-musl@1.9.4", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==" + ], + "@biomejs/cli-linux-x64": [ + "@biomejs/cli-linux-x64@1.9.4", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==" + ], + "@biomejs/cli-linux-x64-musl": [ + "@biomejs/cli-linux-x64-musl@1.9.4", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==" + ], + "@biomejs/cli-win32-arm64": [ + "@biomejs/cli-win32-arm64@1.9.4", + "", + { + "os": "win32", + "cpu": "arm64" + }, + "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==" + ], + "@biomejs/cli-win32-x64": [ + "@biomejs/cli-win32-x64@1.9.4", + "", + { + "os": "win32", + "cpu": "x64" + }, + "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==" + ], + "@bogeychan/elysia-etag": [ + "@bogeychan/elysia-etag@0.0.6", + "", + { + "peerDependencies": { + "elysia": ">= 1.0.22" + } + }, + "sha512-DPHRQJLm4mR5zkQk+DYhDEX1YFh0S5M3ZTqLy/SJ+kdT68c3wxynkyZZtL2YRNhZ0LKHuDXzqAXd77eTZeKxMg==" + ], + "@bogeychan/elysia-logger": [ + "@bogeychan/elysia-logger@0.1.8", + "", + { + "dependencies": { + "pino": "^9.6.0" + }, + "peerDependencies": { + "elysia": ">= 1.2.10" + } + }, + "sha512-TbCpMX+m68t0FbvpbBjMrCs4HQ9f1twkvTSGf6ShAkjash7zP9vGLGnEJ0iSG0ymgqLNN8Dgq0SdAEaMC6XLug==" + ], + "@cspotcode/source-map-support": [ + "@cspotcode/source-map-support@0.8.1", + "", + { + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==" + ], + "@datastructures-js/heap": [ + "@datastructures-js/heap@4.3.3", + "", + {}, + "sha512-UcUu/DLh/aM4W3C8zZfwxxm6/6FIZUlm3mcAXuNOCa6Aj4iizNvNXQyb8DjZQH2jKSQbMRyNlngP6TPimuGjpQ==" + ], + "@datastructures-js/priority-queue": [ + "@datastructures-js/priority-queue@6.3.3", + "", + { + "dependencies": { + "@datastructures-js/heap": "^4.3.3" + } + }, + "sha512-CIbUf0h4TPu1tHJ2gV4OTjwPkh7XZNb37u44bwoW6Py2vPdgaRUhFh3qFET6jvhyMNq/+ChWfOBRh+9s5WUtvA==" + ], + "@elysiajs/swagger": [ + "@elysiajs/swagger@1.3.1", + "", + { + "dependencies": { + "@scalar/themes": "^0.9.52", + "@scalar/types": "^0.0.12", + "openapi-types": "^12.1.3", + "pathe": "^1.1.2" + }, + "peerDependencies": { + "elysia": ">= 1.3.0" + } + }, + "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ==" + ], + "@jridgewell/resolve-uri": [ + "@jridgewell/resolve-uri@3.1.2", + "", + {}, + "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + ], + "@jridgewell/sourcemap-codec": [ + "@jridgewell/sourcemap-codec@1.5.5", + "", + {}, + "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + ], + "@jridgewell/trace-mapping": [ + "@jridgewell/trace-mapping@0.3.9", + "", + { + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==" + ], + "@mongodb-js/saslprep": [ + "@mongodb-js/saslprep@1.3.0", + "", + { + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==" + ], + "@noble/ed25519": [ + "@noble/ed25519@3.0.0", + "", + {}, + "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg==" + ], + "@rocket.chat/emitter": [ + "@rocket.chat/emitter@0.31.25", + "", + {}, + "sha512-hw5BpDlNwpYSb+K5X3DNMNUVEVXxmXugUPetGZGCWvntSVFsOjYuVEypoKW6vBBXSfqCBb0kN1npYcKEb4NFBw==" + ], + "@rocket.chat/federation-core": [ + "@rocket.chat/federation-core@workspace:packages/core" + ], + "@rocket.chat/federation-crypto": [ + "@rocket.chat/federation-crypto@workspace:packages/crypto" + ], + "@rocket.chat/federation-room": [ + "@rocket.chat/federation-room@workspace:packages/room" + ], + "@rocket.chat/federation-sdk": [ + "@rocket.chat/federation-sdk@workspace:packages/federation-sdk" + ], + "@rocket.chat/homeserver": [ + "@rocket.chat/homeserver@workspace:packages/homeserver" + ], + "@rollup/rollup-android-arm-eabi": [ + "@rollup/rollup-android-arm-eabi@4.52.4", + "", + { + "os": "android", + "cpu": "arm" + }, + "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==" + ], + "@rollup/rollup-android-arm64": [ + "@rollup/rollup-android-arm64@4.52.4", + "", + { + "os": "android", + "cpu": "arm64" + }, + "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==" + ], + "@rollup/rollup-darwin-arm64": [ + "@rollup/rollup-darwin-arm64@4.52.4", + "", + { + "os": "darwin", + "cpu": "arm64" + }, + "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==" + ], + "@rollup/rollup-darwin-x64": [ + "@rollup/rollup-darwin-x64@4.52.4", + "", + { + "os": "darwin", + "cpu": "x64" + }, + "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==" + ], + "@rollup/rollup-freebsd-arm64": [ + "@rollup/rollup-freebsd-arm64@4.52.4", + "", + { + "os": "freebsd", + "cpu": "arm64" + }, + "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==" + ], + "@rollup/rollup-freebsd-x64": [ + "@rollup/rollup-freebsd-x64@4.52.4", + "", + { + "os": "freebsd", + "cpu": "x64" + }, + "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==" + ], + "@rollup/rollup-linux-arm-gnueabihf": [ + "@rollup/rollup-linux-arm-gnueabihf@4.52.4", + "", + { + "os": "linux", + "cpu": "arm" + }, + "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==" + ], + "@rollup/rollup-linux-arm-musleabihf": [ + "@rollup/rollup-linux-arm-musleabihf@4.52.4", + "", + { + "os": "linux", + "cpu": "arm" + }, + "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==" + ], + "@rollup/rollup-linux-arm64-gnu": [ + "@rollup/rollup-linux-arm64-gnu@4.52.4", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==" + ], + "@rollup/rollup-linux-arm64-musl": [ + "@rollup/rollup-linux-arm64-musl@4.52.4", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==" + ], + "@rollup/rollup-linux-loong64-gnu": [ + "@rollup/rollup-linux-loong64-gnu@4.52.4", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==" + ], + "@rollup/rollup-linux-ppc64-gnu": [ + "@rollup/rollup-linux-ppc64-gnu@4.52.4", + "", + { + "os": "linux", + "cpu": "ppc64" + }, + "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==" + ], + "@rollup/rollup-linux-riscv64-gnu": [ + "@rollup/rollup-linux-riscv64-gnu@4.52.4", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==" + ], + "@rollup/rollup-linux-riscv64-musl": [ + "@rollup/rollup-linux-riscv64-musl@4.52.4", + "", + { + "os": "linux", + "cpu": "none" + }, + "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==" + ], + "@rollup/rollup-linux-s390x-gnu": [ + "@rollup/rollup-linux-s390x-gnu@4.52.4", + "", + { + "os": "linux", + "cpu": "s390x" + }, + "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==" + ], + "@rollup/rollup-linux-x64-gnu": [ + "@rollup/rollup-linux-x64-gnu@4.52.4", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==" + ], + "@rollup/rollup-linux-x64-musl": [ + "@rollup/rollup-linux-x64-musl@4.52.4", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==" + ], + "@rollup/rollup-openharmony-arm64": [ + "@rollup/rollup-openharmony-arm64@4.52.4", + "", + { + "os": "none", + "cpu": "arm64" + }, + "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==" + ], + "@rollup/rollup-win32-arm64-msvc": [ + "@rollup/rollup-win32-arm64-msvc@4.52.4", + "", + { + "os": "win32", + "cpu": "arm64" + }, + "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==" + ], + "@rollup/rollup-win32-ia32-msvc": [ + "@rollup/rollup-win32-ia32-msvc@4.52.4", + "", + { + "os": "win32", + "cpu": "ia32" + }, + "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==" + ], + "@rollup/rollup-win32-x64-gnu": [ + "@rollup/rollup-win32-x64-gnu@4.52.4", + "", + { + "os": "win32", + "cpu": "x64" + }, + "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==" + ], + "@rollup/rollup-win32-x64-msvc": [ + "@rollup/rollup-win32-x64-msvc@4.52.4", + "", + { + "os": "win32", + "cpu": "x64" + }, + "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==" + ], + "@scalar/openapi-types": [ + "@scalar/openapi-types@0.1.1", + "", + {}, + "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg==" + ], + "@scalar/themes": [ + "@scalar/themes@0.9.86", + "", + { + "dependencies": { + "@scalar/types": "0.1.7" + } + }, + "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row==" + ], + "@scalar/types": [ + "@scalar/types@0.0.12", + "", + { + "dependencies": { + "@scalar/openapi-types": "0.1.1", + "@unhead/schema": "^1.9.5" + } + }, + "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ==" + ], + "@sinclair/typebox": [ + "@sinclair/typebox@0.34.37", + "", + {}, + "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==" + ], + "@sinonjs/commons": [ + "@sinonjs/commons@3.0.1", + "", + { + "dependencies": { + "type-detect": "4.0.8" + } + }, + "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==" + ], + "@sinonjs/fake-timers": [ + "@sinonjs/fake-timers@13.0.5", + "", + { + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==" + ], + "@sinonjs/samsam": [ + "@sinonjs/samsam@8.0.2", + "", + { + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==" + ], + "@tokenizer/inflate": [ + "@tokenizer/inflate@0.2.7", + "", + { + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + } + }, + "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==" + ], + "@tokenizer/token": [ + "@tokenizer/token@0.3.0", + "", + {}, + "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + ], + "@tsconfig/node10": [ + "@tsconfig/node10@1.0.11", + "", + {}, + "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" + ], + "@tsconfig/node12": [ + "@tsconfig/node12@1.0.11", + "", + {}, + "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + ], + "@tsconfig/node14": [ + "@tsconfig/node14@1.0.3", + "", + {}, + "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + ], + "@tsconfig/node16": [ + "@tsconfig/node16@1.0.4", + "", + {}, + "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + ], + "@types/body-parser": [ + "@types/body-parser@1.19.6", + "", + { + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==" + ], + "@types/bun": [ + "@types/bun@1.2.18", + "", + { + "dependencies": { + "bun-types": "1.2.18" + } + }, + "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ==" + ], + "@types/connect": [ + "@types/connect@3.4.38", + "", + { + "dependencies": { + "@types/node": "*" + } + }, + "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==" + ], + "@types/estree": [ + "@types/estree@1.0.8", + "", + {}, + "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + ], + "@types/express": [ + "@types/express@5.0.3", + "", + { + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==" + ], + "@types/express-serve-static-core": [ + "@types/express-serve-static-core@5.0.6", + "", + { + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==" + ], + "@types/http-errors": [ + "@types/http-errors@2.0.5", + "", + {}, + "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==" + ], + "@types/mime": [ + "@types/mime@1.3.5", + "", + {}, + "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + ], + "@types/node": [ + "@types/node@22.16.0", + "", + { + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==" + ], + "@types/qs": [ + "@types/qs@6.14.0", + "", + {}, + "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==" + ], + "@types/range-parser": [ + "@types/range-parser@1.2.7", + "", + {}, + "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + ], + "@types/react": [ + "@types/react@19.1.8", + "", + { + "dependencies": { + "csstype": "^3.0.2" + } + }, + "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==" + ], + "@types/send": [ + "@types/send@0.17.5", + "", + { + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==" + ], + "@types/serve-static": [ + "@types/serve-static@1.15.8", + "", + { + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==" + ], + "@types/sinon": [ + "@types/sinon@17.0.4", + "", + { + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==" + ], + "@types/sinonjs__fake-timers": [ + "@types/sinonjs__fake-timers@8.1.5", + "", + {}, + "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==" + ], + "@types/webidl-conversions": [ + "@types/webidl-conversions@7.0.3", + "", + {}, + "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + ], + "@types/whatwg-url": [ + "@types/whatwg-url@11.0.5", + "", + { + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==" + ], + "@unhead/schema": [ + "@unhead/schema@1.11.20", + "", + { + "dependencies": { + "hookable": "^5.5.3", + "zhead": "^2.2.4" + } + }, + "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==" + ], + "acorn": [ + "acorn@8.15.0", + "", + { + "bin": { + "acorn": "bin/acorn" + } + }, + "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==" + ], + "acorn-walk": [ + "acorn-walk@8.3.4", + "", + { + "dependencies": { + "acorn": "^8.11.0" + } + }, + "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==" + ], + "ansi-escapes": [ + "ansi-escapes@7.0.0", + "", + { + "dependencies": { + "environment": "^1.0.0" + } + }, + "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==" + ], + "ansi-regex": [ + "ansi-regex@5.0.1", + "", + {}, + "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + ], + "ansi-styles": [ + "ansi-styles@4.3.0", + "", + { + "dependencies": { + "color-convert": "^2.0.1" + } + }, + "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==" + ], + "arg": [ + "arg@4.1.3", + "", + {}, + "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + ], + "atomic-sleep": [ + "atomic-sleep@1.0.0", + "", + {}, + "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" + ], + "braces": [ + "braces@3.0.3", + "", + { + "dependencies": { + "fill-range": "^7.1.1" + } + }, + "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==" + ], + "bson": [ + "bson@6.10.4", + "", + {}, + "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==" + ], + "bun-types": [ + "bun-types@1.2.22", + "", + { + "dependencies": { + "@types/node": "*" + }, + "peerDependencies": { + "@types/react": "^19" + } + }, + "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA==" + ], + "chalk": [ + "chalk@5.4.1", + "", + {}, + "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==" + ], + "cli-cursor": [ + "cli-cursor@5.0.0", + "", + { + "dependencies": { + "restore-cursor": "^5.0.0" + } + }, + "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==" + ], + "cli-truncate": [ + "cli-truncate@4.0.0", + "", + { + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + } + }, + "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==" + ], + "color-convert": [ + "color-convert@2.0.1", + "", + { + "dependencies": { + "color-name": "~1.1.4" + } + }, + "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==" + ], + "color-name": [ + "color-name@1.1.4", + "", + {}, + "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + ], + "colorette": [ + "colorette@2.0.20", + "", + {}, + "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + ], + "commander": [ + "commander@14.0.0", + "", + {}, + "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==" + ], + "cookie": [ + "cookie@1.0.2", + "", + {}, + "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==" + ], + "create-require": [ + "create-require@1.1.1", + "", + {}, + "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + ], + "csstype": [ + "csstype@3.1.3", + "", + {}, + "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + ], + "dateformat": [ + "dateformat@4.6.3", + "", + {}, + "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==" + ], + "debug": [ + "debug@4.4.1", + "", + { + "dependencies": { + "ms": "^2.1.3" + } + }, + "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==" + ], + "diff": [ + "diff@7.0.0", + "", + {}, + "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==" + ], + "dotenv": [ + "dotenv@16.6.1", + "", + {}, + "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==" + ], + "elysia": [ + "elysia@1.3.5", + "", + { + "dependencies": { + "cookie": "^1.0.2", + "exact-mirror": "0.1.2", + "fast-decode-uri-component": "^1.0.1" + }, + "optionalDependencies": { + "@sinclair/typebox": "^0.34.33", + "openapi-types": "^12.1.3" + }, + "peerDependencies": { + "file-type": ">= 20.0.0", + "typescript": ">= 5.0.0" + } + }, + "sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw==" + ], + "emoji-regex": [ + "emoji-regex@10.4.0", + "", + {}, + "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==" + ], + "end-of-stream": [ + "end-of-stream@1.4.5", + "", + { + "dependencies": { + "once": "^1.4.0" + } + }, + "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==" + ], + "environment": [ + "environment@1.1.0", + "", + {}, + "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==" + ], + "eventemitter3": [ + "eventemitter3@5.0.1", + "", + {}, + "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + ], + "exact-mirror": [ + "exact-mirror@0.1.2", + "", + { + "peerDependencies": { + "@sinclair/typebox": "^0.34.15" + }, + "optionalPeers": [ + "@sinclair/typebox" + ] + }, + "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw==" + ], + "fast-copy": [ + "fast-copy@3.0.2", + "", + {}, + "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==" + ], + "fast-decode-uri-component": [ + "fast-decode-uri-component@1.0.1", + "", + {}, + "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + ], + "fast-redact": [ + "fast-redact@3.5.0", + "", + {}, + "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==" + ], + "fast-safe-stringify": [ + "fast-safe-stringify@2.1.1", + "", + {}, + "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + ], + "fflate": [ + "fflate@0.8.2", + "", + {}, + "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + ], + "file-type": [ + "file-type@21.0.0", + "", + { + "dependencies": { + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + } + }, + "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==" + ], + "fill-range": [ + "fill-range@7.1.1", + "", + { + "dependencies": { + "to-regex-range": "^5.0.1" + } + }, + "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==" + ], + "fsevents": [ + "fsevents@2.3.3", + "", + { + "os": "darwin" + }, + "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" + ], + "function-bind": [ + "function-bind@1.1.2", + "", + {}, + "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + ], + "get-east-asian-width": [ + "get-east-asian-width@1.3.0", + "", + {}, + "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==" + ], + "global-prefix": [ + "global-prefix@4.0.0", + "", + { + "dependencies": { + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" + } + }, + "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==" + ], + "has-flag": [ + "has-flag@4.0.0", + "", + {}, + "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + ], + "hasown": [ + "hasown@2.0.2", + "", + { + "dependencies": { + "function-bind": "^1.1.2" + } + }, + "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==" + ], + "help-me": [ + "help-me@5.0.0", + "", + {}, + "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + ], + "hookable": [ + "hookable@5.5.3", + "", + {}, + "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" + ], + "husky": [ + "husky@9.1.7", + "", + { + "bin": { + "husky": "bin.js" + } + }, + "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==" + ], + "ieee754": [ + "ieee754@1.2.1", + "", + {}, + "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + ], + "ini": [ + "ini@4.1.3", + "", + {}, + "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==" + ], + "is-core-module": [ + "is-core-module@2.16.1", + "", + { + "dependencies": { + "hasown": "^2.0.2" + } + }, + "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==" + ], + "is-fullwidth-code-point": [ + "is-fullwidth-code-point@4.0.0", + "", + {}, + "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==" + ], + "is-number": [ + "is-number@7.0.0", + "", + {}, + "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + ], + "isexe": [ + "isexe@3.1.1", + "", + {}, + "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==" + ], + "joycon": [ + "joycon@3.1.1", + "", + {}, + "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==" + ], + "js-tokens": [ + "js-tokens@4.0.0", + "", + {}, + "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + ], + "json5": [ + "json5@2.2.3", + "", + { + "bin": { + "json5": "lib/cli.js" + } + }, + "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + ], + "kind-of": [ + "kind-of@6.0.3", + "", + {}, + "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + ], + "lilconfig": [ + "lilconfig@3.1.3", + "", + {}, + "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==" + ], + "lint-staged": [ + "lint-staged@16.1.2", + "", + { + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0", + "debug": "^4.4.1", + "lilconfig": "^3.1.3", + "listr2": "^8.3.3", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + } + }, + "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==" + ], + "listr2": [ + "listr2@8.3.3", + "", + { + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + } + }, + "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==" + ], + "lodash.get": [ + "lodash.get@4.4.2", + "", + {}, + "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + ], + "log-update": [ + "log-update@6.1.0", + "", + { + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + } + }, + "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==" + ], + "magic-string": [ + "magic-string@0.30.19", + "", + { + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==" + ], + "make-error": [ + "make-error@1.3.6", + "", + {}, + "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + ], + "memoize": [ + "memoize@10.1.0", + "", + { + "dependencies": { + "mimic-function": "^5.0.1" + } + }, + "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==" + ], + "memory-pager": [ + "memory-pager@1.5.0", + "", + {}, + "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + ], + "micromatch": [ + "micromatch@4.0.8", + "", + { + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==" + ], + "mimic-function": [ + "mimic-function@5.0.1", + "", + {}, + "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==" + ], + "minimist": [ + "minimist@1.2.8", + "", + {}, + "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + ], + "mongodb": [ + "mongodb@6.17.0", + "", + { + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "optionalPeers": [ + "@aws-sdk/credential-providers", + "@mongodb-js/zstd", + "gcp-metadata", + "kerberos", + "mongodb-client-encryption", + "snappy", + "socks" + ] + }, + "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==" + ], + "mongodb-connection-string-url": [ + "mongodb-connection-string-url@3.0.2", + "", + { + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==" + ], + "ms": [ + "ms@2.1.3", + "", + {}, + "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + ], + "nano-spawn": [ + "nano-spawn@1.0.2", + "", + {}, + "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==" + ], + "nanoid": [ + "nanoid@5.1.5", + "", + { + "bin": { + "nanoid": "bin/nanoid.js" + } + }, + "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==" + ], + "on-exit-leak-free": [ + "on-exit-leak-free@2.1.2", + "", + {}, + "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==" + ], + "once": [ + "once@1.4.0", + "", + { + "dependencies": { + "wrappy": "1" + } + }, + "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==" + ], + "onetime": [ + "onetime@7.0.0", + "", + { + "dependencies": { + "mimic-function": "^5.0.0" + } + }, + "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==" + ], + "openapi-types": [ + "openapi-types@12.1.3", + "", + {}, + "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" + ], + "path-parse": [ + "path-parse@1.0.7", + "", + {}, + "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + ], + "pathe": [ + "pathe@1.1.2", + "", + {}, + "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" + ], + "picocolors": [ + "picocolors@1.1.1", + "", + {}, + "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + ], + "picomatch": [ + "picomatch@2.3.1", + "", + {}, + "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + ], + "pidtree": [ + "pidtree@0.6.0", + "", + { + "bin": { + "pidtree": "bin/pidtree.js" + } + }, + "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==" + ], + "pino": [ + "pino@9.11.0", + "", + { + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "sha512-+YIodBB9sxcWeR8PrXC2K3gEDyfkUuVEITOcbqrfcj+z5QW4ioIcqZfYFbrLTYLsmAwunbS7nfU/dpBB6PZc1g==" + ], + "pino-abstract-transport": [ + "pino-abstract-transport@2.0.0", + "", + { + "dependencies": { + "split2": "^4.0.0" + } + }, + "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==" + ], + "pino-pretty": [ + "pino-pretty@13.1.1", + "", + { + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA==" + ], + "pino-std-serializers": [ + "pino-std-serializers@7.0.0", + "", + {}, + "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" + ], + "process-warning": [ + "process-warning@5.0.0", + "", + {}, + "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==" + ], + "pump": [ + "pump@3.0.3", + "", + { + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==" + ], + "punycode": [ + "punycode@2.3.1", + "", + {}, + "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + ], + "quick-format-unescaped": [ + "quick-format-unescaped@4.0.4", + "", + {}, + "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + ], + "real-require": [ + "real-require@0.2.0", + "", + {}, + "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + ], + "reflect-metadata": [ + "reflect-metadata@0.2.2", + "", + {}, + "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + ], + "resolve": [ + "resolve@1.22.10", + "", + { + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + } + }, + "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==" + ], + "restore-cursor": [ + "restore-cursor@5.1.0", + "", + { + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + } + }, + "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==" + ], + "rfdc": [ + "rfdc@1.4.1", + "", + {}, + "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + ], + "rollup": [ + "rollup@4.52.4", + "", + { + "dependencies": { + "@types/estree": "1.0.8" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + }, + "bin": { + "rollup": "dist/bin/rollup" + } + }, + "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==" + ], + "rollup-plugin-dts": [ + "rollup-plugin-dts@6.2.3", + "", + { + "dependencies": { + "magic-string": "^0.30.17" + }, + "optionalDependencies": { + "@babel/code-frame": "^7.27.1" + }, + "peerDependencies": { + "rollup": "^3.29.4 || ^4", + "typescript": "^4.5 || ^5.0" + } + }, + "sha512-UgnEsfciXSPpASuOelix7m4DrmyQgiaWBnvI0TM4GxuDh5FkqW8E5hu57bCxXB90VvR1WNfLV80yEDN18UogSA==" + ], + "safe-stable-stringify": [ + "safe-stable-stringify@2.5.0", + "", + {}, + "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" + ], + "secure-json-parse": [ + "secure-json-parse@4.0.0", + "", + {}, + "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==" + ], + "semver": [ + "semver@7.7.2", + "", + { + "bin": { + "semver": "bin/semver.js" + } + }, + "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==" + ], + "signal-exit": [ + "signal-exit@4.1.0", + "", + {}, + "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + ], + "sinon": [ + "sinon@20.0.0", + "", + { + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.5", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "supports-color": "^7.2.0" + } + }, + "sha512-+FXOAbdnj94AQIxH0w1v8gzNxkawVvNqE3jUzRLptR71Oykeu2RrQXXl/VQjKay+Qnh73fDt/oDfMo6xMeDQbQ==" + ], + "slice-ansi": [ + "slice-ansi@5.0.0", + "", + { + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + } + }, + "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==" + ], + "sonic-boom": [ + "sonic-boom@4.2.0", + "", + { + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==" + ], + "sparse-bitfield": [ + "sparse-bitfield@3.0.3", + "", + { + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==" + ], + "split2": [ + "split2@4.2.0", + "", + {}, + "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + ], + "string-argv": [ + "string-argv@0.3.2", + "", + {}, + "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==" + ], + "string-width": [ + "string-width@7.2.0", + "", + { + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, + "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==" + ], + "strip-ansi": [ + "strip-ansi@6.0.1", + "", + { + "dependencies": { + "ansi-regex": "^5.0.1" + } + }, + "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==" + ], + "strip-bom": [ + "strip-bom@3.0.0", + "", + {}, + "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" + ], + "strip-json-comments": [ + "strip-json-comments@5.0.3", + "", + {}, + "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==" + ], + "strtok3": [ + "strtok3@10.3.1", + "", + { + "dependencies": { + "@tokenizer/token": "^0.3.0" + } + }, + "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==" + ], + "supports-color": [ + "supports-color@7.2.0", + "", + { + "dependencies": { + "has-flag": "^4.0.0" + } + }, + "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" + ], + "supports-preserve-symlinks-flag": [ + "supports-preserve-symlinks-flag@1.0.0", + "", + {}, + "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + ], + "thread-stream": [ + "thread-stream@3.1.0", + "", + { + "dependencies": { + "real-require": "^0.2.0" + } + }, + "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==" + ], + "to-regex-range": [ + "to-regex-range@5.0.1", + "", + { + "dependencies": { + "is-number": "^7.0.0" + } + }, + "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==" + ], + "token-types": [ + "token-types@6.0.3", + "", + { + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + } + }, + "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==" + ], + "tr46": [ + "tr46@5.1.1", + "", + { + "dependencies": { + "punycode": "^2.3.1" + } + }, + "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==" + ], + "ts-node": [ + "ts-node@10.9.2", + "", + { + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "optionalPeers": [ + "@swc/core", + "@swc/wasm" + ], + "bin": { + "ts-node": "dist/bin.js", + "ts-script": "dist/bin-script-deprecated.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js" + } + }, + "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==" + ], + "ts-patch": [ + "ts-patch@3.3.0", + "", + { + "dependencies": { + "chalk": "^4.1.2", + "global-prefix": "^4.0.0", + "minimist": "^1.2.8", + "resolve": "^1.22.2", + "semver": "^7.6.3", + "strip-ansi": "^6.0.1" + }, + "bin": { + "ts-patch": "bin/ts-patch.js", + "tspc": "bin/tspc.js" + } + }, + "sha512-zAOzDnd5qsfEnjd9IGy1IRuvA7ygyyxxdxesbhMdutt8AHFjD8Vw8hU2rMF89HX1BKRWFYqKHrO8Q6lw0NeUZg==" + ], + "tsconfig-paths": [ + "tsconfig-paths@4.2.0", + "", + { + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==" + ], + "tslib": [ + "tslib@1.14.1", + "", + {}, + "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + ], + "tsyringe": [ + "tsyringe@4.10.0", + "", + { + "dependencies": { + "tslib": "^1.9.3" + } + }, + "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==" + ], + "turbo": [ + "turbo@2.5.6", + "", + { + "optionalDependencies": { + "turbo-darwin-64": "2.5.6", + "turbo-darwin-arm64": "2.5.6", + "turbo-linux-64": "2.5.6", + "turbo-linux-arm64": "2.5.6", + "turbo-windows-64": "2.5.6", + "turbo-windows-arm64": "2.5.6" + }, + "bin": { + "turbo": "bin/turbo" + } + }, + "sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==" + ], + "turbo-darwin-64": [ + "turbo-darwin-64@2.5.6", + "", + { + "os": "darwin", + "cpu": "x64" + }, + "sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==" + ], + "turbo-darwin-arm64": [ + "turbo-darwin-arm64@2.5.6", + "", + { + "os": "darwin", + "cpu": "arm64" + }, + "sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA==" + ], + "turbo-linux-64": [ + "turbo-linux-64@2.5.6", + "", + { + "os": "linux", + "cpu": "x64" + }, + "sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA==" + ], + "turbo-linux-arm64": [ + "turbo-linux-arm64@2.5.6", + "", + { + "os": "linux", + "cpu": "arm64" + }, + "sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ==" + ], + "turbo-windows-64": [ + "turbo-windows-64@2.5.6", + "", + { + "os": "win32", + "cpu": "x64" + }, + "sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg==" + ], + "turbo-windows-arm64": [ + "turbo-windows-arm64@2.5.6", + "", + { + "os": "win32", + "cpu": "arm64" + }, + "sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q==" + ], + "tweetnacl": [ + "tweetnacl@1.0.3", + "", + {}, + "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + ], + "type-detect": [ + "type-detect@4.0.8", + "", + {}, + "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + ], + "type-fest": [ + "type-fest@4.41.0", + "", + {}, + "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==" + ], + "typescript": [ + "typescript@5.9.2", + "", + { + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + } + }, + "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==" + ], + "uint8array-extras": [ + "uint8array-extras@1.4.0", + "", + {}, + "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==" + ], + "undici-types": [ + "undici-types@6.21.0", + "", + {}, + "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + ], + "v8-compile-cache-lib": [ + "v8-compile-cache-lib@3.0.1", + "", + {}, + "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + ], + "webidl-conversions": [ + "webidl-conversions@7.0.0", + "", + {}, + "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + ], + "whatwg-url": [ + "whatwg-url@14.2.0", + "", + { + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + } + }, + "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==" + ], + "which": [ + "which@4.0.0", + "", + { + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + } + }, + "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==" + ], + "wrap-ansi": [ + "wrap-ansi@9.0.0", + "", + { + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + } + }, + "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==" + ], + "wrappy": [ + "wrappy@1.0.2", + "", + {}, + "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + ], + "yaml": [ + "yaml@2.8.0", + "", + { + "bin": { + "yaml": "bin.mjs" + } + }, + "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==" + ], + "yn": [ + "yn@3.1.1", + "", + {}, + "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + ], + "zhead": [ + "zhead@2.2.4", + "", + {}, + "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==" + ], + "zod": [ + "zod@3.25.71", + "", + {}, + "sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q==" + ], + "@scalar/themes/@scalar/types": [ + "@scalar/types@0.1.7", + "", + { + "dependencies": { + "@scalar/openapi-types": "0.2.0", + "@unhead/schema": "^1.11.11", + "nanoid": "^5.1.5", + "type-fest": "^4.20.0", + "zod": "^3.23.8" + } + }, + "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw==" + ], + "@sinonjs/samsam/type-detect": [ + "type-detect@4.1.0", + "", + {}, + "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==" + ], + "@types/bun/bun-types": [ + "bun-types@1.2.18", + "", + { + "dependencies": { + "@types/node": "*" + }, + "peerDependencies": { + "@types/react": "^19" + } + }, + "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw==" + ], + "log-update/slice-ansi": [ + "slice-ansi@7.1.0", + "", + { + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + } + }, + "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==" + ], + "log-update/strip-ansi": [ + "strip-ansi@7.1.0", + "", + { + "dependencies": { + "ansi-regex": "^6.0.1" + } + }, + "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==" + ], + "slice-ansi/ansi-styles": [ + "ansi-styles@6.2.1", + "", + {}, + "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + ], + "string-width/strip-ansi": [ + "strip-ansi@7.1.0", + "", + { + "dependencies": { + "ansi-regex": "^6.0.1" + } + }, + "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==" + ], + "ts-node/diff": [ + "diff@4.0.2", + "", + {}, + "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + ], + "ts-patch/chalk": [ + "chalk@4.1.2", + "", + { + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==" + ], + "wrap-ansi/ansi-styles": [ + "ansi-styles@6.2.1", + "", + {}, + "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + ], + "wrap-ansi/strip-ansi": [ + "strip-ansi@7.1.0", + "", + { + "dependencies": { + "ansi-regex": "^6.0.1" + } + }, + "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==" + ], + "@scalar/themes/@scalar/types/@scalar/openapi-types": [ + "@scalar/openapi-types@0.2.0", + "", + { + "dependencies": { + "zod": "^3.23.8" + } + }, + "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA==" + ], + "log-update/slice-ansi/ansi-styles": [ + "ansi-styles@6.2.1", + "", + {}, + "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + ], + "log-update/slice-ansi/is-fullwidth-code-point": [ + "is-fullwidth-code-point@5.0.0", + "", + { + "dependencies": { + "get-east-asian-width": "^1.0.0" + } + }, + "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==" + ], + "log-update/strip-ansi/ansi-regex": [ + "ansi-regex@6.1.0", + "", + {}, + "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" + ], + "string-width/strip-ansi/ansi-regex": [ + "ansi-regex@6.1.0", + "", + {}, + "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" + ], + "wrap-ansi/strip-ansi/ansi-regex": [ + "ansi-regex@6.1.0", + "", + {}, + "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" + ], + } +} \ No newline at end of file From 644d4dc2484239d9f54b419641a509bf11d69808 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Sat, 30 Aug 2025 19:38:57 +0530 Subject: [PATCH 06/56] .... playing with it --- .../examples/signing-key.ssh-keygen.spec.ts | 15 ++ .../src/examples/signing-key.ssh-keygen.ts | 173 ++++++++++++++++++ packages/crypto/src/signing-key.ts | 32 ++++ packages/room/src/manager/event-wrapper.ts | 6 +- 4 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts create mode 100644 packages/crypto/src/examples/signing-key.ssh-keygen.ts create mode 100644 packages/crypto/src/signing-key.ts diff --git a/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts b/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts new file mode 100644 index 000000000..0a974774e --- /dev/null +++ b/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts @@ -0,0 +1,15 @@ +import { describe, it } from 'node:test'; +import { getSshKeygenSigningKey } from './signing-key.ssh-keygen'; + +describe('SshKeygenSigningKey', async () => { + const signingKey = await getSshKeygenSigningKey(); + + it('should sign and verify data', async () => { + const data = 'Hello, World!'; + const signature = await signingKey.sign(data); + const isValid = await signingKey.verify(data, signature); + if (!isValid) { + throw new Error('Signature verification failed'); + } + }); +}); diff --git a/packages/crypto/src/examples/signing-key.ssh-keygen.ts b/packages/crypto/src/examples/signing-key.ssh-keygen.ts new file mode 100644 index 000000000..12242a134 --- /dev/null +++ b/packages/crypto/src/examples/signing-key.ssh-keygen.ts @@ -0,0 +1,173 @@ +// this file is intended to be an example of how SigningKey can be implemented using different key storage/generation mechanisms + +import { exists, writeFile } from 'node:fs/promises'; +import { exec } from 'node:child_process'; +import crypto from 'node:crypto'; + +import { + EncryptionValidAlgorithm, + GenerateSigningKeyFunc, + SigningKey, +} from '../signing-key'; +import { readFile } from 'node:fs/promises'; + +export type SshKeygenSigningKeyConfig = { + // path to the private key file generated by ssh-keygen + privateKeyPath?: string; + // path to the public key file generated by ssh-keygen + publicKeyPath?: string; + + configDir: string; +}; + +async function loadKey(path: string): Promise { + const data = await readFile(path); + return data; +} + +function getSigningKeyFunc({ + privateKeyPath, + publicKeyPath, + configDir, +}: SshKeygenSigningKeyConfig): GenerateSigningKeyFunc { + return async (): Promise => { + if (!privateKeyPath || !publicKeyPath) { + // we generate the keys ourselves + // must be mac or linux for this + if (process.platform !== 'darwin' && process.platform !== 'linux') { + throw new Error( + 'ssh-keygen signing key without provided keys is only supported on macOS and Linux', + ); + } + + if (!configDir) { + throw new Error('configDir must be provided if keys are not provided'); + } + + // set some defaults + const privateKeyPath = `${configDir}/id_ed25519` as const; + const publicKeyPath = `${configDir}/id_ed25519.pub` as const; + + // maybe they exist in the configDir already + const privateKeyExists = await exists(privateKeyPath); + const publicKeyExists = await exists(publicKeyPath); + + // both must exist or not exist + if (privateKeyExists !== publicKeyExists) { + throw new Error( + 'Both private and public key files must exist or not exist', + ); + } + + type Key = { privateKey: Uint8Array; publicKey: Uint8Array }; + + // both exists, we load them + if (privateKeyExists) { + // TODO: handle loading this + const key: Key = await loadKey(privateKeyPath).then((privateKey) => + loadKey(publicKeyPath).then((publicKey) => ({ + privateKey, + publicKey, + })), + ); + + return new SshKeygenSigningKey(key.privateKey, key.publicKey); + } + // doesn't exist, generate + const key: Key = await new Promise((resolve, reject) => + exec( + `ssh-keygen -m PEM -N "" -t rsa -f ${privateKeyPath} -C "generaetd"`, + async (error, _stdout, stderr) => { + if (error) { + console.error(`Error generating ssh keys: ${error.message}`); + reject(error); + } + if (stderr) { + reject(`ssh-keygen stderr: ${stderr}`); + } + + await new Promise((resolve, reject) => + exec( + `ssh-keygen -f ${publicKeyPath} -e -m PEM`, + (error, stdout, stderr) => { + if (error) { + console.error( + `Error generating public key PEM: ${error.message}`, + ); + reject(error); + } + if (stderr) { + reject(`ssh-keygen stderr: ${stderr}`); + } + + writeFile(publicKeyPath, stdout).then(() => + // write the public key PEM to a file + resolve({}), + ); + }, + ), + ); + + // load the generated keys + loadKey(privateKeyPath).then((privateKey) => + loadKey(publicKeyPath).then((publicKey) => + resolve({ privateKey, publicKey }), + ), + ); + }, + ), + ); + + return new SshKeygenSigningKey(key.privateKey, key.publicKey); + } + + // both keys are provided, we load them + const key: { privateKey: Uint8Array; publicKey: Uint8Array } = + await loadKey(privateKeyPath).then((privateKey) => + loadKey(publicKeyPath).then((publicKey) => ({ + privateKey, + publicKey, + })), + ); + + return new SshKeygenSigningKey(key.privateKey, key.publicKey); + }; +} + +export const getSshKeygenSigningKey = getSigningKeyFunc({ configDir: '/tmp' }); + +export class SshKeygenSigningKey implements SigningKey { + public algorithm = EncryptionValidAlgorithm.ed25519; + public version = '0'; + public constructor( + private readonly privateKey: Uint8Array, + private readonly publicKey: Uint8Array, + ) {} + + async sign(data: string | Buffer): Promise { + const signature = await crypto.sign( + 'sha256', + Buffer.isBuffer(data) ? data : Buffer.from(data), + this.privateKey.toString(), + ); + return signature; + } + + async verify(data: string, signature: Buffer): Promise { + return new Promise((resolve, reject) => + crypto.verify( + 'sha256', + Buffer.from(data), + this.publicKey.toString(), + signature, + (error) => { + if (error) { + reject(error); + } + + resolve(true); + }, + ), + ); + } +} diff --git a/packages/crypto/src/signing-key.ts b/packages/crypto/src/signing-key.ts new file mode 100644 index 000000000..9f972e7e3 --- /dev/null +++ b/packages/crypto/src/signing-key.ts @@ -0,0 +1,32 @@ +/* + * From a seed to a SigningKey object. + * The main thing to understand, are, + * 1. the purpose is to get signature of a payload, generally a string or a Uint8Array - signing happens with the public key + * 2. verify an existing signature - need the private key for this + * ^ normal asynchronous cryptographic operations. + * What changes are how the keys are stored and generated. + * The easiest tool to use for that is ssh-keygen, for example `ssh-keygen -t ed25519 -C "your_email@example.com"` + * This will generate a private and a public key pair. + * An example of a SigningKey object that uses ssh-keygen is in + */ + +export enum EncryptionValidAlgorithm { + ed25519 = 'ed25519', + + rsa = 'rsa', // NOT TO BE USED IN PRODUCTION +} + +type Signature = Buffer; + +export type GenerateSigningKeyFunc = () => Promise; + +export interface SigningKey { + // algorithm used, currently only ed25519 is supported + algorithm: EncryptionValidAlgorithm; + // key version, can change if rotated for example, can be any arbitrary string + version: string; + + // main implementors + sign(data: string | Buffer): Promise; + verify(data: string | Buffer, signature: Signature): Promise; +} diff --git a/packages/room/src/manager/event-wrapper.ts b/packages/room/src/manager/event-wrapper.ts index 586d63128..8f201cfed 100644 --- a/packages/room/src/manager/event-wrapper.ts +++ b/packages/room/src/manager/event-wrapper.ts @@ -340,12 +340,8 @@ export abstract class PersistentEventBase< const { unsigned, signatures, ...toHash } = redactedEvent; // 2. The event is converted into Canonical JSON. - const canonicalJson = encodeCanonicalJson(toHash); // 3. A sha256 hash is calculated on the resulting JSON object. - const referenceHash = crypto - .createHash('sha256') - .update(canonicalJson) - .digest(); + const referenceHash = computeHashBuffer(toHash); return referenceHash; } From 6b81995adec733b7f011bad942c3e99bbac9ea66 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 3 Sep 2025 15:02:27 +0530 Subject: [PATCH 07/56] committing --- packages/crypto/src/constants.ts | 7 + packages/crypto/src/ed25519/index.ts | 82 ++++++++ packages/crypto/src/examples/ed25519.ts | 77 +++++++ packages/crypto/src/examples/edwithsodium.ts | 66 ++++++ .../examples/signing-key.ssh-keygen.spec.ts | 39 +++- .../src/examples/signing-key.ssh-keygen.ts | 193 +++++++++--------- packages/crypto/src/signing-key.ts | 30 +-- packages/crypto/src/tsconfig.json | 15 +- 8 files changed, 378 insertions(+), 131 deletions(-) create mode 100644 packages/crypto/src/constants.ts create mode 100644 packages/crypto/src/ed25519/index.ts create mode 100644 packages/crypto/src/examples/ed25519.ts create mode 100644 packages/crypto/src/examples/edwithsodium.ts diff --git a/packages/crypto/src/constants.ts b/packages/crypto/src/constants.ts new file mode 100644 index 000000000..2e2655762 --- /dev/null +++ b/packages/crypto/src/constants.ts @@ -0,0 +1,7 @@ +export enum EncryptionValidAlgorithm { + ed25519 = 'ed25519', +} + +export type DataType = string | Buffer | Uint8Array; + +export type SignatureType = Buffer | Uint8Array; diff --git a/packages/crypto/src/ed25519/index.ts b/packages/crypto/src/ed25519/index.ts new file mode 100644 index 000000000..6ad501522 --- /dev/null +++ b/packages/crypto/src/ed25519/index.ts @@ -0,0 +1,82 @@ +import { + algorithmIdentifierTlv, + bitStringTlv, + octetStringTlv, + privateKeyVersionTlv, + sequenceOrderedTlv, +} from '../der'; + +enum KeyType { + private = 'PRIVATE KEY', + public = 'PUBLIC KEY', +} + +export function toPem(base64: string, type: KeyType): string { + const lines = [`-----BEGIN ${type}-----`]; + for (let i = 0; i < base64.length; i += 64) { + lines.push(base64.substring(i, 64)); + } + lines.push(`-----END ${type}-----`); + return lines.join('\n'); +} + +/* + * OneAsymmetricKey ::= SEQUENCE { + version Version, + privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, + privateKey PrivateKey, + attributes [0] IMPLICIT Attributes OPTIONAL, + ..., + [[2: publicKey [1] IMPLICIT PublicKey OPTIONAL ]], + ... + } + + CTET + PrivateKey ::= OCTET { OCTET STRING } + * We don't use extensions +*/ +export function ed25519PrivateKeyRawToPem(rawKey: Uint8Array): string { + // version Version + const version = privateKeyVersionTlv; + // privateKeyAlgorithm PrivateKeyAlgorithmIdentifier + const algId = algorithmIdentifierTlv; + // privateKey PrivateKey -> OCTET STRING + + const privKeyOctet = octetStringTlv( + octetStringTlv(rawKey), + ); /* The ASN.1 type CurvePrivateKey is defined in + this document to hold the byte sequence. Thus, when encoding a + OneAsymmetricKey object, the private key is wrapped in a + CurvePrivateKey object and wrapped by the OCTET STRING of the + "privateKey" field. + */ + // OneAsymmetricKey -> SEQUENCE + const oneAsymmetricKey = sequenceOrderedTlv([version, algId, privKeyOctet]); + // :) + return toPem( + Buffer.from(oneAsymmetricKey).toString('base64'), + KeyType.private, + ); +} + +/* + * SubjectPublicKeyInfo ::= SEQUENCE { + algorithm AlgorithmIdentifier, + subjectPublicKey BIT STRING + } + + AlgorithmIdentifier ::= SEQUENCE { + algorithm OBJECT IDENTIFIER (1.3.101.112 for Ed25519), + parameters NULL + } +*/ +export function ed25519PublicKeyRawToPem(rawKey: Uint8Array): string { + // algorithm AlgorithmIdentifier + const algId = algorithmIdentifierTlv; + // subhjectPublicKey BIT STRING + const pubKeyBitString = bitStringTlv(rawKey); + // SubjectPublicKeyInfo -> SEQUENCE + const spki = sequenceOrderedTlv([algId, pubKeyBitString]); + + return toPem(Buffer.from(spki).toString('base64'), KeyType.public); +} diff --git a/packages/crypto/src/examples/ed25519.ts b/packages/crypto/src/examples/ed25519.ts new file mode 100644 index 000000000..3771fd373 --- /dev/null +++ b/packages/crypto/src/examples/ed25519.ts @@ -0,0 +1,77 @@ +import { + DataType, + EncryptionValidAlgorithm, + SignatureType, +} from '../constants'; +import { SigningKey } from '../signing-key'; + +import crypto from 'node:crypto'; + +import * as ed25519 from '@noble/ed25519'; +import { + ed25519PrivateKeyRawToPem, + ed25519PublicKeyRawToPem, +} from '../ed25519'; + +// using native crypto module to implement ed25519 signing key +export class Ed25519SigningKeyImpl implements SigningKey { + public readonly algorithm = EncryptionValidAlgorithm.ed25519; + + constructor(public readonly version: string) {} + + private privateKey?: string; + private publicKey?: string; + + // generate and store the keys + async load(seed: Uint8Array) { + const keypair = await ed25519.keygenAsync(seed); + this.privateKey = ed25519PrivateKeyRawToPem(keypair.secretKey); + this.publicKey = ed25519PublicKeyRawToPem(keypair.publicKey); + } + + async sign(data: DataType): Promise { + const dataLike = typeof data === 'string' ? Buffer.from(data) : data; + return new Promise((resolve, reject) => { + if (!this.privateKey) { + reject('Private key not loaded'); + return; + } + + crypto.sign( + null, + dataLike, + Buffer.from(this.privateKey), + (error, signature) => { + if (error) { + reject(error); + } else { + resolve(signature); + } + }, + ); + }); + } + + async verify(data: DataType, signature: SignatureType): Promise { + const dataLike = typeof data === 'string' ? Buffer.from(data) : data; + return new Promise((resolve, reject) => { + if (!this.publicKey) { + throw new Error('Public key not loaded'); + } + + crypto.verify( + null, + dataLike, + Buffer.from(this.publicKey), + signature, + (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }, + ); + }); + } +} diff --git a/packages/crypto/src/examples/edwithsodium.ts b/packages/crypto/src/examples/edwithsodium.ts new file mode 100644 index 000000000..31c9c9152 --- /dev/null +++ b/packages/crypto/src/examples/edwithsodium.ts @@ -0,0 +1,66 @@ +import { + EncryptionValidAlgorithm, + DataType, + SignatureType, +} from '../constants'; +import { SigningKey } from '../signing-key'; + +import { SodiumPlus } from 'sodium-plus'; + +const sodium = await SodiumPlus.auto(); + +// import {} from 'sodium-native'; + +const { + crypto_sign_seed_keypair, + + crypto_sign_secretkey, + crypto_sign_publickey, + crypto_sign_detached, + crypto_sign_verify_detached, +} = await sodium; + +export class SodiumEd25519Impl implements SigningKey { + algorithm: EncryptionValidAlgorithm = EncryptionValidAlgorithm.ed25519; + version = '0'; + + private secretKey?: any; + private publicKey?: any; + + constructor(_seed: string) {} + + async load() { + const seedBytes = Buffer.from(new Uint8Array(32)); + const key = await crypto_sign_seed_keypair(seedBytes); + + const secretKey = await crypto_sign_secretkey(key); + const publicKey = await crypto_sign_publickey(key); + + this.secretKey = secretKey; + this.publicKey = publicKey; + } + + async sign(data: DataType): Promise { + const message = + typeof data === 'string' ? Buffer.from(data) : Buffer.from(data); + const signature = await crypto_sign_detached(message, this.secretKey!); + + return Promise.resolve(signature); + } + + verify(data: DataType, signature: SignatureType): Promise { + const message = + typeof data === 'string' ? Buffer.from(data) : Buffer.from(data); + const isValid = crypto_sign_verify_detached( + message, + this.publicKey!, + Buffer.from(signature), + ); + + if (!isValid) { + return Promise.reject(new Error('Invalid signature')); + } + + return Promise.resolve(); + } +} diff --git a/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts b/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts index 0a974774e..74e16e4fb 100644 --- a/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts +++ b/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts @@ -1,15 +1,42 @@ import { describe, it } from 'node:test'; -import { getSshKeygenSigningKey } from './signing-key.ssh-keygen'; +// import { toUnpaddedBase64 } from '..'; +import { SshKeygenSigningKey } from './signing-key.ssh-keygen'; +import type { SigningKey } from '../signing-key'; +import { Ed25519SigningKeyImpl } from './ed25519'; + +async function getsigningkey(): Promise { + const signingKey = new SshKeygenSigningKey({ + configDir: '/tmp', + }); + + await signingKey.load(); + + return signingKey; +} + +async function getedsigningkey(): Promise { + const signingkey = new Ed25519SigningKeyImpl('0'); + await signingkey.load(new Uint8Array(32)); + return signingkey; +} describe('SshKeygenSigningKey', async () => { - const signingKey = await getSshKeygenSigningKey(); + const signingKey = await getsigningkey(); it('should sign and verify data', async () => { const data = 'Hello, World!'; const signature = await signingKey.sign(data); - const isValid = await signingKey.verify(data, signature); - if (!isValid) { - throw new Error('Signature verification failed'); - } + + // console.log(toUnpaddedBase64(signature)); + + await signingKey.verify(data, signature); + }); + + it('should work with crypto + ed keys', async () => { + const edsigningkey = await getedsigningkey(); + const data = 'Hello, World!'; + const signature = await edsigningkey.sign(data); + + await edsigningkey.verify(data, signature); }); }); diff --git a/packages/crypto/src/examples/signing-key.ssh-keygen.ts b/packages/crypto/src/examples/signing-key.ssh-keygen.ts index 12242a134..c49645dab 100644 --- a/packages/crypto/src/examples/signing-key.ssh-keygen.ts +++ b/packages/crypto/src/examples/signing-key.ssh-keygen.ts @@ -4,12 +4,13 @@ import { exists, writeFile } from 'node:fs/promises'; import { exec } from 'node:child_process'; import crypto from 'node:crypto'; -import { - EncryptionValidAlgorithm, - GenerateSigningKeyFunc, - SigningKey, -} from '../signing-key'; +import { type SigningKey } from '../signing-key'; import { readFile } from 'node:fs/promises'; +import { + type DataType, + type EncryptionValidAlgorithm, + type SignatureType, +} from '../constants'; export type SshKeygenSigningKeyConfig = { // path to the private key file generated by ssh-keygen @@ -25,12 +26,37 @@ async function loadKey(path: string): Promise { return data; } -function getSigningKeyFunc({ - privateKeyPath, - publicKeyPath, - configDir, -}: SshKeygenSigningKeyConfig): GenerateSigningKeyFunc { - return async (): Promise => { +type Key = { privateKey: Buffer; publicKey: Buffer }; + +async function loadKeys( + privateKeyPath: string, + publicKeyPath: string, +): Promise { + const sk = await loadKey(privateKeyPath); + const pk = await loadKey(publicKeyPath); + + return { privateKey: Buffer.from(sk), publicKey: Buffer.from(pk) }; +} + +export class SshKeygenSigningKey implements SigningKey { + public version = '0'; + public constructor(private readonly config: SshKeygenSigningKeyConfig) {} + + public algorithm = 'rsa' as EncryptionValidAlgorithm; + + private privateKey?: Buffer; + private publicKey?: Buffer; + + private setkey(key: Key) { + this.privateKey = key.privateKey; + this.publicKey = key.publicKey; + } + + public async load(): Promise { + const { privateKeyPath, publicKeyPath, configDir } = this.config; + + const algorithm = this.algorithm; + if (!privateKeyPath || !publicKeyPath) { // we generate the keys ourselves // must be mac or linux for this @@ -45,38 +71,39 @@ function getSigningKeyFunc({ } // set some defaults - const privateKeyPath = `${configDir}/id_ed25519` as const; - const publicKeyPath = `${configDir}/id_ed25519.pub` as const; + const sk = `${configDir}/id_${algorithm}` as const; + const pk = `${sk}.pub` as const; // maybe they exist in the configDir already - const privateKeyExists = await exists(privateKeyPath); - const publicKeyExists = await exists(publicKeyPath); + const skExists = await exists(sk); + const pkExists = await exists(pk); // both must exist or not exist - if (privateKeyExists !== publicKeyExists) { + if (skExists !== pkExists) { throw new Error( 'Both private and public key files must exist or not exist', ); } - type Key = { privateKey: Uint8Array; publicKey: Uint8Array }; - // both exists, we load them - if (privateKeyExists) { + if (skExists) { // TODO: handle loading this - const key: Key = await loadKey(privateKeyPath).then((privateKey) => - loadKey(publicKeyPath).then((publicKey) => ({ - privateKey, - publicKey, - })), - ); + const key = await loadKeys(sk, pk); - return new SshKeygenSigningKey(key.privateKey, key.publicKey); + this.setkey(key); + return; } // doesn't exist, generate const key: Key = await new Promise((resolve, reject) => exec( - `ssh-keygen -m PEM -N "" -t rsa -f ${privateKeyPath} -C "generaetd"`, + // biome-ignore lint/style/useTemplate: + 'ssh-keygen' + // using ssh-keygen + ' -m PEM' + // export to PEM format, which crypto library will need, technically not a limitatiion for the IDEA of a SigningKey + ' -N ""' + // no passphrase + ' -t ' + + algorithm + // == rsa, needed for PEM export easily + ' -f ' + + sk, // file to write the private key to async (error, _stdout, stderr) => { if (error) { console.error(`Error generating ssh keys: ${error.message}`); @@ -86,88 +113,70 @@ function getSigningKeyFunc({ reject(`ssh-keygen stderr: ${stderr}`); } + // need public key in PEM format too, so we convert it await new Promise((resolve, reject) => - exec( - `ssh-keygen -f ${publicKeyPath} -e -m PEM`, - (error, stdout, stderr) => { - if (error) { - console.error( - `Error generating public key PEM: ${error.message}`, - ); - reject(error); - } - if (stderr) { - reject(`ssh-keygen stderr: ${stderr}`); - } - - writeFile(publicKeyPath, stdout).then(() => - // write the public key PEM to a file - resolve({}), + exec(`ssh-keygen -f ${pk} -e -m PEM`, (error, stdout, stderr) => { + if (error) { + console.error( + `Error generating public key PEM: ${error.message}`, ); - }, - ), + reject(error); + } + if (stderr) { + reject(`ssh-keygen stderr: ${stderr}`); + } + + // ssh-keygen sends the pem to stdout, we overwrite existikng pk with it + writeFile(pk, stdout).then(() => + // write the public key PEM to a file + resolve({}), + ); + }), ); // load the generated keys - loadKey(privateKeyPath).then((privateKey) => - loadKey(publicKeyPath).then((publicKey) => - resolve({ privateKey, publicKey }), - ), - ); + const key = await loadKeys(sk, pk); + resolve(key); }, ), ); - return new SshKeygenSigningKey(key.privateKey, key.publicKey); + this.setkey(key); + return; } // both keys are provided, we load them - const key: { privateKey: Uint8Array; publicKey: Uint8Array } = - await loadKey(privateKeyPath).then((privateKey) => - loadKey(publicKeyPath).then((publicKey) => ({ - privateKey, - publicKey, - })), - ); + const key = await loadKeys(privateKeyPath, publicKeyPath); - return new SshKeygenSigningKey(key.privateKey, key.publicKey); - }; -} + this.setkey(key); + } -export const getSshKeygenSigningKey = getSigningKeyFunc({ configDir: '/tmp' }); + async sign(data: DataType): Promise { + if (!this.privateKey) { + throw new Error('Private key not loaded'); + } + + const dataLike = typeof data === 'string' ? Buffer.from(data) : data; + const signature = await crypto.sign('sha256', dataLike, this.privateKey); -export class SshKeygenSigningKey implements SigningKey { - public algorithm = EncryptionValidAlgorithm.ed25519; - public version = '0'; - public constructor( - private readonly privateKey: Uint8Array, - private readonly publicKey: Uint8Array, - ) {} - - async sign(data: string | Buffer): Promise { - const signature = await crypto.sign( - 'sha256', - Buffer.isBuffer(data) ? data : Buffer.from(data), - this.privateKey.toString(), - ); return signature; } - async verify(data: string, signature: Buffer): Promise { - return new Promise((resolve, reject) => - crypto.verify( - 'sha256', - Buffer.from(data), - this.publicKey.toString(), - signature, - (error) => { - if (error) { - reject(error); - } - - resolve(true); - }, - ), - ); + async verify(data: DataType, signature: SignatureType): Promise { + const dataLike = typeof data === 'string' ? Buffer.from(data) : data; + return new Promise((resolve, reject) => { + if (!this.publicKey) { + reject('Public key not loaded'); + return; + } + + crypto.verify('sha256', dataLike, this.publicKey, signature, (error) => { + if (error) { + reject(error); + } + + resolve(); + }); + }); } } diff --git a/packages/crypto/src/signing-key.ts b/packages/crypto/src/signing-key.ts index 9f972e7e3..5eb80e847 100644 --- a/packages/crypto/src/signing-key.ts +++ b/packages/crypto/src/signing-key.ts @@ -1,24 +1,8 @@ -/* - * From a seed to a SigningKey object. - * The main thing to understand, are, - * 1. the purpose is to get signature of a payload, generally a string or a Uint8Array - signing happens with the public key - * 2. verify an existing signature - need the private key for this - * ^ normal asynchronous cryptographic operations. - * What changes are how the keys are stored and generated. - * The easiest tool to use for that is ssh-keygen, for example `ssh-keygen -t ed25519 -C "your_email@example.com"` - * This will generate a private and a public key pair. - * An example of a SigningKey object that uses ssh-keygen is in - */ - -export enum EncryptionValidAlgorithm { - ed25519 = 'ed25519', - - rsa = 'rsa', // NOT TO BE USED IN PRODUCTION -} - -type Signature = Buffer; - -export type GenerateSigningKeyFunc = () => Promise; +import type { + DataType, + SignatureType, + EncryptionValidAlgorithm, +} from './constants'; export interface SigningKey { // algorithm used, currently only ed25519 is supported @@ -27,6 +11,6 @@ export interface SigningKey { version: string; // main implementors - sign(data: string | Buffer): Promise; - verify(data: string | Buffer, signature: Signature): Promise; + sign(data: DataType): Promise; + verify(data: DataType, signature: SignatureType): Promise; // throws if invalid } diff --git a/packages/crypto/src/tsconfig.json b/packages/crypto/src/tsconfig.json index 5f974e888..5894d9d8d 100644 --- a/packages/crypto/src/tsconfig.json +++ b/packages/crypto/src/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ "composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */, @@ -9,7 +8,6 @@ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ @@ -23,7 +21,6 @@ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ "module": "ES2022" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ @@ -39,12 +36,10 @@ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "resolveJsonModule": true, /* Enable importing .json files. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, // "declarationMap": true, /* Create sourcemaps for d.ts files. */ @@ -69,14 +64,12 @@ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ @@ -97,9 +90,11 @@ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ "allowUnusedLabels": false /* Disable error reporting for unused labels. */, "allowUnreachableCode": false /* Disable error reporting for unreachable code. */, - /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } -} + }, + "exclude": [ + "**/examples/**" + ] +} \ No newline at end of file From 3fa2276f3b5db3852683cc3fa0e8e949a9f0348a Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 3 Sep 2025 16:19:28 +0530 Subject: [PATCH 08/56] show old test working --- .../examples/signing-key.ssh-keygen.spec.ts | 72 ++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts b/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts index 74e16e4fb..a5f1f0492 100644 --- a/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts +++ b/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts @@ -1,8 +1,9 @@ -import { describe, it } from 'node:test'; +import { describe, it, expect } from 'bun:test'; // import { toUnpaddedBase64 } from '..'; import { SshKeygenSigningKey } from './signing-key.ssh-keygen'; import type { SigningKey } from '../signing-key'; import { Ed25519SigningKeyImpl } from './ed25519'; +import { encodeCanonicalJson, toBinaryData, toUnpaddedBase64 } from '..'; async function getsigningkey(): Promise { const signingKey = new SshKeygenSigningKey({ @@ -14,9 +15,13 @@ async function getsigningkey(): Promise { return signingKey; } -async function getedsigningkey(): Promise { +async function getedsigningkey(seed?: string): Promise { const signingkey = new Ed25519SigningKeyImpl('0'); - await signingkey.load(new Uint8Array(32)); + // convert seed from base64 to Uint8Array + const seedBytes = seed + ? Uint8Array.from(atob(seed), (c) => c.charCodeAt(0)) + : new Uint8Array(32); + await signingkey.load(seedBytes); return signingkey; } @@ -39,4 +44,65 @@ describe('SshKeygenSigningKey', async () => { await edsigningkey.verify(data, signature); }); + + const seed = 'YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U'; + + const key = await getedsigningkey(seed); + + it('should sign data correctly with seed', async () => { + const data = new TextEncoder().encode('test data'); + const signature = await key.sign(data); + + // console.log(toUnpaddedBase64(signature)); + + await key.verify(data, signature); + // authentication.spec.ts + + const event = Object.freeze({ + auth_events: [ + '$KMCKA2rA1vVCoN3ugpEnAja70o0jSksI-s2fqWy_1to', + '$DcuwuadjnOUTC-IZmPdWHfCyxEgzuYcDvAoNpIJHous', + '$tMNgmLPOG2gBqdDmNaT2iAjD54UQYaIzPpiGplxF5J4', + '$8KCjO1lBtHMCUAYwe8y4-FMTwXnzXUb6F2g_Y6jHr4c', + ], + prev_events: ['$KYvjqKYmahXxkpD7O_217w6P6g6DMrUixsFrJ_NI0nA'], + type: 'm.room.member', + room_id: '!EAuqyrnzwQoPNHvvmX:hs1', + sender: '@admin:hs2', + depth: 10, + + content: { + // avatar_url: null, + // displayname: "admin", + membership: 'join', + }, + + hashes: { + sha256: 'WUqhTZqxv+8GhGQv58qE/QFQ4Oua5BKqGFQGT35Dv10', + }, + origin: 'hs2', + origin_server_ts: 1733069433734, + + state_key: '@admin:hs2', + signatures: { + hs2: { + 'ed25519:a_XRhW': + 'DR+DBqFTm7IUa35pFeOczsNw4shglIXW+3Ze63wC3dqQ4okzaSRgLuAUkYnVyxM2sZkSvlbeSBS7G6DeeaDEAA', + }, + }, + unsigned: { + age: 1, + }, + }); + + const { signatures, unsigned, ...rest } = event; + + const json = encodeCanonicalJson(rest); + + const newSignature = await key.sign(toBinaryData(json)); + + expect(toUnpaddedBase64(newSignature)).toBe( + event.signatures.hs2['ed25519:a_XRhW'], + ); + }); }); From 9a9e87d2af1dd705acc39529c0dd9f5c4dd71567 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Thu, 4 Sep 2025 12:38:25 +0530 Subject: [PATCH 09/56] .. --- benchmark-scripts/.gitignore | 34 ++++ benchmark-scripts/README.md | 15 ++ benchmark-scripts/bun.lock | 187 ++++++++++++++++++ benchmark-scripts/package.json | 17 ++ .../signing-and-verifications/script.js | 28 +++ .../signing-and-verifications/server.ts | 88 +++++++++ benchmark-scripts/tsconfig.json | 27 +++ packages/crypto/package.json | 13 +- .../signing-key.ssh-keygen.ts | 4 +- packages/crypto/src/ed25519/index.ts | 82 -------- packages/crypto/src/examples/ed25519.ts | 77 -------- packages/crypto/src/examples/edwithsodium.ts | 66 ------- .../examples/signing-key.ssh-keygen.spec.ts | 108 ---------- packages/crypto/src/signing-key.ts | 16 -- packages/crypto/tsconfig.json | 16 +- 15 files changed, 421 insertions(+), 357 deletions(-) create mode 100644 benchmark-scripts/.gitignore create mode 100644 benchmark-scripts/README.md create mode 100644 benchmark-scripts/bun.lock create mode 100644 benchmark-scripts/package.json create mode 100644 benchmark-scripts/signing-and-verifications/script.js create mode 100644 benchmark-scripts/signing-and-verifications/server.ts create mode 100644 benchmark-scripts/tsconfig.json rename packages/crypto/src/{examples => __examples}/signing-key.ssh-keygen.ts (97%) delete mode 100644 packages/crypto/src/ed25519/index.ts delete mode 100644 packages/crypto/src/examples/ed25519.ts delete mode 100644 packages/crypto/src/examples/edwithsodium.ts delete mode 100644 packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts delete mode 100644 packages/crypto/src/signing-key.ts diff --git a/benchmark-scripts/.gitignore b/benchmark-scripts/.gitignore new file mode 100644 index 000000000..a14702c40 --- /dev/null +++ b/benchmark-scripts/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/benchmark-scripts/README.md b/benchmark-scripts/README.md new file mode 100644 index 000000000..c963c26ce --- /dev/null +++ b/benchmark-scripts/README.md @@ -0,0 +1,15 @@ +# benchmark-scripts + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.15. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/benchmark-scripts/bun.lock b/benchmark-scripts/bun.lock new file mode 100644 index 000000000..74a25e2c6 --- /dev/null +++ b/benchmark-scripts/bun.lock @@ -0,0 +1,187 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "benchmark-scripts", + "devDependencies": { + "@types/bun": "latest", + "@types/express": "^5.0.3", + "express": "^5.1.0", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], + + "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], + + "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + + "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + + "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + + "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + } +} diff --git a/benchmark-scripts/package.json b/benchmark-scripts/package.json new file mode 100644 index 000000000..3961ac9c6 --- /dev/null +++ b/benchmark-scripts/package.json @@ -0,0 +1,17 @@ +{ + "name": "benchmark-scripts", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest", + "@types/express": "^5.0.3", + "express": "^5.1.0" + }, + "peerDependencies": { + "typescript": "^5" + }, + "scripts": { + "crypto": "bun run ./signing-and-verifications/server.ts" + } +} \ No newline at end of file diff --git a/benchmark-scripts/signing-and-verifications/script.js b/benchmark-scripts/signing-and-verifications/script.js new file mode 100644 index 000000000..348ecfee2 --- /dev/null +++ b/benchmark-scripts/signing-and-verifications/script.js @@ -0,0 +1,28 @@ +import http from "k6/http"; + +function _payload() { + const messagelength = __ENV.MESSAGE_LENGTH || "1024"; + const engine = __ENV.ENGINE || "tweetnacl"; + + const stream = !!__ENV.STREAM; + + return JSON.stringify({ + message: messagelength, + api: { + engine, + stream, + }, + }); +} + +const payload = _payload(); + +console.log("payload:", payload); + +export default function () { + http.post(`http://localhost:${__ENV.PORT || 8080}/signAndVerify`, payload, { + headers: { + "content-type": "application/json", + }, + }); +} diff --git a/benchmark-scripts/signing-and-verifications/server.ts b/benchmark-scripts/signing-and-verifications/server.ts new file mode 100644 index 000000000..8ca35b59a --- /dev/null +++ b/benchmark-scripts/signing-and-verifications/server.ts @@ -0,0 +1,88 @@ +import nacl from 'tweetnacl'; +import express, { type Request, type Response } from 'express'; + +import { loadEd25519SignerFromSeed } from '../../packages/crypto/src/utils/keys'; +import { Readable } from 'node:stream'; +import { toUnpaddedBase64 } from '../../packages/crypto/src'; + +const app = express(); + +app.use(express.json()); + +type SignAndVerifyRequest = { + message: string; // allow big payloads + api: { + engine: 'native' | 'tweetnacl'; // sodium + stream: boolean; // whether to use streaming API or not + }; +}; + +const seedBytes = new Uint8Array(32).fill(1); // for testing, should be random in real world +const tweetKeyPair = nacl.sign.keyPair.fromSeed(seedBytes); + +function handleTweetnaclSignAndVerify(message: Uint8Array) { + const signature = nacl.sign.detached(message, tweetKeyPair.secretKey); + + nacl.sign.detached.verify(message, signature, tweetKeyPair.publicKey); +} + +const nativeSigner = await loadEd25519SignerFromSeed( + Buffer.from(seedBytes).toString('base64'), +); + +async function handleNativeSignAndVerify(message: Uint8Array, stream: boolean) { + if (!stream) { + const signature = await nativeSigner.sign(message); + await nativeSigner.verify(message, signature); + return; + } +} + +// @ts-ignore +app.post('/signAndVerify', async (req, res) => { + const { message, api } = req.body as SignAndVerifyRequest; + if (typeof message !== 'string') { + return res.status(400).json({ error: 'Message must be a string' }); + } + + if (!message) { + return res.status(400).json({ error: 'Message cannot be empty' }); + } + + const { engine, stream = false } = api; + + // encode should mimick copy of memory that we'll experience in real world, json -> string + const encodedMessage = new TextEncoder().encode(message); + + if (engine === 'tweetnacl') { + if (stream) { + return res + .status(400) + .json({ error: 'Stream not supported for tweetnacl' }); + } + + try { + handleTweetnaclSignAndVerify(encodedMessage); + return res.json({ success: true }); + } catch (e) { + console.error('Tweetnacl sign/verify error', e); + return res.status(500).json({ error: 'Signing or verification failed' }); + } + } + + if (engine === 'native') { + try { + await handleNativeSignAndVerify(encodedMessage, stream); + return res.json({ success: true }); + } catch (e) { + console.error('Native sign/verify error', e); + return res.status(500).json({ error: 'Signing or verification failed' }); + } + } + + return res.status(400).json({ error: 'Invalid engine' }); +}); + +const port = Number.parseInt(process.env.PORT || '', 10) || 8080; + +app.listen(port, '127.0.0.1', () => console.log(`Listening on ${port}`)); diff --git a/benchmark-scripts/tsconfig.json b/benchmark-scripts/tsconfig.json new file mode 100644 index 000000000..de12b6235 --- /dev/null +++ b/benchmark-scripts/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": [ + "ESNext" + ], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "allowJs": true, + // Bundler mode + "moduleResolution": "node", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} \ No newline at end of file diff --git a/packages/crypto/package.json b/packages/crypto/package.json index db97ed31d..88dcc7694 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -19,7 +19,14 @@ "@noble/ed25519": "^3.0.0" }, "devDependencies": { - "bun-types": "latest" + "@types/libsodium-wrappers": "^0.7.14", + "@types/sodium-native": "^2.3.9", + "bun-types": "latest", + "express": "^5.1.0", + "k6": "^0.0.0" }, - "files": ["src", "dist"] -} + "files": [ + "src", + "dist" + ] +} \ No newline at end of file diff --git a/packages/crypto/src/examples/signing-key.ssh-keygen.ts b/packages/crypto/src/__examples/signing-key.ssh-keygen.ts similarity index 97% rename from packages/crypto/src/examples/signing-key.ssh-keygen.ts rename to packages/crypto/src/__examples/signing-key.ssh-keygen.ts index c49645dab..d6e0c9aed 100644 --- a/packages/crypto/src/examples/signing-key.ssh-keygen.ts +++ b/packages/crypto/src/__examples/signing-key.ssh-keygen.ts @@ -4,7 +4,7 @@ import { exists, writeFile } from 'node:fs/promises'; import { exec } from 'node:child_process'; import crypto from 'node:crypto'; -import { type SigningKey } from '../signing-key'; +import { type Signer } from '../contracts/key'; import { readFile } from 'node:fs/promises'; import { type DataType, @@ -38,7 +38,7 @@ async function loadKeys( return { privateKey: Buffer.from(sk), publicKey: Buffer.from(pk) }; } -export class SshKeygenSigningKey implements SigningKey { +export class SshKeygenSigningKey implements Signer { public version = '0'; public constructor(private readonly config: SshKeygenSigningKeyConfig) {} diff --git a/packages/crypto/src/ed25519/index.ts b/packages/crypto/src/ed25519/index.ts deleted file mode 100644 index 6ad501522..000000000 --- a/packages/crypto/src/ed25519/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - algorithmIdentifierTlv, - bitStringTlv, - octetStringTlv, - privateKeyVersionTlv, - sequenceOrderedTlv, -} from '../der'; - -enum KeyType { - private = 'PRIVATE KEY', - public = 'PUBLIC KEY', -} - -export function toPem(base64: string, type: KeyType): string { - const lines = [`-----BEGIN ${type}-----`]; - for (let i = 0; i < base64.length; i += 64) { - lines.push(base64.substring(i, 64)); - } - lines.push(`-----END ${type}-----`); - return lines.join('\n'); -} - -/* - * OneAsymmetricKey ::= SEQUENCE { - version Version, - privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, - privateKey PrivateKey, - attributes [0] IMPLICIT Attributes OPTIONAL, - ..., - [[2: publicKey [1] IMPLICIT PublicKey OPTIONAL ]], - ... - } - - CTET - PrivateKey ::= OCTET { OCTET STRING } - * We don't use extensions -*/ -export function ed25519PrivateKeyRawToPem(rawKey: Uint8Array): string { - // version Version - const version = privateKeyVersionTlv; - // privateKeyAlgorithm PrivateKeyAlgorithmIdentifier - const algId = algorithmIdentifierTlv; - // privateKey PrivateKey -> OCTET STRING - - const privKeyOctet = octetStringTlv( - octetStringTlv(rawKey), - ); /* The ASN.1 type CurvePrivateKey is defined in - this document to hold the byte sequence. Thus, when encoding a - OneAsymmetricKey object, the private key is wrapped in a - CurvePrivateKey object and wrapped by the OCTET STRING of the - "privateKey" field. - */ - // OneAsymmetricKey -> SEQUENCE - const oneAsymmetricKey = sequenceOrderedTlv([version, algId, privKeyOctet]); - // :) - return toPem( - Buffer.from(oneAsymmetricKey).toString('base64'), - KeyType.private, - ); -} - -/* - * SubjectPublicKeyInfo ::= SEQUENCE { - algorithm AlgorithmIdentifier, - subjectPublicKey BIT STRING - } - - AlgorithmIdentifier ::= SEQUENCE { - algorithm OBJECT IDENTIFIER (1.3.101.112 for Ed25519), - parameters NULL - } -*/ -export function ed25519PublicKeyRawToPem(rawKey: Uint8Array): string { - // algorithm AlgorithmIdentifier - const algId = algorithmIdentifierTlv; - // subhjectPublicKey BIT STRING - const pubKeyBitString = bitStringTlv(rawKey); - // SubjectPublicKeyInfo -> SEQUENCE - const spki = sequenceOrderedTlv([algId, pubKeyBitString]); - - return toPem(Buffer.from(spki).toString('base64'), KeyType.public); -} diff --git a/packages/crypto/src/examples/ed25519.ts b/packages/crypto/src/examples/ed25519.ts deleted file mode 100644 index 3771fd373..000000000 --- a/packages/crypto/src/examples/ed25519.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - DataType, - EncryptionValidAlgorithm, - SignatureType, -} from '../constants'; -import { SigningKey } from '../signing-key'; - -import crypto from 'node:crypto'; - -import * as ed25519 from '@noble/ed25519'; -import { - ed25519PrivateKeyRawToPem, - ed25519PublicKeyRawToPem, -} from '../ed25519'; - -// using native crypto module to implement ed25519 signing key -export class Ed25519SigningKeyImpl implements SigningKey { - public readonly algorithm = EncryptionValidAlgorithm.ed25519; - - constructor(public readonly version: string) {} - - private privateKey?: string; - private publicKey?: string; - - // generate and store the keys - async load(seed: Uint8Array) { - const keypair = await ed25519.keygenAsync(seed); - this.privateKey = ed25519PrivateKeyRawToPem(keypair.secretKey); - this.publicKey = ed25519PublicKeyRawToPem(keypair.publicKey); - } - - async sign(data: DataType): Promise { - const dataLike = typeof data === 'string' ? Buffer.from(data) : data; - return new Promise((resolve, reject) => { - if (!this.privateKey) { - reject('Private key not loaded'); - return; - } - - crypto.sign( - null, - dataLike, - Buffer.from(this.privateKey), - (error, signature) => { - if (error) { - reject(error); - } else { - resolve(signature); - } - }, - ); - }); - } - - async verify(data: DataType, signature: SignatureType): Promise { - const dataLike = typeof data === 'string' ? Buffer.from(data) : data; - return new Promise((resolve, reject) => { - if (!this.publicKey) { - throw new Error('Public key not loaded'); - } - - crypto.verify( - null, - dataLike, - Buffer.from(this.publicKey), - signature, - (error) => { - if (error) { - reject(error); - } else { - resolve(); - } - }, - ); - }); - } -} diff --git a/packages/crypto/src/examples/edwithsodium.ts b/packages/crypto/src/examples/edwithsodium.ts deleted file mode 100644 index 31c9c9152..000000000 --- a/packages/crypto/src/examples/edwithsodium.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - EncryptionValidAlgorithm, - DataType, - SignatureType, -} from '../constants'; -import { SigningKey } from '../signing-key'; - -import { SodiumPlus } from 'sodium-plus'; - -const sodium = await SodiumPlus.auto(); - -// import {} from 'sodium-native'; - -const { - crypto_sign_seed_keypair, - - crypto_sign_secretkey, - crypto_sign_publickey, - crypto_sign_detached, - crypto_sign_verify_detached, -} = await sodium; - -export class SodiumEd25519Impl implements SigningKey { - algorithm: EncryptionValidAlgorithm = EncryptionValidAlgorithm.ed25519; - version = '0'; - - private secretKey?: any; - private publicKey?: any; - - constructor(_seed: string) {} - - async load() { - const seedBytes = Buffer.from(new Uint8Array(32)); - const key = await crypto_sign_seed_keypair(seedBytes); - - const secretKey = await crypto_sign_secretkey(key); - const publicKey = await crypto_sign_publickey(key); - - this.secretKey = secretKey; - this.publicKey = publicKey; - } - - async sign(data: DataType): Promise { - const message = - typeof data === 'string' ? Buffer.from(data) : Buffer.from(data); - const signature = await crypto_sign_detached(message, this.secretKey!); - - return Promise.resolve(signature); - } - - verify(data: DataType, signature: SignatureType): Promise { - const message = - typeof data === 'string' ? Buffer.from(data) : Buffer.from(data); - const isValid = crypto_sign_verify_detached( - message, - this.publicKey!, - Buffer.from(signature), - ); - - if (!isValid) { - return Promise.reject(new Error('Invalid signature')); - } - - return Promise.resolve(); - } -} diff --git a/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts b/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts deleted file mode 100644 index a5f1f0492..000000000 --- a/packages/crypto/src/examples/signing-key.ssh-keygen.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { describe, it, expect } from 'bun:test'; -// import { toUnpaddedBase64 } from '..'; -import { SshKeygenSigningKey } from './signing-key.ssh-keygen'; -import type { SigningKey } from '../signing-key'; -import { Ed25519SigningKeyImpl } from './ed25519'; -import { encodeCanonicalJson, toBinaryData, toUnpaddedBase64 } from '..'; - -async function getsigningkey(): Promise { - const signingKey = new SshKeygenSigningKey({ - configDir: '/tmp', - }); - - await signingKey.load(); - - return signingKey; -} - -async function getedsigningkey(seed?: string): Promise { - const signingkey = new Ed25519SigningKeyImpl('0'); - // convert seed from base64 to Uint8Array - const seedBytes = seed - ? Uint8Array.from(atob(seed), (c) => c.charCodeAt(0)) - : new Uint8Array(32); - await signingkey.load(seedBytes); - return signingkey; -} - -describe('SshKeygenSigningKey', async () => { - const signingKey = await getsigningkey(); - - it('should sign and verify data', async () => { - const data = 'Hello, World!'; - const signature = await signingKey.sign(data); - - // console.log(toUnpaddedBase64(signature)); - - await signingKey.verify(data, signature); - }); - - it('should work with crypto + ed keys', async () => { - const edsigningkey = await getedsigningkey(); - const data = 'Hello, World!'; - const signature = await edsigningkey.sign(data); - - await edsigningkey.verify(data, signature); - }); - - const seed = 'YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U'; - - const key = await getedsigningkey(seed); - - it('should sign data correctly with seed', async () => { - const data = new TextEncoder().encode('test data'); - const signature = await key.sign(data); - - // console.log(toUnpaddedBase64(signature)); - - await key.verify(data, signature); - // authentication.spec.ts - - const event = Object.freeze({ - auth_events: [ - '$KMCKA2rA1vVCoN3ugpEnAja70o0jSksI-s2fqWy_1to', - '$DcuwuadjnOUTC-IZmPdWHfCyxEgzuYcDvAoNpIJHous', - '$tMNgmLPOG2gBqdDmNaT2iAjD54UQYaIzPpiGplxF5J4', - '$8KCjO1lBtHMCUAYwe8y4-FMTwXnzXUb6F2g_Y6jHr4c', - ], - prev_events: ['$KYvjqKYmahXxkpD7O_217w6P6g6DMrUixsFrJ_NI0nA'], - type: 'm.room.member', - room_id: '!EAuqyrnzwQoPNHvvmX:hs1', - sender: '@admin:hs2', - depth: 10, - - content: { - // avatar_url: null, - // displayname: "admin", - membership: 'join', - }, - - hashes: { - sha256: 'WUqhTZqxv+8GhGQv58qE/QFQ4Oua5BKqGFQGT35Dv10', - }, - origin: 'hs2', - origin_server_ts: 1733069433734, - - state_key: '@admin:hs2', - signatures: { - hs2: { - 'ed25519:a_XRhW': - 'DR+DBqFTm7IUa35pFeOczsNw4shglIXW+3Ze63wC3dqQ4okzaSRgLuAUkYnVyxM2sZkSvlbeSBS7G6DeeaDEAA', - }, - }, - unsigned: { - age: 1, - }, - }); - - const { signatures, unsigned, ...rest } = event; - - const json = encodeCanonicalJson(rest); - - const newSignature = await key.sign(toBinaryData(json)); - - expect(toUnpaddedBase64(newSignature)).toBe( - event.signatures.hs2['ed25519:a_XRhW'], - ); - }); -}); diff --git a/packages/crypto/src/signing-key.ts b/packages/crypto/src/signing-key.ts deleted file mode 100644 index 5eb80e847..000000000 --- a/packages/crypto/src/signing-key.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { - DataType, - SignatureType, - EncryptionValidAlgorithm, -} from './constants'; - -export interface SigningKey { - // algorithm used, currently only ed25519 is supported - algorithm: EncryptionValidAlgorithm; - // key version, can change if rotated for example, can be any arbitrary string - version: string; - - // main implementors - sign(data: DataType): Promise; - verify(data: DataType, signature: SignatureType): Promise; // throws if invalid -} diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json index 91ec6e497..ec444c1d0 100644 --- a/packages/crypto/tsconfig.json +++ b/packages/crypto/tsconfig.json @@ -8,6 +8,16 @@ "verbatimModuleSyntax": false, "tsBuildInfoFile": "./.tsbuildinfo" }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] -} + "include": [ + "src/**/*", + "../../benchmark-scripts/signing-and-verifications/server.ts", + "../../benchmark-scripts/signing-and-verifications/script.js", + "src/contracts/key.ts" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.spec.ts", + "**/*.test.ts" + ] +} \ No newline at end of file From e682d4bda631e6282af64b872e0c854f867940c7 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Thu, 4 Sep 2025 21:27:55 +0530 Subject: [PATCH 10/56] committing all --- .../signing-and-verifications/server.ts | 20 +- benchmark-scripts/tsconfig.json | 2 + bun.lock | 414 +++--------------- package.json | 10 +- packages/crypto/package.json | 6 +- .../src/__examples/signing-key.ssh-keygen.ts | 2 +- packages/crypto/src/constants.ts | 7 - packages/crypto/src/tsconfig.json | 2 +- packages/crypto/tsconfig.json | 6 +- 9 files changed, 77 insertions(+), 392 deletions(-) delete mode 100644 packages/crypto/src/constants.ts diff --git a/benchmark-scripts/signing-and-verifications/server.ts b/benchmark-scripts/signing-and-verifications/server.ts index 8ca35b59a..ccba03031 100644 --- a/benchmark-scripts/signing-and-verifications/server.ts +++ b/benchmark-scripts/signing-and-verifications/server.ts @@ -3,14 +3,18 @@ import express, { type Request, type Response } from 'express'; import { loadEd25519SignerFromSeed } from '../../packages/crypto/src/utils/keys'; import { Readable } from 'node:stream'; -import { toUnpaddedBase64 } from '../../packages/crypto/src'; +import { + encodeCanonicalJson, + encodeCanonicalJsonAsync, + toUnpaddedBase64, +} from '../../packages/crypto/src'; const app = express(); app.use(express.json()); type SignAndVerifyRequest = { - message: string; // allow big payloads + message: string | object; // allow big payloads api: { engine: 'native' | 'tweetnacl'; // sodium stream: boolean; // whether to use streaming API or not @@ -26,9 +30,7 @@ function handleTweetnaclSignAndVerify(message: Uint8Array) { nacl.sign.detached.verify(message, signature, tweetKeyPair.publicKey); } -const nativeSigner = await loadEd25519SignerFromSeed( - Buffer.from(seedBytes).toString('base64'), -); +const nativeSigner = await loadEd25519SignerFromSeed(seedBytes); async function handleNativeSignAndVerify(message: Uint8Array, stream: boolean) { if (!stream) { @@ -41,10 +43,6 @@ async function handleNativeSignAndVerify(message: Uint8Array, stream: boolean) { // @ts-ignore app.post('/signAndVerify', async (req, res) => { const { message, api } = req.body as SignAndVerifyRequest; - if (typeof message !== 'string') { - return res.status(400).json({ error: 'Message must be a string' }); - } - if (!message) { return res.status(400).json({ error: 'Message cannot be empty' }); } @@ -52,7 +50,9 @@ app.post('/signAndVerify', async (req, res) => { const { engine, stream = false } = api; // encode should mimick copy of memory that we'll experience in real world, json -> string - const encodedMessage = new TextEncoder().encode(message); + const encodedMessage = new TextEncoder().encode( + typeof message === 'string' ? message : encodeCanonicalJson(message), + ); if (engine === 'tweetnacl') { if (stream) { diff --git a/benchmark-scripts/tsconfig.json b/benchmark-scripts/tsconfig.json index de12b6235..7258c7c33 100644 --- a/benchmark-scripts/tsconfig.json +++ b/benchmark-scripts/tsconfig.json @@ -4,6 +4,8 @@ "lib": [ "ESNext" ], + "outDir": "dist", + "sourceMap": false, "target": "ESNext", "module": "Preserve", "moduleDetection": "force", diff --git a/bun.lock b/bun.lock index 248fa06d9..f61ed975f 100644 --- a/bun.lock +++ b/bun.lock @@ -5,10 +5,9 @@ "name": "homeserver", "dependencies": { "dotenv": "^16.5.0", - "memoize": "^10.1.0", + "pino": "^9.7.0", + "pino-pretty": "^13.0.0", "reflect-metadata": "^0.2.2", - "rollup": "^4.52.4", - "rollup-plugin-dts": "^6.2.3", "tsyringe": "^4.10.0", "tweetnacl": "^1.0.3", }, @@ -27,23 +26,21 @@ }, }, "packages/core": { - "name": "@rocket.chat/federation-core", + "name": "@hs/core", "version": "1.0.50", "dependencies": { - "@rocket.chat/federation-crypto": "workspace:*", - "@rocket.chat/federation-room": "workspace:*", - "pino": "^9.11.0", + "@hs/crypto": "workspace:*", + "@hs/room": "workspace:*", }, "devDependencies": { "bun-types": "latest", - "pino-pretty": "^13.1.1", "ts-node": "^10.9.2", "ts-patch": "^3.1.2", "typescript": "~5.9.2", }, }, "packages/crypto": { - "name": "@rocket.chat/federation-crypto", + "name": "@hs/crypto", "version": "0.0.1", "dependencies": { "@noble/ed25519": "^3.0.0", @@ -53,34 +50,33 @@ }, }, "packages/federation-sdk": { - "name": "@rocket.chat/federation-sdk", - "version": "0.1.24", + "name": "@hs/federation-sdk", + "version": "0.1.0", "dependencies": { + "@hs/core": "workspace:*", + "@hs/room": "workspace:*", "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-core": "workspace:*", - "@rocket.chat/federation-crypto": "workspace:*", - "@rocket.chat/federation-room": "workspace:*", "mongodb": "^6.16.0", "reflect-metadata": "^0.2.2", "tsyringe": "^4.10.0", "tweetnacl": "^1.0.3", - "zod": "^3.24.1", + "zod": "^3.22.4", }, "peerDependencies": { "typescript": "~5.9.2", }, }, "packages/homeserver": { - "name": "@rocket.chat/homeserver", + "name": "@hs/homeserver", "version": "1.0.50", "dependencies": { "@bogeychan/elysia-etag": "^0.0.6", "@bogeychan/elysia-logger": "^0.1.4", "@elysiajs/swagger": "^1.3.0", + "@hs/core": "workspace:*", + "@hs/federation-sdk": "workspace:*", + "@hs/room": "workspace:*", "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-core": "workspace:*", - "@rocket.chat/federation-room": "workspace:*", - "@rocket.chat/federation-sdk": "workspace:*", "elysia": "^1.1.26", "mongodb": "^6.16.0", "tsyringe": "^4.10.0", @@ -90,11 +86,11 @@ }, }, "packages/room": { - "name": "@rocket.chat/federation-room", + "name": "@hs/room", "version": "1.0.50", "dependencies": { "@datastructures-js/priority-queue": "^6.3.3", - "@rocket.chat/federation-crypto": "workspace:*", + "@hs/crypto": "workspace:*", "zod": "^3.22.4", }, "devDependencies": { @@ -103,24 +99,6 @@ }, }, "packages": { - "@babel/code-frame": [ - "@babel/code-frame@7.27.1", - "", - { - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - } - }, - "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==" - ], - "@babel/helper-validator-identifier": [ - "@babel/helper-validator-identifier@7.27.1", - "", - {}, - "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" - ], "@biomejs/biome": [ "@biomejs/biome@1.9.4", "", @@ -278,6 +256,21 @@ }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ==" ], + "@hs/core": [ + "@hs/core@workspace:packages/core" + ], + "@hs/crypto": [ + "@hs/crypto@workspace:packages/crypto" + ], + "@hs/federation-sdk": [ + "@hs/federation-sdk@workspace:packages/federation-sdk" + ], + "@hs/homeserver": [ + "@hs/homeserver@workspace:packages/homeserver" + ], + "@hs/room": [ + "@hs/room@workspace:packages/room" + ], "@jridgewell/resolve-uri": [ "@jridgewell/resolve-uri@3.1.2", "", @@ -285,10 +278,10 @@ "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" ], "@jridgewell/sourcemap-codec": [ - "@jridgewell/sourcemap-codec@1.5.5", + "@jridgewell/sourcemap-codec@1.5.4", "", {}, - "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" ], "@jridgewell/trace-mapping": [ "@jridgewell/trace-mapping@0.3.9", @@ -323,219 +316,6 @@ {}, "sha512-hw5BpDlNwpYSb+K5X3DNMNUVEVXxmXugUPetGZGCWvntSVFsOjYuVEypoKW6vBBXSfqCBb0kN1npYcKEb4NFBw==" ], - "@rocket.chat/federation-core": [ - "@rocket.chat/federation-core@workspace:packages/core" - ], - "@rocket.chat/federation-crypto": [ - "@rocket.chat/federation-crypto@workspace:packages/crypto" - ], - "@rocket.chat/federation-room": [ - "@rocket.chat/federation-room@workspace:packages/room" - ], - "@rocket.chat/federation-sdk": [ - "@rocket.chat/federation-sdk@workspace:packages/federation-sdk" - ], - "@rocket.chat/homeserver": [ - "@rocket.chat/homeserver@workspace:packages/homeserver" - ], - "@rollup/rollup-android-arm-eabi": [ - "@rollup/rollup-android-arm-eabi@4.52.4", - "", - { - "os": "android", - "cpu": "arm" - }, - "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==" - ], - "@rollup/rollup-android-arm64": [ - "@rollup/rollup-android-arm64@4.52.4", - "", - { - "os": "android", - "cpu": "arm64" - }, - "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==" - ], - "@rollup/rollup-darwin-arm64": [ - "@rollup/rollup-darwin-arm64@4.52.4", - "", - { - "os": "darwin", - "cpu": "arm64" - }, - "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==" - ], - "@rollup/rollup-darwin-x64": [ - "@rollup/rollup-darwin-x64@4.52.4", - "", - { - "os": "darwin", - "cpu": "x64" - }, - "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==" - ], - "@rollup/rollup-freebsd-arm64": [ - "@rollup/rollup-freebsd-arm64@4.52.4", - "", - { - "os": "freebsd", - "cpu": "arm64" - }, - "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==" - ], - "@rollup/rollup-freebsd-x64": [ - "@rollup/rollup-freebsd-x64@4.52.4", - "", - { - "os": "freebsd", - "cpu": "x64" - }, - "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==" - ], - "@rollup/rollup-linux-arm-gnueabihf": [ - "@rollup/rollup-linux-arm-gnueabihf@4.52.4", - "", - { - "os": "linux", - "cpu": "arm" - }, - "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==" - ], - "@rollup/rollup-linux-arm-musleabihf": [ - "@rollup/rollup-linux-arm-musleabihf@4.52.4", - "", - { - "os": "linux", - "cpu": "arm" - }, - "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==" - ], - "@rollup/rollup-linux-arm64-gnu": [ - "@rollup/rollup-linux-arm64-gnu@4.52.4", - "", - { - "os": "linux", - "cpu": "arm64" - }, - "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==" - ], - "@rollup/rollup-linux-arm64-musl": [ - "@rollup/rollup-linux-arm64-musl@4.52.4", - "", - { - "os": "linux", - "cpu": "arm64" - }, - "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==" - ], - "@rollup/rollup-linux-loong64-gnu": [ - "@rollup/rollup-linux-loong64-gnu@4.52.4", - "", - { - "os": "linux", - "cpu": "none" - }, - "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==" - ], - "@rollup/rollup-linux-ppc64-gnu": [ - "@rollup/rollup-linux-ppc64-gnu@4.52.4", - "", - { - "os": "linux", - "cpu": "ppc64" - }, - "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==" - ], - "@rollup/rollup-linux-riscv64-gnu": [ - "@rollup/rollup-linux-riscv64-gnu@4.52.4", - "", - { - "os": "linux", - "cpu": "none" - }, - "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==" - ], - "@rollup/rollup-linux-riscv64-musl": [ - "@rollup/rollup-linux-riscv64-musl@4.52.4", - "", - { - "os": "linux", - "cpu": "none" - }, - "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==" - ], - "@rollup/rollup-linux-s390x-gnu": [ - "@rollup/rollup-linux-s390x-gnu@4.52.4", - "", - { - "os": "linux", - "cpu": "s390x" - }, - "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==" - ], - "@rollup/rollup-linux-x64-gnu": [ - "@rollup/rollup-linux-x64-gnu@4.52.4", - "", - { - "os": "linux", - "cpu": "x64" - }, - "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==" - ], - "@rollup/rollup-linux-x64-musl": [ - "@rollup/rollup-linux-x64-musl@4.52.4", - "", - { - "os": "linux", - "cpu": "x64" - }, - "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==" - ], - "@rollup/rollup-openharmony-arm64": [ - "@rollup/rollup-openharmony-arm64@4.52.4", - "", - { - "os": "none", - "cpu": "arm64" - }, - "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==" - ], - "@rollup/rollup-win32-arm64-msvc": [ - "@rollup/rollup-win32-arm64-msvc@4.52.4", - "", - { - "os": "win32", - "cpu": "arm64" - }, - "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==" - ], - "@rollup/rollup-win32-ia32-msvc": [ - "@rollup/rollup-win32-ia32-msvc@4.52.4", - "", - { - "os": "win32", - "cpu": "ia32" - }, - "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==" - ], - "@rollup/rollup-win32-x64-gnu": [ - "@rollup/rollup-win32-x64-gnu@4.52.4", - "", - { - "os": "win32", - "cpu": "x64" - }, - "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==" - ], - "@rollup/rollup-win32-x64-msvc": [ - "@rollup/rollup-win32-x64-msvc@4.52.4", - "", - { - "os": "win32", - "cpu": "x64" - }, - "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==" - ], "@scalar/openapi-types": [ "@scalar/openapi-types@0.1.1", "", @@ -674,12 +454,6 @@ }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==" ], - "@types/estree": [ - "@types/estree@1.0.8", - "", - {}, - "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" - ], "@types/express": [ "@types/express@5.0.3", "", @@ -890,7 +664,7 @@ "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==" ], "bun-types": [ - "bun-types@1.2.22", + "bun-types@1.2.21", "", { "dependencies": { @@ -900,7 +674,7 @@ "@types/react": "^19" } }, - "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA==" + "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw==" ], "chalk": [ "chalk@5.4.1", @@ -1117,14 +891,6 @@ }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==" ], - "fsevents": [ - "fsevents@2.3.3", - "", - { - "os": "darwin" - }, - "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" - ], "function-bind": [ "function-bind@1.1.2", "", @@ -1233,12 +999,6 @@ {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==" ], - "js-tokens": [ - "js-tokens@4.0.0", - "", - {}, - "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - ], "json5": [ "json5@2.2.3", "", @@ -1318,32 +1078,12 @@ }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==" ], - "magic-string": [ - "magic-string@0.30.19", - "", - { - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==" - ], "make-error": [ "make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" ], - "memoize": [ - "memoize@10.1.0", - "", - { - "dependencies": { - "mimic-function": "^5.0.1" - } - }, - "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==" - ], "memory-pager": [ "memory-pager@1.5.0", "", @@ -1480,12 +1220,6 @@ {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" ], - "picocolors": [ - "picocolors@1.1.1", - "", - {}, - "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - ], "picomatch": [ "picomatch@2.3.1", "", @@ -1503,7 +1237,7 @@ "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==" ], "pino": [ - "pino@9.11.0", + "pino@9.7.0", "", { "dependencies": { @@ -1523,7 +1257,7 @@ "pino": "bin.js" } }, - "sha512-+YIodBB9sxcWeR8PrXC2K3gEDyfkUuVEITOcbqrfcj+z5QW4ioIcqZfYFbrLTYLsmAwunbS7nfU/dpBB6PZc1g==" + "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==" ], "pino-abstract-transport": [ "pino-abstract-transport@2.0.0", @@ -1639,61 +1373,6 @@ {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" ], - "rollup": [ - "rollup@4.52.4", - "", - { - "dependencies": { - "@types/estree": "1.0.8" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.4", - "@rollup/rollup-android-arm64": "4.52.4", - "@rollup/rollup-darwin-arm64": "4.52.4", - "@rollup/rollup-darwin-x64": "4.52.4", - "@rollup/rollup-freebsd-arm64": "4.52.4", - "@rollup/rollup-freebsd-x64": "4.52.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", - "@rollup/rollup-linux-arm-musleabihf": "4.52.4", - "@rollup/rollup-linux-arm64-gnu": "4.52.4", - "@rollup/rollup-linux-arm64-musl": "4.52.4", - "@rollup/rollup-linux-loong64-gnu": "4.52.4", - "@rollup/rollup-linux-ppc64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-musl": "4.52.4", - "@rollup/rollup-linux-s390x-gnu": "4.52.4", - "@rollup/rollup-linux-x64-gnu": "4.52.4", - "@rollup/rollup-linux-x64-musl": "4.52.4", - "@rollup/rollup-openharmony-arm64": "4.52.4", - "@rollup/rollup-win32-arm64-msvc": "4.52.4", - "@rollup/rollup-win32-ia32-msvc": "4.52.4", - "@rollup/rollup-win32-x64-gnu": "4.52.4", - "@rollup/rollup-win32-x64-msvc": "4.52.4", - "fsevents": "~2.3.2" - }, - "bin": { - "rollup": "dist/bin/rollup" - } - }, - "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==" - ], - "rollup-plugin-dts": [ - "rollup-plugin-dts@6.2.3", - "", - { - "dependencies": { - "magic-string": "^0.30.17" - }, - "optionalDependencies": { - "@babel/code-frame": "^7.27.1" - }, - "peerDependencies": { - "rollup": "^3.29.4 || ^4", - "typescript": "^4.5 || ^5.0" - } - }, - "sha512-UgnEsfciXSPpASuOelix7m4DrmyQgiaWBnvI0TM4GxuDh5FkqW8E5hu57bCxXB90VvR1WNfLV80yEDN18UogSA==" - ], "safe-stable-stringify": [ "safe-stable-stringify@2.5.0", "", @@ -1808,10 +1487,10 @@ "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" ], "strip-json-comments": [ - "strip-json-comments@5.0.3", + "strip-json-comments@5.0.2", "", {}, - "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==" + "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==" ], "strtok3": [ "strtok3@10.3.1", @@ -2162,6 +1841,19 @@ {}, "sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q==" ], + "@hs/crypto/bun-types": [ + "bun-types@1.2.22", + "", + { + "dependencies": { + "@types/node": "*" + }, + "peerDependencies": { + "@types/react": "^19" + } + }, + "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA==" + ], "@scalar/themes/@scalar/types": [ "@scalar/types@0.1.7", "", diff --git a/package.json b/package.json index 5e5ec4a37..3ded29f18 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "turbo": "~2.5.6", "typescript": "~5.9.2" }, - "workspaces": ["packages/*"], + "workspaces": [ + "packages/*" + ], "dependencies": { "dotenv": "^16.5.0", "memoize": "^10.1.0", @@ -41,7 +43,9 @@ "biome format --write --no-errors-on-unmatched --diagnostic-level=error", "biome lint --write --no-errors-on-unmatched --diagnostic-level=error" ], - "*": ["biome check --no-errors-on-unmatched --files-ignore-unknown=true"] + "*": [ + "biome check --no-errors-on-unmatched --files-ignore-unknown=true" + ] }, "scripts": { "prepare": "husky", @@ -55,4 +59,4 @@ "tsc": "tsc --noEmit", "bundle:sdk": "bun run build && bun run bundle.ts" } -} +} \ No newline at end of file diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 88dcc7694..9c98a11e1 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -19,11 +19,7 @@ "@noble/ed25519": "^3.0.0" }, "devDependencies": { - "@types/libsodium-wrappers": "^0.7.14", - "@types/sodium-native": "^2.3.9", - "bun-types": "latest", - "express": "^5.1.0", - "k6": "^0.0.0" + "bun-types": "latest" }, "files": [ "src", diff --git a/packages/crypto/src/__examples/signing-key.ssh-keygen.ts b/packages/crypto/src/__examples/signing-key.ssh-keygen.ts index d6e0c9aed..d91d8bb64 100644 --- a/packages/crypto/src/__examples/signing-key.ssh-keygen.ts +++ b/packages/crypto/src/__examples/signing-key.ssh-keygen.ts @@ -10,7 +10,7 @@ import { type DataType, type EncryptionValidAlgorithm, type SignatureType, -} from '../constants'; +} from '../utils/constants'; export type SshKeygenSigningKeyConfig = { // path to the private key file generated by ssh-keygen diff --git a/packages/crypto/src/constants.ts b/packages/crypto/src/constants.ts deleted file mode 100644 index 2e2655762..000000000 --- a/packages/crypto/src/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum EncryptionValidAlgorithm { - ed25519 = 'ed25519', -} - -export type DataType = string | Buffer | Uint8Array; - -export type SignatureType = Buffer | Uint8Array; diff --git a/packages/crypto/src/tsconfig.json b/packages/crypto/src/tsconfig.json index 5894d9d8d..7843b7cb4 100644 --- a/packages/crypto/src/tsconfig.json +++ b/packages/crypto/src/tsconfig.json @@ -95,6 +95,6 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "exclude": [ - "**/examples/**" + "**/__examples/**" ] } \ No newline at end of file diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json index ec444c1d0..70930c74b 100644 --- a/packages/crypto/tsconfig.json +++ b/packages/crypto/tsconfig.json @@ -10,14 +10,12 @@ }, "include": [ "src/**/*", - "../../benchmark-scripts/signing-and-verifications/server.ts", - "../../benchmark-scripts/signing-and-verifications/script.js", - "src/contracts/key.ts" ], "exclude": [ "node_modules", "dist", "**/*.spec.ts", - "**/*.test.ts" + "**/*.test.ts", + "../../benchmark-scripts/**", ] } \ No newline at end of file From 31c24186778151793db7ffbc1346a06094bd6c0c Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 8 Sep 2025 13:36:16 +0530 Subject: [PATCH 11/56] ... --- .../src/__examples/signing-key.ssh-keygen.ts | 182 ------------------ packages/crypto/tsconfig.json | 3 +- tsconfig.json | 34 +++- 3 files changed, 27 insertions(+), 192 deletions(-) delete mode 100644 packages/crypto/src/__examples/signing-key.ssh-keygen.ts diff --git a/packages/crypto/src/__examples/signing-key.ssh-keygen.ts b/packages/crypto/src/__examples/signing-key.ssh-keygen.ts deleted file mode 100644 index d91d8bb64..000000000 --- a/packages/crypto/src/__examples/signing-key.ssh-keygen.ts +++ /dev/null @@ -1,182 +0,0 @@ -// this file is intended to be an example of how SigningKey can be implemented using different key storage/generation mechanisms - -import { exists, writeFile } from 'node:fs/promises'; -import { exec } from 'node:child_process'; -import crypto from 'node:crypto'; - -import { type Signer } from '../contracts/key'; -import { readFile } from 'node:fs/promises'; -import { - type DataType, - type EncryptionValidAlgorithm, - type SignatureType, -} from '../utils/constants'; - -export type SshKeygenSigningKeyConfig = { - // path to the private key file generated by ssh-keygen - privateKeyPath?: string; - // path to the public key file generated by ssh-keygen - publicKeyPath?: string; - - configDir: string; -}; - -async function loadKey(path: string): Promise { - const data = await readFile(path); - return data; -} - -type Key = { privateKey: Buffer; publicKey: Buffer }; - -async function loadKeys( - privateKeyPath: string, - publicKeyPath: string, -): Promise { - const sk = await loadKey(privateKeyPath); - const pk = await loadKey(publicKeyPath); - - return { privateKey: Buffer.from(sk), publicKey: Buffer.from(pk) }; -} - -export class SshKeygenSigningKey implements Signer { - public version = '0'; - public constructor(private readonly config: SshKeygenSigningKeyConfig) {} - - public algorithm = 'rsa' as EncryptionValidAlgorithm; - - private privateKey?: Buffer; - private publicKey?: Buffer; - - private setkey(key: Key) { - this.privateKey = key.privateKey; - this.publicKey = key.publicKey; - } - - public async load(): Promise { - const { privateKeyPath, publicKeyPath, configDir } = this.config; - - const algorithm = this.algorithm; - - if (!privateKeyPath || !publicKeyPath) { - // we generate the keys ourselves - // must be mac or linux for this - if (process.platform !== 'darwin' && process.platform !== 'linux') { - throw new Error( - 'ssh-keygen signing key without provided keys is only supported on macOS and Linux', - ); - } - - if (!configDir) { - throw new Error('configDir must be provided if keys are not provided'); - } - - // set some defaults - const sk = `${configDir}/id_${algorithm}` as const; - const pk = `${sk}.pub` as const; - - // maybe they exist in the configDir already - const skExists = await exists(sk); - const pkExists = await exists(pk); - - // both must exist or not exist - if (skExists !== pkExists) { - throw new Error( - 'Both private and public key files must exist or not exist', - ); - } - - // both exists, we load them - if (skExists) { - // TODO: handle loading this - const key = await loadKeys(sk, pk); - - this.setkey(key); - return; - } - // doesn't exist, generate - const key: Key = await new Promise((resolve, reject) => - exec( - // biome-ignore lint/style/useTemplate: - 'ssh-keygen' + // using ssh-keygen - ' -m PEM' + // export to PEM format, which crypto library will need, technically not a limitatiion for the IDEA of a SigningKey - ' -N ""' + // no passphrase - ' -t ' + - algorithm + // == rsa, needed for PEM export easily - ' -f ' + - sk, // file to write the private key to - async (error, _stdout, stderr) => { - if (error) { - console.error(`Error generating ssh keys: ${error.message}`); - reject(error); - } - if (stderr) { - reject(`ssh-keygen stderr: ${stderr}`); - } - - // need public key in PEM format too, so we convert it - await new Promise((resolve, reject) => - exec(`ssh-keygen -f ${pk} -e -m PEM`, (error, stdout, stderr) => { - if (error) { - console.error( - `Error generating public key PEM: ${error.message}`, - ); - reject(error); - } - if (stderr) { - reject(`ssh-keygen stderr: ${stderr}`); - } - - // ssh-keygen sends the pem to stdout, we overwrite existikng pk with it - writeFile(pk, stdout).then(() => - // write the public key PEM to a file - resolve({}), - ); - }), - ); - - // load the generated keys - const key = await loadKeys(sk, pk); - resolve(key); - }, - ), - ); - - this.setkey(key); - return; - } - - // both keys are provided, we load them - const key = await loadKeys(privateKeyPath, publicKeyPath); - - this.setkey(key); - } - - async sign(data: DataType): Promise { - if (!this.privateKey) { - throw new Error('Private key not loaded'); - } - - const dataLike = typeof data === 'string' ? Buffer.from(data) : data; - const signature = await crypto.sign('sha256', dataLike, this.privateKey); - - return signature; - } - - async verify(data: DataType, signature: SignatureType): Promise { - const dataLike = typeof data === 'string' ? Buffer.from(data) : data; - return new Promise((resolve, reject) => { - if (!this.publicKey) { - reject('Public key not loaded'); - return; - } - - crypto.verify('sha256', dataLike, this.publicKey, signature, (error) => { - if (error) { - reject(error); - } - - resolve(); - }); - }); - } -} diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json index 70930c74b..b3bdc837c 100644 --- a/packages/crypto/tsconfig.json +++ b/packages/crypto/tsconfig.json @@ -15,7 +15,6 @@ "node_modules", "dist", "**/*.spec.ts", - "**/*.test.ts", - "../../benchmark-scripts/**", + "**/*.test.ts" ] } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 748ed619b..4f9b1bad8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,13 +5,31 @@ "noEmit": true, "tsBuildInfoFile": ".tsbuildinfo" }, - "include": ["index.ts", "test-setup.ts"], - "exclude": ["node_modules", "dist", "packages/*/dist"], + "include": [ + "index.ts", + "test-setup.ts" + ], + "exclude": [ + "node_modules", + "dist", + "packages/*/dist", + "benchmark-scripts/**" + ], "references": [ - { "path": "./packages/core" }, - { "path": "./packages/crypto" }, - { "path": "./packages/federation-sdk" }, - { "path": "./packages/homeserver" }, - { "path": "./packages/room" } + { + "path": "./packages/core" + }, + { + "path": "./packages/crypto" + }, + { + "path": "./packages/federation-sdk" + }, + { + "path": "./packages/homeserver" + }, + { + "path": "./packages/room" + } ] -} +} \ No newline at end of file From 0070e01eb639b3d2d78ab40aa8811589e51cf8af Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 8 Sep 2025 13:50:42 +0530 Subject: [PATCH 12/56] biome lint --- .vscode/settings.json | 13 ++- benchmark-scripts/package.json | 2 +- .../signing-and-verifications/script.js | 34 +++--- .../signing-and-verifications/server.ts | 9 +- benchmark-scripts/tsconfig.json | 6 +- package.json | 10 +- packages/crypto/package.json | 7 +- packages/crypto/src/tsconfig.json | 100 ------------------ packages/crypto/tsconfig.json | 13 +-- tsconfig.json | 7 +- 10 files changed, 43 insertions(+), 158 deletions(-) delete mode 100644 packages/crypto/src/tsconfig.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 74ca7de4c..6d4a2a179 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,5 +14,14 @@ "[typescript]": { "editor.defaultFormatter": "biomejs.biome" }, - "typescript.enablePromptUseWorkspaceTsdk": true - } + "typescript.enablePromptUseWorkspaceTsdk": true, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + } +} \ No newline at end of file diff --git a/benchmark-scripts/package.json b/benchmark-scripts/package.json index 3961ac9c6..6b817b353 100644 --- a/benchmark-scripts/package.json +++ b/benchmark-scripts/package.json @@ -14,4 +14,4 @@ "scripts": { "crypto": "bun run ./signing-and-verifications/server.ts" } -} \ No newline at end of file +} diff --git a/benchmark-scripts/signing-and-verifications/script.js b/benchmark-scripts/signing-and-verifications/script.js index 348ecfee2..83e00dfac 100644 --- a/benchmark-scripts/signing-and-verifications/script.js +++ b/benchmark-scripts/signing-and-verifications/script.js @@ -1,28 +1,28 @@ -import http from "k6/http"; +import http from 'k6/http'; function _payload() { - const messagelength = __ENV.MESSAGE_LENGTH || "1024"; - const engine = __ENV.ENGINE || "tweetnacl"; + const messagelength = __ENV.MESSAGE_LENGTH || '1024'; + const engine = __ENV.ENGINE || 'tweetnacl'; - const stream = !!__ENV.STREAM; + const stream = !!__ENV.STREAM; - return JSON.stringify({ - message: messagelength, - api: { - engine, - stream, - }, - }); + return JSON.stringify({ + message: messagelength, + api: { + engine, + stream, + }, + }); } const payload = _payload(); -console.log("payload:", payload); +console.log('payload:', payload); export default function () { - http.post(`http://localhost:${__ENV.PORT || 8080}/signAndVerify`, payload, { - headers: { - "content-type": "application/json", - }, - }); + http.post(`http://localhost:${__ENV.PORT || 8080}/signAndVerify`, payload, { + headers: { + 'content-type': 'application/json', + }, + }); } diff --git a/benchmark-scripts/signing-and-verifications/server.ts b/benchmark-scripts/signing-and-verifications/server.ts index ccba03031..b837f3a2b 100644 --- a/benchmark-scripts/signing-and-verifications/server.ts +++ b/benchmark-scripts/signing-and-verifications/server.ts @@ -1,13 +1,8 @@ +import express from 'express'; import nacl from 'tweetnacl'; -import express, { type Request, type Response } from 'express'; +import { encodeCanonicalJson } from '../../packages/crypto/src'; import { loadEd25519SignerFromSeed } from '../../packages/crypto/src/utils/keys'; -import { Readable } from 'node:stream'; -import { - encodeCanonicalJson, - encodeCanonicalJsonAsync, - toUnpaddedBase64, -} from '../../packages/crypto/src'; const app = express(); diff --git a/benchmark-scripts/tsconfig.json b/benchmark-scripts/tsconfig.json index 7258c7c33..5d2612202 100644 --- a/benchmark-scripts/tsconfig.json +++ b/benchmark-scripts/tsconfig.json @@ -1,9 +1,7 @@ { "compilerOptions": { // Environment setup & latest features - "lib": [ - "ESNext" - ], + "lib": ["ESNext"], "outDir": "dist", "sourceMap": false, "target": "ESNext", @@ -26,4 +24,4 @@ "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 3ded29f18..5e5ec4a37 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,7 @@ "turbo": "~2.5.6", "typescript": "~5.9.2" }, - "workspaces": [ - "packages/*" - ], + "workspaces": ["packages/*"], "dependencies": { "dotenv": "^16.5.0", "memoize": "^10.1.0", @@ -43,9 +41,7 @@ "biome format --write --no-errors-on-unmatched --diagnostic-level=error", "biome lint --write --no-errors-on-unmatched --diagnostic-level=error" ], - "*": [ - "biome check --no-errors-on-unmatched --files-ignore-unknown=true" - ] + "*": ["biome check --no-errors-on-unmatched --files-ignore-unknown=true"] }, "scripts": { "prepare": "husky", @@ -59,4 +55,4 @@ "tsc": "tsc --noEmit", "bundle:sdk": "bun run build && bun run bundle.ts" } -} \ No newline at end of file +} diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 9c98a11e1..db97ed31d 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -21,8 +21,5 @@ "devDependencies": { "bun-types": "latest" }, - "files": [ - "src", - "dist" - ] -} \ No newline at end of file + "files": ["src", "dist"] +} diff --git a/packages/crypto/src/tsconfig.json b/packages/crypto/src/tsconfig.json deleted file mode 100644 index 7843b7cb4..000000000 --- a/packages/crypto/src/tsconfig.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - "composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */, - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */, - "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "ES2022" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": [ - "bun-types" - ] /* Specify type package names to be included without being referenced in a source file. */, - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - "sourceMap": true /* Create source map files for emitted JavaScript files. */, - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - "useUnknownInCatchVariables": true /* Default catch clause variables as 'unknown' instead of 'any'. */, - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - "noUnusedParameters": true /* Raise an error when a function parameter isn't read. */, - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - "noImplicitReturns": false /* Enable error reporting for codepaths that do not explicitly return in a function. */, - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - "allowUnusedLabels": false /* Disable error reporting for unused labels. */, - "allowUnreachableCode": false /* Disable error reporting for unreachable code. */, - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "exclude": [ - "**/__examples/**" - ] -} \ No newline at end of file diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json index b3bdc837c..91ec6e497 100644 --- a/packages/crypto/tsconfig.json +++ b/packages/crypto/tsconfig.json @@ -8,13 +8,6 @@ "verbatimModuleSyntax": false, "tsBuildInfoFile": "./.tsbuildinfo" }, - "include": [ - "src/**/*", - ], - "exclude": [ - "node_modules", - "dist", - "**/*.spec.ts", - "**/*.test.ts" - ] -} \ No newline at end of file + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index 4f9b1bad8..0b212526f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,10 +5,7 @@ "noEmit": true, "tsBuildInfoFile": ".tsbuildinfo" }, - "include": [ - "index.ts", - "test-setup.ts" - ], + "include": ["index.ts", "test-setup.ts"], "exclude": [ "node_modules", "dist", @@ -32,4 +29,4 @@ "path": "./packages/room" } ] -} \ No newline at end of file +} From 0d8ec53f5b1f9e2d56f6f07ac578293e9b7e4b86 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 8 Sep 2025 16:26:08 +0530 Subject: [PATCH 13/56] correct event signature validation --- .../signature-verification.service.spec.ts | 229 ++++++++++++++++++ .../signature-verification.service.ts | 208 ++++++++++++++++ 2 files changed, 437 insertions(+) create mode 100644 packages/federation-sdk/src/services/signature-verification.service.spec.ts create mode 100644 packages/federation-sdk/src/services/signature-verification.service.ts diff --git a/packages/federation-sdk/src/services/signature-verification.service.spec.ts b/packages/federation-sdk/src/services/signature-verification.service.spec.ts new file mode 100644 index 000000000..6b53cd591 --- /dev/null +++ b/packages/federation-sdk/src/services/signature-verification.service.spec.ts @@ -0,0 +1,229 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, + mock, + test, + type Mock, +} from 'bun:test'; +import { SignatureVerificationService } from './signature-verification.service'; +import { PersistentEventFactory } from '@hs/room'; + +const originServer = 'syn1.tunnel.dev.rocket.chat'; + +const keyId = 'ed25519:a_FAET'; + +/* + * {"type":"m.room.create","state_key":"","content":{"room_version":"10","creator":"@debdut:rc1.tunnel.dev.rocket.chat"},"sender":"@debdut:rc1.tunnel.dev.rocket.chat","origin_server_ts":1753363422133,"origin":"rc1.tunnel.dev.rocket.chat","room_id":"!uUGiqDlq:rc1.tunnel.dev.rocket.chat","prev_events":[],"auth_events":[],"depth":0,"hashes":{"sha256":"n4Vml4VWf+qXqS0AtFD8WK3JYGQWQO4sNB8CXV3HarM"},"signatures":{"rc1.tunnel.dev.rocket.chat":{"ed25519:0":"m9ccld2sylkt4E5kn36BPyLiWL/wJFYE2vzkp62FGmxO2DQ66a+qMz5lq18+rkEjNxONREfTioJov3s6nHjhAA"}},"unsigned":{}} + * {"old_verify_keys":{},"server_name":"rc1.tunnel.dev.rocket.chat","signatures":{"rc1.tunnel.dev.rocket.chat":{"ed25519:0":"kNs1Wqt3MDNVZg1gQ+c0mXiiOmpCnR41siXdHes8wNvq0SQb5VCSa1Fz+LF6WN10fNyBoBp6ukc19bKQKreNCw"}},"valid_until_ts":1754130035648,"verify_keys":{"ed25519:0":{"key":"RL+t3F91NXGsUI9YZtKBRMETgUgfApqfeA3q8Go/Uo4"}}} + */ + +// v10 +const event = { + auth_events: [ + '$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA', + '$Ulggyo4m1OlI08Z0jJDVeceigjSZP9SdEFVoAn9mEh8', + '$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg', + '$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs', + ], + content: { + displayname: 'debdut1', + membership: 'join' as const, + }, + depth: 10, + hashes: { sha256: '6MnKSCFJy1fYf6ukILBEbqx2DkoaD1wRyKXhv689a0A' }, + origin: 'syn1.tunnel.dev.rocket.chat', + origin_server_ts: 1757328411218, + prev_events: ['$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs'], + room_id: '!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat', + sender: '@debdut1:syn1.tunnel.dev.rocket.chat', + state_key: '@debdut1:syn1.tunnel.dev.rocket.chat', + type: 'm.room.member' as const, + signatures: { + 'syn1.tunnel.dev.rocket.chat': { + 'ed25519:a_FAET': + 'eJlvqxPWPe3u+BM4wOwID9YBlh/ZfVVxGYyA5WgpNs5Fe1+c36qrvCKHuXGGjfQoZFrHmZ3/GJw2pv5EvxCZAA', + }, + }, + unsigned: { + age: 1, + replaces_state: '$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs', + prev_content: { displayname: 'debdut1', membership: 'invite' }, + prev_sender: '@debdut:syn2.tunnel.dev.rocket.chat', + }, +}; + +/* + * {"event":{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$Ulggyo4m1OlI08Z0jJDVeceigjSZP9SdEFVoAn9mEh8","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs"],"content":{"avatar_url":null,"displayname":"debdut1","membership":"join"},"depth":10,"hashes":{"sha256":"6MnKSCFJy1fYf6ukILBEbqx2DkoaD1wRyKXhv689a0A"},"origin":"syn1.tunnel.dev.rocket.chat","origin_server_ts":1757328411218,"prev_events":["$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs"],"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","sender":"@debdut1:syn1.tunnel.dev.rocket.chat","state_key":"@debdut1:syn1.tunnel.dev.rocket.chat","type":"m.room.member","signatures":{"syn1.tunnel.dev.rocket.chat":{"ed25519:a_FAET":"eJlvqxPWPe3u+BM4wOwID9YBlh/ZfVVxGYyA5WgpNs5Fe1+c36qrvCKHuXGGjfQoZFrHmZ3/GJw2pv5EvxCZAA"}},"unsigned":{"age":1,"replaces_state":"$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs","prev_content":{"displayname":"debdut1","membership":"invite"},"prev_sender":"@debdut:syn2.tunnel.dev.rocket.chat"}},"state":[{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$sOuxoPjLRgDVgtgKrmBxdVFRc9hfODoXHvwfznrBJvk"],"type":"m.room.guest_access","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"guest_access":"can_join"},"depth":6,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362604,"hashes":{"sha256":"TrP386yOPRnjhRF0wIrR+558ZPbmv3dVP66rMOrqapA"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"kDuj6bQcTT7TBAkSFFjw/qwTg7afo2VFyRDma1O3HVFt00wUkgmB2yHqOehmD64uwsirCdYgy6v+geeR2z3xBw"}},"unsigned":{"age":49854}},{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$_wPeMHrY4MrlDX21jtu0z6jFBz37G8akqmZDhORIqco"],"type":"m.room.name","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"name":"aaaaaaaa"},"depth":8,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362604,"hashes":{"sha256":"RnymDqNP7NRKoaqEtVGBW2DV3tevZ/jCZY7AcdbZynM"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"+D1Ir0W1UY5nd+rIQlHgAedmTciPW6CFPp4bbARYhiB5upIERuE46zSUqpkjIPYKV2tC5SI4EVONQwtEmPRBBQ"}},"unsigned":{"age":49854}},{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$Ulggyo4m1OlI08Z0jJDVeceigjSZP9SdEFVoAn9mEh8","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$jjtu7bXR1KQ5pyHjtu71l4Y0HsDRxfpyA7XZ3Y7K7Hg"],"type":"m.room.member","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"displayname":"debdut1","membership":"invite"},"depth":9,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"@debdut1:syn1.tunnel.dev.rocket.chat","origin_server_ts":1757328402120,"hashes":{"sha256":"o5Ph/P2Lybk2fU1NUSsAYQi36SOLdvClaBWBTa5KUL8"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"Uj9SNp+B95gi29U+BnIaQA60B/kY8ZnbMfaUGog4SU7OEPLhTNh7SOcki1TBkdl6OlRCkGypKVJXyx7b/J7MAA"},"syn1.tunnel.dev.rocket.chat":{"ed25519:a_FAET":"plQW1ZLVhu6EIGCa2hHBv4GWyjOzXBSQ+hMyj+DlX0BHMD3+3GsmJbiYySmh2kmMEP3IsI4YS0D9MquN6VHsDg"}},"unsigned":{"invite_room_state":[{"type":"m.room.join_rules","state_key":"","content":{"join_rule":"invite"},"sender":"@debdut:syn2.tunnel.dev.rocket.chat"},{"type":"m.room.create","state_key":"","content":{"room_version":"10","creator":"@debdut:syn2.tunnel.dev.rocket.chat"},"sender":"@debdut:syn2.tunnel.dev.rocket.chat"},{"type":"m.room.encryption","state_key":"","content":{"algorithm":"m.megolm.v1.aes-sha2"},"sender":"@debdut:syn2.tunnel.dev.rocket.chat"},{"type":"m.room.name","state_key":"","content":{"name":"aaaaaaaa"},"sender":"@debdut:syn2.tunnel.dev.rocket.chat"},{"type":"m.room.member","state_key":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"displayname":"debdut","membership":"join"},"sender":"@debdut:syn2.tunnel.dev.rocket.chat"}],"age":10338}},{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA"],"type":"m.room.join_rules","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"join_rule":"invite"},"depth":4,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362603,"hashes":{"sha256":"ak/Qv/mMfgvK/XdJGoc9Poby2BUtriHMgRGZcbYspMU"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"hYm15xFhJQfG90noqFPKnV7ZLIqENBM5ICMAZZS8GQnXvWOGdGEOkeKj4uXIEEsxxxad/Sc1xce5vV+raTuQCA"}},"unsigned":{"age":49855}},{"auth_events":["$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"type":"m.room.power_levels","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"users":{"@debdut:syn2.tunnel.dev.rocket.chat":100},"users_default":0,"events":{"m.room.name":50,"m.room.avatar":50,"m.room.power_levels":100,"m.room.history_visibility":100,"m.room.canonical_alias":50,"m.room.tombstone":100,"m.room.server_acl":100,"m.room.encryption":100,"org.matrix.msc3401.call.member":0,"org.matrix.msc3401.call":100},"events_default":0,"state_default":50,"ban":50,"kick":50,"redact":50,"invite":0,"historical":100},"depth":3,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362598,"hashes":{"sha256":"N8XBL3oJrwU/5SP4Uqa+jbIEqsts+cgFs04bnB0zMFM"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"zJ3u25+gz99P+WmRBYgNOtqqIBb1jngMALLxqljlrIG/ifDLy1UyXtwm/g5a6r9qTLE/taql99DCPY4kXH3nAA"}},"unsigned":{"age":49860}},{"auth_events":[],"prev_events":[],"type":"m.room.create","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"room_version":"10","creator":"@debdut:syn2.tunnel.dev.rocket.chat"},"depth":1,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362536,"hashes":{"sha256":"Wqul5TbaOj83JuQkKj/CTlYeaUtocHePDFIVqBzJJS4"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"4uOTMonGEXg74DOOcWgkpRMzmkPg/cQVp9b16jYPPFOgMk8Tr5lOWDa/jAUlVEALj2kYHRblk3UacMaQzvPaDw"}},"unsigned":{"age":49922}},{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$Ulggyo4m1OlI08Z0jJDVeceigjSZP9SdEFVoAn9mEh8"],"type":"m.room.history_visibility","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"history_visibility":"shared"},"depth":5,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362604,"hashes":{"sha256":"+UW9TzftG+Q5aM46uLOiOiMlOSDdvMVDMSf/EZonvR4"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"oNyD4B2gfy8+e6r+ZDSAb807gEhx40d6exLrVWmsn4s+N6qZ28i41DWfttpZap+GPwVDA4mpCZWkZet0rBBxDw"}},"unsigned":{"age":49854}},{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$-1FmjaDcaLllAM91flimqEJ9xqCw5zBv3kdMUcr7Etk"],"type":"m.room.encryption","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"algorithm":"m.megolm.v1.aes-sha2"},"depth":7,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362604,"hashes":{"sha256":"L0FLRolYQPHm4AuxTLHzysz6Nmh8tmWuERKYUHO5yeM"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"qJyKdg+J1ER4Et1hrqiQNc3mM3VCM8Q6DNnlSu/9lypRW5VRZy3HvPKukvgoyQr1/ewc+0x0EoeT0ca5wrxCDA"}},"unsigned":{"age":49854}}],"auth_chain":[{"auth_events":["$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg"],"prev_events":["$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg"],"type":"m.room.member","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"displayname":"debdut","membership":"join"},"depth":2,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"@debdut:syn2.tunnel.dev.rocket.chat","origin_server_ts":1757328362573,"hashes":{"sha256":"OQ9F8BCBAjylOXjPPt6ZZGfaVUe0aInmPLoaAS9PY8E"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"wPLUukVrilo2byP4sptw2gJCXPPSuLrv4pQtSPvazx9aw0Fs/tZNVANaum1lt4adigGn5Zzlj6gnA4k65z3hAQ"}},"unsigned":{"age":49885}}],"members_omitted":true,"servers_in_room":["syn2.tunnel.dev.rocket.chat"]} + * + * {"old_verify_keys":{},"server_name":"syn1.tunnel.dev.rocket.chat","signatures":{"syn1.tunnel.dev.rocket.chat":{"ed25519:a_FAET":"32jfhYKQGENYAByGWZlMPcqgLcGJCoU9RyxOz4TGrmGbTwmbBi8BGbgNJHH8DmWuyoD6FnZ4yI5YBZTJqPjQAA"}},"valid_until_ts":1757414678669,"verify_keys":{"ed25519:a_FAET":{"key":"kryovKVnhHESOdWuZ05ViNotRMVdEh/mG2yJ0npLzEo"}}} + */ + +describe('SignatureVerificationService', () => { + let service: SignatureVerificationService; + + let originalFetch: typeof globalThis.fetch; + + const mockKeyData = { + old_verify_keys: {}, + server_name: 'syn1.tunnel.dev.rocket.chat', + signatures: { + 'syn1.tunnel.dev.rocket.chat': { + 'ed25519:a_FAET': + '32jfhYKQGENYAByGWZlMPcqgLcGJCoU9RyxOz4TGrmGbTwmbBi8BGbgNJHH8DmWuyoD6FnZ4yI5YBZTJqPjQAA', + }, + }, + valid_until_ts: 1757414678669, + verify_keys: { + 'ed25519:a_FAET': { key: 'kryovKVnhHESOdWuZ05ViNotRMVdEh/mG2yJ0npLzEo' }, + }, + }; + + type FetchJson = Awaited>['json']; + + const fetchJsonMock: Mock = mock(() => + Promise.resolve(mockKeyData), + ); + + beforeEach(() => { + originalFetch = globalThis.fetch; + + service = new SignatureVerificationService(); // invalidates internal cache + + fetchJsonMock.mockReturnValue(Promise.resolve(mockKeyData)); + + globalThis.fetch = Object.assign( + async (_url: string, _options?: RequestInit) => { + return { + ok: true, + status: 200, + json: fetchJsonMock as unknown as FetchJson, + } as Response; + }, + { preconnect: () => {} }, + ) as typeof fetch; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + mock.restore(); + }); + + describe('verifyEventSignature', async () => { + it('should verify a valid event signature', async () => { + const pdu = PersistentEventFactory.createFromRawEvent( + structuredClone(event), + '10', + ); + + return expect(service.verifyEventSignature(pdu)).resolves.toBeUndefined(); + }); + + // each step of the spec + it('should fail if not signed by the origin server (1)', async () => { + const eventClone = structuredClone(event); + + const pdu = PersistentEventFactory.createFromRawEvent({ + ...eventClone, + signatures: {}, // no signatures + }); + + return expect(service.verifyEventSignature(pdu)).rejects.toThrow( + `No signature found for origin ${originServer}`, + ); + }); + + it('should fail if signed by algorithm not supported by us (ed25519) (2)', async () => { + const eventClone = structuredClone(event); + + const pdu = PersistentEventFactory.createFromRawEvent({ + ...eventClone, + signatures: { + [originServer]: { + // different algorithm + 'not-supported:0': event.signatures[originServer][keyId], + }, + }, + }); + + return expect(service.verifyEventSignature(pdu)).rejects.toThrow( + `No valid signature keys found for origin ${originServer} with supported algorithms`, + ); + }); + + it('should fail if service could not find the public key from the origin homeserver (3.1)', async () => { + const eventClone = structuredClone(event); + + const pdu = PersistentEventFactory.createFromRawEvent(eventClone); + + // making fetch fail + fetchJsonMock.mockReturnValue(Promise.reject(new Error('network error'))); + + return expect(service.verifyEventSignature(pdu)).rejects.toThrow( + `No valid verification key found for origin ${originServer} with supported algorithms`, + ); + }); + + test.todo( + 'should pass if service find any of the supported keys from the origin homeserver (3.2)', + async () => { + // need event to be signed by multiple keys + }, + ); + + test.todo( + 'should fail if the signature itself is invalid base64 (4.1)', + async () => { + // need event to be signed by multiple keys + }, + ); + + it('should fail if the signature itself is invalid (4.2)', async () => { + const eventClone = structuredClone(event); + + const pdu = PersistentEventFactory.createFromRawEvent({ + ...eventClone, + signatures: { + [originServer]: { + [keyId]: '@@@@', // invalid base64 + }, + }, + }); + + // should fail because the signature length isn't correct for ed25519 + await expect(service.verifyEventSignature(pdu)).rejects.toThrow( + /Invalid signature length/, + ); + + await mock.module('./signature-verification.service', () => ({ + MAX_SIGNATURE_LENGTH_FOR_ED25519: 4, + })); + + await expect(service.verifyEventSignature(pdu)).rejects.toThrow( + /Failed to decode base64 signature /, + ); + + const anyString = 'abc123'; + const base64String = btoa(anyString); // valid base64 but not a valid signature + + const pdu2 = PersistentEventFactory.createFromRawEvent({ + ...eventClone, + signatures: { + [originServer]: { + [keyId]: base64String, + }, + }, + }); + + await mock.module('./signature-verification.service', () => ({ + MAX_SIGNATURE_LENGTH_FOR_ED25519: base64String.length, + })); + + await expect(service.verifyEventSignature(pdu2)).rejects.toThrow( + 'Invalid signature', + ); + }); + }); +}); diff --git a/packages/federation-sdk/src/services/signature-verification.service.ts b/packages/federation-sdk/src/services/signature-verification.service.ts new file mode 100644 index 000000000..ef39f5c3b --- /dev/null +++ b/packages/federation-sdk/src/services/signature-verification.service.ts @@ -0,0 +1,208 @@ +import { createLogger } from '@hs/core'; +import { + encodeCanonicalJson, + fromBase64ToBytes, + isValidAlgorithm, + loadEd25519VerifierFromPublicKey, + VerifierKey, +} from '@hs/crypto'; +import type { PersistentEventBase } from '@hs/room'; + +interface KeyData { + server_name: string; + verify_keys: { + [keyId: string]: { + key: string; + }; + }; + old_verify_keys?: { + [keyId: string]: { + key: string; + expired_ts: number; + }; + }; +} + +// this is to not process the keyid string multiple times +type KeyId = { + alg: string; + ver: string; + id: string; +}; + +// low cost optimization in case of bad implementations +// ed25519 signatures in unpaddedbase64 are always 86 characters long (doing math here for future reference) +// 64 bytes in general, base64 => 21*3 = 63 + 1 padding "==" => 21 * 4 = 84 + 2 padding "==" (each char one byte) => 88 characters +// since in matrix it is unpadded base64, we remove the 2 padding chars => 86 characters +export const MAX_SIGNATURE_LENGTH_FOR_ED25519 = 86; + +export class SignatureVerificationService { + private get logger() { + return createLogger('SignatureVerificationService'); + } + private cachedKeys = new Map(); + + /** + * Implements SPEC: https://spec.matrix.org/v1.12/appendices/#checking-for-a-signature + * and part of https://spec.matrix.org/v1.12/server-server-api/#validating-hashes-and-signatures-on-received-events + * The event structure should be verifier by the time this method is utilized, thus justifying the use of PersistentEventBase. + */ + async verifyEventSignature(event: PersistentEventBase): Promise { + // SPEC: First the signature is checked. The event is redacted following the redaction algorithm + const { redactedEvent, origin } = event; + + if (!origin) { + throw new Error( + `Invalid event sender, unable to find origin part from it ${event.sender}`, + ); + } + + // 1. Checks if the signatures member of the object contains an entry with the name of the entity. If the entry is missing then the check fails. + const originSignature = redactedEvent.signatures?.[origin]; + if (!originSignature) { + throw new Error(`No signature found for origin ${origin}`); + } + + // 2. Removes any signing key identifiers from the entry with algorithms it doesn’t understand. If there are no signing key identifiers left then the check fails. + const signatureEntries = Object.entries(originSignature); + const validSignatureEntries = [] as Array<[KeyId, string /* signature */]>; + for (const [keyId, signature] of signatureEntries) { + const parts = keyId.split(':'); + if (parts.length < 2) { + this.logger.warn(`Invalid keyId format: ${keyId}`); + continue; // we discard this entry but we do not fail yet + } + + const algorithm = parts[0]; + const version = parts[1]; + + if (!isValidAlgorithm(algorithm)) { + this.logger.warn(`Unsupported algorithm: ${algorithm}`); + continue; // we discard this entry but we do not fail yet + } + + validSignatureEntries.push([ + { alg: algorithm, ver: version, id: keyId }, + signature as string, + ]); + } + if (validSignatureEntries.length === 0) { + throw new Error( + `No valid signature keys found for origin ${origin} with supported algorithms`, + ); + } + + // 3. Looks up verification keys for the remaining signing key identifiers either from a local cache or by consulting a trusted key server. If it cannot find a verification key then the check fails. + // one origin can sign with multiple keys - given how the spec AND the schema structures it. + // we do NOT need all though, one is enough, one that we can fetch first + let verifier: VerifierKey | undefined; + for (const [keyId] of validSignatureEntries) { + try { + verifier = await this.getSignatureVerifierForServer(origin, keyId); + break; // found one, should be enough + } catch (error) { + this.logger.warn( + `Failed to get verifier for ${origin} with keyId ${keyId.id}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + if (!verifier) { + throw new Error( + `No valid verification key found for origin ${origin} with supported algorithms`, + ); + } + + // 4. Decodes the base64 encoded signature bytes. If base64 decoding fails then the check fails. + // we needed to know which key to use to know which signature to decode. + const signatureEntry: string = originSignature[verifier.id]; + if (!signatureEntry) { + throw new Error( + `No signature entry found for keyId ${verifier.id} from origin ${origin}`, + ); + } + + if (signatureEntry.length !== MAX_SIGNATURE_LENGTH_FOR_ED25519) { + throw new Error( + `Invalid signature length for keyId ${verifier.id} from origin ${origin}, expected 86 got ${signatureEntry.length} characters`, + ); + } + + let signatureBytes: Uint8Array; + try { + signatureBytes = fromBase64ToBytes(signatureEntry); + } catch (error) { + throw new Error( + `Failed to decode base64 signature for keyId ${verifier.id} from origin ${origin}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // 5. Removes the signatures and unsigned members of the object. + const { signatures, unsigned, ...rest } = redactedEvent; + + // 6. Encodes the remainder of the JSON object using the Canonical JSON encoding. + const canonicalJson = encodeCanonicalJson(rest); + + // 7. Checks the signature bytes against the encoded object using the verification key. If this fails then the check fails. Otherwise the check succeeds. + await verifier.verify(canonicalJson, signatureBytes); + } + + // throws if no key + async getSignatureVerifierForServer( + serverName: string, + keyId: KeyId, + ): Promise { + const keyData = await this.getOrFetchPublicKey(serverName, keyId.id); + if (!keyData || !keyData.verify_keys[keyId.id]) { + throw new Error(`Public key not found for ${serverName}:${keyId.id}`); + } + + const publicKey = keyData.verify_keys[keyId.id].key; + + const verifier = await loadEd25519VerifierFromPublicKey( + fromBase64ToBytes(publicKey), + keyId.ver, + ); + + return verifier; + } + + /** + * Get public key from cache or fetch it from the server + */ + private async getOrFetchPublicKey( + serverName: string, + keyId: string, + ): Promise { + const cacheKey = `${serverName}:${keyId}`; + + if (this.cachedKeys.has(cacheKey)) { + return this.cachedKeys.get(cacheKey); + } + + try { + const response = await fetch( + `https://${serverName}/_matrix/key/v2/server`, + ); + + if (!response.ok) { + this.logger.error( + `Failed to fetch keys from ${serverName}: ${response.status}`, + ); + return null; + } + + const keyData = (await response.json()) as KeyData; + + this.cachedKeys.set(cacheKey, keyData); + + return keyData; + } catch (error: any) { + this.logger.error( + `Error fetching public key: ${error.message}`, + error.stack, + ); + return null; + } + } +} From 9ed8bc482eced7ea10e3d2e77ec5b33ee7a68191 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 8 Sep 2025 16:28:03 +0530 Subject: [PATCH 14/56] reduce lines --- .../services/signature-verification.service.spec.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/federation-sdk/src/services/signature-verification.service.spec.ts b/packages/federation-sdk/src/services/signature-verification.service.spec.ts index 6b53cd591..de66e5dae 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.spec.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.spec.ts @@ -15,11 +15,6 @@ const originServer = 'syn1.tunnel.dev.rocket.chat'; const keyId = 'ed25519:a_FAET'; -/* - * {"type":"m.room.create","state_key":"","content":{"room_version":"10","creator":"@debdut:rc1.tunnel.dev.rocket.chat"},"sender":"@debdut:rc1.tunnel.dev.rocket.chat","origin_server_ts":1753363422133,"origin":"rc1.tunnel.dev.rocket.chat","room_id":"!uUGiqDlq:rc1.tunnel.dev.rocket.chat","prev_events":[],"auth_events":[],"depth":0,"hashes":{"sha256":"n4Vml4VWf+qXqS0AtFD8WK3JYGQWQO4sNB8CXV3HarM"},"signatures":{"rc1.tunnel.dev.rocket.chat":{"ed25519:0":"m9ccld2sylkt4E5kn36BPyLiWL/wJFYE2vzkp62FGmxO2DQ66a+qMz5lq18+rkEjNxONREfTioJov3s6nHjhAA"}},"unsigned":{}} - * {"old_verify_keys":{},"server_name":"rc1.tunnel.dev.rocket.chat","signatures":{"rc1.tunnel.dev.rocket.chat":{"ed25519:0":"kNs1Wqt3MDNVZg1gQ+c0mXiiOmpCnR41siXdHes8wNvq0SQb5VCSa1Fz+LF6WN10fNyBoBp6ukc19bKQKreNCw"}},"valid_until_ts":1754130035648,"verify_keys":{"ed25519:0":{"key":"RL+t3F91NXGsUI9YZtKBRMETgUgfApqfeA3q8Go/Uo4"}}} - */ - // v10 const event = { auth_events: [ @@ -55,12 +50,6 @@ const event = { }, }; -/* - * {"event":{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$Ulggyo4m1OlI08Z0jJDVeceigjSZP9SdEFVoAn9mEh8","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs"],"content":{"avatar_url":null,"displayname":"debdut1","membership":"join"},"depth":10,"hashes":{"sha256":"6MnKSCFJy1fYf6ukILBEbqx2DkoaD1wRyKXhv689a0A"},"origin":"syn1.tunnel.dev.rocket.chat","origin_server_ts":1757328411218,"prev_events":["$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs"],"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","sender":"@debdut1:syn1.tunnel.dev.rocket.chat","state_key":"@debdut1:syn1.tunnel.dev.rocket.chat","type":"m.room.member","signatures":{"syn1.tunnel.dev.rocket.chat":{"ed25519:a_FAET":"eJlvqxPWPe3u+BM4wOwID9YBlh/ZfVVxGYyA5WgpNs5Fe1+c36qrvCKHuXGGjfQoZFrHmZ3/GJw2pv5EvxCZAA"}},"unsigned":{"age":1,"replaces_state":"$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs","prev_content":{"displayname":"debdut1","membership":"invite"},"prev_sender":"@debdut:syn2.tunnel.dev.rocket.chat"}},"state":[{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$sOuxoPjLRgDVgtgKrmBxdVFRc9hfODoXHvwfznrBJvk"],"type":"m.room.guest_access","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"guest_access":"can_join"},"depth":6,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362604,"hashes":{"sha256":"TrP386yOPRnjhRF0wIrR+558ZPbmv3dVP66rMOrqapA"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"kDuj6bQcTT7TBAkSFFjw/qwTg7afo2VFyRDma1O3HVFt00wUkgmB2yHqOehmD64uwsirCdYgy6v+geeR2z3xBw"}},"unsigned":{"age":49854}},{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$_wPeMHrY4MrlDX21jtu0z6jFBz37G8akqmZDhORIqco"],"type":"m.room.name","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"name":"aaaaaaaa"},"depth":8,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362604,"hashes":{"sha256":"RnymDqNP7NRKoaqEtVGBW2DV3tevZ/jCZY7AcdbZynM"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"+D1Ir0W1UY5nd+rIQlHgAedmTciPW6CFPp4bbARYhiB5upIERuE46zSUqpkjIPYKV2tC5SI4EVONQwtEmPRBBQ"}},"unsigned":{"age":49854}},{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$Ulggyo4m1OlI08Z0jJDVeceigjSZP9SdEFVoAn9mEh8","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$jjtu7bXR1KQ5pyHjtu71l4Y0HsDRxfpyA7XZ3Y7K7Hg"],"type":"m.room.member","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"displayname":"debdut1","membership":"invite"},"depth":9,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"@debdut1:syn1.tunnel.dev.rocket.chat","origin_server_ts":1757328402120,"hashes":{"sha256":"o5Ph/P2Lybk2fU1NUSsAYQi36SOLdvClaBWBTa5KUL8"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"Uj9SNp+B95gi29U+BnIaQA60B/kY8ZnbMfaUGog4SU7OEPLhTNh7SOcki1TBkdl6OlRCkGypKVJXyx7b/J7MAA"},"syn1.tunnel.dev.rocket.chat":{"ed25519:a_FAET":"plQW1ZLVhu6EIGCa2hHBv4GWyjOzXBSQ+hMyj+DlX0BHMD3+3GsmJbiYySmh2kmMEP3IsI4YS0D9MquN6VHsDg"}},"unsigned":{"invite_room_state":[{"type":"m.room.join_rules","state_key":"","content":{"join_rule":"invite"},"sender":"@debdut:syn2.tunnel.dev.rocket.chat"},{"type":"m.room.create","state_key":"","content":{"room_version":"10","creator":"@debdut:syn2.tunnel.dev.rocket.chat"},"sender":"@debdut:syn2.tunnel.dev.rocket.chat"},{"type":"m.room.encryption","state_key":"","content":{"algorithm":"m.megolm.v1.aes-sha2"},"sender":"@debdut:syn2.tunnel.dev.rocket.chat"},{"type":"m.room.name","state_key":"","content":{"name":"aaaaaaaa"},"sender":"@debdut:syn2.tunnel.dev.rocket.chat"},{"type":"m.room.member","state_key":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"displayname":"debdut","membership":"join"},"sender":"@debdut:syn2.tunnel.dev.rocket.chat"}],"age":10338}},{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA"],"type":"m.room.join_rules","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"join_rule":"invite"},"depth":4,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362603,"hashes":{"sha256":"ak/Qv/mMfgvK/XdJGoc9Poby2BUtriHMgRGZcbYspMU"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"hYm15xFhJQfG90noqFPKnV7ZLIqENBM5ICMAZZS8GQnXvWOGdGEOkeKj4uXIEEsxxxad/Sc1xce5vV+raTuQCA"}},"unsigned":{"age":49855}},{"auth_events":["$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"type":"m.room.power_levels","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"users":{"@debdut:syn2.tunnel.dev.rocket.chat":100},"users_default":0,"events":{"m.room.name":50,"m.room.avatar":50,"m.room.power_levels":100,"m.room.history_visibility":100,"m.room.canonical_alias":50,"m.room.tombstone":100,"m.room.server_acl":100,"m.room.encryption":100,"org.matrix.msc3401.call.member":0,"org.matrix.msc3401.call":100},"events_default":0,"state_default":50,"ban":50,"kick":50,"redact":50,"invite":0,"historical":100},"depth":3,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362598,"hashes":{"sha256":"N8XBL3oJrwU/5SP4Uqa+jbIEqsts+cgFs04bnB0zMFM"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"zJ3u25+gz99P+WmRBYgNOtqqIBb1jngMALLxqljlrIG/ifDLy1UyXtwm/g5a6r9qTLE/taql99DCPY4kXH3nAA"}},"unsigned":{"age":49860}},{"auth_events":[],"prev_events":[],"type":"m.room.create","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"room_version":"10","creator":"@debdut:syn2.tunnel.dev.rocket.chat"},"depth":1,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362536,"hashes":{"sha256":"Wqul5TbaOj83JuQkKj/CTlYeaUtocHePDFIVqBzJJS4"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"4uOTMonGEXg74DOOcWgkpRMzmkPg/cQVp9b16jYPPFOgMk8Tr5lOWDa/jAUlVEALj2kYHRblk3UacMaQzvPaDw"}},"unsigned":{"age":49922}},{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$Ulggyo4m1OlI08Z0jJDVeceigjSZP9SdEFVoAn9mEh8"],"type":"m.room.history_visibility","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"history_visibility":"shared"},"depth":5,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362604,"hashes":{"sha256":"+UW9TzftG+Q5aM46uLOiOiMlOSDdvMVDMSf/EZonvR4"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"oNyD4B2gfy8+e6r+ZDSAb807gEhx40d6exLrVWmsn4s+N6qZ28i41DWfttpZap+GPwVDA4mpCZWkZet0rBBxDw"}},"unsigned":{"age":49854}},{"auth_events":["$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA","$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg","$ZDZHSsYiyL4LlRhIIUKVZnlDcVmn8NonWySS0DbvzFQ"],"prev_events":["$-1FmjaDcaLllAM91flimqEJ9xqCw5zBv3kdMUcr7Etk"],"type":"m.room.encryption","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"algorithm":"m.megolm.v1.aes-sha2"},"depth":7,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"","origin_server_ts":1757328362604,"hashes":{"sha256":"L0FLRolYQPHm4AuxTLHzysz6Nmh8tmWuERKYUHO5yeM"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"qJyKdg+J1ER4Et1hrqiQNc3mM3VCM8Q6DNnlSu/9lypRW5VRZy3HvPKukvgoyQr1/ewc+0x0EoeT0ca5wrxCDA"}},"unsigned":{"age":49854}}],"auth_chain":[{"auth_events":["$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg"],"prev_events":["$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg"],"type":"m.room.member","sender":"@debdut:syn2.tunnel.dev.rocket.chat","content":{"displayname":"debdut","membership":"join"},"depth":2,"room_id":"!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat","state_key":"@debdut:syn2.tunnel.dev.rocket.chat","origin_server_ts":1757328362573,"hashes":{"sha256":"OQ9F8BCBAjylOXjPPt6ZZGfaVUe0aInmPLoaAS9PY8E"},"signatures":{"syn2.tunnel.dev.rocket.chat":{"ed25519:a_ZsSJ":"wPLUukVrilo2byP4sptw2gJCXPPSuLrv4pQtSPvazx9aw0Fs/tZNVANaum1lt4adigGn5Zzlj6gnA4k65z3hAQ"}},"unsigned":{"age":49885}}],"members_omitted":true,"servers_in_room":["syn2.tunnel.dev.rocket.chat"]} - * - * {"old_verify_keys":{},"server_name":"syn1.tunnel.dev.rocket.chat","signatures":{"syn1.tunnel.dev.rocket.chat":{"ed25519:a_FAET":"32jfhYKQGENYAByGWZlMPcqgLcGJCoU9RyxOz4TGrmGbTwmbBi8BGbgNJHH8DmWuyoD6FnZ4yI5YBZTJqPjQAA"}},"valid_until_ts":1757414678669,"verify_keys":{"ed25519:a_FAET":{"key":"kryovKVnhHESOdWuZ05ViNotRMVdEh/mG2yJ0npLzEo"}}} - */ - describe('SignatureVerificationService', () => { let service: SignatureVerificationService; From b04ff5d9816769f9dd58e462dbdac95220717017 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 8 Sep 2025 17:39:27 +0530 Subject: [PATCH 15/56] now using in the services, more incoming --- .../src/services/config.service.ts | 6 +- .../services/federation-request.service.ts | 158 ++++++++++-------- .../src/services/federation.service.ts | 1 - .../src/services/server.service.ts | 42 ++--- .../signature-verification.service.ts | 41 ++++- .../src/services/state.service.ts | 16 +- 6 files changed, 152 insertions(+), 112 deletions(-) diff --git a/packages/federation-sdk/src/services/config.service.ts b/packages/federation-sdk/src/services/config.service.ts index 62cc9d2f3..b79ca923a 100644 --- a/packages/federation-sdk/src/services/config.service.ts +++ b/packages/federation-sdk/src/services/config.service.ts @@ -16,7 +16,6 @@ export interface AppConfig { keyRefreshInterval: number; signingKey?: string; timeout?: number; - signingKeyPath?: string; database: { uri: string; name: string; @@ -49,7 +48,6 @@ export const AppConfigSchema = z.object({ .min(1, 'Key refresh interval must be at least 1'), signingKey: z.string().optional(), timeout: z.number().optional(), - signingKeyPath: z.string(), database: z.object({ uri: z.string().min(1, 'Database URI is required'), name: z.string().min(1, 'Database name is required'), @@ -126,6 +124,10 @@ export class ConfigService { } async getSigningKey() { + if (this.signer) { + return this.signer; + } + // If config contains a signing key, use it if (!this.config.signingKey) { throw new Error('Signing key is not configured'); diff --git a/packages/federation-sdk/src/services/federation-request.service.ts b/packages/federation-sdk/src/services/federation-request.service.ts index c528b6002..5925c936a 100644 --- a/packages/federation-sdk/src/services/federation-request.service.ts +++ b/packages/federation-sdk/src/services/federation-request.service.ts @@ -12,10 +12,17 @@ import { fetch, signJson, } from '@rocket.chat/federation-core'; +import type { SigningKey } from '@hs/core'; +import { authorizationHeaders, computeAndMergeHash } from '@hs/core'; +import { extractURIfromURL } from '@hs/core'; +import { EncryptionValidAlgorithm } from '@hs/core'; +import { createLogger } from '@hs/core'; +import { fetch } from '@hs/core'; import { singleton } from 'tsyringe'; import * as nacl from 'tweetnacl'; import { getHomeserverFinalAddress } from '../server-discovery/discovery'; import { ConfigService } from './config.service'; +import { signJson } from '@hs/crypto'; interface SignedRequest { method: string; @@ -33,92 +40,103 @@ export class FederationRequestService { constructor(private readonly configService: ConfigService) {} + // Implements SPEC: https://spec.matrix.org/v1.12/server-server-api/#request-authentication async makeSignedRequest({ method, domain, uri, body, queryString, - }: SignedRequest): Promise> { - try { - const serverName = this.configService.serverName; - const signingKeyBase64 = await this.configService.getSigningKeyBase64(); - const signingKeyId = await this.configService.getSigningKeyId(); - const privateKeyBytes = Buffer.from(signingKeyBase64, 'base64'); - const keyPair = nacl.sign.keyPair.fromSecretKey(privateKeyBytes); - - const signingKey: SigningKey = { - algorithm: EncryptionValidAlgorithm.ed25519, - version: signingKeyId.split(':')[1] || '1', - privateKey: keyPair.secretKey, - publicKey: keyPair.publicKey, - sign: async (data: Uint8Array) => - nacl.sign.detached(data, keyPair.secretKey), - }; - - const [address, discoveryHeaders] = await getHomeserverFinalAddress( - domain, - this.logger, - ); + }: SignedRequest): Promise { + const signer = await this.configService.getSigningKey(); - const url = new URL(`${address}${uri}`); - if (queryString) { - url.search = queryString; - } - - this.logger.debug(`Making ${method} request to ${url.toString()}`); + const [address, discoveryHeaders] = await getHomeserverFinalAddress( + domain, + this.logger, + ); - let signedBody: Record | undefined; - if (body) { - signedBody = await signJson( - body.hashes ? body : computeAndMergeHash({ ...body, signatures: {} }), - signingKey, - serverName, - ); - } + const origin = this.configService.serverName; - const auth = await authorizationHeaders( - serverName, - signingKey, - domain, - method, - extractURIfromURL(url), - signedBody, - ); + const url = new URL(`${address}${uri}`); + if (queryString) { + url.search = queryString; + } - const headers = { - Authorization: auth, - ...discoveryHeaders, - ...(signedBody && { 'Content-Type': 'application/json' }), + if (body) { + const signature = await signJson(body, signer); + body.signatures = { + [origin]: { [signer.id]: signature }, }; + } - const response = await fetch(url, { - method, - ...(signedBody && { body: JSON.stringify(signedBody) }), - headers, - }); - - if (!response.ok) { - const errorText = await response.text(); - let errorDetail = errorText; - try { - errorDetail = JSON.stringify(JSON.parse(errorText || '')); - } catch { - /* use raw text if parsing fails */ + /* + { + "method": "POST", + "uri": "/target", + "origin": "origin.hs.example.com", + "destination": "destination.hs.example.com", + "content": , + "signatures": { + "origin.hs.example.com": { + "ed25519:key1": "ABCDEF..." + } } - throw new Error( - `Federation request failed: ${response.status} ${errorDetail}`, - ); } + */ + // build the auth request + const request = { + method, + uri: url.pathname + url.search, + origin, + destination: domain, + ...(body && { content: body }), + }; + + const requestSignature = await signJson(request, signer); + + // authorization_headers.append(bytes( + // "X-Matrix origin=\"%s\",destination=\"%s\",key=\"%s\",sig=\"%s\"" % ( + // origin_name, destination_name, key, sig, + // ) + // )) + const authorizationHeaderValue = `X-Matrix origin=${origin},destination=${domain},key=${signer.id},sig=${requestSignature}`; + + const headers = { + Authorization: authorizationHeaderValue, + ...discoveryHeaders, + }; + + // TODO: make logging take a function for object to avoid unnecessary computation when log level is high + this.logger.debug( + { + method, + body: body, + headers, + url: url.toString(), + }, + 'making http request', + ); - return response; - } catch (err) { - this.logger.error({ - msg: 'Error making signed federation request', - err, - }); - throw err; + const response = await fetch(url, { + method, + ...(body && { body: JSON.stringify(body) }), + headers, + }); + + if (!response.ok) { + const errorText = await response.text(); + let errorDetail = errorText; + try { + errorDetail = JSON.stringify(JSON.parse(errorText)); + } catch { + /* use raw text if parsing fails */ + } + throw new Error( + `Federation request failed: ${response.status} ${errorDetail}`, + ); } + + return response.json(); } async request( diff --git a/packages/federation-sdk/src/services/federation.service.ts b/packages/federation-sdk/src/services/federation.service.ts index f6f1a7b80..fbcc432e6 100644 --- a/packages/federation-sdk/src/services/federation.service.ts +++ b/packages/federation-sdk/src/services/federation.service.ts @@ -27,7 +27,6 @@ export class FederationService { constructor( private readonly configService: ConfigService, - private readonly requestService: FederationRequestService, private readonly stateService: StateService, diff --git a/packages/federation-sdk/src/services/server.service.ts b/packages/federation-sdk/src/services/server.service.ts index ea9b0d118..a2ed756a2 100644 --- a/packages/federation-sdk/src/services/server.service.ts +++ b/packages/federation-sdk/src/services/server.service.ts @@ -1,9 +1,4 @@ -import { - type SigningKey, - getPublicKeyFromRemoteServer, - signJson, - toUnpaddedBase64, -} from '@rocket.chat/federation-core'; +import { toUnpaddedBase64, signJson } from '@hs/crypto'; import { singleton } from 'tsyringe'; import { ServerRepository } from '../repositories/server.repository'; import { ConfigService } from './config.service'; @@ -58,34 +53,31 @@ export class ServerService { } async getSignedServerKey() { - const signingKeys = await this.configService.getSigningKey(); + const signer = await this.configService.getSigningKey(); - const keys = Object.fromEntries( - signingKeys.map((signingKey: SigningKey) => [ - `${signingKey.algorithm}:${signingKey.version}`, - { - key: toUnpaddedBase64(signingKey.publicKey), - }, - ]), - ); + const keys = { + [signer.id]: { + key: toUnpaddedBase64(signer.publicKey), + }, + }; - const baseResponse = { + const response = { old_verify_keys: {}, server_name: this.configService.serverName, signatures: {}, + // TODO: what should this actually be and how to handle the expiration valid_until_ts: new Date().getTime() + 60 * 60 * 24 * 1000, // 1 day verify_keys: keys, }; - let signedResponse = baseResponse; - for (const key of signingKeys) { - signedResponse = await signJson( - signedResponse, - key, - this.configService.serverName, - ); - } + const responseSignature = await signJson(response, signer); + + response.signatures = { + [this.configService.serverName]: { + [signer.id]: responseSignature, + }, + }; - return signedResponse; + return response; } } diff --git a/packages/federation-sdk/src/services/signature-verification.service.ts b/packages/federation-sdk/src/services/signature-verification.service.ts index ef39f5c3b..3f125de0a 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.ts @@ -36,6 +36,23 @@ type KeyId = { // since in matrix it is unpadded base64, we remove the 2 padding chars => 86 characters export const MAX_SIGNATURE_LENGTH_FOR_ED25519 = 86; +export type FederationRequest = + | { + method: 'GET' | 'DELETE'; + uri: `/${string}`; + origin: string; + destination: string; + signature: Record>; + } + | { + method: 'PUT' | 'POST' | 'DELETE'; + uri: `/${string}`; + origin: string; + destination: string; + content: Content; + signature: Record>; + }; + export class SignatureVerificationService { private get logger() { return createLogger('SignatureVerificationService'); @@ -43,8 +60,7 @@ export class SignatureVerificationService { private cachedKeys = new Map(); /** - * Implements SPEC: https://spec.matrix.org/v1.12/appendices/#checking-for-a-signature - * and part of https://spec.matrix.org/v1.12/server-server-api/#validating-hashes-and-signatures-on-received-events + * Implements part of SPEC: https://spec.matrix.org/v1.12/server-server-api/#validating-hashes-and-signatures-on-received-events * The event structure should be verifier by the time this method is utilized, thus justifying the use of PersistentEventBase. */ async verifyEventSignature(event: PersistentEventBase): Promise { @@ -57,8 +73,23 @@ export class SignatureVerificationService { ); } + const { unsigned, ...toCheck } = redactedEvent; + + await this.verifySignature(toCheck, origin); + } + + async verifyRequestSignature() {} + + /** + * Implements SPEC: https://spec.matrix.org/v1.12/appendices/#checking-for-a-signature + */ + async verifySignature< + T extends { + signatures: Record>; + }, + >(data: T, origin: string) { // 1. Checks if the signatures member of the object contains an entry with the name of the entity. If the entry is missing then the check fails. - const originSignature = redactedEvent.signatures?.[origin]; + const originSignature = data.signatures?.[origin]; if (!originSignature) { throw new Error(`No signature found for origin ${origin}`); } @@ -124,7 +155,7 @@ export class SignatureVerificationService { if (signatureEntry.length !== MAX_SIGNATURE_LENGTH_FOR_ED25519) { throw new Error( - `Invalid signature length for keyId ${verifier.id} from origin ${origin}, expected 86 got ${signatureEntry.length} characters`, + `Invalid signature length for keyId ${verifier.id} from origin ${origin}, expected ${MAX_SIGNATURE_LENGTH_FOR_ED25519} got ${signatureEntry.length} characters`, ); } @@ -138,7 +169,7 @@ export class SignatureVerificationService { } // 5. Removes the signatures and unsigned members of the object. - const { signatures, unsigned, ...rest } = redactedEvent; + const { signatures, ...rest } = data; // 6. Encodes the remainder of the JSON object using the Canonical JSON encoding. const canonicalJson = encodeCanonicalJson(rest); diff --git a/packages/federation-sdk/src/services/state.service.ts b/packages/federation-sdk/src/services/state.service.ts index 9492b3788..bc3c73966 100644 --- a/packages/federation-sdk/src/services/state.service.ts +++ b/packages/federation-sdk/src/services/state.service.ts @@ -1,4 +1,4 @@ -import { createLogger, signEvent } from '@rocket.chat/federation-core'; +import { createLogger, signJson } from '@rocket.chat/federation-crypto'; import { type EventID, type EventStore, @@ -326,19 +326,17 @@ export class StateService { const origin = this.configService.serverName; - const result = await signEvent( + const { signatures, ...toSign } = event.redactedEvent; + + const signature = await signJson( // Before signing the event, the content hash of the event is calculated as described below. The hash is encoded using Unpadded Base64 and stored in the event object, in a hashes object, under a sha256 key. // ^^ is done already through redactedEvent fgetter // The event object is then redacted, following the redaction algorithm. Finally it is signed as described in Signing JSON, using the server’s signing key (see also Retrieving server keys). - event.redactedEvent as any, - signingKey[0], - origin, - false, // already passed through redactedEvent, hash is already part of this + toSign, + signingKey, ); - const keyId = `${signingKey[0].algorithm}:${signingKey[0].version}`; - - event.addSignature(origin, keyId, result.signatures[origin][keyId]); + event.addSignature(origin, signingKey.id, signature); return event; } From c0f71e1e8145d4564d440a8f4c276be8e5e5e563 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 8 Sep 2025 18:17:27 +0530 Subject: [PATCH 16/56] fix tests for request service --- .../federation-request.service.spec.ts | 350 ++++-------------- .../services/federation-request.service.ts | 9 +- 2 files changed, 79 insertions(+), 280 deletions(-) diff --git a/packages/federation-sdk/src/services/federation-request.service.spec.ts b/packages/federation-sdk/src/services/federation-request.service.spec.ts index 4af81b0bb..a442f82cd 100644 --- a/packages/federation-sdk/src/services/federation-request.service.spec.ts +++ b/packages/federation-sdk/src/services/federation-request.service.spec.ts @@ -12,24 +12,23 @@ import * as core from '@rocket.chat/federation-core'; import * as nacl from 'tweetnacl'; import { ConfigService } from './config.service'; import { FederationRequestService } from './federation-request.service'; +import { loadEd25519SignerFromSeed, fromBase64ToBytes } from '@hs/crypto'; + +const signingKeyContent = + 'ed25519 a_FAET FC6cwY3DNmHo3B7GRugaHNyXz+TkBRVx8RvQH0kSZ04'; + +const origin = 'syn1.tunnel.dev.rocket.chat'; + +const destination = 'syn2.tunnel.dev.rocket.chat'; describe('FederationRequestService', async () => { let service: FederationRequestService; let configService: ConfigService; - const mockServerName = 'example.com'; - const mockSigningKey = 'aGVsbG93b3JsZA=='; - const mockSigningKeyId = 'ed25519:1'; - - const mockKeyPair = { - publicKey: new Uint8Array([1, 2, 3]), - secretKey: new Uint8Array([4, 5, 6]), - }; - const mockDiscoveryResult = [ - 'https://target.example.com:443' as const, + `https://${destination}:443` as const, { - Host: 'target.example.com', + Host: destination, }, ]; @@ -65,35 +64,13 @@ describe('FederationRequestService', async () => { })); }); - const mockSignature = new Uint8Array([7, 8, 9]); - - const mockSignedJson = { - content: 'test', - signatures: { - 'example.com': { - 'ed25519:1': 'abcdef', - }, - }, - }; - - const mockAuthHeaders = - 'X-Matrix origin="example.com",destination="target.example.com",key="ed25519:1",sig="xyz123"'; - beforeEach(() => { - spyOn(nacl.sign.keyPair, 'fromSecretKey').mockReturnValue(mockKeyPair); - spyOn(nacl.sign, 'detached').mockReturnValue(mockSignature); - - spyOn(core, 'extractURIfromURL').mockReturnValue('/test/path?query=value'); - spyOn(core, 'authorizationHeaders').mockResolvedValue(mockAuthHeaders); - spyOn(core, 'signJson').mockResolvedValue(mockSignedJson); - spyOn(core, 'computeAndMergeHash').mockImplementation( - (obj: unknown) => obj, - ); - configService = { - serverName: mockServerName, - getSigningKeyBase64: async () => mockSigningKey, - getSigningKeyId: async () => mockSigningKeyId, + serverName: origin, + getSigningKey: async () => { + const [, version, seed] = signingKeyContent.split(' '); + return loadEd25519SignerFromSeed(fromBase64ToBytes(seed), version); + }, } as ConfigService; service = new FederationRequestService(configService); @@ -104,260 +81,89 @@ describe('FederationRequestService', async () => { }); describe('makeSignedRequest', () => { - it('should make a successful signed request without body', async () => { + it('should make a successful signed request with a body', async () => { + const transactionBody = { + edus: [ + { + content: { + push: [ + { + last_active_ago: 561472, + presence: 'unavailable', + user_id: '@debdut1:syn1.tunnel.dev.rocket.chat', + }, + ], + }, + edu_type: 'm.presence', + }, + ], + origin: 'syn1.tunnel.dev.rocket.chat', + origin_server_ts: 1757329414731, + pdus: [], + }; + + // PUT /_matrix/federation/v1/send/1757328278684 HTTP/1.1 + + const uri = '/_matrix/federation/v1/send/1757328278684'; + + const method = 'PUT'; const fetchSpy = spyOn(core, 'fetch'); - const result = await service.makeSignedRequest({ - method: 'GET', - domain: 'target.example.com', - uri: '/test/path', + await service.makeSignedRequest({ + method, + // Host: syn2.tunnel.dev.rocket.chat + domain: 'syn2.tunnel.dev.rocket.chat', + uri, + body: transactionBody, }); - expect(configService.serverName).toBe(mockServerName); - expect(await configService.getSigningKeyBase64()).toBe(mockSigningKey); - expect(await configService.getSigningKeyId()).toBe(mockSigningKeyId); - expect(configService.serverName).toBe(mockServerName); - expect(await configService.getSigningKeyBase64()).toBe(mockSigningKey); - expect(await configService.getSigningKeyId()).toBe(mockSigningKeyId); - - expect(nacl.sign.keyPair.fromSecretKey).toHaveBeenCalled(); - expect(fetchSpy).toHaveBeenCalledWith( - new URL('https://target.example.com/test/path'), + new URL(`https://${destination}${uri}`), expect.objectContaining({ - method: 'GET', + method: 'PUT', headers: expect.objectContaining({ - Authorization: mockAuthHeaders, - Host: 'target.example.com', + // Authorization: X-Matrix origin="syn1.tunnel.dev.rocket.chat",key="ed25519:a_FAET",sig="+MRd0eKdc/3T7mS7ZR+ltpOiN7RBXgfxTWWYLejy5gBRXG717aXHPCDm044D10kgqQvs2HqR3MdPEIx+2a0nDg",destination="syn2.tunnel.dev.rocket.chat" + Authorization: + 'X-Matrix origin="syn1.tunnel.dev.rocket.chat",destination="syn2.tunnel.dev.rocket.chat",key="ed25519:a_FAET",sig="+MRd0eKdc/3T7mS7ZR+ltpOiN7RBXgfxTWWYLejy5gBRXG717aXHPCDm044D10kgqQvs2HqR3MdPEIx+2a0nDg"', + Host: destination, }), }), ); - - expect(result.ok).toBe(true); - expect(result.status).toBe(200); - expect(await result.json()).toEqual({ result: 'success' }); }); - it('should make a successful signed request with body', async () => { - const fetchSpy = spyOn(core, 'fetch'); + it('should make a successful signed GET request', async () => { + /* + GET /_matrix/federation/v1/make_join/%21VoUasOLSpcdtRbGHdT%3Asyn2.tunnel.dev.rocket.chat/%40debdut1%3Asyn1.tunnel.dev.rocket.chat?ver=1&ver=2&ver=3&ver=4&ver=5&ver=6&ver=7&ver=8&ver=9&ver=10&ver=11&ver=org.matrix.msc3757.10&ver=org.matrix.msc3757.11 HTTP/1.1 + Host: syn2.tunnel.dev.rocket.chat + User-Agent: Synapse/1.132.0 + Authorization: X-Matrix origin="syn1.tunnel.dev.rocket.chat",key="ed25519:a_FAET",sig="PNSix5GF9IquSmMOj+yx6rPDEZwcI1KrAQ6TzspAQyrwapQuFYXfhQmxoxKA1X7PUhUGSmQZUWrO4VInIpwwCA",destination="syn2.tunnel.dev.rocket.chat" + */ + const uri = + '/_matrix/federation/v1/make_join/%21VoUasOLSpcdtRbGHdT%3Asyn2.tunnel.dev.rocket.chat/%40debdut1%3Asyn1.tunnel.dev.rocket.chat'; + const method = 'GET'; + const queryString = + 'ver=1&ver=2&ver=3&ver=4&ver=5&ver=6&ver=7&ver=8&ver=9&ver=10&ver=11&ver=org.matrix.msc3757.10&ver=org.matrix.msc3757.11'; - const mockBody = { key: 'value' }; + const fetchSpy = spyOn(core, 'fetch'); - const result = await service.makeSignedRequest({ - method: 'POST', - domain: 'target.example.com', - uri: '/test/path', - body: mockBody, + await service.makeSignedRequest({ + method, + domain: destination, + uri, + queryString, }); - expect(core.signJson).toHaveBeenCalledWith( - expect.objectContaining({ key: 'value', signatures: {} }), - expect.any(Object), - mockServerName, - ); - - expect(core.authorizationHeaders).toHaveBeenCalledWith( - mockServerName, - expect.any(Object), - 'target.example.com', - 'POST', - '/test/path?query=value', - mockSignedJson, - ); - expect(fetchSpy).toHaveBeenCalledWith( - new URL('https://target.example.com/test/path'), + new URL(`https://${destination}${uri}?${queryString}`), expect.objectContaining({ - method: 'POST', - body: JSON.stringify(mockSignedJson), + method: 'GET', + headers: expect.objectContaining({ + Authorization: + 'X-Matrix origin="syn1.tunnel.dev.rocket.chat",destination="syn2.tunnel.dev.rocket.chat",key="ed25519:a_FAET",sig="PNSix5GF9IquSmMOj+yx6rPDEZwcI1KrAQ6TzspAQyrwapQuFYXfhQmxoxKA1X7PUhUGSmQZUWrO4VInIpwwCA"', + Host: destination, + }), }), ); - - expect(result.ok).toBe(true); - expect(result.status).toBe(200); - expect(await result.json()).toEqual({ result: 'success' }); - }); - - it('should make a signed request with query parameters', async () => { - const fetchSpy = spyOn(core, 'fetch'); - - const result = await service.makeSignedRequest({ - method: 'GET', - domain: 'target.example.com', - uri: '/test/path', - queryString: 'param1=value1¶m2=value2', - }); - - expect(fetchSpy).toHaveBeenCalledWith( - new URL( - 'https://target.example.com/test/path?param1=value1¶m2=value2', - ), - expect.any(Object), - ); - - expect(result.ok).toBe(true); - expect(result.status).toBe(200); - expect(await result.json()).toEqual({ result: 'success' }); - }); - - it('should handle fetch errors properly', async () => { - globalThis.fetch = Object.assign( - async () => { - return { - ok: false, - status: 404, - text: async () => 'Not Found', - multipart: async () => null, - } as Response; - }, - { preconnect: () => {} }, - ) as typeof fetch; - - try { - await service.makeSignedRequest({ - method: 'GET', - domain: 'target.example.com', - uri: '/test/path', - }); - } catch (error: unknown) { - if (error instanceof Error) { - expect(error.message).toContain( - 'Federation request failed: 404 Not Found', - ); - } else { - throw error; - } - } - }); - - it('should handle JSON error responses properly', async () => { - globalThis.fetch = Object.assign( - async () => { - return { - ok: false, - status: 400, - text: async () => - '{"error":"Bad Request","code":"M_INVALID_PARAM"}', - multipart: async () => null, - } as Response; - }, - { preconnect: () => {} }, - ) as typeof fetch; - - try { - await service.makeSignedRequest({ - method: 'GET', - domain: 'target.example.com', - uri: '/test/path', - }); - } catch (error: unknown) { - if (error instanceof Error) { - expect(error.message).toContain( - 'Federation request failed: 400 {"error":"Bad Request","code":"M_INVALID_PARAM"}', - ); - } else { - throw error; - } - } - }); - - it('should handle network errors properly', async () => { - globalThis.fetch = Object.assign( - async () => { - throw new Error('Network Error'); - }, - { preconnect: () => {} }, - ) as typeof fetch; - - try { - await service.makeSignedRequest({ - method: 'GET', - domain: 'target.example.com', - uri: '/test/path', - }); - } catch (error: unknown) { - if (error instanceof Error) { - expect(error.message).toBe('Network Error'); - } else { - throw error; - } - } - }); - }); - - describe('convenience methods', () => { - it('should call makeSignedRequest with correct parameters for GET', async () => { - const makeSignedRequestSpy = spyOn( - service, - 'makeSignedRequest', - ).mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ result: 'success' }), - text: async () => '{"result":"success"}', - multipart: async () => null, - }); - - await service.get('target.example.com', '/api/resource', { - filter: 'active', - }); - - expect(makeSignedRequestSpy).toHaveBeenCalledWith({ - method: 'GET', - domain: 'target.example.com', - uri: '/api/resource', - queryString: 'filter=active', - }); - }); - - it('should call makeSignedRequest with correct parameters for POST', async () => { - const makeSignedRequestSpy = spyOn( - service, - 'makeSignedRequest', - ).mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ result: 'success' }), - text: async () => '{"result":"success"}', - multipart: async () => null, - }); - - const body = { data: 'example' }; - await service.post('target.example.com', '/api/resource', body, { - version: '1', - }); - - expect(makeSignedRequestSpy).toHaveBeenCalledWith({ - method: 'POST', - domain: 'target.example.com', - uri: '/api/resource', - body, - queryString: 'version=1', - }); - }); - - it('should call makeSignedRequest with correct parameters for PUT', async () => { - const makeSignedRequestSpy = spyOn( - service, - 'makeSignedRequest', - ).mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ result: 'success' }), - text: async () => '{"result":"success"}', - multipart: async () => null, - }); - - const body = { data: 'updated' }; - await service.put('target.example.com', '/api/resource/123', body); - - expect(makeSignedRequestSpy).toHaveBeenCalledWith({ - method: 'PUT', - domain: 'target.example.com', - uri: '/api/resource/123', - body, - queryString: '', - }); }); }); diff --git a/packages/federation-sdk/src/services/federation-request.service.ts b/packages/federation-sdk/src/services/federation-request.service.ts index 5925c936a..43b52caba 100644 --- a/packages/federation-sdk/src/services/federation-request.service.ts +++ b/packages/federation-sdk/src/services/federation-request.service.ts @@ -62,13 +62,6 @@ export class FederationRequestService { url.search = queryString; } - if (body) { - const signature = await signJson(body, signer); - body.signatures = { - [origin]: { [signer.id]: signature }, - }; - } - /* { "method": "POST", @@ -99,7 +92,7 @@ export class FederationRequestService { // origin_name, destination_name, key, sig, // ) // )) - const authorizationHeaderValue = `X-Matrix origin=${origin},destination=${domain},key=${signer.id},sig=${requestSignature}`; + const authorizationHeaderValue = `X-Matrix origin="${origin}",destination="${domain}",key="${signer.id}",sig="${requestSignature}"`; const headers = { Authorization: authorizationHeaderValue, From 92512b5aa011c30217d84dec56352c5a4efc4f32 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 15 Sep 2025 09:09:27 +0530 Subject: [PATCH 17/56] ... --- packages/federation-sdk/src/services/server.service.ts | 8 +++++--- packages/federation-sdk/src/services/state.service.ts | 2 +- .../src/controllers/federation/profiles.controller.ts | 4 ++-- packages/homeserver/src/homeserver.module.ts | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/federation-sdk/src/services/server.service.ts b/packages/federation-sdk/src/services/server.service.ts index a2ed756a2..a409dffb9 100644 --- a/packages/federation-sdk/src/services/server.service.ts +++ b/packages/federation-sdk/src/services/server.service.ts @@ -64,7 +64,6 @@ export class ServerService { const response = { old_verify_keys: {}, server_name: this.configService.serverName, - signatures: {}, // TODO: what should this actually be and how to handle the expiration valid_until_ts: new Date().getTime() + 60 * 60 * 24 * 1000, // 1 day verify_keys: keys, @@ -72,12 +71,15 @@ export class ServerService { const responseSignature = await signJson(response, signer); - response.signatures = { + const signatures = { [this.configService.serverName]: { [signer.id]: responseSignature, }, }; - return response; + return { + ...response, + signatures, + }; } } diff --git a/packages/federation-sdk/src/services/state.service.ts b/packages/federation-sdk/src/services/state.service.ts index bc3c73966..4f3b63191 100644 --- a/packages/federation-sdk/src/services/state.service.ts +++ b/packages/federation-sdk/src/services/state.service.ts @@ -326,7 +326,7 @@ export class StateService { const origin = this.configService.serverName; - const { signatures, ...toSign } = event.redactedEvent; + const { signatures, unsigned, ...toSign } = event.redactedEvent; const signature = await signJson( // Before signing the event, the content hash of the event is calculated as described below. The hash is encoded using Unpadded Base64 and stored in the event object, in a hashes object, under a sha256 key. diff --git a/packages/homeserver/src/controllers/federation/profiles.controller.ts b/packages/homeserver/src/controllers/federation/profiles.controller.ts index 37b550be4..56e8e920b 100644 --- a/packages/homeserver/src/controllers/federation/profiles.controller.ts +++ b/packages/homeserver/src/controllers/federation/profiles.controller.ts @@ -116,8 +116,8 @@ export const profilesPlugin = (app: Elysia) => { params: MakeJoinParamsDto, query: t.Any(), response: { - 200: MakeJoinResponseDto, - 400: ErrorResponseDto, + 200: t.Any(), + 400: t.Any(), }, detail: { tags: ['Federation'], diff --git a/packages/homeserver/src/homeserver.module.ts b/packages/homeserver/src/homeserver.module.ts index c5665ef8b..a0e983b16 100644 --- a/packages/homeserver/src/homeserver.module.ts +++ b/packages/homeserver/src/homeserver.module.ts @@ -45,6 +45,7 @@ export async function setup(options?: HomeserverSetupOptions) { } const config = new ConfigService({ + signingKey: process.env.SIGNING_KEY || undefined, instanceId: crypto.randomUUID(), serverName: process.env.SERVER_NAME || 'rc1', port: Number.parseInt(process.env.SERVER_PORT || '8080', 10), From fd22b03ab37417f62e6a3518f878bced217143c0 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Mon, 15 Sep 2025 18:15:51 +0530 Subject: [PATCH 18/56] tests are passing at this point --- packages/core/src/index.ts | 2 +- packages/core/src/types.ts | 38 ++ packages/federation-sdk/src/container.ts | 10 +- packages/federation-sdk/src/index.ts | 2 + .../src/repositories/key.repository.ts | 76 +++- .../src/services/key.service.ts | 416 ++++++++++++++++++ .../src/services/server.service.ts | 2 +- .../src/controllers/key/server.controller.ts | 60 ++- 8 files changed, 568 insertions(+), 38 deletions(-) create mode 100644 packages/federation-sdk/src/services/key.service.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 57aeb37df..2d7391543 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,6 @@ // Types export { EncryptionValidAlgorithm } from './types'; -export type { SignedEvent, SigningKey } from './types'; +export type * from './types'; // Event utilities export { signEvent } from './utils/signEvent'; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1400cee3f..166da144e 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -24,3 +24,41 @@ export type SigningKey = { publicKey: Uint8Array; sign(data: Uint8Array): Promise; }; + +export type KeyV2ServerResponse = { + // still valid for signing events + old_verify_keys: Record< + string, + { + expired_ts: number; + key: string; + } + >; + server_name: string; + signatures: Record>; + valid_until_ts: number; + // only federation requests + verify_keys: Record< + string, // keyAlgo:algoVersion => KeyId + { + key: string; // base64 encoded + } + >; +}; + +export type ServerKey = { + serverName: string; + keys: { + [keyId: string]: { + key: string; + pem: string; + + // + _createdAt: Date; + _updatedAt: Date; + expiresAt: number; + }; + }; + // should this save the responses here? + // does the spec dictate the signatures +}; diff --git a/packages/federation-sdk/src/container.ts b/packages/federation-sdk/src/container.ts index 95e51868c..57ed46a6e 100644 --- a/packages/federation-sdk/src/container.ts +++ b/packages/federation-sdk/src/container.ts @@ -1,5 +1,6 @@ import 'reflect-metadata'; +import type { EventStagingStore, EventStore, ServerKey } from '@hs/core'; import type { Emitter } from '@rocket.chat/emitter'; import type { EventStagingStore, @@ -13,7 +14,7 @@ import { StagingAreaListener } from './listeners/staging-area.listener'; import { StagingAreaQueue } from './queues/staging-area.queue'; import { EventStagingRepository } from './repositories/event-staging.repository'; import { EventRepository } from './repositories/event.repository'; -import { Key, KeyRepository } from './repositories/key.repository'; +import { KeyRepository } from './repositories/key.repository'; import { Lock, LockRepository } from './repositories/lock.repository'; import { Room, RoomRepository } from './repositories/room.repository'; import { Server, ServerRepository } from './repositories/server.repository'; @@ -42,6 +43,7 @@ import { ServerService } from './services/server.service'; import { StagingAreaService } from './services/staging-area.service'; import { StateService } from './services/state.service'; import { WellKnownService } from './services/well-known.service'; +import { KeyService } from './services/key.service'; export interface FederationContainerOptions { emitter?: Emitter; @@ -73,8 +75,9 @@ export async function createFederationContainer( ), }); - container.register>('KeyCollection', { - useValue: db.collection('rocketchat_federation_keys'), + container.register>('KeyCollection', { + // TODO change collection name to include at least the "rocketchat_" prefix + useValue: db.collection('keys'), }); container.register>('LockCollection', { @@ -126,6 +129,7 @@ export async function createFederationContainer( container.registerSingleton(SendJoinService); container.registerSingleton(StagingAreaService); container.registerSingleton(EduService); + container.registerSingleton(KeyService); container.registerSingleton(StagingAreaListener); diff --git a/packages/federation-sdk/src/index.ts b/packages/federation-sdk/src/index.ts index 369758c66..ace0fe227 100644 --- a/packages/federation-sdk/src/index.ts +++ b/packages/federation-sdk/src/index.ts @@ -97,6 +97,8 @@ export { RoomRepository } from './repositories/room.repository'; export { ServerRepository } from './repositories/server.repository'; export { KeyRepository } from './repositories/key.repository'; +export { KeyService } from './services/key.service'; + export interface HomeserverServices { room: RoomService; message: MessageService; diff --git a/packages/federation-sdk/src/repositories/key.repository.ts b/packages/federation-sdk/src/repositories/key.repository.ts index 1ef3cd295..3b63a2c29 100644 --- a/packages/federation-sdk/src/repositories/key.repository.ts +++ b/packages/federation-sdk/src/repositories/key.repository.ts @@ -1,30 +1,58 @@ -import { Collection } from 'mongodb'; +import { ServerKey } from '@hs/core'; +import { Collection, Filter, FindOptions } from 'mongodb'; import { inject, singleton } from 'tsyringe'; -export type Key = { - origin: string; - key_id: string; - public_key: string; - valid_until: Date; -}; - @singleton() export class KeyRepository { constructor( - @inject('KeyCollection') private readonly collection: Collection, + @inject('KeyCollection') private readonly collection: Collection, ) {} - async getValidPublicKeyFromLocal( - origin: string, + async findByServerName( + serverName: string, + validUntil?: number, + ): Promise { + return this.collection.findOne({ + serverName, + ...(validUntil ? { 'keys.expiresAt': { $gt: validUntil } } : {}), + }); + } + + async findByServerNameAndKeyId( + serverName: string, keyId: string, - ): Promise { - const key = await this.collection.findOne({ - origin, - key_id: keyId, - valid_until: { $gt: new Date() }, + validUntil?: number, + ): Promise { + return this.collection.findOne({ + serverName, + [`keys.${keyId}`]: { $exists: true }, + ...(validUntil + ? { [`keys.${keyId}.expiresAt`]: { $gte: validUntil } } + : {}), }); + } - return key?.public_key; + async findAllByServerNameAndKeyIds( + serverName: string, + keyIds: string[], + ): Promise { + const query: Filter = { + serverName, + }; + + for (const keyId of keyIds) { + query[`keys.${keyId}`] = { $exists: true }; + } + + return this.collection.findOne(query); + } + + async storeKeys(serverKey: ServerKey): Promise { + await this.collection.updateOne( + { serverName: serverKey.serverName }, + { $set: serverKey }, + { upsert: true }, + ); } async storePublicKey( @@ -47,4 +75,18 @@ export class KeyRepository { { upsert: true }, ); } + + async getValidPublicKeyFromLocal( + origin: string, + keyId: string, + ): Promise { + const key = await this.collection.findOne({ + origin, + key_id: keyId, + valid_until: { $gt: new Date() }, + }); + + // @ts-ignore + return key?.public_key; + } } diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts new file mode 100644 index 000000000..eecea572e --- /dev/null +++ b/packages/federation-sdk/src/services/key.service.ts @@ -0,0 +1,416 @@ +import { singleton } from 'tsyringe'; +import { ConfigService } from './config.service'; +import { KeyRepository } from '../repositories/key.repository'; +import { + fromBase64ToBytes, + isValidAlgorithm, + loadEd25519VerifierFromPublicKey, + signJson, + type Signer, +} from '@hs/crypto'; +import { + fetch as _myFetch, + type KeyV2ServerResponse, + type ServerKey, +} from '@hs/core'; +import { createLogger } from '../utils/logger'; +import { getHomeserverFinalAddress } from '../server-discovery/discovery'; +import { ConnectionCheckOutFailedEvent } from 'mongodb'; + +type QueryCriteria = { + // If not supplied, the current time as determined by the notary server is used. + minimum_valid_until_ts?: number; +}; + +type QueryRequestBody = { + server_keys: Record< + string /* serverName */, + Record + >; +}; + +function isKeyV2ServerResponse(obj: unknown): obj is KeyV2ServerResponse { + if ( + typeof obj === 'object' && + obj !== null && + 'signatures' in obj && + 'server_name' in obj && + 'verify_keys' in obj && + 'valid_until_ts' in obj && + 'old_verify_keys' in obj + ) { + return true; + } + return false; +} + +@singleton() +export class KeyService { + private signer: Signer | undefined; + + private logger = createLogger('KeyService'); + constructor( + private readonly configService: ConfigService, + private readonly keyRepository: KeyRepository, + ) { + this.configService.getSigningKey().then((signer) => { + this.signer = signer; + }); + } + + private shouldRefetchKey( + localKey: ServerKey, + keyId: string, + validUntil?: number, + ) { + const key = localKey.keys[keyId]; + + const { serverName } = localKey; + + if (validUntil) { + if (key.expiresAt < validUntil) { + this.logger.warn( + `Key for ${serverName} is expired, ${key.expiresAt} < ${validUntil}, refetching keys`, + ); + return true; + } + + return false; + } + + // SPEC: Intermediate notary servers should cache a response for half of its lifetime to avoid serving a stale response. + // this could be part of an aggregation, however, the key data stored aren't big enough to justify the complexity right now. + if ((key._updatedAt.getTime() + key.expiresAt) / 2 < Date.now()) { + this.logger.warn(`Half life for key for ${serverName} is expired`); + return true; + } + } + + async fetchKeysFromRemoteServerRaw( + serverName: string, + ): Promise { + // FIXME: + // const [address, hostHeaders] = await getHomeserverFinalAddress( + // serverName, + // this.logger, + // ); + + // TODO: move this to federation service?? + // const keyV2ServerUrl = new URL(`${address}/_matrix/key/v2/server`); + + const response = await fetch( + `https://${serverName}/_matrix/key/v2/server`, + { + // headers: hostHeaders, + method: 'GET', + }, + ); + + if (!response.ok) { + this.logger.error( + `Failed to fetch keys from remote server ${serverName}: ${response.status} ${response.text()}`, + ); + throw new Error('Failed to fetch keys'); + } + + const data = await response.json(); + + if (!isKeyV2ServerResponse(data)) { + this.logger.error( + { data }, + `Invalid key response from remote server ${serverName}`, + ); + throw new Error('Invalid key response'); + } + + void this.storeKeysFromResponse(data); + + return data; + } + + private parseKeyId(keyId: string) { + // keyId should be in the format : + const [algorithm, version] = keyId.split(':'); + if (!algorithm || !version) { + throw new Error('Invalid keyId format'); + } + + if (!isValidAlgorithm(algorithm)) { + throw new Error('Invalid algorithm in keyId'); + } + + return { algorithm, version }; + } + + private async storeKeysFromResponse( + response: KeyV2ServerResponse, + ): Promise { + const existingKey = await this.keyRepository.findByServerName( + response.server_name, + ); + const keys: ServerKey = { + serverName: response.server_name, + keys: existingKey ? existingKey.keys : {}, + }; + + for (const [keyId, keyInfo] of Object.entries(response.verify_keys)) { + const { version } = this.parseKeyId(keyId); + + const verifier = await loadEd25519VerifierFromPublicKey( + fromBase64ToBytes(keyInfo.key), + version, + ); + + keys.keys[verifier.id] = { + key: keyInfo.key, + pem: verifier.getPublicKeyPem(), + + _createdAt: existingKey?.keys?.[verifier.id]?._createdAt ?? new Date(), + _updatedAt: new Date(), + expiresAt: response.valid_until_ts, + }; + } + + for (const [keyId, keyInfo] of Object.entries(response.old_verify_keys)) { + const { version } = this.parseKeyId(keyId); + + const verifier = await loadEd25519VerifierFromPublicKey( + fromBase64ToBytes(keyInfo.key), + version, + ); + + keys.keys[verifier.id] = { + key: keyInfo.key, + pem: verifier.getPublicKeyPem(), + + _createdAt: existingKey?.keys?.[verifier.id]?._createdAt ?? new Date(), + _updatedAt: new Date(), + expiresAt: keyInfo.expired_ts, + }; + } + + await this.keyRepository.storeKeys(keys); + } + + async convertToKeyV2Response( + serverKey: ServerKey, + minimumValidUntil = Date.now(), + ): Promise { + if (!this.signer) { + // need this to sign the response json, no point in calculating anythingg if this isn't ready + throw new Error('Signing key not configured'); + } + const verifyKeys: KeyV2ServerResponse['verify_keys'] = {}; + + const oldVerifyKeys: KeyV2ServerResponse['old_verify_keys'] = {}; + + let validUntil = 0; + + for (const [keyId, keyInfo] of Object.entries(serverKey.keys)) { + if (keyInfo.expiresAt > validUntil) { + validUntil = keyInfo.expiresAt; + } + + // if expired, move to old keys + if (keyInfo.expiresAt < minimumValidUntil) { + oldVerifyKeys[keyId] = { + expired_ts: keyInfo.expiresAt, + key: keyInfo.key, + }; + } else { + verifyKeys[keyId] = { + key: keyInfo.key, + }; + } + } + + const response = { + server_name: serverKey.serverName, + verify_keys: verifyKeys, + old_verify_keys: oldVerifyKeys, + valid_until_ts: validUntil, + }; + + const signature = await signJson(response, this.signer); + + return { + ...response, + signatures: { + [this.configService.serverName]: { + [this.signer.id]: signature, + }, + }, + }; + } + + // this shouldn't be here, however, to copy the controller level logic from homeserver router to rocket.chat would be a pain to keep up to date if changes are needed. for now, keeping here. + async handleQuery({ server_keys: serverKeys }: QueryRequestBody) { + if (!this.signer) { + // need this to sign the response json, no point in calculating anythingg if this isn't ready + throw new Error('Signing key not configured'); + } + + const serverKeysResponse = [] as KeyV2ServerResponse[]; + + const localKeys = [] as ServerKey[]; + + for (const [serverName, query] of Object.entries(serverKeys)) { + const keysAsked = Object.keys(query); + + this.logger.debug({ serverName, query, keyIds: keysAsked }, 'keys asked'); + + if (keysAsked.length === 0) { + const keys = await this.keyRepository.findByServerName(serverName); + + if (!keys) { + this.logger.debug({ serverName, keys }, 'no cached keys found'); + // no cache, fetch from remote and stpre + try { + const remoteKeys = + await this.fetchKeysFromRemoteServerRaw(serverName); + serverKeysResponse.push(remoteKeys); + + this.logger.debug( + { + response: remoteKeys, + serverName, + }, + 'keys from remote', + ); + } catch (error) { + // SPEC: If the server fails to respond to this request, intermediate notary servers should continue to return the last response they received from the server so that the signatures of old events can still be checked. + this.logger.warn( + { error }, + `Failed to fetch keys from remote server ${serverName}, continuing with cached keys if any`, + ); + + // no cached keys in this instance, we return empty list in that case. + } + continue; + } + + // check if any of the cached keys need refetching + if ( + Object.keys(keys.keys).every((keyId) => + this.shouldRefetchKey(keys, keyId), + ) + ) { + try { + const remoteKeys = + await this.fetchKeysFromRemoteServerRaw(serverName); + serverKeysResponse.push(remoteKeys); + + this.logger.debug( + { + response: remoteKeys, + serverName, + }, + 'keys from remote after refetch', + ); + } catch (error) { + // SPEC: If the server fails to respond to this request, intermediate notary servers should continue to return the last response they received from the server so that the signatures of old events can still be checked. + this.logger.warn( + { error }, + `Failed to fetch keys from remote server ${serverName}, continuing with cached keys if any`, + ); + + localKeys.push(keys); + } + + continue; + } + + this.logger.debug({ serverName, keys }, 'using cached keys'); + + localKeys.push(keys); + + continue; + } + + if ( + !keysAsked.some((keyId) => + isValidAlgorithm(keyId.split(':').shift() ?? ''), + ) + ) { + throw new Error('Invalid keyId format when querying for keys'); + } + + // intentionally querying for all keys from db, since asked, if we didn't find one, should trigger a refetch + const keysForQuery = + await this.keyRepository.findAllByServerNameAndKeyIds( + serverName, + keysAsked, + ); + + this.logger.debug( + { serverName, query, keysForQuery }, + 'keys found for query', + ); + + if ( + !keysForQuery || + keysAsked.every((keyId) => + this.shouldRefetchKey( + keysForQuery, + keyId, + query[keyId]?.minimum_valid_until_ts, + ), + ) + ) { + // no valid cache, fetch from remote and stpre + try { + const remoteKeys = + await this.fetchKeysFromRemoteServerRaw(serverName); + // TODO: apply actual filter + serverKeysResponse.push(remoteKeys); + + this.logger.debug( + { + response: remoteKeys, + serverName, + }, + 'keys from remote after refetch for query', + ); + } catch (error) { + // SPEC: If the server fails to respond to this request, intermediate notary servers should continue to return the last response they received from the server so that the signatures of old events can still be checked. + this.logger.warn( + { error }, + `Failed to fetch keys from remote server ${serverName}, continuing with cached keys if any`, + ); + + if (keysForQuery) { + localKeys.push(keysForQuery); + } + } + + continue; + } + + localKeys.push(keysForQuery); + } + + const keys = await Promise.all([ + // convert and sign + ...localKeys.map(this.convertToKeyV2Response.bind(this)), + // sign with our keys + ...serverKeysResponse.map(async (key): Promise => { + const { signatures, ...rest } = key; + const signature = await signJson(rest, this.signer!); + + return { + ...rest, + signatures: { + ...signatures, + [this.configService.serverName]: { + [this.signer!.id]: signature, + }, + }, + }; + }), + ]); + + return { + server_keys: keys, + }; + } +} diff --git a/packages/federation-sdk/src/services/server.service.ts b/packages/federation-sdk/src/services/server.service.ts index a409dffb9..8f4509e1d 100644 --- a/packages/federation-sdk/src/services/server.service.ts +++ b/packages/federation-sdk/src/services/server.service.ts @@ -57,7 +57,7 @@ export class ServerService { const keys = { [signer.id]: { - key: toUnpaddedBase64(signer.publicKey), + key: toUnpaddedBase64(signer.getPublicKey()), }, }; diff --git a/packages/homeserver/src/controllers/key/server.controller.ts b/packages/homeserver/src/controllers/key/server.controller.ts index 9088d9210..9a5d56dad 100644 --- a/packages/homeserver/src/controllers/key/server.controller.ts +++ b/packages/homeserver/src/controllers/key/server.controller.ts @@ -1,24 +1,52 @@ -import { ServerService } from '@rocket.chat/federation-sdk'; -import type { Elysia } from 'elysia'; +import { ConfigService, ServerService } from '@hs/federation-sdk'; +import { t, type Elysia } from 'elysia'; import { container } from 'tsyringe'; import { ServerKeyResponseDto } from '../../dtos'; +import { KeyService } from '@hs/federation-sdk'; export const serverKeyPlugin = (app: Elysia) => { const serverService = container.resolve(ServerService); - return app.get( - '/_matrix/key/v2/server', - async () => { - return serverService.getSignedServerKey(); - }, - { - response: { - 200: ServerKeyResponseDto, + const keyService = container.resolve(KeyService); + return app + .get( + '/_matrix/key/v2/server', + async () => { + return serverService.getSignedServerKey(); }, - detail: { - tags: ['Key'], - summary: 'Get server key', - description: 'Get the server key', + { + response: { + 200: ServerKeyResponseDto, + }, + detail: { + tags: ['Key'], + summary: 'Get server key', + description: 'Get the server key', + }, }, - }, - ); + ) + .post( + '/_matrix/key/v2/query', + async ({ body }: any) => { + const resp = await keyService.handleQuery(body); + return resp; + }, + { + body: t.Any(), + }, + ) + .get( + '/_matrix/key/v2/query/:serverName', + async ({ params }) => { + const { serverName } = params; + + const resp = await keyService.handleQuery({ + server_keys: { [serverName]: {} }, + }); + + return resp; + }, + { + params: t.Object({ serverName: t.String() }), + }, + ); }; From 1bb49d7379db0ab795d7e13fbf72a40e6d362cee Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Tue, 16 Sep 2025 14:39:36 +0530 Subject: [PATCH 19/56] ts all there --- packages/core/src/types.ts | 18 +- packages/crypto/src/utils/keys.ts | 1 + packages/crypto/src/utils/utils.spec.ts | 56 +++ .../src/__mocks__/config.service.spec.ts | 21 ++ .../src/__mocks__/repositories.spec.ts | 40 ++ .../src/__mocks__/services.spec.ts | 43 +++ .../src/__mocks__/singer.spec.ts | 18 + .../repositories/event-staging.repository.ts | 3 +- .../src/repositories/key.repository.ts | 68 ++-- .../src/services/event.service.spec.ts | 327 ++++++++++++++++ .../src/services/event.service.ts | 167 ++++++--- .../services/federation-request.service.ts | 1 - .../src/services/key.service.spec.ts | 227 +++++++++++ .../src/services/key.service.ts | 353 ++++++++++++++---- .../signature-verification.service.spec.ts | 204 +++++----- .../signature-verification.service.ts | 190 ++++------ .../src/services/state.service.ts | 22 +- packages/room/src/manager/event-wrapper.ts | 10 +- packages/room/src/types/v3-11.ts | 2 +- 19 files changed, 1390 insertions(+), 381 deletions(-) create mode 100644 packages/federation-sdk/src/__mocks__/config.service.spec.ts create mode 100644 packages/federation-sdk/src/__mocks__/repositories.spec.ts create mode 100644 packages/federation-sdk/src/__mocks__/services.spec.ts create mode 100644 packages/federation-sdk/src/__mocks__/singer.spec.ts create mode 100644 packages/federation-sdk/src/services/event.service.spec.ts create mode 100644 packages/federation-sdk/src/services/key.service.spec.ts diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 166da144e..9d19f329d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -48,17 +48,11 @@ export type KeyV2ServerResponse = { export type ServerKey = { serverName: string; - keys: { - [keyId: string]: { - key: string; - pem: string; + keyId: string; + key: string; + pem: string; - // - _createdAt: Date; - _updatedAt: Date; - expiresAt: number; - }; - }; - // should this save the responses here? - // does the spec dictate the signatures + _createdAt: Date; + _updatedAt: Date; + expiresAt: Date; }; diff --git a/packages/crypto/src/utils/keys.ts b/packages/crypto/src/utils/keys.ts index eeded239d..4967cecd2 100644 --- a/packages/crypto/src/utils/keys.ts +++ b/packages/crypto/src/utils/keys.ts @@ -41,6 +41,7 @@ export async function verifyJsonSignature( signature: string, key: VerifierKey, ): Promise { + console.log(jsonObject); const sortedSerializedForm = encodeCanonicalJson(jsonObject); const signatureBuffer = fromBase64ToBytes(signature); diff --git a/packages/crypto/src/utils/utils.spec.ts b/packages/crypto/src/utils/utils.spec.ts index a1182cfe2..15b6aa67e 100644 --- a/packages/crypto/src/utils/utils.spec.ts +++ b/packages/crypto/src/utils/utils.spec.ts @@ -103,6 +103,62 @@ describe('Signing and verifying payloads', async () => { 'Invalid signature', ); }); + + it('should validate signature (paranoid test 2)', async () => { + const event = { + auth_events: [ + '$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA', + '$Ulggyo4m1OlI08Z0jJDVeceigjSZP9SdEFVoAn9mEh8', + '$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg', + '$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs', + ], + content: { + // displayname: 'debdut1', + membership: 'join' as const, + }, + depth: 10, + hashes: { sha256: '6MnKSCFJy1fYf6ukILBEbqx2DkoaD1wRyKXhv689a0A' }, + origin: 'syn1.tunnel.dev.rocket.chat', + origin_server_ts: 1757328411218, + prev_events: ['$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs'], + room_id: '!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat', + sender: '@debdut1:syn1.tunnel.dev.rocket.chat', + state_key: '@debdut1:syn1.tunnel.dev.rocket.chat', + type: 'm.room.member' as const, + signatures: { + 'syn1.tunnel.dev.rocket.chat': { + 'ed25519:a_FAET': + 'eJlvqxPWPe3u+BM4wOwID9YBlh/ZfVVxGYyA5WgpNs5Fe1+c36qrvCKHuXGGjfQoZFrHmZ3/GJw2pv5EvxCZAA', + }, + }, + unsigned: { + age: 1, + replaces_state: '$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs', + prev_content: { displayname: 'debdut1', membership: 'invite' }, + prev_sender: '@debdut:syn2.tunnel.dev.rocket.chat', + }, + }; + + const { unsigned, signatures, ...rest } = event; + + const key = 'kryovKVnhHESOdWuZ05ViNotRMVdEh/mG2yJ0npLzEo'; + + const verifier = await loadEd25519VerifierFromPublicKey( + fromBase64ToBytes(key), + '0', + ); + + const serialized = encodeCanonicalJson(rest); + + console.log({ serialized }); + + const signature = + signatures['syn1.tunnel.dev.rocket.chat']['ed25519:a_FAET']; + + const signatureBytes = fromBase64ToBytes(signature); + + await verifier.verify(serialized, signatureBytes); + }); }); describe('Canonical json serialization', () => { diff --git a/packages/federation-sdk/src/__mocks__/config.service.spec.ts b/packages/federation-sdk/src/__mocks__/config.service.spec.ts new file mode 100644 index 000000000..9975f89ad --- /dev/null +++ b/packages/federation-sdk/src/__mocks__/config.service.spec.ts @@ -0,0 +1,21 @@ +import { ConfigService } from '../services/config.service'; +import { DatabaseConnectionService } from '../services/database-connection.service'; +import { signer } from './singer.spec'; + +export const config = { + serverName: 'test.local', + getSigningKey: async () => signer, + database: { + uri: 'mongodb://localhost:27017/matrix_test', + name: 'matrix_test', + poolSize: 100, + }, + getDatabaseConfig: function () { + // @ts-ignore + return this.database; + }, +} as unknown as ConfigService; + +const database = new DatabaseConnectionService(config); + +export const db = await database.getDb(); diff --git a/packages/federation-sdk/src/__mocks__/repositories.spec.ts b/packages/federation-sdk/src/__mocks__/repositories.spec.ts new file mode 100644 index 000000000..2ff9b63a8 --- /dev/null +++ b/packages/federation-sdk/src/__mocks__/repositories.spec.ts @@ -0,0 +1,40 @@ +import { EventStagingStore, EventStore, ServerKey } from '@hs/core'; +import { db } from './config.service.spec'; +import { KeyRepository } from '../repositories/key.repository'; +import { Lock, LockRepository } from '../repositories/lock.repository'; +import { EventStagingRepository } from '../repositories/event-staging.repository'; +import { StateStore, StateRepository } from '../repositories/state.repository'; +import { EventRepository } from '../repositories/event.repository'; + +const keysCollection = db.collection('test_keys'); +const eventsCollection = db.collection('test_events'); +const eventStagingCollection = + db.collection('test_event_staging'); +const lockCollection = db.collection('test_locks'); +const statesCollection = db.collection('test_states'); + +export const collections = { + keys: keysCollection, + events: eventsCollection, + eventsStaging: eventStagingCollection, + locks: lockCollection, + states: statesCollection, +}; + +const keyRepository = new KeyRepository(keysCollection); + +const eventStagingRepository = new EventStagingRepository( + eventStagingCollection, +); +const lockRepository = new LockRepository(lockCollection); +const stateRepository = new StateRepository(statesCollection as any); // TODO: fix this + +const eventsRepository = new EventRepository(eventsCollection); + +export const repositories = { + keys: keyRepository, + locks: lockRepository, + eventStaging: eventStagingRepository, + states: stateRepository, + events: eventsRepository, +}; diff --git a/packages/federation-sdk/src/__mocks__/services.spec.ts b/packages/federation-sdk/src/__mocks__/services.spec.ts new file mode 100644 index 000000000..b81f80366 --- /dev/null +++ b/packages/federation-sdk/src/__mocks__/services.spec.ts @@ -0,0 +1,43 @@ +import { KeyService } from '../services/key.service'; +import { config } from './config.service.spec'; +import { repositories } from './repositories.spec'; +import { StagingAreaQueue } from '../queues/staging-area.queue'; +import { StateService } from '../services/state.service'; +import { EventEmitterService } from '../services/event-emitter.service'; +import { SignatureVerificationService } from '../services/signature-verification.service'; +import { EventService } from '../services/event.service'; + +const keyService = new KeyService(config, repositories.keys); + +const stagingAreaQueue = new StagingAreaQueue(); + +const stateService = new StateService( + repositories.states, + repositories.events, + config, +); + +const eventEmitter = new EventEmitterService(); + +const signatureVerificationService = new SignatureVerificationService(); + +const eventService = new EventService( + repositories.events, + repositories.eventStaging, + repositories.locks, + config, + stagingAreaQueue, + stateService, + eventEmitter, + keyService, + signatureVerificationService, +); + +export { + keyService, + stateService, + stagingAreaQueue, + eventEmitter, + signatureVerificationService, + eventService, +}; diff --git a/packages/federation-sdk/src/__mocks__/singer.spec.ts b/packages/federation-sdk/src/__mocks__/singer.spec.ts new file mode 100644 index 000000000..a4a6cbf6a --- /dev/null +++ b/packages/federation-sdk/src/__mocks__/singer.spec.ts @@ -0,0 +1,18 @@ +import { + loadEd25519SignerFromSeed, + fromBase64ToBytes, + VerifierKey, +} from '@hs/crypto'; + +const seed = 'zSkmr713LnEDbxlkYq2ZqIiKTQNsyMOU0T2CEeC44C4'; + +const version = '0'; + +const signer = await loadEd25519SignerFromSeed( + fromBase64ToBytes(seed), + version, +); + +const verifier: VerifierKey = signer; + +export { verifier, signer }; diff --git a/packages/federation-sdk/src/repositories/event-staging.repository.ts b/packages/federation-sdk/src/repositories/event-staging.repository.ts index f814bcddd..a02d589dc 100644 --- a/packages/federation-sdk/src/repositories/event-staging.repository.ts +++ b/packages/federation-sdk/src/repositories/event-staging.repository.ts @@ -1,6 +1,5 @@ -import { generateId } from '@rocket.chat/federation-core'; -import type { EventStagingStore } from '@rocket.chat/federation-core'; import { type EventID, Pdu } from '@rocket.chat/federation-room'; +import type { EventStagingStore } from '@hs/core'; import type { Collection, DeleteResult, UpdateResult } from 'mongodb'; import { inject, singleton } from 'tsyringe'; diff --git a/packages/federation-sdk/src/repositories/key.repository.ts b/packages/federation-sdk/src/repositories/key.repository.ts index 3b63a2c29..1b6504f5f 100644 --- a/packages/federation-sdk/src/repositories/key.repository.ts +++ b/packages/federation-sdk/src/repositories/key.repository.ts @@ -1,5 +1,5 @@ import { ServerKey } from '@hs/core'; -import { Collection, Filter, FindOptions } from 'mongodb'; +import type { Collection, Filter, FindCursor, FindOptions } from 'mongodb'; import { inject, singleton } from 'tsyringe'; @singleton() @@ -8,49 +8,65 @@ export class KeyRepository { @inject('KeyCollection') private readonly collection: Collection, ) {} - async findByServerName( + findByServerName( serverName: string, - validUntil?: number, - ): Promise { - return this.collection.findOne({ - serverName, - ...(validUntil ? { 'keys.expiresAt': { $gt: validUntil } } : {}), - }); + validUntil?: Date, + options?: FindOptions, + ): FindCursor { + return this.collection.find( + { + serverName, + ...(validUntil && { expiresAt: { $gt: validUntil } }), + }, + options ?? {}, + ); } async findByServerNameAndKeyId( serverName: string, keyId: string, - validUntil?: number, + validUntil?: Date, + options?: FindOptions, ): Promise { - return this.collection.findOne({ - serverName, - [`keys.${keyId}`]: { $exists: true }, - ...(validUntil - ? { [`keys.${keyId}.expiresAt`]: { $gte: validUntil } } - : {}), - }); + return this.collection.findOne( + { + serverName, + keyId, + ...(validUntil && { expiresAt: { $gte: validUntil } }), + }, + options ?? {}, + ); } - async findAllByServerNameAndKeyIds( + findAllByServerNameAndKeyIds( serverName: string, keyIds: string[], - ): Promise { + options?: FindOptions, + ): FindCursor { const query: Filter = { serverName, + keyId: { $in: keyIds }, }; - for (const keyId of keyIds) { - query[`keys.${keyId}`] = { $exists: true }; - } - - return this.collection.findOne(query); + return this.collection.find(query, options ?? {}); } - async storeKeys(serverKey: ServerKey): Promise { + // cache can be refreshed + async insertOrUpdateKey(serverKey: ServerKey): Promise { await this.collection.updateOne( - { serverName: serverKey.serverName }, - { $set: serverKey }, + { serverName: serverKey.serverName, keyId: serverKey.keyId }, + { + $setOnInsert: { + _createdAt: new Date(), + // following shouldn't change along with keyId + key: serverKey.key, + pem: serverKey.pem, + }, + $set: { + expiresAt: serverKey.expiresAt, + _updatedAt: new Date(), + }, + }, { upsert: true }, ); } diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts new file mode 100644 index 000000000..778b04c7c --- /dev/null +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -0,0 +1,327 @@ +import { + afterAll, + afterEach, + beforeEach, + describe, + expect, + it, + Mock, + mock, + test, +} from 'bun:test'; +import { eventService } from '../__mocks__/services.spec'; +import { Pdu, PersistentEventFactory } from '@hs/room'; +import { BaseEDU } from '@hs/core'; +import { StateService } from './state.service'; +import { collections, repositories } from '../__mocks__/repositories.spec'; +import { config } from '../__mocks__/config.service.spec'; +import { loadEd25519SignerFromSeed } from '../../../crypto/dist/utils/keys'; +import { fromBase64ToBytes } from '../../../crypto/dist/utils/data-types'; +import { prettyFactory } from 'pino-pretty'; + +const event = { + auth_events: [ + '$Hvb-xPPDhTvlXZe2kMubgj8J7iUa5W7YvjTqMTffgUA', + '$Ulggyo4m1OlI08Z0jJDVeceigjSZP9SdEFVoAn9mEh8', + '$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg', + '$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs', + ], + content: { + avatar_url: null, + displayname: 'debdut1', + membership: 'join' as const, + }, + depth: 10, + hashes: { sha256: '6MnKSCFJy1fYf6ukILBEbqx2DkoaD1wRyKXhv689a0A' }, + origin: 'syn1.tunnel.dev.rocket.chat', + origin_server_ts: 1757328411218, + prev_events: ['$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs'], + room_id: '!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat', + sender: '@debdut1:syn1.tunnel.dev.rocket.chat', + state_key: '@debdut1:syn1.tunnel.dev.rocket.chat', + type: 'm.room.member' as const, + signatures: { + 'syn1.tunnel.dev.rocket.chat': { + 'ed25519:a_FAET': + 'eJlvqxPWPe3u+BM4wOwID9YBlh/ZfVVxGYyA5WgpNs5Fe1+c36qrvCKHuXGGjfQoZFrHmZ3/GJw2pv5EvxCZAA', + }, + }, + unsigned: { + age: 1, + replaces_state: '$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs', + prev_content: { displayname: 'debdut1', membership: 'invite' }, + prev_sender: '@debdut:syn2.tunnel.dev.rocket.chat', + }, +}; + +describe('EventService', async () => { + it('should fail to fetch room informatin of unknown room, sanity check for mock loading', async () => { + expect( + eventService.getRoomVersion({ room_id: 'abc123' } as Pdu), + ).rejects.toThrowError(/Create event not found/); + }); + + const { getHomeserverFinalAddress: originalServerDiscovery } = await import( + '../server-discovery/discovery' + ); + + await mock.module('../server-discovery/discovery', () => ({ + // this mock doesn't matter, or doesn't change, we just need to skip actual server discovery + // and mock the /key/v2/server responses + getHomeserverFinalAddress: async (..._args: any[]) => [ + 'https://127.0.0.1', + {}, + ], + })); + + // random server name for each run + let inboundServer = `localhost${Math.floor(Math.random() * 10000).toString()}`; + + let originalFetch: typeof globalThis.fetch; + + type FetchJson = Awaited>['json']; + + const fetchJsonMock: Mock = mock(() => Promise.resolve()); + + beforeEach(() => { + originalFetch = globalThis.fetch; + globalThis.fetch = Object.assign( + async (_url: string, _options?: RequestInit) => { + return { + ok: true, + status: 200, + json: fetchJsonMock as unknown as FetchJson, + } as Response; + }, + { preconnect: () => {} }, + ) as typeof fetch; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + mock.restore(); + }); + + describe('processIncomingTransaction', async () => { + it('should fail basic malformed payloads (sanity checks)', async () => { + expect( + eventService.processIncomingTransaction({ + origin: 'test.local', + // @ts-expect-error + pdus: {}, + }), + ).rejects.toThrowError(/pdus must be an array/); + expect( + eventService.processIncomingTransaction({ + origin: 'test.local', + pdus: [], + // @ts-expect-error + edus: {}, + }), + ).rejects.toThrowError(/edus must be an array/); + + expect( + eventService.processIncomingTransaction({ + origin: 'test.local', + pdus: Array.from({ length: 51 }).fill({}) as Pdu[], + edus: [], + }), + ).rejects.toThrowError(/too-many-events/); + + expect( + eventService.processIncomingTransaction({ + origin: 'test.local', + edus: Array.from({ length: 101 }).fill({}) as BaseEDU[], + pdus: [], + }), + ).rejects.toThrowError(/too-many-events/); + + // NOTE(deb): should also check the happy path but not running all function tests so skipping that + }); + }); + + describe('_validateHashAndSignatures', async () => { + const roomVersion = '10' as const; + + // to build events with different signatures, creating new instance of stateService here + const newSeed = 'JFU4ln6/aSnXWF5EY9m7N9Z/MDUHRLt9C+Z6Vv34Ims'; + const version = 'xxx'; + const signer = await loadEd25519SignerFromSeed( + fromBase64ToBytes(newSeed), + version, + ); + + let stateService: StateService; + + beforeEach(async () => { + inboundServer = `localhost${Math.floor(Math.random() * 10000).toString()}`; + const newConfig = { + ...config, + getSigningKey: async () => signer, + serverName: inboundServer, + } as unknown as typeof config; + + stateService = new StateService( + repositories.states, + repositories.events, + newConfig, + ); + }); + // this should be changed as needed + const originalKeyResponse = { + old_verify_keys: {}, + server_name: inboundServer, + signatures: {}, + verify_keys: { + [signer.id]: { + key: Buffer.from(signer.getPublicKey()).toString('base64'), + }, + }, + valid_until_ts: Date.now() + 100000, + }; + + afterAll(async () => { + await mock.restore(); + await mock.module('../server-discovery/discovery', () => ({ + getHomeserverFinalAddress: originalServerDiscovery, + })); + }); + + // sanity check + it('should sign events with new keys', async () => { + const pdu = PersistentEventFactory.createFromRawEvent(event, roomVersion); + + await stateService.signEvent(pdu); + + expect(pdu.event.signatures[inboundServer]).toBeDefined(); + expect(pdu.event.signatures?.[inboundServer]).toHaveProperty( + `ed25519:${version}`, + ); + + // now this stateService will pretend to be the other homeserver + }); + + it('should fail if event has an invalid hash', async () => { + const eventCopy = JSON.parse(JSON.stringify(event)); + eventCopy.content.avatar_url = undefined; + + await expect( + eventService.validateHashAndSignatures(eventCopy, roomVersion), + ).rejects.toThrowError(/M_INVALID_HASH/); + }); + + it('should successfully validate hash and signature (happy path)', async () => { + // 1. create an event + const pdu = PersistentEventFactory.newCreateEvent( + `@creator:${inboundServer}`, + roomVersion, + ); + + await stateService.signEvent(pdu); + + // now OUR event service gets this event + // to allow fetchign the key we mock + fetchJsonMock.mockReturnValue(Promise.resolve(originalKeyResponse)); + + await expect( + eventService.validateHashAndSignatures(pdu.event, roomVersion), + ).resolves.toHaveProperty('eventId', pdu.eventId); + }); + + it('should fail if signed by a key expired at the point of event creation', async () => { + const pdu = PersistentEventFactory.newCreateEvent( + `@creator:${inboundServer}`, + roomVersion, + ); + + // event created NOW, so will we sign + await stateService.signEvent(pdu); + + // but the key is expired + const expiredKeyResponse = { + ...originalKeyResponse, + valid_until_ts: Date.now() - 10000000, + }; + fetchJsonMock.mockReturnValue(Promise.resolve(expiredKeyResponse)); + + await expect( + eventService.validateHashAndSignatures(pdu.event, roomVersion), + ).rejects.toThrow(); + + // not enough, now we add the key to old_verify_keys + const oldKeyResponse = { + ...originalKeyResponse, + verify_keys: {}, + old_verify_keys: { + [signer.id]: { + key: Buffer.from(signer.getPublicKey()).toString('base64'), + expired_ts: Date.now() - 1000, + }, + }, + }; + fetchJsonMock.mockReturnValue(Promise.resolve(oldKeyResponse)); + + // need to invalidate the cache though + // new room id does it + const pdu2 = PersistentEventFactory.newCreateEvent( + `@creator:${inboundServer}`, + roomVersion, + ); + + // event created NOW, so will we sign + await stateService.signEvent(pdu2); + + await expect( + eventService.validateHashAndSignatures(pdu2.event, roomVersion), + ).rejects.toThrow(); + }); + + it('should fail if signed by an unknown key', async () => { + // 1. create an event + const pdu = PersistentEventFactory.newCreateEvent( + `@creator:${inboundServer}`, + roomVersion, + ); + + await stateService.signEvent(pdu); + + // don't send any keys + fetchJsonMock.mockReturnValue( + Promise.resolve({ ...originalKeyResponse, verify_keys: {} }), + ); + + await expect( + eventService.validateHashAndSignatures(pdu.event, roomVersion), + ).rejects.toThrow(); + }); + + it('should pass if signed by an old key', async () => { + const pdu = PersistentEventFactory.newCreateEvent( + `@creator:${inboundServer}`, + roomVersion, + ); + + (pdu as any).rawEvent.origin_server_ts -= 2000; // slightly older event + + // event created NOW, so will we sign + await stateService.signEvent(pdu); + + // key is expired but valid at event time + const oldKeyResponse = { + ...originalKeyResponse, + verify_keys: {}, + old_verify_keys: { + [signer.id]: { + key: Buffer.from(signer.getPublicKey()).toString('base64'), + expired_ts: pdu.originServerTs + 1, + }, + }, + }; + fetchJsonMock.mockReturnValue(Promise.resolve(oldKeyResponse)); + + await expect( + eventService.validateHashAndSignatures(pdu.event, roomVersion), + ).resolves.toHaveProperty('eventId', pdu.eventId); + }); + }); +}); diff --git a/packages/federation-sdk/src/services/event.service.ts b/packages/federation-sdk/src/services/event.service.ts index ea9030133..8dd07ff65 100644 --- a/packages/federation-sdk/src/services/event.service.ts +++ b/packages/federation-sdk/src/services/event.service.ts @@ -10,19 +10,27 @@ import type { RedactionEvent } from '@rocket.chat/federation-core'; import { generateId } from '@rocket.chat/federation-core'; import type { EventStore } from '@rocket.chat/federation-core'; import { pruneEventDict } from '@rocket.chat/federation-core'; - -import { checkSignAndHashes } from '@rocket.chat/federation-core'; -import { createLogger } from '@rocket.chat/federation-core'; +} from '@hs/core' +import { isPresenceEDU, isTypingEDU } from '@hs/core'; +import type { RedactionEvent } from '@hs/core'; +import { generateId } from '@hs/core'; +import type { EventStore } from '@hs/core'; +import { pruneEventDict } from '@hs/core'; + +import { createLogger } from '@hs/core'; import { type EventID, type Pdu, type PduForType, type PduType, + PersistentEventBase, PersistentEventFactory, RoomID, RoomVersion, getAuthChain, } from '@rocket.chat/federation-room'; +RoomVersion, +} from '@hs/room' import { singleton } from 'tsyringe'; import type { z } from 'zod'; import { StagingAreaQueue } from '../queues/staging-area.queue'; @@ -34,6 +42,8 @@ import { ConfigService } from './config.service'; import { EventEmitterService } from './event-emitter.service'; import { ServerService } from './server.service'; import { StateService } from './state.service'; +import { KeyService, OldVerifierKey } from './key.service'; +import { SignatureVerificationService } from './signature-verification.service'; export interface AuthEventParams { roomId: string; @@ -57,6 +67,9 @@ export class EventService { private readonly serverService: ServerService, private readonly eventEmitterService: EventEmitterService, + + private readonly keyService: KeyService, + private readonly signatureVerificationService: SignatureVerificationService, ) { // on startup we look for old staged events and try to process them setTimeout(() => { @@ -164,26 +177,33 @@ export class EventService { eventsByRoomId.get(roomId)?.push(event); } - const roomIdToRoomVersionmap = new Map(); - const getRoomVersion = async (roomId: RoomID) => { - if (roomIdToRoomVersionmap.has(roomId)) { - return roomIdToRoomVersionmap.get(roomId) as RoomVersion; + const roomVersionByRoomId = new Map(); + const getRoomVersion = async (roomId: string): Promise => { + if (roomVersionByRoomId.has(roomId)) { + return roomVersionByRoomId.get(roomId) as RoomVersion; } - const roomVersion = await this.getRoomVersion({ room_id: roomId }); - - roomIdToRoomVersionmap.set(roomId, roomVersion); + const roomVersion = await this.stateService.getRoomVersion(roomId); + if (roomVersion) { + roomVersionByRoomId.set(roomId, roomVersion); + return roomVersion; + } - return roomVersion; + throw new Error('M_UNKNOWN_ROOM_VERSION'); }; // process each room's events in parallel // TODO implement a concurrency limit await Promise.all( Array.from(eventsByRoomId.entries()).map(async ([roomId, events]) => { + // TODO: need to validate using zod schemas + + const roomVersion = await getRoomVersion(roomId); + for await (const event of events) { + let pdu: PersistentEventBase; try { - await this.validateEvent(event); + pdu = await this.validateEvent(event, roomVersion); } catch (err) { this.logger.error({ msg: 'Event validation failed', @@ -194,19 +214,10 @@ export class EventService { continue; } - const roomVersion = await getRoomVersion(event.room_id); - - const pdu = PersistentEventFactory.createFromRawEvent( - event, - roomVersion, - ); - - const eventId = pdu.eventId; - - const existing = await this.eventRepository.findById(eventId); + const existing = await this.eventRepository.findById(pdu.eventId); if (existing) { this.logger.info( - `Ignoring received event ${eventId} which we have already seen`, + `Ignoring received event ${pdu.eventId} which we have already seen`, ); // TODO we may need to check if an event is an outlier and re-process it @@ -214,7 +225,7 @@ export class EventService { } // save the event as staged to be processed - await this.eventStagingRepository.create(eventId, origin, event); + await this.eventStagingRepository.create(pdu.eventId, origin, event); // acquire a lock for processing the event const lock = await this.lockRepository.getLock( @@ -234,27 +245,19 @@ export class EventService { } }), ); - } - private async validateEvent(event: Pdu): Promise { - const roomVersion = await this.getRoomVersion(event); - if (!roomVersion) { - throw new Error('M_UNKNOWN_ROOM_VERSION'); - } - - if ( - event.type === 'm.room.member' && - event.content.membership === 'invite' && - 'third_party_invite' in event.content - ) { - throw new Error('Third party invites are not supported'); - } + // don't want the cache to indefinitely grow, nor cause stale keys to stay + // once a transaction is dfone, we can clear the cache + this.cachedVerifierKey.clear(); + } - const origin = event.sender.split(':').pop(); - if (!origin) { - throw new Error('Event sender is missing domain'); - } + private cachedVerifierKey: Map<`${string}:${string}`, OldVerifierKey> = + new Map(); + private async validateEvent( + event: Pdu, + roomVersion: RoomVersion, + ): Promise { const eventSchema = this.getEventSchema(roomVersion, event.type); const validationResult = eventSchema.safeParse(event); @@ -282,9 +285,81 @@ export class EventService { throw new Error('M_MISSING_SIGNATURES_OR_HASHES'); } - await checkSignAndHashes(event, origin, (origin, key) => { - return this.serverService.getPublicKey(origin, key); - }); + return this.validateHashAndSignatures(event, roomVersion); + } + + public async validateHashAndSignatures( + event: Pdu, + roomVersion: RoomVersion, + ): Promise { + const pdu = PersistentEventFactory.createFromRawEvent(event, roomVersion); + + const expectedHash = event.hashes.sha256; + + const hash = pdu.getContentHashString(); + + if (expectedHash !== hash) { + this.logger.error( + { event: pdu.event, expectedHash, hash }, + 'content hash validation failed', + ); + throw new Error('M_INVALID_HASH'); + } + + const keysRequired = pdu.getOriginKeys(); + + this.logger.debug( + { keys: keysRequired, eventId: pdu.eventId }, + 'Keys required to verify event', + ); + + for (const keyId of keysRequired) { + const cacheKey = `${pdu.roomId}:${keyId}` as const; + const verifier = this.cachedVerifierKey.get(cacheKey); + if (verifier) { + if (this.keyService.isVerifierAllowedToCheckEvent(pdu, verifier)) { + this.logger.debug( + { + id: verifier.key.id, + }, + 'using cached verifier', + ); + await this.signatureVerificationService.verifyEventSignature( + pdu, + verifier.key, + ); + + return pdu; + } + } + } + + this.logger.info( + { eventId: pdu.eventId }, + 'keys not in cache, fetching from KeyService', + ); + + const requiredVerifier = + await this.keyService.getRequiredVerifierForEvent(pdu); + + this.logger.debug( + { + eventId: pdu.eventId, + verifier: { + id: requiredVerifier.key.id, + expiredAt: requiredVerifier.expiredAt.toISOString(), + }, + }, + 'verifier required for pdu validation', + ); + + this.cachedVerifierKey.set(requiredVerifier.key.id, requiredVerifier); + + await this.signatureVerificationService.verifyEventSignature( + pdu, + requiredVerifier.key, + ); + return pdu; } private async processIncomingEDUs(edus: BaseEDU[]): Promise { @@ -455,7 +530,7 @@ export class EventService { return parts.length > 1 ? parts[1] : ''; } - private async getRoomVersion(event: Pick) { + public async getRoomVersion(event: Pdu) { return ( this.stateService.getRoomVersion(event.room_id) || PersistentEventFactory.defaultRoomVersion diff --git a/packages/federation-sdk/src/services/federation-request.service.ts b/packages/federation-sdk/src/services/federation-request.service.ts index 43b52caba..5d624bcdc 100644 --- a/packages/federation-sdk/src/services/federation-request.service.ts +++ b/packages/federation-sdk/src/services/federation-request.service.ts @@ -19,7 +19,6 @@ import { EncryptionValidAlgorithm } from '@hs/core'; import { createLogger } from '@hs/core'; import { fetch } from '@hs/core'; import { singleton } from 'tsyringe'; -import * as nacl from 'tweetnacl'; import { getHomeserverFinalAddress } from '../server-discovery/discovery'; import { ConfigService } from './config.service'; import { signJson } from '@hs/crypto'; diff --git a/packages/federation-sdk/src/services/key.service.spec.ts b/packages/federation-sdk/src/services/key.service.spec.ts new file mode 100644 index 000000000..d05e3c663 --- /dev/null +++ b/packages/federation-sdk/src/services/key.service.spec.ts @@ -0,0 +1,227 @@ +import { verifyJsonSignature } from '@hs/crypto'; + +import { describe, expect, it, Mock, mock } from 'bun:test'; +import { afterEach, beforeEach } from 'node:test'; + +import { signer } from '../__mocks__/singer.spec'; +import { keyService } from '../__mocks__/services.spec'; +import { config } from '../__mocks__/config.service.spec'; + +describe('KeyService', async () => { + // fetch mocking + let originalFetch: typeof globalThis.fetch; + let inboundServer = ''; // skips server discovery + + beforeEach(() => { + originalFetch = globalThis.fetch; + globalThis.fetch = Object.assign( + async (_url: string, _options?: RequestInit) => { + return { + ok: true, + status: 200, + json: fetchJsonMock as unknown as FetchJson, + } as Response; + }, + { preconnect: () => {} }, + ) as typeof fetch; + inboundServer = `localhost:${Math.floor(Math.random() * 10000)}`; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + mock.restore(); + }); + + type FetchJson = Awaited>['json']; + + const fetchJsonMock: Mock = mock(() => Promise.resolve()); + const publicKey = Buffer.from(signer.getPublicKey()).toString('base64'); + + it('should act as a notary server', async () => { + fetchJsonMock.mockReturnValue( + Promise.resolve({ + server_name: inboundServer, + valid_until_ts: Date.now() + 100000, + verify_keys: { + 'ed25519:0': { key: publicKey }, + }, + signatures: { + [inboundServer]: { + 'ed25519:0': 'c2lnbmF0dXJl', // dummy signature, not verified in this test + }, + }, + old_verify_keys: {}, + }), + ); + + const response = await keyService.handleQuery({ + server_keys: { [inboundServer]: {} }, + }); + + expect(response).toHaveProperty('server_keys'); + expect(response.server_keys).toBeArray(); + + const key = response.server_keys.find( + (k: unknown) => + typeof k === 'object' && + k !== null && + 'server_name' in k && + k.server_name === inboundServer && + 'verify_keys' in k && + typeof k.verify_keys === 'object' && + k.verify_keys !== null && + 'ed25519:0' in k.verify_keys && + k.verify_keys['ed25519:0'], + ); + + expect(key).toBeDefined(); + + expect(key).toHaveProperty('verify_keys'); + expect(key.verify_keys).toHaveProperty('ed25519:0'); + expect(key.verify_keys['ed25519:0']).toHaveProperty('key'); + expect(key.verify_keys['ed25519:0'].key).toBeString(); + expect(key.verify_keys['ed25519:0'].key).toBe(publicKey); + + const signature = key?.signatures?.[config.serverName]; + + expect(signature).toBeDefined(); + expect(Object.keys(signature).length).toBeGreaterThanOrEqual(1); + + const signatureValue = signature?.['ed25519:0']; + + expect(signatureValue).toBeDefined(); + + const { signatures, ...rest } = key; + + expect( + verifyJsonSignature(rest, signatureValue, signer), + ).resolves.toBeUndefined(); + }); + + it('should return an expired key if it can not find any others', async () => { + const keyId0 = 'ed25519:0'; + // -24 hours + const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000); + + const keyResponse0 = { + server_name: inboundServer, + valid_until_ts: expiresAt.getTime(), + verify_keys: { + [keyId0]: { key: publicKey }, + }, + old_verify_keys: {}, + signatures: {}, + }; + + fetchJsonMock.mockReturnValue(Promise.resolve(keyResponse0)); + + // fills the database with an expired key + await keyService.handleQuery({ + server_keys: { [inboundServer]: { [keyId0]: {} } }, + }); + + // make a second request + await keyService.handleQuery({ + server_keys: { + [inboundServer]: { + [keyId0]: { + minimum_valid_until_ts: expiresAt.getTime() + 1000, + }, + }, + }, + }); + + // FIXME: don't do it here + globalThis.fetch = Object.assign( + async (_url: string, _options?: RequestInit) => { + return { + ok: false, + status: 400, + } as Response; + }, + { preconnect: () => {} }, + ) as typeof fetch; + + const { server_keys: serverKeys } = await keyService.handleQuery({ + server_keys: { + [inboundServer]: { + [keyId0]: { + minimum_valid_until_ts: expiresAt.getTime() + 1000, + }, + }, + }, + }); + + expect(serverKeys).toBeArray(); + expect(serverKeys[0]).toHaveProperty('server_name', inboundServer); + expect(serverKeys[0].verify_keys).toHaveProperty(keyId0); + expect(serverKeys[0].valid_until_ts).toBe(expiresAt.getTime()); + }); + + it('must not overwrite a valid key with a spurious result from the origin server', async () => { + const keyid1 = 'ed25519:1'; + // -24 houts + const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000); + + const keyResponse1 = { + server_name: inboundServer, + valid_until_ts: expiresAt.getTime(), + verify_keys: { + [keyid1]: { key: publicKey }, + }, + old_verify_keys: {}, + signatures: {}, + }; + + fetchJsonMock.mockReturnValue(Promise.resolve(keyResponse1)); + + const response1 = await keyService.handleQuery({ + server_keys: { [inboundServer]: { [keyid1]: {} } }, + }); + + expect(response1.server_keys[0]).toHaveProperty( + 'server_name', + inboundServer, + ); + expect(response1.server_keys[0].verify_keys).toHaveProperty(keyid1); + + const keyid2 = 'ed25519:2'; + fetchJsonMock.mockReturnValue( + Promise.resolve({ + server_name: inboundServer, + valid_until_ts: expiresAt.getTime() + 1000, + verify_keys: { + [keyid2]: { key: publicKey }, + }, + old_verify_keys: {}, + signatures: {}, + }), + ); + + await keyService.handleQuery({ + server_keys: { + [inboundServer]: { + [keyid1]: { + minimum_valid_until_ts: Date.now(), + }, + }, + }, + }); + + const finalResponse = await keyService.handleQuery({ + server_keys: { + [inboundServer]: { + [keyid1]: { + minimum_valid_until_ts: expiresAt.getTime(), + }, + }, + }, + }); + + expect(finalResponse.server_keys[0]).toHaveProperty( + 'server_name', + inboundServer, + ); + expect(finalResponse.server_keys[0].verify_keys).toHaveProperty(keyid1); + }); +}); diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index eecea572e..c5a7bdd6a 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -6,16 +6,17 @@ import { isValidAlgorithm, loadEd25519VerifierFromPublicKey, signJson, + VerifierKey, type Signer, } from '@hs/crypto'; import { - fetch as _myFetch, + fetch as coreFetch, type KeyV2ServerResponse, type ServerKey, } from '@hs/core'; import { createLogger } from '../utils/logger'; import { getHomeserverFinalAddress } from '../server-discovery/discovery'; -import { ConnectionCheckOutFailedEvent } from 'mongodb'; +import { PersistentEventBase } from '@hs/room'; type QueryCriteria = { // If not supplied, the current time as determined by the notary server is used. @@ -44,6 +45,13 @@ function isKeyV2ServerResponse(obj: unknown): obj is KeyV2ServerResponse { return false; } +// to help with caching and cehcking if a key can still be used +// check isVerifierAllowedToCheckEvent +export type OldVerifierKey = { + key: VerifierKey; + expiredAt: Date; +}; + @singleton() export class KeyService { private signer: Signer | undefined; @@ -58,17 +66,25 @@ export class KeyService { }); } - private shouldRefetchKey( - localKey: ServerKey, - keyId: string, - validUntil?: number, - ) { - const key = localKey.keys[keyId]; + public isVerifierAllowedToCheckEvent( + event: PersistentEventBase, + verifier: OldVerifierKey, + ): boolean { + if (event.originServerTs > verifier.expiredAt.getTime()) { + this.logger.warn( + `Key ${verifier.key.id} expired at ${verifier.expiredAt.toISOString()} cannot be used to verify event from ${event.origin} with originServerTs ${new Date(event.originServerTs).toISOString()}`, + ); + return false; + } - const { serverName } = localKey; + return true; + } + + private shouldRefetchKey(key: ServerKey, validUntil?: number) { + const { serverName } = key; if (validUntil) { - if (key.expiresAt < validUntil) { + if (key.expiresAt.getTime() < validUntil) { this.logger.warn( `Key for ${serverName} is expired, ${key.expiresAt} < ${validUntil}, refetching keys`, ); @@ -80,31 +96,30 @@ export class KeyService { // SPEC: Intermediate notary servers should cache a response for half of its lifetime to avoid serving a stale response. // this could be part of an aggregation, however, the key data stored aren't big enough to justify the complexity right now. - if ((key._updatedAt.getTime() + key.expiresAt) / 2 < Date.now()) { + if ((key._updatedAt.getTime() + key.expiresAt.getTime()) / 2 < Date.now()) { this.logger.warn(`Half life for key for ${serverName} is expired`); return true; } } - async fetchKeysFromRemoteServerRaw( + async fetchAndSaveKeysFromRemoteServerRaw( serverName: string, ): Promise { - // FIXME: - // const [address, hostHeaders] = await getHomeserverFinalAddress( - // serverName, - // this.logger, - // ); + const [address, hostHeaders] = await getHomeserverFinalAddress( + serverName, + this.logger, + ); // TODO: move this to federation service?? - // const keyV2ServerUrl = new URL(`${address}/_matrix/key/v2/server`); - - const response = await fetch( - `https://${serverName}/_matrix/key/v2/server`, - { - // headers: hostHeaders, - method: 'GET', - }, - ); + const keyV2ServerUrl = Bun + ? new URL(`https://${serverName}/_matrix/key/v2/server`) + : new URL(`${address}/_matrix/key/v2/server`); + + const response = await (Bun ? fetch : coreFetch)(keyV2ServerUrl, { + headers: hostHeaders, + method: 'GET', + signal: AbortSignal.timeout(10_000), + }); if (!response.ok) { this.logger.error( @@ -145,13 +160,7 @@ export class KeyService { private async storeKeysFromResponse( response: KeyV2ServerResponse, ): Promise { - const existingKey = await this.keyRepository.findByServerName( - response.server_name, - ); - const keys: ServerKey = { - serverName: response.server_name, - keys: existingKey ? existingKey.keys : {}, - }; + const keys: ServerKey[] = []; for (const [keyId, keyInfo] of Object.entries(response.verify_keys)) { const { version } = this.parseKeyId(keyId); @@ -161,14 +170,16 @@ export class KeyService { version, ); - keys.keys[verifier.id] = { + keys.push({ + serverName: response.server_name, + keyId: verifier.id, key: keyInfo.key, pem: verifier.getPublicKeyPem(), - _createdAt: existingKey?.keys?.[verifier.id]?._createdAt ?? new Date(), + _createdAt: new Date(), _updatedAt: new Date(), - expiresAt: response.valid_until_ts, - }; + expiresAt: new Date(response.valid_until_ts), + }); } for (const [keyId, keyInfo] of Object.entries(response.old_verify_keys)) { @@ -179,21 +190,26 @@ export class KeyService { version, ); - keys.keys[verifier.id] = { + keys.push({ + serverName: response.server_name, + keyId: verifier.id, key: keyInfo.key, pem: verifier.getPublicKeyPem(), - _createdAt: existingKey?.keys?.[verifier.id]?._createdAt ?? new Date(), + _createdAt: new Date(), _updatedAt: new Date(), - expiresAt: keyInfo.expired_ts, - }; + expiresAt: new Date(keyInfo.expired_ts), + }); } - await this.keyRepository.storeKeys(keys); + await Promise.all( + keys.map((key) => this.keyRepository.insertOrUpdateKey(key)), + ); } + // multiple keys -> single repnse async convertToKeyV2Response( - serverKey: ServerKey, + serverKeys: ServerKey[], minimumValidUntil = Date.now(), ): Promise { if (!this.signer) { @@ -206,26 +222,32 @@ export class KeyService { let validUntil = 0; - for (const [keyId, keyInfo] of Object.entries(serverKey.keys)) { - if (keyInfo.expiresAt > validUntil) { - validUntil = keyInfo.expiresAt; + const serverName = serverKeys[0]?.serverName; + + for (const key of serverKeys) { + if (key.serverName !== serverName) { + throw new Error('All keys must be from the same server'); + } + + if (key.expiresAt.getTime() > validUntil) { + validUntil = key.expiresAt.getTime(); } // if expired, move to old keys - if (keyInfo.expiresAt < minimumValidUntil) { - oldVerifyKeys[keyId] = { - expired_ts: keyInfo.expiresAt, - key: keyInfo.key, + if (key.expiresAt.getTime() < minimumValidUntil) { + oldVerifyKeys[key.keyId] = { + expired_ts: key.expiresAt.getTime(), + key: key.key, }; } else { - verifyKeys[keyId] = { - key: keyInfo.key, + verifyKeys[key.keyId] = { + key: key.key, }; } } const response = { - server_name: serverKey.serverName, + server_name: serverName, verify_keys: verifyKeys, old_verify_keys: oldVerifyKeys, valid_until_ts: validUntil, @@ -252,7 +274,7 @@ export class KeyService { const serverKeysResponse = [] as KeyV2ServerResponse[]; - const localKeys = [] as ServerKey[]; + const localKeysPerServer: Map = new Map(); for (const [serverName, query] of Object.entries(serverKeys)) { const keysAsked = Object.keys(query); @@ -260,14 +282,16 @@ export class KeyService { this.logger.debug({ serverName, query, keyIds: keysAsked }, 'keys asked'); if (keysAsked.length === 0) { - const keys = await this.keyRepository.findByServerName(serverName); + const keys = await this.keyRepository + .findByServerName(serverName) + .toArray(); - if (!keys) { + if (!keys.length) { this.logger.debug({ serverName, keys }, 'no cached keys found'); // no cache, fetch from remote and stpre try { const remoteKeys = - await this.fetchKeysFromRemoteServerRaw(serverName); + await this.fetchAndSaveKeysFromRemoteServerRaw(serverName); serverKeysResponse.push(remoteKeys); this.logger.debug( @@ -290,14 +314,10 @@ export class KeyService { } // check if any of the cached keys need refetching - if ( - Object.keys(keys.keys).every((keyId) => - this.shouldRefetchKey(keys, keyId), - ) - ) { + if (keys.every((key) => this.shouldRefetchKey(key))) { try { const remoteKeys = - await this.fetchKeysFromRemoteServerRaw(serverName); + await this.fetchAndSaveKeysFromRemoteServerRaw(serverName); serverKeysResponse.push(remoteKeys); this.logger.debug( @@ -314,7 +334,7 @@ export class KeyService { `Failed to fetch keys from remote server ${serverName}, continuing with cached keys if any`, ); - localKeys.push(keys); + localKeysPerServer.set(serverName, keys); } continue; @@ -322,7 +342,7 @@ export class KeyService { this.logger.debug({ serverName, keys }, 'using cached keys'); - localKeys.push(keys); + localKeysPerServer.set(serverName, keys); continue; } @@ -336,11 +356,9 @@ export class KeyService { } // intentionally querying for all keys from db, since asked, if we didn't find one, should trigger a refetch - const keysForQuery = - await this.keyRepository.findAllByServerNameAndKeyIds( - serverName, - keysAsked, - ); + const keysForQuery = await this.keyRepository + .findAllByServerNameAndKeyIds(serverName, keysAsked) + .toArray(); this.logger.debug( { serverName, query, keysForQuery }, @@ -348,19 +366,16 @@ export class KeyService { ); if ( - !keysForQuery || - keysAsked.every((keyId) => - this.shouldRefetchKey( - keysForQuery, - keyId, - query[keyId]?.minimum_valid_until_ts, - ), + keysForQuery.length === 0 || + keysForQuery.every((key) => + this.shouldRefetchKey(key, query[key.keyId]?.minimum_valid_until_ts), ) ) { + this.logger.debug('need to refetch keys'); // no valid cache, fetch from remote and stpre try { const remoteKeys = - await this.fetchKeysFromRemoteServerRaw(serverName); + await this.fetchAndSaveKeysFromRemoteServerRaw(serverName); // TODO: apply actual filter serverKeysResponse.push(remoteKeys); @@ -378,20 +393,22 @@ export class KeyService { `Failed to fetch keys from remote server ${serverName}, continuing with cached keys if any`, ); - if (keysForQuery) { - localKeys.push(keysForQuery); + if (keysForQuery.length !== 0) { + localKeysPerServer.set(serverName, keysForQuery); } } continue; } - localKeys.push(keysForQuery); + localKeysPerServer.set(serverName, keysForQuery); } const keys = await Promise.all([ // convert and sign - ...localKeys.map(this.convertToKeyV2Response.bind(this)), + ...localKeysPerServer + .values() + .map(this.convertToKeyV2Response.bind(this)), // sign with our keys ...serverKeysResponse.map(async (key): Promise => { const { signatures, ...rest } = key; @@ -413,4 +430,176 @@ export class KeyService { server_keys: keys, }; } + + // use only for request signature verification + // does nto include expired keys + async getRequestVerifier( + serverName: string, + keyId: string, + ): Promise { + const { version } = this.parseKeyId(keyId); + + const localKey = await this.keyRepository.findByServerNameAndKeyId( + serverName, + keyId, + new Date(), + ); + + if (localKey && !this.shouldRefetchKey(localKey)) { + const verifier = await loadEd25519VerifierFromPublicKey( + fromBase64ToBytes(localKey.key), // TODO: use saved pem here + version, + ); + + return verifier; + } + + // either no key saved or needs a refetch + const remoteKeys = + await this.fetchAndSaveKeysFromRemoteServerRaw(serverName); + if (!remoteKeys.verify_keys[keyId]) { + throw new Error(`Key ${keyId} not found on server ${serverName}`); + } + + if (remoteKeys.valid_until_ts < Date.now()) { + throw new Error(`Key ${keyId} from server ${serverName} is expired`); + } + + const verifier = await loadEd25519VerifierFromPublicKey( + fromBase64ToBytes(remoteKeys.verify_keys[keyId].key), + version, + ); + + return verifier; + } + + // use for event signature verification + // includes expired keys + async getEventVerifier( + serverName: string, + keyId: string, + expiredAt?: Date, + ): Promise { + const { version } = this.parseKeyId(keyId); + const localKey = await this.keyRepository.findByServerNameAndKeyId( + serverName, + keyId, + expiredAt, + ); + + if (localKey) { + this.logger.debug( + { serverName, keyId, expiredAt }, + 'Found local key for event verification', + ); + // we won't check for half life here, since this is for event signature verification, we need to use whatever is available andd is valid + const verifier = await loadEd25519VerifierFromPublicKey( + fromBase64ToBytes(localKey.key), + version, + ); + + return { + key: verifier, + expiredAt: localKey.expiresAt, + }; + } + + const expectedExpiry = expiredAt?.getTime() ?? Date.now(); + + this.logger.debug( + { serverName, keyId }, + `expected expiry: ${new Date(expectedExpiry).toISOString()}`, + ); + + const remoteKeys = + await this.fetchAndSaveKeysFromRemoteServerRaw(serverName); + + this.logger.debug({ response: remoteKeys }, 'Remote keys fetched'); + + if (remoteKeys.verify_keys[keyId]) { + // expired, against the required expiry time + if (remoteKeys.valid_until_ts < expectedExpiry) { + throw new Error(`Key ${keyId} from server ${serverName} is expired`); + } + + const expiredAt_ = new Date(remoteKeys.valid_until_ts); + const publicKey = remoteKeys.verify_keys[keyId].key; + const verifier = await loadEd25519VerifierFromPublicKey( + fromBase64ToBytes(publicKey), + version, + ); + + return { + key: verifier, + expiredAt: expiredAt_, + }; + } + + // either are valid for event signing + const publicKey = remoteKeys.old_verify_keys[keyId]; + if (!publicKey) { + throw new Error(`Key ${keyId} not found on server ${serverName}`); + } + + if (publicKey.expired_ts < expectedExpiry) { + throw new Error(`Key ${keyId} from server ${serverName} is expired`); + } + + const verifier = await loadEd25519VerifierFromPublicKey( + fromBase64ToBytes(publicKey.key), + version, + ); + + return { + key: verifier, + expiredAt: new Date(publicKey.expired_ts), + }; + } + + async getRequiredVerifierForEvent( + event: PersistentEventBase, + ): Promise { + this.logger.debug( + { eventId: event.eventId }, + 'Getting required verifier for event', + ); + + const signaturesFromOrigin = event.event.signatures[event.origin]; + + if (!signaturesFromOrigin) { + throw new Error(`No signatures from origin ${event.origin}`); + } + + // can be signed by multiple keys + for (const keyId of Object.keys(signaturesFromOrigin)) { + try { + const verifier = await this.getEventVerifier( + event.origin, + keyId, + new Date(event.originServerTs), + ); + + this.logger.debug( + { + eventId: event.eventId, + keyId, + origin: event.origin, + }, + 'Found verifier', + ); + + return verifier; + } catch (error) { + // if couldn't find, it's ok, try next + this.logger.warn( + { error, keyId, origin: event.origin }, + `Failed to get verifier for event from ${event.origin} with keyId ${keyId}`, + ); + } + } + + throw new Error( + `No valid signature keys found for origin ${event.origin} with supported algorithms`, + ); + } } diff --git a/packages/federation-sdk/src/services/signature-verification.service.spec.ts b/packages/federation-sdk/src/services/signature-verification.service.spec.ts index de66e5dae..bece74538 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.spec.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.spec.ts @@ -10,6 +10,12 @@ import { } from 'bun:test'; import { SignatureVerificationService } from './signature-verification.service'; import { PersistentEventFactory } from '@hs/room'; +import { + VerifierKey, + loadEd25519SignerFromSeed, + fromBase64ToBytes, + loadEd25519VerifierFromPublicKey, +} from '@hs/crypto'; const originServer = 'syn1.tunnel.dev.rocket.chat'; @@ -24,6 +30,7 @@ const event = { '$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs', ], content: { + avatar_url: null, displayname: 'debdut1', membership: 'join' as const, }, @@ -50,11 +57,9 @@ const event = { }, }; -describe('SignatureVerificationService', () => { +describe('SignatureVerificationService', async () => { let service: SignatureVerificationService; - let originalFetch: typeof globalThis.fetch; - const mockKeyData = { old_verify_keys: {}, server_name: 'syn1.tunnel.dev.rocket.chat', @@ -70,88 +75,57 @@ describe('SignatureVerificationService', () => { }, }; - type FetchJson = Awaited>['json']; - - const fetchJsonMock: Mock = mock(() => - Promise.resolve(mockKeyData), + const verifier = await loadEd25519VerifierFromPublicKey( + fromBase64ToBytes(mockKeyData.verify_keys[keyId].key), + 'a_FAET', ); beforeEach(() => { - originalFetch = globalThis.fetch; - service = new SignatureVerificationService(); // invalidates internal cache - - fetchJsonMock.mockReturnValue(Promise.resolve(mockKeyData)); - - globalThis.fetch = Object.assign( - async (_url: string, _options?: RequestInit) => { - return { - ok: true, - status: 200, - json: fetchJsonMock as unknown as FetchJson, - } as Response; - }, - { preconnect: () => {} }, - ) as typeof fetch; - }); - - afterEach(() => { - globalThis.fetch = originalFetch; - mock.restore(); }); describe('verifyEventSignature', async () => { - it('should verify a valid event signature', async () => { - const pdu = PersistentEventFactory.createFromRawEvent( - structuredClone(event), - '10', - ); + it('01 should verify a valid event signature', async () => { + const pdu = PersistentEventFactory.createFromRawEvent(event, '10'); - return expect(service.verifyEventSignature(pdu)).resolves.toBeUndefined(); + return expect( + service.verifyEventSignature(pdu, verifier), + ).resolves.toBeUndefined(); }); // each step of the spec - it('should fail if not signed by the origin server (1)', async () => { - const eventClone = structuredClone(event); - - const pdu = PersistentEventFactory.createFromRawEvent({ - ...eventClone, - signatures: {}, // no signatures - }); - - return expect(service.verifyEventSignature(pdu)).rejects.toThrow( - `No signature found for origin ${originServer}`, + it('02 should fail if not signed by the origin server (1)', async () => { + const pdu = PersistentEventFactory.createFromRawEvent( + { + ...event, + signatures: {}, // no signatures + }, + '10', ); - }); - it('should fail if signed by algorithm not supported by us (ed25519) (2)', async () => { - const eventClone = structuredClone(event); + return expect( + service.verifyEventSignature(pdu, verifier), + ).rejects.toThrow(`No signature found for origin ${originServer}`); + }); - const pdu = PersistentEventFactory.createFromRawEvent({ - ...eventClone, - signatures: { - [originServer]: { - // different algorithm - 'not-supported:0': event.signatures[originServer][keyId], + it('03 should fail if signed by algorithm not supported by us (ed25519) (2)', async () => { + const pdu = PersistentEventFactory.createFromRawEvent( + { + ...event, + signatures: { + [originServer]: { + // different algorithm + 'not-supported:0': event.signatures[originServer][keyId], + }, }, }, - }); - - return expect(service.verifyEventSignature(pdu)).rejects.toThrow( - `No valid signature keys found for origin ${originServer} with supported algorithms`, + '10', ); - }); - - it('should fail if service could not find the public key from the origin homeserver (3.1)', async () => { - const eventClone = structuredClone(event); - - const pdu = PersistentEventFactory.createFromRawEvent(eventClone); - // making fetch fail - fetchJsonMock.mockReturnValue(Promise.reject(new Error('network error'))); - - return expect(service.verifyEventSignature(pdu)).rejects.toThrow( - `No valid verification key found for origin ${originServer} with supported algorithms`, + return expect( + service.verifyEventSignature(pdu, verifier), + ).rejects.toThrow( + `No valid signature keys found for origin ${originServer} with supported algorithms`, ); }); @@ -169,20 +143,21 @@ describe('SignatureVerificationService', () => { }, ); - it('should fail if the signature itself is invalid (4.2)', async () => { - const eventClone = structuredClone(event); - - const pdu = PersistentEventFactory.createFromRawEvent({ - ...eventClone, - signatures: { - [originServer]: { - [keyId]: '@@@@', // invalid base64 + it('04 should fail if the signature itself is invalid (4.2)', async () => { + const pdu = PersistentEventFactory.createFromRawEvent( + { + ...event, + signatures: { + [originServer]: { + [keyId]: '@@@@', // invalid base64 + }, }, }, - }); + '10', + ); // should fail because the signature length isn't correct for ed25519 - await expect(service.verifyEventSignature(pdu)).rejects.toThrow( + await expect(service.verifyEventSignature(pdu, verifier)).rejects.toThrow( /Invalid signature length/, ); @@ -190,29 +165,84 @@ describe('SignatureVerificationService', () => { MAX_SIGNATURE_LENGTH_FOR_ED25519: 4, })); - await expect(service.verifyEventSignature(pdu)).rejects.toThrow( + await expect(service.verifyEventSignature(pdu, verifier)).rejects.toThrow( /Failed to decode base64 signature /, ); const anyString = 'abc123'; const base64String = btoa(anyString); // valid base64 but not a valid signature - const pdu2 = PersistentEventFactory.createFromRawEvent({ - ...eventClone, - signatures: { - [originServer]: { - [keyId]: base64String, + const pdu2 = PersistentEventFactory.createFromRawEvent( + { + ...event, + signatures: { + [originServer]: { + [keyId]: base64String, + }, }, }, - }); + '10', + ); await mock.module('./signature-verification.service', () => ({ MAX_SIGNATURE_LENGTH_FOR_ED25519: base64String.length, })); - await expect(service.verifyEventSignature(pdu2)).rejects.toThrow( - 'Invalid signature', - ); + await expect( + service.verifyEventSignature(pdu2, verifier), + ).rejects.toThrow('Invalid signature'); + }); + }); + + describe('verifyRequestSignature', async () => { + const seed = 'FC6cwY3DNmHo3B7GRugaHNyXz+TkBRVx8RvQH0kSZ04'; + const version = 'a_FAET'; + const signer = await loadEd25519SignerFromSeed( + fromBase64ToBytes(seed), + version, + ); + const verifier: VerifierKey = signer; + it('should successfully validate the request', async () => { + const header = + 'X-Matrix origin="syn1.tunnel.dev.rocket.chat",destination="syn2.tunnel.dev.rocket.chat",key="ed25519:a_FAET",sig="+MRd0eKdc/3T7mS7ZR+ltpOiN7RBXgfxTWWYLejy5gBRXG717aXHPCDm044D10kgqQvs2HqR3MdPEIx+2a0nDg"'; + + const body = { + edus: [ + { + content: { + push: [ + { + last_active_ago: 561472, + presence: 'unavailable', + user_id: '@debdut1:syn1.tunnel.dev.rocket.chat', + }, + ], + }, + edu_type: 'm.presence', + }, + ], + origin: 'syn1.tunnel.dev.rocket.chat', + origin_server_ts: 1757329414731, + pdus: [], + }; + + // PUT /_matrix/federation/v1/send/1757328278684 HTTP/1.1 + + const uri = '/_matrix/federation/v1/send/1757328278684'; + + const method = 'PUT'; + + await expect( + service.verifyRequestSignature( + { + uri, + method, + body, + authorizationHeader: header, + }, + verifier, + ), + ).resolves.toBeUndefined(); }); }); }); diff --git a/packages/federation-sdk/src/services/signature-verification.service.ts b/packages/federation-sdk/src/services/signature-verification.service.ts index 3f125de0a..207ce854a 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.ts @@ -8,28 +8,6 @@ import { } from '@hs/crypto'; import type { PersistentEventBase } from '@hs/room'; -interface KeyData { - server_name: string; - verify_keys: { - [keyId: string]: { - key: string; - }; - }; - old_verify_keys?: { - [keyId: string]: { - key: string; - expired_ts: number; - }; - }; -} - -// this is to not process the keyid string multiple times -type KeyId = { - alg: string; - ver: string; - id: string; -}; - // low cost optimization in case of bad implementations // ed25519 signatures in unpaddedbase64 are always 86 characters long (doing math here for future reference) // 64 bytes in general, base64 => 21*3 = 63 + 1 padding "==" => 21 * 4 = 84 + 2 padding "==" (each char one byte) => 88 characters @@ -53,17 +31,20 @@ export type FederationRequest = signature: Record>; }; +// Signature verification service, wihtout dependency on anything just implements the parts of spec that validates json signatures, from requyests and events export class SignatureVerificationService { private get logger() { return createLogger('SignatureVerificationService'); } - private cachedKeys = new Map(); /** * Implements part of SPEC: https://spec.matrix.org/v1.12/server-server-api/#validating-hashes-and-signatures-on-received-events * The event structure should be verifier by the time this method is utilized, thus justifying the use of PersistentEventBase. */ - async verifyEventSignature(event: PersistentEventBase): Promise { + async verifyEventSignature( + event: PersistentEventBase, + verifier: VerifierKey, + ): Promise { // SPEC: First the signature is checked. The event is redacted following the redaction algorithm const { redactedEvent, origin } = event; @@ -75,10 +56,74 @@ export class SignatureVerificationService { const { unsigned, ...toCheck } = redactedEvent; - await this.verifySignature(toCheck, origin); + await this.verifySignature(toCheck, origin, verifier); } - async verifyRequestSignature() {} + async verifyRequestSignature( + { + authorizationHeader, + method, + body, + uri, + }: { + authorizationHeader: string; + method: string; // TODO: type better + body: object | undefined; + uri: string; + }, + verifier: VerifierKey, + ) { + // `X-Matrix origin="${origin}",destination="${destination}",key="${key}",sig="${signed}"` + + const regex = /\b(origin|destination|key|sig)="([^"]+)"/g; + const { + origin, + destination, + key, + sig: signature, + ...rest + } = Object.fromEntries( + [...authorizationHeader.matchAll(regex)].map( + ([, key, value]) => [key, value] as const, + ), + ); + + if (Object.keys(rest).length) { + // it should never happen since the regex should match all the parameters + throw new Error('Invalid authorization header, unexpected parameters'); + } + + if ([origin, destination, key, signature].some((value) => !value)) { + throw new Error('Invalid authorization header'); + } + + /* + { + "method": "POST", + "uri": "/target", + "origin": "origin.hs.example.com", + "destination": "destination.hs.example.com", + "content": , + "signatures": { + "origin.hs.example.com": { + "ed25519:key1": "ABCDEF..." + } + } + } + */ + const toVerify = { + method, + uri, + origin, + destination, + ...(body && { content: body }), + signatures: { + [origin]: { [key]: signature }, + }, + }; + + await this.verifySignature(toVerify, origin, verifier); + } /** * Implements SPEC: https://spec.matrix.org/v1.12/appendices/#checking-for-a-signature @@ -87,7 +132,7 @@ export class SignatureVerificationService { T extends { signatures: Record>; }, - >(data: T, origin: string) { + >(data: T, origin: string, verifier: VerifierKey) { // 1. Checks if the signatures member of the object contains an entry with the name of the entity. If the entry is missing then the check fails. const originSignature = data.signatures?.[origin]; if (!originSignature) { @@ -96,7 +141,7 @@ export class SignatureVerificationService { // 2. Removes any signing key identifiers from the entry with algorithms it doesn’t understand. If there are no signing key identifiers left then the check fails. const signatureEntries = Object.entries(originSignature); - const validSignatureEntries = [] as Array<[KeyId, string /* signature */]>; + const validSignatureEntries = new Map(); for (const [keyId, signature] of signatureEntries) { const parts = keyId.split(':'); if (parts.length < 2) { @@ -105,19 +150,15 @@ export class SignatureVerificationService { } const algorithm = parts[0]; - const version = parts[1]; if (!isValidAlgorithm(algorithm)) { this.logger.warn(`Unsupported algorithm: ${algorithm}`); continue; // we discard this entry but we do not fail yet } - validSignatureEntries.push([ - { alg: algorithm, ver: version, id: keyId }, - signature as string, - ]); + validSignatureEntries.set(keyId, signature); } - if (validSignatureEntries.length === 0) { + if (validSignatureEntries.size === 0) { throw new Error( `No valid signature keys found for origin ${origin} with supported algorithms`, ); @@ -126,19 +167,7 @@ export class SignatureVerificationService { // 3. Looks up verification keys for the remaining signing key identifiers either from a local cache or by consulting a trusted key server. If it cannot find a verification key then the check fails. // one origin can sign with multiple keys - given how the spec AND the schema structures it. // we do NOT need all though, one is enough, one that we can fetch first - let verifier: VerifierKey | undefined; - for (const [keyId] of validSignatureEntries) { - try { - verifier = await this.getSignatureVerifierForServer(origin, keyId); - break; // found one, should be enough - } catch (error) { - this.logger.warn( - `Failed to get verifier for ${origin} with keyId ${keyId.id}: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - if (!verifier) { + if (!validSignatureEntries.has(verifier.id)) { throw new Error( `No valid verification key found for origin ${origin} with supported algorithms`, ); @@ -159,12 +188,10 @@ export class SignatureVerificationService { ); } - let signatureBytes: Uint8Array; - try { - signatureBytes = fromBase64ToBytes(signatureEntry); - } catch (error) { + const signatureBytes = fromBase64ToBytes(signatureEntry); + if (signatureBytes.byteLength === 0) { throw new Error( - `Failed to decode base64 signature for keyId ${verifier.id} from origin ${origin}: ${error instanceof Error ? error.message : String(error)}`, + `Failed to decode base64 signature for keyId ${verifier.id} from origin ${origin}`, ); } @@ -177,63 +204,4 @@ export class SignatureVerificationService { // 7. Checks the signature bytes against the encoded object using the verification key. If this fails then the check fails. Otherwise the check succeeds. await verifier.verify(canonicalJson, signatureBytes); } - - // throws if no key - async getSignatureVerifierForServer( - serverName: string, - keyId: KeyId, - ): Promise { - const keyData = await this.getOrFetchPublicKey(serverName, keyId.id); - if (!keyData || !keyData.verify_keys[keyId.id]) { - throw new Error(`Public key not found for ${serverName}:${keyId.id}`); - } - - const publicKey = keyData.verify_keys[keyId.id].key; - - const verifier = await loadEd25519VerifierFromPublicKey( - fromBase64ToBytes(publicKey), - keyId.ver, - ); - - return verifier; - } - - /** - * Get public key from cache or fetch it from the server - */ - private async getOrFetchPublicKey( - serverName: string, - keyId: string, - ): Promise { - const cacheKey = `${serverName}:${keyId}`; - - if (this.cachedKeys.has(cacheKey)) { - return this.cachedKeys.get(cacheKey); - } - - try { - const response = await fetch( - `https://${serverName}/_matrix/key/v2/server`, - ); - - if (!response.ok) { - this.logger.error( - `Failed to fetch keys from ${serverName}: ${response.status}`, - ); - return null; - } - - const keyData = (await response.json()) as KeyData; - - this.cachedKeys.set(cacheKey, keyData); - - return keyData; - } catch (error: any) { - this.logger.error( - `Error fetching public key: ${error.message}`, - error.stack, - ); - return null; - } - } } diff --git a/packages/federation-sdk/src/services/state.service.ts b/packages/federation-sdk/src/services/state.service.ts index 4f3b63191..661633ed5 100644 --- a/packages/federation-sdk/src/services/state.service.ts +++ b/packages/federation-sdk/src/services/state.service.ts @@ -91,16 +91,28 @@ export class StateService { return event.content; } - async getRoomVersion(roomId: string): Promise { - const createEvent = await this.eventRepository.findByRoomIdAndType( - roomId, - 'm.room.create', + async getRoomVersion(roomId: string): Promise { + const createEntry = + await this.stateRepository.findCreateEventByRoomId(roomId); + if (!createEntry) { + throw new Error( + 'Create event not found for room version maybe event hasn;t been processed yet', + ); + } + + const createEvent = await this.eventRepository.findById( + createEntry.delta.eventId, ); if (!createEvent) { throw new UnknownRoomError(roomId as RoomID); } - return createEvent.event.content?.room_version as RoomVersion; + if (createEvent.event.type === 'm.room.create') { + return createEvent.event.content.room_version; + } + + // should be unreachable + throw new Error('Create event content malformed for room version'); } // helps with logging state diff --git a/packages/room/src/manager/event-wrapper.ts b/packages/room/src/manager/event-wrapper.ts index 8f201cfed..788921732 100644 --- a/packages/room/src/manager/event-wrapper.ts +++ b/packages/room/src/manager/event-wrapper.ts @@ -473,14 +473,8 @@ export abstract class PersistentEventBase< return this; } - toStrippedJson() { - return encodeCanonicalJson({ - eventId: this.eventId, - type: this.type, - roomId: this.roomId, - sender: this.sender, - stateKey: this.stateKey, - }); + getOriginKeys() { + return Object.keys(this.signatures[this.origin]); } } export type { EventStore }; diff --git a/packages/room/src/types/v3-11.ts b/packages/room/src/types/v3-11.ts index d0ae6cdd9..d7603f3fb 100644 --- a/packages/room/src/types/v3-11.ts +++ b/packages/room/src/types/v3-11.ts @@ -89,7 +89,7 @@ export const PduMembershipTypeSchema = z.enum([ ]); export const PduMembershipEventContentSchema = z.object({ - avatar_url: z.string().url().optional(), + avatar_url: z.string().url().nullish(), displayname: z.string().optional(), is_direct: z .boolean() From df8a8f6883a28399b932b473ba34d5996d2287b5 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Tue, 16 Sep 2025 14:44:33 +0530 Subject: [PATCH 20/56] reduce diff 1 --- benchmark-scripts/.gitignore | 34 ---- benchmark-scripts/README.md | 15 -- benchmark-scripts/bun.lock | 187 ------------------ benchmark-scripts/package.json | 17 -- .../signing-and-verifications/script.js | 28 --- .../signing-and-verifications/server.ts | 83 -------- benchmark-scripts/tsconfig.json | 27 --- 7 files changed, 391 deletions(-) delete mode 100644 benchmark-scripts/.gitignore delete mode 100644 benchmark-scripts/README.md delete mode 100644 benchmark-scripts/bun.lock delete mode 100644 benchmark-scripts/package.json delete mode 100644 benchmark-scripts/signing-and-verifications/script.js delete mode 100644 benchmark-scripts/signing-and-verifications/server.ts delete mode 100644 benchmark-scripts/tsconfig.json diff --git a/benchmark-scripts/.gitignore b/benchmark-scripts/.gitignore deleted file mode 100644 index a14702c40..000000000 --- a/benchmark-scripts/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/benchmark-scripts/README.md b/benchmark-scripts/README.md deleted file mode 100644 index c963c26ce..000000000 --- a/benchmark-scripts/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# benchmark-scripts - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.2.15. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/benchmark-scripts/bun.lock b/benchmark-scripts/bun.lock deleted file mode 100644 index 74a25e2c6..000000000 --- a/benchmark-scripts/bun.lock +++ /dev/null @@ -1,187 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "benchmark-scripts", - "devDependencies": { - "@types/bun": "latest", - "@types/express": "^5.0.3", - "express": "^5.1.0", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - - "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], - - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - - "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], - - "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="], - - "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], - - "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - - "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], - - "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], - - "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - - "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], - - "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], - - "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], - - "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], - - "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], - - "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - - "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - - "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - - "raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], - } -} diff --git a/benchmark-scripts/package.json b/benchmark-scripts/package.json deleted file mode 100644 index 6b817b353..000000000 --- a/benchmark-scripts/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "benchmark-scripts", - "module": "index.ts", - "type": "module", - "private": true, - "devDependencies": { - "@types/bun": "latest", - "@types/express": "^5.0.3", - "express": "^5.1.0" - }, - "peerDependencies": { - "typescript": "^5" - }, - "scripts": { - "crypto": "bun run ./signing-and-verifications/server.ts" - } -} diff --git a/benchmark-scripts/signing-and-verifications/script.js b/benchmark-scripts/signing-and-verifications/script.js deleted file mode 100644 index 83e00dfac..000000000 --- a/benchmark-scripts/signing-and-verifications/script.js +++ /dev/null @@ -1,28 +0,0 @@ -import http from 'k6/http'; - -function _payload() { - const messagelength = __ENV.MESSAGE_LENGTH || '1024'; - const engine = __ENV.ENGINE || 'tweetnacl'; - - const stream = !!__ENV.STREAM; - - return JSON.stringify({ - message: messagelength, - api: { - engine, - stream, - }, - }); -} - -const payload = _payload(); - -console.log('payload:', payload); - -export default function () { - http.post(`http://localhost:${__ENV.PORT || 8080}/signAndVerify`, payload, { - headers: { - 'content-type': 'application/json', - }, - }); -} diff --git a/benchmark-scripts/signing-and-verifications/server.ts b/benchmark-scripts/signing-and-verifications/server.ts deleted file mode 100644 index b837f3a2b..000000000 --- a/benchmark-scripts/signing-and-verifications/server.ts +++ /dev/null @@ -1,83 +0,0 @@ -import express from 'express'; -import nacl from 'tweetnacl'; - -import { encodeCanonicalJson } from '../../packages/crypto/src'; -import { loadEd25519SignerFromSeed } from '../../packages/crypto/src/utils/keys'; - -const app = express(); - -app.use(express.json()); - -type SignAndVerifyRequest = { - message: string | object; // allow big payloads - api: { - engine: 'native' | 'tweetnacl'; // sodium - stream: boolean; // whether to use streaming API or not - }; -}; - -const seedBytes = new Uint8Array(32).fill(1); // for testing, should be random in real world -const tweetKeyPair = nacl.sign.keyPair.fromSeed(seedBytes); - -function handleTweetnaclSignAndVerify(message: Uint8Array) { - const signature = nacl.sign.detached(message, tweetKeyPair.secretKey); - - nacl.sign.detached.verify(message, signature, tweetKeyPair.publicKey); -} - -const nativeSigner = await loadEd25519SignerFromSeed(seedBytes); - -async function handleNativeSignAndVerify(message: Uint8Array, stream: boolean) { - if (!stream) { - const signature = await nativeSigner.sign(message); - await nativeSigner.verify(message, signature); - return; - } -} - -// @ts-ignore -app.post('/signAndVerify', async (req, res) => { - const { message, api } = req.body as SignAndVerifyRequest; - if (!message) { - return res.status(400).json({ error: 'Message cannot be empty' }); - } - - const { engine, stream = false } = api; - - // encode should mimick copy of memory that we'll experience in real world, json -> string - const encodedMessage = new TextEncoder().encode( - typeof message === 'string' ? message : encodeCanonicalJson(message), - ); - - if (engine === 'tweetnacl') { - if (stream) { - return res - .status(400) - .json({ error: 'Stream not supported for tweetnacl' }); - } - - try { - handleTweetnaclSignAndVerify(encodedMessage); - return res.json({ success: true }); - } catch (e) { - console.error('Tweetnacl sign/verify error', e); - return res.status(500).json({ error: 'Signing or verification failed' }); - } - } - - if (engine === 'native') { - try { - await handleNativeSignAndVerify(encodedMessage, stream); - return res.json({ success: true }); - } catch (e) { - console.error('Native sign/verify error', e); - return res.status(500).json({ error: 'Signing or verification failed' }); - } - } - - return res.status(400).json({ error: 'Invalid engine' }); -}); - -const port = Number.parseInt(process.env.PORT || '', 10) || 8080; - -app.listen(port, '127.0.0.1', () => console.log(`Listening on ${port}`)); diff --git a/benchmark-scripts/tsconfig.json b/benchmark-scripts/tsconfig.json deleted file mode 100644 index 5d2612202..000000000 --- a/benchmark-scripts/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "outDir": "dist", - "sourceMap": false, - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "allowJs": true, - // Bundler mode - "moduleResolution": "node", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} From 53b0cce527efac19e0cf62dc4f2d08f57f1df3a5 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Tue, 16 Sep 2025 14:48:24 +0530 Subject: [PATCH 21/56] 2 --- packages/federation-sdk/src/services/config.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/federation-sdk/src/services/config.service.ts b/packages/federation-sdk/src/services/config.service.ts index b79ca923a..89068124e 100644 --- a/packages/federation-sdk/src/services/config.service.ts +++ b/packages/federation-sdk/src/services/config.service.ts @@ -6,6 +6,7 @@ import { } from '@rocket.chat/federation-core'; import { z } from 'zod'; +import { loadEd25519SignerFromSeed, fromBase64ToBytes } from '@hs/crypto'; export interface AppConfig { serverName: string; From c69bb92a9f5b0441372478fdd75a869ad897dbc8 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Tue, 16 Sep 2025 15:05:27 +0530 Subject: [PATCH 22/56] remove my bun hack --- .../src/services/federation-request.service.spec.ts | 3 +-- packages/federation-sdk/src/services/key.service.ts | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/federation-sdk/src/services/federation-request.service.spec.ts b/packages/federation-sdk/src/services/federation-request.service.spec.ts index a442f82cd..ed90dc017 100644 --- a/packages/federation-sdk/src/services/federation-request.service.spec.ts +++ b/packages/federation-sdk/src/services/federation-request.service.spec.ts @@ -8,8 +8,7 @@ import { mock, spyOn, } from 'bun:test'; -import * as core from '@rocket.chat/federation-core'; -import * as nacl from 'tweetnacl'; +import * as core from '@hs/core'; import { ConfigService } from './config.service'; import { FederationRequestService } from './federation-request.service'; import { loadEd25519SignerFromSeed, fromBase64ToBytes } from '@hs/crypto'; diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index c5a7bdd6a..5fb130cdb 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -111,11 +111,9 @@ export class KeyService { ); // TODO: move this to federation service?? - const keyV2ServerUrl = Bun - ? new URL(`https://${serverName}/_matrix/key/v2/server`) - : new URL(`${address}/_matrix/key/v2/server`); + const keyV2ServerUrl = new URL(`${address}/_matrix/key/v2/server`); - const response = await (Bun ? fetch : coreFetch)(keyV2ServerUrl, { + const response = await coreFetch(keyV2ServerUrl, { headers: hostHeaders, method: 'GET', signal: AbortSignal.timeout(10_000), From 9ba45fd433a7d104f9bfde39b6c955f642d0fd61 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Tue, 16 Sep 2025 15:39:19 +0530 Subject: [PATCH 23/56] fix races --- .../src/services/event.service.spec.ts | 51 ++++---- .../src/services/key.service.spec.ts | 28 ++--- .../signature-verification.service.spec.ts | 117 ++++++++++-------- 3 files changed, 94 insertions(+), 102 deletions(-) diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts index 778b04c7c..5e75df515 100644 --- a/packages/federation-sdk/src/services/event.service.spec.ts +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -1,5 +1,4 @@ import { - afterAll, afterEach, beforeEach, describe, @@ -7,17 +6,15 @@ import { it, Mock, mock, - test, } from 'bun:test'; import { eventService } from '../__mocks__/services.spec'; import { Pdu, PersistentEventFactory } from '@hs/room'; import { BaseEDU } from '@hs/core'; import { StateService } from './state.service'; -import { collections, repositories } from '../__mocks__/repositories.spec'; +import { repositories } from '../__mocks__/repositories.spec'; import { config } from '../__mocks__/config.service.spec'; import { loadEd25519SignerFromSeed } from '../../../crypto/dist/utils/keys'; import { fromBase64ToBytes } from '../../../crypto/dist/utils/data-types'; -import { prettyFactory } from 'pino-pretty'; const event = { auth_events: [ @@ -65,40 +62,41 @@ describe('EventService', async () => { '../server-discovery/discovery' ); - await mock.module('../server-discovery/discovery', () => ({ - // this mock doesn't matter, or doesn't change, we just need to skip actual server discovery - // and mock the /key/v2/server responses - getHomeserverFinalAddress: async (..._args: any[]) => [ - 'https://127.0.0.1', - {}, - ], - })); + const { fetch } = await import('@hs/core'); // random server name for each run let inboundServer = `localhost${Math.floor(Math.random() * 10000).toString()}`; - let originalFetch: typeof globalThis.fetch; - type FetchJson = Awaited>['json']; const fetchJsonMock: Mock = mock(() => Promise.resolve()); - beforeEach(() => { - originalFetch = globalThis.fetch; - globalThis.fetch = Object.assign( - async (_url: string, _options?: RequestInit) => { + beforeEach(async () => { + await mock.module('../server-discovery/discovery', () => ({ + // this mock doesn't matter, or doesn't change, we just need to skip actual server discovery + // and mock the /key/v2/server responses + getHomeserverFinalAddress: async (..._args: any[]) => [ + 'https://127.0.0.1', + {}, + ], + })); + + await mock.module('@hs/core', () => ({ + fetch: async (..._args: any[]) => { return { ok: true, status: 200, json: fetchJsonMock as unknown as FetchJson, } as Response; }, - { preconnect: () => {} }, - ) as typeof fetch; + })); }); - afterEach(() => { - globalThis.fetch = originalFetch; + afterEach(async () => { + await mock.module('@hs/core', () => ({ fetch })); + await mock.module('../server-discovery/discovery', () => ({ + getHomeserverFinalAddress: originalServerDiscovery, + })); mock.restore(); }); @@ -180,13 +178,6 @@ describe('EventService', async () => { valid_until_ts: Date.now() + 100000, }; - afterAll(async () => { - await mock.restore(); - await mock.module('../server-discovery/discovery', () => ({ - getHomeserverFinalAddress: originalServerDiscovery, - })); - }); - // sanity check it('should sign events with new keys', async () => { const pdu = PersistentEventFactory.createFromRawEvent(event, roomVersion); @@ -217,6 +208,8 @@ describe('EventService', async () => { roomVersion, ); + console.log('PDU', pdu.eventId); + await stateService.signEvent(pdu); // now OUR event service gets this event diff --git a/packages/federation-sdk/src/services/key.service.spec.ts b/packages/federation-sdk/src/services/key.service.spec.ts index d05e3c663..ba3eeaace 100644 --- a/packages/federation-sdk/src/services/key.service.spec.ts +++ b/packages/federation-sdk/src/services/key.service.spec.ts @@ -9,26 +9,25 @@ import { config } from '../__mocks__/config.service.spec'; describe('KeyService', async () => { // fetch mocking - let originalFetch: typeof globalThis.fetch; + const { fetch } = await import('@hs/core'); + let inboundServer = ''; // skips server discovery - beforeEach(() => { - originalFetch = globalThis.fetch; - globalThis.fetch = Object.assign( - async (_url: string, _options?: RequestInit) => { + beforeEach(async () => { + await mock.module('@hs/core', () => ({ + fetch: async (..._args: any[]) => { return { ok: true, status: 200, json: fetchJsonMock as unknown as FetchJson, } as Response; }, - { preconnect: () => {} }, - ) as typeof fetch; + })); inboundServer = `localhost:${Math.floor(Math.random() * 10000)}`; }); - afterEach(() => { - globalThis.fetch = originalFetch; + afterEach(async () => { + await mock.module('@hs/core', () => ({ fetch })); mock.restore(); }); @@ -131,17 +130,6 @@ describe('KeyService', async () => { }, }); - // FIXME: don't do it here - globalThis.fetch = Object.assign( - async (_url: string, _options?: RequestInit) => { - return { - ok: false, - status: 400, - } as Response; - }, - { preconnect: () => {} }, - ) as typeof fetch; - const { server_keys: serverKeys } = await keyService.handleQuery({ server_keys: { [inboundServer]: { diff --git a/packages/federation-sdk/src/services/signature-verification.service.spec.ts b/packages/federation-sdk/src/services/signature-verification.service.spec.ts index bece74538..28dc5dc64 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.spec.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.spec.ts @@ -6,7 +6,6 @@ import { it, mock, test, - type Mock, } from 'bun:test'; import { SignatureVerificationService } from './signature-verification.service'; import { PersistentEventFactory } from '@hs/room'; @@ -16,6 +15,7 @@ import { fromBase64ToBytes, loadEd25519VerifierFromPublicKey, } from '@hs/crypto'; +import { after } from 'node:test'; const originServer = 'syn1.tunnel.dev.rocket.chat'; @@ -80,10 +80,73 @@ describe('SignatureVerificationService', async () => { 'a_FAET', ); + const { MAX_SIGNATURE_LENGTH_FOR_ED25519 } = await import( + './signature-verification.service' + ); + beforeEach(() => { service = new SignatureVerificationService(); // invalidates internal cache }); + afterEach(async () => { + await mock.module('./signature-verification.service', () => ({ + MAX_SIGNATURE_LENGTH_FOR_ED25519, + })); + }); + + describe('verifyRequestSignature', async () => { + const seed = 'FC6cwY3DNmHo3B7GRugaHNyXz+TkBRVx8RvQH0kSZ04'; + const version = 'a_FAET'; + const signer = await loadEd25519SignerFromSeed( + fromBase64ToBytes(seed), + version, + ); + const thisVerifier: VerifierKey = signer; + + it('should successfully validate the request', async () => { + const header = + 'X-Matrix origin="syn1.tunnel.dev.rocket.chat",destination="syn2.tunnel.dev.rocket.chat",key="ed25519:a_FAET",sig="+MRd0eKdc/3T7mS7ZR+ltpOiN7RBXgfxTWWYLejy5gBRXG717aXHPCDm044D10kgqQvs2HqR3MdPEIx+2a0nDg"'; + + const body = { + edus: [ + { + content: { + push: [ + { + last_active_ago: 561472, + presence: 'unavailable', + user_id: '@debdut1:syn1.tunnel.dev.rocket.chat', + }, + ], + }, + edu_type: 'm.presence', + }, + ], + origin: 'syn1.tunnel.dev.rocket.chat', + origin_server_ts: 1757329414731, + pdus: [], + }; + + // PUT /_matrix/federation/v1/send/1757328278684 HTTP/1.1 + + const uri = '/_matrix/federation/v1/send/1757328278684'; + + const method = 'PUT'; + + await expect( + service.verifyRequestSignature( + { + uri, + method, + body, + authorizationHeader: header, + }, + thisVerifier, + ), + ).resolves.toBeUndefined(); + }); + }); + describe('verifyEventSignature', async () => { it('01 should verify a valid event signature', async () => { const pdu = PersistentEventFactory.createFromRawEvent(event, '10'); @@ -193,56 +256,4 @@ describe('SignatureVerificationService', async () => { ).rejects.toThrow('Invalid signature'); }); }); - - describe('verifyRequestSignature', async () => { - const seed = 'FC6cwY3DNmHo3B7GRugaHNyXz+TkBRVx8RvQH0kSZ04'; - const version = 'a_FAET'; - const signer = await loadEd25519SignerFromSeed( - fromBase64ToBytes(seed), - version, - ); - const verifier: VerifierKey = signer; - it('should successfully validate the request', async () => { - const header = - 'X-Matrix origin="syn1.tunnel.dev.rocket.chat",destination="syn2.tunnel.dev.rocket.chat",key="ed25519:a_FAET",sig="+MRd0eKdc/3T7mS7ZR+ltpOiN7RBXgfxTWWYLejy5gBRXG717aXHPCDm044D10kgqQvs2HqR3MdPEIx+2a0nDg"'; - - const body = { - edus: [ - { - content: { - push: [ - { - last_active_ago: 561472, - presence: 'unavailable', - user_id: '@debdut1:syn1.tunnel.dev.rocket.chat', - }, - ], - }, - edu_type: 'm.presence', - }, - ], - origin: 'syn1.tunnel.dev.rocket.chat', - origin_server_ts: 1757329414731, - pdus: [], - }; - - // PUT /_matrix/federation/v1/send/1757328278684 HTTP/1.1 - - const uri = '/_matrix/federation/v1/send/1757328278684'; - - const method = 'PUT'; - - await expect( - service.verifyRequestSignature( - { - uri, - method, - body, - authorizationHeader: header, - }, - verifier, - ), - ).resolves.toBeUndefined(); - }); - }); }); From df62bf7a77d6bd5b64943a32976b524942befbb3 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Tue, 16 Sep 2025 15:54:09 +0530 Subject: [PATCH 24/56] all lints --- .../src/__mocks__/repositories.spec.ts | 8 +++---- .../src/__mocks__/services.spec.ts | 10 ++++---- .../src/__mocks__/singer.spec.ts | 4 ++-- packages/federation-sdk/src/container.ts | 2 +- .../src/services/config.service.ts | 2 +- .../src/services/event.service.spec.ts | 13 +++++----- .../src/services/event.service.ts | 3 +-- .../federation-request.service.spec.ts | 2 +- .../services/federation-request.service.ts | 2 +- .../src/services/key.service.spec.ts | 6 ++--- .../src/services/key.service.ts | 24 +++++++++---------- .../src/services/server.service.ts | 2 +- .../signature-verification.service.spec.ts | 7 +++--- .../signature-verification.service.ts | 3 +-- .../src/controllers/key/server.controller.ts | 6 ++--- packages/room/src/manager/event-wrapper.ts | 5 +++- 16 files changed, 49 insertions(+), 50 deletions(-) diff --git a/packages/federation-sdk/src/__mocks__/repositories.spec.ts b/packages/federation-sdk/src/__mocks__/repositories.spec.ts index 2ff9b63a8..7b88784d3 100644 --- a/packages/federation-sdk/src/__mocks__/repositories.spec.ts +++ b/packages/federation-sdk/src/__mocks__/repositories.spec.ts @@ -1,10 +1,10 @@ import { EventStagingStore, EventStore, ServerKey } from '@hs/core'; -import { db } from './config.service.spec'; -import { KeyRepository } from '../repositories/key.repository'; -import { Lock, LockRepository } from '../repositories/lock.repository'; import { EventStagingRepository } from '../repositories/event-staging.repository'; -import { StateStore, StateRepository } from '../repositories/state.repository'; import { EventRepository } from '../repositories/event.repository'; +import { KeyRepository } from '../repositories/key.repository'; +import { Lock, LockRepository } from '../repositories/lock.repository'; +import { StateRepository, StateStore } from '../repositories/state.repository'; +import { db } from './config.service.spec'; const keysCollection = db.collection('test_keys'); const eventsCollection = db.collection('test_events'); diff --git a/packages/federation-sdk/src/__mocks__/services.spec.ts b/packages/federation-sdk/src/__mocks__/services.spec.ts index b81f80366..eb2c31c32 100644 --- a/packages/federation-sdk/src/__mocks__/services.spec.ts +++ b/packages/federation-sdk/src/__mocks__/services.spec.ts @@ -1,11 +1,11 @@ -import { KeyService } from '../services/key.service'; -import { config } from './config.service.spec'; -import { repositories } from './repositories.spec'; import { StagingAreaQueue } from '../queues/staging-area.queue'; -import { StateService } from '../services/state.service'; import { EventEmitterService } from '../services/event-emitter.service'; -import { SignatureVerificationService } from '../services/signature-verification.service'; import { EventService } from '../services/event.service'; +import { KeyService } from '../services/key.service'; +import { SignatureVerificationService } from '../services/signature-verification.service'; +import { StateService } from '../services/state.service'; +import { config } from './config.service.spec'; +import { repositories } from './repositories.spec'; const keyService = new KeyService(config, repositories.keys); diff --git a/packages/federation-sdk/src/__mocks__/singer.spec.ts b/packages/federation-sdk/src/__mocks__/singer.spec.ts index a4a6cbf6a..932c2e072 100644 --- a/packages/federation-sdk/src/__mocks__/singer.spec.ts +++ b/packages/federation-sdk/src/__mocks__/singer.spec.ts @@ -1,7 +1,7 @@ import { - loadEd25519SignerFromSeed, - fromBase64ToBytes, VerifierKey, + fromBase64ToBytes, + loadEd25519SignerFromSeed, } from '@hs/crypto'; const seed = 'zSkmr713LnEDbxlkYq2ZqIiKTQNsyMOU0T2CEeC44C4'; diff --git a/packages/federation-sdk/src/container.ts b/packages/federation-sdk/src/container.ts index 57ed46a6e..97722c670 100644 --- a/packages/federation-sdk/src/container.ts +++ b/packages/federation-sdk/src/container.ts @@ -33,6 +33,7 @@ import { EventService } from './services/event.service'; import { FederationRequestService } from './services/federation-request.service'; import { FederationService } from './services/federation.service'; import { InviteService } from './services/invite.service'; +import { KeyService } from './services/key.service'; import { MediaService } from './services/media.service'; import { MessageService } from './services/message.service'; import { MissingEventService } from './services/missing-event.service'; @@ -43,7 +44,6 @@ import { ServerService } from './services/server.service'; import { StagingAreaService } from './services/staging-area.service'; import { StateService } from './services/state.service'; import { WellKnownService } from './services/well-known.service'; -import { KeyService } from './services/key.service'; export interface FederationContainerOptions { emitter?: Emitter; diff --git a/packages/federation-sdk/src/services/config.service.ts b/packages/federation-sdk/src/services/config.service.ts index 89068124e..ddd9a46bf 100644 --- a/packages/federation-sdk/src/services/config.service.ts +++ b/packages/federation-sdk/src/services/config.service.ts @@ -5,8 +5,8 @@ import { toUnpaddedBase64, } from '@rocket.chat/federation-core'; +import { fromBase64ToBytes, loadEd25519SignerFromSeed } from '@hs/crypto'; import { z } from 'zod'; -import { loadEd25519SignerFromSeed, fromBase64ToBytes } from '@hs/crypto'; export interface AppConfig { serverName: string; diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts index 5e75df515..3eda26654 100644 --- a/packages/federation-sdk/src/services/event.service.spec.ts +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -1,20 +1,19 @@ import { + Mock, afterEach, beforeEach, describe, expect, it, - Mock, mock, } from 'bun:test'; -import { eventService } from '../__mocks__/services.spec'; -import { Pdu, PersistentEventFactory } from '@hs/room'; import { BaseEDU } from '@hs/core'; -import { StateService } from './state.service'; -import { repositories } from '../__mocks__/repositories.spec'; +import { fromBase64ToBytes, loadEd25519SignerFromSeed } from '@hs/crypto'; +import { Pdu, PersistentEventFactory } from '@hs/room'; import { config } from '../__mocks__/config.service.spec'; -import { loadEd25519SignerFromSeed } from '../../../crypto/dist/utils/keys'; -import { fromBase64ToBytes } from '../../../crypto/dist/utils/data-types'; +import { repositories } from '../__mocks__/repositories.spec'; +import { eventService } from '../__mocks__/services.spec'; +import { StateService } from './state.service'; const event = { auth_events: [ diff --git a/packages/federation-sdk/src/services/event.service.ts b/packages/federation-sdk/src/services/event.service.ts index 8dd07ff65..0a9149b7e 100644 --- a/packages/federation-sdk/src/services/event.service.ts +++ b/packages/federation-sdk/src/services/event.service.ts @@ -40,10 +40,9 @@ import { LockRepository } from '../repositories/lock.repository'; import { eventSchemas } from '../utils/event-schemas'; import { ConfigService } from './config.service'; import { EventEmitterService } from './event-emitter.service'; -import { ServerService } from './server.service'; -import { StateService } from './state.service'; import { KeyService, OldVerifierKey } from './key.service'; import { SignatureVerificationService } from './signature-verification.service'; +import { StateService } from './state.service'; export interface AuthEventParams { roomId: string; diff --git a/packages/federation-sdk/src/services/federation-request.service.spec.ts b/packages/federation-sdk/src/services/federation-request.service.spec.ts index ed90dc017..28f5401fc 100644 --- a/packages/federation-sdk/src/services/federation-request.service.spec.ts +++ b/packages/federation-sdk/src/services/federation-request.service.spec.ts @@ -9,9 +9,9 @@ import { spyOn, } from 'bun:test'; import * as core from '@hs/core'; +import { fromBase64ToBytes, loadEd25519SignerFromSeed } from '@hs/crypto'; import { ConfigService } from './config.service'; import { FederationRequestService } from './federation-request.service'; -import { loadEd25519SignerFromSeed, fromBase64ToBytes } from '@hs/crypto'; const signingKeyContent = 'ed25519 a_FAET FC6cwY3DNmHo3B7GRugaHNyXz+TkBRVx8RvQH0kSZ04'; diff --git a/packages/federation-sdk/src/services/federation-request.service.ts b/packages/federation-sdk/src/services/federation-request.service.ts index 5d624bcdc..9d309579d 100644 --- a/packages/federation-sdk/src/services/federation-request.service.ts +++ b/packages/federation-sdk/src/services/federation-request.service.ts @@ -18,10 +18,10 @@ import { extractURIfromURL } from '@hs/core'; import { EncryptionValidAlgorithm } from '@hs/core'; import { createLogger } from '@hs/core'; import { fetch } from '@hs/core'; +import { signJson } from '@hs/crypto'; import { singleton } from 'tsyringe'; import { getHomeserverFinalAddress } from '../server-discovery/discovery'; import { ConfigService } from './config.service'; -import { signJson } from '@hs/crypto'; interface SignedRequest { method: string; diff --git a/packages/federation-sdk/src/services/key.service.spec.ts b/packages/federation-sdk/src/services/key.service.spec.ts index ba3eeaace..1e8230e7d 100644 --- a/packages/federation-sdk/src/services/key.service.spec.ts +++ b/packages/federation-sdk/src/services/key.service.spec.ts @@ -1,11 +1,11 @@ import { verifyJsonSignature } from '@hs/crypto'; -import { describe, expect, it, Mock, mock } from 'bun:test'; +import { Mock, describe, expect, it, mock } from 'bun:test'; import { afterEach, beforeEach } from 'node:test'; -import { signer } from '../__mocks__/singer.spec'; -import { keyService } from '../__mocks__/services.spec'; import { config } from '../__mocks__/config.service.spec'; +import { keyService } from '../__mocks__/services.spec'; +import { signer } from '../__mocks__/singer.spec'; describe('KeyService', async () => { // fetch mocking diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index 5fb130cdb..99971c624 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -1,22 +1,22 @@ -import { singleton } from 'tsyringe'; -import { ConfigService } from './config.service'; -import { KeyRepository } from '../repositories/key.repository'; import { + type KeyV2ServerResponse, + type ServerKey, + fetch as coreFetch, +} from '@hs/core'; +import { + type Signer, + VerifierKey, fromBase64ToBytes, isValidAlgorithm, loadEd25519VerifierFromPublicKey, signJson, - VerifierKey, - type Signer, } from '@hs/crypto'; -import { - fetch as coreFetch, - type KeyV2ServerResponse, - type ServerKey, -} from '@hs/core'; -import { createLogger } from '../utils/logger'; -import { getHomeserverFinalAddress } from '../server-discovery/discovery'; import { PersistentEventBase } from '@hs/room'; +import { singleton } from 'tsyringe'; +import { KeyRepository } from '../repositories/key.repository'; +import { getHomeserverFinalAddress } from '../server-discovery/discovery'; +import { createLogger } from '../utils/logger'; +import { ConfigService } from './config.service'; type QueryCriteria = { // If not supplied, the current time as determined by the notary server is used. diff --git a/packages/federation-sdk/src/services/server.service.ts b/packages/federation-sdk/src/services/server.service.ts index 8f4509e1d..de0042b0e 100644 --- a/packages/federation-sdk/src/services/server.service.ts +++ b/packages/federation-sdk/src/services/server.service.ts @@ -1,4 +1,4 @@ -import { toUnpaddedBase64, signJson } from '@hs/crypto'; +import { signJson, toUnpaddedBase64 } from '@hs/crypto'; import { singleton } from 'tsyringe'; import { ServerRepository } from '../repositories/server.repository'; import { ConfigService } from './config.service'; diff --git a/packages/federation-sdk/src/services/signature-verification.service.spec.ts b/packages/federation-sdk/src/services/signature-verification.service.spec.ts index 28dc5dc64..959b14885 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.spec.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.spec.ts @@ -7,15 +7,14 @@ import { mock, test, } from 'bun:test'; -import { SignatureVerificationService } from './signature-verification.service'; -import { PersistentEventFactory } from '@hs/room'; import { VerifierKey, - loadEd25519SignerFromSeed, fromBase64ToBytes, + loadEd25519SignerFromSeed, loadEd25519VerifierFromPublicKey, } from '@hs/crypto'; -import { after } from 'node:test'; +import { PersistentEventFactory } from '@hs/room'; +import { SignatureVerificationService } from './signature-verification.service'; const originServer = 'syn1.tunnel.dev.rocket.chat'; diff --git a/packages/federation-sdk/src/services/signature-verification.service.ts b/packages/federation-sdk/src/services/signature-verification.service.ts index 207ce854a..abbbd39a8 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.ts @@ -1,10 +1,9 @@ import { createLogger } from '@hs/core'; import { + VerifierKey, encodeCanonicalJson, fromBase64ToBytes, isValidAlgorithm, - loadEd25519VerifierFromPublicKey, - VerifierKey, } from '@hs/crypto'; import type { PersistentEventBase } from '@hs/room'; diff --git a/packages/homeserver/src/controllers/key/server.controller.ts b/packages/homeserver/src/controllers/key/server.controller.ts index 9a5d56dad..47247f53a 100644 --- a/packages/homeserver/src/controllers/key/server.controller.ts +++ b/packages/homeserver/src/controllers/key/server.controller.ts @@ -1,8 +1,8 @@ -import { ConfigService, ServerService } from '@hs/federation-sdk'; -import { t, type Elysia } from 'elysia'; +import { ServerService } from '@hs/federation-sdk'; +import { KeyService } from '@hs/federation-sdk'; +import { type Elysia, t } from 'elysia'; import { container } from 'tsyringe'; import { ServerKeyResponseDto } from '../../dtos'; -import { KeyService } from '@hs/federation-sdk'; export const serverKeyPlugin = (app: Elysia) => { const serverService = container.resolve(ServerService); diff --git a/packages/room/src/manager/event-wrapper.ts b/packages/room/src/manager/event-wrapper.ts index 788921732..a940b503f 100644 --- a/packages/room/src/manager/event-wrapper.ts +++ b/packages/room/src/manager/event-wrapper.ts @@ -1,8 +1,11 @@ import crypto from 'node:crypto'; import { + computeHashBuffer, encodeCanonicalJson, toUnpaddedBase64, -} from '@rocket.chat/federation-crypto'; +} from '@hs/crypto'; +toUnpaddedBase64, +} from '@rocket.chat/federation-crypto' import { type RejectCode, RejectCodes } from '../authorizartion-rules/errors'; import { type EventStore, From ba832083f660a398b8d78cb099a126151bbb8898 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Tue, 16 Sep 2025 15:56:54 +0530 Subject: [PATCH 25/56] add mongo to ci? dump later --- .github/workflows/ci.yml | 72 ++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56bc19fd8..e9074e5be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,40 +1,40 @@ on: - release: - types: [published] - pull_request: - branches: "**" - paths-ignore: - - "**.md" - push: - branches: - - main - paths-ignore: - - "**.md" + release: + types: [published] + pull_request: + branches: "**" + paths-ignore: + - "**.md" + push: + branches: + - main + paths-ignore: + - "**.md" name: my-workflow jobs: - unit-tests: - name: Code Quality Checks(lint, test, tsc) - runs-on: ubuntu-latest - steps: - # ... - - uses: actions/checkout@v4 - - name: Cache turbo build setup - uses: actions/cache@v4 - with: - path: .turbo - key: ${{ runner.os }}-turbo-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-turbo- - - uses: oven-sh/setup-bun@v2 - - run: bun install - - run: bun run build - - run: bun lint:ci - - run: bun tsc --noEmit - - uses: supercharge/mongodb-github-action@1.12.0 - - run: LOG_LEVEL=debug RUN_MONGO_TESTS=1 bun test packages/federation-sdk/src/services/state.service.spec.ts - - run: LOG_LEVEL=debug bun test:coverage - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} + unit-tests: + name: Code Quality Checks(lint, test, tsc) + runs-on: ubuntu-latest + steps: + # ... + - uses: actions/checkout@v4 + - name: Cache turbo build setup + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + - uses: oven-sh/setup-bun@v2 + - run: bun install + - run: bun run build + - run: bun lint:ci + - run: bun tsc --noEmit + - uses: supercharge/mongodb-github-action@1.12.0 + - run: LOG_LEVEL=debug RUN_MONGO_TESTS=1 bun test packages/federation-sdk/src/services/state.service.spec.ts + - run: LOG_LEVEL=debug bun test:coverage + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} From 1b0cc566e6767ac56a139ed00e6a3a19478fcb9c Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Tue, 16 Sep 2025 16:46:28 +0530 Subject: [PATCH 26/56] ,, --- packages/federation-sdk/src/services/key.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index 99971c624..1e31d343f 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -100,6 +100,8 @@ export class KeyService { this.logger.warn(`Half life for key for ${serverName} is expired`); return true; } + + return false; } async fetchAndSaveKeysFromRemoteServerRaw( From 74d3b4a28d371a6340d785e0ff8c28cb3d27165f Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 17 Sep 2025 14:12:30 +0530 Subject: [PATCH 27/56] fix cachekey --- packages/federation-sdk/src/services/event.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/federation-sdk/src/services/event.service.ts b/packages/federation-sdk/src/services/event.service.ts index 0a9149b7e..d6ceb1539 100644 --- a/packages/federation-sdk/src/services/event.service.ts +++ b/packages/federation-sdk/src/services/event.service.ts @@ -352,7 +352,9 @@ export class EventService { 'verifier required for pdu validation', ); - this.cachedVerifierKey.set(requiredVerifier.key.id, requiredVerifier); + const cacheKey = `${pdu.roomId}:${requiredVerifier.key.id}` as const; + + this.cachedVerifierKey.set(cacheKey, requiredVerifier); await this.signatureVerificationService.verifyEventSignature( pdu, From 08850124f0a7d3ea223e2cdd0a6f267c0086845f Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 17 Sep 2025 14:12:58 +0530 Subject: [PATCH 28/56] better --- .../src/services/signature-verification.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/federation-sdk/src/services/signature-verification.service.ts b/packages/federation-sdk/src/services/signature-verification.service.ts index abbbd39a8..7d333724c 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.ts @@ -32,9 +32,7 @@ export type FederationRequest = // Signature verification service, wihtout dependency on anything just implements the parts of spec that validates json signatures, from requyests and events export class SignatureVerificationService { - private get logger() { - return createLogger('SignatureVerificationService'); - } + private readonly logger = createLogger('SignatureVerificationService'); /** * Implements part of SPEC: https://spec.matrix.org/v1.12/server-server-api/#validating-hashes-and-signatures-on-received-events From 760e926ef19a71d5c6c6cef120f236f88eba9910 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 12:19:13 +0530 Subject: [PATCH 29/56] fix: all import package names --- packages/core/src/utils/fetch.ts | 4 ++-- .../src/__mocks__/repositories.spec.ts | 6 +++++- .../federation-sdk/src/__mocks__/singer.spec.ts | 2 +- .../repositories/event-staging.repository.ts | 2 +- .../src/repositories/key.repository.ts | 2 +- .../src/services/config.service.ts | 5 ++++- .../src/services/event.service.spec.ts | 15 +++++++++------ .../src/services/event.service.ts | 16 ++++++++-------- .../services/federation-request.service.spec.ts | 7 +++++-- .../src/services/federation-request.service.ts | 17 ++++++++++------- .../src/services/key.service.spec.ts | 8 ++++---- .../federation-sdk/src/services/key.service.ts | 6 +++--- .../signature-verification.service.spec.ts | 4 ++-- .../services/signature-verification.service.ts | 6 +++--- packages/room/src/manager/event-wrapper.ts | 2 +- 15 files changed, 59 insertions(+), 43 deletions(-) diff --git a/packages/core/src/utils/fetch.ts b/packages/core/src/utils/fetch.ts index 85fe861b9..8ceccc403 100644 --- a/packages/core/src/utils/fetch.ts +++ b/packages/core/src/utils/fetch.ts @@ -177,7 +177,7 @@ export async function fetch( body() { if (!body) { body = new Promise((resBody, rejBody) => { - // TODO: Make @hs/core fetch size limit configurable + // TODO: Make @rocket.chat/federation-core fetch size limit configurable let total = 0; const MAX_RESPONSE_BYTES = 50 * 1024 * 1024; // 50 MB @@ -233,7 +233,7 @@ export async function fetch( reject(err); }); - // TODO: Make @hs/core fetch timeout configurable + // TODO: Make @rocket.chat/federation-core fetch timeout configurable request.setTimeout(20_000, () => { request.destroy(new Error('Request timed out after 20s')); }); diff --git a/packages/federation-sdk/src/__mocks__/repositories.spec.ts b/packages/federation-sdk/src/__mocks__/repositories.spec.ts index 7b88784d3..2605e18ba 100644 --- a/packages/federation-sdk/src/__mocks__/repositories.spec.ts +++ b/packages/federation-sdk/src/__mocks__/repositories.spec.ts @@ -1,4 +1,8 @@ -import { EventStagingStore, EventStore, ServerKey } from '@hs/core'; +import { + EventStagingStore, + EventStore, + ServerKey, +} from '@rocket.chat/federation-core'; import { EventStagingRepository } from '../repositories/event-staging.repository'; import { EventRepository } from '../repositories/event.repository'; import { KeyRepository } from '../repositories/key.repository'; diff --git a/packages/federation-sdk/src/__mocks__/singer.spec.ts b/packages/federation-sdk/src/__mocks__/singer.spec.ts index 932c2e072..aa2349324 100644 --- a/packages/federation-sdk/src/__mocks__/singer.spec.ts +++ b/packages/federation-sdk/src/__mocks__/singer.spec.ts @@ -2,7 +2,7 @@ import { VerifierKey, fromBase64ToBytes, loadEd25519SignerFromSeed, -} from '@hs/crypto'; +} from '@rocket.chat/federation-crypto'; const seed = 'zSkmr713LnEDbxlkYq2ZqIiKTQNsyMOU0T2CEeC44C4'; diff --git a/packages/federation-sdk/src/repositories/event-staging.repository.ts b/packages/federation-sdk/src/repositories/event-staging.repository.ts index a02d589dc..344e3c546 100644 --- a/packages/federation-sdk/src/repositories/event-staging.repository.ts +++ b/packages/federation-sdk/src/repositories/event-staging.repository.ts @@ -1,5 +1,5 @@ import { type EventID, Pdu } from '@rocket.chat/federation-room'; -import type { EventStagingStore } from '@hs/core'; +import type { EventStagingStore } from '@rocket.chat/federation-core'; import type { Collection, DeleteResult, UpdateResult } from 'mongodb'; import { inject, singleton } from 'tsyringe'; diff --git a/packages/federation-sdk/src/repositories/key.repository.ts b/packages/federation-sdk/src/repositories/key.repository.ts index 1b6504f5f..09cdfb966 100644 --- a/packages/federation-sdk/src/repositories/key.repository.ts +++ b/packages/federation-sdk/src/repositories/key.repository.ts @@ -1,4 +1,4 @@ -import { ServerKey } from '@hs/core'; +import { ServerKey } from '@rocket.chat/federation-core'; import type { Collection, Filter, FindCursor, FindOptions } from 'mongodb'; import { inject, singleton } from 'tsyringe'; diff --git a/packages/federation-sdk/src/services/config.service.ts b/packages/federation-sdk/src/services/config.service.ts index d00589853..6c320d82c 100644 --- a/packages/federation-sdk/src/services/config.service.ts +++ b/packages/federation-sdk/src/services/config.service.ts @@ -6,7 +6,10 @@ import { } from '@rocket.chat/federation-core'; import { singleton } from 'tsyringe'; -import { fromBase64ToBytes, loadEd25519SignerFromSeed } from '@hs/crypto'; +import { + fromBase64ToBytes, + loadEd25519SignerFromSeed, +} from '@rocket.chat/federation-crypto'; import { z } from 'zod'; export interface AppConfig { diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts index 3eda26654..f0601c87b 100644 --- a/packages/federation-sdk/src/services/event.service.spec.ts +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -7,9 +7,12 @@ import { it, mock, } from 'bun:test'; -import { BaseEDU } from '@hs/core'; -import { fromBase64ToBytes, loadEd25519SignerFromSeed } from '@hs/crypto'; -import { Pdu, PersistentEventFactory } from '@hs/room'; +import { BaseEDU } from '@rocket.chat/federation-core'; +import { + fromBase64ToBytes, + loadEd25519SignerFromSeed, +} from '@rocket.chat/federation-crypto'; +import { Pdu, PersistentEventFactory } from '@rocket.chat/federation-room'; import { config } from '../__mocks__/config.service.spec'; import { repositories } from '../__mocks__/repositories.spec'; import { eventService } from '../__mocks__/services.spec'; @@ -61,7 +64,7 @@ describe('EventService', async () => { '../server-discovery/discovery' ); - const { fetch } = await import('@hs/core'); + const { fetch } = await import('@rocket.chat/federation-core'); // random server name for each run let inboundServer = `localhost${Math.floor(Math.random() * 10000).toString()}`; @@ -80,7 +83,7 @@ describe('EventService', async () => { ], })); - await mock.module('@hs/core', () => ({ + await mock.module('@rocket.chat/federation-core', () => ({ fetch: async (..._args: any[]) => { return { ok: true, @@ -92,7 +95,7 @@ describe('EventService', async () => { }); afterEach(async () => { - await mock.module('@hs/core', () => ({ fetch })); + await mock.module('@rocket.chat/federation-core', () => ({ fetch })); await mock.module('../server-discovery/discovery', () => ({ getHomeserverFinalAddress: originalServerDiscovery, })); diff --git a/packages/federation-sdk/src/services/event.service.ts b/packages/federation-sdk/src/services/event.service.ts index cab9d8c1b..a1291cd74 100644 --- a/packages/federation-sdk/src/services/event.service.ts +++ b/packages/federation-sdk/src/services/event.service.ts @@ -10,14 +10,14 @@ import type { RedactionEvent } from '@rocket.chat/federation-core'; import { generateId } from '@rocket.chat/federation-core'; import type { EventStore } from '@rocket.chat/federation-core'; import { pruneEventDict } from '@rocket.chat/federation-core'; -} from '@hs/core' -import { isPresenceEDU, isTypingEDU } from '@hs/core'; -import type { RedactionEvent } from '@hs/core'; -import { generateId } from '@hs/core'; -import type { EventStore } from '@hs/core'; -import { pruneEventDict } from '@hs/core'; - -import { createLogger } from '@hs/core'; +} from '@rocket.chat/federation-core' +import { isPresenceEDU, isTypingEDU } from '@rocket.chat/federation-core'; +import type { RedactionEvent } from '@rocket.chat/federation-core'; +import { generateId } from '@rocket.chat/federation-core'; +import type { EventStore } from '@rocket.chat/federation-core'; +import { pruneEventDict } from '@rocket.chat/federation-core'; + +import { createLogger } from '@rocket.chat/federation-core'; import { type EventID, type Pdu, diff --git a/packages/federation-sdk/src/services/federation-request.service.spec.ts b/packages/federation-sdk/src/services/federation-request.service.spec.ts index 975fe6282..2a96c0915 100644 --- a/packages/federation-sdk/src/services/federation-request.service.spec.ts +++ b/packages/federation-sdk/src/services/federation-request.service.spec.ts @@ -8,8 +8,11 @@ import { mock, spyOn, } from 'bun:test'; -import * as core from '@hs/core'; -import { fromBase64ToBytes, loadEd25519SignerFromSeed } from '@hs/crypto'; +import * as core from '@rocket.chat/federation-core'; +import { + fromBase64ToBytes, + loadEd25519SignerFromSeed, +} from '@rocket.chat/federation-crypto'; import { ConfigService } from './config.service'; import { FederationRequestService } from './federation-request.service'; diff --git a/packages/federation-sdk/src/services/federation-request.service.ts b/packages/federation-sdk/src/services/federation-request.service.ts index e09c74ce1..2f96bcd01 100644 --- a/packages/federation-sdk/src/services/federation-request.service.ts +++ b/packages/federation-sdk/src/services/federation-request.service.ts @@ -12,13 +12,16 @@ import { fetch, signJson, } from '@rocket.chat/federation-core'; -import type { SigningKey } from '@hs/core'; -import { authorizationHeaders, computeAndMergeHash } from '@hs/core'; -import { extractURIfromURL } from '@hs/core'; -import { EncryptionValidAlgorithm } from '@hs/core'; -import { createLogger } from '@hs/core'; -import { fetch } from '@hs/core'; -import { signJson } from '@hs/crypto'; +import type { SigningKey } from '@rocket.chat/federation-core'; +import { + authorizationHeaders, + computeAndMergeHash, +} from '@rocket.chat/federation-core'; +import { extractURIfromURL } from '@rocket.chat/federation-core'; +import { EncryptionValidAlgorithm } from '@rocket.chat/federation-core'; +import { createLogger } from '@rocket.chat/federation-core'; +import { fetch } from '@rocket.chat/federation-core'; +import { signJson } from '@rocket.chat/federation-crypto'; import { singleton } from 'tsyringe'; import { getHomeserverFinalAddress } from '../server-discovery/discovery'; import { ConfigService } from './config.service'; diff --git a/packages/federation-sdk/src/services/key.service.spec.ts b/packages/federation-sdk/src/services/key.service.spec.ts index 1e8230e7d..2f330ad8d 100644 --- a/packages/federation-sdk/src/services/key.service.spec.ts +++ b/packages/federation-sdk/src/services/key.service.spec.ts @@ -1,4 +1,4 @@ -import { verifyJsonSignature } from '@hs/crypto'; +import { verifyJsonSignature } from '@rocket.chat/federation-crypto'; import { Mock, describe, expect, it, mock } from 'bun:test'; import { afterEach, beforeEach } from 'node:test'; @@ -9,12 +9,12 @@ import { signer } from '../__mocks__/singer.spec'; describe('KeyService', async () => { // fetch mocking - const { fetch } = await import('@hs/core'); + const { fetch } = await import('@rocket.chat/federation-core'); let inboundServer = ''; // skips server discovery beforeEach(async () => { - await mock.module('@hs/core', () => ({ + await mock.module('@rocket.chat/federation-core', () => ({ fetch: async (..._args: any[]) => { return { ok: true, @@ -27,7 +27,7 @@ describe('KeyService', async () => { }); afterEach(async () => { - await mock.module('@hs/core', () => ({ fetch })); + await mock.module('@rocket.chat/federation-core', () => ({ fetch })); mock.restore(); }); diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index 1e31d343f..8db2d6162 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -2,7 +2,7 @@ import { type KeyV2ServerResponse, type ServerKey, fetch as coreFetch, -} from '@hs/core'; +} from '@rocket.chat/federation-core'; import { type Signer, VerifierKey, @@ -10,8 +10,8 @@ import { isValidAlgorithm, loadEd25519VerifierFromPublicKey, signJson, -} from '@hs/crypto'; -import { PersistentEventBase } from '@hs/room'; +} from '@rocket.chat/federation-crypto'; +import { PersistentEventBase } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; import { KeyRepository } from '../repositories/key.repository'; import { getHomeserverFinalAddress } from '../server-discovery/discovery'; diff --git a/packages/federation-sdk/src/services/signature-verification.service.spec.ts b/packages/federation-sdk/src/services/signature-verification.service.spec.ts index 959b14885..9bf68aab1 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.spec.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.spec.ts @@ -12,8 +12,8 @@ import { fromBase64ToBytes, loadEd25519SignerFromSeed, loadEd25519VerifierFromPublicKey, -} from '@hs/crypto'; -import { PersistentEventFactory } from '@hs/room'; +} from '@rocket.chat/federation-crypto'; +import { PersistentEventFactory } from '@rocket.chat/federation-room'; import { SignatureVerificationService } from './signature-verification.service'; const originServer = 'syn1.tunnel.dev.rocket.chat'; diff --git a/packages/federation-sdk/src/services/signature-verification.service.ts b/packages/federation-sdk/src/services/signature-verification.service.ts index 7d333724c..a8387e031 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.ts @@ -1,11 +1,11 @@ -import { createLogger } from '@hs/core'; +import { createLogger } from '@rocket.chat/federation-core'; import { VerifierKey, encodeCanonicalJson, fromBase64ToBytes, isValidAlgorithm, -} from '@hs/crypto'; -import type { PersistentEventBase } from '@hs/room'; +} from '@rocket.chat/federation-crypto'; +import type { PersistentEventBase } from '@rocket.chat/federation-room'; // low cost optimization in case of bad implementations // ed25519 signatures in unpaddedbase64 are always 86 characters long (doing math here for future reference) diff --git a/packages/room/src/manager/event-wrapper.ts b/packages/room/src/manager/event-wrapper.ts index a940b503f..c3dd69b15 100644 --- a/packages/room/src/manager/event-wrapper.ts +++ b/packages/room/src/manager/event-wrapper.ts @@ -3,7 +3,7 @@ import { computeHashBuffer, encodeCanonicalJson, toUnpaddedBase64, -} from '@hs/crypto'; +} from '@rocket.chat/federation-crypto'; toUnpaddedBase64, } from '@rocket.chat/federation-crypto' import { type RejectCode, RejectCodes } from '../authorizartion-rules/errors'; From 265c47c0968fb293bfa63e6702fd33d915e76b7c Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 12:38:44 +0530 Subject: [PATCH 30/56] fix: allow svs to fetch keys as it needs --- .../src/services/config.service.ts | 39 +++++-------------- .../src/services/event.service.ts | 10 +---- .../src/services/key.service.ts | 6 +-- .../signature-verification.service.ts | 28 ++++++++++--- 4 files changed, 37 insertions(+), 46 deletions(-) diff --git a/packages/federation-sdk/src/services/config.service.ts b/packages/federation-sdk/src/services/config.service.ts index 6c320d82c..0618d9271 100644 --- a/packages/federation-sdk/src/services/config.service.ts +++ b/packages/federation-sdk/src/services/config.service.ts @@ -1,14 +1,10 @@ -import { - SigningKey, - createLogger, - generateKeyPairsFromString, - toUnpaddedBase64, -} from '@rocket.chat/federation-core'; +import { createLogger } from '@rocket.chat/federation-core'; import { singleton } from 'tsyringe'; import { fromBase64ToBytes, loadEd25519SignerFromSeed, + Signer, } from '@rocket.chat/federation-crypto'; import { z } from 'zod'; @@ -21,6 +17,7 @@ export interface AppConfig { keyRefreshInterval: number; signingKey?: string; timeout?: number; + // TODO: need this still? signingKeyPath?: string; database: { uri: string; @@ -96,7 +93,8 @@ export const AppConfigSchema = z.object({ export class ConfigService { private config: AppConfig = {} as AppConfig; private logger = createLogger('ConfigService'); - private serverKeys: SigningKey[] = []; + + private signer: Signer | undefined; setConfig(values: AppConfig) { try { @@ -141,29 +139,10 @@ export class ConfigService { throw new Error('Signing key is not configured'); } - if (!this.serverKeys.length) { - const signingKey = await generateKeyPairsFromString( - this.config.signingKey, - ); - this.serverKeys = [signingKey]; - } - - return this.serverKeys; - } - - async getSigningKeyId(): Promise { - const signingKeys = await this.getSigningKey(); - const signingKey = signingKeys[0]; - return `${signingKey.algorithm}:${signingKey.version}` || 'ed25519:1'; - } - - async getSigningKeyBase64(): Promise { - const signingKeys = await this.getSigningKey(); - return toUnpaddedBase64(signingKeys[0].privateKey); - } + this.signer = await loadEd25519SignerFromSeed( + fromBase64ToBytes(this.config.signingKey), + ); - async getPublicSigningKeyBase64(): Promise { - const signingKeys = await this.getSigningKey(); - return toUnpaddedBase64(signingKeys[0].publicKey); + return this.signer; } } diff --git a/packages/federation-sdk/src/services/event.service.ts b/packages/federation-sdk/src/services/event.service.ts index a1291cd74..49f8b4b96 100644 --- a/packages/federation-sdk/src/services/event.service.ts +++ b/packages/federation-sdk/src/services/event.service.ts @@ -10,12 +10,6 @@ import type { RedactionEvent } from '@rocket.chat/federation-core'; import { generateId } from '@rocket.chat/federation-core'; import type { EventStore } from '@rocket.chat/federation-core'; import { pruneEventDict } from '@rocket.chat/federation-core'; -} from '@rocket.chat/federation-core' -import { isPresenceEDU, isTypingEDU } from '@rocket.chat/federation-core'; -import type { RedactionEvent } from '@rocket.chat/federation-core'; -import { generateId } from '@rocket.chat/federation-core'; -import type { EventStore } from '@rocket.chat/federation-core'; -import { pruneEventDict } from '@rocket.chat/federation-core'; import { createLogger } from '@rocket.chat/federation-core'; import { @@ -25,7 +19,6 @@ import { type PduType, PersistentEventBase, PersistentEventFactory, - RoomID, RoomVersion, getAuthChain, } from '@rocket.chat/federation-room'; @@ -57,8 +50,9 @@ export class EventService { private readonly configService: ConfigService, private readonly stagingAreaQueue: StagingAreaQueue, private readonly stateService: StateService, - private readonly serverService: ServerService, private readonly eventEmitterService: EventEmitterService, + private readonly keyService: KeyService, + private readonly signatureVerificationService: SignatureVerificationService, @inject(delay(() => EventRepository)) private readonly eventRepository: EventRepository, @inject(delay(() => EventStagingRepository)) diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index 8db2d6162..ef520ac8b 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -2,6 +2,7 @@ import { type KeyV2ServerResponse, type ServerKey, fetch as coreFetch, + createLogger, } from '@rocket.chat/federation-core'; import { type Signer, @@ -15,7 +16,6 @@ import { PersistentEventBase } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; import { KeyRepository } from '../repositories/key.repository'; import { getHomeserverFinalAddress } from '../server-discovery/discovery'; -import { createLogger } from '../utils/logger'; import { ConfigService } from './config.service'; type QueryCriteria = { @@ -104,7 +104,7 @@ export class KeyService { return false; } - async fetchAndSaveKeysFromRemoteServerRaw( + private async fetchAndSaveKeysFromRemoteServerRaw( serverName: string, ): Promise { const [address, hostHeaders] = await getHomeserverFinalAddress( @@ -208,7 +208,7 @@ export class KeyService { } // multiple keys -> single repnse - async convertToKeyV2Response( + private async convertToKeyV2Response( serverKeys: ServerKey[], minimumValidUntil = Date.now(), ): Promise { diff --git a/packages/federation-sdk/src/services/signature-verification.service.ts b/packages/federation-sdk/src/services/signature-verification.service.ts index a8387e031..afc14f1e8 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.ts @@ -6,6 +6,8 @@ import { isValidAlgorithm, } from '@rocket.chat/federation-crypto'; import type { PersistentEventBase } from '@rocket.chat/federation-room'; +import { singleton } from 'tsyringe'; +import { KeyService } from './key.service'; // low cost optimization in case of bad implementations // ed25519 signatures in unpaddedbase64 are always 86 characters long (doing math here for future reference) @@ -30,8 +32,10 @@ export type FederationRequest = signature: Record>; }; -// Signature verification service, wihtout dependency on anything just implements the parts of spec that validates json signatures, from requyests and events +@singleton() export class SignatureVerificationService { + constructor(private readonly keyService: KeyService) {} + private readonly logger = createLogger('SignatureVerificationService'); /** @@ -40,7 +44,7 @@ export class SignatureVerificationService { */ async verifyEventSignature( event: PersistentEventBase, - verifier: VerifierKey, + verifier?: VerifierKey, ): Promise { // SPEC: First the signature is checked. The event is redacted following the redaction algorithm const { redactedEvent, origin } = event; @@ -51,9 +55,14 @@ export class SignatureVerificationService { ); } - const { unsigned, ...toCheck } = redactedEvent; + const { unsigned: _, ...toCheck } = redactedEvent; + + if (verifier) return this.verifySignature(toCheck, origin, verifier); - await this.verifySignature(toCheck, origin, verifier); + const { key: requiredVerifier } = + await this.keyService.getRequiredVerifierForEvent(event); + + return this.verifySignature(toCheck, origin, requiredVerifier); } async verifyRequestSignature( @@ -119,7 +128,16 @@ export class SignatureVerificationService { }, }; - await this.verifySignature(toVerify, origin, verifier); + if (verifier) { + return this.verifySignature(toVerify, origin, verifier); + } + + const requiredVerifier = await this.keyService.getRequestVerifier( + origin, + key, + ); + + return this.verifySignature(toVerify, origin, requiredVerifier); } /** From f77a240dd3edd8c85d205ceb92afb61790b8d5d5 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 13:12:21 +0530 Subject: [PATCH 31/56] fix: repassing some tests --- .../src/__mocks__/config.service.spec.ts | 14 ++++---- .../src/__mocks__/repositories.spec.ts | 9 ++++-- .../src/__mocks__/services.spec.ts | 10 +++--- packages/federation-sdk/src/index.ts | 6 ++-- .../repositories/state-graph.repository.ts | 11 +++++++ packages/federation-sdk/src/sdk.ts | 6 ++-- .../src/services/event.service.spec.ts | 32 +++++++++++-------- .../federation-request.service.spec.ts | 23 +++++++------ .../services/federation-request.service.ts | 20 ++---------- .../signature-verification.service.ts | 2 +- .../src/services/state.service.ts | 17 +++++----- .../src/middlewares/isAuthenticated.ts | 20 ++++++------ packages/room/src/manager/event-wrapper.ts | 13 ++++++-- 13 files changed, 106 insertions(+), 77 deletions(-) diff --git a/packages/federation-sdk/src/__mocks__/config.service.spec.ts b/packages/federation-sdk/src/__mocks__/config.service.spec.ts index 9975f89ad..c26131cd6 100644 --- a/packages/federation-sdk/src/__mocks__/config.service.spec.ts +++ b/packages/federation-sdk/src/__mocks__/config.service.spec.ts @@ -2,20 +2,22 @@ import { ConfigService } from '../services/config.service'; import { DatabaseConnectionService } from '../services/database-connection.service'; import { signer } from './singer.spec'; +const databaseConfig = { + uri: 'mongodb://localhost:27017/matrix_test', + name: 'matrix_test', + poolSize: 100, +}; + export const config = { serverName: 'test.local', getSigningKey: async () => signer, - database: { - uri: 'mongodb://localhost:27017/matrix_test', - name: 'matrix_test', - poolSize: 100, - }, + database: databaseConfig, getDatabaseConfig: function () { // @ts-ignore return this.database; }, } as unknown as ConfigService; -const database = new DatabaseConnectionService(config); +const database = new DatabaseConnectionService(databaseConfig); export const db = await database.getDb(); diff --git a/packages/federation-sdk/src/__mocks__/repositories.spec.ts b/packages/federation-sdk/src/__mocks__/repositories.spec.ts index 2605e18ba..387d0d586 100644 --- a/packages/federation-sdk/src/__mocks__/repositories.spec.ts +++ b/packages/federation-sdk/src/__mocks__/repositories.spec.ts @@ -7,7 +7,10 @@ import { EventStagingRepository } from '../repositories/event-staging.repository import { EventRepository } from '../repositories/event.repository'; import { KeyRepository } from '../repositories/key.repository'; import { Lock, LockRepository } from '../repositories/lock.repository'; -import { StateRepository, StateStore } from '../repositories/state.repository'; +import { + StateGraphRepository, + type StateGraphStore, +} from '../repositories/state-graph.repository'; import { db } from './config.service.spec'; const keysCollection = db.collection('test_keys'); @@ -15,7 +18,7 @@ const eventsCollection = db.collection('test_events'); const eventStagingCollection = db.collection('test_event_staging'); const lockCollection = db.collection('test_locks'); -const statesCollection = db.collection('test_states'); +const statesCollection = db.collection('test_states'); export const collections = { keys: keysCollection, @@ -31,7 +34,7 @@ const eventStagingRepository = new EventStagingRepository( eventStagingCollection, ); const lockRepository = new LockRepository(lockCollection); -const stateRepository = new StateRepository(statesCollection as any); // TODO: fix this +const stateRepository = new StateGraphRepository(statesCollection); const eventsRepository = new EventRepository(eventsCollection); diff --git a/packages/federation-sdk/src/__mocks__/services.spec.ts b/packages/federation-sdk/src/__mocks__/services.spec.ts index eb2c31c32..cdba1865e 100644 --- a/packages/federation-sdk/src/__mocks__/services.spec.ts +++ b/packages/federation-sdk/src/__mocks__/services.spec.ts @@ -19,18 +19,20 @@ const stateService = new StateService( const eventEmitter = new EventEmitterService(); -const signatureVerificationService = new SignatureVerificationService(); +const signatureVerificationService = new SignatureVerificationService( + keyService, +); const eventService = new EventService( - repositories.events, - repositories.eventStaging, - repositories.locks, config, stagingAreaQueue, stateService, eventEmitter, keyService, signatureVerificationService, + repositories.events, + repositories.eventStaging, + repositories.locks, ); export { diff --git a/packages/federation-sdk/src/index.ts b/packages/federation-sdk/src/index.ts index b8393484f..7e18cb284 100644 --- a/packages/federation-sdk/src/index.ts +++ b/packages/federation-sdk/src/index.ts @@ -5,6 +5,7 @@ import type { EventStagingStore, Membership, MessageType, + ServerKey, } from '@rocket.chat/federation-core'; import type { EventID, @@ -14,7 +15,6 @@ import type { import { Collection } from 'mongodb'; import { container } from 'tsyringe'; import { StagingAreaListener } from './listeners/staging-area.listener'; -import { Key } from './repositories/key.repository'; import { Lock } from './repositories/lock.repository'; import { Room } from './repositories/room.repository'; import { Server } from './repositories/server.repository'; @@ -286,8 +286,8 @@ export async function init({ ), }); - container.register>('KeyCollection', { - useValue: db.collection('rocketchat_federation_keys'), + container.register>('KeyCollection', { + useValue: db.collection('rocketchat_federation_keys'), }); container.register>('LockCollection', { diff --git a/packages/federation-sdk/src/repositories/state-graph.repository.ts b/packages/federation-sdk/src/repositories/state-graph.repository.ts index 3e0bf36fc..435743ef9 100644 --- a/packages/federation-sdk/src/repositories/state-graph.repository.ts +++ b/packages/federation-sdk/src/repositories/state-graph.repository.ts @@ -2,6 +2,7 @@ import { type EventID, type PduType, type PersistentEventBase, + RoomID, type StateID, type StateMapKey, getStateMapKey, @@ -221,4 +222,14 @@ export class StateGraphRepository { { sort: { depth: -1 } }, ); } + + async findCreateEventIdByRoomId(roomId: RoomID) { + const doc = await this.collection.findOne({ + roomId, + type: 'm.room.create', + stateKey: '', + }); + + return doc?.eventId; + } } diff --git a/packages/federation-sdk/src/sdk.ts b/packages/federation-sdk/src/sdk.ts index 933bfe5d5..f63831ec2 100644 --- a/packages/federation-sdk/src/sdk.ts +++ b/packages/federation-sdk/src/sdk.ts @@ -17,6 +17,7 @@ import { SendJoinService } from './services/send-join.service'; import { ServerService } from './services/server.service'; import { StateService } from './services/state.service'; import { WellKnownService } from './services/well-known.service'; +import { SignatureVerificationService } from './services/signature-verification.service'; // create a federation sdk class to export @singleton() @@ -37,6 +38,7 @@ export class FederationSDK { private readonly wellKnownService: WellKnownService, private readonly federationRequestService: FederationRequestService, private readonly federationService: FederationService, + private readonly signatureVerficationService: SignatureVerificationService, ) {} createDirectMessageRoom( @@ -131,10 +133,10 @@ export class FederationSDK { verifyRequestSignature( ...args: Parameters< - typeof this.eventAuthorizationService.verifyRequestSignature + typeof this.signatureVerficationService.verifyRequestSignature > ) { - return this.eventAuthorizationService.verifyRequestSignature(...args); + return this.signatureVerficationService.verifyRequestSignature(...args); } joinUser(...args: Parameters) { diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts index f0601c87b..b80fb4c77 100644 --- a/packages/federation-sdk/src/services/event.service.spec.ts +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -12,7 +12,13 @@ import { fromBase64ToBytes, loadEd25519SignerFromSeed, } from '@rocket.chat/federation-crypto'; -import { Pdu, PersistentEventFactory } from '@rocket.chat/federation-room'; +import { + EventID, + Pdu, + PersistentEventFactory, + RoomID, + UserID, +} from '@rocket.chat/federation-room'; import { config } from '../__mocks__/config.service.spec'; import { repositories } from '../__mocks__/repositories.spec'; import { eventService } from '../__mocks__/services.spec'; @@ -24,7 +30,7 @@ const event = { '$Ulggyo4m1OlI08Z0jJDVeceigjSZP9SdEFVoAn9mEh8', '$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg', '$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs', - ], + ] as EventID[], content: { avatar_url: null, displayname: 'debdut1', @@ -34,10 +40,10 @@ const event = { hashes: { sha256: '6MnKSCFJy1fYf6ukILBEbqx2DkoaD1wRyKXhv689a0A' }, origin: 'syn1.tunnel.dev.rocket.chat', origin_server_ts: 1757328411218, - prev_events: ['$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs'], - room_id: '!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat', - sender: '@debdut1:syn1.tunnel.dev.rocket.chat', - state_key: '@debdut1:syn1.tunnel.dev.rocket.chat', + prev_events: ['$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs'] as EventID[], + room_id: '!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat' as RoomID, + sender: '@debdut1:syn1.tunnel.dev.rocket.chat' as UserID, + state_key: '@debdut1:syn1.tunnel.dev.rocket.chat' as UserID, type: 'm.room.member' as const, signatures: { 'syn1.tunnel.dev.rocket.chat': { @@ -47,9 +53,9 @@ const event = { }, unsigned: { age: 1, - replaces_state: '$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs', + replaces_state: '$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs' as EventID, prev_content: { displayname: 'debdut1', membership: 'invite' }, - prev_sender: '@debdut:syn2.tunnel.dev.rocket.chat', + prev_sender: '@debdut:syn2.tunnel.dev.rocket.chat' as UserID, }, }; @@ -206,7 +212,7 @@ describe('EventService', async () => { it('should successfully validate hash and signature (happy path)', async () => { // 1. create an event const pdu = PersistentEventFactory.newCreateEvent( - `@creator:${inboundServer}`, + `@creator:${inboundServer}` as UserID, roomVersion, ); @@ -225,7 +231,7 @@ describe('EventService', async () => { it('should fail if signed by a key expired at the point of event creation', async () => { const pdu = PersistentEventFactory.newCreateEvent( - `@creator:${inboundServer}`, + `@creator:${inboundServer}` as UserID, roomVersion, ); @@ -259,7 +265,7 @@ describe('EventService', async () => { // need to invalidate the cache though // new room id does it const pdu2 = PersistentEventFactory.newCreateEvent( - `@creator:${inboundServer}`, + `@creator:${inboundServer}` as UserID, roomVersion, ); @@ -274,7 +280,7 @@ describe('EventService', async () => { it('should fail if signed by an unknown key', async () => { // 1. create an event const pdu = PersistentEventFactory.newCreateEvent( - `@creator:${inboundServer}`, + `@creator:${inboundServer}` as UserID, roomVersion, ); @@ -292,7 +298,7 @@ describe('EventService', async () => { it('should pass if signed by an old key', async () => { const pdu = PersistentEventFactory.newCreateEvent( - `@creator:${inboundServer}`, + `@creator:${inboundServer}` as UserID, roomVersion, ); diff --git a/packages/federation-sdk/src/services/federation-request.service.spec.ts b/packages/federation-sdk/src/services/federation-request.service.spec.ts index 2a96c0915..8e2e8d61d 100644 --- a/packages/federation-sdk/src/services/federation-request.service.spec.ts +++ b/packages/federation-sdk/src/services/federation-request.service.spec.ts @@ -52,7 +52,7 @@ describe('FederationRequestService', async () => { json: async () => ({ result: 'success' }), text: async () => '{"result":"success"}', multipart: async () => null, - } as Response; + }; }, })); @@ -70,14 +70,19 @@ describe('FederationRequestService', async () => { configService = { getConfig: (key: string) => { if (key === 'serverName') { - return mockServerName; + return origin; } throw new Error(`Unknown config key: ${key}`); }, - serverName: mockServerName, - getSigningKeyBase64: async () => mockSigningKey, - getSigningKeyId: async () => mockSigningKeyId, - } as ConfigService; + serverName: origin, + getSigningKey: async () => { + const [, version, signingKey] = signingKeyContent.split(' '); + return loadEd25519SignerFromSeed( + fromBase64ToBytes(signingKey), + version, + ); + }, + } as unknown as ConfigService; service = new FederationRequestService(configService); }); @@ -118,7 +123,7 @@ describe('FederationRequestService', async () => { await service.makeSignedRequest({ method, // Host: syn2.tunnel.dev.rocket.chat - domain: 'syn2.tunnel.dev.rocket.chat', + domain: destination, uri, body: transactionBody, }); @@ -183,7 +188,7 @@ describe('FederationRequestService', async () => { ok: true, status: 200, multipart: async () => ({ content: mockBuffer }), - }); + } as any); const result = await service.requestBinaryData( 'GET', @@ -209,7 +214,7 @@ describe('FederationRequestService', async () => { ok: true, status: 200, multipart: async () => ({ content: mockBuffer }), - }); + } as any); const result = await service.requestBinaryData( 'GET', diff --git a/packages/federation-sdk/src/services/federation-request.service.ts b/packages/federation-sdk/src/services/federation-request.service.ts index 2f96bcd01..158ded27e 100644 --- a/packages/federation-sdk/src/services/federation-request.service.ts +++ b/packages/federation-sdk/src/services/federation-request.service.ts @@ -3,16 +3,6 @@ import type { MultipartResult, SigningKey, } from '@rocket.chat/federation-core'; -import { - EncryptionValidAlgorithm, - authorizationHeaders, - computeAndMergeHash, - createLogger, - extractURIfromURL, - fetch, - signJson, -} from '@rocket.chat/federation-core'; -import type { SigningKey } from '@rocket.chat/federation-core'; import { authorizationHeaders, computeAndMergeHash, @@ -49,7 +39,7 @@ export class FederationRequestService { uri, body, queryString, - }: SignedRequest): Promise { + }: SignedRequest): Promise> { const signer = await this.configService.getSigningKey(); const [address, discoveryHeaders] = await getHomeserverFinalAddress( @@ -59,10 +49,6 @@ export class FederationRequestService { const origin = this.configService.serverName; - const url = new URL(`${address}${uri}`); - if (queryString) { - url.search = queryString; - } const url = new URL(`${address}${uri}`); if (queryString) { url.search = queryString; @@ -116,7 +102,7 @@ export class FederationRequestService { 'making http request', ); - const response = await fetch(url, { + const response = await fetch(url, { method, ...(body && { body: JSON.stringify(body) }), headers, @@ -135,7 +121,7 @@ export class FederationRequestService { ); } - return response.json(); + return response; } async request( diff --git a/packages/federation-sdk/src/services/signature-verification.service.ts b/packages/federation-sdk/src/services/signature-verification.service.ts index afc14f1e8..a4b4c0091 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.ts @@ -77,7 +77,7 @@ export class SignatureVerificationService { body: object | undefined; uri: string; }, - verifier: VerifierKey, + verifier?: VerifierKey, ) { // `X-Matrix origin="${origin}",destination="${destination}",key="${key}",sig="${signed}"` diff --git a/packages/federation-sdk/src/services/state.service.ts b/packages/federation-sdk/src/services/state.service.ts index e3dd308aa..429e6cf88 100644 --- a/packages/federation-sdk/src/services/state.service.ts +++ b/packages/federation-sdk/src/services/state.service.ts @@ -1,4 +1,4 @@ -import { createLogger, signJson } from '@rocket.chat/federation-crypto'; +import { signJson } from '@rocket.chat/federation-crypto'; import { type EventID, type EventStore, @@ -25,6 +25,7 @@ import { delay, inject, singleton } from 'tsyringe'; import { EventRepository } from '../repositories/event.repository'; import { StateGraphRepository } from '../repositories/state-graph.repository'; import { ConfigService } from './config.service'; +import { createLogger } from '@rocket.chat/federation-core'; type State = Map; @@ -92,23 +93,23 @@ export class StateService { return event.content; } - async getRoomVersion(roomId: string): Promise { - const createEntry = - await this.stateRepository.findCreateEventByRoomId(roomId); - if (!createEntry) { + async getRoomVersion(roomId: string): Promise { + const createEventId = await this.stateRepository.findCreateEventIdByRoomId( + roomId as RoomID, + ); + if (!createEventId) { throw new Error( 'Create event not found for room version maybe event hasn;t been processed yet', ); } - const createEvent = await this.eventRepository.findById( - createEntry.delta.eventId, - ); + const createEvent = await this.eventRepository.findById(createEventId); if (!createEvent) { throw new UnknownRoomError(roomId as RoomID); } if (createEvent.event.type === 'm.room.create') { + // just getting typescriopt to help return createEvent.event.content.room_version; } diff --git a/packages/homeserver/src/middlewares/isAuthenticated.ts b/packages/homeserver/src/middlewares/isAuthenticated.ts index 125912618..22e2bf3bd 100644 --- a/packages/homeserver/src/middlewares/isAuthenticated.ts +++ b/packages/homeserver/src/middlewares/isAuthenticated.ts @@ -30,14 +30,16 @@ export const isAuthenticatedMiddleware = () => { } } - const isValid = await federationSDK.verifyRequestSignature( - authorizationHeader, - method, - uri, - body, - ); - - if (!isValid) { + try { + await federationSDK.verifyRequestSignature({ + authorizationHeader, + method, + uri, + body, + }); + } catch (error) { + // TODO: log better + console.error(error); set.status = 401; return { authenticatedServer: undefined, @@ -45,7 +47,7 @@ export const isAuthenticatedMiddleware = () => { } return { - authenticatedServer: isValid, + authenticatedServer: true, }; } catch (error) { console.error('Authentication error:', error); diff --git a/packages/room/src/manager/event-wrapper.ts b/packages/room/src/manager/event-wrapper.ts index c3dd69b15..5dc3716b7 100644 --- a/packages/room/src/manager/event-wrapper.ts +++ b/packages/room/src/manager/event-wrapper.ts @@ -4,8 +4,6 @@ import { encodeCanonicalJson, toUnpaddedBase64, } from '@rocket.chat/federation-crypto'; -toUnpaddedBase64, -} from '@rocket.chat/federation-crypto' import { type RejectCode, RejectCodes } from '../authorizartion-rules/errors'; import { type EventStore, @@ -479,5 +477,16 @@ export abstract class PersistentEventBase< getOriginKeys() { return Object.keys(this.signatures[this.origin]); } + + toStrippedJson() { + return encodeCanonicalJson({ + eventId: this.eventId, + type: this.type, + roomId: this.roomId, + sender: this.sender, + stateKey: this.stateKey, + }); + } } + export type { EventStore }; From aa3ea9832597a9a179531b887c19589e6cc7cbc2 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 13:16:54 +0530 Subject: [PATCH 32/56] fix: passing event service tests --- packages/federation-sdk/src/services/event.service.spec.ts | 6 ++++++ packages/federation-sdk/src/services/state.service.ts | 4 +--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts index b80fb4c77..589f624b2 100644 --- a/packages/federation-sdk/src/services/event.service.spec.ts +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -224,6 +224,12 @@ describe('EventService', async () => { // to allow fetchign the key we mock fetchJsonMock.mockReturnValue(Promise.resolve(originalKeyResponse)); + console.log(pdu, pdu.event); + + eventService + .validateHashAndSignatures(pdu.event, roomVersion) + .catch(console.error); + await expect( eventService.validateHashAndSignatures(pdu.event, roomVersion), ).resolves.toHaveProperty('eventId', pdu.eventId); diff --git a/packages/federation-sdk/src/services/state.service.ts b/packages/federation-sdk/src/services/state.service.ts index 429e6cf88..fb21fcd55 100644 --- a/packages/federation-sdk/src/services/state.service.ts +++ b/packages/federation-sdk/src/services/state.service.ts @@ -334,13 +334,11 @@ export class StateService { } public async signEvent(event: T) { - if (process.env.NODE_ENV === 'test') return event; - const signingKey = await this.configService.getSigningKey(); const origin = this.configService.serverName; - const { signatures, unsigned, ...toSign } = event.redactedEvent; + const { signatures: _, unsigned: __, ...toSign } = event.redactedEvent; const signature = await signJson( // Before signing the event, the content hash of the event is calculated as described below. The hash is encoded using Unpadded Base64 and stored in the event object, in a hashes object, under a sha256 key. From 58d5e7f46c52b212314db46443a4d404179aef22 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 13:18:56 +0530 Subject: [PATCH 33/56] fix: signature verification service tests --- .../signature-verification.service.spec.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/federation-sdk/src/services/signature-verification.service.spec.ts b/packages/federation-sdk/src/services/signature-verification.service.spec.ts index 9bf68aab1..9bfe65876 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.spec.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.spec.ts @@ -13,8 +13,14 @@ import { loadEd25519SignerFromSeed, loadEd25519VerifierFromPublicKey, } from '@rocket.chat/federation-crypto'; -import { PersistentEventFactory } from '@rocket.chat/federation-room'; +import { + EventID, + PersistentEventFactory, + RoomID, + UserID, +} from '@rocket.chat/federation-room'; import { SignatureVerificationService } from './signature-verification.service'; +import { keyService } from '../__mocks__/services.spec'; const originServer = 'syn1.tunnel.dev.rocket.chat'; @@ -27,7 +33,7 @@ const event = { '$Ulggyo4m1OlI08Z0jJDVeceigjSZP9SdEFVoAn9mEh8', '$G2TzsvetG2YlHr20tZLHCCzOd-yxPa1jeFT8OU4_6kg', '$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs', - ], + ] as EventID[], content: { avatar_url: null, displayname: 'debdut1', @@ -37,10 +43,10 @@ const event = { hashes: { sha256: '6MnKSCFJy1fYf6ukILBEbqx2DkoaD1wRyKXhv689a0A' }, origin: 'syn1.tunnel.dev.rocket.chat', origin_server_ts: 1757328411218, - prev_events: ['$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs'], - room_id: '!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat', - sender: '@debdut1:syn1.tunnel.dev.rocket.chat', - state_key: '@debdut1:syn1.tunnel.dev.rocket.chat', + prev_events: ['$kXOAfDVvahrwzHEOInzmG941IeEJTn-qUOY0YnLIigs'] as EventID[], + room_id: '!VoUasOLSpcdtRbGHdT:syn2.tunnel.dev.rocket.chat' as RoomID, + sender: '@debdut1:syn1.tunnel.dev.rocket.chat' as UserID, + state_key: '@debdut1:syn1.tunnel.dev.rocket.chat' as UserID, type: 'm.room.member' as const, signatures: { 'syn1.tunnel.dev.rocket.chat': { @@ -84,7 +90,7 @@ describe('SignatureVerificationService', async () => { ); beforeEach(() => { - service = new SignatureVerificationService(); // invalidates internal cache + service = new SignatureVerificationService(keyService); // invalidates internal cache }); afterEach(async () => { From ae6ea98ec19dfcf07f97216e810c6994d2bd0fe7 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 13:34:22 +0530 Subject: [PATCH 34/56] chore: sep tests that require mongo --- .github/workflows/ci.yml | 2 +- package.json | 1 + .../src/__mocks__/block-if-no-mongo.ts | 8 + .../src/services/event.service.spec.ts | 500 +-- .../src/services/key.service.spec.ts | 361 +- .../signature-verification.service.spec.ts | 337 +- .../src/services/state.service.spec.ts | 3874 +++++++++-------- 7 files changed, 2560 insertions(+), 2523 deletions(-) create mode 100644 packages/federation-sdk/src/__mocks__/block-if-no-mongo.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9074e5be..e28af18a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - run: bun lint:ci - run: bun tsc --noEmit - uses: supercharge/mongodb-github-action@1.12.0 - - run: LOG_LEVEL=debug RUN_MONGO_TESTS=1 bun test packages/federation-sdk/src/services/state.service.spec.ts + - run: LOG_LEVEL=debug bun run test:withMongo - run: LOG_LEVEL=debug bun test:coverage - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 diff --git a/package.json b/package.json index 5e5ec4a37..c9f99df21 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "test": "bun test", "build": "turbo build", "test:coverage": "bun test --coverage", + "test:withMongo": "export RUN_MONGO_TESTS=1; grep -r runIfMongoExists packages/federation-sdk/src/services/ | awk -F: '/spec/ { print $1 }' | xargs bun test {}", "lint": "bunx @biomejs/biome lint --diagnostic-level=error", "lint:ci": "bunx @biomejs/biome ci --diagnostic-level=error", "lint:fix": "bunx @biomejs/biome lint --fix", diff --git a/packages/federation-sdk/src/__mocks__/block-if-no-mongo.ts b/packages/federation-sdk/src/__mocks__/block-if-no-mongo.ts new file mode 100644 index 000000000..17911b17f --- /dev/null +++ b/packages/federation-sdk/src/__mocks__/block-if-no-mongo.ts @@ -0,0 +1,8 @@ +export function runIfMongoExists(t: () => void) { + if (!process.env.RUN_MONGO_TESTS) { + console.warn('Skipping tests that require a database'); + return; + } + + t(); +} diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts index 589f624b2..a575227b0 100644 --- a/packages/federation-sdk/src/services/event.service.spec.ts +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -23,6 +23,7 @@ import { config } from '../__mocks__/config.service.spec'; import { repositories } from '../__mocks__/repositories.spec'; import { eventService } from '../__mocks__/services.spec'; import { StateService } from './state.service'; +import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; const event = { auth_events: [ @@ -59,276 +60,281 @@ const event = { }, }; -describe('EventService', async () => { - it('should fail to fetch room informatin of unknown room, sanity check for mock loading', async () => { - expect( - eventService.getRoomVersion({ room_id: 'abc123' } as Pdu), - ).rejects.toThrowError(/Create event not found/); - }); - - const { getHomeserverFinalAddress: originalServerDiscovery } = await import( - '../server-discovery/discovery' - ); - - const { fetch } = await import('@rocket.chat/federation-core'); - - // random server name for each run - let inboundServer = `localhost${Math.floor(Math.random() * 10000).toString()}`; - - type FetchJson = Awaited>['json']; - - const fetchJsonMock: Mock = mock(() => Promise.resolve()); - - beforeEach(async () => { - await mock.module('../server-discovery/discovery', () => ({ - // this mock doesn't matter, or doesn't change, we just need to skip actual server discovery - // and mock the /key/v2/server responses - getHomeserverFinalAddress: async (..._args: any[]) => [ - 'https://127.0.0.1', - {}, - ], - })); - - await mock.module('@rocket.chat/federation-core', () => ({ - fetch: async (..._args: any[]) => { - return { - ok: true, - status: 200, - json: fetchJsonMock as unknown as FetchJson, - } as Response; - }, - })); - }); - - afterEach(async () => { - await mock.module('@rocket.chat/federation-core', () => ({ fetch })); - await mock.module('../server-discovery/discovery', () => ({ - getHomeserverFinalAddress: originalServerDiscovery, - })); - mock.restore(); - }); - - describe('processIncomingTransaction', async () => { - it('should fail basic malformed payloads (sanity checks)', async () => { +runIfMongoExists(() => + describe('EventService', async () => { + it('should fail to fetch room informatin of unknown room, sanity check for mock loading', async () => { expect( - eventService.processIncomingTransaction({ - origin: 'test.local', - // @ts-expect-error - pdus: {}, - }), - ).rejects.toThrowError(/pdus must be an array/); - expect( - eventService.processIncomingTransaction({ - origin: 'test.local', - pdus: [], - // @ts-expect-error - edus: {}, - }), - ).rejects.toThrowError(/edus must be an array/); - - expect( - eventService.processIncomingTransaction({ - origin: 'test.local', - pdus: Array.from({ length: 51 }).fill({}) as Pdu[], - edus: [], - }), - ).rejects.toThrowError(/too-many-events/); - - expect( - eventService.processIncomingTransaction({ - origin: 'test.local', - edus: Array.from({ length: 101 }).fill({}) as BaseEDU[], - pdus: [], - }), - ).rejects.toThrowError(/too-many-events/); - - // NOTE(deb): should also check the happy path but not running all function tests so skipping that + eventService.getRoomVersion({ room_id: 'abc123' } as Pdu), + ).rejects.toThrowError(/Create event not found/); }); - }); - - describe('_validateHashAndSignatures', async () => { - const roomVersion = '10' as const; - // to build events with different signatures, creating new instance of stateService here - const newSeed = 'JFU4ln6/aSnXWF5EY9m7N9Z/MDUHRLt9C+Z6Vv34Ims'; - const version = 'xxx'; - const signer = await loadEd25519SignerFromSeed( - fromBase64ToBytes(newSeed), - version, + const { getHomeserverFinalAddress: originalServerDiscovery } = await import( + '../server-discovery/discovery' ); - let stateService: StateService; + const { fetch } = await import('@rocket.chat/federation-core'); - beforeEach(async () => { - inboundServer = `localhost${Math.floor(Math.random() * 10000).toString()}`; - const newConfig = { - ...config, - getSigningKey: async () => signer, - serverName: inboundServer, - } as unknown as typeof config; - - stateService = new StateService( - repositories.states, - repositories.events, - newConfig, - ); - }); - // this should be changed as needed - const originalKeyResponse = { - old_verify_keys: {}, - server_name: inboundServer, - signatures: {}, - verify_keys: { - [signer.id]: { - key: Buffer.from(signer.getPublicKey()).toString('base64'), - }, - }, - valid_until_ts: Date.now() + 100000, - }; - - // sanity check - it('should sign events with new keys', async () => { - const pdu = PersistentEventFactory.createFromRawEvent(event, roomVersion); + // random server name for each run + let inboundServer = `localhost${Math.floor(Math.random() * 10000).toString()}`; - await stateService.signEvent(pdu); + type FetchJson = Awaited>['json']; - expect(pdu.event.signatures[inboundServer]).toBeDefined(); - expect(pdu.event.signatures?.[inboundServer]).toHaveProperty( - `ed25519:${version}`, - ); + const fetchJsonMock: Mock = mock(() => Promise.resolve()); - // now this stateService will pretend to be the other homeserver + beforeEach(async () => { + await mock.module('../server-discovery/discovery', () => ({ + // this mock doesn't matter, or doesn't change, we just need to skip actual server discovery + // and mock the /key/v2/server responses + getHomeserverFinalAddress: async (..._args: any[]) => [ + 'https://127.0.0.1', + {}, + ], + })); + + await mock.module('@rocket.chat/federation-core', () => ({ + fetch: async (..._args: any[]) => { + return { + ok: true, + status: 200, + json: fetchJsonMock as unknown as FetchJson, + } as Response; + }, + })); }); - it('should fail if event has an invalid hash', async () => { - const eventCopy = JSON.parse(JSON.stringify(event)); - eventCopy.content.avatar_url = undefined; - - await expect( - eventService.validateHashAndSignatures(eventCopy, roomVersion), - ).rejects.toThrowError(/M_INVALID_HASH/); + afterEach(async () => { + await mock.module('@rocket.chat/federation-core', () => ({ fetch })); + await mock.module('../server-discovery/discovery', () => ({ + getHomeserverFinalAddress: originalServerDiscovery, + })); + mock.restore(); }); - it('should successfully validate hash and signature (happy path)', async () => { - // 1. create an event - const pdu = PersistentEventFactory.newCreateEvent( - `@creator:${inboundServer}` as UserID, - roomVersion, - ); - - console.log('PDU', pdu.eventId); - - await stateService.signEvent(pdu); - - // now OUR event service gets this event - // to allow fetchign the key we mock - fetchJsonMock.mockReturnValue(Promise.resolve(originalKeyResponse)); - - console.log(pdu, pdu.event); - - eventService - .validateHashAndSignatures(pdu.event, roomVersion) - .catch(console.error); - - await expect( - eventService.validateHashAndSignatures(pdu.event, roomVersion), - ).resolves.toHaveProperty('eventId', pdu.eventId); + describe('processIncomingTransaction', async () => { + it('should fail basic malformed payloads (sanity checks)', async () => { + expect( + eventService.processIncomingTransaction({ + origin: 'test.local', + // @ts-expect-error + pdus: {}, + }), + ).rejects.toThrowError(/pdus must be an array/); + expect( + eventService.processIncomingTransaction({ + origin: 'test.local', + pdus: [], + // @ts-expect-error + edus: {}, + }), + ).rejects.toThrowError(/edus must be an array/); + + expect( + eventService.processIncomingTransaction({ + origin: 'test.local', + pdus: Array.from({ length: 51 }).fill({}) as Pdu[], + edus: [], + }), + ).rejects.toThrowError(/too-many-events/); + + expect( + eventService.processIncomingTransaction({ + origin: 'test.local', + edus: Array.from({ length: 101 }).fill({}) as BaseEDU[], + pdus: [], + }), + ).rejects.toThrowError(/too-many-events/); + + // NOTE(deb): should also check the happy path but not running all function tests so skipping that + }); }); - it('should fail if signed by a key expired at the point of event creation', async () => { - const pdu = PersistentEventFactory.newCreateEvent( - `@creator:${inboundServer}` as UserID, - roomVersion, - ); - - // event created NOW, so will we sign - await stateService.signEvent(pdu); + describe('_validateHashAndSignatures', async () => { + const roomVersion = '10' as const; - // but the key is expired - const expiredKeyResponse = { - ...originalKeyResponse, - valid_until_ts: Date.now() - 10000000, - }; - fetchJsonMock.mockReturnValue(Promise.resolve(expiredKeyResponse)); - - await expect( - eventService.validateHashAndSignatures(pdu.event, roomVersion), - ).rejects.toThrow(); + // to build events with different signatures, creating new instance of stateService here + const newSeed = 'JFU4ln6/aSnXWF5EY9m7N9Z/MDUHRLt9C+Z6Vv34Ims'; + const version = 'xxx'; + const signer = await loadEd25519SignerFromSeed( + fromBase64ToBytes(newSeed), + version, + ); - // not enough, now we add the key to old_verify_keys - const oldKeyResponse = { - ...originalKeyResponse, - verify_keys: {}, - old_verify_keys: { + let stateService: StateService; + + beforeEach(async () => { + inboundServer = `localhost${Math.floor(Math.random() * 10000).toString()}`; + const newConfig = { + ...config, + getSigningKey: async () => signer, + serverName: inboundServer, + } as unknown as typeof config; + + stateService = new StateService( + repositories.states, + repositories.events, + newConfig, + ); + }); + // this should be changed as needed + const originalKeyResponse = { + old_verify_keys: {}, + server_name: inboundServer, + signatures: {}, + verify_keys: { [signer.id]: { key: Buffer.from(signer.getPublicKey()).toString('base64'), - expired_ts: Date.now() - 1000, }, }, + valid_until_ts: Date.now() + 100000, }; - fetchJsonMock.mockReturnValue(Promise.resolve(oldKeyResponse)); - - // need to invalidate the cache though - // new room id does it - const pdu2 = PersistentEventFactory.newCreateEvent( - `@creator:${inboundServer}` as UserID, - roomVersion, - ); - - // event created NOW, so will we sign - await stateService.signEvent(pdu2); - - await expect( - eventService.validateHashAndSignatures(pdu2.event, roomVersion), - ).rejects.toThrow(); - }); - - it('should fail if signed by an unknown key', async () => { - // 1. create an event - const pdu = PersistentEventFactory.newCreateEvent( - `@creator:${inboundServer}` as UserID, - roomVersion, - ); - - await stateService.signEvent(pdu); - - // don't send any keys - fetchJsonMock.mockReturnValue( - Promise.resolve({ ...originalKeyResponse, verify_keys: {} }), - ); - - await expect( - eventService.validateHashAndSignatures(pdu.event, roomVersion), - ).rejects.toThrow(); - }); - - it('should pass if signed by an old key', async () => { - const pdu = PersistentEventFactory.newCreateEvent( - `@creator:${inboundServer}` as UserID, - roomVersion, - ); - - (pdu as any).rawEvent.origin_server_ts -= 2000; // slightly older event - // event created NOW, so will we sign - await stateService.signEvent(pdu); - - // key is expired but valid at event time - const oldKeyResponse = { - ...originalKeyResponse, - verify_keys: {}, - old_verify_keys: { - [signer.id]: { - key: Buffer.from(signer.getPublicKey()).toString('base64'), - expired_ts: pdu.originServerTs + 1, + // sanity check + it('should sign events with new keys', async () => { + const pdu = PersistentEventFactory.createFromRawEvent( + event, + roomVersion, + ); + + await stateService.signEvent(pdu); + + expect(pdu.event.signatures[inboundServer]).toBeDefined(); + expect(pdu.event.signatures?.[inboundServer]).toHaveProperty( + `ed25519:${version}`, + ); + + // now this stateService will pretend to be the other homeserver + }); + + it('should fail if event has an invalid hash', async () => { + const eventCopy = JSON.parse(JSON.stringify(event)); + eventCopy.content.avatar_url = undefined; + + await expect( + eventService.validateHashAndSignatures(eventCopy, roomVersion), + ).rejects.toThrowError(/M_INVALID_HASH/); + }); + + it('should successfully validate hash and signature (happy path)', async () => { + // 1. create an event + const pdu = PersistentEventFactory.newCreateEvent( + `@creator:${inboundServer}` as UserID, + roomVersion, + ); + + console.log('PDU', pdu.eventId); + + await stateService.signEvent(pdu); + + // now OUR event service gets this event + // to allow fetchign the key we mock + fetchJsonMock.mockReturnValue(Promise.resolve(originalKeyResponse)); + + console.log(pdu, pdu.event); + + eventService + .validateHashAndSignatures(pdu.event, roomVersion) + .catch(console.error); + + await expect( + eventService.validateHashAndSignatures(pdu.event, roomVersion), + ).resolves.toHaveProperty('eventId', pdu.eventId); + }); + + it('should fail if signed by a key expired at the point of event creation', async () => { + const pdu = PersistentEventFactory.newCreateEvent( + `@creator:${inboundServer}` as UserID, + roomVersion, + ); + + // event created NOW, so will we sign + await stateService.signEvent(pdu); + + // but the key is expired + const expiredKeyResponse = { + ...originalKeyResponse, + valid_until_ts: Date.now() - 10000000, + }; + fetchJsonMock.mockReturnValue(Promise.resolve(expiredKeyResponse)); + + await expect( + eventService.validateHashAndSignatures(pdu.event, roomVersion), + ).rejects.toThrow(); + + // not enough, now we add the key to old_verify_keys + const oldKeyResponse = { + ...originalKeyResponse, + verify_keys: {}, + old_verify_keys: { + [signer.id]: { + key: Buffer.from(signer.getPublicKey()).toString('base64'), + expired_ts: Date.now() - 1000, + }, }, - }, - }; - fetchJsonMock.mockReturnValue(Promise.resolve(oldKeyResponse)); + }; + fetchJsonMock.mockReturnValue(Promise.resolve(oldKeyResponse)); + + // need to invalidate the cache though + // new room id does it + const pdu2 = PersistentEventFactory.newCreateEvent( + `@creator:${inboundServer}` as UserID, + roomVersion, + ); + + // event created NOW, so will we sign + await stateService.signEvent(pdu2); + + await expect( + eventService.validateHashAndSignatures(pdu2.event, roomVersion), + ).rejects.toThrow(); + }); + + it('should fail if signed by an unknown key', async () => { + // 1. create an event + const pdu = PersistentEventFactory.newCreateEvent( + `@creator:${inboundServer}` as UserID, + roomVersion, + ); + + await stateService.signEvent(pdu); + + // don't send any keys + fetchJsonMock.mockReturnValue( + Promise.resolve({ ...originalKeyResponse, verify_keys: {} }), + ); + + await expect( + eventService.validateHashAndSignatures(pdu.event, roomVersion), + ).rejects.toThrow(); + }); + + it('should pass if signed by an old key', async () => { + const pdu = PersistentEventFactory.newCreateEvent( + `@creator:${inboundServer}` as UserID, + roomVersion, + ); + + (pdu as any).rawEvent.origin_server_ts -= 2000; // slightly older event + + // event created NOW, so will we sign + await stateService.signEvent(pdu); + + // key is expired but valid at event time + const oldKeyResponse = { + ...originalKeyResponse, + verify_keys: {}, + old_verify_keys: { + [signer.id]: { + key: Buffer.from(signer.getPublicKey()).toString('base64'), + expired_ts: pdu.originServerTs + 1, + }, + }, + }; + fetchJsonMock.mockReturnValue(Promise.resolve(oldKeyResponse)); - await expect( - eventService.validateHashAndSignatures(pdu.event, roomVersion), - ).resolves.toHaveProperty('eventId', pdu.eventId); + await expect( + eventService.validateHashAndSignatures(pdu.event, roomVersion), + ).resolves.toHaveProperty('eventId', pdu.eventId); + }); }); - }); -}); + }), +); diff --git a/packages/federation-sdk/src/services/key.service.spec.ts b/packages/federation-sdk/src/services/key.service.spec.ts index 2f330ad8d..8550b2ad7 100644 --- a/packages/federation-sdk/src/services/key.service.spec.ts +++ b/packages/federation-sdk/src/services/key.service.spec.ts @@ -6,210 +6,213 @@ import { afterEach, beforeEach } from 'node:test'; import { config } from '../__mocks__/config.service.spec'; import { keyService } from '../__mocks__/services.spec'; import { signer } from '../__mocks__/singer.spec'; +import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; + +runIfMongoExists(() => + describe('KeyService', async () => { + // fetch mocking + const { fetch } = await import('@rocket.chat/federation-core'); + + let inboundServer = ''; // skips server discovery + + beforeEach(async () => { + await mock.module('@rocket.chat/federation-core', () => ({ + fetch: async (..._args: any[]) => { + return { + ok: true, + status: 200, + json: fetchJsonMock as unknown as FetchJson, + } as Response; + }, + })); + inboundServer = `localhost:${Math.floor(Math.random() * 10000)}`; + }); + + afterEach(async () => { + await mock.module('@rocket.chat/federation-core', () => ({ fetch })); + mock.restore(); + }); + + type FetchJson = Awaited>['json']; + + const fetchJsonMock: Mock = mock(() => Promise.resolve()); + const publicKey = Buffer.from(signer.getPublicKey()).toString('base64'); + + it('should act as a notary server', async () => { + fetchJsonMock.mockReturnValue( + Promise.resolve({ + server_name: inboundServer, + valid_until_ts: Date.now() + 100000, + verify_keys: { + 'ed25519:0': { key: publicKey }, + }, + signatures: { + [inboundServer]: { + 'ed25519:0': 'c2lnbmF0dXJl', // dummy signature, not verified in this test + }, + }, + old_verify_keys: {}, + }), + ); + + const response = await keyService.handleQuery({ + server_keys: { [inboundServer]: {} }, + }); -describe('KeyService', async () => { - // fetch mocking - const { fetch } = await import('@rocket.chat/federation-core'); - - let inboundServer = ''; // skips server discovery - - beforeEach(async () => { - await mock.module('@rocket.chat/federation-core', () => ({ - fetch: async (..._args: any[]) => { - return { - ok: true, - status: 200, - json: fetchJsonMock as unknown as FetchJson, - } as Response; - }, - })); - inboundServer = `localhost:${Math.floor(Math.random() * 10000)}`; - }); - - afterEach(async () => { - await mock.module('@rocket.chat/federation-core', () => ({ fetch })); - mock.restore(); - }); - - type FetchJson = Awaited>['json']; - - const fetchJsonMock: Mock = mock(() => Promise.resolve()); - const publicKey = Buffer.from(signer.getPublicKey()).toString('base64'); - - it('should act as a notary server', async () => { - fetchJsonMock.mockReturnValue( - Promise.resolve({ + expect(response).toHaveProperty('server_keys'); + expect(response.server_keys).toBeArray(); + + const key = response.server_keys.find( + (k: unknown) => + typeof k === 'object' && + k !== null && + 'server_name' in k && + k.server_name === inboundServer && + 'verify_keys' in k && + typeof k.verify_keys === 'object' && + k.verify_keys !== null && + 'ed25519:0' in k.verify_keys && + k.verify_keys['ed25519:0'], + ); + + expect(key).toBeDefined(); + + expect(key).toHaveProperty('verify_keys'); + expect(key.verify_keys).toHaveProperty('ed25519:0'); + expect(key.verify_keys['ed25519:0']).toHaveProperty('key'); + expect(key.verify_keys['ed25519:0'].key).toBeString(); + expect(key.verify_keys['ed25519:0'].key).toBe(publicKey); + + const signature = key?.signatures?.[config.serverName]; + + expect(signature).toBeDefined(); + expect(Object.keys(signature).length).toBeGreaterThanOrEqual(1); + + const signatureValue = signature?.['ed25519:0']; + + expect(signatureValue).toBeDefined(); + + const { signatures, ...rest } = key; + + expect( + verifyJsonSignature(rest, signatureValue, signer), + ).resolves.toBeUndefined(); + }); + + it('should return an expired key if it can not find any others', async () => { + const keyId0 = 'ed25519:0'; + // -24 hours + const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000); + + const keyResponse0 = { server_name: inboundServer, - valid_until_ts: Date.now() + 100000, + valid_until_ts: expiresAt.getTime(), verify_keys: { - 'ed25519:0': { key: publicKey }, - }, - signatures: { - [inboundServer]: { - 'ed25519:0': 'c2lnbmF0dXJl', // dummy signature, not verified in this test - }, + [keyId0]: { key: publicKey }, }, old_verify_keys: {}, - }), - ); + signatures: {}, + }; - const response = await keyService.handleQuery({ - server_keys: { [inboundServer]: {} }, - }); + fetchJsonMock.mockReturnValue(Promise.resolve(keyResponse0)); - expect(response).toHaveProperty('server_keys'); - expect(response.server_keys).toBeArray(); - - const key = response.server_keys.find( - (k: unknown) => - typeof k === 'object' && - k !== null && - 'server_name' in k && - k.server_name === inboundServer && - 'verify_keys' in k && - typeof k.verify_keys === 'object' && - k.verify_keys !== null && - 'ed25519:0' in k.verify_keys && - k.verify_keys['ed25519:0'], - ); - - expect(key).toBeDefined(); - - expect(key).toHaveProperty('verify_keys'); - expect(key.verify_keys).toHaveProperty('ed25519:0'); - expect(key.verify_keys['ed25519:0']).toHaveProperty('key'); - expect(key.verify_keys['ed25519:0'].key).toBeString(); - expect(key.verify_keys['ed25519:0'].key).toBe(publicKey); - - const signature = key?.signatures?.[config.serverName]; - - expect(signature).toBeDefined(); - expect(Object.keys(signature).length).toBeGreaterThanOrEqual(1); - - const signatureValue = signature?.['ed25519:0']; - - expect(signatureValue).toBeDefined(); - - const { signatures, ...rest } = key; - - expect( - verifyJsonSignature(rest, signatureValue, signer), - ).resolves.toBeUndefined(); - }); - - it('should return an expired key if it can not find any others', async () => { - const keyId0 = 'ed25519:0'; - // -24 hours - const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000); - - const keyResponse0 = { - server_name: inboundServer, - valid_until_ts: expiresAt.getTime(), - verify_keys: { - [keyId0]: { key: publicKey }, - }, - old_verify_keys: {}, - signatures: {}, - }; - - fetchJsonMock.mockReturnValue(Promise.resolve(keyResponse0)); - - // fills the database with an expired key - await keyService.handleQuery({ - server_keys: { [inboundServer]: { [keyId0]: {} } }, - }); + // fills the database with an expired key + await keyService.handleQuery({ + server_keys: { [inboundServer]: { [keyId0]: {} } }, + }); - // make a second request - await keyService.handleQuery({ - server_keys: { - [inboundServer]: { - [keyId0]: { - minimum_valid_until_ts: expiresAt.getTime() + 1000, + // make a second request + await keyService.handleQuery({ + server_keys: { + [inboundServer]: { + [keyId0]: { + minimum_valid_until_ts: expiresAt.getTime() + 1000, + }, }, }, - }, - }); + }); - const { server_keys: serverKeys } = await keyService.handleQuery({ - server_keys: { - [inboundServer]: { - [keyId0]: { - minimum_valid_until_ts: expiresAt.getTime() + 1000, + const { server_keys: serverKeys } = await keyService.handleQuery({ + server_keys: { + [inboundServer]: { + [keyId0]: { + minimum_valid_until_ts: expiresAt.getTime() + 1000, + }, }, }, - }, - }); + }); - expect(serverKeys).toBeArray(); - expect(serverKeys[0]).toHaveProperty('server_name', inboundServer); - expect(serverKeys[0].verify_keys).toHaveProperty(keyId0); - expect(serverKeys[0].valid_until_ts).toBe(expiresAt.getTime()); - }); - - it('must not overwrite a valid key with a spurious result from the origin server', async () => { - const keyid1 = 'ed25519:1'; - // -24 houts - const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000); - - const keyResponse1 = { - server_name: inboundServer, - valid_until_ts: expiresAt.getTime(), - verify_keys: { - [keyid1]: { key: publicKey }, - }, - old_verify_keys: {}, - signatures: {}, - }; - - fetchJsonMock.mockReturnValue(Promise.resolve(keyResponse1)); - - const response1 = await keyService.handleQuery({ - server_keys: { [inboundServer]: { [keyid1]: {} } }, + expect(serverKeys).toBeArray(); + expect(serverKeys[0]).toHaveProperty('server_name', inboundServer); + expect(serverKeys[0].verify_keys).toHaveProperty(keyId0); + expect(serverKeys[0].valid_until_ts).toBe(expiresAt.getTime()); }); - expect(response1.server_keys[0]).toHaveProperty( - 'server_name', - inboundServer, - ); - expect(response1.server_keys[0].verify_keys).toHaveProperty(keyid1); + it('must not overwrite a valid key with a spurious result from the origin server', async () => { + const keyid1 = 'ed25519:1'; + // -24 houts + const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000); - const keyid2 = 'ed25519:2'; - fetchJsonMock.mockReturnValue( - Promise.resolve({ + const keyResponse1 = { server_name: inboundServer, - valid_until_ts: expiresAt.getTime() + 1000, + valid_until_ts: expiresAt.getTime(), verify_keys: { - [keyid2]: { key: publicKey }, + [keyid1]: { key: publicKey }, }, old_verify_keys: {}, signatures: {}, - }), - ); - - await keyService.handleQuery({ - server_keys: { - [inboundServer]: { - [keyid1]: { - minimum_valid_until_ts: Date.now(), + }; + + fetchJsonMock.mockReturnValue(Promise.resolve(keyResponse1)); + + const response1 = await keyService.handleQuery({ + server_keys: { [inboundServer]: { [keyid1]: {} } }, + }); + + expect(response1.server_keys[0]).toHaveProperty( + 'server_name', + inboundServer, + ); + expect(response1.server_keys[0].verify_keys).toHaveProperty(keyid1); + + const keyid2 = 'ed25519:2'; + fetchJsonMock.mockReturnValue( + Promise.resolve({ + server_name: inboundServer, + valid_until_ts: expiresAt.getTime() + 1000, + verify_keys: { + [keyid2]: { key: publicKey }, + }, + old_verify_keys: {}, + signatures: {}, + }), + ); + + await keyService.handleQuery({ + server_keys: { + [inboundServer]: { + [keyid1]: { + minimum_valid_until_ts: Date.now(), + }, }, }, - }, - }); + }); - const finalResponse = await keyService.handleQuery({ - server_keys: { - [inboundServer]: { - [keyid1]: { - minimum_valid_until_ts: expiresAt.getTime(), + const finalResponse = await keyService.handleQuery({ + server_keys: { + [inboundServer]: { + [keyid1]: { + minimum_valid_until_ts: expiresAt.getTime(), + }, }, }, - }, - }); + }); - expect(finalResponse.server_keys[0]).toHaveProperty( - 'server_name', - inboundServer, - ); - expect(finalResponse.server_keys[0].verify_keys).toHaveProperty(keyid1); - }); -}); + expect(finalResponse.server_keys[0]).toHaveProperty( + 'server_name', + inboundServer, + ); + expect(finalResponse.server_keys[0].verify_keys).toHaveProperty(keyid1); + }); + }), +); diff --git a/packages/federation-sdk/src/services/signature-verification.service.spec.ts b/packages/federation-sdk/src/services/signature-verification.service.spec.ts index 9bfe65876..d33af663f 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.spec.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.spec.ts @@ -21,6 +21,7 @@ import { } from '@rocket.chat/federation-room'; import { SignatureVerificationService } from './signature-verification.service'; import { keyService } from '../__mocks__/services.spec'; +import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; const originServer = 'syn1.tunnel.dev.rocket.chat'; @@ -62,203 +63,207 @@ const event = { }, }; -describe('SignatureVerificationService', async () => { - let service: SignatureVerificationService; - - const mockKeyData = { - old_verify_keys: {}, - server_name: 'syn1.tunnel.dev.rocket.chat', - signatures: { - 'syn1.tunnel.dev.rocket.chat': { - 'ed25519:a_FAET': - '32jfhYKQGENYAByGWZlMPcqgLcGJCoU9RyxOz4TGrmGbTwmbBi8BGbgNJHH8DmWuyoD6FnZ4yI5YBZTJqPjQAA', +runIfMongoExists(() => + describe('SignatureVerificationService', async () => { + let service: SignatureVerificationService; + + const mockKeyData = { + old_verify_keys: {}, + server_name: 'syn1.tunnel.dev.rocket.chat', + signatures: { + 'syn1.tunnel.dev.rocket.chat': { + 'ed25519:a_FAET': + '32jfhYKQGENYAByGWZlMPcqgLcGJCoU9RyxOz4TGrmGbTwmbBi8BGbgNJHH8DmWuyoD6FnZ4yI5YBZTJqPjQAA', + }, }, - }, - valid_until_ts: 1757414678669, - verify_keys: { - 'ed25519:a_FAET': { key: 'kryovKVnhHESOdWuZ05ViNotRMVdEh/mG2yJ0npLzEo' }, - }, - }; - - const verifier = await loadEd25519VerifierFromPublicKey( - fromBase64ToBytes(mockKeyData.verify_keys[keyId].key), - 'a_FAET', - ); - - const { MAX_SIGNATURE_LENGTH_FOR_ED25519 } = await import( - './signature-verification.service' - ); - - beforeEach(() => { - service = new SignatureVerificationService(keyService); // invalidates internal cache - }); - - afterEach(async () => { - await mock.module('./signature-verification.service', () => ({ - MAX_SIGNATURE_LENGTH_FOR_ED25519, - })); - }); - - describe('verifyRequestSignature', async () => { - const seed = 'FC6cwY3DNmHo3B7GRugaHNyXz+TkBRVx8RvQH0kSZ04'; - const version = 'a_FAET'; - const signer = await loadEd25519SignerFromSeed( - fromBase64ToBytes(seed), - version, + valid_until_ts: 1757414678669, + verify_keys: { + 'ed25519:a_FAET': { + key: 'kryovKVnhHESOdWuZ05ViNotRMVdEh/mG2yJ0npLzEo', + }, + }, + }; + + const verifier = await loadEd25519VerifierFromPublicKey( + fromBase64ToBytes(mockKeyData.verify_keys[keyId].key), + 'a_FAET', ); - const thisVerifier: VerifierKey = signer; - it('should successfully validate the request', async () => { - const header = - 'X-Matrix origin="syn1.tunnel.dev.rocket.chat",destination="syn2.tunnel.dev.rocket.chat",key="ed25519:a_FAET",sig="+MRd0eKdc/3T7mS7ZR+ltpOiN7RBXgfxTWWYLejy5gBRXG717aXHPCDm044D10kgqQvs2HqR3MdPEIx+2a0nDg"'; + const { MAX_SIGNATURE_LENGTH_FOR_ED25519 } = await import( + './signature-verification.service' + ); - const body = { - edus: [ - { - content: { - push: [ - { - last_active_ago: 561472, - presence: 'unavailable', - user_id: '@debdut1:syn1.tunnel.dev.rocket.chat', - }, - ], + beforeEach(() => { + service = new SignatureVerificationService(keyService); // invalidates internal cache + }); + + afterEach(async () => { + await mock.module('./signature-verification.service', () => ({ + MAX_SIGNATURE_LENGTH_FOR_ED25519, + })); + }); + + describe('verifyRequestSignature', async () => { + const seed = 'FC6cwY3DNmHo3B7GRugaHNyXz+TkBRVx8RvQH0kSZ04'; + const version = 'a_FAET'; + const signer = await loadEd25519SignerFromSeed( + fromBase64ToBytes(seed), + version, + ); + const thisVerifier: VerifierKey = signer; + + it('should successfully validate the request', async () => { + const header = + 'X-Matrix origin="syn1.tunnel.dev.rocket.chat",destination="syn2.tunnel.dev.rocket.chat",key="ed25519:a_FAET",sig="+MRd0eKdc/3T7mS7ZR+ltpOiN7RBXgfxTWWYLejy5gBRXG717aXHPCDm044D10kgqQvs2HqR3MdPEIx+2a0nDg"'; + + const body = { + edus: [ + { + content: { + push: [ + { + last_active_ago: 561472, + presence: 'unavailable', + user_id: '@debdut1:syn1.tunnel.dev.rocket.chat', + }, + ], + }, + edu_type: 'm.presence', }, - edu_type: 'm.presence', - }, - ], - origin: 'syn1.tunnel.dev.rocket.chat', - origin_server_ts: 1757329414731, - pdus: [], - }; + ], + origin: 'syn1.tunnel.dev.rocket.chat', + origin_server_ts: 1757329414731, + pdus: [], + }; - // PUT /_matrix/federation/v1/send/1757328278684 HTTP/1.1 + // PUT /_matrix/federation/v1/send/1757328278684 HTTP/1.1 - const uri = '/_matrix/federation/v1/send/1757328278684'; + const uri = '/_matrix/federation/v1/send/1757328278684'; - const method = 'PUT'; + const method = 'PUT'; - await expect( - service.verifyRequestSignature( - { - uri, - method, - body, - authorizationHeader: header, - }, - thisVerifier, - ), - ).resolves.toBeUndefined(); + await expect( + service.verifyRequestSignature( + { + uri, + method, + body, + authorizationHeader: header, + }, + thisVerifier, + ), + ).resolves.toBeUndefined(); + }); }); - }); - describe('verifyEventSignature', async () => { - it('01 should verify a valid event signature', async () => { - const pdu = PersistentEventFactory.createFromRawEvent(event, '10'); + describe('verifyEventSignature', async () => { + it('01 should verify a valid event signature', async () => { + const pdu = PersistentEventFactory.createFromRawEvent(event, '10'); - return expect( - service.verifyEventSignature(pdu, verifier), - ).resolves.toBeUndefined(); - }); + return expect( + service.verifyEventSignature(pdu, verifier), + ).resolves.toBeUndefined(); + }); - // each step of the spec - it('02 should fail if not signed by the origin server (1)', async () => { - const pdu = PersistentEventFactory.createFromRawEvent( - { - ...event, - signatures: {}, // no signatures - }, - '10', - ); + // each step of the spec + it('02 should fail if not signed by the origin server (1)', async () => { + const pdu = PersistentEventFactory.createFromRawEvent( + { + ...event, + signatures: {}, // no signatures + }, + '10', + ); - return expect( - service.verifyEventSignature(pdu, verifier), - ).rejects.toThrow(`No signature found for origin ${originServer}`); - }); + return expect( + service.verifyEventSignature(pdu, verifier), + ).rejects.toThrow(`No signature found for origin ${originServer}`); + }); - it('03 should fail if signed by algorithm not supported by us (ed25519) (2)', async () => { - const pdu = PersistentEventFactory.createFromRawEvent( - { - ...event, - signatures: { - [originServer]: { - // different algorithm - 'not-supported:0': event.signatures[originServer][keyId], + it('03 should fail if signed by algorithm not supported by us (ed25519) (2)', async () => { + const pdu = PersistentEventFactory.createFromRawEvent( + { + ...event, + signatures: { + [originServer]: { + // different algorithm + 'not-supported:0': event.signatures[originServer][keyId], + }, }, }, + '10', + ); + + return expect( + service.verifyEventSignature(pdu, verifier), + ).rejects.toThrow( + `No valid signature keys found for origin ${originServer} with supported algorithms`, + ); + }); + + test.todo( + 'should pass if service find any of the supported keys from the origin homeserver (3.2)', + async () => { + // need event to be signed by multiple keys }, - '10', ); - return expect( - service.verifyEventSignature(pdu, verifier), - ).rejects.toThrow( - `No valid signature keys found for origin ${originServer} with supported algorithms`, + test.todo( + 'should fail if the signature itself is invalid base64 (4.1)', + async () => { + // need event to be signed by multiple keys + }, ); - }); - - test.todo( - 'should pass if service find any of the supported keys from the origin homeserver (3.2)', - async () => { - // need event to be signed by multiple keys - }, - ); - - test.todo( - 'should fail if the signature itself is invalid base64 (4.1)', - async () => { - // need event to be signed by multiple keys - }, - ); - it('04 should fail if the signature itself is invalid (4.2)', async () => { - const pdu = PersistentEventFactory.createFromRawEvent( - { - ...event, - signatures: { - [originServer]: { - [keyId]: '@@@@', // invalid base64 + it('04 should fail if the signature itself is invalid (4.2)', async () => { + const pdu = PersistentEventFactory.createFromRawEvent( + { + ...event, + signatures: { + [originServer]: { + [keyId]: '@@@@', // invalid base64 + }, }, }, - }, - '10', - ); + '10', + ); - // should fail because the signature length isn't correct for ed25519 - await expect(service.verifyEventSignature(pdu, verifier)).rejects.toThrow( - /Invalid signature length/, - ); + // should fail because the signature length isn't correct for ed25519 + await expect( + service.verifyEventSignature(pdu, verifier), + ).rejects.toThrow(/Invalid signature length/); - await mock.module('./signature-verification.service', () => ({ - MAX_SIGNATURE_LENGTH_FOR_ED25519: 4, - })); + await mock.module('./signature-verification.service', () => ({ + MAX_SIGNATURE_LENGTH_FOR_ED25519: 4, + })); - await expect(service.verifyEventSignature(pdu, verifier)).rejects.toThrow( - /Failed to decode base64 signature /, - ); + await expect( + service.verifyEventSignature(pdu, verifier), + ).rejects.toThrow(/Failed to decode base64 signature /); - const anyString = 'abc123'; - const base64String = btoa(anyString); // valid base64 but not a valid signature + const anyString = 'abc123'; + const base64String = btoa(anyString); // valid base64 but not a valid signature - const pdu2 = PersistentEventFactory.createFromRawEvent( - { - ...event, - signatures: { - [originServer]: { - [keyId]: base64String, + const pdu2 = PersistentEventFactory.createFromRawEvent( + { + ...event, + signatures: { + [originServer]: { + [keyId]: base64String, + }, }, }, - }, - '10', - ); + '10', + ); - await mock.module('./signature-verification.service', () => ({ - MAX_SIGNATURE_LENGTH_FOR_ED25519: base64String.length, - })); + await mock.module('./signature-verification.service', () => ({ + MAX_SIGNATURE_LENGTH_FOR_ED25519: base64String.length, + })); - await expect( - service.verifyEventSignature(pdu2, verifier), - ).rejects.toThrow('Invalid signature'); + await expect( + service.verifyEventSignature(pdu2, verifier), + ).rejects.toThrow('Invalid signature'); + }); }); - }); -}); + }), +); diff --git a/packages/federation-sdk/src/services/state.service.spec.ts b/packages/federation-sdk/src/services/state.service.spec.ts index 0bc5f2504..7f9e0bf46 100644 --- a/packages/federation-sdk/src/services/state.service.spec.ts +++ b/packages/federation-sdk/src/services/state.service.spec.ts @@ -22,6 +22,8 @@ import { import { type ConfigService } from './config.service'; import { DatabaseConnectionService } from './database-connection.service'; import { StateService } from './state.service'; +import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; +import { signer } from '../__mocks__/singer.spec'; type State = Map; @@ -111,164 +113,65 @@ async function copyDepth< return toStripped; } -describe('StateService', async () => { - if (!process.env.RUN_MONGO_TESTS) { - console.warn('Skipping tests that require a database'); - return; - } - - const databaseConfig = { - uri: 'mongodb://localhost:27017', - name: 'matrix_test', - poolSize: 100, - }; - - const configServiceInstance = { - getSigningKey: async () => {}, - serverName: 'example.com', - } as unknown as ConfigService; - - const database = new DatabaseConnectionService(databaseConfig); - - const eventCollection = (await database.getDb()).collection< - WithId - >('events_test'); - const stateGraphCollection = ( - await database.getDb() - ).collection('state_graph_test'); - - beforeEach(async () => { - await Promise.all([ - eventCollection.deleteMany(), - stateGraphCollection.deleteMany(), - ]); - }); - - const eventRepository = new EventRepository(eventCollection); - const stateGraphRepository = new StateGraphRepository(stateGraphCollection); - - // TODO: use IStateService - stateService = new StateService( - stateGraphRepository, - eventRepository, - configServiceInstance, - ); - - const createRoom = async ( - joinRule: PduJoinRuleEventContent['join_rule'], - userPowers: PduPowerLevelsEventContent['users'] = {}, - ) => { - const username = '@alice:example.com'; - const name = 'Test Room'; - - const roomCreateEvent = PersistentEventFactory.newCreateEvent( - username as room.UserID, - PersistentEventFactory.defaultRoomVersion, - ); - await stateService.handlePdu(roomCreateEvent); - - const roomVersion: RoomVersion = - roomCreateEvent.getContent().room_version; - - const creatorMembershipEvent = - await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - state_key: username as room.UserID, - content: { membership: 'join' }, - ...getDefaultFields(), - }, - roomVersion, - ); - - await stateService.handlePdu(creatorMembershipEvent); - - const roomNameEvent = await stateService.buildEvent<'m.room.name'>( - { - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - content: { name }, - state_key: '', - type: 'm.room.name', - ...getDefaultFields(), - }, - roomVersion, - ); +runIfMongoExists(() => + describe('StateService', async () => { + if (!process.env.RUN_MONGO_TESTS) { + console.warn('Skipping tests that require a database'); + return; + } - await stateService.handlePdu(roomNameEvent); + const databaseConfig = { + uri: 'mongodb://localhost:27017', + name: 'matrix_test', + poolSize: 100, + }; - const powerLevelEvent = - await stateService.buildEvent<'m.room.power_levels'>( - { - type: 'm.room.power_levels', - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - state_key: '', - content: { - users: { - [username]: 100, - ...userPowers, - }, - users_default: 0, - events: {}, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 50, - }, - ...getDefaultFields(), - }, - roomVersion, - ); + const configServiceInstance = { + getSigningKey: async () => signer, + serverName: 'example.com', + } as unknown as ConfigService; + + const database = new DatabaseConnectionService(databaseConfig); + + const eventCollection = (await database.getDb()).collection< + WithId + >('events_test'); + const stateGraphCollection = ( + await database.getDb() + ).collection('state_graph_test'); + + beforeEach(async () => { + await Promise.all([ + eventCollection.deleteMany(), + stateGraphCollection.deleteMany(), + ]); + }); - await stateService.handlePdu(powerLevelEvent); + const eventRepository = new EventRepository(eventCollection); + const stateGraphRepository = new StateGraphRepository(stateGraphCollection); - const joinRuleEvent = await stateService.buildEvent<'m.room.join_rules'>( - { - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - content: { join_rule: joinRule }, - type: 'm.room.join_rules', - state_key: '', - ...getDefaultFields(), - }, - roomVersion, + // TODO: use IStateService + stateService = new StateService( + stateGraphRepository, + eventRepository, + configServiceInstance, ); - await stateService.handlePdu(joinRuleEvent); - - return { - roomCreateEvent, - joinRuleEvent, - powerLevelEvent, - creatorMembershipEvent, - roomNameEvent, - }; - }; - - const getStore = ( - cache: Map, - ): room.EventStore => ({ - getEvents: (eventIds: EventID[]) => { - return Promise.resolve(eventIds.map((eid) => cache.get(eid)!)); - }, - }); - - const partialStateEvents = await Promise.all([ - async () => { - const username = '@alice:anotherserver.com' as room.UserID; - const name = 'Test Partial State Room'; + const createRoom = async ( + joinRule: PduJoinRuleEventContent['join_rule'], + userPowers: PduPowerLevelsEventContent['users'] = {}, + ) => { + const username = '@alice:example.com'; + const name = 'Test Room'; const roomCreateEvent = PersistentEventFactory.newCreateEvent( username as room.UserID, PersistentEventFactory.defaultRoomVersion, ); + await stateService.handlePdu(roomCreateEvent); - const roomVersion = roomCreateEvent.version; + const roomVersion: RoomVersion = + roomCreateEvent.getContent().room_version; const creatorMembershipEvent = await stateService.buildEvent<'m.room.member'>( @@ -279,30 +182,11 @@ describe('StateService', async () => { state_key: username as room.UserID, content: { membership: 'join' }, ...getDefaultFields(), - prev_events: [roomCreateEvent.eventId], - auth_events: [roomCreateEvent.eventId], - depth: 1, }, roomVersion, ); - // insert a random message event to make the tree incomplete - const messageEvent = await stateService.buildEvent<'m.room.message'>( - { - room_id: roomCreateEvent.roomId, - sender: username, - content: { body: 'hello world', msgtype: 'm.text' }, - type: 'm.room.message', - ...getDefaultFields(), - prev_events: [creatorMembershipEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], - depth: 2, - }, - roomVersion, - ); + await stateService.handlePdu(creatorMembershipEvent); const roomNameEvent = await stateService.buildEvent<'m.room.name'>( { @@ -312,33 +196,11 @@ describe('StateService', async () => { state_key: '', type: 'm.room.name', ...getDefaultFields(), - prev_events: [messageEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], - depth: 3, }, roomVersion, ); - const joinRuleEvent = await stateService.buildEvent<'m.room.join_rules'>( - { - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - content: { join_rule: 'public' }, - type: 'm.room.join_rules', - state_key: '', - ...getDefaultFields(), - prev_events: [roomNameEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - creatorMembershipEvent.eventId, - ], - depth: 4, - }, - roomVersion, - ); + await stateService.handlePdu(roomNameEvent); const powerLevelEvent = await stateService.buildEvent<'m.room.power_levels'>( @@ -350,6 +212,7 @@ describe('StateService', async () => { content: { users: { [username]: 100, + ...userPowers, }, users_default: 0, events: {}, @@ -361,2065 +224,2216 @@ describe('StateService', async () => { invite: 50, }, ...getDefaultFields(), - prev_events: [joinRuleEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - creatorMembershipEvent.eventId, - joinRuleEvent.eventId, - ], - depth: 5, }, roomVersion, ); - const ourUserJoinEvent = await stateService.buildEvent<'m.room.member'>( + await stateService.handlePdu(powerLevelEvent); + + const joinRuleEvent = await stateService.buildEvent<'m.room.join_rules'>( { - type: 'm.room.member', room_id: roomCreateEvent.roomId, - sender: '@us:example.com' as room.UserID, - state_key: '@us:example.com' as room.UserID, - content: { membership: 'join' }, + sender: username as room.UserID, + content: { join_rule: joinRule }, + type: 'm.room.join_rules', + state_key: '', ...getDefaultFields(), - prev_events: [powerLevelEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - powerLevelEvent.eventId, - joinRuleEvent.eventId, - ], - depth: 6, }, roomVersion, ); - const state = { + + await stateService.handlePdu(joinRuleEvent); + + return { roomCreateEvent, + joinRuleEvent, powerLevelEvent, creatorMembershipEvent, roomNameEvent, - ourUserJoinEvent, - joinRuleEvent, }; + }; - const map = new Map(); - const authChainSet = new Set(); - for (const event of Object.values(state)) { - map.set(event.eventId, event); - } - - const store = getStore(map); - - for (const event of Object.values(state)) { - for (const eventId of await room.getAuthChain(event, store)) { - authChainSet.add(eventId); - } - } - - const authChain = Array.from(authChainSet.values()).map( - (eid) => map.get(eid)!, - ); + const getStore = ( + cache: Map, + ): room.EventStore => ({ + getEvents: (eventIds: EventID[]) => { + return Promise.resolve(eventIds.map((eid) => cache.get(eid)!)); + }, + }); - return { - state, - authChain, - missingEvents: [messageEvent] as PersistentEventBase[], - }; - }, - // multiple missing in the middle - async () => { - const username = '@alice:anotherserver.com' as room.UserID; - const name = 'Test Partial State Room'; + const partialStateEvents = await Promise.all([ + async () => { + const username = '@alice:anotherserver.com' as room.UserID; + const name = 'Test Partial State Room'; - const roomCreateEvent = PersistentEventFactory.newCreateEvent( - username as room.UserID, - PersistentEventFactory.defaultRoomVersion, - ); + const roomCreateEvent = PersistentEventFactory.newCreateEvent( + username as room.UserID, + PersistentEventFactory.defaultRoomVersion, + ); - const roomVersion = roomCreateEvent.version; + const roomVersion = roomCreateEvent.version; + + const creatorMembershipEvent = + await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + state_key: username as room.UserID, + content: { membership: 'join' }, + ...getDefaultFields(), + prev_events: [roomCreateEvent.eventId], + auth_events: [roomCreateEvent.eventId], + depth: 1, + }, + roomVersion, + ); - const creatorMembershipEvent = - await stateService.buildEvent<'m.room.member'>( + // insert a random message event to make the tree incomplete + const messageEvent = await stateService.buildEvent<'m.room.message'>( { - type: 'm.room.member', room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - state_key: username as room.UserID, - content: { membership: 'join' }, + sender: username, + content: { body: 'hello world', msgtype: 'm.text' }, + type: 'm.room.message', ...getDefaultFields(), - prev_events: [roomCreateEvent.eventId], - auth_events: [roomCreateEvent.eventId], - depth: 1, + prev_events: [creatorMembershipEvent.eventId], + auth_events: [ + creatorMembershipEvent.eventId, + roomCreateEvent.eventId, + ], + depth: 2, }, roomVersion, ); - // insert a random message event to make the tree incomplete - const messageEvent = await stateService.buildEvent<'m.room.message'>( - { - room_id: roomCreateEvent.roomId, - sender: username, - content: { body: 'hello world', msgtype: 'm.text' }, - type: 'm.room.message', - ...getDefaultFields(), - prev_events: [creatorMembershipEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], - depth: 2, - }, - roomVersion, - ); - - // this should become an extremity now since no known event will point to this - const roomNameEvent = await stateService.buildEvent<'m.room.name'>( - { - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - content: { name }, - state_key: '', - type: 'm.room.name', - ...getDefaultFields(), - prev_events: [messageEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], - depth: 3, - }, - roomVersion, - ); - - // insert another random message event to make the tree incomplete - const messageEvent2 = await stateService.buildEvent<'m.room.message'>( - { - room_id: roomCreateEvent.roomId, - sender: username, - content: { body: 'hello world', msgtype: 'm.text' }, - type: 'm.room.message', - ...getDefaultFields(), - prev_events: [roomNameEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], - depth: 4, - }, - roomVersion, - ); - - const joinRuleEvent = await stateService.buildEvent<'m.room.join_rules'>( - { - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - content: { join_rule: 'public' }, - type: 'm.room.join_rules', - state_key: '', - ...getDefaultFields(), - prev_events: [messageEvent2.eventId], - auth_events: [ - roomCreateEvent.eventId, - creatorMembershipEvent.eventId, - ], - depth: 5, - }, - roomVersion, - ); - - const powerLevelEvent = - await stateService.buildEvent<'m.room.power_levels'>( + const roomNameEvent = await stateService.buildEvent<'m.room.name'>( { - type: 'm.room.power_levels', room_id: roomCreateEvent.roomId, sender: username as room.UserID, + content: { name }, state_key: '', - content: { - users: { - [username]: 100, + type: 'm.room.name', + ...getDefaultFields(), + prev_events: [messageEvent.eventId], + auth_events: [ + creatorMembershipEvent.eventId, + roomCreateEvent.eventId, + ], + depth: 3, + }, + roomVersion, + ); + + const joinRuleEvent = + await stateService.buildEvent<'m.room.join_rules'>( + { + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + content: { join_rule: 'public' }, + type: 'm.room.join_rules', + state_key: '', + ...getDefaultFields(), + prev_events: [roomNameEvent.eventId], + auth_events: [ + roomCreateEvent.eventId, + creatorMembershipEvent.eventId, + ], + depth: 4, + }, + roomVersion, + ); + + const powerLevelEvent = + await stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + state_key: '', + content: { + users: { + [username]: 100, + }, + users_default: 0, + events: {}, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 50, }, - users_default: 0, - events: {}, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 50, + ...getDefaultFields(), + prev_events: [joinRuleEvent.eventId], + auth_events: [ + roomCreateEvent.eventId, + creatorMembershipEvent.eventId, + joinRuleEvent.eventId, + ], + depth: 5, }, + roomVersion, + ); + + const ourUserJoinEvent = await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: '@us:example.com' as room.UserID, + state_key: '@us:example.com' as room.UserID, + content: { membership: 'join' }, ...getDefaultFields(), - prev_events: [joinRuleEvent.eventId], + prev_events: [powerLevelEvent.eventId], auth_events: [ roomCreateEvent.eventId, - creatorMembershipEvent.eventId, + powerLevelEvent.eventId, joinRuleEvent.eventId, ], depth: 6, }, roomVersion, ); + const state = { + roomCreateEvent, + powerLevelEvent, + creatorMembershipEvent, + roomNameEvent, + ourUserJoinEvent, + joinRuleEvent, + }; - const ourUserJoinEvent = await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: '@us:example.com' as room.UserID, - state_key: '@us:example.com' as room.UserID, - content: { membership: 'join' }, - ...getDefaultFields(), - prev_events: [powerLevelEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - powerLevelEvent.eventId, - joinRuleEvent.eventId, - ], - depth: 7, - }, - roomVersion, - ); - - const state = { - roomCreateEvent, - powerLevelEvent, - creatorMembershipEvent, - roomNameEvent, - ourUserJoinEvent, - joinRuleEvent, - }; - - const map = new Map(); - const authChainSet = new Set(); - for (const event of Object.values(state)) { - map.set(event.eventId, event); - } - - const store = getStore(map); - - for (const event of Object.values(state)) { - for (const eventId of await room.getAuthChain(event, store)) { - authChainSet.add(eventId); + const map = new Map(); + const authChainSet = new Set(); + for (const event of Object.values(state)) { + map.set(event.eventId, event); } - } - const authChain = Array.from(authChainSet.values()).map( - (eid) => map.get(eid)!, - ); + const store = getStore(map); - return { - state, - authChain, + for (const event of Object.values(state)) { + for (const eventId of await room.getAuthChain(event, store)) { + authChainSet.add(eventId); + } + } - missingEvents: [messageEvent, messageEvent2] as PersistentEventBase[], - }; - }, - async () => { - const creator = '@alice:anotherserver.com' as room.UserID; - const name = 'Test Partial State Room'; + const authChain = Array.from(authChainSet.values()).map( + (eid) => map.get(eid)!, + ); - const roomCreateEvent = PersistentEventFactory.newCreateEvent( - creator as room.UserID, - PersistentEventFactory.defaultRoomVersion, - ); + return { + state, + authChain, + missingEvents: [messageEvent] as PersistentEventBase[], + }; + }, + // multiple missing in the middle + async () => { + const username = '@alice:anotherserver.com' as room.UserID; + const name = 'Test Partial State Room'; + + const roomCreateEvent = PersistentEventFactory.newCreateEvent( + username as room.UserID, + PersistentEventFactory.defaultRoomVersion, + ); - const roomVersion = roomCreateEvent.version; + const roomVersion = roomCreateEvent.version; + + const creatorMembershipEvent = + await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + state_key: username as room.UserID, + content: { membership: 'join' }, + ...getDefaultFields(), + prev_events: [roomCreateEvent.eventId], + auth_events: [roomCreateEvent.eventId], + depth: 1, + }, + roomVersion, + ); - const creatorMembershipEvent = - await stateService.buildEvent<'m.room.member'>( + // insert a random message event to make the tree incomplete + const messageEvent = await stateService.buildEvent<'m.room.message'>( { - type: 'm.room.member', room_id: roomCreateEvent.roomId, - sender: creator as room.UserID, - state_key: creator as room.UserID, - content: { membership: 'join' }, + sender: username, + content: { body: 'hello world', msgtype: 'm.text' }, + type: 'm.room.message', ...getDefaultFields(), - prev_events: [roomCreateEvent.eventId], - auth_events: [roomCreateEvent.eventId], - depth: 1, + prev_events: [creatorMembershipEvent.eventId], + auth_events: [ + creatorMembershipEvent.eventId, + roomCreateEvent.eventId, + ], + depth: 2, }, roomVersion, ); - // insert a random message event to make the tree incomplete - const messageEvent = await stateService.buildEvent<'m.room.message'>( - { - room_id: roomCreateEvent.roomId, - sender: creator, - content: { body: 'hello world', msgtype: 'm.text' }, - type: 'm.room.message', - ...getDefaultFields(), - prev_events: [creatorMembershipEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], - depth: 2, - }, - roomVersion, - ); + // this should become an extremity now since no known event will point to this + const roomNameEvent = await stateService.buildEvent<'m.room.name'>( + { + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + content: { name }, + state_key: '', + type: 'm.room.name', + ...getDefaultFields(), + prev_events: [messageEvent.eventId], + auth_events: [ + creatorMembershipEvent.eventId, + roomCreateEvent.eventId, + ], + depth: 3, + }, + roomVersion, + ); - // this should become an extremity now since no known event will point to this - const roomNameEvent = await stateService.buildEvent<'m.room.name'>( - { - room_id: roomCreateEvent.roomId, - sender: creator as room.UserID, - content: { name }, - state_key: '', - type: 'm.room.name', - ...getDefaultFields(), - prev_events: [messageEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], - depth: 3, - }, - roomVersion, - ); + // insert another random message event to make the tree incomplete + const messageEvent2 = await stateService.buildEvent<'m.room.message'>( + { + room_id: roomCreateEvent.roomId, + sender: username, + content: { body: 'hello world', msgtype: 'm.text' }, + type: 'm.room.message', + ...getDefaultFields(), + prev_events: [roomNameEvent.eventId], + auth_events: [ + creatorMembershipEvent.eventId, + roomCreateEvent.eventId, + ], + depth: 4, + }, + roomVersion, + ); - // insert another random message event to make the tree incomplete - const messageEvent2 = await stateService.buildEvent<'m.room.message'>( - { - room_id: roomCreateEvent.roomId, - sender: creator, - content: { body: 'hello world', msgtype: 'm.text' }, - type: 'm.room.message', - ...getDefaultFields(), - prev_events: [roomNameEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], - depth: 4, - }, - roomVersion, - ); + const joinRuleEvent = + await stateService.buildEvent<'m.room.join_rules'>( + { + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + content: { join_rule: 'public' }, + type: 'm.room.join_rules', + state_key: '', + ...getDefaultFields(), + prev_events: [messageEvent2.eventId], + auth_events: [ + roomCreateEvent.eventId, + creatorMembershipEvent.eventId, + ], + depth: 5, + }, + roomVersion, + ); - const joinRuleEvent = await stateService.buildEvent<'m.room.join_rules'>( - { - room_id: roomCreateEvent.roomId, - sender: creator as room.UserID, - content: { join_rule: 'invite' }, - type: 'm.room.join_rules', - state_key: '', - ...getDefaultFields(), - prev_events: [messageEvent2.eventId], - auth_events: [ - roomCreateEvent.eventId, - creatorMembershipEvent.eventId, - ], - depth: 5, - }, - roomVersion, - ); + const powerLevelEvent = + await stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + state_key: '', + content: { + users: { + [username]: 100, + }, + users_default: 0, + events: {}, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 50, + }, + ...getDefaultFields(), + prev_events: [joinRuleEvent.eventId], + auth_events: [ + roomCreateEvent.eventId, + creatorMembershipEvent.eventId, + joinRuleEvent.eventId, + ], + depth: 6, + }, + roomVersion, + ); - const powerLevelEvent = - await stateService.buildEvent<'m.room.power_levels'>( + const ourUserJoinEvent = await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: '@us:example.com' as room.UserID, + state_key: '@us:example.com' as room.UserID, + content: { membership: 'join' }, + ...getDefaultFields(), + prev_events: [powerLevelEvent.eventId], + auth_events: [ + roomCreateEvent.eventId, + powerLevelEvent.eventId, + joinRuleEvent.eventId, + ], + depth: 7, + }, + roomVersion, + ); + + const state = { + roomCreateEvent, + powerLevelEvent, + creatorMembershipEvent, + roomNameEvent, + ourUserJoinEvent, + joinRuleEvent, + }; + + const map = new Map(); + const authChainSet = new Set(); + for (const event of Object.values(state)) { + map.set(event.eventId, event); + } + + const store = getStore(map); + + for (const event of Object.values(state)) { + for (const eventId of await room.getAuthChain(event, store)) { + authChainSet.add(eventId); + } + } + + const authChain = Array.from(authChainSet.values()).map( + (eid) => map.get(eid)!, + ); + + return { + state, + authChain, + + missingEvents: [messageEvent, messageEvent2] as PersistentEventBase[], + }; + }, + async () => { + const creator = '@alice:anotherserver.com' as room.UserID; + const name = 'Test Partial State Room'; + + const roomCreateEvent = PersistentEventFactory.newCreateEvent( + creator as room.UserID, + PersistentEventFactory.defaultRoomVersion, + ); + + const roomVersion = roomCreateEvent.version; + + const creatorMembershipEvent = + await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: creator as room.UserID, + state_key: creator as room.UserID, + content: { membership: 'join' }, + ...getDefaultFields(), + prev_events: [roomCreateEvent.eventId], + auth_events: [roomCreateEvent.eventId], + depth: 1, + }, + roomVersion, + ); + + // insert a random message event to make the tree incomplete + const messageEvent = await stateService.buildEvent<'m.room.message'>( + { + room_id: roomCreateEvent.roomId, + sender: creator, + content: { body: 'hello world', msgtype: 'm.text' }, + type: 'm.room.message', + ...getDefaultFields(), + prev_events: [creatorMembershipEvent.eventId], + auth_events: [ + creatorMembershipEvent.eventId, + roomCreateEvent.eventId, + ], + depth: 2, + }, + roomVersion, + ); + + // this should become an extremity now since no known event will point to this + const roomNameEvent = await stateService.buildEvent<'m.room.name'>( { - type: 'm.room.power_levels', room_id: roomCreateEvent.roomId, sender: creator as room.UserID, + content: { name }, state_key: '', - content: { - users: { - [creator]: 100, + type: 'm.room.name', + ...getDefaultFields(), + prev_events: [messageEvent.eventId], + auth_events: [ + creatorMembershipEvent.eventId, + roomCreateEvent.eventId, + ], + depth: 3, + }, + roomVersion, + ); + + // insert another random message event to make the tree incomplete + const messageEvent2 = await stateService.buildEvent<'m.room.message'>( + { + room_id: roomCreateEvent.roomId, + sender: creator, + content: { body: 'hello world', msgtype: 'm.text' }, + type: 'm.room.message', + ...getDefaultFields(), + prev_events: [roomNameEvent.eventId], + auth_events: [ + creatorMembershipEvent.eventId, + roomCreateEvent.eventId, + ], + depth: 4, + }, + roomVersion, + ); + + const joinRuleEvent = + await stateService.buildEvent<'m.room.join_rules'>( + { + room_id: roomCreateEvent.roomId, + sender: creator as room.UserID, + content: { join_rule: 'invite' }, + type: 'm.room.join_rules', + state_key: '', + ...getDefaultFields(), + prev_events: [messageEvent2.eventId], + auth_events: [ + roomCreateEvent.eventId, + creatorMembershipEvent.eventId, + ], + depth: 5, + }, + roomVersion, + ); + + const powerLevelEvent = + await stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + room_id: roomCreateEvent.roomId, + sender: creator as room.UserID, + state_key: '', + content: { + users: { + [creator]: 100, + }, + users_default: 0, + events: {}, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 50, }, - users_default: 0, - events: {}, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 50, + ...getDefaultFields(), + prev_events: [joinRuleEvent.eventId], + auth_events: [ + roomCreateEvent.eventId, + creatorMembershipEvent.eventId, + joinRuleEvent.eventId, + ], + depth: 6, + }, + roomVersion, + ); + + const ourUserInviteEvent = + await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: creator, + state_key: '@us:example.com' as room.UserID, + content: { membership: 'invite' }, + ...getDefaultFields(), + prev_events: [powerLevelEvent.eventId], + auth_events: [ + roomCreateEvent.eventId, + powerLevelEvent.eventId, + joinRuleEvent.eventId, + creatorMembershipEvent.eventId, + ], + depth: 7, }, + roomVersion, + ); + + const ourUserJoinEvent = await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: '@us:example.com' as room.UserID, + state_key: '@us:example.com' as room.UserID, + content: { membership: 'join' }, ...getDefaultFields(), - prev_events: [joinRuleEvent.eventId], + prev_events: [ourUserInviteEvent.eventId], auth_events: [ roomCreateEvent.eventId, - creatorMembershipEvent.eventId, + powerLevelEvent.eventId, joinRuleEvent.eventId, + ourUserInviteEvent.eventId, ], - depth: 6, + depth: 8, }, roomVersion, ); - const ourUserInviteEvent = await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: creator, - state_key: '@us:example.com' as room.UserID, - content: { membership: 'invite' }, - ...getDefaultFields(), - prev_events: [powerLevelEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - powerLevelEvent.eventId, - joinRuleEvent.eventId, - creatorMembershipEvent.eventId, - ], - depth: 7, - }, - roomVersion, - ); + const state = { + roomCreateEvent, + powerLevelEvent, + creatorMembershipEvent, + roomNameEvent, + ourUserJoinEvent, + ourUserInviteEvent, + joinRuleEvent, + }; + + const map = new Map(); + const authChainSet = new Set(); + for (const event of Object.values(state)) { + map.set(event.eventId, event); + } + + const store = getStore(map); + + for (const event of Object.values(state)) { + for (const eventId of await room.getAuthChain(event, store)) { + authChainSet.add(eventId); + } + } + + const authChain = Array.from(authChainSet.values()).map( + (eid) => map.get(eid)!, + ); + + return { + state, + authChain, + + missingEvents: [messageEvent, messageEvent2] as PersistentEventBase[], + }; + }, + ]); - const ourUserJoinEvent = await stateService.buildEvent<'m.room.member'>( + const joinUser = async (roomId: string, userId: string) => { + return _setUserMembership(roomId, userId, 'join'); + }; + + const banUser = async (roomId: string, userId: string, sender: string) => { + return _setUserMembership(roomId, userId, 'ban', sender); + }; + + const leaveUser = async (roomId: string, userId: string) => { + return _setUserMembership(roomId, userId, 'leave'); + }; + + const inviteUser = async ( + roomId: string, + userId: string, + sender: string, + ) => { + return _setUserMembership(roomId, userId, 'invite', sender); + }; + + const _setUserMembership = async ( + roomId: string, + userId: string, + membership: room.PduMembershipEventContent['membership'], + sender?: string, + ) => { + const roomVersion = await stateService.getRoomVersion(roomId); + const membershipEvent = await stateService.buildEvent<'m.room.member'>( { type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: '@us:example.com' as room.UserID, - state_key: '@us:example.com' as room.UserID, - content: { membership: 'join' }, + room_id: roomId as room.RoomID, + sender: (sender || userId) as room.UserID, + state_key: userId as room.UserID, + content: { membership: membership }, ...getDefaultFields(), - prev_events: [ourUserInviteEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - powerLevelEvent.eventId, - joinRuleEvent.eventId, - ourUserInviteEvent.eventId, - ], - depth: 8, }, roomVersion, ); - const state = { + await stateService.handlePdu(membershipEvent); + + return membershipEvent; + }; + + it('001 should correctly calculate state through linear changes', async () => { + const { roomCreateEvent, - powerLevelEvent, - creatorMembershipEvent, roomNameEvent, - ourUserJoinEvent, - ourUserInviteEvent, joinRuleEvent, - }; + powerLevelEvent, + creatorMembershipEvent, + } = await createRoom('public'); - const map = new Map(); - const authChainSet = new Set(); - for (const event of Object.values(state)) { - map.set(event.eventId, event); - } + const stateAtEvent = new Map(); - const store = getStore(map); + const roomId = roomCreateEvent.roomId; + const creator = roomCreateEvent.getContent().creator as room.UserID; - for (const event of Object.values(state)) { - for (const eventId of await room.getAuthChain(event, store)) { - authChainSet.add(eventId); - } - } + const state = await stateService.getLatestRoomState(roomId); - const authChain = Array.from(authChainSet.values()).map( - (eid) => map.get(eid)!, - ); + // check each event + expect( + state.get(roomCreateEvent.getUniqueStateIdentifier()), + ).toHaveProperty('eventId', roomCreateEvent.eventId); + expect( + state.get(roomNameEvent.getUniqueStateIdentifier()), + ).toHaveProperty('eventId', roomNameEvent.eventId); + expect( + state.get(joinRuleEvent.getUniqueStateIdentifier()), + ).toHaveProperty('eventId', joinRuleEvent.eventId); + expect( + state.get(powerLevelEvent.getUniqueStateIdentifier()), + ).toHaveProperty('eventId', powerLevelEvent.eventId); + expect( + state.get(creatorMembershipEvent.getUniqueStateIdentifier()), + ).toHaveProperty('eventId', creatorMembershipEvent.eventId); - return { - state, - authChain, + expect(state.size).toBe(5); - missingEvents: [messageEvent, messageEvent2] as PersistentEventBase[], - }; - }, - ]); + const bob = '@bob:example.com'; + const bobJoinEvent = await joinUser(roomId, bob); - const joinUser = async (roomId: string, userId: string) => { - return _setUserMembership(roomId, userId, 'join'); - }; + const state2 = await stateService.getLatestRoomState(roomId); + expect(state2.size).toBe(6); + expect( + state2.get(bobJoinEvent.getUniqueStateIdentifier()), + ).toHaveProperty('eventId', bobJoinEvent.eventId); - const banUser = async (roomId: string, userId: string, sender: string) => { - return _setUserMembership(roomId, userId, 'ban', sender); - }; + stateAtEvent.set(bobJoinEvent.eventId, state2); - const leaveUser = async (roomId: string, userId: string) => { - return _setUserMembership(roomId, userId, 'leave'); - }; + const bobLeaveEvent = await leaveUser(roomId, bob); + const state3 = await stateService.getLatestRoomState(roomId); - const inviteUser = async (roomId: string, userId: string, sender: string) => { - return _setUserMembership(roomId, userId, 'invite', sender); - }; + expect(state3.size).toBe(6); // same as before + expect( + state3.get(bobLeaveEvent.getUniqueStateIdentifier()), + ).toHaveProperty('eventId', bobLeaveEvent.eventId); - const _setUserMembership = async ( - roomId: string, - userId: string, - membership: room.PduMembershipEventContent['membership'], - sender?: string, - ) => { - const roomVersion = await stateService.getRoomVersion(roomId); - const membershipEvent = await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomId as room.RoomID, - sender: (sender || userId) as room.UserID, - state_key: userId as room.UserID, - content: { membership: membership }, - ...getDefaultFields(), - }, - roomVersion, - ); + stateAtEvent.set(bobLeaveEvent.eventId, state3); - await stateService.handlePdu(membershipEvent); + // do same for random1 and random2 + const random1 = '@random1:example.com'; + const random1JoinEvent = await joinUser(roomId, random1); + const state4 = await stateService.getLatestRoomState(roomId); + expect(state4.size).toBe(7); + expect( + state4.get(random1JoinEvent.getUniqueStateIdentifier()), + ).toHaveProperty('eventId', random1JoinEvent.eventId); + const random2 = '@random2:example.com'; + const random2JoinEvent = await joinUser(roomId, random2); + const state5 = await stateService.getLatestRoomState(roomId); + expect(state5.size).toBe(8); + expect( + state5.get(random2JoinEvent.getUniqueStateIdentifier()), + ).toHaveProperty('eventId', random2JoinEvent.eventId); - return membershipEvent; - }; + stateAtEvent.set(random1JoinEvent.eventId, state4); + stateAtEvent.set(random2JoinEvent.eventId, state5); - it('001 should correctly calculate state through linear changes', async () => { - const { - roomCreateEvent, - roomNameEvent, - joinRuleEvent, - powerLevelEvent, - creatorMembershipEvent, - } = await createRoom('public'); - - const stateAtEvent = new Map(); - - const roomId = roomCreateEvent.roomId; - const creator = roomCreateEvent.getContent().creator as room.UserID; - - const state = await stateService.getLatestRoomState(roomId); - - // check each event - expect( - state.get(roomCreateEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', roomCreateEvent.eventId); - expect(state.get(roomNameEvent.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - roomNameEvent.eventId, - ); - expect(state.get(joinRuleEvent.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - joinRuleEvent.eventId, - ); - expect( - state.get(powerLevelEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', powerLevelEvent.eventId); - expect( - state.get(creatorMembershipEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', creatorMembershipEvent.eventId); - - expect(state.size).toBe(5); - - const bob = '@bob:example.com'; - const bobJoinEvent = await joinUser(roomId, bob); - - const state2 = await stateService.getLatestRoomState(roomId); - expect(state2.size).toBe(6); - expect(state2.get(bobJoinEvent.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - bobJoinEvent.eventId, - ); + // change room name now + const newRoomName = 'New Room Name'; + const roomNameEvent2 = await stateService.buildEvent<'m.room.name'>( + { + room_id: roomId, + sender: roomCreateEvent.getContent() + .creator as room.UserID, + content: { name: newRoomName }, + state_key: '', + type: 'm.room.name', + ...getDefaultFields(), + }, + roomCreateEvent.getContent().room_version, + ); + await stateService.handlePdu(roomNameEvent2); + const state6 = await stateService.getLatestRoomState(roomId); + expect(state6.size).toBe(8); // same as before, overwriting existing name + expect( + state6.get(roomNameEvent2.getUniqueStateIdentifier()), + ).toHaveProperty('eventId', roomNameEvent2.eventId); - stateAtEvent.set(bobJoinEvent.eventId, state2); + stateAtEvent.set(roomNameEvent2.eventId, state6); - const bobLeaveEvent = await leaveUser(roomId, bob); - const state3 = await stateService.getLatestRoomState(roomId); + // ban random3 + const random3 = '@random3:example.com'; + const banRandom1Event = await banUser(roomId, random3, creator); + const state7 = await stateService.getLatestRoomState(roomId); + expect(state7.size).toBe(9); + expect( + state7.get(banRandom1Event.getUniqueStateIdentifier()), + ).toHaveProperty('eventId', banRandom1Event.eventId); - expect(state3.size).toBe(6); // same as before - expect(state3.get(bobLeaveEvent.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - bobLeaveEvent.eventId, - ); + stateAtEvent.set(banRandom1Event.eventId, state7); - stateAtEvent.set(bobLeaveEvent.eventId, state3); - - // do same for random1 and random2 - const random1 = '@random1:example.com'; - const random1JoinEvent = await joinUser(roomId, random1); - const state4 = await stateService.getLatestRoomState(roomId); - expect(state4.size).toBe(7); - expect( - state4.get(random1JoinEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', random1JoinEvent.eventId); - const random2 = '@random2:example.com'; - const random2JoinEvent = await joinUser(roomId, random2); - const state5 = await stateService.getLatestRoomState(roomId); - expect(state5.size).toBe(8); - expect( - state5.get(random2JoinEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', random2JoinEvent.eventId); - - stateAtEvent.set(random1JoinEvent.eventId, state4); - stateAtEvent.set(random2JoinEvent.eventId, state5); - - // change room name now - const newRoomName = 'New Room Name'; - const roomNameEvent2 = await stateService.buildEvent<'m.room.name'>( - { - room_id: roomId, - sender: roomCreateEvent.getContent() - .creator as room.UserID, - content: { name: newRoomName }, - state_key: '', - type: 'm.room.name', - ...getDefaultFields(), - }, - roomCreateEvent.getContent().room_version, - ); - await stateService.handlePdu(roomNameEvent2); - const state6 = await stateService.getLatestRoomState(roomId); - expect(state6.size).toBe(8); // same as before, overwriting existing name - expect( - state6.get(roomNameEvent2.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', roomNameEvent2.eventId); - - stateAtEvent.set(roomNameEvent2.eventId, state6); - - // ban random3 - const random3 = '@random3:example.com'; - const banRandom1Event = await banUser(roomId, random3, creator); - const state7 = await stateService.getLatestRoomState(roomId); - expect(state7.size).toBe(9); - expect( - state7.get(banRandom1Event.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', banRandom1Event.eventId); - - stateAtEvent.set(banRandom1Event.eventId, state7); - - // have random3 change the name of the room - const roomNameEvent3 = await stateService.buildEvent<'m.room.name'>( - { - room_id: roomId, - sender: random3 as room.UserID, - content: { name: 'Hacked Name' }, - state_key: '', - type: 'm.room.name', - ...getDefaultFields(), - }, - roomCreateEvent.getContent().room_version, - ); - expect(stateService.handlePdu(roomNameEvent3)).rejects.toThrow(); - const state8 = await stateService.getLatestRoomState(roomId); - expect(state8.size).toBe(9); // same as before, bob was banned can't change name - compareStates(state7, state8); + // have random3 change the name of the room + const roomNameEvent3 = await stateService.buildEvent<'m.room.name'>( + { + room_id: roomId, + sender: random3 as room.UserID, + content: { name: 'Hacked Name' }, + state_key: '', + type: 'm.room.name', + ...getDefaultFields(), + }, + roomCreateEvent.getContent().room_version, + ); + expect(stateService.handlePdu(roomNameEvent3)).rejects.toThrow(); + const state8 = await stateService.getLatestRoomState(roomId); + expect(state8.size).toBe(9); // same as before, bob was banned can't change name + compareStates(state7, state8); - const stateShouldBe6 = await stateService.getStateAtEvent(roomNameEvent2); - compareStates(stateAtEvent.get(roomNameEvent2.eventId)!, stateShouldBe6); + const stateShouldBe6 = await stateService.getStateAtEvent(roomNameEvent2); + compareStates(stateAtEvent.get(roomNameEvent2.eventId)!, stateShouldBe6); - // send a message - const message = await stateService.buildEvent<'m.room.message'>( - { - type: 'm.room.message', - room_id: roomId, - sender: creator, - content: { msgtype: 'm.text', body: '' }, - ...getDefaultFields(), - }, - roomCreateEvent.version, - ); + // send a message + const message = await stateService.buildEvent<'m.room.message'>( + { + type: 'm.room.message', + room_id: roomId, + sender: creator, + content: { msgtype: 'm.text', body: '' }, + ...getDefaultFields(), + }, + roomCreateEvent.version, + ); - await stateService.handlePdu(message); + await stateService.handlePdu(message); - const state9 = await stateService.getLatestRoomState(roomId); - compareStates(state9, state8); // shouldn't change state + const state9 = await stateService.getLatestRoomState(roomId); + compareStates(state9, state8); // shouldn't change state - const stateAtMessage = await stateService.getStateAtEvent(message); - compareStates(stateAtMessage, state9); - }); + const stateAtMessage = await stateService.getStateAtEvent(message); + compareStates(stateAtMessage, state9); + }); - it('01 should return the correct room information for room id', async () => { - expect(stateService.getRoomInformation('abcd')).rejects.toThrowError( - /Create event mapping not found/, - ); + it('01 should return the correct room information for room id', async () => { + expect(stateService.getRoomInformation('abcd')).rejects.toThrowError( + /Create event mapping not found/, + ); - const { roomCreateEvent } = await createRoom('public'); + const { roomCreateEvent } = await createRoom('public'); - expect( - stateService.getRoomInformation(roomCreateEvent.roomId), - ).resolves.toHaveProperty( - 'creator', - roomCreateEvent.getContent().creator, - ); - }); + expect( + stateService.getRoomInformation(roomCreateEvent.roomId), + ).resolves.toHaveProperty( + 'creator', + roomCreateEvent.getContent().creator, + ); + }); - it('02 should get the correct room version', async () => { - const { roomCreateEvent } = await createRoom('public'); + it('02 should get the correct room version', async () => { + const { roomCreateEvent } = await createRoom('public'); - const roomVersion = await stateService.getRoomVersion( - roomCreateEvent.roomId, - ); + const roomVersion = await stateService.getRoomVersion( + roomCreateEvent.roomId, + ); - expect(roomVersion).toBe( - roomCreateEvent.getContent() - .room_version as RoomVersion, - ); + expect(roomVersion).toBe( + roomCreateEvent.getContent() + .room_version as RoomVersion, + ); - expect(stateService.getRoomVersion('roomId')).rejects.toThrowError(); - }); + expect(stateService.getRoomVersion('roomId')).rejects.toThrowError(); + }); - it('03 should find the correct state at an event', async () => { - const { roomCreateEvent } = await createRoom('public'); + it('03 should find the correct state at an event', async () => { + const { roomCreateEvent } = await createRoom('public'); - const events = await Promise.all([ - joinUser(roomCreateEvent.roomId, '@bob:example.com'), - joinUser(roomCreateEvent.roomId, '@charlie:example.com'), - // random1 - joinUser(roomCreateEvent.roomId, '@random1:example.com'), - joinUser(roomCreateEvent.roomId, '@random2:example.com'), - ]); + const events = await Promise.all([ + joinUser(roomCreateEvent.roomId, '@bob:example.com'), + joinUser(roomCreateEvent.roomId, '@charlie:example.com'), + // random1 + joinUser(roomCreateEvent.roomId, '@random1:example.com'), + joinUser(roomCreateEvent.roomId, '@random2:example.com'), + ]); - // at each event the corresponding user should be in state + // at each event the corresponding user should be in state - for (const event of events) { - const stateAtEvent = await stateService.getStateAtEvent(event); - expect( - stateAtEvent.get(event.getUniqueStateIdentifier())?.getContent(), - ).toHaveProperty('membership', 'join'); - } - }); + for (const event of events) { + const stateAtEvent = await stateService.getStateAtEvent(event); + expect( + stateAtEvent.get(event.getUniqueStateIdentifier())?.getContent(), + ).toHaveProperty('membership', 'join'); + } + }); - // NOTE: need state_id implementation and correlate with synapse to confirm the behavior - // challenge is if we get a duplicate and drop it from state, but is still part of prev_events as new events come in, how are we supposed to behave then? - // at the same time if we send out a duplicate, it will be dropped by the other side, but we will continue to - // add it in prev_events. - // idempotency both ways is important, at the same time need to be able to handle non idempotent requests - test.failing('should make idempotent state changes', async () => { - const { roomCreateEvent } = await createRoom('public'); + // NOTE: need state_id implementation and correlate with synapse to confirm the behavior + // challenge is if we get a duplicate and drop it from state, but is still part of prev_events as new events come in, how are we supposed to behave then? + // at the same time if we send out a duplicate, it will be dropped by the other side, but we will continue to + // add it in prev_events. + // idempotency both ways is important, at the same time need to be able to handle non idempotent requests + test.failing('should make idempotent state changes', async () => { + const { roomCreateEvent } = await createRoom('public'); - const newUser = '@bob:example.com'; + const newUser = '@bob:example.com'; - const joinEvent1 = await joinUser(roomCreateEvent.roomId, newUser); + const joinEvent1 = await joinUser(roomCreateEvent.roomId, newUser); - const state1 = await stateService.getLatestRoomState( - roomCreateEvent.roomId, - ); - expect(state1.get(joinEvent1.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - joinEvent1.eventId, - ); + const state1 = await stateService.getLatestRoomState( + roomCreateEvent.roomId, + ); + expect(state1.get(joinEvent1.getUniqueStateIdentifier())).toHaveProperty( + 'eventId', + joinEvent1.eventId, + ); - const joinEvent2 = await joinUser(roomCreateEvent.roomId, newUser); + const joinEvent2 = await joinUser(roomCreateEvent.roomId, newUser); - expect(joinEvent1.eventId).not.toBe(joinEvent2.eventId); + expect(joinEvent1.eventId).not.toBe(joinEvent2.eventId); - const state2 = await stateService.getLatestRoomState( - roomCreateEvent.roomId, - ); - expect(state2.get(joinEvent2.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - joinEvent1.eventId, // same as old eventid - ); - }); + const state2 = await stateService.getLatestRoomState( + roomCreateEvent.roomId, + ); + expect(state2.get(joinEvent2.getUniqueStateIdentifier())).toHaveProperty( + 'eventId', + joinEvent1.eventId, // same as old eventid + ); + }); - it('05 should create a room successfully', async () => { - const { - roomCreateEvent: { roomId }, - } = await createRoom('public'); - expect(roomId).toBeDefined(); - return expect( - stateService.getLatestRoomState2(roomId), - ).resolves.toBeDefined(); - }); + it('05 should create a room successfully', async () => { + const { + roomCreateEvent: { roomId }, + } = await createRoom('public'); + expect(roomId).toBeDefined(); + return expect( + stateService.getLatestRoomState2(roomId), + ).resolves.toBeDefined(); + }); - it('06 should successfully have a user join the room', async () => { - const { roomCreateEvent } = await createRoom('public'); + it('06 should successfully have a user join the room', async () => { + const { roomCreateEvent } = await createRoom('public'); - const newUser = '@bob:example.com'; + const newUser = '@bob:example.com'; - await joinUser(roomCreateEvent.roomId, newUser); + await joinUser(roomCreateEvent.roomId, newUser); - const state = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); - expect(state.isUserInRoom(newUser)).toBe(true); - }); + const state = await stateService.getLatestRoomState2( + roomCreateEvent.roomId, + ); + expect(state.isUserInRoom(newUser)).toBe(true); + }); - it('07 should have a user leave the room successfully', async () => { - const { roomCreateEvent } = await createRoom('public'); - const newUser = '@bob:example.com'; + it('07 should have a user leave the room successfully', async () => { + const { roomCreateEvent } = await createRoom('public'); + const newUser = '@bob:example.com'; - await joinUser(roomCreateEvent.roomId, newUser); + await joinUser(roomCreateEvent.roomId, newUser); - await leaveUser(roomCreateEvent.roomId, newUser); + await leaveUser(roomCreateEvent.roomId, newUser); - const state = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); - expect(state.getUserMembership(newUser)).toBe('leave'); - }); + const state = await stateService.getLatestRoomState2( + roomCreateEvent.roomId, + ); + expect(state.getUserMembership(newUser)).toBe('leave'); + }); - it('08 should not allow joining if room is imenvite only', async () => { - const { roomCreateEvent } = await createRoom('invite'); - const newUser = '@bob:example.com' as room.UserID; - const membershipEvent = await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: newUser, - state_key: newUser, - content: { membership: 'join' }, - ...getDefaultFields(), - }, - roomCreateEvent.getContent().room_version, - ); + it('08 should not allow joining if room is imenvite only', async () => { + const { roomCreateEvent } = await createRoom('invite'); + const newUser = '@bob:example.com' as room.UserID; + const membershipEvent = await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: newUser, + state_key: newUser, + content: { membership: 'join' }, + ...getDefaultFields(), + }, + roomCreateEvent.getContent().room_version, + ); - expect(stateService.handlePdu(membershipEvent)).rejects.toThrow(); + expect(stateService.handlePdu(membershipEvent)).rejects.toThrow(); - expect(membershipEvent.rejected).toBeTrue(); - expect(membershipEvent.rejectCode).toBe(RejectCodes.AuthError); - }); + expect(membershipEvent.rejected).toBeTrue(); + expect(membershipEvent.rejectCode).toBe(RejectCodes.AuthError); + }); - it('09 should allow joining if invited in invite only room', async () => { - const { roomCreateEvent } = await createRoom('invite'); - const newUser = '@bob:example.com' as room.UserID; + it('09 should allow joining if invited in invite only room', async () => { + const { roomCreateEvent } = await createRoom('invite'); + const newUser = '@bob:example.com' as room.UserID; - await inviteUser( - roomCreateEvent.roomId, - newUser, - roomCreateEvent.getContent().creator, - ); + await inviteUser( + roomCreateEvent.roomId, + newUser, + roomCreateEvent.getContent().creator, + ); - expect( - ( - await stateService.getLatestRoomState2(roomCreateEvent.roomId) - ).isUserInvited(newUser), - ).toBeTrue(); + expect( + ( + await stateService.getLatestRoomState2(roomCreateEvent.roomId) + ).isUserInvited(newUser), + ).toBeTrue(); - await joinUser(roomCreateEvent.roomId, newUser); + await joinUser(roomCreateEvent.roomId, newUser); - const state = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); - expect(state.isUserInRoom(newUser)).toBe(true); - }); + const state = await stateService.getLatestRoomState2( + roomCreateEvent.roomId, + ); + expect(state.isUserInRoom(newUser)).toBe(true); + }); - it('10 should not allow joining if banned', async () => { - const { roomCreateEvent } = await createRoom('public'); - const newUser = '@bob:example.com' as room.UserID; - // join first - await joinUser(roomCreateEvent.roomId, newUser); - - expect( - ( - await stateService.getLatestRoomState2(roomCreateEvent.roomId) - ).isUserInRoom(newUser), - ).toBeTrue(); - - await banUser( - roomCreateEvent.roomId, - newUser, - roomCreateEvent.getContent().creator, - ); + it('10 should not allow joining if banned', async () => { + const { roomCreateEvent } = await createRoom('public'); + const newUser = '@bob:example.com' as room.UserID; + // join first + await joinUser(roomCreateEvent.roomId, newUser); - expect( - ( - await stateService.getLatestRoomState2(roomCreateEvent.roomId) - ).getUserMembership(newUser), - ).toBe('ban'); + expect( + ( + await stateService.getLatestRoomState2(roomCreateEvent.roomId) + ).isUserInRoom(newUser), + ).toBeTrue(); + + await banUser( + roomCreateEvent.roomId, + newUser, + roomCreateEvent.getContent().creator, + ); - const membershipEventJoin2 = await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: newUser, - state_key: newUser, - content: { membership: 'join' }, - ...getDefaultFields(), - }, - roomCreateEvent.getContent().room_version, - ); + expect( + ( + await stateService.getLatestRoomState2(roomCreateEvent.roomId) + ).getUserMembership(newUser), + ).toBe('ban'); - expect(stateService.handlePdu(membershipEventJoin2)).rejects.toThrow(); - expect(membershipEventJoin2.rejected).toBeTrue(); - expect(membershipEventJoin2.rejectCode).toBe(RejectCodes.AuthError); - }); + const membershipEventJoin2 = + await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: newUser, + state_key: newUser, + content: { membership: 'join' }, + ...getDefaultFields(), + }, + roomCreateEvent.getContent().room_version, + ); - it('11 should soft fail events', async () => { - const { roomCreateEvent } = await createRoom('public'); - - // add a user - const bob = '@bob:example.com' as room.UserID; - await joinUser(roomCreateEvent.roomId, bob); - // ban bob now - const banBobEvent = await banUser( - roomCreateEvent.roomId, - bob, - roomCreateEvent.getContent().creator, - ); + expect(stateService.handlePdu(membershipEventJoin2)).rejects.toThrow(); + expect(membershipEventJoin2.rejected).toBeTrue(); + expect(membershipEventJoin2.rejectCode).toBe(RejectCodes.AuthError); + }); - const state1 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); - expect(state1.getUserMembership(bob)).toBe('ban'); + it('11 should soft fail events', async () => { + const { roomCreateEvent } = await createRoom('public'); + + // add a user + const bob = '@bob:example.com' as room.UserID; + await joinUser(roomCreateEvent.roomId, bob); + // ban bob now + const banBobEvent = await banUser( + roomCreateEvent.roomId, + bob, + roomCreateEvent.getContent().creator, + ); - // now we try to make bob "leave", but set the depth manually to be before he was banned - // leave is a state event - const bobLeaveEvent = stripPreviousAndAuthEvents( - await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: bob, - state_key: bob, - content: { membership: 'leave' }, - ...getDefaultFields(), - }, - roomCreateEvent.getContent().room_version, - ), - ); + const state1 = await stateService.getLatestRoomState2( + roomCreateEvent.roomId, + ); + expect(state1.getUserMembership(bob)).toBe('ban'); - const store = stateService._getStore( - roomCreateEvent.getContent() - .room_version as RoomVersion, - ); + // now we try to make bob "leave", but set the depth manually to be before he was banned + // leave is a state event + const bobLeaveEvent = stripPreviousAndAuthEvents( + await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: bob, + state_key: bob, + content: { membership: 'leave' }, + ...getDefaultFields(), + }, + roomCreateEvent.getContent().room_version, + ), + ); - const eventsBeforeBobWasBanned = await store.getEvents( - banBobEvent.getPreviousEventIds(), - ); + const store = stateService._getStore( + roomCreateEvent.getContent() + .room_version as RoomVersion, + ); + + const eventsBeforeBobWasBanned = await store.getEvents( + banBobEvent.getPreviousEventIds(), + ); - const authEventsForBobBan = await store.getEvents( - banBobEvent.getAuthEventIds(), - ); // should be the same for bob + const authEventsForBobBan = await store.getEvents( + banBobEvent.getAuthEventIds(), + ); // should be the same for bob - bobLeaveEvent.addPrevEvents(eventsBeforeBobWasBanned); + bobLeaveEvent.addPrevEvents(eventsBeforeBobWasBanned); - // biome-ignore lint/complexity/noForEach: - authEventsForBobBan.forEach((e) => bobLeaveEvent.authedBy(e)); + // biome-ignore lint/complexity/noForEach: + authEventsForBobBan.forEach((e) => bobLeaveEvent.authedBy(e)); - expect(stateService.handlePdu(bobLeaveEvent)).rejects.toThrow(); - expect(bobLeaveEvent.rejected).toBeTrue(); - expect(bobLeaveEvent.rejectCode).toBe(RejectCodes.AuthError); - }); + expect(stateService.handlePdu(bobLeaveEvent)).rejects.toThrow(); + expect(bobLeaveEvent.rejected).toBeTrue(); + expect(bobLeaveEvent.rejectCode).toBe(RejectCodes.AuthError); + }); + + it('01#arriving_late should fix state in case of older event arriving late', async () => { + const { roomCreateEvent, powerLevelEvent, roomNameEvent } = + await createRoom('public'); - it('01#arriving_late should fix state in case of older event arriving late', async () => { - const { roomCreateEvent, powerLevelEvent, roomNameEvent } = - await createRoom('public'); + const roomId = roomCreateEvent.roomId; - const roomId = roomCreateEvent.roomId; + // add a user + const bob = '@bob:example.com' as room.UserID; + await joinUser(roomCreateEvent.roomId, bob); - // add a user - const bob = '@bob:example.com' as room.UserID; - await joinUser(roomCreateEvent.roomId, bob); + const powerLevelContent = structuredClone(powerLevelEvent.getContent()); - const powerLevelContent = structuredClone(powerLevelEvent.getContent()); + // we increase bob to 50 allowing room name change + + powerLevelContent.users[bob] = 50; + + const newPowerLevelEvent = + await stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + room_id: roomCreateEvent.roomId, + sender: roomCreateEvent.getContent() + .creator as room.UserID, + state_key: '', + content: powerLevelContent, + ...getDefaultFields(), + }, + PersistentEventFactory.defaultRoomVersion, + ); - // we increase bob to 50 allowing room name change + await stateService.handlePdu(newPowerLevelEvent); - powerLevelContent.users[bob] = 50; + const state1 = await stateService.getLatestRoomState2( + roomCreateEvent.roomId, + ); + expect(state1.powerLevels?.users[bob]).toBe(50); - const newPowerLevelEvent = - await stateService.buildEvent<'m.room.power_levels'>( + // now we make bob change the room name, this should work + const newRoomName = 'New Room Name'; + const roomNameEventByBob = await stateService.buildEvent<'m.room.name'>( { - type: 'm.room.power_levels', room_id: roomCreateEvent.roomId, - sender: roomCreateEvent.getContent() - .creator as room.UserID, + sender: bob, + content: { name: newRoomName }, state_key: '', - content: powerLevelContent, + type: 'm.room.name', ...getDefaultFields(), }, PersistentEventFactory.defaultRoomVersion, ); - await stateService.handlePdu(newPowerLevelEvent); + await stateService.handlePdu(roomNameEventByBob); - const state1 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); - expect(state1.powerLevels?.users[bob]).toBe(50); - - // now we make bob change the room name, this should work - const newRoomName = 'New Room Name'; - const roomNameEventByBob = await stateService.buildEvent<'m.room.name'>( - { - room_id: roomCreateEvent.roomId, - sender: bob, - content: { name: newRoomName }, - state_key: '', - type: 'm.room.name', - ...getDefaultFields(), - }, - PersistentEventFactory.defaultRoomVersion, - ); - - await stateService.handlePdu(roomNameEventByBob); + expect((await stateService.getLatestRoomState2(roomId)).name).toBe( + newRoomName, + ); - expect((await stateService.getLatestRoomState2(roomId)).name).toBe( - newRoomName, - ); + // add another delta so both events point to the same state + const state2 = await stateService.getLatestRoomState2( + roomCreateEvent.roomId, + ); + expect(state2.name).toBe(newRoomName); - // add another delta so both events point to the same state - const state2 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); - expect(state2.name).toBe(newRoomName); + // we now mimick sending a ban event for bob, but before the power level event was sent + const banBobEvent = stripPreviousAndAuthEvents( + await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: roomCreateEvent.getContent() + .creator as room.UserID, + state_key: bob, + content: { membership: 'ban' }, + ...getDefaultFields(), + }, + roomCreateEvent.getContent() + .room_version as RoomVersion, + ), + ); - // we now mimick sending a ban event for bob, but before the power level event was sent - const banBobEvent = stripPreviousAndAuthEvents( - await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: roomCreateEvent.getContent() - .creator as room.UserID, - state_key: bob, - content: { membership: 'ban' }, - ...getDefaultFields(), - }, + const store = stateService._getStore( roomCreateEvent.getContent() .room_version as RoomVersion, - ), - ); - - const store = stateService._getStore( - roomCreateEvent.getContent() - .room_version as RoomVersion, - ); + ); - const eventsBeforePowerLevel = await store.getEvents( - newPowerLevelEvent.getPreviousEventIds(), - ); + const eventsBeforePowerLevel = await store.getEvents( + newPowerLevelEvent.getPreviousEventIds(), + ); - banBobEvent.addPrevEvents(eventsBeforePowerLevel); + banBobEvent.addPrevEvents(eventsBeforePowerLevel); - const stateBeforePowerLevelEvent = - await stateService.getStateBeforeEvent(powerLevelEvent); + const stateBeforePowerLevelEvent = + await stateService.getStateBeforeEvent(powerLevelEvent); - for (const requiredAuthEvent of banBobEvent.getAuthEventStateKeys()) { - const authEvent = stateBeforePowerLevelEvent.get(requiredAuthEvent); - if (authEvent) { - banBobEvent.authedBy(authEvent); + for (const requiredAuthEvent of banBobEvent.getAuthEventStateKeys()) { + const authEvent = stateBeforePowerLevelEvent.get(requiredAuthEvent); + if (authEvent) { + banBobEvent.authedBy(authEvent); + } } - } - await stateService.handlePdu(banBobEvent); + await stateService.handlePdu(banBobEvent); - const state3 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state3 = await stateService.getLatestRoomState2( + roomCreateEvent.roomId, + ); - // console.log((stte3 as any).stateMap); + // console.log((stte3 as any).stateMap); - expect(state3.name).toBe( - roomNameEvent.getContent().name, - ); // should set the state to right versions - }); + expect(state3.name).toBe( + roomNameEvent.getContent().name, + ); // should set the state to right versions + }); - it('02#arriving_late should fix state in case of older event arriving late', async () => { - const { roomCreateEvent } = await createRoom('public'); + it('02#arriving_late should fix state in case of older event arriving late', async () => { + const { roomCreateEvent } = await createRoom('public'); - // add a user - const bob = '@bob:example.com' as room.UserID; - await joinUser(roomCreateEvent.roomId, bob); + // add a user + const bob = '@bob:example.com' as room.UserID; + await joinUser(roomCreateEvent.roomId, bob); - const diego = '@diego:example.com'; - await joinUser(roomCreateEvent.roomId, diego); + const diego = '@diego:example.com'; + await joinUser(roomCreateEvent.roomId, diego); - const joinRuleInvite = stripPreviousAndAuthEvents( - await stateService.buildEvent<'m.room.join_rules'>( - { - room_id: roomCreateEvent.roomId, - sender: roomCreateEvent.getContent() - .creator as room.UserID, - content: { join_rule: 'invite' }, - type: 'm.room.join_rules', - state_key: '', - ...getDefaultFields(), - }, - PersistentEventFactory.defaultRoomVersion, - ), - ); + const joinRuleInvite = stripPreviousAndAuthEvents( + await stateService.buildEvent<'m.room.join_rules'>( + { + room_id: roomCreateEvent.roomId, + sender: roomCreateEvent.getContent() + .creator as room.UserID, + content: { join_rule: 'invite' }, + type: 'm.room.join_rules', + state_key: '', + ...getDefaultFields(), + }, + PersistentEventFactory.defaultRoomVersion, + ), + ); - // we will NOT fill or send the event yet. + // we will NOT fill or send the event yet. - const randomUser1 = '@random1:example.com'; - const randomUserJoinEvent = await joinUser( - roomCreateEvent.roomId, - randomUser1, - ); + const randomUser1 = '@random1:example.com'; + const randomUserJoinEvent = await joinUser( + roomCreateEvent.roomId, + randomUser1, + ); - const randomUser2 = '@random2:example.com'; - await joinUser(roomCreateEvent.roomId, randomUser2); + const randomUser2 = '@random2:example.com'; + await joinUser(roomCreateEvent.roomId, randomUser2); - const state1 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state1 = await stateService.getLatestRoomState2( + roomCreateEvent.roomId, + ); - expect(state1.isUserInRoom(randomUser1)).toBe(true); - expect(state1.isUserInRoom(randomUser2)).toBe(true); + expect(state1.isUserInRoom(randomUser1)).toBe(true); + expect(state1.isUserInRoom(randomUser2)).toBe(true); - // join rule was changed before randomuser joined - const store = stateService._getStore(state1.version); + // join rule was changed before randomuser joined + const store = stateService._getStore(state1.version); - const previousEventIdsForJoinRule = - randomUserJoinEvent.getPreviousEventIds(); - const previousEventsForJoinRule = await store.getEvents( - previousEventIdsForJoinRule, - ); - joinRuleInvite.addPrevEvents(previousEventsForJoinRule); - - // while the join doesn't affect the auth events for joinrule, still doing it this way as an example for the correct way - const stateBeforeRandomUserJoin = - await stateService.getStateBeforeEvent(randomUserJoinEvent); - for (const requiredAuthEvent of joinRuleInvite.getAuthEventStateKeys()) { - const authEvent = stateBeforeRandomUserJoin.get(requiredAuthEvent); - if (authEvent) { - joinRuleInvite.authedBy(authEvent); + const previousEventIdsForJoinRule = + randomUserJoinEvent.getPreviousEventIds(); + const previousEventsForJoinRule = await store.getEvents( + previousEventIdsForJoinRule, + ); + joinRuleInvite.addPrevEvents(previousEventsForJoinRule); + + // while the join doesn't affect the auth events for joinrule, still doing it this way as an example for the correct way + const stateBeforeRandomUserJoin = + await stateService.getStateBeforeEvent(randomUserJoinEvent); + for (const requiredAuthEvent of joinRuleInvite.getAuthEventStateKeys()) { + const authEvent = stateBeforeRandomUserJoin.get(requiredAuthEvent); + if (authEvent) { + joinRuleInvite.authedBy(authEvent); + } } - } - await stateService.handlePdu(joinRuleInvite); + await stateService.handlePdu(joinRuleInvite); - // const _state2 = await stateService.findStateAtEvent(joinRuleInvite.eventId); + // const _state2 = await stateService.findStateAtEvent(joinRuleInvite.eventId); - // const state2 = new RoomState(_state2); + // const state2 = new RoomState(_state2); - // console.log('state', [..._state2.entries()]); + // console.log('state', [..._state2.entries()]); - const state2 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state2 = await stateService.getLatestRoomState2( + roomCreateEvent.roomId, + ); - expect(state2.isUserInRoom(randomUser1)).toBe(false); - expect(state2.isUserInRoom(randomUser2)).toBe(false); - }); + expect(state2.isUserInRoom(randomUser1)).toBe(false); + expect(state2.isUserInRoom(randomUser2)).toBe(false); + }); - // it('should minimize amount of required state resolutions', async () => { - // const spy = spyOn(room, 'resolveStateV2Plus'); - // // once an old event gets to us, we run state res. - // // assume the state res stored new deltas randomly, would cause forward extremeties to behave the same way. - // // the next new event we will receive, should be pointing to the latest state, for that to happenm - // // we must make sure we associate latest state with the latest event we could have at that time. - // // --- - // // create a room - // // do stuff - // // send an out of order event - // // now send a normal event - // // see if it triggered stat res or not - // // --- - // const { roomCreateEvent, powerLevelEvent } = await createRoom('public'); - - // // add a user - // const bob = '@bob:example.com'; - // await joinUser(roomCreateEvent.roomId, bob); - - // const powerLevelContent = structuredClone( - // powerLevelEvent.getContent(), - // ); - - // // we increase bob to 50 allowing room name change - - // powerLevelContent.users[bob] = 50; - - // const newPowerLevelEvent = PersistentEventFactory.newPowerLevelEvent( - // roomCreateEvent.roomId, - // roomCreateEvent.getContent().creator, - // powerLevelContent, - // PersistentEventFactory.defaultRoomVersion, - // ); - - // await Promise.all([ - // stateService.addAuthEvents(newPowerLevelEvent), - // stateService.addPrevEvents(newPowerLevelEvent), - // ]); - - // // to test the out of order saving, and state res being triggered, we need to force multiple state deltas to be created. - // // one will be the current one that triggers a new delta. - // // we'll also need an event that is supposed to be valid but was rejected previouysly - // // trhe power level event is supposed to "allow" bob to change the room name - // // but we processed/received the name change first. - - // // increase the graph - // await Promise.all([ - // joinUser(roomCreateEvent.roomId, '@random1:example.com'), - // joinUser(roomCreateEvent.roomId, '@random2:example.com'), - // joinUser(roomCreateEvent.roomId, '@random3:example.com'), - // joinUser(roomCreateEvent.roomId, '@random4:example.com'), - // joinUser(roomCreateEvent.roomId, '@random5:example.com'), - // joinUser(roomCreateEvent.roomId, '@random6:example.com'), - // joinUser(roomCreateEvent.roomId, '@random7:example.com'), - // joinUser(roomCreateEvent.roomId, '@random8:example.com'), - // joinUser(roomCreateEvent.roomId, '@random9:example.com'), - // ]); - - // // bob tries to change the room name, but at this point the power level event has not been "sent" yet - - // const bobChangeNameEvent = PersistentEventFactory.newRoomNameEvent( - // roomCreateEvent.roomId, - // bob, - // 'Bob Changed Name', - // PersistentEventFactory.defaultRoomVersion, - // ); - - // await Promise.all([ - // stateService.addAuthEvents(bobChangeNameEvent), - // stateService.addPrevEvents(bobChangeNameEvent), - // ]); - - // await stateService.handlePdu(bobChangeNameEvent); - - // expect(bobChangeNameEvent.rejected).toBeTrue(); - // expect(bobChangeNameEvent.rejectCode).toBe(RejectReason.AuthError); - // expect(bobChangeNameEvent.rejectedBy).toBe(powerLevelEvent.eventId); - - // // ^ rejected event, will not participate in graph, thus nextEventId stays the same (for our immplementatioon) - - // // since name change event was rejected, it did not create a new state delta - // // need to send another to now create one - // }); - - it('should list correct servers for a room', async () => { - const { roomCreateEvent } = await createRoom('public'); - - // add a user - const bob = '@bob:example.com'; - await joinUser(roomCreateEvent.roomId, bob); - - const diego = '@diego:example.com'; - await joinUser(roomCreateEvent.roomId, diego); - - const servers = await stateService.getServersInRoom(roomCreateEvent.roomId); - expect(servers).toContain('example.com'); - expect(servers.length).toBe(1); - - const remoteUser = '@alice:remote.com'; - await joinUser(roomCreateEvent.roomId, remoteUser); - - const servers2 = await stateService.getServersInRoom( - roomCreateEvent.roomId, - ); - expect(servers2).toContain('example.com'); - expect(servers2).toContain('remote.com'); - expect(servers2.length).toBe(2); + // it('should minimize amount of required state resolutions', async () => { + // const spy = spyOn(room, 'resolveStateV2Plus'); + // // once an old event gets to us, we run state res. + // // assume the state res stored new deltas randomly, would cause forward extremeties to behave the same way. + // // the next new event we will receive, should be pointing to the latest state, for that to happenm + // // we must make sure we associate latest state with the latest event we could have at that time. + // // --- + // // create a room + // // do stuff + // // send an out of order event + // // now send a normal event + // // see if it triggered stat res or not + // // --- + // const { roomCreateEvent, powerLevelEvent } = await createRoom('public'); + + // // add a user + // const bob = '@bob:example.com'; + // await joinUser(roomCreateEvent.roomId, bob); + + // const powerLevelContent = structuredClone( + // powerLevelEvent.getContent(), + // ); + + // // we increase bob to 50 allowing room name change + + // powerLevelContent.users[bob] = 50; + + // const newPowerLevelEvent = PersistentEventFactory.newPowerLevelEvent( + // roomCreateEvent.roomId, + // roomCreateEvent.getContent().creator, + // powerLevelContent, + // PersistentEventFactory.defaultRoomVersion, + // ); + + // await Promise.all([ + // stateService.addAuthEvents(newPowerLevelEvent), + // stateService.addPrevEvents(newPowerLevelEvent), + // ]); + + // // to test the out of order saving, and state res being triggered, we need to force multiple state deltas to be created. + // // one will be the current one that triggers a new delta. + // // we'll also need an event that is supposed to be valid but was rejected previouysly + // // trhe power level event is supposed to "allow" bob to change the room name + // // but we processed/received the name change first. + + // // increase the graph + // await Promise.all([ + // joinUser(roomCreateEvent.roomId, '@random1:example.com'), + // joinUser(roomCreateEvent.roomId, '@random2:example.com'), + // joinUser(roomCreateEvent.roomId, '@random3:example.com'), + // joinUser(roomCreateEvent.roomId, '@random4:example.com'), + // joinUser(roomCreateEvent.roomId, '@random5:example.com'), + // joinUser(roomCreateEvent.roomId, '@random6:example.com'), + // joinUser(roomCreateEvent.roomId, '@random7:example.com'), + // joinUser(roomCreateEvent.roomId, '@random8:example.com'), + // joinUser(roomCreateEvent.roomId, '@random9:example.com'), + // ]); + + // // bob tries to change the room name, but at this point the power level event has not been "sent" yet + + // const bobChangeNameEvent = PersistentEventFactory.newRoomNameEvent( + // roomCreateEvent.roomId, + // bob, + // 'Bob Changed Name', + // PersistentEventFactory.defaultRoomVersion, + // ); + + // await Promise.all([ + // stateService.addAuthEvents(bobChangeNameEvent), + // stateService.addPrevEvents(bobChangeNameEvent), + // ]); + + // await stateService.handlePdu(bobChangeNameEvent); + + // expect(bobChangeNameEvent.rejected).toBeTrue(); + // expect(bobChangeNameEvent.rejectCode).toBe(RejectReason.AuthError); + // expect(bobChangeNameEvent.rejectedBy).toBe(powerLevelEvent.eventId); + + // // ^ rejected event, will not participate in graph, thus nextEventId stays the same (for our immplementatioon) + + // // since name change event was rejected, it did not create a new state delta + // // need to send another to now create one + // }); + + it('should list correct servers for a room', async () => { + const { roomCreateEvent } = await createRoom('public'); + + // add a user + const bob = '@bob:example.com'; + await joinUser(roomCreateEvent.roomId, bob); + + const diego = '@diego:example.com'; + await joinUser(roomCreateEvent.roomId, diego); + + const servers = await stateService.getServersInRoom( + roomCreateEvent.roomId, + ); + expect(servers).toContain('example.com'); + expect(servers.length).toBe(1); - // now leave the remote user - await leaveUser(roomCreateEvent.roomId, remoteUser); + const remoteUser = '@alice:remote.com'; + await joinUser(roomCreateEvent.roomId, remoteUser); - const servers3 = await stateService.getServersInRoom( - roomCreateEvent.roomId, - ); - expect(servers3).toContain('example.com'); - expect(servers3.length).toBe(1); + const servers2 = await stateService.getServersInRoom( + roomCreateEvent.roomId, + ); + expect(servers2).toContain('example.com'); + expect(servers2).toContain('remote.com'); + expect(servers2.length).toBe(2); - // now add her again - await joinUser(roomCreateEvent.roomId, remoteUser); + // now leave the remote user + await leaveUser(roomCreateEvent.roomId, remoteUser); - const servers4 = await stateService.getServersInRoom( - roomCreateEvent.roomId, - ); - expect(servers4).toContain('example.com'); - expect(servers4).toContain('remote.com'); - expect(servers4.length).toBe(2); - }); + const servers3 = await stateService.getServersInRoom( + roomCreateEvent.roomId, + ); + expect(servers3).toContain('example.com'); + expect(servers3.length).toBe(1); - it('should allow previously rejected events through multiple state resolutions', async () => { - const { roomCreateEvent } = await createRoom('public'); - const roomId = roomCreateEvent.roomId; - const roomVersion = roomCreateEvent.getContent().room_version; - const creator = roomCreateEvent.getContent().creator as room.UserID; + // now add her again + await joinUser(roomCreateEvent.roomId, remoteUser); - const referenceDepthEvent = await joinUser(roomId, '@dummy:example.com'); + const servers4 = await stateService.getServersInRoom( + roomCreateEvent.roomId, + ); + expect(servers4).toContain('example.com'); + expect(servers4).toContain('remote.com'); + expect(servers4.length).toBe(2); + }); - // try to join - const bob = '@bob:example.com'; - const bobJoinEvent = await joinUser(roomId, bob); - expect(bobJoinEvent.rejected).toBeFalse(); + it('should allow previously rejected events through multiple state resolutions', async () => { + const { roomCreateEvent } = await createRoom('public'); + const roomId = roomCreateEvent.roomId; + const roomVersion = roomCreateEvent.getContent().room_version; + const creator = roomCreateEvent.getContent().creator as room.UserID; - const joinRuleEvent = await copyDepth( - referenceDepthEvent, - await stateService.buildEvent<'m.room.join_rules'>( - { - type: 'm.room.join_rules', - content: { join_rule: 'invite' }, - room_id: roomId, - state_key: '', - sender: creator, - ...getDefaultFields(), - }, - roomVersion, - ), - ); + const referenceDepthEvent = await joinUser(roomId, '@dummy:example.com'); - await stateService.handlePdu(joinRuleEvent); + // try to join + const bob = '@bob:example.com'; + const bobJoinEvent = await joinUser(roomId, bob); + expect(bobJoinEvent.rejected).toBeFalse(); - // should have triggered a state res, causing bob to no longer be part of the room - const state1 = await stateService.getLatestRoomState2(roomId); + const joinRuleEvent = await copyDepth( + referenceDepthEvent, + await stateService.buildEvent<'m.room.join_rules'>( + { + type: 'm.room.join_rules', + content: { join_rule: 'invite' }, + room_id: roomId, + state_key: '', + sender: creator, + ...getDefaultFields(), + }, + roomVersion, + ), + ); - expect(state1.isUserInRoom(bob)).toBeFalse(); + await stateService.handlePdu(joinRuleEvent); - // but what if join rule were to be immediately switched back? - const joinRulePublicEvent = await copyDepth( - joinRuleEvent, - await stateService.buildEvent<'m.room.join_rules'>( - { - type: 'm.room.join_rules', - sender: creator, - state_key: '', - room_id: roomId, - content: { join_rule: 'public' }, - ...getDefaultFields(), - }, - roomVersion, - ), - ); + // should have triggered a state res, causing bob to no longer be part of the room + const state1 = await stateService.getLatestRoomState2(roomId); - await stateService.handlePdu(joinRulePublicEvent); + expect(state1.isUserInRoom(bob)).toBeFalse(); - const state2 = await stateService.getLatestRoomState2(roomId); + // but what if join rule were to be immediately switched back? + const joinRulePublicEvent = await copyDepth( + joinRuleEvent, + await stateService.buildEvent<'m.room.join_rules'>( + { + type: 'm.room.join_rules', + sender: creator, + state_key: '', + room_id: roomId, + content: { join_rule: 'public' }, + ...getDefaultFields(), + }, + roomVersion, + ), + ); - expect(state2.isPublic()).toBeTrue(); - expect(state2.isUserInRoom(bob)).toBeTrue(); - }); + await stateService.handlePdu(joinRulePublicEvent); - it('should consider previously rejected event as part of state if new out of order event allows it', async () => { - const { roomCreateEvent } = await createRoom('public'); - const roomId = roomCreateEvent.roomId; - const creator = roomCreateEvent.getContent().creator as room.UserID; - const roomVersion = roomCreateEvent.version; - - // make bob join - const bob = '@bob:example.com'; - const bobJoinEvent = await joinUser(roomId, bob); - const state1 = await stateService.getLatestRoomState2(roomId); - expect(state1.isUserInRoom(bob)).toBeTrue(); - - // change join rule to private - const joinRuleInvite = await copyDepth( - bobJoinEvent, - await stateService.buildEvent<'m.room.join_rules'>( - { - type: 'm.room.join_rules', - sender: creator, - state_key: '', - room_id: roomId, - content: { join_rule: 'invite' }, - ...getDefaultFields(), - }, - roomVersion, - ), - ); + const state2 = await stateService.getLatestRoomState2(roomId); - await stateService.handlePdu(joinRuleInvite); + expect(state2.isPublic()).toBeTrue(); + expect(state2.isUserInRoom(bob)).toBeTrue(); + }); - // bob not in room - const state2 = await stateService.getLatestRoomState2(roomId); - expect(state2.isInviteOnly()).toBeTrue(); - expect(state2.isUserInRoom(bob)).toBeFalse(); - //.... + it('should consider previously rejected event as part of state if new out of order event allows it', async () => { + const { roomCreateEvent } = await createRoom('public'); + const roomId = roomCreateEvent.roomId; + const creator = roomCreateEvent.getContent().creator as room.UserID; + const roomVersion = roomCreateEvent.version; - const joinRulePublic = await copyDepth( - bobJoinEvent, - await stateService.buildEvent<'m.room.join_rules'>( - { - type: 'm.room.join_rules', - sender: creator, - state_key: '', - room_id: roomId, - content: { join_rule: 'public' }, - ...getDefaultFields(), - }, - roomVersion, - ), - ); + // make bob join + const bob = '@bob:example.com'; + const bobJoinEvent = await joinUser(roomId, bob); + const state1 = await stateService.getLatestRoomState2(roomId); + expect(state1.isUserInRoom(bob)).toBeTrue(); - // bob allowed to be in room again - await stateService.handlePdu(joinRulePublic); - // - const state3 = await stateService.getLatestRoomState2(roomId); - expect(state3.isPublic()).toBeTrue(); - expect(state3.isUserInRoom(bob)).toBeTrue(); - }); + // change join rule to private + const joinRuleInvite = await copyDepth( + bobJoinEvent, + await stateService.buildEvent<'m.room.join_rules'>( + { + type: 'm.room.join_rules', + sender: creator, + state_key: '', + room_id: roomId, + content: { join_rule: 'invite' }, + ...getDefaultFields(), + }, + roomVersion, + ), + ); + + await stateService.handlePdu(joinRuleInvite); - it('should build the correct latest state even if event is not accepted', async () => { - const don = '@don:example.com' as room.UserID; + // bob not in room + const state2 = await stateService.getLatestRoomState2(roomId); + expect(state2.isInviteOnly()).toBeTrue(); + expect(state2.isUserInRoom(bob)).toBeFalse(); + //.... + + const joinRulePublic = await copyDepth( + bobJoinEvent, + await stateService.buildEvent<'m.room.join_rules'>( + { + type: 'm.room.join_rules', + sender: creator, + state_key: '', + room_id: roomId, + content: { join_rule: 'public' }, + ...getDefaultFields(), + }, + roomVersion, + ), + ); - const { roomCreateEvent, roomNameEvent } = await createRoom('public', { - [don]: 50, + // bob allowed to be in room again + await stateService.handlePdu(joinRulePublic); + // + const state3 = await stateService.getLatestRoomState2(roomId); + expect(state3.isPublic()).toBeTrue(); + expect(state3.isUserInRoom(bob)).toBeTrue(); }); - const roomId = roomCreateEvent.roomId; - const roomVersion = roomCreateEvent.version; - const creator = roomCreateEvent.getContent().creator as room.UserID; + it('should build the correct latest state even if event is not accepted', async () => { + const don = '@don:example.com' as room.UserID; - await joinUser(roomId, don); + const { roomCreateEvent, roomNameEvent } = await createRoom('public', { + [don]: 50, + }); - // prepare a room name event - const roomName = stripPreviousEvents( - // auth events stay the same - await stateService.buildEvent<'m.room.name'>( - { - type: 'm.room.name', - sender: don, - state_key: '', - content: { name: 'new name' }, - room_id: roomId, - ...getDefaultFields(), - }, - roomVersion, - ), - ); + const roomId = roomCreateEvent.roomId; + const roomVersion = roomCreateEvent.version; + const creator = roomCreateEvent.getContent().creator as room.UserID; - const stateResSpy = spyOn(room, 'resolveStateV2Plus'); + await joinUser(roomId, don); - // two state events creating two chains - // 1. normal join EVent - const bob = '@bob:example.com'; - const bobJoin = await joinUser(roomId, bob); + // prepare a room name event + const roomName = stripPreviousEvents( + // auth events stay the same + await stateService.buildEvent<'m.room.name'>( + { + type: 'm.room.name', + sender: don, + state_key: '', + content: { name: 'new name' }, + room_id: roomId, + ...getDefaultFields(), + }, + roomVersion, + ), + ); - expect(stateResSpy).toHaveBeenCalledTimes(0); + const stateResSpy = spyOn(room, 'resolveStateV2Plus'); - // 2. another normal join event but same depth - const donBan = await copyDepth( - bobJoin, - await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - sender: creator, - state_key: don, - content: { membership: 'ban' }, - room_id: roomId, - ...getDefaultFields(), - }, - roomVersion, - ), - ); + // two state events creating two chains + // 1. normal join EVent + const bob = '@bob:example.com'; + const bobJoin = await joinUser(roomId, bob); - await stateService.handlePdu(donBan); + expect(stateResSpy).toHaveBeenCalledTimes(0); - expect(stateResSpy).toHaveBeenCalledTimes(1); // soft fail check + // 2. another normal join event but same depth + const donBan = await copyDepth( + bobJoin, + await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + sender: creator, + state_key: don, + content: { membership: 'ban' }, + room_id: roomId, + ...getDefaultFields(), + }, + roomVersion, + ), + ); - // stateResSpy.mockReset(); + await stateService.handlePdu(donBan); - const state1 = await stateService.getLatestRoomState2(roomId); - expect(state1.isUserInRoom(bob)).toBeTrue(); - expect(state1.getUserMembership(don)).toBe('ban'); + expect(stateResSpy).toHaveBeenCalledTimes(1); // soft fail check - // a new event that - roomName.addPrevEvents([bobJoin, donBan]); + // stateResSpy.mockReset(); - expect(stateService.handlePdu(roomName)).rejects.toThrowError(); + const state1 = await stateService.getLatestRoomState2(roomId); + expect(state1.isUserInRoom(bob)).toBeTrue(); + expect(state1.getUserMembership(don)).toBe('ban'); - const state2 = await stateService.getStateAtEvent(roomName); - // must not be new name - expect(state2.get(roomName.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - roomNameEvent.eventId, - ); - }); + // a new event that + roomName.addPrevEvents([bobJoin, donBan]); - // removing bias from own code - it('should fetch the right state ids', async () => { - const toEventBase = (pdu: room.Pdu) => { - return PersistentEventFactory.createFromRawEvent(pdu, '10'); - }; + expect(stateService.handlePdu(roomName)).rejects.toThrowError(); - const createEvent = { - type: 'm.room.create', - state_key: '', - content: { - room_version: '10', - creator: '@debdut:rc1.tunnel.dev.rocket.chat', - }, - sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, - origin_server_ts: 1759757583361, - room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, - prev_events: [], - auth_events: [], - depth: 0, - hashes: { - sha256: 'FIP9UiTEzoBmUJDr6wX5e2N6Xl0pg68xC8OSFXfbNac', - }, - signatures: { - 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'lMv8+0wXgFSGRtHgBNhu8T8xkcCc6SLZfJwcLGjFIgaXAdAxMjx7HiZNv+JuDWl8qEbgdisuTkPzUTmdsgxgDQ', + const state2 = await stateService.getStateAtEvent(roomName); + // must not be new name + expect(state2.get(roomName.getUniqueStateIdentifier())).toHaveProperty( + 'eventId', + roomNameEvent.eventId, + ); + }); + + // removing bias from own code + it('should fetch the right state ids', async () => { + const toEventBase = (pdu: room.Pdu) => { + return PersistentEventFactory.createFromRawEvent(pdu, '10'); + }; + + const createEvent = { + type: 'm.room.create', + state_key: '', + content: { + room_version: '10', + creator: '@debdut:rc1.tunnel.dev.rocket.chat', }, - }, - unsigned: {}, - } satisfies room.Pdu; + sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, + origin_server_ts: 1759757583361, + room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, + prev_events: [], + auth_events: [], + depth: 0, + hashes: { + sha256: 'FIP9UiTEzoBmUJDr6wX5e2N6Xl0pg68xC8OSFXfbNac', + }, + signatures: { + 'rc1.tunnel.dev.rocket.chat': { + 'ed25519:0': + 'lMv8+0wXgFSGRtHgBNhu8T8xkcCc6SLZfJwcLGjFIgaXAdAxMjx7HiZNv+JuDWl8qEbgdisuTkPzUTmdsgxgDQ', + }, + }, + unsigned: {}, + } satisfies room.Pdu; - await stateService.handlePdu(toEventBase(createEvent)); + await stateService.handlePdu(toEventBase(createEvent)); - const memberEvent = { - type: 'm.room.member', - content: { - membership: 'join', - }, - room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, - state_key: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, - auth_events: [ - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ] as EventID[], - depth: 1, - prev_events: [ - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ] as EventID[], - origin_server_ts: 1759757583403, - sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, - hashes: { - sha256: 'xxHM8Wz5s70Z24XGVlQuQlP6wu4WKHKrWJkX+VZsN7Y', - }, - signatures: { - 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'LCl4QANTD9cSDpIrXj3bv7nJxs9x7QA4y6tZl49//C3CJJiGzZUfjGG3dH11RcSD2esTNSYJwQAbKqNGiWe4Ag', + const memberEvent = { + type: 'm.room.member', + content: { + membership: 'join', }, - }, - unsigned: {}, - } satisfies room.Pdu; + room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, + state_key: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, + auth_events: [ + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + ] as EventID[], + depth: 1, + prev_events: [ + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + ] as EventID[], + origin_server_ts: 1759757583403, + sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, + hashes: { + sha256: 'xxHM8Wz5s70Z24XGVlQuQlP6wu4WKHKrWJkX+VZsN7Y', + }, + signatures: { + 'rc1.tunnel.dev.rocket.chat': { + 'ed25519:0': + 'LCl4QANTD9cSDpIrXj3bv7nJxs9x7QA4y6tZl49//C3CJJiGzZUfjGG3dH11RcSD2esTNSYJwQAbKqNGiWe4Ag', + }, + }, + unsigned: {}, + } satisfies room.Pdu; - await stateService.handlePdu(toEventBase(memberEvent)); + await stateService.handlePdu(toEventBase(memberEvent)); - const name = { - type: 'm.room.name', - content: { - name: 'a', - }, - room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, - state_key: '', - auth_events: [ - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ] as EventID[], - depth: 2, - prev_events: [ - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - ] as EventID[], - origin_server_ts: 1759757583427, - sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, - hashes: { - sha256: 'kSKUQ1qEW1kMBFFT8T738BJRpgKQaZoeX9K/RcgOxx8', - }, - signatures: { - 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'GSgPxsTF7VnbtGEGmwhH7F/Ets4R15BJpl1NjWi+SdwkVp7nvcQm/hKUNY803QlBNYur5OcLIi47DkxJ2Cg1Cw', + const name = { + type: 'm.room.name', + content: { + name: 'a', }, - }, - unsigned: {}, - } satisfies room.Pdu; + room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, + state_key: '', + auth_events: [ + '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + ] as EventID[], + depth: 2, + prev_events: [ + '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', + ] as EventID[], + origin_server_ts: 1759757583427, + sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, + hashes: { + sha256: 'kSKUQ1qEW1kMBFFT8T738BJRpgKQaZoeX9K/RcgOxx8', + }, + signatures: { + 'rc1.tunnel.dev.rocket.chat': { + 'ed25519:0': + 'GSgPxsTF7VnbtGEGmwhH7F/Ets4R15BJpl1NjWi+SdwkVp7nvcQm/hKUNY803QlBNYur5OcLIi47DkxJ2Cg1Cw', + }, + }, + unsigned: {}, + } satisfies room.Pdu; - await stateService.handlePdu(toEventBase(name)); + await stateService.handlePdu(toEventBase(name)); - const powerLevel = { - type: 'm.room.power_levels', - content: { - users: { - '@debdut:rc1.tunnel.dev.rocket.chat': 100, + const powerLevel = { + type: 'm.room.power_levels', + content: { + users: { + '@debdut:rc1.tunnel.dev.rocket.chat': 100, + }, + users_default: 0, + events: {}, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 50, }, - users_default: 0, - events: {}, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 50, - }, - room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, - state_key: '', - auth_events: [ - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ] as EventID[], - depth: 3, - prev_events: [ - '$yvGHQAk_VvuInS5WsW3_w-mi5zLSC_ZWz724wSla_z4', - ] as EventID[], - origin_server_ts: 1759757583446, - sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, - hashes: { - sha256: 'tyTsDppTjJWviE4U2dU5SIhofkWOg1mM50m43dmJUWk', - }, - signatures: { - 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'Tftd0/LAn8NaEcrGYb5nXp69nbSyVBNqlDuqSfq3XbAOlWSRZIiP7/Zm4RZmrdZ6zZgjvDABD+TrCiRFccxdDg', + room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, + state_key: '', + auth_events: [ + '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + ] as EventID[], + depth: 3, + prev_events: [ + '$yvGHQAk_VvuInS5WsW3_w-mi5zLSC_ZWz724wSla_z4', + ] as EventID[], + origin_server_ts: 1759757583446, + sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, + hashes: { + sha256: 'tyTsDppTjJWviE4U2dU5SIhofkWOg1mM50m43dmJUWk', }, - }, - unsigned: {}, - } satisfies room.Pdu; + signatures: { + 'rc1.tunnel.dev.rocket.chat': { + 'ed25519:0': + 'Tftd0/LAn8NaEcrGYb5nXp69nbSyVBNqlDuqSfq3XbAOlWSRZIiP7/Zm4RZmrdZ6zZgjvDABD+TrCiRFccxdDg', + }, + }, + unsigned: {}, + } satisfies room.Pdu; - await stateService.handlePdu(toEventBase(powerLevel)); + await stateService.handlePdu(toEventBase(powerLevel)); - const joinRule = { - type: 'm.room.join_rules', - content: { - join_rule: 'public', - }, - room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, - state_key: '', - auth_events: [ - '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ] as EventID[], - depth: 4, - prev_events: [ - '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', - ] as EventID[], - origin_server_ts: 1759757583461, - sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, - hashes: { - sha256: 'jt3bItyFElVVoEOJWaqtNf93dzpWi4D8kx6+ApABA6c', - }, - signatures: { - 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - '4yNSSn29xebrwW2LpM+P7qGgHhpNFbmIFrvKQw7sN0qRHNaLIx7mlwiFyfm2P4gdHaiIV+6WyeueH+QtPJejAA', + const joinRule = { + type: 'm.room.join_rules', + content: { + join_rule: 'public', }, - }, - unsigned: {}, - } satisfies room.Pdu; + room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, + state_key: '', + auth_events: [ + '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', + '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + ] as EventID[], + depth: 4, + prev_events: [ + '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', + ] as EventID[], + origin_server_ts: 1759757583461, + sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, + hashes: { + sha256: 'jt3bItyFElVVoEOJWaqtNf93dzpWi4D8kx6+ApABA6c', + }, + signatures: { + 'rc1.tunnel.dev.rocket.chat': { + 'ed25519:0': + '4yNSSn29xebrwW2LpM+P7qGgHhpNFbmIFrvKQw7sN0qRHNaLIx7mlwiFyfm2P4gdHaiIV+6WyeueH+QtPJejAA', + }, + }, + unsigned: {}, + } satisfies room.Pdu; - await stateService.handlePdu(toEventBase(joinRule)); + await stateService.handlePdu(toEventBase(joinRule)); - const alias = { - type: 'm.room.canonical_alias', - content: { - alias: '#a:rc1.tunnel.dev.rocket.chat', - alt_aliases: [], - }, - room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, - state_key: '', - auth_events: [ - '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ] as EventID[], - depth: 5, - prev_events: [ - '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', - ] as EventID[], - origin_server_ts: 1759757583476, - sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, - hashes: { - sha256: 'hvUo6j/yFIjhst7AJSgeMFKGtk4MuPAROKDdDv/Eb5c', - }, - signatures: { - 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'HwCvEEM4l7HQDiHVyoNQOgsPuKR4Q7ZCfaD025XZ+rv9AG2Gu8rTGJ9Bvtq8oKSCgtqvNrPplLdk/KVow7ZXCA', + const alias = { + type: 'm.room.canonical_alias', + content: { + alias: '#a:rc1.tunnel.dev.rocket.chat', + alt_aliases: [], }, - }, - unsigned: {}, - } satisfies room.Pdu; + room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, + state_key: '', + auth_events: [ + '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', + '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + ] as EventID[], + depth: 5, + prev_events: [ + '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', + ] as EventID[], + origin_server_ts: 1759757583476, + sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, + hashes: { + sha256: 'hvUo6j/yFIjhst7AJSgeMFKGtk4MuPAROKDdDv/Eb5c', + }, + signatures: { + 'rc1.tunnel.dev.rocket.chat': { + 'ed25519:0': + 'HwCvEEM4l7HQDiHVyoNQOgsPuKR4Q7ZCfaD025XZ+rv9AG2Gu8rTGJ9Bvtq8oKSCgtqvNrPplLdk/KVow7ZXCA', + }, + }, + unsigned: {}, + } satisfies room.Pdu; - await stateService.handlePdu(toEventBase(alias)); + await stateService.handlePdu(toEventBase(alias)); - const invite = { - type: 'm.room.member', - content: { - membership: 'invite', - }, - room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, - state_key: '@ah:syn1.tunnel.dev.rocket.chat' as room.UserID, - auth_events: [ - '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', - ] as EventID[], - depth: 6, - prev_events: [ - '$3Ttw6n2x6EALDnf4Cm5BKtYrIfzlyuE0VMmfXI2j680', - ] as EventID[], - origin_server_ts: 1759757778902, - sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, - hashes: { - sha256: 'fcw6kOo6W9yufNeeLB9QI+WLz78ED/QvMNbSut0sXMM', - }, - signatures: { - 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'hoNC177zwdlYHURCAsavTPVYsBJJMINeuVCsbZjFiBFuO4SEaMEmTU/5Ht0KADE/XwHCOeGg4xB9sD9L+yeWDQ', + const invite = { + type: 'm.room.member', + content: { + membership: 'invite', }, - 'syn1.tunnel.dev.rocket.chat': { - 'ed25519:a_FAET': - 'hpFY8m5g1e4s/0VrV8fEP3hIsvn1sh68ZfXaUrV5wpMSFwAzZaC5wW5UxwHAopUTDNfNRnR1lANti6a9ddUFBw', + room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, + state_key: '@ah:syn1.tunnel.dev.rocket.chat' as room.UserID, + auth_events: [ + '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', + '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', + ] as EventID[], + depth: 6, + prev_events: [ + '$3Ttw6n2x6EALDnf4Cm5BKtYrIfzlyuE0VMmfXI2j680', + ] as EventID[], + origin_server_ts: 1759757778902, + sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, + hashes: { + sha256: 'fcw6kOo6W9yufNeeLB9QI+WLz78ED/QvMNbSut0sXMM', }, - }, - unsigned: { - invite_room_state: [ - { - content: { - alias: '#a:rc1.tunnel.dev.rocket.chat', - alt_aliases: [], - }, - sender: '@debdut:rc1.tunnel.dev.rocket.chat', - state_key: '', - type: 'm.room.canonical_alias', + signatures: { + 'rc1.tunnel.dev.rocket.chat': { + 'ed25519:0': + 'hoNC177zwdlYHURCAsavTPVYsBJJMINeuVCsbZjFiBFuO4SEaMEmTU/5Ht0KADE/XwHCOeGg4xB9sD9L+yeWDQ', }, - { - content: { - join_rule: 'public', - }, - sender: '@debdut:rc1.tunnel.dev.rocket.chat', - state_key: '', - type: 'm.room.join_rules', + 'syn1.tunnel.dev.rocket.chat': { + 'ed25519:a_FAET': + 'hpFY8m5g1e4s/0VrV8fEP3hIsvn1sh68ZfXaUrV5wpMSFwAzZaC5wW5UxwHAopUTDNfNRnR1lANti6a9ddUFBw', }, - { - content: { - users: { - '@debdut:rc1.tunnel.dev.rocket.chat': 100, + }, + unsigned: { + invite_room_state: [ + { + content: { + alias: '#a:rc1.tunnel.dev.rocket.chat', + alt_aliases: [], }, - users_default: 0, - events: {}, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 50, + sender: '@debdut:rc1.tunnel.dev.rocket.chat', + state_key: '', + type: 'm.room.canonical_alias', }, - sender: '@debdut:rc1.tunnel.dev.rocket.chat', - state_key: '', - type: 'm.room.power_levels', - }, - { - content: { - name: 'a', + { + content: { + join_rule: 'public', + }, + sender: '@debdut:rc1.tunnel.dev.rocket.chat', + state_key: '', + type: 'm.room.join_rules', }, - sender: '@debdut:rc1.tunnel.dev.rocket.chat', - state_key: '', - type: 'm.room.name', - }, - { - content: { - membership: 'join', + { + content: { + users: { + '@debdut:rc1.tunnel.dev.rocket.chat': 100, + }, + users_default: 0, + events: {}, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 50, + }, + sender: '@debdut:rc1.tunnel.dev.rocket.chat', + state_key: '', + type: 'm.room.power_levels', }, - sender: '@debdut:rc1.tunnel.dev.rocket.chat', - state_key: '@debdut:rc1.tunnel.dev.rocket.chat', - type: 'm.room.member', - }, - { - content: { - room_version: '10', - creator: '@debdut:rc1.tunnel.dev.rocket.chat', + { + content: { + name: 'a', + }, + sender: '@debdut:rc1.tunnel.dev.rocket.chat', + state_key: '', + type: 'm.room.name', }, - sender: '@debdut:rc1.tunnel.dev.rocket.chat', - state_key: '', - type: 'm.room.create', + { + content: { + membership: 'join', + }, + sender: '@debdut:rc1.tunnel.dev.rocket.chat', + state_key: '@debdut:rc1.tunnel.dev.rocket.chat', + type: 'm.room.member', + }, + { + content: { + room_version: '10', + creator: '@debdut:rc1.tunnel.dev.rocket.chat', + }, + sender: '@debdut:rc1.tunnel.dev.rocket.chat', + state_key: '', + type: 'm.room.create', + }, + ], + }, + } satisfies room.Pdu; + + await stateService.handlePdu(toEventBase(invite)); + // join ecebnt + const ahJoin = { + type: 'm.room.member', + content: { + membership: 'join', + displayname: 'ah', + // @ts-ignore this has been fixed by rodrigo already in zod + avatar_url: null as unknown as undefined, + }, + sender: '@ah:syn1.tunnel.dev.rocket.chat' as room.UserID, + room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, + origin_server_ts: 1759757909955, + depth: 7, + prev_events: [ + '$Ka58p5BSdnjjmCPN22Erj6piAXRHIm9hQjXv1g5DeXw', + ] as EventID[], + auth_events: [ + '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', + '$Ka58p5BSdnjjmCPN22Erj6piAXRHIm9hQjXv1g5DeXw', + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', + ] as EventID[], + // @ts-ignore + origin: 'syn1.tunnel.dev.rocket.chat', + unsigned: { + age: 2, + }, + state_key: '@ah:syn1.tunnel.dev.rocket.chat' as room.UserID, + hashes: { + sha256: 'Hm9e72/DXfNsXJHoS/rGhIyBuNBnp3KcCEtuukMUrUk', + }, + signatures: { + 'rc1.tunnel.dev.rocket.chat': { + 'ed25519:0': + '5IyCqACOVFJ9h5HGbXWNOwuNpn2hdCRpDWMgmfnH11epIjCCGKNHLftaBPdiOLTSO9RyBixEtplphmgBazWCDQ', }, + 'syn1.tunnel.dev.rocket.chat': { + 'ed25519:a_FAET': + 'c6SYtgr3UoFu3OfxjF5Da+Sk2VgEBQxK3StPxC0CzXWrVg2AUkJyqL4RfbfAhMnRm3eDRFTHLZ1+WzllFJv6BA', + }, + }, + } satisfies room.Pdu; + await stateService.handlePdu(toEventBase(ahJoin)); + const state = await stateService.getStateBeforeEvent(toEventBase(ahJoin)); + const pduIds = Array.from(state.values()) + .map((e) => e.eventId) + .sort(); + const expected = { + pdu_ids: [ + '$3Ttw6n2x6EALDnf4Cm5BKtYrIfzlyuE0VMmfXI2j680', + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', + '$yvGHQAk_VvuInS5WsW3_w-mi5zLSC_ZWz724wSla_z4', + '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', + '$Ka58p5BSdnjjmCPN22Erj6piAXRHIm9hQjXv1g5DeXw', + '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', + ].sort() as EventID[], + auth_chain_ids: [ + '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', + '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', + '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', ], - }, - } satisfies room.Pdu; - - await stateService.handlePdu(toEventBase(invite)); - // join ecebnt - const ahJoin = { - type: 'm.room.member', - content: { - membership: 'join', - displayname: 'ah', - // @ts-ignore this has been fixed by rodrigo already in zod - avatar_url: null as unknown as undefined, - }, - sender: '@ah:syn1.tunnel.dev.rocket.chat' as room.UserID, - room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, - origin_server_ts: 1759757909955, - depth: 7, - prev_events: [ - '$Ka58p5BSdnjjmCPN22Erj6piAXRHIm9hQjXv1g5DeXw', - ] as EventID[], - auth_events: [ - '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', - '$Ka58p5BSdnjjmCPN22Erj6piAXRHIm9hQjXv1g5DeXw', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', - ] as EventID[], - // @ts-ignore - origin: 'syn1.tunnel.dev.rocket.chat', - unsigned: { - age: 2, - }, - state_key: '@ah:syn1.tunnel.dev.rocket.chat' as room.UserID, - hashes: { - sha256: 'Hm9e72/DXfNsXJHoS/rGhIyBuNBnp3KcCEtuukMUrUk', - }, - signatures: { - 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - '5IyCqACOVFJ9h5HGbXWNOwuNpn2hdCRpDWMgmfnH11epIjCCGKNHLftaBPdiOLTSO9RyBixEtplphmgBazWCDQ', + }; + expect(pduIds).toStrictEqual(expected.pdu_ids); + const expectedAfterMessageSent = { + pdu_ids: [ + '$3Ttw6n2x6EALDnf4Cm5BKtYrIfzlyuE0VMmfXI2j680', + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', + '$yvGHQAk_VvuInS5WsW3_w-mi5zLSC_ZWz724wSla_z4', + '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', + '$-C1Tf8UTaSZPEGcwVAUD1xGnKVS64HG_DxiEQVbIJBg', + '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', + ].sort() as EventID[], + auth_chain_ids: [ + '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', + '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', + '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', + '$Ka58p5BSdnjjmCPN22Erj6piAXRHIm9hQjXv1g5DeXw', + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + ], + }; + const message = { + type: 'm.room.message', + content: { + body: '1', + // @ts-ignore are we missing this ? TODO: + 'm.mentions': {}, + msgtype: 'm.text', }, - 'syn1.tunnel.dev.rocket.chat': { - 'ed25519:a_FAET': - 'c6SYtgr3UoFu3OfxjF5Da+Sk2VgEBQxK3StPxC0CzXWrVg2AUkJyqL4RfbfAhMnRm3eDRFTHLZ1+WzllFJv6BA', + sender: '@ah:syn1.tunnel.dev.rocket.chat' as room.UserID, + room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, + origin_server_ts: 1759760138291, + depth: 8, + prev_events: [ + '$-C1Tf8UTaSZPEGcwVAUD1xGnKVS64HG_DxiEQVbIJBg', + ] as EventID[], + auth_events: [ + '$-C1Tf8UTaSZPEGcwVAUD1xGnKVS64HG_DxiEQVbIJBg', + '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', + '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', + ] as EventID[], + origin: 'syn1.tunnel.dev.rocket.chat', + unsigned: { + age_ts: 1759760138291, }, - }, - } satisfies room.Pdu; - await stateService.handlePdu(toEventBase(ahJoin)); - const state = await stateService.getStateBeforeEvent(toEventBase(ahJoin)); - const pduIds = Array.from(state.values()) - .map((e) => e.eventId) - .sort(); - const expected = { - pdu_ids: [ - '$3Ttw6n2x6EALDnf4Cm5BKtYrIfzlyuE0VMmfXI2j680', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', - '$yvGHQAk_VvuInS5WsW3_w-mi5zLSC_ZWz724wSla_z4', - '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', - '$Ka58p5BSdnjjmCPN22Erj6piAXRHIm9hQjXv1g5DeXw', - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - ].sort() as EventID[], - auth_chain_ids: [ - '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', - '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ], - }; - expect(pduIds).toStrictEqual(expected.pdu_ids); - const expectedAfterMessageSent = { - pdu_ids: [ - '$3Ttw6n2x6EALDnf4Cm5BKtYrIfzlyuE0VMmfXI2j680', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', - '$yvGHQAk_VvuInS5WsW3_w-mi5zLSC_ZWz724wSla_z4', - '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', - '$-C1Tf8UTaSZPEGcwVAUD1xGnKVS64HG_DxiEQVbIJBg', - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - ].sort() as EventID[], - auth_chain_ids: [ - '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', - '$Ka58p5BSdnjjmCPN22Erj6piAXRHIm9hQjXv1g5DeXw', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ], - }; - const message = { - type: 'm.room.message', - content: { - body: '1', - // @ts-ignore are we missing this ? TODO: - 'm.mentions': {}, - msgtype: 'm.text', - }, - sender: '@ah:syn1.tunnel.dev.rocket.chat' as room.UserID, - room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, - origin_server_ts: 1759760138291, - depth: 8, - prev_events: [ - '$-C1Tf8UTaSZPEGcwVAUD1xGnKVS64HG_DxiEQVbIJBg', - ] as EventID[], - auth_events: [ - '$-C1Tf8UTaSZPEGcwVAUD1xGnKVS64HG_DxiEQVbIJBg', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', - ] as EventID[], - origin: 'syn1.tunnel.dev.rocket.chat', - unsigned: { - age_ts: 1759760138291, - }, - hashes: { - sha256: 'hmapBX++nvDz12pTvdDRzt62kuAeyMqw5h0Mta3YH/I', - }, - signatures: { - 'syn1.tunnel.dev.rocket.chat': { - 'ed25519:a_FAET': - 'enVzK6E2K5gC11j4+G5Z+8aezriR2/2P2qqWI7/S8Qhs03ON3vkj9owszdN+bPNBklGQC5YMFCKQRf+TXp+eDw', + hashes: { + sha256: 'hmapBX++nvDz12pTvdDRzt62kuAeyMqw5h0Mta3YH/I', }, - }, - } satisfies room.Pdu; - await stateService.handlePdu(toEventBase(message)); - const stateAfterMessage = await stateService.getStateBeforeEvent( - toEventBase(message), - ); - const newPduIds = Array.from(stateAfterMessage.values()) - .map((e) => e.eventId) - .sort(); - expect(newPduIds).toStrictEqual(expectedAfterMessageSent.pdu_ids); - }); - - it('should handle concurrent joins fairly and build correct final state', async () => { - const users = []; - for (let i = 0; i < 20; i++) { - users.push(`@user${i}:example.com`); - } - - const { roomCreateEvent } = await createRoom('public'); - const roomId = roomCreateEvent.roomId; - const version = roomCreateEvent.getContent().room_version; - - await Promise.all(users.map((u) => joinUser(roomId, u))); - - const state = await stateService.getLatestRoomState2(roomId); - - for (const user of users) { - expect(state.isUserInRoom(user)).toBeTrue(); - } - - // all users should also be able to send a message - for (const user of users) { - const message = await stateService.buildEvent<'m.room.message'>( - { - type: 'm.room.message', - sender: user as room.UserID, - content: { msgtype: 'm.text', body: 'hello world' }, - room_id: roomId, - ...getDefaultFields(), + signatures: { + 'syn1.tunnel.dev.rocket.chat': { + 'ed25519:a_FAET': + 'enVzK6E2K5gC11j4+G5Z+8aezriR2/2P2qqWI7/S8Qhs03ON3vkj9owszdN+bPNBklGQC5YMFCKQRf+TXp+eDw', + }, }, - version, + } satisfies room.Pdu; + await stateService.handlePdu(toEventBase(message)); + const stateAfterMessage = await stateService.getStateBeforeEvent( + toEventBase(message), ); + const newPduIds = Array.from(stateAfterMessage.values()) + .map((e) => e.eventId) + .sort(); + expect(newPduIds).toStrictEqual(expectedAfterMessageSent.pdu_ids); + }); - await stateService.handlePdu(message); - - const event = await stateService.getEvent(message.eventId); + it('should handle concurrent joins fairly and build correct final state', async () => { + const users = []; + for (let i = 0; i < 20; i++) { + users.push(`@user${i}:example.com`); + } - expect(event?.isAuthRejected()).toBeFalse(); - } - }); + const { roomCreateEvent } = await createRoom('public'); + const roomId = roomCreateEvent.roomId; + const version = roomCreateEvent.getContent().room_version; - const label = (label: string, i: number) => { - return `[${i}] ${label}`; - }; + await Promise.all(users.map((u) => joinUser(roomId, u))); - for (let i = 1; i <= partialStateEvents.length; i++) { - describe(label('partial states', i), () => { - it(label('should not be able to complete the chain', i), async () => { - const { state } = await partialStateEvents[i - 1](); - const eventMap = new Map(); - const events = Object.values(state); + const state = await stateService.getLatestRoomState2(roomId); - for (const event of events) { - eventMap.set(event.eventId, event); - } + for (const user of users) { + expect(state.isUserInRoom(user)).toBeTrue(); + } - const hasNoPartial = events.every((event) => - event.getPreviousEventIds().every((prev) => eventMap.has(prev)), + // all users should also be able to send a message + for (const user of users) { + const message = await stateService.buildEvent<'m.room.message'>( + { + type: 'm.room.message', + sender: user as room.UserID, + content: { msgtype: 'm.text', body: 'hello world' }, + room_id: roomId, + ...getDefaultFields(), + }, + version, ); - expect(hasNoPartial).toBeFalse(); - }); + await stateService.handlePdu(message); - it(label('should be able to save partial states', i), async () => { - const { state, authChain } = await partialStateEvents[i - 1](); - const events = Object.values(state); + const event = await stateService.getEvent(message.eventId); - const stateId = await stateService.processInitialState( - events.map((e) => e.event), - authChain.map((e) => e.event), - ); + expect(event?.isAuthRejected()).toBeFalse(); + } + }); + + const label = (label: string, i: number) => { + return `[${i}] ${label}`; + }; - console.log(state.ourUserJoinEvent.eventId); + for (let i = 1; i <= partialStateEvents.length; i++) { + describe(label('partial states', i), () => { + it(label('should not be able to complete the chain', i), async () => { + const { state } = await partialStateEvents[i - 1](); + const eventMap = new Map(); + const events = Object.values(state); - expect(stateId).toBeString(); + for (const event of events) { + eventMap.set(event.eventId, event); + } - const event = await stateService.getEvent( - state.ourUserJoinEvent.eventId, - ); - expect(event?.isPartial()).toBeTrue(); - }); + const hasNoPartial = events.every((event) => + event.getPreviousEventIds().every((prev) => eventMap.has(prev)), + ); - it(label( - 'should be able to save and detect partial states', - i, - ), async () => { - const { state, authChain } = await partialStateEvents[i - 1](); - const events = Object.values(state); + expect(hasNoPartial).toBeFalse(); + }); - await stateService.processInitialState( - events.map((e) => e.event), - authChain.map((e) => e.event), - ); + it(label('should be able to save partial states', i), async () => { + const { state, authChain } = await partialStateEvents[i - 1](); + const events = Object.values(state); - expect( - stateService.isRoomStatePartial(events[0].roomId), - ).resolves.toBeTrue(); - }); + const stateId = await stateService.processInitialState( + events.map((e) => e.event), + authChain.map((e) => e.event), + ); - it(label( - 'should complete the state as missing events get filled', - i, - ), async () => { - const { state, authChain, missingEvents } = - await partialStateEvents[i - 1](); + console.log(state.ourUserJoinEvent.eventId); - await stateService.processInitialState( - Object.values(state).map((e) => e.event), - authChain.map((e) => e.event), - ); + expect(stateId).toBeString(); - expect( - stateService.isRoomStatePartial(state.roomCreateEvent.roomId), - ).resolves.toBeTrue(); - - const eventStore = new Map(); - for (const e of (Object.values(state) as PersistentEventBase[]).concat( - missingEvents, - )) { - eventStore.set(e.eventId, e); - } + const event = await stateService.getEvent( + state.ourUserJoinEvent.eventId, + ); + expect(event?.isPartial()).toBeTrue(); + }); + + it(label( + 'should be able to save and detect partial states', + i, + ), async () => { + const { state, authChain } = await partialStateEvents[i - 1](); + const events = Object.values(state); + + await stateService.processInitialState( + events.map((e) => e.event), + authChain.map((e) => e.event), + ); - const eventsToWalk = await stateService.getPartialEvents( - state.creatorMembershipEvent.roomId, - ); + expect( + stateService.isRoomStatePartial(events[0].roomId), + ).resolves.toBeTrue(); + }); + + it(label( + 'should complete the state as missing events get filled', + i, + ), async () => { + const { state, authChain, missingEvents } = + await partialStateEvents[i - 1](); + + await stateService.processInitialState( + Object.values(state).map((e) => e.event), + authChain.map((e) => e.event), + ); - const store = stateService._getStore(state.roomCreateEvent.version); + expect( + stateService.isRoomStatePartial(state.roomCreateEvent.roomId), + ).resolves.toBeTrue(); - const remoteFetch = async (eventIds: EventID[]) => { - return eventIds.map((e) => eventStore.get(e)); - }; + const eventStore = new Map(); + for (const e of ( + Object.values(state) as PersistentEventBase[] + ).concat(missingEvents)) { + eventStore.set(e.eventId, e); + } - const walk = async (event: PersistentEventBase) => { - // for each previous event, walk - const previousEventsInStore = await store.getEvents( - event.getPreviousEventIds(), + const eventsToWalk = await stateService.getPartialEvents( + state.creatorMembershipEvent.roomId, ); - if ( - previousEventsInStore.length === event.getPreviousEventIds().length - ) { - console.log(`All previous events found in store ${event.eventId}`); - // start processing this event now - await stateService._resolveStateAtEvent(event); - return; - } - const eventIdsToFind = [] as EventID[]; - for (const previousEventId of event.getPreviousEventIds()) { + const store = stateService._getStore(state.roomCreateEvent.version); + + const remoteFetch = async (eventIds: EventID[]) => { + return eventIds.map((e) => eventStore.get(e)); + }; + + const walk = async (event: PersistentEventBase) => { + // for each previous event, walk + const previousEventsInStore = await store.getEvents( + event.getPreviousEventIds(), + ); if ( - !previousEventsInStore - .map((p) => p.eventId) - .includes(previousEventId) + previousEventsInStore.length === + event.getPreviousEventIds().length ) { - eventIdsToFind.push(previousEventId); + console.log( + `All previous events found in store ${event.eventId}`, + ); + // start processing this event now + await stateService._resolveStateAtEvent(event); + return; + } + + const eventIdsToFind = [] as EventID[]; + for (const previousEventId of event.getPreviousEventIds()) { + if ( + !previousEventsInStore + .map((p) => p.eventId) + .includes(previousEventId) + ) { + eventIdsToFind.push(previousEventId); + } } - } - console.log(`Events to find ${eventIdsToFind}`); + console.log(`Events to find ${eventIdsToFind}`); - const previousEvents = (await remoteFetch( - eventIdsToFind, - )) as PersistentEventBase[]; + const previousEvents = (await remoteFetch( + eventIdsToFind, + )) as PersistentEventBase[]; - expect(previousEvents.length).toBe( - event.getPreviousEventIds().length, - ); + expect(previousEvents.length).toBe( + event.getPreviousEventIds().length, + ); - previousEvents - .sort((e1, e2) => { - if (e1.depth !== e2.depth) { - return e1.depth - e2.depth; - } + previousEvents + .sort((e1, e2) => { + if (e1.depth !== e2.depth) { + return e1.depth - e2.depth; + } - if (e1.originServerTs !== e2.originServerTs) { - return e1.originServerTs - e2.originServerTs; - } + if (e1.originServerTs !== e2.originServerTs) { + return e1.originServerTs - e2.originServerTs; + } - return e1.eventId.localeCompare(e2.eventId); - }) - .reverse(); + return e1.eventId.localeCompare(e2.eventId); + }) + .reverse(); - for (const previousEvent of previousEvents) { - console.log(`Waling ${previousEvent.eventId}`); - await walk(previousEvent); - } + for (const previousEvent of previousEvents) { + console.log(`Waling ${previousEvent.eventId}`); + await walk(previousEvent); + } - console.log( - `Finishing saving ${event.eventId}, all [${event.getPreviousEventIds().join(', ')}] events has been saved`, - ); + console.log( + `Finishing saving ${event.eventId}, all [${event.getPreviousEventIds().join(', ')}] events has been saved`, + ); - // once all previous events have been walked we process this event - await stateService._resolveStateAtEvent(event); - }; + // once all previous events have been walked we process this event + await stateService._resolveStateAtEvent(event); + }; - for (const event of eventsToWalk) { - console.log(`Starting walking ${event.eventId}`); - await walk(event).catch(console.error); - } + for (const event of eventsToWalk) { + console.log(`Starting walking ${event.eventId}`); + await walk(event).catch(console.error); + } - // now room should state to not be in partial state - expect( - stateService.isRoomStatePartial(state.roomCreateEvent.roomId), - ).resolves.toBeFalse(); + // now room should state to not be in partial state + expect( + stateService.isRoomStatePartial(state.roomCreateEvent.roomId), + ).resolves.toBeFalse(); + }); }); - }); - } -}); + } + }), +); From 84f5e49197fb65a23e41e67e6827a43ad9d7a6f7 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 14:12:00 +0530 Subject: [PATCH 35/56] refactor: better error handling and remove server service --- packages/crypto/src/index.ts | 1 + packages/crypto/src/keys/ed25519.ts | 3 +- packages/crypto/src/utils/data-types.ts | 4 + packages/federation-sdk/src/index.ts | 2 +- packages/federation-sdk/src/sdk.ts | 8 +- .../services/event-authorization.service.ts | 57 +++------- .../src/services/key.service.ts | 8 ++ .../src/services/server.service.ts | 91 ---------------- .../signature-verification.service.ts | 101 +++++++++++++++--- .../federation/profiles.controller.ts | 3 +- .../src/middlewares/isAuthenticated.ts | 61 ++++++----- 11 files changed, 156 insertions(+), 183 deletions(-) delete mode 100644 packages/federation-sdk/src/services/server.service.ts diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index 904d39f35..19438ec71 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -6,6 +6,7 @@ export { toBinaryData, fromBinaryData, fromBase64ToBytes, + InvalidSignatureError, } from './utils/data-types'; export { isValidAlgorithm } from './utils/constants'; diff --git a/packages/crypto/src/keys/ed25519.ts b/packages/crypto/src/keys/ed25519.ts index 70cc65906..05aa478b9 100644 --- a/packages/crypto/src/keys/ed25519.ts +++ b/packages/crypto/src/keys/ed25519.ts @@ -6,6 +6,7 @@ import { ed25519PublicKeyRawToPem, } from '../rfc/8410/ed25519-pem'; import { EncryptionValidAlgorithm } from '../utils/constants'; +import { InvalidSignatureError } from '../utils/data-types'; export class Ed25519VerifierKeyImpl implements VerifierKey { algorithm = EncryptionValidAlgorithm.ed25519; @@ -44,7 +45,7 @@ export class Ed25519VerifierKeyImpl implements VerifierKey { } else if (verified) { resolve(); } else { - reject(new Error('Invalid signature')); + reject(new InvalidSignatureError()); } }, ); diff --git a/packages/crypto/src/utils/data-types.ts b/packages/crypto/src/utils/data-types.ts index d53537dce..f9a32e849 100644 --- a/packages/crypto/src/utils/data-types.ts +++ b/packages/crypto/src/utils/data-types.ts @@ -102,3 +102,7 @@ export function encodeCanonicalJson(value: unknown): string { export function fromBase64ToBytes(base64: string): Uint8Array { return Buffer.from(base64, 'base64'); } + +export class InvalidSignatureError extends Error { + name = 'InvalidSignatureError'; +} diff --git a/packages/federation-sdk/src/index.ts b/packages/federation-sdk/src/index.ts index 7e18cb284..e0415d82c 100644 --- a/packages/federation-sdk/src/index.ts +++ b/packages/federation-sdk/src/index.ts @@ -221,7 +221,7 @@ export type HomeserverEventSignatures = { content: { membership: Membership; displayname?: string; - avatar_url?: string; + avatar_url?: string | null; reason?: string; }; }; diff --git a/packages/federation-sdk/src/sdk.ts b/packages/federation-sdk/src/sdk.ts index f63831ec2..d6c0faca2 100644 --- a/packages/federation-sdk/src/sdk.ts +++ b/packages/federation-sdk/src/sdk.ts @@ -14,10 +14,10 @@ import { MessageService } from './services/message.service'; import { ProfilesService } from './services/profiles.service'; import { RoomService } from './services/room.service'; import { SendJoinService } from './services/send-join.service'; -import { ServerService } from './services/server.service'; import { StateService } from './services/state.service'; import { WellKnownService } from './services/well-known.service'; import { SignatureVerificationService } from './services/signature-verification.service'; +import { KeyService } from './services/key.service'; // create a federation sdk class to export @singleton() @@ -28,7 +28,6 @@ export class FederationSDK { private readonly inviteService: InviteService, private readonly eventService: EventService, private readonly eduService: EduService, - private readonly serverService: ServerService, private readonly configService: ConfigService, private readonly eventAuthorizationService: EventAuthorizationService, private readonly stateService: StateService, @@ -39,6 +38,7 @@ export class FederationSDK { private readonly federationRequestService: FederationRequestService, private readonly federationService: FederationService, private readonly signatureVerficationService: SignatureVerificationService, + private readonly keyService: KeyService, ) {} createDirectMessageRoom( @@ -118,9 +118,9 @@ export class FederationSDK { } getSignedServerKey( - ...args: Parameters + ...args: Parameters ) { - return this.serverService.getSignedServerKey(...args); + return this.keyService.getSignedServerKey(...args); } getConfig(config: K): AppConfig[K] { diff --git a/packages/federation-sdk/src/services/event-authorization.service.ts b/packages/federation-sdk/src/services/event-authorization.service.ts index 4045bbc95..015239c0a 100644 --- a/packages/federation-sdk/src/services/event-authorization.service.ts +++ b/packages/federation-sdk/src/services/event-authorization.service.ts @@ -13,8 +13,12 @@ import { delay, inject, singleton } from 'tsyringe'; import { UploadRepository } from '../repositories/upload.repository'; import { ConfigService } from './config.service'; import { EventService } from './event.service'; -import { ServerService } from './server.service'; import { StateService } from './state.service'; +import { + FailedSignatureVerificationPreconditionError, + InvalidRequestSignatureError, + SignatureVerificationService, +} from './signature-verification.service'; export class AclDeniedError extends Error { constructor(serverName: string, roomId: string) { @@ -31,9 +35,9 @@ export class EventAuthorizationService { private readonly stateService: StateService, private readonly eventService: EventService, private readonly configService: ConfigService, - private readonly serverService: ServerService, @inject(delay(() => UploadRepository)) private readonly uploadRepository: UploadRepository, + private readonly signatureVerificationService: SignatureVerificationService, ) {} async authorizeEvent(event: Pdu, authEvents: Pdu[]): Promise { @@ -135,51 +139,22 @@ export class EventAuthorizationService { } try { - const { origin, destination, key, signature } = - extractSignaturesFromHeader(authorizationHeader); - - if ( - !origin || - !key || - !signature || - (destination && destination !== this.configService.serverName) - ) { - return; - } - - const [algorithm] = key.split(':'); - if (algorithm !== 'ed25519') { - return; - } - - const publicKey = await this.serverService.getPublicKey(origin, key); - if (!publicKey) { - this.logger.warn(`Could not fetch public key for ${origin}:${key}`); - return; - } - - const actualDestination = destination || this.configService.serverName; - const isValid = await validateAuthorizationHeader( - origin, - publicKey, - actualDestination, + return await this.signatureVerificationService.verifyRequestSignature({ + authorizationHeader, method, uri, - signature, body, - ); - if (!isValid) { - this.logger.warn(`Invalid signature from ${origin}`); + }); + } catch (error) { + if ( + error instanceof InvalidRequestSignatureError || + error instanceof FailedSignatureVerificationPreconditionError + ) { + this.logger.warn('Invalid request signature'); return; } - return origin; - } catch (error) { - this.logger.error({ - msg: 'Error verifying request signature', - err: error, - }); - return; + throw error; } } diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index ef520ac8b..1b5b43e14 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -602,4 +602,12 @@ export class KeyService { `No valid signature keys found for origin ${event.origin} with supported algorithms`, ); } + + async getSignedServerKey() { + return this.convertToKeyV2Response( + await this.keyRepository + .findByServerName(this.configService.serverName) + .toArray(), + ); + } } diff --git a/packages/federation-sdk/src/services/server.service.ts b/packages/federation-sdk/src/services/server.service.ts deleted file mode 100644 index 50c987a89..000000000 --- a/packages/federation-sdk/src/services/server.service.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - type SigningKey, - getPublicKeyFromRemoteServer, - signJson, - toUnpaddedBase64, -} from '@rocket.chat/federation-core'; -import { delay, inject, singleton } from 'tsyringe'; -import { ServerRepository } from '../repositories/server.repository'; -import { ConfigService } from './config.service'; - -@singleton() -export class ServerService { - constructor( - @inject(delay(() => ServerRepository)) - private readonly serverRepository: ServerRepository, - private configService: ConfigService, - ) {} - - async getValidPublicKeyFromLocal( - origin: string, - key: string, - ): Promise { - return await this.serverRepository.getValidPublicKeyFromLocal(origin, key); - } - - async storePublicKey( - origin: string, - key: string, - value: string, - validUntil: number, - ): Promise { - await this.serverRepository.storePublicKey(origin, key, value, validUntil); - } - - async getPublicKey(origin: string, key: string): Promise { - if (origin === this.configService.serverName) { - return this.configService.getPublicSigningKeyBase64(); - } - - const localPublicKey = - await this.serverRepository.getValidPublicKeyFromLocal(origin, key); - if (localPublicKey) { - return localPublicKey; - } - - const { key: remotePublicKey, validUntil } = - await getPublicKeyFromRemoteServer( - origin, - this.configService.serverName, - key, - ); - - if (!remotePublicKey) { - throw new Error('Could not get public key from remote server'); - } - - await this.storePublicKey(origin, key, remotePublicKey, validUntil); - return remotePublicKey; - } - - async getSignedServerKey() { - const signer = await this.configService.getSigningKey(); - - const keys = { - [signer.id]: { - key: toUnpaddedBase64(signer.getPublicKey()), - }, - }; - - const response = { - old_verify_keys: {}, - server_name: this.configService.serverName, - // TODO: what should this actually be and how to handle the expiration - valid_until_ts: new Date().getTime() + 60 * 60 * 24 * 1000, // 1 day - verify_keys: keys, - }; - - const responseSignature = await signJson(response, signer); - - const signatures = { - [this.configService.serverName]: { - [signer.id]: responseSignature, - }, - }; - - return { - ...response, - signatures, - }; - } -} diff --git a/packages/federation-sdk/src/services/signature-verification.service.ts b/packages/federation-sdk/src/services/signature-verification.service.ts index a4b4c0091..aedce28e2 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.ts @@ -1,5 +1,6 @@ import { createLogger } from '@rocket.chat/federation-core'; import { + InvalidSignatureError, VerifierKey, encodeCanonicalJson, fromBase64ToBytes, @@ -9,6 +10,18 @@ import type { PersistentEventBase } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; import { KeyService } from './key.service'; +export class InvalidEventSignatureError extends InvalidSignatureError { + name = 'InvalidEventSignatureError'; +} + +export class InvalidRequestSignatureError extends InvalidSignatureError { + name = 'InvalidRequestSignatureError'; +} + +export class FailedSignatureVerificationPreconditionError extends Error { + name = 'FailedSignatureVerificationPreconditionError'; +} + // low cost optimization in case of bad implementations // ed25519 signatures in unpaddedbase64 are always 86 characters long (doing math here for future reference) // 64 bytes in general, base64 => 21*3 = 63 + 1 padding "==" => 21 * 4 = 84 + 2 padding "==" (each char one byte) => 88 characters @@ -50,19 +63,49 @@ export class SignatureVerificationService { const { redactedEvent, origin } = event; if (!origin) { - throw new Error( + throw new InvalidEventSignatureError( `Invalid event sender, unable to find origin part from it ${event.sender}`, ); } const { unsigned: _, ...toCheck } = redactedEvent; - if (verifier) return this.verifySignature(toCheck, origin, verifier); + if (verifier) { + try { + await this.verifySignature(toCheck, origin, verifier); + } catch (error) { + if (error instanceof InvalidSignatureError) { + throw new InvalidEventSignatureError( + `Invalid event signature for event ${event.eventId} from origin ${origin}: ${error.message}`, + ); + } + + throw error; + } + } - const { key: requiredVerifier } = + const requiredVerifier = await this.keyService.getRequiredVerifierForEvent(event); - return this.verifySignature(toCheck, origin, requiredVerifier); + if ( + this.keyService.isVerifierAllowedToCheckEvent(event, requiredVerifier) + ) { + try { + await this.verifySignature(toCheck, origin, requiredVerifier.key); + } catch (error) { + if (error instanceof InvalidSignatureError) { + throw new InvalidEventSignatureError( + `Invalid event signature for event ${event.eventId} from origin ${origin}: ${error.message}`, + ); + } + + throw error; + } + } + + throw new FailedSignatureVerificationPreconditionError( + `The verifier with id ${requiredVerifier.key.id} is not allowed to verify the event ${event.eventId} from origin ${origin}`, + ); } async verifyRequestSignature( @@ -96,11 +139,15 @@ export class SignatureVerificationService { if (Object.keys(rest).length) { // it should never happen since the regex should match all the parameters - throw new Error('Invalid authorization header, unexpected parameters'); + throw new FailedSignatureVerificationPreconditionError( + 'Invalid authorization header, unexpected parameters', + ); } if ([origin, destination, key, signature].some((value) => !value)) { - throw new Error('Invalid authorization header'); + throw new FailedSignatureVerificationPreconditionError( + 'Invalid authorization header', + ); } /* @@ -129,7 +176,18 @@ export class SignatureVerificationService { }; if (verifier) { - return this.verifySignature(toVerify, origin, verifier); + try { + await this.verifySignature(toVerify, origin, verifier); + return origin; + } catch (error) { + if (error instanceof InvalidSignatureError) { + throw new InvalidRequestSignatureError( + `Invalid request signature from origin ${origin}: ${error.message}`, + ); + } + + throw error; + } } const requiredVerifier = await this.keyService.getRequestVerifier( @@ -137,7 +195,18 @@ export class SignatureVerificationService { key, ); - return this.verifySignature(toVerify, origin, requiredVerifier); + try { + await this.verifySignature(toVerify, origin, requiredVerifier); + return origin; + } catch (error) { + if (error instanceof InvalidSignatureError) { + throw new InvalidRequestSignatureError( + `Invalid request signature from origin ${origin}: ${error.message}`, + ); + } + + throw error; + } } /** @@ -151,7 +220,9 @@ export class SignatureVerificationService { // 1. Checks if the signatures member of the object contains an entry with the name of the entity. If the entry is missing then the check fails. const originSignature = data.signatures?.[origin]; if (!originSignature) { - throw new Error(`No signature found for origin ${origin}`); + throw new FailedSignatureVerificationPreconditionError( + `No signature found for origin ${origin}`, + ); } // 2. Removes any signing key identifiers from the entry with algorithms it doesn’t understand. If there are no signing key identifiers left then the check fails. @@ -174,7 +245,7 @@ export class SignatureVerificationService { validSignatureEntries.set(keyId, signature); } if (validSignatureEntries.size === 0) { - throw new Error( + throw new FailedSignatureVerificationPreconditionError( `No valid signature keys found for origin ${origin} with supported algorithms`, ); } @@ -183,7 +254,7 @@ export class SignatureVerificationService { // one origin can sign with multiple keys - given how the spec AND the schema structures it. // we do NOT need all though, one is enough, one that we can fetch first if (!validSignatureEntries.has(verifier.id)) { - throw new Error( + throw new FailedSignatureVerificationPreconditionError( `No valid verification key found for origin ${origin} with supported algorithms`, ); } @@ -192,26 +263,26 @@ export class SignatureVerificationService { // we needed to know which key to use to know which signature to decode. const signatureEntry: string = originSignature[verifier.id]; if (!signatureEntry) { - throw new Error( + throw new FailedSignatureVerificationPreconditionError( `No signature entry found for keyId ${verifier.id} from origin ${origin}`, ); } if (signatureEntry.length !== MAX_SIGNATURE_LENGTH_FOR_ED25519) { - throw new Error( + throw new FailedSignatureVerificationPreconditionError( `Invalid signature length for keyId ${verifier.id} from origin ${origin}, expected ${MAX_SIGNATURE_LENGTH_FOR_ED25519} got ${signatureEntry.length} characters`, ); } const signatureBytes = fromBase64ToBytes(signatureEntry); if (signatureBytes.byteLength === 0) { - throw new Error( + throw new FailedSignatureVerificationPreconditionError( `Failed to decode base64 signature for keyId ${verifier.id} from origin ${origin}`, ); } // 5. Removes the signatures and unsigned members of the object. - const { signatures, ...rest } = data; + const { signatures: _, ...rest } = data; // 6. Encodes the remainder of the JSON object using the Canonical JSON encoding. const canonicalJson = encodeCanonicalJson(rest); diff --git a/packages/homeserver/src/controllers/federation/profiles.controller.ts b/packages/homeserver/src/controllers/federation/profiles.controller.ts index 4516b337d..4c910eb8e 100644 --- a/packages/homeserver/src/controllers/federation/profiles.controller.ts +++ b/packages/homeserver/src/controllers/federation/profiles.controller.ts @@ -96,7 +96,8 @@ export const profilesPlugin = (app: Elysia) => { .use(canAccessResourceMiddleware('room')) .get( '/_matrix/federation/v1/make_join/:roomId/:userId', - async ({ params, query: _query }) => { + // FIXME: why complaining now? + async ({ params, query: _query }: any) => { const { roomId, userId } = params; // const { ver } = query; diff --git a/packages/homeserver/src/middlewares/isAuthenticated.ts b/packages/homeserver/src/middlewares/isAuthenticated.ts index 22e2bf3bd..2e0e67987 100644 --- a/packages/homeserver/src/middlewares/isAuthenticated.ts +++ b/packages/homeserver/src/middlewares/isAuthenticated.ts @@ -1,5 +1,9 @@ import { federationSDK } from '@rocket.chat/federation-sdk'; import Elysia from 'elysia'; +import { + FailedSignatureVerificationPreconditionError, + InvalidRequestSignatureError, +} from '../../../federation-sdk/src/services/signature-verification.service'; export const isAuthenticatedMiddleware = () => { return new Elysia({ @@ -18,44 +22,43 @@ export const isAuthenticatedMiddleware = () => { }; } - try { - let body: Record | undefined; - if (request.body) { - try { - const clone = request.clone(); - const text = await clone.text(); - body = text ? JSON.parse(text) : undefined; - } catch { - body = undefined; - } + let body: Record | undefined; + if (request.body) { + try { + const clone = request.clone(); + const text = await clone.text(); + body = text ? JSON.parse(text) : undefined; + } catch { + body = undefined; } + } - try { - await federationSDK.verifyRequestSignature({ - authorizationHeader, - method, - uri, - body, - }); - } catch (error) { - // TODO: log better - console.error(error); + try { + await federationSDK.verifyRequestSignature({ + authorizationHeader, + method, + uri, + body, + }); + } catch (error) { + console.error('Signature verification error:', error); + if ( + error instanceof FailedSignatureVerificationPreconditionError || + error instanceof InvalidRequestSignatureError + ) { set.status = 401; - return { - authenticatedServer: undefined, - }; + } else { + set.status = 500; } - return { - authenticatedServer: true, - }; - } catch (error) { - console.error('Authentication error:', error); - set.status = 500; return { authenticatedServer: undefined, }; } + + return { + authenticatedServer: true, + }; }) .onBeforeHandle(({ authenticatedServer, set }) => { if (!authenticatedServer) { From 1f17ab053539fbf95cd3264721d831ec26bb788f Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 14:13:12 +0530 Subject: [PATCH 36/56] refactor: rename own server key mmethod to more accurate one --- packages/federation-sdk/src/sdk.ts | 6 +++--- packages/federation-sdk/src/services/key.service.ts | 2 +- .../homeserver/src/controllers/key/server.controller.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/federation-sdk/src/sdk.ts b/packages/federation-sdk/src/sdk.ts index d6c0faca2..2f36b856d 100644 --- a/packages/federation-sdk/src/sdk.ts +++ b/packages/federation-sdk/src/sdk.ts @@ -117,10 +117,10 @@ export class FederationSDK { return this.eduService.sendTypingNotification(...args); } - getSignedServerKey( - ...args: Parameters + getOwnSignedServerKeyResponse( + ...args: Parameters ) { - return this.keyService.getSignedServerKey(...args); + return this.keyService.getOwnSignedServerKeyResponse(...args); } getConfig(config: K): AppConfig[K] { diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index 1b5b43e14..4bc92a5ef 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -603,7 +603,7 @@ export class KeyService { ); } - async getSignedServerKey() { + async getOwnSignedServerKeyResponse() { return this.convertToKeyV2Response( await this.keyRepository .findByServerName(this.configService.serverName) diff --git a/packages/homeserver/src/controllers/key/server.controller.ts b/packages/homeserver/src/controllers/key/server.controller.ts index b1877620b..a5c3bbc83 100644 --- a/packages/homeserver/src/controllers/key/server.controller.ts +++ b/packages/homeserver/src/controllers/key/server.controller.ts @@ -6,7 +6,7 @@ export const serverKeyPlugin = (app: Elysia) => { return app.get( '/_matrix/key/v2/server', async () => { - return federationSDK.getSignedServerKey(); + return federationSDK.getOwnSignedServerKeyResponse(); }, { response: { From 9ee83a0522b6f383c282697fedaf58ea05a4b32f Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 14:21:07 +0530 Subject: [PATCH 37/56] fix: keyservice.getownserverkeyresponse --- .../src/services/key.service.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index 4bc92a5ef..eff3b4b2b 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -209,7 +209,7 @@ export class KeyService { // multiple keys -> single repnse private async convertToKeyV2Response( - serverKeys: ServerKey[], + serverKeys: Omit[], minimumValidUntil = Date.now(), ): Promise { if (!this.signer) { @@ -604,10 +604,25 @@ export class KeyService { } async getOwnSignedServerKeyResponse() { - return this.convertToKeyV2Response( - await this.keyRepository - .findByServerName(this.configService.serverName) - .toArray(), - ); + // TODO: for old keys, should be saved and code below will make more sense + // return this.convertToKeyV2Response( + // await this.keyRepository + // .findByServerName(this.configService.serverName) + // .toArray(), + // ); + if (!this.signer) { + throw new Error('Signing key not configured'); + } + + return this.convertToKeyV2Response([ + { + serverName: this.configService.serverName, + keyId: this.signer.id, + key: this.signer.getPublicKey().toBase64(), + + // TODO: this isn't currently in config, nor do we handle expiration yet + expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year expiration + }, + ]); } } From 33df5120552e1baf7f39cbd369d5e78b535c94d7 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 14:26:39 +0530 Subject: [PATCH 38/56] fix: linter complains --- packages/crypto/src/index.spec.ts | 2 -- .../src/repositories/event-staging.repository.ts | 2 +- packages/federation-sdk/src/sdk.ts | 4 ++-- packages/federation-sdk/src/services/config.service.ts | 2 +- .../src/services/event-authorization.service.ts | 2 +- packages/federation-sdk/src/services/event.service.spec.ts | 2 +- packages/federation-sdk/src/services/key.service.spec.ts | 2 +- .../src/services/signature-verification.service.spec.ts | 4 ++-- packages/federation-sdk/src/services/state.service.spec.ts | 4 ++-- packages/federation-sdk/src/services/state.service.ts | 2 +- 10 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/crypto/src/index.spec.ts b/packages/crypto/src/index.spec.ts index cbe7d33d5..c689ddcd7 100644 --- a/packages/crypto/src/index.spec.ts +++ b/packages/crypto/src/index.spec.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'bun:test'; import { fromBase64ToBytes } from './utils/data-types'; import { - loadEd25519SignerFromSeed, - loadEd25519VerifierFromPublicKey, loadEd25519SignerFromSeed, loadEd25519VerifierFromPublicKey, signJson, diff --git a/packages/federation-sdk/src/repositories/event-staging.repository.ts b/packages/federation-sdk/src/repositories/event-staging.repository.ts index 344e3c546..0f2459aa3 100644 --- a/packages/federation-sdk/src/repositories/event-staging.repository.ts +++ b/packages/federation-sdk/src/repositories/event-staging.repository.ts @@ -1,5 +1,5 @@ -import { type EventID, Pdu } from '@rocket.chat/federation-room'; import type { EventStagingStore } from '@rocket.chat/federation-core'; +import { type EventID, Pdu } from '@rocket.chat/federation-room'; import type { Collection, DeleteResult, UpdateResult } from 'mongodb'; import { inject, singleton } from 'tsyringe'; diff --git a/packages/federation-sdk/src/sdk.ts b/packages/federation-sdk/src/sdk.ts index 2f36b856d..b488169cf 100644 --- a/packages/federation-sdk/src/sdk.ts +++ b/packages/federation-sdk/src/sdk.ts @@ -9,15 +9,15 @@ import { EventService } from './services/event.service'; import { FederationRequestService } from './services/federation-request.service'; import { FederationService } from './services/federation.service'; import { InviteService } from './services/invite.service'; +import { KeyService } from './services/key.service'; import { MediaService } from './services/media.service'; import { MessageService } from './services/message.service'; import { ProfilesService } from './services/profiles.service'; import { RoomService } from './services/room.service'; import { SendJoinService } from './services/send-join.service'; +import { SignatureVerificationService } from './services/signature-verification.service'; import { StateService } from './services/state.service'; import { WellKnownService } from './services/well-known.service'; -import { SignatureVerificationService } from './services/signature-verification.service'; -import { KeyService } from './services/key.service'; // create a federation sdk class to export @singleton() diff --git a/packages/federation-sdk/src/services/config.service.ts b/packages/federation-sdk/src/services/config.service.ts index 0618d9271..43a67f68a 100644 --- a/packages/federation-sdk/src/services/config.service.ts +++ b/packages/federation-sdk/src/services/config.service.ts @@ -2,9 +2,9 @@ import { createLogger } from '@rocket.chat/federation-core'; import { singleton } from 'tsyringe'; import { + Signer, fromBase64ToBytes, loadEd25519SignerFromSeed, - Signer, } from '@rocket.chat/federation-crypto'; import { z } from 'zod'; diff --git a/packages/federation-sdk/src/services/event-authorization.service.ts b/packages/federation-sdk/src/services/event-authorization.service.ts index 015239c0a..3d67260c5 100644 --- a/packages/federation-sdk/src/services/event-authorization.service.ts +++ b/packages/federation-sdk/src/services/event-authorization.service.ts @@ -13,12 +13,12 @@ import { delay, inject, singleton } from 'tsyringe'; import { UploadRepository } from '../repositories/upload.repository'; import { ConfigService } from './config.service'; import { EventService } from './event.service'; -import { StateService } from './state.service'; import { FailedSignatureVerificationPreconditionError, InvalidRequestSignatureError, SignatureVerificationService, } from './signature-verification.service'; +import { StateService } from './state.service'; export class AclDeniedError extends Error { constructor(serverName: string, roomId: string) { diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts index a575227b0..b2326623e 100644 --- a/packages/federation-sdk/src/services/event.service.spec.ts +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -19,11 +19,11 @@ import { RoomID, UserID, } from '@rocket.chat/federation-room'; +import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; import { config } from '../__mocks__/config.service.spec'; import { repositories } from '../__mocks__/repositories.spec'; import { eventService } from '../__mocks__/services.spec'; import { StateService } from './state.service'; -import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; const event = { auth_events: [ diff --git a/packages/federation-sdk/src/services/key.service.spec.ts b/packages/federation-sdk/src/services/key.service.spec.ts index 8550b2ad7..9ad8c0af5 100644 --- a/packages/federation-sdk/src/services/key.service.spec.ts +++ b/packages/federation-sdk/src/services/key.service.spec.ts @@ -3,10 +3,10 @@ import { verifyJsonSignature } from '@rocket.chat/federation-crypto'; import { Mock, describe, expect, it, mock } from 'bun:test'; import { afterEach, beforeEach } from 'node:test'; +import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; import { config } from '../__mocks__/config.service.spec'; import { keyService } from '../__mocks__/services.spec'; import { signer } from '../__mocks__/singer.spec'; -import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; runIfMongoExists(() => describe('KeyService', async () => { diff --git a/packages/federation-sdk/src/services/signature-verification.service.spec.ts b/packages/federation-sdk/src/services/signature-verification.service.spec.ts index d33af663f..5ba584491 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.spec.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.spec.ts @@ -19,9 +19,9 @@ import { RoomID, UserID, } from '@rocket.chat/federation-room'; -import { SignatureVerificationService } from './signature-verification.service'; -import { keyService } from '../__mocks__/services.spec'; import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; +import { keyService } from '../__mocks__/services.spec'; +import { SignatureVerificationService } from './signature-verification.service'; const originServer = 'syn1.tunnel.dev.rocket.chat'; diff --git a/packages/federation-sdk/src/services/state.service.spec.ts b/packages/federation-sdk/src/services/state.service.spec.ts index 7f9e0bf46..15c7cde9f 100644 --- a/packages/federation-sdk/src/services/state.service.spec.ts +++ b/packages/federation-sdk/src/services/state.service.spec.ts @@ -14,6 +14,8 @@ import { StateMapKey, } from '@rocket.chat/federation-room'; import { type WithId } from 'mongodb'; +import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; +import { signer } from '../__mocks__/singer.spec'; import { EventRepository } from '../repositories/event.repository'; import { StateGraphRepository, @@ -22,8 +24,6 @@ import { import { type ConfigService } from './config.service'; import { DatabaseConnectionService } from './database-connection.service'; import { StateService } from './state.service'; -import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; -import { signer } from '../__mocks__/singer.spec'; type State = Map; diff --git a/packages/federation-sdk/src/services/state.service.ts b/packages/federation-sdk/src/services/state.service.ts index fb21fcd55..86787459c 100644 --- a/packages/federation-sdk/src/services/state.service.ts +++ b/packages/federation-sdk/src/services/state.service.ts @@ -1,3 +1,4 @@ +import { createLogger } from '@rocket.chat/federation-core'; import { signJson } from '@rocket.chat/federation-crypto'; import { type EventID, @@ -25,7 +26,6 @@ import { delay, inject, singleton } from 'tsyringe'; import { EventRepository } from '../repositories/event.repository'; import { StateGraphRepository } from '../repositories/state-graph.repository'; import { ConfigService } from './config.service'; -import { createLogger } from '@rocket.chat/federation-core'; type State = Map; From 57276c84fd8c5c59a7a75b56f460a670769d56ce Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 14:33:36 +0530 Subject: [PATCH 39/56] fix: broken tests --- .../src/services/signature-verification.service.spec.ts | 4 ++-- .../src/services/signature-verification.service.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/federation-sdk/src/services/signature-verification.service.spec.ts b/packages/federation-sdk/src/services/signature-verification.service.spec.ts index 5ba584491..b2a3abfa7 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.spec.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.spec.ts @@ -152,7 +152,7 @@ runIfMongoExists(() => }, thisVerifier, ), - ).resolves.toBeUndefined(); + ).resolves.toBeString(); }); }); @@ -262,7 +262,7 @@ runIfMongoExists(() => await expect( service.verifyEventSignature(pdu2, verifier), - ).rejects.toThrow('Invalid signature'); + ).rejects.toThrow(/Invalid event signature/); }); }); }), diff --git a/packages/federation-sdk/src/services/signature-verification.service.ts b/packages/federation-sdk/src/services/signature-verification.service.ts index aedce28e2..a5f8958f6 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.ts @@ -73,6 +73,7 @@ export class SignatureVerificationService { if (verifier) { try { await this.verifySignature(toCheck, origin, verifier); + return; } catch (error) { if (error instanceof InvalidSignatureError) { throw new InvalidEventSignatureError( @@ -92,6 +93,7 @@ export class SignatureVerificationService { ) { try { await this.verifySignature(toCheck, origin, requiredVerifier.key); + return; } catch (error) { if (error instanceof InvalidSignatureError) { throw new InvalidEventSignatureError( From dca5761717e52a6491936dee70a5ca9f4a4e6884 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 14:37:10 +0530 Subject: [PATCH 40/56] fix: crypto thrown error type --- packages/crypto/src/utils/utils.spec.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/crypto/src/utils/utils.spec.ts b/packages/crypto/src/utils/utils.spec.ts index 15b6aa67e..6821f71ac 100644 --- a/packages/crypto/src/utils/utils.spec.ts +++ b/packages/crypto/src/utils/utils.spec.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'bun:test'; -import { encodeCanonicalJson, fromBase64ToBytes } from './data-types'; +import { + InvalidSignatureError, + encodeCanonicalJson, + fromBase64ToBytes, +} from './data-types'; import { loadEd25519SignerFromSeed, loadEd25519VerifierFromPublicKey, @@ -99,8 +103,8 @@ describe('Signing and verifying payloads', async () => { '0', ); - expect(verifyJsonSignature({}, signature, verifier)).rejects.toThrowError( - 'Invalid signature', + expect(verifyJsonSignature({}, signature, verifier)).rejects.toThrow( + InvalidSignatureError, ); }); From 8c4d820490639c07dcc1b10ffef3158f1041557e Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 14:40:03 +0530 Subject: [PATCH 41/56] chore: workflow file formatting --- .github/workflows/ci.yml | 72 ++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e28af18a9..8f77b305a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,40 +1,40 @@ on: - release: - types: [published] - pull_request: - branches: "**" - paths-ignore: - - "**.md" - push: - branches: - - main - paths-ignore: - - "**.md" + release: + types: [published] + pull_request: + branches: "**" + paths-ignore: + - "**.md" + push: + branches: + - main + paths-ignore: + - "**.md" name: my-workflow jobs: - unit-tests: - name: Code Quality Checks(lint, test, tsc) - runs-on: ubuntu-latest - steps: - # ... - - uses: actions/checkout@v4 - - name: Cache turbo build setup - uses: actions/cache@v4 - with: - path: .turbo - key: ${{ runner.os }}-turbo-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-turbo- - - uses: oven-sh/setup-bun@v2 - - run: bun install - - run: bun run build - - run: bun lint:ci - - run: bun tsc --noEmit - - uses: supercharge/mongodb-github-action@1.12.0 - - run: LOG_LEVEL=debug bun run test:withMongo - - run: LOG_LEVEL=debug bun test:coverage - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} + unit-tests: + name: Code Quality Checks(lint, test, tsc) + runs-on: ubuntu-latest + steps: + # ... + - uses: actions/checkout@v4 + - name: Cache turbo build setup + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + - uses: oven-sh/setup-bun@v2 + - run: bun install + - run: bun run build + - run: bun lint:ci + - run: bun tsc --noEmit + - uses: supercharge/mongodb-github-action@1.12.0 + - run: LOG_LEVEL=debug bun run test:withMongo + - run: LOG_LEVEL=debug bun test:coverage + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} From d62c230754c35e9f6070070c2677dfb5ab918824 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Wed, 5 Nov 2025 14:43:01 +0530 Subject: [PATCH 42/56] update lock file --- bun.lock | 2291 ----------------------------------------------------- bun.lockb | Bin 78800 -> 98312 bytes 2 files changed, 2291 deletions(-) delete mode 100644 bun.lock diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 9b89967f5..000000000 --- a/bun.lock +++ /dev/null @@ -1,2291 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "homeserver", - "dependencies": { - "dotenv": "^16.5.0", - "memoize": "^10.1.0", - "reflect-metadata": "^0.2.2", - "rollup": "^4.52.4", - "rollup-plugin-dts": "^6.2.3", - "tsyringe": "^4.10.0", - "tweetnacl": "^1.0.3" - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@types/bun": "latest", - "@types/express": "^5.0.1", - "@types/node": "^22.15.18", - "@types/sinon": "^17.0.4", - "husky": "^9.1.7", - "lint-staged": "^16.1.2", - "sinon": "^20.0.0", - "tsconfig-paths": "^4.2.0", - "turbo": "~2.5.6", - "typescript": "~5.9.2" - } - }, - "packages/core": { - "name": "@rocket.chat/federation-core", - "version": "1.0.50", - "dependencies": { - "@rocket.chat/federation-crypto": "workspace:*", - "@rocket.chat/federation-room": "workspace:*", - "pino": "^8.21.0" - }, - "devDependencies": { - "bun-types": "latest", - "pino-pretty": "^13.1.1", - "ts-node": "^10.9.2", - "ts-patch": "^3.1.2", - "typescript": "~5.9.2" - } - }, - "packages/crypto": { - "name": "@rocket.chat/federation-crypto", - "version": "0.0.1", - "dependencies": { - "@noble/ed25519": "^3.0.0" - }, - "devDependencies": { - "bun-types": "latest" - } - }, - "packages/federation-sdk": { - "name": "@rocket.chat/federation-sdk", - "version": "0.3.0", - "dependencies": { - "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-core": "workspace:*", - "@rocket.chat/federation-crypto": "workspace:*", - "@rocket.chat/federation-room": "workspace:*", - "mongodb": "^6.16.0", - "reflect-metadata": "^0.2.2", - "tsyringe": "^4.10.0", - "tweetnacl": "^1.0.3", - "zod": "^3.24.1" - }, - "peerDependencies": { - "typescript": "~5.9.2" - } - }, - "packages/homeserver": { - "name": "@rocket.chat/homeserver", - "version": "1.0.50", - "dependencies": { - "@bogeychan/elysia-etag": "^0.0.6", - "@bogeychan/elysia-logger": "^0.1.4", - "@elysiajs/swagger": "^1.3.0", - "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-core": "workspace:*", - "@rocket.chat/federation-room": "workspace:*", - "@rocket.chat/federation-sdk": "workspace:*", - "elysia": "^1.1.26", - "mongodb": "^6.16.0", - "tsyringe": "^4.10.0" - }, - "devDependencies": { - "bun-types": "latest" - } - }, - "packages/room": { - "name": "@rocket.chat/federation-room", - "version": "1.0.50", - "dependencies": { - "@datastructures-js/priority-queue": "^6.3.3", - "@rocket.chat/federation-crypto": "workspace:*", - "zod": "^3.24.1" - }, - "devDependencies": { - "bun-types": "latest" - } - } - }, - "packages": { - "@babel/code-frame": [ - "@babel/code-frame@7.27.1", - "", - { - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - } - }, - "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==" - ], - - "@babel/helper-validator-identifier": [ - "@babel/helper-validator-identifier@7.27.1", - "", - {}, - "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" - ], - - "@biomejs/biome": [ - "@biomejs/biome@1.9.4", - "", - { - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.9.4", - "@biomejs/cli-darwin-x64": "1.9.4", - "@biomejs/cli-linux-arm64": "1.9.4", - "@biomejs/cli-linux-arm64-musl": "1.9.4", - "@biomejs/cli-linux-x64": "1.9.4", - "@biomejs/cli-linux-x64-musl": "1.9.4", - "@biomejs/cli-win32-arm64": "1.9.4", - "@biomejs/cli-win32-x64": "1.9.4" - }, - "bin": { "biome": "bin/biome" } - }, - "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==" - ], - - "@biomejs/cli-darwin-arm64": [ - "@biomejs/cli-darwin-arm64@1.9.4", - "", - { "os": "darwin", "cpu": "arm64" }, - "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==" - ], - - "@biomejs/cli-darwin-x64": [ - "@biomejs/cli-darwin-x64@1.9.4", - "", - { "os": "darwin", "cpu": "x64" }, - "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==" - ], - - "@biomejs/cli-linux-arm64": [ - "@biomejs/cli-linux-arm64@1.9.4", - "", - { "os": "linux", "cpu": "arm64" }, - "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==" - ], - - "@biomejs/cli-linux-arm64-musl": [ - "@biomejs/cli-linux-arm64-musl@1.9.4", - "", - { "os": "linux", "cpu": "arm64" }, - "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==" - ], - - "@biomejs/cli-linux-x64": [ - "@biomejs/cli-linux-x64@1.9.4", - "", - { "os": "linux", "cpu": "x64" }, - "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==" - ], - - "@biomejs/cli-linux-x64-musl": [ - "@biomejs/cli-linux-x64-musl@1.9.4", - "", - { "os": "linux", "cpu": "x64" }, - "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==" - ], - - "@biomejs/cli-win32-arm64": [ - "@biomejs/cli-win32-arm64@1.9.4", - "", - { "os": "win32", "cpu": "arm64" }, - "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==" - ], - - "@biomejs/cli-win32-x64": [ - "@biomejs/cli-win32-x64@1.9.4", - "", - { "os": "win32", "cpu": "x64" }, - "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==" - ], - - "@bogeychan/elysia-etag": [ - "@bogeychan/elysia-etag@0.0.6", - "", - { "peerDependencies": { "elysia": ">= 1.0.22" } }, - "sha512-DPHRQJLm4mR5zkQk+DYhDEX1YFh0S5M3ZTqLy/SJ+kdT68c3wxynkyZZtL2YRNhZ0LKHuDXzqAXd77eTZeKxMg==" - ], - - "@bogeychan/elysia-logger": [ - "@bogeychan/elysia-logger@0.1.8", - "", - { - "dependencies": { "pino": "^9.6.0" }, - "peerDependencies": { "elysia": ">= 1.2.10" } - }, - "sha512-TbCpMX+m68t0FbvpbBjMrCs4HQ9f1twkvTSGf6ShAkjash7zP9vGLGnEJ0iSG0ymgqLNN8Dgq0SdAEaMC6XLug==" - ], - - "@cspotcode/source-map-support": [ - "@cspotcode/source-map-support@0.8.1", - "", - { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, - "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==" - ], - - "@datastructures-js/heap": [ - "@datastructures-js/heap@4.3.3", - "", - {}, - "sha512-UcUu/DLh/aM4W3C8zZfwxxm6/6FIZUlm3mcAXuNOCa6Aj4iizNvNXQyb8DjZQH2jKSQbMRyNlngP6TPimuGjpQ==" - ], - - "@datastructures-js/priority-queue": [ - "@datastructures-js/priority-queue@6.3.3", - "", - { "dependencies": { "@datastructures-js/heap": "^4.3.3" } }, - "sha512-CIbUf0h4TPu1tHJ2gV4OTjwPkh7XZNb37u44bwoW6Py2vPdgaRUhFh3qFET6jvhyMNq/+ChWfOBRh+9s5WUtvA==" - ], - - "@elysiajs/swagger": [ - "@elysiajs/swagger@1.3.1", - "", - { - "dependencies": { - "@scalar/themes": "^0.9.52", - "@scalar/types": "^0.0.12", - "openapi-types": "^12.1.3", - "pathe": "^1.1.2" - }, - "peerDependencies": { "elysia": ">= 1.3.0" } - }, - "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ==" - ], - - "@jridgewell/resolve-uri": [ - "@jridgewell/resolve-uri@3.1.2", - "", - {}, - "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" - ], - - "@jridgewell/sourcemap-codec": [ - "@jridgewell/sourcemap-codec@1.5.5", - "", - {}, - "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" - ], - - "@jridgewell/trace-mapping": [ - "@jridgewell/trace-mapping@0.3.9", - "", - { - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==" - ], - - "@mongodb-js/saslprep": [ - "@mongodb-js/saslprep@1.3.0", - "", - { "dependencies": { "sparse-bitfield": "^3.0.3" } }, - "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==" - ], - - "@noble/ed25519": [ - "@noble/ed25519@3.0.0", - "", - {}, - "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg==" - ], - - "@rocket.chat/emitter": [ - "@rocket.chat/emitter@0.31.25", - "", - {}, - "sha512-hw5BpDlNwpYSb+K5X3DNMNUVEVXxmXugUPetGZGCWvntSVFsOjYuVEypoKW6vBBXSfqCBb0kN1npYcKEb4NFBw==" - ], - - "@rocket.chat/federation-core": [ - "@rocket.chat/federation-core@workspace:packages/core" - ], - - "@rocket.chat/federation-crypto": [ - "@rocket.chat/federation-crypto@workspace:packages/crypto" - ], - - "@rocket.chat/federation-room": [ - "@rocket.chat/federation-room@workspace:packages/room" - ], - - "@rocket.chat/federation-sdk": [ - "@rocket.chat/federation-sdk@workspace:packages/federation-sdk" - ], - - "@rocket.chat/homeserver": [ - "@rocket.chat/homeserver@workspace:packages/homeserver" - ], - - "@rollup/rollup-android-arm-eabi": [ - "@rollup/rollup-android-arm-eabi@4.52.4", - "", - { "os": "android", "cpu": "arm" }, - "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==" - ], - - "@rollup/rollup-android-arm64": [ - "@rollup/rollup-android-arm64@4.52.4", - "", - { "os": "android", "cpu": "arm64" }, - "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==" - ], - - "@rollup/rollup-darwin-arm64": [ - "@rollup/rollup-darwin-arm64@4.52.4", - "", - { "os": "darwin", "cpu": "arm64" }, - "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==" - ], - - "@rollup/rollup-darwin-x64": [ - "@rollup/rollup-darwin-x64@4.52.4", - "", - { "os": "darwin", "cpu": "x64" }, - "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==" - ], - - "@rollup/rollup-freebsd-arm64": [ - "@rollup/rollup-freebsd-arm64@4.52.4", - "", - { "os": "freebsd", "cpu": "arm64" }, - "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==" - ], - - "@rollup/rollup-freebsd-x64": [ - "@rollup/rollup-freebsd-x64@4.52.4", - "", - { "os": "freebsd", "cpu": "x64" }, - "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==" - ], - - "@rollup/rollup-linux-arm-gnueabihf": [ - "@rollup/rollup-linux-arm-gnueabihf@4.52.4", - "", - { "os": "linux", "cpu": "arm" }, - "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==" - ], - - "@rollup/rollup-linux-arm-musleabihf": [ - "@rollup/rollup-linux-arm-musleabihf@4.52.4", - "", - { "os": "linux", "cpu": "arm" }, - "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==" - ], - - "@rollup/rollup-linux-arm64-gnu": [ - "@rollup/rollup-linux-arm64-gnu@4.52.4", - "", - { "os": "linux", "cpu": "arm64" }, - "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==" - ], - - "@rollup/rollup-linux-arm64-musl": [ - "@rollup/rollup-linux-arm64-musl@4.52.4", - "", - { "os": "linux", "cpu": "arm64" }, - "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==" - ], - - "@rollup/rollup-linux-loong64-gnu": [ - "@rollup/rollup-linux-loong64-gnu@4.52.4", - "", - { "os": "linux", "cpu": "none" }, - "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==" - ], - - "@rollup/rollup-linux-ppc64-gnu": [ - "@rollup/rollup-linux-ppc64-gnu@4.52.4", - "", - { "os": "linux", "cpu": "ppc64" }, - "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==" - ], - - "@rollup/rollup-linux-riscv64-gnu": [ - "@rollup/rollup-linux-riscv64-gnu@4.52.4", - "", - { "os": "linux", "cpu": "none" }, - "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==" - ], - - "@rollup/rollup-linux-riscv64-musl": [ - "@rollup/rollup-linux-riscv64-musl@4.52.4", - "", - { "os": "linux", "cpu": "none" }, - "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==" - ], - - "@rollup/rollup-linux-s390x-gnu": [ - "@rollup/rollup-linux-s390x-gnu@4.52.4", - "", - { "os": "linux", "cpu": "s390x" }, - "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==" - ], - - "@rollup/rollup-linux-x64-gnu": [ - "@rollup/rollup-linux-x64-gnu@4.52.4", - "", - { "os": "linux", "cpu": "x64" }, - "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==" - ], - - "@rollup/rollup-linux-x64-musl": [ - "@rollup/rollup-linux-x64-musl@4.52.4", - "", - { "os": "linux", "cpu": "x64" }, - "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==" - ], - - "@rollup/rollup-openharmony-arm64": [ - "@rollup/rollup-openharmony-arm64@4.52.4", - "", - { "os": "none", "cpu": "arm64" }, - "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==" - ], - - "@rollup/rollup-win32-arm64-msvc": [ - "@rollup/rollup-win32-arm64-msvc@4.52.4", - "", - { "os": "win32", "cpu": "arm64" }, - "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==" - ], - - "@rollup/rollup-win32-ia32-msvc": [ - "@rollup/rollup-win32-ia32-msvc@4.52.4", - "", - { "os": "win32", "cpu": "ia32" }, - "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==" - ], - - "@rollup/rollup-win32-x64-gnu": [ - "@rollup/rollup-win32-x64-gnu@4.52.4", - "", - { "os": "win32", "cpu": "x64" }, - "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==" - ], - - "@rollup/rollup-win32-x64-msvc": [ - "@rollup/rollup-win32-x64-msvc@4.52.4", - "", - { "os": "win32", "cpu": "x64" }, - "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==" - ], - - "@scalar/openapi-types": [ - "@scalar/openapi-types@0.1.1", - "", - {}, - "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg==" - ], - - "@scalar/themes": [ - "@scalar/themes@0.9.86", - "", - { "dependencies": { "@scalar/types": "0.1.7" } }, - "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row==" - ], - - "@scalar/types": [ - "@scalar/types@0.0.12", - "", - { - "dependencies": { - "@scalar/openapi-types": "0.1.1", - "@unhead/schema": "^1.9.5" - } - }, - "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ==" - ], - - "@sinclair/typebox": [ - "@sinclair/typebox@0.34.37", - "", - {}, - "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==" - ], - - "@sinonjs/commons": [ - "@sinonjs/commons@3.0.1", - "", - { "dependencies": { "type-detect": "4.0.8" } }, - "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==" - ], - - "@sinonjs/fake-timers": [ - "@sinonjs/fake-timers@13.0.5", - "", - { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, - "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==" - ], - - "@sinonjs/samsam": [ - "@sinonjs/samsam@8.0.2", - "", - { - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "lodash.get": "^4.4.2", - "type-detect": "^4.1.0" - } - }, - "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==" - ], - - "@tokenizer/inflate": [ - "@tokenizer/inflate@0.2.7", - "", - { - "dependencies": { - "debug": "^4.4.0", - "fflate": "^0.8.2", - "token-types": "^6.0.0" - } - }, - "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==" - ], - - "@tokenizer/token": [ - "@tokenizer/token@0.3.0", - "", - {}, - "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - ], - - "@tsconfig/node10": [ - "@tsconfig/node10@1.0.11", - "", - {}, - "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" - ], - - "@tsconfig/node12": [ - "@tsconfig/node12@1.0.11", - "", - {}, - "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" - ], - - "@tsconfig/node14": [ - "@tsconfig/node14@1.0.3", - "", - {}, - "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" - ], - - "@tsconfig/node16": [ - "@tsconfig/node16@1.0.4", - "", - {}, - "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" - ], - - "@types/body-parser": [ - "@types/body-parser@1.19.6", - "", - { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, - "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==" - ], - - "@types/bun": [ - "@types/bun@1.2.18", - "", - { "dependencies": { "bun-types": "1.2.18" } }, - "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ==" - ], - - "@types/connect": [ - "@types/connect@3.4.38", - "", - { "dependencies": { "@types/node": "*" } }, - "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==" - ], - - "@types/estree": [ - "@types/estree@1.0.8", - "", - {}, - "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" - ], - - "@types/express": [ - "@types/express@5.0.3", - "", - { - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" - } - }, - "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==" - ], - - "@types/express-serve-static-core": [ - "@types/express-serve-static-core@5.0.6", - "", - { - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==" - ], - - "@types/http-errors": [ - "@types/http-errors@2.0.5", - "", - {}, - "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==" - ], - - "@types/mime": [ - "@types/mime@1.3.5", - "", - {}, - "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" - ], - - "@types/node": [ - "@types/node@22.16.0", - "", - { "dependencies": { "undici-types": "~6.21.0" } }, - "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==" - ], - - "@types/qs": [ - "@types/qs@6.14.0", - "", - {}, - "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==" - ], - - "@types/range-parser": [ - "@types/range-parser@1.2.7", - "", - {}, - "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" - ], - - "@types/react": [ - "@types/react@19.1.8", - "", - { "dependencies": { "csstype": "^3.0.2" } }, - "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==" - ], - - "@types/send": [ - "@types/send@0.17.5", - "", - { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, - "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==" - ], - - "@types/serve-static": [ - "@types/serve-static@1.15.8", - "", - { - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==" - ], - - "@types/sinon": [ - "@types/sinon@17.0.4", - "", - { "dependencies": { "@types/sinonjs__fake-timers": "*" } }, - "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==" - ], - - "@types/sinonjs__fake-timers": [ - "@types/sinonjs__fake-timers@8.1.5", - "", - {}, - "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==" - ], - - "@types/webidl-conversions": [ - "@types/webidl-conversions@7.0.3", - "", - {}, - "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" - ], - - "@types/whatwg-url": [ - "@types/whatwg-url@11.0.5", - "", - { "dependencies": { "@types/webidl-conversions": "*" } }, - "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==" - ], - - "@unhead/schema": [ - "@unhead/schema@1.11.20", - "", - { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, - "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==" - ], - - "abort-controller": [ - "abort-controller@3.0.0", - "", - { "dependencies": { "event-target-shim": "^5.0.0" } }, - "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==" - ], - - "acorn": [ - "acorn@8.15.0", - "", - { "bin": { "acorn": "bin/acorn" } }, - "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==" - ], - - "acorn-walk": [ - "acorn-walk@8.3.4", - "", - { "dependencies": { "acorn": "^8.11.0" } }, - "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==" - ], - - "ansi-escapes": [ - "ansi-escapes@7.0.0", - "", - { "dependencies": { "environment": "^1.0.0" } }, - "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==" - ], - - "ansi-regex": [ - "ansi-regex@5.0.1", - "", - {}, - "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - ], - - "ansi-styles": [ - "ansi-styles@4.3.0", - "", - { "dependencies": { "color-convert": "^2.0.1" } }, - "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==" - ], - - "arg": [ - "arg@4.1.3", - "", - {}, - "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" - ], - - "atomic-sleep": [ - "atomic-sleep@1.0.0", - "", - {}, - "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" - ], - - "base64-js": [ - "base64-js@1.5.1", - "", - {}, - "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - ], - - "braces": [ - "braces@3.0.3", - "", - { "dependencies": { "fill-range": "^7.1.1" } }, - "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==" - ], - - "bson": [ - "bson@6.10.4", - "", - {}, - "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==" - ], - - "buffer": [ - "buffer@6.0.3", - "", - { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, - "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==" - ], - - "bun-types": [ - "bun-types@1.3.1", - "", - { - "dependencies": { "@types/node": "*" }, - "peerDependencies": { "@types/react": "^19" } - }, - "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw==" - ], - - "chalk": [ - "chalk@5.4.1", - "", - {}, - "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==" - ], - - "cli-cursor": [ - "cli-cursor@5.0.0", - "", - { "dependencies": { "restore-cursor": "^5.0.0" } }, - "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==" - ], - - "cli-truncate": [ - "cli-truncate@4.0.0", - "", - { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, - "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==" - ], - - "color-convert": [ - "color-convert@2.0.1", - "", - { "dependencies": { "color-name": "~1.1.4" } }, - "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==" - ], - - "color-name": [ - "color-name@1.1.4", - "", - {}, - "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - ], - - "colorette": [ - "colorette@2.0.20", - "", - {}, - "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" - ], - - "commander": [ - "commander@14.0.0", - "", - {}, - "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==" - ], - - "cookie": [ - "cookie@1.0.2", - "", - {}, - "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==" - ], - - "create-require": [ - "create-require@1.1.1", - "", - {}, - "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - ], - - "csstype": [ - "csstype@3.1.3", - "", - {}, - "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - ], - - "dateformat": [ - "dateformat@4.6.3", - "", - {}, - "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==" - ], - - "debug": [ - "debug@4.4.1", - "", - { "dependencies": { "ms": "^2.1.3" } }, - "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==" - ], - - "diff": [ - "diff@7.0.0", - "", - {}, - "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==" - ], - - "dotenv": [ - "dotenv@16.6.1", - "", - {}, - "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==" - ], - - "elysia": [ - "elysia@1.3.5", - "", - { - "dependencies": { - "cookie": "^1.0.2", - "exact-mirror": "0.1.2", - "fast-decode-uri-component": "^1.0.1" - }, - "optionalDependencies": { - "@sinclair/typebox": "^0.34.33", - "openapi-types": "^12.1.3" - }, - "peerDependencies": { - "file-type": ">= 20.0.0", - "typescript": ">= 5.0.0" - } - }, - "sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw==" - ], - - "emoji-regex": [ - "emoji-regex@10.4.0", - "", - {}, - "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==" - ], - - "end-of-stream": [ - "end-of-stream@1.4.5", - "", - { "dependencies": { "once": "^1.4.0" } }, - "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==" - ], - - "environment": [ - "environment@1.1.0", - "", - {}, - "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==" - ], - - "event-target-shim": [ - "event-target-shim@5.0.1", - "", - {}, - "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" - ], - - "eventemitter3": [ - "eventemitter3@5.0.1", - "", - {}, - "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" - ], - - "events": [ - "events@3.3.0", - "", - {}, - "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - ], - - "exact-mirror": [ - "exact-mirror@0.1.2", - "", - { - "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, - "optionalPeers": ["@sinclair/typebox"] - }, - "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw==" - ], - - "fast-copy": [ - "fast-copy@3.0.2", - "", - {}, - "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==" - ], - - "fast-decode-uri-component": [ - "fast-decode-uri-component@1.0.1", - "", - {}, - "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" - ], - - "fast-redact": [ - "fast-redact@3.5.0", - "", - {}, - "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==" - ], - - "fast-safe-stringify": [ - "fast-safe-stringify@2.1.1", - "", - {}, - "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" - ], - - "fflate": [ - "fflate@0.8.2", - "", - {}, - "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" - ], - - "file-type": [ - "file-type@21.0.0", - "", - { - "dependencies": { - "@tokenizer/inflate": "^0.2.7", - "strtok3": "^10.2.2", - "token-types": "^6.0.0", - "uint8array-extras": "^1.4.0" - } - }, - "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==" - ], - - "fill-range": [ - "fill-range@7.1.1", - "", - { "dependencies": { "to-regex-range": "^5.0.1" } }, - "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==" - ], - - "fsevents": [ - "fsevents@2.3.3", - "", - { "os": "darwin" }, - "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" - ], - - "function-bind": [ - "function-bind@1.1.2", - "", - {}, - "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - ], - - "get-east-asian-width": [ - "get-east-asian-width@1.3.0", - "", - {}, - "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==" - ], - - "global-prefix": [ - "global-prefix@4.0.0", - "", - { - "dependencies": { - "ini": "^4.1.3", - "kind-of": "^6.0.3", - "which": "^4.0.0" - } - }, - "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==" - ], - - "has-flag": [ - "has-flag@4.0.0", - "", - {}, - "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - ], - - "hasown": [ - "hasown@2.0.2", - "", - { "dependencies": { "function-bind": "^1.1.2" } }, - "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==" - ], - - "help-me": [ - "help-me@5.0.0", - "", - {}, - "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" - ], - - "hookable": [ - "hookable@5.5.3", - "", - {}, - "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" - ], - - "husky": [ - "husky@9.1.7", - "", - { "bin": { "husky": "bin.js" } }, - "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==" - ], - - "ieee754": [ - "ieee754@1.2.1", - "", - {}, - "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - ], - - "ini": [ - "ini@4.1.3", - "", - {}, - "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==" - ], - - "is-core-module": [ - "is-core-module@2.16.1", - "", - { "dependencies": { "hasown": "^2.0.2" } }, - "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==" - ], - - "is-fullwidth-code-point": [ - "is-fullwidth-code-point@4.0.0", - "", - {}, - "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==" - ], - - "is-number": [ - "is-number@7.0.0", - "", - {}, - "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - ], - - "isexe": [ - "isexe@3.1.1", - "", - {}, - "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==" - ], - - "joycon": [ - "joycon@3.1.1", - "", - {}, - "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==" - ], - - "js-tokens": [ - "js-tokens@4.0.0", - "", - {}, - "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - ], - - "json5": [ - "json5@2.2.3", - "", - { "bin": { "json5": "lib/cli.js" } }, - "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" - ], - - "kind-of": [ - "kind-of@6.0.3", - "", - {}, - "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - ], - - "lilconfig": [ - "lilconfig@3.1.3", - "", - {}, - "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==" - ], - - "lint-staged": [ - "lint-staged@16.1.2", - "", - { - "dependencies": { - "chalk": "^5.4.1", - "commander": "^14.0.0", - "debug": "^4.4.1", - "lilconfig": "^3.1.3", - "listr2": "^8.3.3", - "micromatch": "^4.0.8", - "nano-spawn": "^1.0.2", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.8.0" - }, - "bin": { "lint-staged": "bin/lint-staged.js" } - }, - "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==" - ], - - "listr2": [ - "listr2@8.3.3", - "", - { - "dependencies": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - } - }, - "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==" - ], - - "lodash.get": [ - "lodash.get@4.4.2", - "", - {}, - "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - ], - - "log-update": [ - "log-update@6.1.0", - "", - { - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - } - }, - "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==" - ], - - "magic-string": [ - "magic-string@0.30.19", - "", - { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==" - ], - - "make-error": [ - "make-error@1.3.6", - "", - {}, - "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - ], - - "memoize": [ - "memoize@10.1.0", - "", - { "dependencies": { "mimic-function": "^5.0.1" } }, - "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==" - ], - - "memory-pager": [ - "memory-pager@1.5.0", - "", - {}, - "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" - ], - - "micromatch": [ - "micromatch@4.0.8", - "", - { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, - "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==" - ], - - "mimic-function": [ - "mimic-function@5.0.1", - "", - {}, - "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==" - ], - - "minimist": [ - "minimist@1.2.8", - "", - {}, - "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - ], - - "mongodb": [ - "mongodb@6.17.0", - "", - { - "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.0" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "optionalPeers": [ - "@aws-sdk/credential-providers", - "@mongodb-js/zstd", - "gcp-metadata", - "kerberos", - "mongodb-client-encryption", - "snappy", - "socks" - ] - }, - "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==" - ], - - "mongodb-connection-string-url": [ - "mongodb-connection-string-url@3.0.2", - "", - { - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, - "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==" - ], - - "ms": [ - "ms@2.1.3", - "", - {}, - "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - ], - - "nano-spawn": [ - "nano-spawn@1.0.2", - "", - {}, - "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==" - ], - - "nanoid": [ - "nanoid@5.1.5", - "", - { "bin": { "nanoid": "bin/nanoid.js" } }, - "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==" - ], - - "on-exit-leak-free": [ - "on-exit-leak-free@2.1.2", - "", - {}, - "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==" - ], - - "once": [ - "once@1.4.0", - "", - { "dependencies": { "wrappy": "1" } }, - "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==" - ], - - "onetime": [ - "onetime@7.0.0", - "", - { "dependencies": { "mimic-function": "^5.0.0" } }, - "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==" - ], - - "openapi-types": [ - "openapi-types@12.1.3", - "", - {}, - "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" - ], - - "path-parse": [ - "path-parse@1.0.7", - "", - {}, - "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - ], - - "pathe": [ - "pathe@1.1.2", - "", - {}, - "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" - ], - - "picocolors": [ - "picocolors@1.1.1", - "", - {}, - "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - ], - - "picomatch": [ - "picomatch@2.3.1", - "", - {}, - "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" - ], - - "pidtree": [ - "pidtree@0.6.0", - "", - { "bin": { "pidtree": "bin/pidtree.js" } }, - "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==" - ], - - "pino": [ - "pino@8.21.0", - "", - { - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^1.2.0", - "pino-std-serializers": "^6.0.0", - "process-warning": "^3.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^3.7.0", - "thread-stream": "^2.6.0" - }, - "bin": { "pino": "bin.js" } - }, - "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==" - ], - - "pino-abstract-transport": [ - "pino-abstract-transport@2.0.0", - "", - { "dependencies": { "split2": "^4.0.0" } }, - "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==" - ], - - "pino-pretty": [ - "pino-pretty@13.1.1", - "", - { - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^3.0.2", - "fast-safe-stringify": "^2.1.1", - "help-me": "^5.0.0", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pump": "^3.0.0", - "secure-json-parse": "^4.0.0", - "sonic-boom": "^4.0.1", - "strip-json-comments": "^5.0.2" - }, - "bin": { "pino-pretty": "bin.js" } - }, - "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA==" - ], - - "pino-std-serializers": [ - "pino-std-serializers@6.2.2", - "", - {}, - "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" - ], - - "process": [ - "process@0.11.10", - "", - {}, - "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" - ], - - "process-warning": [ - "process-warning@3.0.0", - "", - {}, - "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" - ], - - "pump": [ - "pump@3.0.3", - "", - { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, - "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==" - ], - - "punycode": [ - "punycode@2.3.1", - "", - {}, - "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" - ], - - "quick-format-unescaped": [ - "quick-format-unescaped@4.0.4", - "", - {}, - "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" - ], - - "readable-stream": [ - "readable-stream@4.7.0", - "", - { - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - } - }, - "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==" - ], - - "real-require": [ - "real-require@0.2.0", - "", - {}, - "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" - ], - - "reflect-metadata": [ - "reflect-metadata@0.2.2", - "", - {}, - "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" - ], - - "resolve": [ - "resolve@1.22.10", - "", - { - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { "resolve": "bin/resolve" } - }, - "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==" - ], - - "restore-cursor": [ - "restore-cursor@5.1.0", - "", - { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, - "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==" - ], - - "rfdc": [ - "rfdc@1.4.1", - "", - {}, - "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" - ], - - "rollup": [ - "rollup@4.52.4", - "", - { - "dependencies": { "@types/estree": "1.0.8" }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.4", - "@rollup/rollup-android-arm64": "4.52.4", - "@rollup/rollup-darwin-arm64": "4.52.4", - "@rollup/rollup-darwin-x64": "4.52.4", - "@rollup/rollup-freebsd-arm64": "4.52.4", - "@rollup/rollup-freebsd-x64": "4.52.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", - "@rollup/rollup-linux-arm-musleabihf": "4.52.4", - "@rollup/rollup-linux-arm64-gnu": "4.52.4", - "@rollup/rollup-linux-arm64-musl": "4.52.4", - "@rollup/rollup-linux-loong64-gnu": "4.52.4", - "@rollup/rollup-linux-ppc64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-musl": "4.52.4", - "@rollup/rollup-linux-s390x-gnu": "4.52.4", - "@rollup/rollup-linux-x64-gnu": "4.52.4", - "@rollup/rollup-linux-x64-musl": "4.52.4", - "@rollup/rollup-openharmony-arm64": "4.52.4", - "@rollup/rollup-win32-arm64-msvc": "4.52.4", - "@rollup/rollup-win32-ia32-msvc": "4.52.4", - "@rollup/rollup-win32-x64-gnu": "4.52.4", - "@rollup/rollup-win32-x64-msvc": "4.52.4", - "fsevents": "~2.3.2" - }, - "bin": { "rollup": "dist/bin/rollup" } - }, - "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==" - ], - - "rollup-plugin-dts": [ - "rollup-plugin-dts@6.2.3", - "", - { - "dependencies": { "magic-string": "^0.30.17" }, - "optionalDependencies": { "@babel/code-frame": "^7.27.1" }, - "peerDependencies": { - "rollup": "^3.29.4 || ^4", - "typescript": "^4.5 || ^5.0" - } - }, - "sha512-UgnEsfciXSPpASuOelix7m4DrmyQgiaWBnvI0TM4GxuDh5FkqW8E5hu57bCxXB90VvR1WNfLV80yEDN18UogSA==" - ], - - "safe-buffer": [ - "safe-buffer@5.2.1", - "", - {}, - "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - ], - - "safe-stable-stringify": [ - "safe-stable-stringify@2.5.0", - "", - {}, - "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" - ], - - "secure-json-parse": [ - "secure-json-parse@4.0.0", - "", - {}, - "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==" - ], - - "semver": [ - "semver@7.7.2", - "", - { "bin": { "semver": "bin/semver.js" } }, - "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==" - ], - - "signal-exit": [ - "signal-exit@4.1.0", - "", - {}, - "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" - ], - - "sinon": [ - "sinon@20.0.0", - "", - { - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.5", - "@sinonjs/samsam": "^8.0.1", - "diff": "^7.0.0", - "supports-color": "^7.2.0" - } - }, - "sha512-+FXOAbdnj94AQIxH0w1v8gzNxkawVvNqE3jUzRLptR71Oykeu2RrQXXl/VQjKay+Qnh73fDt/oDfMo6xMeDQbQ==" - ], - - "slice-ansi": [ - "slice-ansi@5.0.0", - "", - { - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - } - }, - "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==" - ], - - "sonic-boom": [ - "sonic-boom@4.2.0", - "", - { "dependencies": { "atomic-sleep": "^1.0.0" } }, - "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==" - ], - - "sparse-bitfield": [ - "sparse-bitfield@3.0.3", - "", - { "dependencies": { "memory-pager": "^1.0.2" } }, - "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==" - ], - - "split2": [ - "split2@4.2.0", - "", - {}, - "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" - ], - - "string-argv": [ - "string-argv@0.3.2", - "", - {}, - "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==" - ], - - "string-width": [ - "string-width@7.2.0", - "", - { - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - } - }, - "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==" - ], - - "string_decoder": [ - "string_decoder@1.3.0", - "", - { "dependencies": { "safe-buffer": "~5.2.0" } }, - "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==" - ], - - "strip-ansi": [ - "strip-ansi@6.0.1", - "", - { "dependencies": { "ansi-regex": "^5.0.1" } }, - "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==" - ], - - "strip-bom": [ - "strip-bom@3.0.0", - "", - {}, - "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" - ], - - "strip-json-comments": [ - "strip-json-comments@5.0.3", - "", - {}, - "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==" - ], - - "strtok3": [ - "strtok3@10.3.1", - "", - { "dependencies": { "@tokenizer/token": "^0.3.0" } }, - "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==" - ], - - "supports-color": [ - "supports-color@7.2.0", - "", - { "dependencies": { "has-flag": "^4.0.0" } }, - "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" - ], - - "supports-preserve-symlinks-flag": [ - "supports-preserve-symlinks-flag@1.0.0", - "", - {}, - "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - ], - - "thread-stream": [ - "thread-stream@2.7.0", - "", - { "dependencies": { "real-require": "^0.2.0" } }, - "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==" - ], - - "to-regex-range": [ - "to-regex-range@5.0.1", - "", - { "dependencies": { "is-number": "^7.0.0" } }, - "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==" - ], - - "token-types": [ - "token-types@6.0.3", - "", - { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, - "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==" - ], - - "tr46": [ - "tr46@5.1.1", - "", - { "dependencies": { "punycode": "^2.3.1" } }, - "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==" - ], - - "ts-node": [ - "ts-node@10.9.2", - "", - { - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "optionalPeers": ["@swc/core", "@swc/wasm"], - "bin": { - "ts-node": "dist/bin.js", - "ts-script": "dist/bin-script-deprecated.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js" - } - }, - "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==" - ], - - "ts-patch": [ - "ts-patch@3.3.0", - "", - { - "dependencies": { - "chalk": "^4.1.2", - "global-prefix": "^4.0.0", - "minimist": "^1.2.8", - "resolve": "^1.22.2", - "semver": "^7.6.3", - "strip-ansi": "^6.0.1" - }, - "bin": { "ts-patch": "bin/ts-patch.js", "tspc": "bin/tspc.js" } - }, - "sha512-zAOzDnd5qsfEnjd9IGy1IRuvA7ygyyxxdxesbhMdutt8AHFjD8Vw8hU2rMF89HX1BKRWFYqKHrO8Q6lw0NeUZg==" - ], - - "tsconfig-paths": [ - "tsconfig-paths@4.2.0", - "", - { - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==" - ], - - "tslib": [ - "tslib@1.14.1", - "", - {}, - "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - ], - - "tsyringe": [ - "tsyringe@4.10.0", - "", - { "dependencies": { "tslib": "^1.9.3" } }, - "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==" - ], - - "turbo": [ - "turbo@2.5.6", - "", - { - "optionalDependencies": { - "turbo-darwin-64": "2.5.6", - "turbo-darwin-arm64": "2.5.6", - "turbo-linux-64": "2.5.6", - "turbo-linux-arm64": "2.5.6", - "turbo-windows-64": "2.5.6", - "turbo-windows-arm64": "2.5.6" - }, - "bin": { "turbo": "bin/turbo" } - }, - "sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==" - ], - - "turbo-darwin-64": [ - "turbo-darwin-64@2.5.6", - "", - { "os": "darwin", "cpu": "x64" }, - "sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==" - ], - - "turbo-darwin-arm64": [ - "turbo-darwin-arm64@2.5.6", - "", - { "os": "darwin", "cpu": "arm64" }, - "sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA==" - ], - - "turbo-linux-64": [ - "turbo-linux-64@2.5.6", - "", - { "os": "linux", "cpu": "x64" }, - "sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA==" - ], - - "turbo-linux-arm64": [ - "turbo-linux-arm64@2.5.6", - "", - { "os": "linux", "cpu": "arm64" }, - "sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ==" - ], - - "turbo-windows-64": [ - "turbo-windows-64@2.5.6", - "", - { "os": "win32", "cpu": "x64" }, - "sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg==" - ], - - "turbo-windows-arm64": [ - "turbo-windows-arm64@2.5.6", - "", - { "os": "win32", "cpu": "arm64" }, - "sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q==" - ], - - "tweetnacl": [ - "tweetnacl@1.0.3", - "", - {}, - "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" - ], - - "type-detect": [ - "type-detect@4.0.8", - "", - {}, - "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" - ], - - "type-fest": [ - "type-fest@4.41.0", - "", - {}, - "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==" - ], - - "typescript": [ - "typescript@5.9.2", - "", - { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, - "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==" - ], - - "uint8array-extras": [ - "uint8array-extras@1.4.0", - "", - {}, - "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==" - ], - - "undici-types": [ - "undici-types@6.21.0", - "", - {}, - "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" - ], - - "v8-compile-cache-lib": [ - "v8-compile-cache-lib@3.0.1", - "", - {}, - "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" - ], - - "webidl-conversions": [ - "webidl-conversions@7.0.0", - "", - {}, - "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" - ], - - "whatwg-url": [ - "whatwg-url@14.2.0", - "", - { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, - "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==" - ], - - "which": [ - "which@4.0.0", - "", - { - "dependencies": { "isexe": "^3.1.1" }, - "bin": { "node-which": "bin/which.js" } - }, - "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==" - ], - - "wrap-ansi": [ - "wrap-ansi@9.0.0", - "", - { - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - } - }, - "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==" - ], - - "wrappy": [ - "wrappy@1.0.2", - "", - {}, - "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - ], - - "yaml": [ - "yaml@2.8.0", - "", - { "bin": { "yaml": "bin.mjs" } }, - "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==" - ], - - "yn": [ - "yn@3.1.1", - "", - {}, - "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" - ], - - "zhead": [ - "zhead@2.2.4", - "", - {}, - "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==" - ], - - "zod": [ - "zod@3.25.71", - "", - {}, - "sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q==" - ], - - "@bogeychan/elysia-logger/pino": [ - "pino@9.7.0", - "", - { - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { "pino": "bin.js" } - }, - "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==" - ], - - "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": [ - "@jridgewell/sourcemap-codec@1.5.4", - "", - {}, - "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" - ], - - "@rocket.chat/homeserver/bun-types": [ - "bun-types@1.2.22", - "", - { - "dependencies": { "@types/node": "*" }, - "peerDependencies": { "@types/react": "^19" } - }, - "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA==" - ], - - "@scalar/themes/@scalar/types": [ - "@scalar/types@0.1.7", - "", - { - "dependencies": { - "@scalar/openapi-types": "0.2.0", - "@unhead/schema": "^1.11.11", - "nanoid": "^5.1.5", - "type-fest": "^4.20.0", - "zod": "^3.23.8" - } - }, - "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw==" - ], - - "@sinonjs/samsam/type-detect": [ - "type-detect@4.1.0", - "", - {}, - "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==" - ], - - "@types/bun/bun-types": [ - "bun-types@1.2.18", - "", - { - "dependencies": { "@types/node": "*" }, - "peerDependencies": { "@types/react": "^19" } - }, - "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw==" - ], - - "log-update/slice-ansi": [ - "slice-ansi@7.1.0", - "", - { - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - } - }, - "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==" - ], - - "log-update/strip-ansi": [ - "strip-ansi@7.1.0", - "", - { "dependencies": { "ansi-regex": "^6.0.1" } }, - "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==" - ], - - "pino/pino-abstract-transport": [ - "pino-abstract-transport@1.2.0", - "", - { "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, - "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==" - ], - - "pino/sonic-boom": [ - "sonic-boom@3.8.1", - "", - { "dependencies": { "atomic-sleep": "^1.0.0" } }, - "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==" - ], - - "slice-ansi/ansi-styles": [ - "ansi-styles@6.2.1", - "", - {}, - "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" - ], - - "string-width/strip-ansi": [ - "strip-ansi@7.1.0", - "", - { "dependencies": { "ansi-regex": "^6.0.1" } }, - "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==" - ], - - "ts-node/diff": [ - "diff@4.0.2", - "", - {}, - "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" - ], - - "ts-patch/chalk": [ - "chalk@4.1.2", - "", - { - "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } - }, - "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==" - ], - - "wrap-ansi/ansi-styles": [ - "ansi-styles@6.2.1", - "", - {}, - "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" - ], - - "wrap-ansi/strip-ansi": [ - "strip-ansi@7.1.0", - "", - { "dependencies": { "ansi-regex": "^6.0.1" } }, - "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==" - ], - - "@bogeychan/elysia-logger/pino/pino-std-serializers": [ - "pino-std-serializers@7.0.0", - "", - {}, - "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" - ], - - "@bogeychan/elysia-logger/pino/process-warning": [ - "process-warning@5.0.0", - "", - {}, - "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==" - ], - - "@bogeychan/elysia-logger/pino/thread-stream": [ - "thread-stream@3.1.0", - "", - { "dependencies": { "real-require": "^0.2.0" } }, - "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==" - ], - - "@scalar/themes/@scalar/types/@scalar/openapi-types": [ - "@scalar/openapi-types@0.2.0", - "", - { "dependencies": { "zod": "^3.23.8" } }, - "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA==" - ], - - "log-update/slice-ansi/ansi-styles": [ - "ansi-styles@6.2.1", - "", - {}, - "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" - ], - - "log-update/slice-ansi/is-fullwidth-code-point": [ - "is-fullwidth-code-point@5.0.0", - "", - { "dependencies": { "get-east-asian-width": "^1.0.0" } }, - "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==" - ], - - "log-update/strip-ansi/ansi-regex": [ - "ansi-regex@6.1.0", - "", - {}, - "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" - ], - - "string-width/strip-ansi/ansi-regex": [ - "ansi-regex@6.1.0", - "", - {}, - "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" - ], - - "wrap-ansi/strip-ansi/ansi-regex": [ - "ansi-regex@6.1.0", - "", - {}, - "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" - ] - } -} diff --git a/bun.lockb b/bun.lockb index c99b59ee989d99075d0b26eaa8a1643bcc674d0e..3fff2edf7fa1fdad2f99541cdc47f3b133ab5afa 100755 GIT binary patch delta 28852 zcmeHwcUTn5)9%g+%BmokSOiHDOymr*f{F>u0n9G22$GWuh6OXngtp2t=NvI-#dOSy zIp>@(2UNUo&FsjEdcN=8`+J`I$Nl>8oto-UUEN(BW_BpgJklAPRX*3Teoap_4C$x2;`#1i}j(AuEMpk|<8LjD9M=s zN(v-s;yY@1FHlp&N2RI;s1njr>w{P8BO*;Iv@F*^5Ih=`S~wJxdLkO>l|j!`5d^eF z4;X@vSH&mBWU3^)z#wZ@WMY~sVSq%EmYx!xh#otI?y83PU7%G#SAkLw&ccPtk2jL2 zC6Jjl3=t&26M~6eLn*2_J|-b1J|;D-e`;ca2Lz_ZBqSz)zNs#e;9u5VTu9J_8WKq* z&~xBPp2!~pPx7LbsSZ(b%6^eCQBf!^X@Ghte}+;WA4d^s#zI6=Ok`S$N+szJo_e6K zhOUqc3WcXABUGsv9tNTKR53@VqHph}C0S4kEl zofJ8ziFeY-QKv*jMoY-@FeIh z1X2sT+6nn*f>K2hsi|ohNh-+$O@&-tVH!TP7Zf`QiqXm142n_AnxmnkL5*R5SxLC5 ziG=PNdJ2N5>*Es>`Xxq&OIkD#1h#D`SnncG64(rs6bf+=6irKU_e4=iQhGv0L}H}M z4drNPyE+O+xeAmN4u>&QK2=;sYK&Sbk%$Y*awnl>LC!)&zM#}qF)EeH%fnrA6FgZv zOe`%i)=g3uJoQi%Ogl{_kxW5(GP-O~nr6|Uq-al2syHGsF*XMI=A&|$TUKUbB-)P< z572_q1e6w#d^e%X#jef;k73T5;4btGdWX-Sgz1P|$u7)I>aluge-+S)pa!6~L8$>r zDT$ahsS;t?kx0VRqoPzP63J07)K6nS3>m3qoxKIkQGagj7}h0MaN$m6{kg zKqZm;3rnj3C>4}|Qj4&VBo4&rbyNrye)MB2bsW{=EOmi6(j!YF7WE~-5+rrdv$YYw z0+dEXTu2R@3LRGk@zk#GfkHnNY3MBtJ*}YyprpoDP*Q7+Kv}4+Rjo%D9BQj>3t^&( zZGP8$@-cI@SxZ4Paj5zy!bZ`MXjNR21J;K-&4p5zL1{!zfl>`O>I<5k0Hq#B_eH~m zBoA8)@)ClDk?9M{cMxn$86Ky~j8-Wlk?%6{AwD>(Y=P$?LPZJ6gv1!++=L2fdWtqB z4yicA;?U2n&)$@`Q+HKGC3LT^r$5g&t6_sts_^!4*IHIt5;(cZ9-Bor3ng>Lrp&Ux zF}9U{mAi#54Zj}$Iy`t@?6VaWLuzNX;~osTkTpE0RpEm@``sE>=n$1M_Go11n|l|G zd6!*l#?hNwht1pD+BRxr?-AFE{_(P3*c8NJIxdKJ{mVTzvmS7;0rIAo?c7688@bG=`i}C^Tzgi0i4`7YGo?3@7@2y z-!oT6F9?*$zE7UlH|-9$ak{JJrD%eLOkHp_aPU*}hn^nwX4XB`V93XN5e}*ijTNs34SwG50`2yC?mYW2e0l1oF;>@{ zdfWH5dbGJoEyJqUb{)A@VQG^f=_~cTL#>*PubWvgDYWBkNl8)hF>9%{Z|(-2ordS? zTEyFgRz2wHD1AR}+3Jz=?$}k?n;BO8yd>ens)D0#FFQw-Zx%aaf4S$2Yt;I(<i zV4rb)#(WGK%gm(`3Fb&vc@`-5(q9R#5jb5;9U0>+8nE^Hdiw7Xixn(O2eIxfu(}X? z44IU_#5O1|k@WZ_b~IwU{Stc%v0Z+Nb+1rX!mEa|%82|`z+=RA`bB`BoXR?R-Y#4h}8EQj6Y z=cc`~l~qRMuYI!>vA>F~iS6OnyorbnEvtg;Fk-PGW=UCgZ7*2~I6rV43pDZ4_rapt z3>*ePV#*4t2_~@tu~C{>Gtp3Fa%_=gR2(ZX@RD@~=L=4UWmoaip9l_{PZq>T%vrWA zFGuzau^ssmTum>jzah&sP{>AMhxS5bMHX1aOLhz#DWJ&){ABlWM~yE>jhC8NXFH4((!SN1uUsLUf*sfmY1R3(@-^5FxkB2u z1}g^ny9V<$QOGXVkkCmWOOF-kd&#QdXo7P9da|IJm#hOgenj~0nFfx=j_+mJNpLu< zWmO>2vMM+$H3nzMvW>i?YGYPhOCeon%zRB1(l5p=(^MgI!B!{?8CS(iH=a1=Z{jE2 zAZNa23fXf+V7o^%NRRp^Lgotm_@;sTF)*?MaMWPFX6Y9bmRVaNi^Sod2Av-}*-~%; z-~?5_YPbq~GgLSyk{%F^Ca%(O<%yF%GG)c)3YnLg&>EP9p_gm`I4YpaxAZ)?uA*pZ z{o1V9LLrOBNr8k%BV5z&lNvP@gjnqf}fFHX2;1=riDiW|C$W7TQD63!Ep) z3Ej=WQTqh$rY0vQ1KMC_$$YCRq@k9q*jgbS1FnujR$wVuu@2+vc*)*^Ylz}f76{J5 zN|=QPyt>`Nk-eiM>v&1WTd_rx>1l1{W` zJM0zGySA(t#L|xW)>Y{Cv4eulzpkHjsU6#ah}($pWd3#hq!#wfx1K`Rye^g?=3mcG zcOdS%@OLM0*MYyQT~8uu$KS>MxZ8=lHhi33eVpa!PTHwH^KGDz?UYFP$2tj?0gGBm zQR-#i)p)CbSPD{sNWRu*nVt&SnFcufL;Z3LRxRs;UZoMN#LyVoE^wW|VdP=bRUCv4 zGT^PSH#k}=gb`c`jwTa2&%jIfDmZWEZ|0|4)e-X8H*-Ik3U{LR!~}WrE~8u^lxOvKLOmsMerS)$I!Dt(m`}pKLzvXoi=kp_IJ@M`pz_&eBU} z<07btmRoqql;DKLk#qNw{S7V^V4673E-S4Y3;0EJuA+oLEu=J>4Air!o5$U~Gqn;~zon^;w!2vDf>$u}ptY zD%hbflJrTiCY0Au93kkK|09T^=4fKD?(zYe@DExVRze+OPiGU^UL^TLGQXnAzJep$ zM_-$H$vmMwZEMI~+e^CGon?9`*rx`D9Hze9*h;Kykw&Vh_Zt*L2V#}Yw)K*o!^wvh z0f>arcw+!)Xt7vT^U|FSP8jlMxTBH6OtkdUuZN+a)t3ZUVw{1nt0@sHteP}p(=;4( z(f86n3yxPDC1659Boegs;RpUoIC9)(tF-4v${g+<->fdD`Yu7 z5=ndHLYoaR^gb-pNFl2O(Skw3zGN}rnjj1Iby`%x1%ShtV@rDG%QCAdWDWd;=9D9w zmBoOgl@FYYmuxRMY8++}nxTUPVKv1*EDO+ZypGbGMy$A@LVBbT^L0?@dSRHN*ih_( zb8tsxU|3|H{;b$RAu~t!QSV5F$q9~h-$M3HDkrD3h61H z$n>#_A_>id($Wq=Y=@gdIw6P^gPaXwzU~T{E(Qz}p7g_*d4MZzNd!3B6ZkeuCpBj~ z3=~p%3swvg)q?qA1Q)gt^b@-5IXDu=uSn8*Et#*OLe>qoMLkaQ+e(}?Vf zNEipIwQVJuEOl--aMZcNemDtSTRtbH-2g`h%&)Cd(2-Pya?|k(miI z^DQ_ULVkFqw!zF-p^%LT7F3q-opLgm?NBJBl|ooCNYfDJ+gKq@4`G>&6|xOjjj1vT z%eL{7y$44_Ay{zZHq19bAsf?1&>WU%>m}XUhV2MYNR8UEOl-G-ZP|`$s1MrHtPq-W z6dX+);iO{Hjui*^s*N!n2^5HOfSecvqB?*XF$hE{4qeKJ{{U70rv&|P%PEHvUJ?ul z1*}bc(5BQASOxf?O(`Br5FfN@c{a3(F&p1RPaE~98caJr{EpK80MnIlYHq3{Aqa-g(laDCL>!zXIHM9pP1yHr5JZ_XcD^L;;0Z=od020s- zpzu3N`D6IhpD8sn0U$Za0Od;sNM4$TrcMNfoMuEPMn&fhegqN<3lVh7Uxkz*vFhDP{Z%Ai?7_bUY}}Kq(N_ z1M-ML_^+ujGk%+~5(F#+$O={iRN-2H6j=vQAWHH1#2^r*^bN!y5G8)2hHe6-@Pjr3 zQvujXu?R$|tM(FuK$I#dAO=C3lHmP7Ikr2?l%jNSA*!oMCrbJCD4z9dVa(!M=)Lau! zKMhektu*QX0jlOJw$=#zH;PlP#7UEnDBU`1c%r13t0vw}6HkSQTTV1dPsZEq^!hKybm!5+O)ic4QoTtm&Zfy57WPcdYLX}LKW(D_vw8CC-2ZHz|A9@DH!sNf|9$hUi*^0K zwRvtdX*+~dca$B>|7hK-&J(V|#LTe#mkH(8#^nyTn_hnKkwQ0bQ_~S+CP%JF)Ol#S z{l)tHo)&3c-=&9LIv#q`)zB^db!*F(sEw19R4DmXw;r#qVyC&KFE3Di5-X!IleA0xKc8#`A z{@&~Q@FwMl7`QjgNcreh;evyym-2$TMvhl(Xx5Q5*NG>u*ye=p%3hPyz-LQqf6d`g z&kz#nI4lO8{_96u9iLF&Il8XyfiI4i7Df)uo;dzM{-f%qpIaEb3N(Gychlf3L(9;O zrdM>{Oc;1>_w6>-E4AA?aqq2F7v9?(r%%H0kC!VGpLG@>)jVYIp^{`7B+rY81EN0;2k{%BxtoZ24#eK6dYiaT! z>VifvDNE;nG3ZR(y*csw(7{z4C(N3qm=rg8$B|9Rde!FCOz-2Hd|ti0d6P9WqV`nw zEgtq__II<%?E=-^R$p1NV#k!WN6d^@wcK-@j>DvJ79`SgE(V>D3YJZ3uWlW(*uTcO zHP<&>)|)-(9~+Mn)%=l9`xv!+&?9$(V|?r9Ufp}v_Byui&DVPkb*t?jJ*Gqz(XxAw z+^$dPt2+)`2QRp?C>2zvec#UV*D)7A*Lgb6Syt=n^2_}T)Sq(CR|%e5pcmSxU~Jlm zo7SC2ZM`Lp52!rQbmy~)Zt+2@!(Bp_xV3Uv_O7s%Mq#oFIzEd*r^gKsMb}-*bs61f zH=XzWg7MkiJJsPm7M!S2Qt~;ZjodP|Q$nq&ZNDeo>5=>H@bszY7U#A&w7*50QG?7J z7oGik$fhBJU@6&zfhZMJXSmwN?CJfT`#!lVPc-Q2=WcMsL#Hsk)3Pl$+RDbShjy`OorRY_@20*kC3 zczgBpsA95e9X%uAA1{|=i$Q1fntPo-mp|rlQQh{H)s@xuZ?d1ww|k~Ylr2A1*m26D z{vD0FsSciNpX>AeMf(dA%zb7#4{AI*qDjH&)2A~$+I{-&s3n*lmBgU4!M)PDGhJU; zT*=8elM`{j>c&0|kJ^O&B|W{R^1Ot9rk!0aaj6(P?e77juf~Q&d9AWl@4Q+waD8Tp zON{-!57t4kv$OqsBnSmry} zfU)))n(X-cG-+H%xyzTElLtMIt6%x6)CXGFpLja?YQpY&uPmO{3W~Kp8Wp&u3$L*t7(dhg2s(-b0r4l+ zy|Xt@(yf(UVP1jSqKP`VY0i}?x!2}bTeBr@-mnz=7EVU5&(D8qmYf&a`+;5y-I7s# zv+h_c5Ix{-99*C!Ku{AIQ`-N1ET>tdb95wnk98MH7||Ne__k@+RA zejQYmJ9|`$-FekgS~G28^4*~PSesPU49{hgM|N&^>ihc5ykH%^Cyaln3%SxS@jCaM z{mxzA@#xE}bN4HCI&ZnkB3e3#8=LY`J+Xd$!0EAd`}i zfzww!vYFR()6n3z5gv;wwG8b%d%?*5N2kqw@j&lbc9BLfjh(5s;2GS!_1U^9FE-CC zUTnBDs?btgTU+5rwaN`u&vV-9 zbCvVCI$SkrNq+o(i^8JKOP!CcG>hDE-=Sz%ZMpe2-A}c{X3Y6?uSdwF8O1l-cwSPS zoIl%by78^1QkD>AtWHwd12zhX*&*?2s^rP^0Ly_?$AGDt{%O!c~8iPu$2Z!M;@5F zWbm-!8J>|w>U&0e3lA2iWR^?F+d9EyL;JkOg-s{DxxO>#Yu%9X$@f0Yz1rP;{lU)c zB(w`U+j&^1&W%HE_3PAcQc1eA?I=Z2=d|H%3%C9q_o; z>D-pw54m-GTkX1i+iq=s;^}%?eOE_YyWKZN-5KR-lrlOg|M9B(2anW!z5V`N^W@~? zbx$rk={h3l!rk{)v%OZSOL`PdU(~q&`i%HA{rhY7-WDy9HRj-w zqM(jRU5`|J)34$DG6lEdfIh7I2|HKWogT9*%MSnXwOK0L+z78FJ8>_D^cToH7eOw=Ar}UXE-P-=( z!_EQ2_uua6Q|n^-b9LREq<$R}2j2Gd4Y(y=B-y)RTbnu6#xGeobNQvoOS3YJ4c8wS zxF$m*m?mliZNcge-#1HkFEcM0t$Or$wfeyRUSGT2v}#r47^?SuQ*-ktuZKLTXtV11 z!aAu(>(#AY#c@^lR&%SS9V}$iMtHxTp5sF&JX&Mv_|Z^Xume}gWbHNSk^$-$&q5Bi zo2zL4_0GP*D#e*wf%^|94mti$tLD#}4k&Qx*D-LZe$FwQqH;5m2ED!Fqtkk*vhUh_ zdY;5T{_yCaE%(Nlo-y&C+HOqe;_7D)|CX(8y)^1TwL6O2uO|kaZoKvYC+qRDVfOw= zYp0mLJ*VEu?Ks}_{;+#ja-%I;UtV?Ju&J#^F!h9^wqOJE9(t06WxS4S?_&S5c=Ax!-Pr;CSH4Dt^6R5%pD}@oA03! zOg-VGEqLUKb*a1jjxGzFmfJkJlKL>p==vC-QK~R7wgbFltnA`*=BG)jO!E1`Z17r+HEtB6qM9i`7k zMFev#SRuH_;5;IOxmGMYGL-$@PoF&i7tCB#p{!-JKAWZr=Gw43;68y1iVEi1vB^=P zYBa%nBPXdk%v*kG;~GwO==C7^xa`Y@w7v@a3uiwowItN`3)aJKQmTm(yw zNBfe{K5!~#m4NmoqkRd%Tt8L_?lCxz#9%IlWhbJ2DQF+KSmu(1_NAhINx@t^y94eM zxS-@A6I_#7U`i;L!e-!_%3kA|##*I@a_MXVt^?R-TnDlaX`%S)b_K2( zj7tyYGFdlV2eW)!hcMZIP;Mww;yR3N$5qXY2BJNgm06X)1DXDyN?bO}8I3k&>a!Q4 zgSipRXAIgjSf9-q6U>cbPr!K((PwSO26JQBjIn4FxNqQcSgRbgX{bJ1lM~F1W1qpb z9H!5D{T0kjU@QJYo4`%z7R*gz+&GMx8ofO(n48S}->P-04p=&!X7=5;qdeYu{&P&x9QI(Xp3%a!2pIu>3GsiyheLC*z zdSFm$@+IFbi_HrLWbJVabC%1F+n?emsIcEI(4L^~$F3jWX}4xo_c`GM{yubL$*S(t z$GtcdW;w+_J>K52`{RiV((cwBx5Xr*^XBh6W*wRI>Bzu2TaLajn(?W1f-Jf4cpr9h zl$_ld8O$wW^`Pt6(Y)qumS|V<*WCI8z3-Q^JP_5YbIc60W?esw)lYw=?wBnfa@_rX zE9oxtdU4*92d}YfWIk&9rwNts%{R$bKRL7`RvlY(QZllTIgF93Ydu!JYTl=j{Xd`H z8gqMA)qQ+8ptb8clK^Y=!qvAAKWu5C?33NJ^%FWLnJ>ys>r*gMr_w{@BVekXn?iM{irw6aOYIpWXxf-KYmf8KI42I4z z>7Sc6Z|m22<)`GB_Fi1c!ruOp<;$kB_67#*2DBU9EcA7ouHz^7dbP8heO&H$-RXx` z)~mXY>(u2|?rBM}zvAtbfLb%-FD+|uA-eD5g{Fyr*>qY|KW~29Nzc|U`&x03*7&T@ z)^6>bvy0juOTMzSUjDl5MahBIu`lwcwd&>*Tv-2npA}0sMHjXEJ7fN96K2qK;DL5t zcSbzf_9#4l+O{vld-qQW|2(`Gn=n?cPA;6Q{umq9-8Id>)%p`JYkc=O*Xl~%9rnd; zc52kYAz>U;fn-iB47vUX=@fw$fWmyEWR{Nmh0b@9^l&&8l5Cj;Z7{ z>(Sl6hQAxMXJ@%LABxs&%e9;3H)Q^~DL&;));TU3*ZRDj^Csqba?qPsr)%vRS3mk* z_qk#01+*J;wEdflvsF2TH{Z5zt2b-Xv$mB}e7-+Pdt7OY|FOG!UoXwPVcuU+tw8zd z;(^`!lMm!n(wWq!f8o`)>HT_31}9}|X(#@qPD<*G`<9oGwz1@S2U*)a9v50=Eg2DV z%CqQRedTt;6aF90`wb76R^Rt-YZFU-J-5W^*G$7|pPe{kZtWJ!4j%qzP^*-ZZfwq9 za@MO;FqhAchGHEWr_Y9l2DhQ#;l-enH0ZRVK(R;ueaoOG$G=*;);@8||4xa2#)&lJ z&rZ!JJNI2ai*1{I*?w&GBa(3=J#${R@{eB^-ppr~MQp21ZBC74-^R(==1#$FHj5%b zb!M)3m)lu&rgi<%Dep}5oiZ1+I5HyrSY7{SgVfeTP6c^bM<#cWJ}rqp-XXWi$$hc@ z(bp>muj>0%_Q@lwooS^PC)t|u@{kpJle-^ky#HIUXZ%$o)5Z|hLn~esEz1RW4P0b6PN zU~W4r48!s|Q9tDP=phX)3wAu&)2Q0VX|{u#WnRiZws^zGy$KGrHf~=~(5;|H(W{ON z+4t~SFQ4o>woLbUiv;DC)BpGlc|Uv0?AtRBYgMt6B@e?q1;2Z0M|$CGp>(W zx6Yy56PAzbQzq*Yiocps;`*Fz$Mprvm=wyrWZAi)ToJp1>nr9mIh1?Na&Uda?%?{C z`AiAr-m%HJzGqKx{lEfQDEE=g!1WV*jq7LDYHBD~%ogDKg~fCY<-W24T)#1^uu!gq zW#IaqeV)3J^_{Q7Zs%3k7ytJAA%0h=!nnT|ZESx`Pl^xibSjE}{M^)tmE2gHSI~>#`&}59IwXX zx!$NWmm}${VcvSA=unCtYi8IG!u6Qtyvim+8VMi%((fPQA2;qs9KMSEdoZu>J$!_T z_upAB6ztTz>ALh{xiXtP`wO3_j7UtO-*%4%q4FjaRayKy!|<2{M>cteF_gF?DA8z% zo^ECv)Cbwd8nv=EPFi`&_t&xH}-lC0@$2EO(TraFeC4E8NVVee|cCeD+^1 zZ2s)ERrh^^Y6y+K3$$V*R_~;%y5zeMDDpXg8OMJ)rp;4-T%!nt7Xam<&jzjo6zB&? zile8R+W>_kfXdOYIx_$YuK`hhGd>2d2;XQT=>sMDl!FS;XF&XCHK6olLqYri2)}Dc zdh;>DM?h2w{UpZaXE;^@}|b4}SVxTjkhFZ!H>e#ZF9$Ke4|A2&*X0`(DPq!;kv z0EH5O;^^IZEIr7xCFcp{#OaZ0?lYnUe19E{Oz%XDa zFc_e(?m_@qAbl4rX@{(1PV1{X;l4A_1qcF~12j}kfTloWAOHvid;x#J2WSNN0hk~B z*C95b7Jxb609XOefGt4pMXUjOu~QeIR|55b`t*xcL)^Fku7De03D^UUfD=#~Fat~g zdS6Q~gD(N}nNuCW2B4>GJHQ^GH%avYS~wa44gmeYU;@ye;nx6a0>*$Ly-zU0jR8;% zs0zpd`jx5}m3#%rM93uW0e6ANz(e3Ea36RAJOC~Oi-3i|Vu0+JexLIK=)klUahm|z z*S7%MfgIG&zcn8YnhlHrGJrlnU!XV81Ly^G1ww(AKrrA5xC0)57ch}{R74+gxd1d7 z==GN_P#&lNkinAOlAV4B+kvL{24D@a8t_8`z1!M}`(40pU=J_?Py*pV4A3741L$ip z3xIy%q>tjtF{8q2>NiN)2&@Ix0qcPPqy_^10Dbax0C5GtJ|G&103v}wKpM~uppWV3 z6TA<&e+Rq;@_}?<0MH#62UJ0x&!EqMJERybEUm!=gN6Vd09puQ5l8bk0iZc+iMXaf zKit!NO$JhcC@n0f35@VbkEX4yng(|sFc+8&WB~(#RDilhltqp42WX6FDv-(R0yG6^ zO3>6O59k0=fNCiR)B!92N~8SdKy83#fhjFBwQyqs$XU+WI_fI8GXV5~%0MNcA|M0w zfC_*$UHNl25CCS@alX1zB+=0>JQl+2B&4_PlQ12lMK z8hrt>U9!<8Kp+qRGzJtvGoUFD1T+U)0KEaS@6JFcfX1yA&>jc|+5v5W5TFgv5$FKW zya=V)(*-w_NCGI43K38DT>&agwo2AQRzq0^7WcL?qW1DQYuK<2#w_zn~UFM$`pVnDR03AoP&q`-I}2N({p z$rnt`zmLM*2w)^I1{e*D1^xoYv2W*%)st{H5ts%{1ttTz00X9IXkXCj0Hx81K-6I# zc+!LFCkrJVIA9Jy)=0W91SlWH5kDWG4x#ohB9oxVTfiw`CqM;7!Ao$z1K1911GWNN zfX%=rU?Z>r$Oo1J>w#s!T3|J>3Rnp&2UY-U0BY1afMjhWlQ@Q(Lx3x=7uW;r0(JxY zfdXJ3Z~!<690iU5hk<{9&wa8UPd07#jy8lKA3 zrELabJ#D%$vvC)#_&-iD!{R8qHLxsS5lQ-pX##2vDmq56EboSxDu{^(r81)H1k3V$ z5kqC>gPMYhP8KZ7)0{9y44D{F(J_N%`JITVMio&17=SLE9k)FBfZbX>uD6S$kE1&} zgSEM2;z`Y-^t}g`_c*fQ>P@73I66DJ2nU5qNSW-^eS+O)e;+-L_BR<~EI2m%lB=`< z$IgIwa!luPeam64jvkJl!WYVDW!5~6-wi!(mLGe2XUhBFVXEtX`z2mkp(Ie9g~!%i2lPw*CnMXqK7fjxk+h? z=z~mjf1;!Q(IU}J*+aoa%E8h?q9ZfWiArgT=;};#vr?L}T!-J!vqVQMr75DjG{IGi zdT;B}q(>-3(?ay-Qko(pr75E0HqmEHX^QCD4PIr`yjz*lB+>hu z=y;|yMf49Rx~3^j5k1I>&TC3jL=SAD_ZQyy$@V3pk2baFAO=Y;m}{8Yq8m5Sg$zF$N_ZXyT77Ecf+m$MfPVc|1ds2s^rNA3z5BF`7Dk^mMIJ=qi zPA_;ow=_xg@K*In#}z?C^1@0}L?3XqT6Hy6NzY9wO%c7s^=&NiGd+ECS!v2CBewo} zbtR*jWhcQrdyU0B(I4n#zpw11l-G?IGeTy++5LoU5rot*-CQ=Mh~;LJqF za4ny2TGaFDvH&Tp7n2x`(PW=cgFU_xB5iBTJa4*6LycK~kiN!j?oGM0zcJeeF3p%d zzZv2wx`gY}_xbnYT;&HSK$9N*Z)qZ|A^vaoATTbFasZ88%LyCJ&|ltN1aYq3|ihc$R&d%ns#K09+j(2o_P$&yaAu-UIMK%wZG*0kEPtikPV}})b{jZuAm1Yoq z_!W9?Y_j*%o8F}1G$;#^l!&ZmAu8&{s&4k;Nz0{59I2y>g!3#cV225o(M34 zHL;j=+uGvuanG-r=rtHAoq0spWJiy+HSAGPJC;vzp+lDF70vfnRG+X8r*BeE^K(cd z`b`r(MDb@BHGiQCj{rs;0A8*~4LJ%6yd3J{)RNSoWK0{(Pkv1W$?R zvG76#v-^50?q(y!)6oU<^v{PV*5SFS)U5$acrKT=Xu!rkkCH|Q4O;j#AW;Ez+{&5K-#WEO>}|D3(7lGQazx_JD-e*V>b%06{yj098jUo7j- zHp8FUU5yeoj|E5D>RInQW+<1k39n4l{D8Q+W4rscw}dAGyqK^P`cqsgO6QvBP*QXd z$9JF`4c{NyfF_W)N(#L5Ie6eJC{^Ll6^WiKMbC768GfWR%bDm!v{nsUqwG4JP8h-< z(P0P^+}Ww8|F&R=KZJZcrn6?FQKTL{dtv`amxyj3M}~Pu86COVn^K(VDTz{e2b6+C zxmib%LPhYTP_`R24_5I=`g5QBdBG6fD2iU`&~R^I8xs9OioWgmF7cvG*TI7sUc-3= z$3c4F5uM>715Ht6Xy(CIUz2-^t{Fv7cgW!CNUMxQbk->PzT;){MdLi!o-6W5(Tk(# z6b~6R#|Y8Ir|7be7s?-}L?@u4Q$M880RzS&IWKBK)AfDYQBg7ss4Vwq6Cb%sJ1W@zM_FdSIV)nJpynN_&U!zVo4EuCkJ91E zHvR#Xs(ABN@H%7#uqltT%+>@78;I4e>W|tzY&;(sy!n|W*&4_kpU6E6kb#tZ^Uqb8 zXIL92UKPHI(?~HyO3+>ZL%U|*+fl0Itw1*9iQKG6lOb&I#|PEaYj$ZeK)dgOY&~R) z&R7puetWOjbnj4PAOk@UiEdm?{3}X5D`e2C2s}Jc!vdPH5)^HPsY*M@)IP?I!>{WH zRp%7`bUgBKE<3#qk>W~SM`yB-r*bpVHEhP~&6lq=-ZhLiG&e_DiVT~wtfzA6nJR4N zQ&&&XakJ>N6FR_hNRsHX)wrGG#-^WcxFE$H(-P);xjAceMJ~P5oOQhtB^9Qkbe9La ze8pAzt~slGRW7aL!ERrZOLbeYfUEzu1empCX;-t%G;Uq9CarvM&EN1883`?L9p5d=+BWH(2xjso&LEr?=-cL_RtOVueWvW^G@GNRfT^>kv=T8*D*UzxRDY zrX1&WccoeRTM0NbxVS)q`J3K0dToV1|C8~H>zbzBX1`f9r~@&GLp@k;EN{PA#5B|F z&nuBI0nPrrhzb2`_M4?sv-k)S0(qy0BwR_10Xv z#E-m@wZLYkM;m~r z2^?pgTXR@JtlvT(M9>K_Z_K_WNAg`x1%s#K_{y^{=dS5ff1eV34e%$P+}c05A3L^Q z(+#FWnd>`$bpTvU$EwmC;if#zDM}ToN>Qf8BqliE`vp~!G9p&lPnGHvk&=;=miWi4 zeCm(PDT#^kf67W}KQdBnH8s_xM#gHFVGG~Ysj015bYi?JRh0r?)9lu}n0hIys5n(b z8XPdEDI=9>O5wevLsDFNznBDv$h6e7fhtv6f-)jcS%WiUW8N=|fFZ$>S`X3TR{UVhq z1JNpFO1!6gnIvUGWD2}M|27R}++2kehxpV15dkUcpTtI?rts9?myL@_NFSsr+be4Y44Z4pg>r-e8Ze>eKtY1)FySfZe{nE9u!{3YqX6r;6tlx>t&lD3#Kohyvq zkM*fkNSOp}5iNe#Gnjz<2?*Q`CILdvFS3n|md+F5@y;QWqBE2*}KGLui7<9yyyC4P$5E?%ZDgwYf#qGh>M zLfI5;A!T&X>Nf33{8O}caj&1pL;HMCTEBLzrZBrxY*|zDe;6U7`0J|!etu!|-&^I) zH{vSP5RM>77H%BqN!2^g#hJ6}S>_moJf&wM2jQ18I?MD=b-;hKqDn|jiit>!NQ_HN zN!1*o=s(R+r8o>w#>L>&mYCuY6N&#%DJCjLl|p+`4;#yvisjH znZt*cH8nrtQz7BMcXGhHE7!aomGMVb?Mf`zmJ%2I68E@dJ?EXrah&C}cAPHT^W9F1 zomP>vDP`?TZz*GiX^S`$t*B{tIdg#>mzQJ8nKHxT+FQqS+!b9J-|4EfG}TsB1+IrK zpPc8QkH4)N@WpTI7Jbg4V&0BAT*pcS6H{VSaUfUu)~5sN)?6EIvkuEGv0_W#n{ExN z$EB9b`__=Fk$2aavoZ)sNW}k8;-rdn_3&`<;ZFl5KSiPKnnVY;ylhjhPTrnsoSbvZ z3v}eH^K9yH(<;&XHHV}WD3^g6EoiIfqlA}i)V26uesJ^-$CE;oGBwR1MHQ)xNQ1S+ z(}6rrr9xq49HNucV<09PTqGUTRmynHJ0*vK%9I40+wl~m8WfY}fCFSK4(=)yEBsQu zMyfJOh5Yn?t!U2oF;N*T;9E_Y3{+EwLl!D@z!espn34uH+ZfTy2xrocs-v>Z<*Rio zks%#KRjxDyIyGC_Ggqq|xM6r3jA}eIivfg;MgYDsg6R{L_XQ1z|tu!Azt_ zUg{xgg7DIfFKAZEQ>jwU0dL3hTnsoXxu$EVw}t;x2*yk=1K8v*mequ}bVw6!9PmP! zC4aWE7OFtJaD%5KdbI)t!j-8itlRxl$!JV|aFkO_fwmre)&6B$j$CDkYrv1 z2RzW>zmIcBjgHCdz;R~wWn<$-UA1z?J9y+-HRS5BDWBb${kP8S!q<+h;9J9Tc&1}T zpRL&8&#l?uZ`QCzvP{5lz9mpvD;&vGpNgAH5w!OkdE6_K8zLRY}rl9VZ_Dpu*^9JcDS>BcIb zMsOQj7CUlz+Zu8~TOAy@jB+ghOAR*gvngtC0%fM>wRh&K=52H4JSv6_qLnraGrhS@{>xT&ve!Sr53NypAd|sprXQ}xN3ty12WPK<4 zLxH+<(aW=R#V>qW_)~}@`aCU-t$14_P*2UP)RWh*tX5^U5w&bjt``x+|26Q3R-O3& uGO81A?2ZCaSbDR@?@Vm*QyBlL5MI6UFGX@djnO6iXIr3sDII?#!TuL*2nN*v delta 16905 zcmeHvcU%-#*ZhMjg7JhN)xbP+qL(CR~=jI#%@$p zOw`z;5RJhSHO52}H6}5(#5_-uC&~Lgvoq@BFTTIL@B8mFAHQ?v+;Z+Y=bn4->~OC$ zE%u*$W4kb{dEfnOO1#hCzf$lp@3_m#`5UEn8k@#*etFV2#eL5=`KDgUQB0ugf>4p( z$Xx%t%qTalAXw@kTLW5FT3(V@IMh^DnxB`6{NDfFxD zocxTT*?BoRs4tucsC;=wK|b+4p#|~9dD&$pCX>(^Jan<>6*>djk|LQU8Cj-MLHGka z<+rZ~|5F1?VHOq0hucfg6)j;d+P3(Ek_%5OG#qME#Ze0N0VPXZaU%r|p&aEeg1
3AIR}Z$YVIQK6|U4}G8OX-zLjIa26X@RVPYlbuB# zh=HXfINOvtawzzMQc8s(N_tjqM*eW{xg$%5BL;=z0Ld}mX($NZcsPIr61V^iWvl^D zis*d=!5{PzD3rIH1*LK(Un@Ndo*HhUPM>92aa&o(@XY&|YCtwn+$SZnfuKP_iSmh1ISGprpS$(kX=O zK+##2XCYR4SD{xFdIpr#I0C8#t+Y~ZbZBgz9ViHmk>=3KDl;FpQ`y3dGC2+khnvbv z!-ka>6cr9F%FYyW!>u|MMOX*&DNxeQ1WK|CFa)Uc%1WYRAxbD7S%|49+teHNNY~L( z)^U0cRPMw~jBqMw$}ca?%MgU>XlpMwkFhr32TDYy0wkVtQ5EdYV zDx%ACQBH77unJrV>W=iupyZ)k2+GLJH;v6TWl+suO9uDRXR`RLg6t5-LkH14SD74@)pj(z*# z%P%b0{L|%6K6o&0&wxedaaSI$X<}*FVwx$FpVf4(^wcIZ&uMcHH7Yv#&79Ub+e&$} zLBoDtwqjfEx)e=iaUB=t`S5tn^dsB52^K^3e%|s(^ZUi8=fupeZ&#UK*PjIsbzk?% zoi41g`-QnFt>)a*-u&vgc45t_B-=TYJ${*GKdWG&cfoft$3EpVopSjJR%IDhP%%g4C9$=rCgOA_nOHLgi)2JhmUB)-phjh9{{ zVqr^ylXbk-Gfwj%xbEQWc&&e&#to}&8*sMVwPBoQ0Jvmuj92)@X;y(tmGg+Z2M%#- zvEj8YahgD^zQi$}78GaK3tTim@1H2nu;toRy`~xoeUUB7g8b}|EeoPXdVwRcBCqv} z6W_MuT3@~9ic$p9p`*rL5HMn(Bek3ajxO@O1-HbJ&D6I@SOW?EF7W*@lT;B0wqNSww|BM2SA$*tRU z2lpnQ=a;BijVHY<4|T-PHGFnMz38ju+GxEd6XQKnmRI2wr}+mstH#82z>w|wJg)+r z)qKjk0j~4&yg&>}Y8h@rULLsi&$*onXYHw9!CAdP9@1lwE3Lc6X^O#NMzOe&_ryCo zUIPcW#4x2Gu;FRwgHhnf6bOS$_kn8*&XKxSd{UR!G}3EYIa^zIko{E(4r3Y>Nal8D zKHE<({@~1OKnA*St-oHJ=fYD#uDkHrAkMD5#$T^7VD?br%QH?~=*m+Y>oqki&>G-QW`h^kwKy#?-t=uP0LGcoL-#7`dNW4lPqeX(SvILcEt`+X!N z%VyhQo~9mRa``vBxHecXmU{8jV7+E1@~FGfd%=3=Uts$=g`LiL{HrI=Ny?H9gN^d^9xnA=*-W4bmpe90LkPp|k(2Mtc zcq&M!FP{xk?8|FFHv4jIOTBo*m#4PWYhv(z(m__D);CTZ+lbd7@8d>X8wywZ@zhYg zodItHiM(5AqPWP9*C62>5@LC`;6$;GKTi$Q+i9`*_T$~c6790^)JJ|gh^I99NfRIl zJ>;kK=TDpQ)J;zN2~P%k5|aXXYPg<_l}4CZ9Jho!Qnv`syy8d{w^c%haV9PZ;@V``62w!J5e!WPAqRF^XUGlUsEZkQZ4oDa z*@SCT^qQci)_}!yg$dRKuAN+@76Wy&!dZj%PjJ?Lq(L0f%*t7(z$|dFsE4S-bh{i} z7&u$GkIyJ2FN^LU~O`y=E5@$UBa_A|_7!F_fov(rcQ+3|cs#JG|2u+zY#=;*lcR>Ve(h zNLzToBTj78il-*%?fRiB>&sn<9@Oo_YeNKzh)DHCFSd~T^)6CruCfjt-*8?NuGc(4 zqnTFOXxD;TG#cx0gsbKzxL)8eis9>UbO{ZCx-{|GnZb3HN90F%BF!;0Jmc(s0~gP` zHBPjPiXyvpL5Z5_c6=pN+)Pae~la7DlswEjXGw1uA0a3SU_l?+)?2#tnJe@yE;M#7OI$HBo5L0VD8$@c& zYr5$*mobmxA4*ohUJtm%S`Tg)GZOji?t0BuB+}%M`Kx)Hcr%e}d+5ceHazvs*5(FK zkw6zw26zyI>t%}7PzaJ!t(3}P+{)L>l!6FDNtS~_Bws|SJbom|6az@UK+WpI|C(ay z7tqPFV04;%{e5cqe=FD{2?I{P{wLIipHK4$u|vJ~0NL9?p@?SrB1-3yyIw)O4$(BLH;37)(hX8mB6|%_tBp@50>#rzP z$dPmZosyZu0TNUQP`P4&CmjTr9a)9`^0qWZg0HtpP=z5uw0h{I2f2UM_3jmrUY*h*nr3yQ& z8NyB_{jVrl{H{`Nw?g+Q<%zliCzSM)pdt_L(~P(2;|aL%-hEu;u>TM_biGVTwKH<6 zl{(@15uE&{I3AIh*J6;VsH_q{JX^90&OiP+yg@y-%u*9m#G8z zhkz~c6rhW!$j|rj&`_;sfDLck*MoQM>j=1{AVK(rZn%h&47mY?(vY#EstP3~9Tc9Z z$dB~(FjImSH!AH6O1ul*UZGUGo{~>_4HT%)RORLQo6?E@|IFiZ*T@q)HP8$oLBRlB zM2Qbk=t~nhQUu}u!dvHYv`-EE{Rv!d_bM#$QtmixO?zP79;cIwFivMi-%MhSwlVoZF%$eAjgMi;O@&ORT=rUVLE=d z%E0`1&5-=KHHpbUg8{-cSqAb zHO3~i|EgOZy1%n$La+IIAO3Mg5lj<%kSZ0_wtJ7|<$(k3r(d6Wu5Ipst?h2wEI!nr zbDv#TL$>X9=o0qzePKevmS)%X`?UXbQQ?r$8iy6b0w0}8^zO2*ZuFzVNmYtm7szU& z{*EE}wz{Ij+VBn4hxU!$cY5X3+#}Ifru0c$-OuaOsz!-(zuU9^``I^7TFTFV^;!HO zr(Nb7vBt;6h7!*aJ|FlLrLN1>kKQ*69)gb`?spGJ$_Ca^+A{8 zieT~tE$r%L+hNwF3kwQw4JvSIyQr5_s1$O{_LJlZhxuoBlM43^sA-h_-J2hsy?QO= z;yYV!)_nF>JlkRk9dZ7NwpKp@n(C^~3V~PHb*Dvn+c+cvDgTJpzIBILz z_>W7n!hbmb0UE+J2wc;a6jofjBj$bJ?un69` z%*YRdn^tCEQM?vhZi$Y^j5M$qK53+pdzK=k!Nu{2QAU0i+>%iSmcVa=8&js^9Yz~i zBCi;2`gWTJuf;3Fo&zVWaRTpDj|hJE0snGLKLe^LPZCg}K0aD8})1@=vZeHH^V@`y>W z58RST2G)MT^aGRzW*kJwy+{{U^Z>oW1@O4vR z-(=V~&A_sF+BDb)Za+8^XVYQd6xcW2z=rZY;5tu*eKQO!k7vw)ec(=m8_r#4!oF#+ zZ>E72@Z;dpr^CKk23EvJ%rdfKUXA+*-gvf=mGE-hOL;BsWjth#k&Wb&a3965<35^4 z%r&wxeA2%0Iy*<@g|__|H74V>LO1~!MM zy#w18!YAP7akd$@Rlp~k4Qv751I}>~e6q#BDtN{g^c}d<;5c{PioWCU$yNhf!jFUV zTnwLVGcbvd*oM9XcLm&X-grCuZVCE)yMe9bwcvu6I`QZoUD?}w(hl?;xI5t1@PuWs zPtx(Xml@bP)gL^5na;fLj@Z7X_{eh0E+KwX&IIH9d_H5`rtfS&8*}BIN>@ks@29RF zcK1W4hha;9F+BckQ;QKlE?Rwm$Igj2>igcfnLj4^rD3^&#<_CY?(_TX;@N(!mU#7R z*>c^3Z4(ls;xGU3RkvjZLt(q-PVsHWU;5g6ea>8bf= z-JcWs&8znzzq!nVKbYH@;Pd6sex8AC=HG(ryh6vj%{Q>EeEEF%9Nh2Vw(~9v z;PaI_zH@K3{_#;09L3kApi4uE}BpdykJ;444}d$w8!v&+*P#cN7}ybB3+@^?-ChGb z`rOMi*X#Jar3QAK_uGrO+Mwe{kbjbEDiK%U3MvikG=C4=vW+_KyU)PR@VtGnag&ap z2UpF#_QOVS6ZRX}N4y%`mUnbK^nih#=j8`r<7ORidvG(p)Y>{eMhD>6lclowbTOk8 z?tjk07rKP=I|n^=>b{wN_|c3%w;b6Zm#`dmw2Q`XLA*{JsiG9^c}2<%XWsKjYw;tO z%K1l9ZS6#VYqc-=vXgD(ssqkAdQHYsirNAuCRNR)H77BJ?H4b!|z5A zD#Py%utQ;QQGVjPk7VNEme?Sj)WpQ!3Bc&5 zv<{%pf22SyKpNHsG68B#{l>Ce@t$x|;GO;~H`C`P@)UXIPk^ke2FM^PN{vz@By$Nc z7nldk2NnPefeK&|z=3jLEHDnBk971wuM3b07=W%oH=sMv10s`{&YXnjWFQ4-2fP8$ z&w6~vr|C0nq?`81V$WfQEoK-~;&6 zi(O+p1OoJhWj3$}*bD3f_5%liD&QdS9`HVJ2sjKJ0agQRfpx%ofVznRB47i2gQWYw zx4?J61AsnqeF|KqPo&g46o1o!DZo@= zlfWt9U0?^W8K4b~P5^!Jas+-x+E2ibz&+qA;A`MEFa$7X;5HH%0Tcm!f!)9s;0{m% zlmbSe45$R?^JoWv2G(%U0w52dF;Ew2nV?xfp+d7kP0DllMLggdPyR>P8vbnDZ!8`t zc8>zi8i51;J1t z6UYImJ4il#zfG%yOF=%mP_D5PklNG%4a{2D-w9^wh} zfpGvu#%#bm4L9lwQe+}99+&`31||U(U+Z4lE$2C=2ADq!C%a5Lg6M03>8BK$?FHoCl5rWYtk%4DbQ48`ub} z2Si{4z<_sw9l$0)O0j1i(wp|o&b$TeJHTdO8?Y7F4(tSW0Y`wtz#-s$;5}duKq^)N z2Y^aoFR&ljr%(#bg8=1`SyY}>B{gjUQVnVi5RT#DEN~Ja;U@sH;WTgxI0JkLR0GF= zbHGOc<<|hm08fDG$oLcwxK=+EBpVHDLY*n7`#sfNYyS3Hr0US6O+oQ&QTMg^+tc8{ z2!FnNP~D>DZy$~vVm4f-2Ck+w(BEB-Dj%qmh-nC>xe9BXoj?1!@Rty&#Ao{ z`;a4s@JT;8inO`06mh#o`Vs|v$f0#mprTd(c}@4XO>kt?6&j=*7bG_=ixkfbQfH#q z1ZljMH8&5642ubiez8Ma-P-5K2E~NMhecs%DSNop&3_^r6cLL`;ext7T-{cPoP@AA zWXOvQ>7j02q&iVy;mG<8`nVuR+1x1Asl)0>w|rSckl%cnmo%{sYbd6O((0znGgjS@ z?$INx@{NDqjDVczuvpSc-Lii6@uP{!XR1ddhZ?{r8YoH+nz9_RSd`ZKGf#1nC>1ti zkv{4!`n^LBRAm;l*e1&&m4vOLbhsJw^ilWUs~as*0%nrTH*KWHC?Phmm9)XkBC33$ zgVc!H`1iIvV`(G28?00}P(l*?LT#yA<<%{gN*l;oTdb#&TTH7xMg?VmQm-%z~k!?H58U}nyy%LVcq@JFLKl!_w~N* zxwXT1Dg8x`x+lM3m%bh*@zSCfIqI(cAxT1__k}MuD>)6(kh-yd&RE-JmpexJDp}GT zXNK)`PSPo778$EgHFB1GNX|9!!!mFeH4eFWJ`)y^)`M3fMFx=4FnQFDgt(s~F_b=QB}ACK*+ zaf!*54T&Ii+t;&>a@+LC-skPNokk9Y5^|Q-le*VKW9k_Tb#E?8zy)EE;exKdvP-N2iPgl!y(BAUFBfmp-BP)k6iFXZ>Jmni1UTMJwuYfRw>Woou(Se5B-{ z?&W~dwPCgst?u1Yf&OrJ+82$C}Lm!!>rXPL9T=h^}K<)Qy4kQ(3sXhdPvg$ zLiaeU-AenS>LGQfeD_u-)_c{Qbd&XnBrhvgimJx1FS*EF`T+j$dA-$=hxF79J*J+Z z_@HOrN%4b%|lHD;>fAy65yCY2QG?eCpk5!L8Xo}i=_+8qHLn50P8x~1b z)WZxtHneg3+~*=RZ;k+^tA&sBIqLeT$0S;Gn!nh`5c>qJQ3Sz*M|~uh259YoAcc9L zqVpM z4)B+MrCD}-)*$Bi&)sWnQ7eH~D4_&7w1PVSC@uBVUs(GhXO_S82i0AN5?IbHwXv1$ zj(+@f(2El9A;$|jM|N~h@-fD*d69F`U+U_~Jk39#gfB|;S&%$=SL#=RFG{>>NAUAi zS3M=6?qCm~aS%nz)2r_c0k3^$2(Z30;7#NCNfy;%FB*Ei3hJSdsp+vfu4lg(6f6(p z1T0$W@svj?@2>e}@XBiBL}3_VHM=KRUnA7D9yu|#wAf3<0nA7IJy4n-z;b-lb1l9B zw*!x90^UQ^D5HFoo8%G*f7>^)&T<=ujT%}0$6LNAfeuC?K|Sg+Ww+@<^MoE$m*$W- z{MKnA4GP2*hS)^G#$D^LHfF9A-NPT^1$tO{u zS-+}IzkC$?udxB7dw5vvi?b@#HQim_I_^7M&WWHws-BW*{bkPJ^t20KASVLtm|^hi zheRhRjP=r@zAOhke_4L@l{mnjeg@DRvGtq|UZ<6_AhHTka9oUfV(5*vU;p!qw%4bT z3R0#QYhcC`7ElUL^%#?S!~{8!VUYDJH*q#}dM%tzJy$(;Kd!SHnYn!Y{TMUH6no9>Nrd zpQ_=~MK6|Po`w=mDB(Ez>($eT+O~6}nO}ZAT8A7L+wyYl%{oz3ub_{_;{=6qhb$!s9 zE=CIWL2G_7(g5&2>KU*>_LpO-p4gb=*6@0P-+?jGLMmycp3>(&ELyE5LY0=jnCV{~ zSK;Je`KQk7L-h55_{tBOg$NkD#nCkTnox3sqUwoYHDUs!I*pi@6Y55VMMh#E{TeUL z4Th=esj)>jdJK-4-kaV5X$(e0;y{~pEEun%lM|%6A#l>d1SvQKykmki3e-nEUv|7+ z;#O%qbbIG_r9%082ZGbiJ-+4`VpX7fJLAVCClVHTfTsrIH3{q2Z|@Kmpycnq zIl6vId++K9@br^CKVV^liN}YpPXbT#l{Dknrn7q@K28JQ0Q|O#4_EIT?9=?%P$ak` z;oP@ho)|Zj*Q6_cyBRLc4rOhliZil?XACvr7->m)aaoZZcJkBnj2u(8sU)K;uc$Dz zG%WQ9hkvEi*|I%UDxaG-Mi+R~DLY$`~G+gCoz<>MZ8r zU24i2Sz-#M^VT@*T2hJ=t%Z46p_$a;{xbHqQ$|@)0TN2{O{QXLPXhCn4&<`rs)CVh z3X{g>uxM%HP?jovo5FmhPASa0>hUNR#-wk$u|(-eCUc6DzZis-4IP`4QCb#SV#?0Q zDwEA9Ez1stp?Mkkd1Fl_rNt#hS*Fs`(9szsg*Zen?aE;doJvd?`KU8uWL}9$O37mG z_GP(9%a(3uu>i?wJZmS7f;y5ol=Y+zSX_E1b2lp*NpLde)l7 zNX?VjZYdz1`AZ9vnf=ma*7OA@bxmeL&)MMTPgKl7x|@uT7td=-K`E?}mCa0H`Hnd7 z-zqCVFHEwSm~T~!c5I(r)%{NFEjuY+&kCeL-Po+EfbJ|*D~XxRzp76jo9rN2+OpiL zpb;$AMk>l=^(D7VHbv^5&U#6iZJ2jGTEtp~wjvU0UA?4rvCPk1U8pEeIi?#LAuZ^_ z0;|rXv;DSJ2M4lJ8>!J4)PbV=nXUBecow1cjG(W7 zSi2*n_b0N(UDZ!u>IW}%#a2INsh_T9D)q)hTI>Jigt2x4^S6O;>F5M(Ojw9Nd4}I@ zDfv%i;qpzpMXCHrBsJdjZ#V9#{C5oMf6MTfjfa|9U;r(U^4kP{R> Date: Mon, 24 Nov 2025 13:46:55 -0300 Subject: [PATCH 43/56] restore bun.lock --- bun.lock | 630 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 630 insertions(+) create mode 100644 bun.lock diff --git a/bun.lock b/bun.lock new file mode 100644 index 000000000..d8c4e97c6 --- /dev/null +++ b/bun.lock @@ -0,0 +1,630 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "homeserver", + "dependencies": { + "dotenv": "^16.5.0", + "memoize": "^10.1.0", + "reflect-metadata": "^0.2.2", + "rollup": "^4.52.4", + "rollup-plugin-dts": "^6.2.3", + "tsyringe": "^4.10.0", + "tweetnacl": "^1.0.3", + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/bun": "latest", + "@types/express": "^5.0.1", + "@types/node": "^22.15.18", + "@types/sinon": "^17.0.4", + "husky": "^9.1.7", + "lint-staged": "^16.1.2", + "sinon": "^20.0.0", + "tsconfig-paths": "^4.2.0", + "turbo": "~2.5.6", + "typescript": "~5.9.2", + }, + }, + "packages/core": { + "name": "@rocket.chat/federation-core", + "version": "1.0.50", + "dependencies": { + "@rocket.chat/federation-crypto": "workspace:*", + "@rocket.chat/federation-room": "workspace:*", + "pino": "^8.21.0", + }, + "devDependencies": { + "bun-types": "latest", + "pino-pretty": "^13.1.1", + "ts-node": "^10.9.2", + "ts-patch": "^3.1.2", + "typescript": "~5.9.2", + }, + }, + "packages/crypto": { + "name": "@rocket.chat/federation-crypto", + "version": "0.0.1", + "dependencies": { + "@noble/ed25519": "^3.0.0", + }, + "devDependencies": { + "bun-types": "latest", + }, + }, + "packages/federation-sdk": { + "name": "@rocket.chat/federation-sdk", + "version": "0.3.0", + "dependencies": { + "@rocket.chat/emitter": "^0.31.25", + "@rocket.chat/federation-core": "workspace:*", + "@rocket.chat/federation-crypto": "workspace:*", + "@rocket.chat/federation-room": "workspace:*", + "mongodb": "^6.16.0", + "reflect-metadata": "^0.2.2", + "tsyringe": "^4.10.0", + "tweetnacl": "^1.0.3", + "zod": "^3.24.1", + }, + "peerDependencies": { + "typescript": "~5.9.2", + }, + }, + "packages/homeserver": { + "name": "@rocket.chat/homeserver", + "version": "1.0.50", + "dependencies": { + "@bogeychan/elysia-etag": "^0.0.6", + "@bogeychan/elysia-logger": "^0.1.4", + "@elysiajs/swagger": "^1.3.0", + "@rocket.chat/emitter": "^0.31.25", + "@rocket.chat/federation-core": "workspace:*", + "@rocket.chat/federation-room": "workspace:*", + "@rocket.chat/federation-sdk": "workspace:*", + "elysia": "^1.1.26", + "mongodb": "^6.16.0", + "tsyringe": "^4.10.0", + }, + "devDependencies": { + "bun-types": "latest", + }, + }, + "packages/room": { + "name": "@rocket.chat/federation-room", + "version": "1.0.50", + "dependencies": { + "@datastructures-js/priority-queue": "^6.3.3", + "@rocket.chat/federation-crypto": "workspace:*", + "zod": "^3.24.1", + }, + "devDependencies": { + "bun-types": "latest", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + + "@bogeychan/elysia-etag": ["@bogeychan/elysia-etag@0.0.6", "", { "peerDependencies": { "elysia": ">= 1.0.22" } }, "sha512-DPHRQJLm4mR5zkQk+DYhDEX1YFh0S5M3ZTqLy/SJ+kdT68c3wxynkyZZtL2YRNhZ0LKHuDXzqAXd77eTZeKxMg=="], + + "@bogeychan/elysia-logger": ["@bogeychan/elysia-logger@0.1.8", "", { "dependencies": { "pino": "^9.6.0" }, "peerDependencies": { "elysia": ">= 1.2.10" } }, "sha512-TbCpMX+m68t0FbvpbBjMrCs4HQ9f1twkvTSGf6ShAkjash7zP9vGLGnEJ0iSG0ymgqLNN8Dgq0SdAEaMC6XLug=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@datastructures-js/heap": ["@datastructures-js/heap@4.3.3", "", {}, "sha512-UcUu/DLh/aM4W3C8zZfwxxm6/6FIZUlm3mcAXuNOCa6Aj4iizNvNXQyb8DjZQH2jKSQbMRyNlngP6TPimuGjpQ=="], + + "@datastructures-js/priority-queue": ["@datastructures-js/priority-queue@6.3.3", "", { "dependencies": { "@datastructures-js/heap": "^4.3.3" } }, "sha512-CIbUf0h4TPu1tHJ2gV4OTjwPkh7XZNb37u44bwoW6Py2vPdgaRUhFh3qFET6jvhyMNq/+ChWfOBRh+9s5WUtvA=="], + + "@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.3.0", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ=="], + + "@noble/ed25519": ["@noble/ed25519@3.0.0", "", {}, "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg=="], + + "@rocket.chat/emitter": ["@rocket.chat/emitter@0.31.25", "", {}, "sha512-hw5BpDlNwpYSb+K5X3DNMNUVEVXxmXugUPetGZGCWvntSVFsOjYuVEypoKW6vBBXSfqCBb0kN1npYcKEb4NFBw=="], + + "@rocket.chat/federation-core": ["@rocket.chat/federation-core@workspace:packages/core"], + + "@rocket.chat/federation-crypto": ["@rocket.chat/federation-crypto@workspace:packages/crypto"], + + "@rocket.chat/federation-room": ["@rocket.chat/federation-room@workspace:packages/room"], + + "@rocket.chat/federation-sdk": ["@rocket.chat/federation-sdk@workspace:packages/federation-sdk"], + + "@rocket.chat/homeserver": ["@rocket.chat/homeserver@workspace:packages/homeserver"], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], + + "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], + + "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.37", "", {}, "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw=="], + + "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], + + "@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], + + "@sinonjs/samsam": ["@sinonjs/samsam@8.0.2", "", { "dependencies": { "@sinonjs/commons": "^3.0.1", "lodash.get": "^4.4.2", "type-detect": "^4.1.0" } }, "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], + + "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], + + "@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="], + + "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/node": ["@types/node@22.16.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + + "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], + + "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + + "@types/sinon": ["@types/sinon@17.0.4", "", { "dependencies": { "@types/sinonjs__fake-timers": "*" } }, "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew=="], + + "@types/sinonjs__fake-timers": ["@types/sinonjs__fake-timers@8.1.5", "", {}, "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ=="], + + "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], + + "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], + + "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + + "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], + + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], + + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "elysia": ["elysia@1.3.5", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw=="], + + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="], + + "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + + "global-prefix": ["global-prefix@4.0.0", "", { "dependencies": { "ini": "^4.1.3", "kind-of": "^6.0.3", "which": "^4.0.0" } }, "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lint-staged": ["lint-staged@16.1.2", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0", "debug": "^4.4.1", "lilconfig": "^3.1.3", "listr2": "^8.3.3", "micromatch": "^4.0.8", "nano-spawn": "^1.0.2", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q=="], + + "listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="], + + "lodash.get": ["lodash.get@4.4.2", "", {}, "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="], + + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], + + "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], + + "memoize": ["memoize@10.1.0", "", { "dependencies": { "mimic-function": "^5.0.1" } }, "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg=="], + + "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mongodb": ["mongodb@6.17.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.4", "mongodb-connection-string-url": "^3.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA=="], + + "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nano-spawn": ["nano-spawn@1.0.2", "", {}, "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg=="], + + "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], + + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + + "pino": ["pino@8.21.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^1.2.0", "pino-std-serializers": "^6.0.0", "process-warning": "^3.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^3.7.0", "thread-stream": "^2.6.0" }, "bin": { "pino": "bin.js" } }, "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q=="], + + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], + + "pino-pretty": ["pino-pretty@13.1.1", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA=="], + + "pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + + "process-warning": ["process-warning@3.0.0", "", {}, "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="], + + "rollup-plugin-dts": ["rollup-plugin-dts@6.2.3", "", { "dependencies": { "magic-string": "^0.30.17" }, "optionalDependencies": { "@babel/code-frame": "^7.27.1" }, "peerDependencies": { "rollup": "^3.29.4 || ^4", "typescript": "^4.5 || ^5.0" } }, "sha512-UgnEsfciXSPpASuOelix7m4DrmyQgiaWBnvI0TM4GxuDh5FkqW8E5hu57bCxXB90VvR1WNfLV80yEDN18UogSA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "secure-json-parse": ["secure-json-parse@4.0.0", "", {}, "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA=="], + + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sinon": ["sinon@20.0.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^13.0.5", "@sinonjs/samsam": "^8.0.1", "diff": "^7.0.0", "supports-color": "^7.2.0" } }, "sha512-+FXOAbdnj94AQIxH0w1v8gzNxkawVvNqE3jUzRLptR71Oykeu2RrQXXl/VQjKay+Qnh73fDt/oDfMo6xMeDQbQ=="], + + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + + "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + + "strtok3": ["strtok3@10.3.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "token-types": ["token-types@6.0.3", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + + "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], + + "ts-patch": ["ts-patch@3.3.0", "", { "dependencies": { "chalk": "^4.1.2", "global-prefix": "^4.0.0", "minimist": "^1.2.8", "resolve": "^1.22.2", "semver": "^7.6.3", "strip-ansi": "^6.0.1" }, "bin": { "ts-patch": "bin/ts-patch.js", "tspc": "bin/tspc.js" } }, "sha512-zAOzDnd5qsfEnjd9IGy1IRuvA7ygyyxxdxesbhMdutt8AHFjD8Vw8hU2rMF89HX1BKRWFYqKHrO8Q6lw0NeUZg=="], + + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + + "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], + + "turbo": ["turbo@2.5.6", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.6", "turbo-darwin-arm64": "2.5.6", "turbo-linux-64": "2.5.6", "turbo-linux-arm64": "2.5.6", "turbo-windows-64": "2.5.6", "turbo-windows-arm64": "2.5.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w=="], + + "turbo-darwin-64": ["turbo-darwin-64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A=="], + + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA=="], + + "turbo-linux-64": ["turbo-linux-64@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA=="], + + "turbo-linux-arm64": ["turbo-linux-arm64@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ=="], + + "turbo-windows-64": ["turbo-windows-64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg=="], + + "turbo-windows-arm64": ["turbo-windows-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q=="], + + "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], + + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], + + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + + "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], + + "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], + + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], + + "zod": ["zod@3.25.71", "", {}, "sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q=="], + + "@bogeychan/elysia-logger/pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="], + + "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="], + + "@rocket.chat/homeserver/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + + "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], + + "@sinonjs/samsam/type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], + + "@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], + + "log-update/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "pino/pino-abstract-transport": ["pino-abstract-transport@1.2.0", "", { "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q=="], + + "pino/sonic-boom": ["sonic-boom@3.8.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg=="], + + "slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "ts-node/diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], + + "ts-patch/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "@bogeychan/elysia-logger/pino/pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], + + "@bogeychan/elysia-logger/pino/process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + + "@bogeychan/elysia-logger/pino/thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], + + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], + + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], + + "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + } +} From 59ac6d64f9adbb2a3f049ae1f4de99364d789b0d Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 24 Nov 2025 15:40:04 -0300 Subject: [PATCH 44/56] test: improve config instance --- packages/crypto/src/utils/keys.ts | 1 - packages/crypto/src/utils/utils.spec.ts | 2 -- .../src/__mocks__/config.service.spec.ts | 23 ------------------ .../src/__mocks__/repositories.spec.ts | 21 +++++++++------- .../src/__mocks__/services.spec.ts | 15 ++++++++---- .../src/services/event.service.spec.ts | 9 ++----- .../src/services/key.service.spec.ts | 3 +-- .../src/services/state.service.spec.ts | 24 +------------------ 8 files changed, 27 insertions(+), 71 deletions(-) delete mode 100644 packages/federation-sdk/src/__mocks__/config.service.spec.ts diff --git a/packages/crypto/src/utils/keys.ts b/packages/crypto/src/utils/keys.ts index 4967cecd2..eeded239d 100644 --- a/packages/crypto/src/utils/keys.ts +++ b/packages/crypto/src/utils/keys.ts @@ -41,7 +41,6 @@ export async function verifyJsonSignature( signature: string, key: VerifierKey, ): Promise { - console.log(jsonObject); const sortedSerializedForm = encodeCanonicalJson(jsonObject); const signatureBuffer = fromBase64ToBytes(signature); diff --git a/packages/crypto/src/utils/utils.spec.ts b/packages/crypto/src/utils/utils.spec.ts index 6821f71ac..a6da51371 100644 --- a/packages/crypto/src/utils/utils.spec.ts +++ b/packages/crypto/src/utils/utils.spec.ts @@ -154,8 +154,6 @@ describe('Signing and verifying payloads', async () => { const serialized = encodeCanonicalJson(rest); - console.log({ serialized }); - const signature = signatures['syn1.tunnel.dev.rocket.chat']['ed25519:a_FAET']; diff --git a/packages/federation-sdk/src/__mocks__/config.service.spec.ts b/packages/federation-sdk/src/__mocks__/config.service.spec.ts deleted file mode 100644 index c26131cd6..000000000 --- a/packages/federation-sdk/src/__mocks__/config.service.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ConfigService } from '../services/config.service'; -import { DatabaseConnectionService } from '../services/database-connection.service'; -import { signer } from './singer.spec'; - -const databaseConfig = { - uri: 'mongodb://localhost:27017/matrix_test', - name: 'matrix_test', - poolSize: 100, -}; - -export const config = { - serverName: 'test.local', - getSigningKey: async () => signer, - database: databaseConfig, - getDatabaseConfig: function () { - // @ts-ignore - return this.database; - }, -} as unknown as ConfigService; - -const database = new DatabaseConnectionService(databaseConfig); - -export const db = await database.getDb(); diff --git a/packages/federation-sdk/src/__mocks__/repositories.spec.ts b/packages/federation-sdk/src/__mocks__/repositories.spec.ts index 387d0d586..4eeb36ea5 100644 --- a/packages/federation-sdk/src/__mocks__/repositories.spec.ts +++ b/packages/federation-sdk/src/__mocks__/repositories.spec.ts @@ -11,7 +11,18 @@ import { StateGraphRepository, type StateGraphStore, } from '../repositories/state-graph.repository'; -import { db } from './config.service.spec'; + +import { DatabaseConnectionService } from '../services/database-connection.service'; + +const databaseConfig = { + uri: 'mongodb://localhost:27017/', + name: 'matrix_test', + poolSize: 100, +}; + +const database = new DatabaseConnectionService(databaseConfig); + +const db = await database.getDb(); const keysCollection = db.collection('test_keys'); const eventsCollection = db.collection('test_events'); @@ -20,14 +31,6 @@ const eventStagingCollection = const lockCollection = db.collection('test_locks'); const statesCollection = db.collection('test_states'); -export const collections = { - keys: keysCollection, - events: eventsCollection, - eventsStaging: eventStagingCollection, - locks: lockCollection, - states: statesCollection, -}; - const keyRepository = new KeyRepository(keysCollection); const eventStagingRepository = new EventStagingRepository( diff --git a/packages/federation-sdk/src/__mocks__/services.spec.ts b/packages/federation-sdk/src/__mocks__/services.spec.ts index cdba1865e..4ca2ce027 100644 --- a/packages/federation-sdk/src/__mocks__/services.spec.ts +++ b/packages/federation-sdk/src/__mocks__/services.spec.ts @@ -1,20 +1,27 @@ import { StagingAreaQueue } from '../queues/staging-area.queue'; +import { AppConfig, ConfigService } from '../services/config.service'; import { EventEmitterService } from '../services/event-emitter.service'; import { EventService } from '../services/event.service'; import { KeyService } from '../services/key.service'; import { SignatureVerificationService } from '../services/signature-verification.service'; import { StateService } from '../services/state.service'; -import { config } from './config.service.spec'; import { repositories } from './repositories.spec'; -const keyService = new KeyService(config, repositories.keys); +const configService = new ConfigService(); + +configService.setConfig({ + signingKey: 'FC6cwY3DNmHo3B7GRugaHNyXz+TkBRVx8RvQH0kSZ04', + serverName: 'test.local', +} as AppConfig); + +const keyService = new KeyService(configService, repositories.keys); const stagingAreaQueue = new StagingAreaQueue(); const stateService = new StateService( repositories.states, repositories.events, - config, + configService, ); const eventEmitter = new EventEmitterService(); @@ -24,7 +31,7 @@ const signatureVerificationService = new SignatureVerificationService( ); const eventService = new EventService( - config, + configService, stagingAreaQueue, stateService, eventEmitter, diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts index b2326623e..873524a4b 100644 --- a/packages/federation-sdk/src/services/event.service.spec.ts +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -20,10 +20,10 @@ import { UserID, } from '@rocket.chat/federation-room'; import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; -import { config } from '../__mocks__/config.service.spec'; import { repositories } from '../__mocks__/repositories.spec'; import { eventService } from '../__mocks__/services.spec'; import { StateService } from './state.service'; +import type { ConfigService } from './config.service'; const event = { auth_events: [ @@ -164,10 +164,9 @@ runIfMongoExists(() => beforeEach(async () => { inboundServer = `localhost${Math.floor(Math.random() * 10000).toString()}`; const newConfig = { - ...config, getSigningKey: async () => signer, serverName: inboundServer, - } as unknown as typeof config; + } as unknown as ConfigService; stateService = new StateService( repositories.states, @@ -221,16 +220,12 @@ runIfMongoExists(() => roomVersion, ); - console.log('PDU', pdu.eventId); - await stateService.signEvent(pdu); // now OUR event service gets this event // to allow fetchign the key we mock fetchJsonMock.mockReturnValue(Promise.resolve(originalKeyResponse)); - console.log(pdu, pdu.event); - eventService .validateHashAndSignatures(pdu.event, roomVersion) .catch(console.error); diff --git a/packages/federation-sdk/src/services/key.service.spec.ts b/packages/federation-sdk/src/services/key.service.spec.ts index 9ad8c0af5..1e993a9e6 100644 --- a/packages/federation-sdk/src/services/key.service.spec.ts +++ b/packages/federation-sdk/src/services/key.service.spec.ts @@ -4,7 +4,6 @@ import { Mock, describe, expect, it, mock } from 'bun:test'; import { afterEach, beforeEach } from 'node:test'; import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; -import { config } from '../__mocks__/config.service.spec'; import { keyService } from '../__mocks__/services.spec'; import { signer } from '../__mocks__/singer.spec'; @@ -83,7 +82,7 @@ runIfMongoExists(() => expect(key.verify_keys['ed25519:0'].key).toBeString(); expect(key.verify_keys['ed25519:0'].key).toBe(publicKey); - const signature = key?.signatures?.[config.serverName]; + const signature = key?.signatures?.['test.local']; expect(signature).toBeDefined(); expect(Object.keys(signature).length).toBeGreaterThanOrEqual(1); diff --git a/packages/federation-sdk/src/services/state.service.spec.ts b/packages/federation-sdk/src/services/state.service.spec.ts index eb97481b6..0b4017522 100644 --- a/packages/federation-sdk/src/services/state.service.spec.ts +++ b/packages/federation-sdk/src/services/state.service.spec.ts @@ -115,14 +115,9 @@ async function copyDepth< runIfMongoExists(() => describe('StateService', async () => { - if (!process.env.RUN_MONGO_TESTS) { - console.warn('Skipping tests that require a database'); - return; - } - const databaseConfig = { uri: - process.env.MONGO_URI || + process.env.MONGO_URL || 'mongodb://localhost:27017?directConnection=true', name: 'matrix_test', poolSize: 100, @@ -1393,8 +1388,6 @@ runIfMongoExists(() => roomCreateEvent.roomId, ); - // console.log((stte3 as any).stateMap); - expect(state3.name).toBe( roomNameEvent.getContent().name, ); // should set the state to right versions @@ -1469,8 +1462,6 @@ runIfMongoExists(() => // const state2 = new RoomState(_state2); - // console.log('state', [..._state2.entries()]); - const state2 = await stateService.getLatestRoomState2( roomCreateEvent.roomId, ); @@ -2300,8 +2291,6 @@ runIfMongoExists(() => authChain.map((e) => e.event), ); - console.log(state.ourUserJoinEvent.eventId); - expect(stateId).toBeString(); const event = await stateService.getEvent( @@ -2369,9 +2358,6 @@ runIfMongoExists(() => previousEventsInStore.length === event.getPreviousEventIds().length ) { - console.log( - `All previous events found in store ${event.eventId}`, - ); // start processing this event now await stateService._resolveStateAtEvent(event); return; @@ -2388,8 +2374,6 @@ runIfMongoExists(() => } } - console.log(`Events to find ${eventIdsToFind}`); - const previousEvents = (await remoteFetch( eventIdsToFind, )) as PersistentEventBase[]; @@ -2413,20 +2397,14 @@ runIfMongoExists(() => .reverse(); for (const previousEvent of previousEvents) { - console.log(`Waling ${previousEvent.eventId}`); await walk(previousEvent); } - console.log( - `Finishing saving ${event.eventId}, all [${event.getPreviousEventIds().join(', ')}] events has been saved`, - ); - // once all previous events have been walked we process this event await stateService._resolveStateAtEvent(event); }; for (const event of eventsToWalk) { - console.log(`Starting walking ${event.eventId}`); await walk(event).catch(console.error); } From a0b18a82048f041f0158af56e3a46b8271318684 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 24 Nov 2025 16:08:27 -0300 Subject: [PATCH 45/56] fix: delay KeyRepository inject --- .../src/services/key.service.ts | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index eff3b4b2b..b4d8f811e 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -13,7 +13,7 @@ import { signJson, } from '@rocket.chat/federation-crypto'; import { PersistentEventBase } from '@rocket.chat/federation-room'; -import { singleton } from 'tsyringe'; +import { delay, inject, singleton } from 'tsyringe'; import { KeyRepository } from '../repositories/key.repository'; import { getHomeserverFinalAddress } from '../server-discovery/discovery'; import { ConfigService } from './config.service'; @@ -54,17 +54,12 @@ export type OldVerifierKey = { @singleton() export class KeyService { - private signer: Signer | undefined; - private logger = createLogger('KeyService'); constructor( private readonly configService: ConfigService, + @inject(delay(() => KeyRepository)) private readonly keyRepository: KeyRepository, - ) { - this.configService.getSigningKey().then((signer) => { - this.signer = signer; - }); - } + ) {} public isVerifierAllowedToCheckEvent( event: PersistentEventBase, @@ -212,10 +207,8 @@ export class KeyService { serverKeys: Omit[], minimumValidUntil = Date.now(), ): Promise { - if (!this.signer) { - // need this to sign the response json, no point in calculating anythingg if this isn't ready - throw new Error('Signing key not configured'); - } + const signer = await this.configService.getSigningKey(); + const verifyKeys: KeyV2ServerResponse['verify_keys'] = {}; const oldVerifyKeys: KeyV2ServerResponse['old_verify_keys'] = {}; @@ -253,13 +246,13 @@ export class KeyService { valid_until_ts: validUntil, }; - const signature = await signJson(response, this.signer); + const signature = await signJson(response, signer); return { ...response, signatures: { [this.configService.serverName]: { - [this.signer.id]: signature, + [signer.id]: signature, }, }, }; @@ -267,11 +260,6 @@ export class KeyService { // this shouldn't be here, however, to copy the controller level logic from homeserver router to rocket.chat would be a pain to keep up to date if changes are needed. for now, keeping here. async handleQuery({ server_keys: serverKeys }: QueryRequestBody) { - if (!this.signer) { - // need this to sign the response json, no point in calculating anythingg if this isn't ready - throw new Error('Signing key not configured'); - } - const serverKeysResponse = [] as KeyV2ServerResponse[]; const localKeysPerServer: Map = new Map(); @@ -404,6 +392,8 @@ export class KeyService { localKeysPerServer.set(serverName, keysForQuery); } + const signer = await this.configService.getSigningKey(); + const keys = await Promise.all([ // convert and sign ...localKeysPerServer @@ -412,14 +402,14 @@ export class KeyService { // sign with our keys ...serverKeysResponse.map(async (key): Promise => { const { signatures, ...rest } = key; - const signature = await signJson(rest, this.signer!); + const signature = await signJson(rest, signer); return { ...rest, signatures: { ...signatures, [this.configService.serverName]: { - [this.signer!.id]: signature, + [signer.id]: signature, }, }, }; @@ -610,15 +600,13 @@ export class KeyService { // .findByServerName(this.configService.serverName) // .toArray(), // ); - if (!this.signer) { - throw new Error('Signing key not configured'); - } + const signer = await this.configService.getSigningKey(); return this.convertToKeyV2Response([ { serverName: this.configService.serverName, - keyId: this.signer.id, - key: this.signer.getPublicKey().toBase64(), + keyId: signer.id, + key: signer.getPublicKey().toBase64(), // TODO: this isn't currently in config, nor do we handle expiration yet expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year expiration From def6e7eae26c151a7f119494cd07b86dbdbfbacf Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 24 Nov 2025 16:09:22 -0300 Subject: [PATCH 46/56] test: fix tests --- packages/federation-sdk/src/__mocks__/services.spec.ts | 2 +- packages/federation-sdk/src/services/event.service.spec.ts | 2 +- packages/federation-sdk/src/services/room.service.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/federation-sdk/src/__mocks__/services.spec.ts b/packages/federation-sdk/src/__mocks__/services.spec.ts index 4ca2ce027..6adfb8973 100644 --- a/packages/federation-sdk/src/__mocks__/services.spec.ts +++ b/packages/federation-sdk/src/__mocks__/services.spec.ts @@ -10,7 +10,7 @@ import { repositories } from './repositories.spec'; const configService = new ConfigService(); configService.setConfig({ - signingKey: 'FC6cwY3DNmHo3B7GRugaHNyXz+TkBRVx8RvQH0kSZ04', + signingKey: 'zSkmr713LnEDbxlkYq2ZqIiKTQNsyMOU0T2CEeC44C4', serverName: 'test.local', } as AppConfig); diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts index 873524a4b..f87df38ab 100644 --- a/packages/federation-sdk/src/services/event.service.spec.ts +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -22,8 +22,8 @@ import { import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; import { repositories } from '../__mocks__/repositories.spec'; import { eventService } from '../__mocks__/services.spec'; -import { StateService } from './state.service'; import type { ConfigService } from './config.service'; +import { StateService } from './state.service'; const event = { auth_events: [ diff --git a/packages/federation-sdk/src/services/room.service.spec.ts b/packages/federation-sdk/src/services/room.service.spec.ts index f610e66f9..674c794e3 100644 --- a/packages/federation-sdk/src/services/room.service.spec.ts +++ b/packages/federation-sdk/src/services/room.service.spec.ts @@ -35,7 +35,7 @@ describe('RoomService', async () => { const configService = new ConfigService(); federationSDK.setConfig({ - signingKey: '', + signingKey: 'FC6cwY3DNmHo3B7GRugaHNyXz+TkBRVx8RvQH0kSZ04', serverName: 'example.com', } as AppConfig); const stateService = container From 8bde57ba4fe88ce1821361927294638714a44ad2 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 24 Nov 2025 18:58:54 -0300 Subject: [PATCH 47/56] fix: config getSigningKey --- packages/federation-sdk/src/__mocks__/services.spec.ts | 2 +- packages/federation-sdk/src/services/config.service.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/federation-sdk/src/__mocks__/services.spec.ts b/packages/federation-sdk/src/__mocks__/services.spec.ts index 6adfb8973..fcd66268d 100644 --- a/packages/federation-sdk/src/__mocks__/services.spec.ts +++ b/packages/federation-sdk/src/__mocks__/services.spec.ts @@ -10,7 +10,7 @@ import { repositories } from './repositories.spec'; const configService = new ConfigService(); configService.setConfig({ - signingKey: 'zSkmr713LnEDbxlkYq2ZqIiKTQNsyMOU0T2CEeC44C4', + signingKey: 'ed25519 0 zSkmr713LnEDbxlkYq2ZqIiKTQNsyMOU0T2CEeC44C4', serverName: 'test.local', } as AppConfig); diff --git a/packages/federation-sdk/src/services/config.service.ts b/packages/federation-sdk/src/services/config.service.ts index 1c519767d..1fe64b1dc 100644 --- a/packages/federation-sdk/src/services/config.service.ts +++ b/packages/federation-sdk/src/services/config.service.ts @@ -143,8 +143,11 @@ export class ConfigService { throw new Error('Signing key is not configured'); } + const [, version, signingKey] = this.config.signingKey.split(' '); + this.signer = await loadEd25519SignerFromSeed( - fromBase64ToBytes(this.config.signingKey), + fromBase64ToBytes(signingKey), + version, ); return this.signer; From 1cb5955704c9ed5dc37eedf5cf00a25f4de907b5 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 24 Nov 2025 18:59:07 -0300 Subject: [PATCH 48/56] fix: remove database options from config --- packages/federation-sdk/src/services/config.service.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/federation-sdk/src/services/config.service.ts b/packages/federation-sdk/src/services/config.service.ts index 1fe64b1dc..a1ed626b7 100644 --- a/packages/federation-sdk/src/services/config.service.ts +++ b/packages/federation-sdk/src/services/config.service.ts @@ -19,11 +19,6 @@ export interface AppConfig { timeout?: number; // TODO: need this still? signingKeyPath?: string; - database: { - uri: string; - name: string; - poolSize: number; - }; media: { maxFileSize: number; allowedMimeTypes: string[]; @@ -56,11 +51,6 @@ export const AppConfigSchema = z.object({ signingKey: z.string().optional(), timeout: z.number().optional(), signingKeyPath: z.string(), - database: z.object({ - uri: z.string().min(1, 'Database URI is required'), - name: z.string().min(1, 'Database name is required'), - poolSize: z.number().int().min(1, 'Pool size must be at least 1'), - }), media: z.object({ maxFileSize: z .number() From 653eb6d93420498cc47ae5fc0e23243ed37c4ee1 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 25 Nov 2025 14:24:55 -0300 Subject: [PATCH 49/56] revert sdk methods changes --- packages/federation-sdk/src/sdk.ts | 20 +++++++++++-------- .../src/controllers/key/server.controller.ts | 2 +- .../src/middlewares/isAuthenticated.ts | 4 ++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/federation-sdk/src/sdk.ts b/packages/federation-sdk/src/sdk.ts index 819afe6c1..f8c12c19d 100644 --- a/packages/federation-sdk/src/sdk.ts +++ b/packages/federation-sdk/src/sdk.ts @@ -121,10 +121,8 @@ export class FederationSDK { return this.eduService.sendTypingNotification(...args); } - getOwnSignedServerKeyResponse( - ...args: Parameters - ) { - return this.keyService.getOwnSignedServerKeyResponse(...args); + getSignedServerKey() { + return this.keyService.getOwnSignedServerKeyResponse(); } getConfig(config: K): AppConfig[K] { @@ -136,11 +134,17 @@ export class FederationSDK { } verifyRequestSignature( - ...args: Parameters< - typeof this.signatureVerficationService.verifyRequestSignature - > + authorizationHeader: string, + method: string, + uri: string, + body?: Record, ) { - return this.signatureVerficationService.verifyRequestSignature(...args); + return this.signatureVerficationService.verifyRequestSignature({ + authorizationHeader, + method, + uri, + body, + }); } joinUser(...args: Parameters) { diff --git a/packages/homeserver/src/controllers/key/server.controller.ts b/packages/homeserver/src/controllers/key/server.controller.ts index a5c3bbc83..b1877620b 100644 --- a/packages/homeserver/src/controllers/key/server.controller.ts +++ b/packages/homeserver/src/controllers/key/server.controller.ts @@ -6,7 +6,7 @@ export const serverKeyPlugin = (app: Elysia) => { return app.get( '/_matrix/key/v2/server', async () => { - return federationSDK.getOwnSignedServerKeyResponse(); + return federationSDK.getSignedServerKey(); }, { response: { diff --git a/packages/homeserver/src/middlewares/isAuthenticated.ts b/packages/homeserver/src/middlewares/isAuthenticated.ts index 2e0e67987..26d10e6d2 100644 --- a/packages/homeserver/src/middlewares/isAuthenticated.ts +++ b/packages/homeserver/src/middlewares/isAuthenticated.ts @@ -34,12 +34,12 @@ export const isAuthenticatedMiddleware = () => { } try { - await federationSDK.verifyRequestSignature({ + await federationSDK.verifyRequestSignature( authorizationHeader, method, uri, body, - }); + ); } catch (error) { console.error('Signature verification error:', error); if ( From 0ed45f653c0edad40649a5fdaf2fb4518b906024 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 25 Nov 2025 16:11:22 -0300 Subject: [PATCH 50/56] sign join event --- packages/federation-sdk/src/services/room.service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/federation-sdk/src/services/room.service.ts b/packages/federation-sdk/src/services/room.service.ts index b546b78f4..9406b29a0 100644 --- a/packages/federation-sdk/src/services/room.service.ts +++ b/packages/federation-sdk/src/services/room.service.ts @@ -921,10 +921,9 @@ export class RoomService { makeJoinResponse.room_version, ); - // const signedJoinEvent = await stateService.signEvent(joinEvent); + // TODO stop calling stateService to mutate the event + await stateService.signEvent(joinEvent); - // TODO: sign the event here vvv - // currently makeSignedRequest does the signing const { state, auth_chain: authChain, From dbd133ae92bc8c42fd82c67fa97997f39f6394ae Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 25 Nov 2025 16:12:45 -0300 Subject: [PATCH 51/56] fix get server key --- packages/federation-sdk/src/services/key.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index b4d8f811e..37424a8b6 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -606,7 +606,7 @@ export class KeyService { { serverName: this.configService.serverName, keyId: signer.id, - key: signer.getPublicKey().toBase64(), + key: Buffer.from(signer.getPublicKey()).toString('base64'), // TODO: this isn't currently in config, nor do we handle expiration yet expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year expiration From f9f5bc10eeb6b5762546a9da92337b30b72e681f Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 25 Nov 2025 16:29:44 -0300 Subject: [PATCH 52/56] revert additional check on getRoomVersion --- .../src/repositories/state-graph.repository.ts | 10 ---------- .../federation-sdk/src/services/event.service.spec.ts | 2 +- packages/federation-sdk/src/services/state.service.ts | 11 +++-------- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/packages/federation-sdk/src/repositories/state-graph.repository.ts b/packages/federation-sdk/src/repositories/state-graph.repository.ts index 435743ef9..f799501d8 100644 --- a/packages/federation-sdk/src/repositories/state-graph.repository.ts +++ b/packages/federation-sdk/src/repositories/state-graph.repository.ts @@ -222,14 +222,4 @@ export class StateGraphRepository { { sort: { depth: -1 } }, ); } - - async findCreateEventIdByRoomId(roomId: RoomID) { - const doc = await this.collection.findOne({ - roomId, - type: 'm.room.create', - stateKey: '', - }); - - return doc?.eventId; - } } diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts index f87df38ab..47d8c3e7a 100644 --- a/packages/federation-sdk/src/services/event.service.spec.ts +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -65,7 +65,7 @@ runIfMongoExists(() => it('should fail to fetch room informatin of unknown room, sanity check for mock loading', async () => { expect( eventService.getRoomVersion({ room_id: 'abc123' } as Pdu), - ).rejects.toThrowError(/Create event not found/); + ).rejects.toThrowError('Room abc123 does not exist'); }); const { getHomeserverFinalAddress: originalServerDiscovery } = await import( diff --git a/packages/federation-sdk/src/services/state.service.ts b/packages/federation-sdk/src/services/state.service.ts index 2c934eebb..b753e150e 100644 --- a/packages/federation-sdk/src/services/state.service.ts +++ b/packages/federation-sdk/src/services/state.service.ts @@ -103,16 +103,10 @@ export class StateService { } async getRoomVersion(roomId: string): Promise { - const createEventId = await this.stateRepository.findCreateEventIdByRoomId( + const createEvent = await this.eventRepository.findByRoomIdAndType( roomId as RoomID, + 'm.room.create', ); - if (!createEventId) { - throw new Error( - 'Create event not found for room version maybe event hasn;t been processed yet', - ); - } - - const createEvent = await this.eventRepository.findById(createEventId); if (!createEvent) { throw new UnknownRoomError(roomId as RoomID); } @@ -410,6 +404,7 @@ export class StateService { // saves a full/partial state // returns the final state id async processInitialState(pdus: Pdu[], authChain: Pdu[]) { + console.log('processInitialState pdus/authChain ->', pdus, authChain); const create = authChain.find((pdu) => pdu.type === 'm.room.create'); if (create?.type !== 'm.room.create') { throw new Error('No create event found in auth chain to save'); From b79a356ef7981acaf694943604471db66deffc3a Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 25 Nov 2025 19:00:22 -0300 Subject: [PATCH 53/56] code cleanup --- .../repositories/state-graph.repository.ts | 1 - .../services/federation-request.service.ts | 30 +++++++------------ .../src/services/key.service.ts | 2 +- .../src/services/state.service.ts | 30 ++++--------------- packages/room/src/manager/event-wrapper.ts | 4 +-- tsconfig.json | 27 ++++------------- 6 files changed, 23 insertions(+), 71 deletions(-) diff --git a/packages/federation-sdk/src/repositories/state-graph.repository.ts b/packages/federation-sdk/src/repositories/state-graph.repository.ts index f799501d8..3e0bf36fc 100644 --- a/packages/federation-sdk/src/repositories/state-graph.repository.ts +++ b/packages/federation-sdk/src/repositories/state-graph.repository.ts @@ -2,7 +2,6 @@ import { type EventID, type PduType, type PersistentEventBase, - RoomID, type StateID, type StateMapKey, getStateMapKey, diff --git a/packages/federation-sdk/src/services/federation-request.service.ts b/packages/federation-sdk/src/services/federation-request.service.ts index 158ded27e..8fb605d41 100644 --- a/packages/federation-sdk/src/services/federation-request.service.ts +++ b/packages/federation-sdk/src/services/federation-request.service.ts @@ -54,20 +54,6 @@ export class FederationRequestService { url.search = queryString; } - /* - { - "method": "POST", - "uri": "/target", - "origin": "origin.hs.example.com", - "destination": "destination.hs.example.com", - "content": , - "signatures": { - "origin.hs.example.com": { - "ed25519:key1": "ABCDEF..." - } - } - } - */ // build the auth request const request = { method, @@ -79,11 +65,6 @@ export class FederationRequestService { const requestSignature = await signJson(request, signer); - // authorization_headers.append(bytes( - // "X-Matrix origin=\"%s\",destination=\"%s\",key=\"%s\",sig=\"%s\"" % ( - // origin_name, destination_name, key, sig, - // ) - // )) const authorizationHeaderValue = `X-Matrix origin="${origin}",destination="${domain}",key="${signer.id}",sig="${requestSignature}"`; const headers = { @@ -91,7 +72,6 @@ export class FederationRequestService { ...discoveryHeaders, }; - // TODO: make logging take a function for object to avoid unnecessary computation when log level is high this.logger.debug( { method, @@ -110,6 +90,16 @@ export class FederationRequestService { if (!response.ok) { const errorText = await response.text(); + + this.logger.error({ + msg: 'Federation request failed', + url, + status: response.status, + errorText, + sentHeaders: headers, + responseHeaders: response.headers, + }); + let errorDetail = errorText; try { errorDetail = JSON.stringify(JSON.parse(errorText)); diff --git a/packages/federation-sdk/src/services/key.service.ts b/packages/federation-sdk/src/services/key.service.ts index 37424a8b6..ed67c52e4 100644 --- a/packages/federation-sdk/src/services/key.service.ts +++ b/packages/federation-sdk/src/services/key.service.ts @@ -260,7 +260,7 @@ export class KeyService { // this shouldn't be here, however, to copy the controller level logic from homeserver router to rocket.chat would be a pain to keep up to date if changes are needed. for now, keeping here. async handleQuery({ server_keys: serverKeys }: QueryRequestBody) { - const serverKeysResponse = [] as KeyV2ServerResponse[]; + const serverKeysResponse: KeyV2ServerResponse[] = []; const localKeysPerServer: Map = new Map(); diff --git a/packages/federation-sdk/src/services/state.service.ts b/packages/federation-sdk/src/services/state.service.ts index b753e150e..50aa3df47 100644 --- a/packages/federation-sdk/src/services/state.service.ts +++ b/packages/federation-sdk/src/services/state.service.ts @@ -102,40 +102,21 @@ export class StateService { return event.content; } + // TODO change type to RoomID async getRoomVersion(roomId: string): Promise { const createEvent = await this.eventRepository.findByRoomIdAndType( - roomId as RoomID, + roomId, 'm.room.create', ); if (!createEvent) { throw new UnknownRoomError(roomId as RoomID); } - if (createEvent.event.type === 'm.room.create') { - // just getting typescriopt to help - return createEvent.event.content.room_version; + if (createEvent.event.type !== 'm.room.create') { + throw new Error('Create event content malformed for room version'); } - // should be unreachable - throw new Error('Create event content malformed for room version'); - } - - // helps with logging state - private logState(label: string, state: State) { - const printableState = Array.from(state.entries()).map(([key, value]) => { - return { - internalStateKey: key, - strippedEvent: { - state_key: value.stateKey, - sender: value.sender, - origin: value.origin, - content: value.getContent(), - }, - }; - }); - - // TODO: change to debug later - this.logger.info({ state: printableState }, label); + return createEvent.event.content.room_version; } private async updateNextEventReferencesWithEvent(event: PersistentEventBase) { @@ -404,7 +385,6 @@ export class StateService { // saves a full/partial state // returns the final state id async processInitialState(pdus: Pdu[], authChain: Pdu[]) { - console.log('processInitialState pdus/authChain ->', pdus, authChain); const create = authChain.find((pdu) => pdu.type === 'm.room.create'); if (create?.type !== 'm.room.create') { throw new Error('No create event found in auth chain to save'); diff --git a/packages/room/src/manager/event-wrapper.ts b/packages/room/src/manager/event-wrapper.ts index 5dc3716b7..2bd0e0dcb 100644 --- a/packages/room/src/manager/event-wrapper.ts +++ b/packages/room/src/manager/event-wrapper.ts @@ -342,9 +342,7 @@ export abstract class PersistentEventBase< // 2. The event is converted into Canonical JSON. // 3. A sha256 hash is calculated on the resulting JSON object. - const referenceHash = computeHashBuffer(toHash); - - return referenceHash; + return computeHashBuffer(toHash); } // SPEC: https://spec.matrix.org/v1.12/server-server-api/#calculating-the-content-hash-for-an-event diff --git a/tsconfig.json b/tsconfig.json index 0b212526f..748ed619b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,27 +6,12 @@ "tsBuildInfoFile": ".tsbuildinfo" }, "include": ["index.ts", "test-setup.ts"], - "exclude": [ - "node_modules", - "dist", - "packages/*/dist", - "benchmark-scripts/**" - ], + "exclude": ["node_modules", "dist", "packages/*/dist"], "references": [ - { - "path": "./packages/core" - }, - { - "path": "./packages/crypto" - }, - { - "path": "./packages/federation-sdk" - }, - { - "path": "./packages/homeserver" - }, - { - "path": "./packages/room" - } + { "path": "./packages/core" }, + { "path": "./packages/crypto" }, + { "path": "./packages/federation-sdk" }, + { "path": "./packages/homeserver" }, + { "path": "./packages/room" } ] } From 5531ab709666150ff7a0055674a1d5f79adc8404 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 25 Nov 2025 19:11:34 -0300 Subject: [PATCH 54/56] more cleanup --- .../src/services/federation-request.service.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/federation-sdk/src/services/federation-request.service.ts b/packages/federation-sdk/src/services/federation-request.service.ts index 8fb605d41..e74a22e73 100644 --- a/packages/federation-sdk/src/services/federation-request.service.ts +++ b/packages/federation-sdk/src/services/federation-request.service.ts @@ -1,14 +1,7 @@ import type { FetchResponse, MultipartResult, - SigningKey, } from '@rocket.chat/federation-core'; -import { - authorizationHeaders, - computeAndMergeHash, -} from '@rocket.chat/federation-core'; -import { extractURIfromURL } from '@rocket.chat/federation-core'; -import { EncryptionValidAlgorithm } from '@rocket.chat/federation-core'; import { createLogger } from '@rocket.chat/federation-core'; import { fetch } from '@rocket.chat/federation-core'; import { signJson } from '@rocket.chat/federation-crypto'; @@ -70,6 +63,7 @@ export class FederationRequestService { const headers = { Authorization: authorizationHeaderValue, ...discoveryHeaders, + ...(body && { 'Content-Type': 'application/json' }), }; this.logger.debug( From 665d272d24626ed15211bd90251f313cc20d20e3 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 25 Nov 2025 19:16:25 -0300 Subject: [PATCH 55/56] remove duplicated getRoomVersion --- .../src/services/event.service.spec.ts | 12 ++++++------ .../federation-sdk/src/services/event.service.ts | 7 ------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/federation-sdk/src/services/event.service.spec.ts b/packages/federation-sdk/src/services/event.service.spec.ts index 47d8c3e7a..8f8f7306d 100644 --- a/packages/federation-sdk/src/services/event.service.spec.ts +++ b/packages/federation-sdk/src/services/event.service.spec.ts @@ -62,12 +62,6 @@ const event = { runIfMongoExists(() => describe('EventService', async () => { - it('should fail to fetch room informatin of unknown room, sanity check for mock loading', async () => { - expect( - eventService.getRoomVersion({ room_id: 'abc123' } as Pdu), - ).rejects.toThrowError('Room abc123 does not exist'); - }); - const { getHomeserverFinalAddress: originalServerDiscovery } = await import( '../server-discovery/discovery' ); @@ -187,6 +181,12 @@ runIfMongoExists(() => valid_until_ts: Date.now() + 100000, }; + it('should fail to fetch room informatin of unknown room, sanity check for mock loading', async () => { + expect(stateService.getRoomVersion('abc123')).rejects.toThrowError( + 'Room abc123 does not exist', + ); + }); + // sanity check it('should sign events with new keys', async () => { const pdu = PersistentEventFactory.createFromRawEvent( diff --git a/packages/federation-sdk/src/services/event.service.ts b/packages/federation-sdk/src/services/event.service.ts index 73f733a0b..67bce6299 100644 --- a/packages/federation-sdk/src/services/event.service.ts +++ b/packages/federation-sdk/src/services/event.service.ts @@ -516,13 +516,6 @@ export class EventService { return parts.length > 1 ? parts[1] : ''; } - public async getRoomVersion(event: Pdu) { - return ( - this.stateService.getRoomVersion(event.room_id) || - PersistentEventFactory.defaultRoomVersion - ); - } - private getEventSchema(roomVersion: string, eventType: string): z.ZodSchema { const versionSchemas = eventSchemas[roomVersion]; if (!versionSchemas) { From 76a431483173d350ada66dba4e752563361bda20 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 25 Nov 2025 19:23:44 -0300 Subject: [PATCH 56/56] validate request destination --- .../src/__mocks__/services.spec.ts | 1 + .../signature-verification.service.spec.ts | 8 ++- .../signature-verification.service.ts | 51 ++++++------------- 3 files changed, 24 insertions(+), 36 deletions(-) diff --git a/packages/federation-sdk/src/__mocks__/services.spec.ts b/packages/federation-sdk/src/__mocks__/services.spec.ts index fcd66268d..fe21c67de 100644 --- a/packages/federation-sdk/src/__mocks__/services.spec.ts +++ b/packages/federation-sdk/src/__mocks__/services.spec.ts @@ -28,6 +28,7 @@ const eventEmitter = new EventEmitterService(); const signatureVerificationService = new SignatureVerificationService( keyService, + configService, ); const eventService = new EventService( diff --git a/packages/federation-sdk/src/services/signature-verification.service.spec.ts b/packages/federation-sdk/src/services/signature-verification.service.spec.ts index b2a3abfa7..546244b56 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.spec.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.spec.ts @@ -21,6 +21,7 @@ import { } from '@rocket.chat/federation-room'; import { runIfMongoExists } from '../__mocks__/block-if-no-mongo'; import { keyService } from '../__mocks__/services.spec'; +import { AppConfig, ConfigService } from './config.service'; import { SignatureVerificationService } from './signature-verification.service'; const originServer = 'syn1.tunnel.dev.rocket.chat'; @@ -93,8 +94,13 @@ runIfMongoExists(() => './signature-verification.service' ); + const configService = new ConfigService(); + configService.setConfig({ + serverName: 'syn2.tunnel.dev.rocket.chat', + } as AppConfig); + beforeEach(() => { - service = new SignatureVerificationService(keyService); // invalidates internal cache + service = new SignatureVerificationService(keyService, configService); // invalidates internal cache }); afterEach(async () => { diff --git a/packages/federation-sdk/src/services/signature-verification.service.ts b/packages/federation-sdk/src/services/signature-verification.service.ts index a5f8958f6..5e4f576bd 100644 --- a/packages/federation-sdk/src/services/signature-verification.service.ts +++ b/packages/federation-sdk/src/services/signature-verification.service.ts @@ -8,6 +8,7 @@ import { } from '@rocket.chat/federation-crypto'; import type { PersistentEventBase } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; +import { ConfigService } from './config.service'; import { KeyService } from './key.service'; export class InvalidEventSignatureError extends InvalidSignatureError { @@ -47,7 +48,10 @@ export type FederationRequest = @singleton() export class SignatureVerificationService { - constructor(private readonly keyService: KeyService) {} + constructor( + private readonly keyService: KeyService, + private readonly configService: ConfigService, + ) {} private readonly logger = createLogger('SignatureVerificationService'); @@ -127,6 +131,7 @@ export class SignatureVerificationService { // `X-Matrix origin="${origin}",destination="${destination}",key="${key}",sig="${signed}"` const regex = /\b(origin|destination|key|sig)="([^"]+)"/g; + const { origin, destination, @@ -152,20 +157,12 @@ export class SignatureVerificationService { ); } - /* - { - "method": "POST", - "uri": "/target", - "origin": "origin.hs.example.com", - "destination": "destination.hs.example.com", - "content": , - "signatures": { - "origin.hs.example.com": { - "ed25519:key1": "ABCDEF..." - } - } - } - */ + if (destination !== this.configService.serverName) { + throw new FailedSignatureVerificationPreconditionError( + 'Header destination does not match this server name', + ); + } + const toVerify = { method, uri, @@ -177,28 +174,12 @@ export class SignatureVerificationService { }, }; - if (verifier) { - try { - await this.verifySignature(toVerify, origin, verifier); - return origin; - } catch (error) { - if (error instanceof InvalidSignatureError) { - throw new InvalidRequestSignatureError( - `Invalid request signature from origin ${origin}: ${error.message}`, - ); - } - - throw error; - } - } - - const requiredVerifier = await this.keyService.getRequestVerifier( - origin, - key, - ); + const signVerifier = + verifier || (await this.keyService.getRequestVerifier(origin, key)); try { - await this.verifySignature(toVerify, origin, requiredVerifier); + await this.verifySignature(toVerify, origin, signVerifier); + return origin; } catch (error) { if (error instanceof InvalidSignatureError) {