Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
5dcb8b3
Add lockbox service and create an invite lockbox on invite creation
islathehut Jan 12, 2026
50ffb68
Update changelog
islathehut Jan 12, 2026
f6b7caa
Self-assign member role on join with QSS
islathehut Jan 16, 2026
4213306
Fix self-assign
islathehut Jan 19, 2026
9b8cf70
Use event to trigger storage setup after self-assign
islathehut Jan 26, 2026
42f6fbe
Merge branch 'develop' into feat/3058-invite-self-assign
islathehut Jan 30, 2026
86a2848
Pull log entries when fully joined via qss (update later to handle jo…
islathehut Jan 26, 2026
b4a05c6
Move identitieswithstorage
islathehut Jan 26, 2026
1852f62
Get janky LFA identity working with orbitdb and get syncing on join w…
islathehut Jan 27, 2026
418c3a0
Also pull entries on connection to qss when already a member
islathehut Jan 27, 2026
7782134
Remove debugging log
islathehut Jan 27, 2026
4def5be
Add comments
islathehut Jan 29, 2026
60fda35
Add comments and return random signature
islathehut Jan 29, 2026
9ea6475
Update qss and auth modules to use feature branches for testing
islathehut Jan 29, 2026
e16c83a
Update qss e2e test to include joining without peers
islathehut Jan 30, 2026
d1d358d
Add self-assign unit tests and fix some unit tests post-LFA identity
islathehut Feb 2, 2026
6f015a7
Fix userProfile integration tests
islathehut Feb 2, 2026
96341eb
Update submodules
islathehut Feb 2, 2026
7e5b1ff
Remove changes left in from testing
islathehut Feb 2, 2026
c6dbdab
Fix last of integration tests and add initializing check to storage i…
islathehut Feb 3, 2026
3162cc7
Forgot a comment
islathehut Feb 3, 2026
f42e66e
Update CHANGELOG.md
islathehut Feb 3, 2026
6d1b056
Update CHANGELOG.md
islathehut Feb 3, 2026
2c5f555
Allow strings
islathehut Feb 3, 2026
11af44c
Missed one unit test update
islathehut Feb 3, 2026
4d2cd3f
Merge branch 'develop' into feat/3058-invite-self-assign
islathehut Feb 4, 2026
9ecdede
Baseline create private channels
islathehut Feb 11, 2026
972326e
Add indicators for channel privacy
islathehut Feb 11, 2026
6191a32
Add start of add member form
islathehut Feb 13, 2026
9a26e3d
Merge branch 'develop' into hack/private-channels
islathehut Mar 18, 2026
3326f56
Update users on chain creation and use correct encryption for private…
islathehut Mar 18, 2026
2548166
Implement backend/state manager for adding members
islathehut Mar 18, 2026
accb179
Merge branch 'develop' into hack/private-channels
islathehut Mar 23, 2026
b57327a
Reformat lock icons in channel titles
islathehut Mar 23, 2026
c50e892
Properly pass member channel IDs to state manager and use them for ui…
islathehut Mar 24, 2026
71a7b2e
Encrypt private channel metadata with channel key
islathehut Mar 25, 2026
324219d
Use lock icon from figma
islathehut Mar 30, 2026
f9409dc
Fix formatting on channel list
islathehut Mar 30, 2026
b88b724
Better match figma design for channel creation
islathehut Mar 30, 2026
f5263a1
Update CreateChannelComponent.tsx
islathehut Mar 30, 2026
d055e60
Move iosswitch to its own file and update snapshots
islathehut Mar 30, 2026
b377937
Clean up and only update member list for add member list when modal i…
islathehut Mar 31, 2026
3fda7ad
Merge branch 'develop' into hack/private-channels
islathehut Mar 31, 2026
d49b675
Update submodules
islathehut Apr 3, 2026
d66e6e7
Merge branch 'develop' into feat/3155-private-channels
islathehut Apr 3, 2026
973bef5
Fix babel dependency vulnerability
islathehut Apr 3, 2026
e411a82
More dependency fixes
islathehut Apr 3, 2026
bab994f
Fix rtl test
islathehut Apr 3, 2026
7f6fef1
Fix tests
islathehut Apr 6, 2026
c1044e0
Remove user profile from channel list item props and mild tweaks to f…
islathehut Apr 6, 2026
6498720
Stashing e2e changes
islathehut Apr 7, 2026
a2cbae9
Finalize private channel e2e test
islathehut Apr 7, 2026
ece7f94
Add qss private channels e2e tests
islathehut Apr 9, 2026
8668e6e
Go back to using PublicChannel type names to reduce PR size
islathehut Apr 9, 2026
cab5d13
Test files in private channels e2e, tweak test IDs and check for lock…
islathehut Apr 10, 2026
4cb88fc
use enum for sigchain events
islathehut Apr 10, 2026
297c842
Clean up logs, update tests, add unit tests for add member saga
islathehut Apr 10, 2026
d64d851
Create channels.service.spec.ts
islathehut Apr 13, 2026
6f985b1
Pass team ID to user update method and reduce duplicated code in acce…
islathehut Apr 14, 2026
98c9f76
Fix user profile test
islathehut Apr 14, 2026
f930b2b
Update CHANGELOG.md
islathehut Apr 14, 2026
c8b16a1
Fill in lock icon contextually and use hash icon
islathehut Apr 16, 2026
aa31c10
Update e2e tests to match new icons
islathehut Apr 16, 2026
f71614f
Missed some test updates
islathehut Apr 16, 2026
f20812b
Update oneClient.test.ts
islathehut Apr 17, 2026
4a49c64
Update multipleClients.test.ts
islathehut Apr 17, 2026
9234f77
Update oneClient.qss.test.ts
islathehut Apr 17, 2026
0fc8fd5
Update channel name in context menu title and add members dialog to u…
islathehut Apr 17, 2026
b701e99
Update AddMembersChannel.test.tsx
islathehut Apr 17, 2026
0865fb2
Fix test issue with icon on channel context menu
islathehut Apr 20, 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
7 changes: 7 additions & 0 deletions .github/workflows/e2e-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ jobs:
timeout_minutes: 25
max_attempts: 3
command: cd packages/e2e-tests && npm run test multipleClients.test.ts

- name: Run multiple clients test (private channels)
uses: nick-fields/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0
with:
timeout_minutes: 25
max_attempts: 3
command: cd packages/e2e-tests && npm run test multipleClients.privateChannels.test.ts

- name: Run invitation link test - Includes 2 separate application clients
uses: nick-fields/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/e2e-mac.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ jobs:
timeout_minutes: 25
max_attempts: 3
command: cd packages/e2e-tests && npm run test multipleClients.test.ts

- name: Run multiple clients test (private channels)
uses: nick-fields/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0
with:
timeout_minutes: 25
max_attempts: 3
command: cd packages/e2e-tests && npm run test multipleClients.privateChannels.test.ts

- name: Run invitation link test - Includes 2 separate application clients
uses: nick-fields/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/e2e-qss-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ jobs:
max_attempts: 3
command: cd packages/e2e-tests && npm run test multipleClients.qss.test.ts

- name: Run multiple clients with QSS test (private channels)
uses: nick-fields/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0
with:
timeout_minutes: 25
max_attempts: 3
command: cd packages/e2e-tests && npm run test multipleClients.privateChannels.qss.test.ts

- name: Run one client with QSS test
uses: nick-fields/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0
with:
Expand Down
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[submodule "3rd-party/auth"]
path = 3rd-party/auth
url = https://github.com/TryQuiet/auth.git
branch = main
branch = feat/3155-private-channels
[submodule "3rd-party/js-libp2p-noise"]
path = 3rd-party/js-libp2p-noise
url = https://github.com/TryQuiet/js-libp2p-noise.git
Expand All @@ -13,4 +13,4 @@
[submodule "3rd-party/qss"]
path = 3rd-party/qss
url = https://github.com/TryQuiet/quiet-storage-service.git
branch = main
branch = feat/3155-private-channels
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* Use LFA-based identity in OrbitDB
* Requests iOS notification permissions when app launches [#3079](https://github.com/TryQuiet/quiet/issues/3079)
* Adds push notification service [#3086](https://github.com/TryQuiet/quiet/issues/3086)
* Adds private channels [#3155](https://github.com/TryQuiet/quiet/issues/3155)

### Fixes

Expand Down
71 changes: 71 additions & 0 deletions packages/backend/src/nest/auth/services/roles/channel.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Handles role-related chain operations
*/

import { SigChain } from '../../sigchain'
import { ChainServiceBase } from '../chainServiceBase'
import { Member } from '@localfirst/auth'
import { createLogger } from '../../../common/logger'
import { hash } from '@localfirst/crypto'

const logger = createLogger('auth:channelService')

class ChannelService extends ChainServiceBase {
constructor(sigChain: SigChain) {
super(sigChain)
}

public create(channelId: string): string {
const roleName = this.generateChannelRoleName(channelId)
logger.info(`Adding new channel role with name ${roleName}`)
this.sigChain.roles.create(roleName)
return roleName
}

public createWithMembers(channelId: string, memberIdsForChannel: string[]): string {
const roleName = this.create(channelId)
for (const memberId of memberIdsForChannel) {
this.addMember(memberId, channelId)
}
return roleName
}

public addMember(memberId: string, channelId: string) {
logger.info(`Adding member with ID ${memberId} to channel ${channelId}`)
const roleName = this.generateChannelRoleName(channelId)
this.sigChain.roles!.addMember(memberId, roleName)
}

public memberInChannel(memberId: string, channelId: string): boolean {
const roleName = this.generateChannelRoleName(channelId)
return this.sigChain.roles.memberHasRole(memberId, roleName)
}

public amIMemberOfChannel(channelId: string): boolean {
const roleName = this.generateChannelRoleName(channelId)
return this.sigChain.roles.amIMemberOfRole(roleName)
}

public getMembersInChannel(channelId: string): Member[] {
const roleName = this.generateChannelRoleName(channelId)
return this.sigChain.roles.getMembersForRole(roleName)
}

public revokeMembership(memberId: string, channelId: string) {
logger.info(`Revoking membership of channel ${channelId} for member with ID ${memberId}`)
const roleName = this.generateChannelRoleName(channelId)
this.sigChain.roles.revokeMembership(memberId, roleName)
}

public delete(channelId: string) {
logger.info(`Removing role for channel ${channelId}`)
const roleName = this.generateChannelRoleName(channelId)
this.sigChain.roles.delete(roleName)
}

public generateChannelRoleName(channelId: string): string {
return hash(this.sigChain.team!.id, `private_channel_${channelId}`)
}
}

export { ChannelService }
105 changes: 105 additions & 0 deletions packages/backend/src/nest/auth/services/roles/channels.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { SigChain } from '../../sigchain'
import { createLogger } from '../../../common/logger'
import { RoleName } from './roles'
import { hash, randomBytes } from '@localfirst/crypto'
import * as uint8arrays from 'uint8arrays'
import { generateProof, InviteResult, MemberContext, redactKeys, Team } from '@localfirst/auth'
import { InviteLockboxMetadata } from '../crypto/types'

const logger = createLogger('auth:services:channels.spec')

describe('channels', () => {
let adminSigChain: SigChain
let secondSigChain: SigChain
const adminUsername = 'admin'
const secondUsername = 'seconduser'
const teamName = 'test'
let invite: InviteResult
let seed: string
let salt: string
let generatedKeys: InviteLockboxMetadata
const channelId = 'foobar'

it('should initialize a new sigchain and be admin', () => {
adminSigChain = SigChain.create(teamName, adminUsername)
expect(adminSigChain).toBeDefined()
expect(adminSigChain.context).toBeDefined()
expect(adminSigChain.team!.teamName).toBe(teamName)
expect(adminSigChain.user.userName).toBe(adminUsername)
expect(adminSigChain.roles.amIMemberOfRole(RoleName.ADMIN)).toBe(true)
expect(adminSigChain.roles.amIMemberOfRole(RoleName.MEMBER)).toBe(true)
})
it('should create channel and admin should be added as member', () => {
const channel = adminSigChain.channels.create(channelId)
expect(channel).toBeDefined()
expect(channel).toBe(adminSigChain.channels.generateChannelRoleName(channelId))
expect(adminSigChain.channels.amIMemberOfChannel(channelId))
})
it('should create an invite', () => {
invite = adminSigChain.invites.createUserInvite()
expect(invite).toBeDefined()
})
it('should create keys from seed and salt for lockboxes', () => {
seed = invite.seed
salt = uint8arrays.toString(randomBytes(32), 'hex')
generatedKeys = adminSigChain.lockbox.generateLockboxKeys(seed, salt)
expect(generatedKeys.id).toBe(hash(salt, seed))
expect(generatedKeys.keys.name).toBe(generatedKeys.id)
expect(generatedKeys.keys.generation).toBe(0)
})
it('should create a lockbox encrypted to our generated keys with MEMBER keys', () => {
const lockboxes = adminSigChain.lockbox.createInviteLockboxes(seed, salt)
expect(lockboxes).toHaveLength(1)
const keysFromLockbox = adminSigChain.team?.allKeys(generatedKeys.keys)
expect(keysFromLockbox).toBeDefined()
expect(keysFromLockbox!['ROLE'][RoleName.MEMBER].length).toBe(1)
})
it('should create second user who is not admin', () => {
secondSigChain = SigChain.createFromInvite(secondUsername, invite.seed)
expect(secondSigChain).toBeDefined()
expect(secondSigChain.context).toBeDefined()
expect(secondSigChain.context.user.userName).toBe(secondUsername)
})
it('should add second user to team', () => {
const proof = generateProof(invite.seed)
adminSigChain.invites.admitMemberFromInvite(
proof,
secondUsername,
secondSigChain.context.user.userId,
redactKeys(secondSigChain.context.user.keys)
)
expect(adminSigChain.users.getUserByName(secondUsername)).toBeDefined()

const teamBytes = adminSigChain.save()
const teamKeyring = adminSigChain.team!.teamKeyring()
expect(teamKeyring).toBeDefined()
const loadedTeam = new Team({
source: teamBytes,
context: {
device: secondSigChain.context.device,
user: secondSigChain.user,
},
teamKeyring,
})
loadedTeam.join(teamKeyring)
secondSigChain.context = {
device: secondSigChain.context.device,
team: loadedTeam,
user: secondSigChain.user,
} as MemberContext
expect(secondSigChain.team).toBeDefined()
})
it('should self-assign MEMBER role on second user', () => {
secondSigChain.roles.addSelf(RoleName.MEMBER, seed, salt)
expect(secondSigChain.roles.amIMemberOfRole(RoleName.MEMBER)).toBe(true)
})
it('should fail to self-assign channel role on second user', () => {
const failedSelfAssign = () =>
secondSigChain.roles.addSelf(secondSigChain.channels.generateChannelRoleName(channelId), seed, salt)
expect(failedSelfAssign).toThrow()
})
it('should add second user to channel', () => {
adminSigChain.channels.addMember(secondSigChain.context.user.userId, channelId)
expect(adminSigChain.channels.memberInChannel(secondSigChain.context.user.userId, channelId)).toBe(true)
})
})
6 changes: 3 additions & 3 deletions packages/backend/src/nest/auth/services/roles/role.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { SigChain } from '../../sigchain'
import { ChainServiceBase } from '../chainServiceBase'
import { Permissions } from './permissions'
import { QuietRole, RoleName, SELF_ASSIGN_ROLES } from './roles'
import { Member, PermissionsMap, Role } from '@localfirst/auth'
import { AddRoleInput, Member, PermissionsMap, Role } from '@localfirst/auth'
import { createLogger } from '../../../common/logger'

const logger = createLogger('auth:roleService')
Expand All @@ -23,12 +23,12 @@ class RoleService extends ChainServiceBase {
permissions[Permissions.MODIFIABLE_MEMBERSHIP] = true
}

const role: Role = {
const input: AddRoleInput = {
roleName,
permissions,
}

this.sigChain.team!.addRole(role)
this.sigChain.team!.addRole(input)
}

// TODO: figure out permissions
Expand Down
10 changes: 10 additions & 0 deletions packages/backend/src/nest/auth/sigchain.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ describe('SigChainService', () => {
let module: TestingModule
let sigChainService: SigChainService
let localDbService: LocalDbService
let handleChainUpdateSpy: jest.SpiedFunction<any>

beforeAll(async () => {
module = await Test.createTestingModule({
imports: [TestModule, SigChainModule, LocalDbModule],
}).compile()
sigChainService = await module.resolve(SigChainService)
localDbService = await module.resolve(LocalDbService)
handleChainUpdateSpy = jest.spyOn(sigChainService as any, 'handleChainUpdate').mockImplementation(() => {
logger.debug('MOCK: handling chain update')
})
})

beforeEach(async () => {
Expand All @@ -29,6 +33,7 @@ describe('SigChainService', () => {
})

afterAll(async () => {
handleChainUpdateSpy.mockReset()
await localDbService.close()
await module.close()
})
Expand All @@ -42,12 +47,14 @@ describe('SigChainService', () => {
it('should add a new chain and it not be active if not set to be', async () => {
const sigChain = await sigChainService.createChain('test', 'user', false)
expect(() => sigChainService.getActiveChain()).toThrowError()
expect(handleChainUpdateSpy).toBeCalledTimes(1)
sigChainService.setActiveChain('test')
expect(sigChainService.getActiveChain()).toBe(sigChain)
})
it('should add a new chain and it be active if set to be', async () => {
const sigChain = await sigChainService.createChain('test2', 'user2', true)
expect(sigChainService.getActiveChain()).toBe(sigChain)
expect(handleChainUpdateSpy).toBeCalledTimes(1)
const prevSigChain = sigChainService.getChain({ teamName: 'test' })
expect(prevSigChain).toBeDefined()
expect(prevSigChain).not.toBe(sigChain)
Expand All @@ -65,6 +72,7 @@ describe('SigChainService', () => {
it('should save and load sigchain using nestjs service', async () => {
const TEAM_NAME = 'test3'
const sigChain = await sigChainService.createChain(TEAM_NAME, 'user', true)
expect(handleChainUpdateSpy).toBeCalledTimes(1)
await sigChainService.saveChain(TEAM_NAME)
await sigChainService.deleteChain(TEAM_NAME, false)
const loadedSigChain = await sigChainService.loadChain(TEAM_NAME, true)
Expand All @@ -79,6 +87,7 @@ describe('SigChainService', () => {
it('should not allow duplicate chains to be added', async () => {
await sigChainService.createChain('test4', 'user4', false)
await expect(sigChainService.createChain('test4', 'user4', false)).rejects.toThrowError()
expect(handleChainUpdateSpy).toBeCalledTimes(1)
})
it('should handle concurrent chain operations correctly', async () => {
const TEAM_NAME1 = 'test6'
Expand All @@ -89,5 +98,6 @@ describe('SigChainService', () => {
])
expect(sigChainService.getChain({ teamName: TEAM_NAME1 })).toBeDefined()
expect(sigChainService.getChain({ teamName: TEAM_NAME2 })).toBeDefined()
expect(handleChainUpdateSpy).toBeCalledTimes(2)
})
})
Loading
Loading