diff --git a/package-lock.json b/package-lock.json index c02c412..07fec9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@doist/cli-core": "0.26.2", - "@doist/comms-sdk": "0.11.0", + "@doist/comms-sdk": "2.0.1", "@pnpm/tabtab": "0.5.4", "chalk": "5.6.2", "commander": "14.0.3", @@ -183,19 +183,20 @@ } }, "node_modules/@doist/comms-sdk": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@doist/comms-sdk/-/comms-sdk-0.11.0.tgz", - "integrity": "sha512-vxMa8Ir2zTIdxFpDVvJO085k2YQ/FW0Q/bSZUIjcpebAKM5PSnRWk6VkF4ShUuBTOj8aFftMnhnb/lFKHwdRZQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@doist/comms-sdk/-/comms-sdk-2.0.1.tgz", + "integrity": "sha512-psbI4hx9ZHONwJawfvxPcmYDZ5tt6/4MhzpyrC5FIVfSDkbxx2sBYtExt0GC9moteFUOVRgz6Ya1cwAIz4RxhA==", "license": "MIT", "dependencies": { "camelcase": "9.0.0", "ts-custom-error": "3.3.1", - "undici": "7.28.0", + "undici": "7.29.0", "uuid": "11.1.1", "zod": "4.4.3" }, "engines": { - "node": ">=20.18.1" + "node": ">=24", + "npm": ">=11" } }, "node_modules/@doist/comms-sdk/node_modules/camelcase": { @@ -9736,9 +9737,9 @@ } }, "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "license": "MIT", "engines": { "node": ">=20.18.1" diff --git a/package.json b/package.json index 2f76c66..2b96d04 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ ], "dependencies": { "@doist/cli-core": "0.26.2", - "@doist/comms-sdk": "0.11.0", + "@doist/comms-sdk": "2.0.1", "@pnpm/tabtab": "0.5.4", "chalk": "5.6.2", "commander": "14.0.3", diff --git a/src/commands/channel/members.test.ts b/src/commands/channel/members.test.ts index e8d98b3..ed079ce 100644 --- a/src/commands/channel/members.test.ts +++ b/src/commands/channel/members.test.ts @@ -108,6 +108,27 @@ describe('tdc channel members list (default)', () => { ]) }) + it('omits email for restricted members', async () => { + refsMocks.resolveChannelRef.mockResolvedValue(createChannel([6])) + apiMocks.getCommsClient.mockResolvedValue({ + workspaceUsers: { + getUserById: vi.fn().mockResolvedValue({ + id: 6, + fullName: 'Restricted User', + email: 'restricted@example.com', + restricted: true, + }), + }, + }) + const consoleSpy = captureConsole('log') + const program = createProgram() + + await program.parseAsync(['node', 'tdc', 'channel', 'members', 'General', '--json']) + + const payload = JSON.parse(consoleSpy.mock.calls[0][0] as string) + expect(payload.members[0]).toEqual({ id: 6, name: 'Restricted User', email: null }) + }) + it('falls back to user: for unknown members', async () => { refsMocks.resolveChannelRef.mockResolvedValue(createChannel([99])) const consoleSpy = captureConsole('log') diff --git a/src/commands/channel/members.ts b/src/commands/channel/members.ts index 4afc403..774240e 100644 --- a/src/commands/channel/members.ts +++ b/src/commands/channel/members.ts @@ -1,3 +1,4 @@ +import { isRestrictedWorkspaceUser } from '@doist/comms-sdk' import { getCurrentWorkspaceId, getWorkspaceGroups } from '../../lib/api.js' import type { ViewOptions } from '../../lib/options.js' import { colors, formatJson, formatNdjson, pluralize } from '../../lib/output.js' @@ -21,7 +22,8 @@ export async function listChannelMembers( const members = userIds.map((id) => { const user = userMap.get(id) - return { id, name: user?.fullName ?? null, email: user?.email ?? null } + const email = user && !isRestrictedWorkspaceUser(user) ? (user.email ?? null) : null + return { id, name: user?.fullName ?? null, email } }) const slimPayload = { diff --git a/src/commands/channel/membership-helpers.ts b/src/commands/channel/membership-helpers.ts index 0285b75..acd95e3 100644 --- a/src/commands/channel/membership-helpers.ts +++ b/src/commands/channel/membership-helpers.ts @@ -1,4 +1,4 @@ -import type { Channel, Group, WorkspaceUser } from '@doist/comms-sdk' +import type { Channel, Group, VisibleWorkspaceUser } from '@doist/comms-sdk' import { addUsersToChannel, getCommsClient, @@ -20,7 +20,7 @@ export function channelUserIds(channel: Channel): number[] { export async function fetchUsersByIds( workspaceId: number, userIds: number[], -): Promise> { +): Promise> { if (userIds.length === 0) return new Map() const client = await getCommsClient() // Per-member fetch keeps latency tied to channel size, not workspace size @@ -35,7 +35,7 @@ export async function fetchUsersByIds( } }), ) - const map = new Map() + const map = new Map() for (const entry of entries) { if (entry) map.set(entry[0], entry[1]) } diff --git a/src/commands/groups/groups.test.ts b/src/commands/groups/groups.test.ts index 7eebb46..7dbefaa 100644 --- a/src/commands/groups/groups.test.ts +++ b/src/commands/groups/groups.test.ts @@ -232,6 +232,22 @@ describe('tdc groups view', () => { expect(output).not.toHaveProperty('version') }) + it('omits email for restricted members', async () => { + mockGetUserById.mockResolvedValueOnce({ + id: 1, + fullName: 'Restricted User', + email: 'restricted@example.com', + restricted: true, + }) + const program = createProgram() + const consoleSpy = captureConsole('log') + + await program.parseAsync(['node', 'tdc', 'groups', 'view', 'Frontend', '--json']) + + const output = JSON.parse(consoleSpy.mock.calls[0][0]) + expect(output.members[0]).toEqual({ id: 1, name: 'Restricted User', email: null }) + }) + it('renders user:N for members whose lookup fails', async () => { mockGetUserById.mockImplementationOnce(async () => { throw new Error('User not found') diff --git a/src/commands/groups/view.ts b/src/commands/groups/view.ts index 747cc32..740270b 100644 --- a/src/commands/groups/view.ts +++ b/src/commands/groups/view.ts @@ -1,3 +1,4 @@ +import { isRestrictedWorkspaceUser } from '@doist/comms-sdk' import { getCommsClient, getCurrentWorkspaceId } from '../../lib/api.js' import type { ViewOptions } from '../../lib/options.js' import { colors, formatJson, formatNdjson, pluralize } from '../../lib/output.js' @@ -18,7 +19,8 @@ export async function viewGroup(ref: string, options: GroupViewOptions): Promise workspaceId, userId: id, }) - return { id, name: user.fullName, email: user.email ?? null } + const email = isRestrictedWorkspaceUser(user) ? null : (user.email ?? null) + return { id, name: user.fullName, email } } catch { return { id, name: null as string | null, email: null as string | null } }