Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
27a20b8
Validate channel metadata authorization
Jun 19, 2026
c99ca22
add validation for DEL to avoid DEL then spoof
adrastaea Jun 19, 2026
503558c
verify owner, enc signature author, and entry writer id are the same
adrastaea Jun 20, 2026
8b4fa45
optimistically prevent malicious updates when already initialized
adrastaea Jun 20, 2026
dc250b8
Implement channel metadata access control and migration logic
adrastaea Jun 24, 2026
9bf675e
Merge branch 'develop' of https://github.com/TryQuiet/quiet into taea…
adrastaea Jun 26, 2026
529b8a9
Revert "chore: disable private channels in UI (#3318)"
adrastaea Jun 26, 2026
ce32843
fix: store private channel metadata separately
adrastaea Jun 26, 2026
3e5c7e7
make puts immutable
adrastaea Jun 26, 2026
e3c0ef9
rm migration; optimize e2e test
adrastaea Jun 26, 2026
4752e7e
fix plaintext channel name leak
adrastaea Jun 29, 2026
8303aad
improve store management and error handling in channel and notificati…
adrastaea Jun 30, 2026
7cfcdae
fix linting
adrastaea Jun 30, 2026
0dfa522
feat(channels): enhance channel subscription handling and metadata up…
adrastaea Jul 1, 2026
ad04c09
update community creation tests with channel subscription handling; a…
adrastaea Jul 1, 2026
0751817
untangle store close handling to keep unrelated stores registered whe…
adrastaea Jul 1, 2026
c379256
refactor(sidebar): enhance waitForChannels method for improved channe…
adrastaea Jul 2, 2026
bf083b1
Merge branch 'develop' into taea/channel-metadata-validation-develop
adrastaea Jul 2, 2026
d193736
Add tests for wait for subscription saga
islathehut Jul 3, 2026
caba20a
Revert e2e test changes
islathehut Jul 3, 2026
a11ce91
Merge branch 'develop' into taea/channel-metadata-validation-develop
islathehut Jul 3, 2026
c988a1c
Fix test post develop merge
islathehut Jul 3, 2026
cf840fb
Mild refactor to consolidate logic
islathehut Jul 3, 2026
ddcb229
Update CHANGELOG
islathehut Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
* Don't send deletion message for private channels [#3273](https://github.com/TryQuiet/quiet/issues/3273)
* Ensure notification registration waits for auth handshake [#3289](https://github.com/TryQuiet/quiet/issues/3289)
* Fixed a race condition that can cause stale data to remain after leaving community [#3253](https://github.com/TryQuiet/quiet/issues/3253)
* Temporarily disable private channels
* Validate user ID on decrypted message matches the signature [#3334](https://github.com/TryQuiet/quiet/issues/3334)

### Chores
Expand All @@ -25,6 +24,7 @@
* Include team ID and createdAt in message encryption and validate on consume [#3304](https://github.com/TryQuiet/quiet/issues/3304)
* Require team ID on invite links, use team ID for all chain operations, and hash team name on sigchains [#3296](https://github.com/TryQuiet/quiet/issues/3296)
* Use randomly generated Base58 usernames on sigchain [#3321](https://github.com/TryQuiet/quiet/issues/3321)
* Tighten controls on channel metadata DB operations, move private channels to separate metadata DB [#3329](https://github.com/TryQuiet/quiet/issues/3329)

## [7.3.0]

Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/nest/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ChannelStore } from '../storage/channels/channel.store'
export interface ChannelRepo {
store: ChannelStore
eventsAttached: boolean
subscribed: boolean
subscriptionPromise?: Promise<void>
public: boolean
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ describe(`OrbitDB Syncing with ${N_PEERS} peers`, () => {
expect(channel).toBeDefined()
const getChannels = await channelsService.getChannels()
expect(getChannels.length).toBe(2)
expect(getChannels[1].id).toBe(newChannel.id)
expect(getChannels.find(channel => channel.id === newChannel.id)).toBeDefined()

// Send a message in the new channel
const message = await factory.build<ChannelMessage>('ChannelMessage', {
Expand Down Expand Up @@ -494,7 +494,7 @@ describe(`OrbitDB Syncing with ${N_PEERS} peers`, () => {
const channelsService = modules[i].get(ChannelsService)
const channels = await channelsService.getChannels()
expect(channels.length).toBe(1) // Only the first channel should be present
expect(channels[0].id).toBe(publicChannels[0].id)
expect(channels.find(channel => channel.id === publicChannels[0].id)).toBeDefined()
}
})

Expand Down
76 changes: 76 additions & 0 deletions packages/backend/src/nest/storage/channels/channel.store.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import EventEmitter from 'node:events'
import { jest } from '@jest/globals'

import { ChannelStore } from './channel.store'
import { StorageEvents } from '../storage.types'

describe('ChannelStore', () => {
const makeLogger = () => ({
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
trace: jest.fn(),
warn: jest.fn(),
})

async function* emptyIterator() {}

it('ignores update entries without a matching channel id', async () => {
const channelId = 'channel-1'
const storeEvents = new EventEmitter()
const messagesService = {
onConsume: jest.fn(),
}
const localDbService = {
getCurrentCommunity: jest.fn(async () => ({ id: 'community-1' })),
}
const auth = Object.assign(new EventEmitter(), {
team: { id: 'team-1' },
})
const channelStore = new ChannelStore(
{} as any,
localDbService as any,
messagesService as any,
{} as any,
{} as any,
auth as any,
{} as any,
{} as any
)
;(channelStore as any).channelData = {
id: channelId,
name: 'general',
public: true,
teamId: 'team-1',
}
;(channelStore as any).logger = makeLogger()
;(channelStore as any)._messagesService = messagesService
;(channelStore as any).store = {
events: storeEvents,
iterator: emptyIterator,
sync: {
start: jest.fn(async () => {}),
},
}

const messageIdsListener = jest.fn()
channelStore.on(StorageEvents.MESSAGE_IDS_STORED, messageIdsListener)
await channelStore.subscribe()
messageIdsListener.mockClear()
localDbService.getCurrentCommunity.mockClear()

storeEvents.emit('update', {
hash: 'non-message-entry',
payload: {
value: {
id: 'non-message-value',
},
},
})
await new Promise(resolve => setImmediate(resolve))

expect(messagesService.onConsume).not.toHaveBeenCalled()
expect(localDbService.getCurrentCommunity).not.toHaveBeenCalled()
expect(messageIdsListener).not.toHaveBeenCalled()
})
})
26 changes: 22 additions & 4 deletions packages/backend/src/nest/storage/channels/channel.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ export class ChannelStore extends EventStoreBase<EncryptedMessage, ConsumedChann
this.getStore().events.on('update', async (entry: LogEntry<EncryptedMessage>) => {
const entryChannelId = entry.payload.value?.channelId
// TODO: seperate event bus for each channel so we don't have to check this on every update
if (entryChannelId != null && entryChannelId !== this.channelData.id) {
if (entryChannelId !== this.channelData.id) {
this.logger.debug(
`Ignoring database update for different channel`,
`Ignoring database update without matching channel`,
entry.hash,
entryChannelId,
this.channelData.id
Expand Down Expand Up @@ -374,8 +374,20 @@ export class ChannelStore extends EventStoreBase<EncryptedMessage, ConsumedChann
*/
public async close(): Promise<void> {
this.logger.info(`Closing channel store`)
const store = this.store
if (store == null) {
this.logger.warn(`Store is already undefined, nothing to close`)
return
}

await this.stopSync()
await this.getStore().close()
await store.close()
if (this.authListenerAttached) {
this.auth.removeListener(SigchainEvents.UPDATED, this.handleAuthUpdated)
this.authListenerAttached = false
}
this.store = undefined
this._subscribing = false
}

/**
Expand All @@ -393,6 +405,7 @@ export class ChannelStore extends EventStoreBase<EncryptedMessage, ConsumedChann
*/
public async clean(): Promise<void> {
this.logger.info(`Cleaning channel store`, this.channelData.id, this.channelData.name)
const store = this.store
try {
await this.stopSync()
} catch (e) {
Expand All @@ -402,11 +415,16 @@ export class ChannelStore extends EventStoreBase<EncryptedMessage, ConsumedChann
if (!this.store) {
this.logger.warn(`Store is already undefined, nothing to drop`)
} else {
await this.getStore().drop()
await store!.drop()
}
} catch (e) {
this.logger.error(`Failed to drop store`, e)
}
try {
await store?.close()
} catch (e) {
this.logger.error(`Failed to close store after drop`, e)
}
if (this.authListenerAttached) {
this.auth.removeListener(SigchainEvents.UPDATED, this.handleAuthUpdated)
this.authListenerAttached = false
Expand Down
Loading
Loading