Skip to content

Commit 644e0c9

Browse files
committed
Merge remote-tracking branch 'origin/staging' into fix/integration-defect-ledger
2 parents c2b13e7 + 5112b07 commit 644e0c9

38 files changed

Lines changed: 1349 additions & 162 deletions

File tree

apps/desktop/src/main/ipc.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ import {
130130
} from '@/main/browser-import'
131131
import { getSearchSuggestions } from '@/main/browser-search/suggestions'
132132
import { trackInputActivity } from '@/main/input-activity'
133-
import { type IpcDeps, registerIpcHandlers } from '@/main/ipc'
133+
import { type IpcDeps, openMicrophoneSettings, registerIpcHandlers } from '@/main/ipc'
134134
import { LocalFilesystemService } from '@/main/local-filesystem'
135135
import { TerminalRegistry } from '@/main/terminal/registry'
136136
import { findCachedTerminalThemeProfile, listTerminalThemeProfiles } from '@/main/terminal-themes'
@@ -337,6 +337,26 @@ describe('registerIpcHandlers', () => {
337337
expect(shell.openExternal).toHaveBeenCalledTimes(1)
338338
})
339339

340+
it('opens microphone privacy settings only for the trusted app origin', async () => {
341+
const { invoke } = collectHandlers()
342+
const handler = invoke.get('desktop:open-microphone-settings')
343+
344+
expect(await handler?.(evilEvent)).toBe(false)
345+
expect(await handler?.(appEvent)).toBe(process.platform === 'darwin')
346+
expect(shell.openExternal).toHaveBeenCalledTimes(process.platform === 'darwin' ? 1 : 0)
347+
})
348+
349+
it('uses fixed native microphone settings URLs', async () => {
350+
await expect(openMicrophoneSettings('darwin')).resolves.toBe(true)
351+
expect(shell.openExternal).toHaveBeenLastCalledWith(
352+
'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone'
353+
)
354+
355+
await expect(openMicrophoneSettings('win32')).resolves.toBe(true)
356+
expect(shell.openExternal).toHaveBeenLastCalledWith('ms-settings:privacy-microphone')
357+
await expect(openMicrophoneSettings('linux')).resolves.toBe(false)
358+
})
359+
340360
it('keeps live search suggestions behind the app origin and privacy preference', async () => {
341361
const { invoke } = collectHandlers()
342362
const handler = invoke.get('browser-agent:search-suggestions')

apps/desktop/src/main/ipc.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ import {
1919
isDesktopZoomPercent,
2020
isPendingDesktopScopeId,
2121
} from '@sim/desktop-bridge'
22+
import { createLogger } from '@sim/logger'
2223
import {
2324
isTerminalOperation,
2425
isTerminalToolName,
2526
type TerminalToolArgs,
2627
} from '@sim/terminal-protocol'
28+
import { getErrorMessage } from '@sim/utils/errors'
2729
import { isRecordLike } from '@sim/utils/object'
2830
import type { BrowserWindow, IpcMainEvent, IpcMainInvokeEvent, WebContents } from 'electron'
29-
import { clipboard, ipcMain } from 'electron'
31+
import { clipboard, ipcMain, shell } from 'electron'
3032
import {
3133
type BrowserToolQueueBoundary,
3234
cancelActiveTool,
@@ -84,9 +86,35 @@ import type { ScopedEventRouter } from '@/main/scoped-event-router'
8486
import type { TerminalRegistry } from '@/main/terminal/registry'
8587
import { findCachedTerminalThemeProfile, listTerminalThemeProfiles } from '@/main/terminal-themes'
8688

89+
const logger = createLogger('DesktopIpc')
90+
8791
/** Workspace/chat ids are opaque tokens; anything else never reaches a URL. */
8892
const ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
8993

94+
const MICROPHONE_SETTINGS_URLS: Partial<Record<NodeJS.Platform, string>> = {
95+
darwin: 'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone',
96+
win32: 'ms-settings:privacy-microphone',
97+
}
98+
99+
/** Opens the native microphone privacy pane without accepting a renderer-provided URL. */
100+
export async function openMicrophoneSettings(
101+
platform: NodeJS.Platform = process.platform
102+
): Promise<boolean> {
103+
const settingsUrl = MICROPHONE_SETTINGS_URLS[platform]
104+
if (!settingsUrl) return false
105+
106+
try {
107+
await shell.openExternal(settingsUrl)
108+
return true
109+
} catch (error) {
110+
logger.warn('Could not open microphone privacy settings', {
111+
error: getErrorMessage(error),
112+
platform,
113+
})
114+
return false
115+
}
116+
}
117+
90118
/**
91119
* Desktop state is partitioned by the existing chat id. A new-chat view uses
92120
* the composer’s existing provisional key until the server assigns that id.
@@ -627,6 +655,12 @@ export function registerIpcHandlers(deps: IpcDeps): void {
627655
handler: (url) =>
628656
typeof url === 'string' ? openExternalSafe(url, deps.allowHttpLocalhost()) : false,
629657
},
658+
'desktop:open-microphone-settings': {
659+
kind: 'invoke',
660+
gate: 'app-origin',
661+
denied: false,
662+
handler: () => openMicrophoneSettings(),
663+
},
630664
// OAuth connect handoff: the whole flow runs in the system browser (state
631665
// is cookie-bound to the initiating user agent), returning via loopback.
632666
'desktop:oauth-connect': {

apps/desktop/src/preload/index.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,21 @@ describe('desktop preload bridge', () => {
4545
['desktop:settings:set-browser-search-suggestions', false],
4646
])
4747
})
48+
49+
it('exposes native microphone settings only on supported platforms', async () => {
50+
const exposed = exposeInMainWorld.mock.calls.find(([name]) => name === 'simDesktop')?.[1] as
51+
| SimDesktopApi
52+
| undefined
53+
if (!exposed) throw new Error('Expected the desktop preload API to be exposed')
54+
55+
const isSupportedPlatform = process.platform === 'darwin' || process.platform === 'win32'
56+
expect(typeof exposed.openMicrophoneSettings).toBe(
57+
isSupportedPlatform ? 'function' : 'undefined'
58+
)
59+
60+
if (isSupportedPlatform) {
61+
await exposed.openMicrophoneSettings?.()
62+
expect(invoke).toHaveBeenLastCalledWith('desktop:open-microphone-settings')
63+
}
64+
})
4865
})

apps/desktop/src/preload/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ function shellVersion(): string {
114114
const api: SimDesktopApi = {
115115
version: shellVersion(),
116116
openExternal: (url: string): Promise<boolean> => ipcRenderer.invoke('desktop:open-external', url),
117+
...(process.platform === 'darwin' || process.platform === 'win32'
118+
? {
119+
openMicrophoneSettings: (): Promise<boolean> =>
120+
ipcRenderer.invoke('desktop:open-microphone-settings'),
121+
}
122+
: {}),
117123
beginOAuthConnect: (providerId: string, scope?: DesktopOAuthConnectScope): Promise<boolean> =>
118124
ipcRenderer.invoke('desktop:oauth-connect', providerId, scope),
119125
onOAuthConnectComplete: (callback: (result: DesktopOAuthConnectResult) => void): (() => void) => {

apps/sim/app/api/organizations/[id]/route.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
22
import { db } from '@sim/db'
3-
import { member, organization } from '@sim/db/schema'
3+
import { member, organization, organizationColumns } from '@sim/db/schema'
44
import { createLogger } from '@sim/logger'
55
import { isOrgAdminRole } from '@sim/platform-authz/workspace'
66
import { and, eq, ne } from 'drizzle-orm'
@@ -64,7 +64,7 @@ export const GET = withRouteHandler(
6464
}
6565

6666
const organizationEntry = await db
67-
.select()
67+
.select(organizationColumns)
6868
.from(organization)
6969
.where(eq(organization.id, organizationId))
7070
.limit(1)
@@ -156,7 +156,7 @@ export const PUT = withRouteHandler(
156156
if (name !== undefined || slug !== undefined || logo !== undefined) {
157157
if (slug !== undefined) {
158158
const existingSlug = await db
159-
.select()
159+
.select(organizationColumns)
160160
.from(organization)
161161
.where(and(eq(organization.slug, slug), ne(organization.id, organizationId)))
162162
.limit(1)
@@ -180,7 +180,7 @@ export const PUT = withRouteHandler(
180180
.update(organization)
181181
.set(updateData)
182182
.where(eq(organization.id, organizationId))
183-
.returning()
183+
.returning(organizationColumns)
184184

185185
if (updatedOrg.length === 0) {
186186
return NextResponse.json({ error: 'Organization not found' }, { status: 404 })

apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
import { db, dbReplica } from '@sim/db'
19-
import { member, organization } from '@sim/db/schema'
19+
import { member, organization, organizationColumns } from '@sim/db/schema'
2020
import { createLogger } from '@sim/logger'
2121
import { count, eq } from 'drizzle-orm'
2222
import {
@@ -155,7 +155,7 @@ export const PATCH = withRouteHandler(
155155
if (!parsed.success) return parsed.response
156156

157157
const [orgData] = await db
158-
.select()
158+
.select(organizationColumns)
159159
.from(organization)
160160
.where(eq(organization.id, organizationId))
161161
.limit(1)

apps/sim/app/api/v1/admin/organizations/[id]/route.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
recordAuditBatch,
3939
} from '@sim/audit'
4040
import { db } from '@sim/db'
41-
import { member, organization, subscription } from '@sim/db/schema'
41+
import { member, organization, organizationColumns, subscription } from '@sim/db/schema'
4242
import { createLogger } from '@sim/logger'
4343
import { and, count, eq, inArray, isNull, not, or } from 'drizzle-orm'
4444
import {
@@ -92,7 +92,7 @@ export const GET = withRouteHandler(
9292

9393
try {
9494
const [orgData] = await db
95-
.select()
95+
.select(organizationColumns)
9696
.from(organization)
9797
.where(eq(organization.id, organizationId))
9898
.limit(1)
@@ -143,7 +143,7 @@ export const PATCH = withRouteHandler(
143143

144144
try {
145145
const [existing] = await db
146-
.select()
146+
.select(organizationColumns)
147147
.from(organization)
148148
.where(eq(organization.id, organizationId))
149149
.limit(1)
@@ -182,7 +182,7 @@ export const PATCH = withRouteHandler(
182182
.update(organization)
183183
.set(updateData)
184184
.where(eq(organization.id, organizationId))
185-
.returning()
185+
.returning(organizationColumns)
186186

187187
const updatedFields = auditUpdatedFields(updateData)
188188
logger.info(`Admin API: Updated organization ${organizationId}`, { updatedFields })

apps/sim/app/api/v1/admin/organizations/route.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
2525
import { db, dbReplica } from '@sim/db'
26-
import { member, organization, user } from '@sim/db/schema'
26+
import { member, organization, organizationColumns, user } from '@sim/db/schema'
2727
import { createLogger } from '@sim/logger'
2828
import { slugify } from '@sim/utils/string'
2929
import { count, eq } from 'drizzle-orm'
@@ -81,7 +81,6 @@ export const GET = withRouteHandler(
8181
logo: organization.logo,
8282
orgUsageLimit: organization.orgUsageLimit,
8383
storageUsedBytes: organization.storageUsedBytes,
84-
departedMemberUsage: organization.departedMemberUsage,
8584
createdAt: organization.createdAt,
8685
updatedAt: organization.updatedAt,
8786
})
@@ -152,7 +151,7 @@ export const POST = withRouteHandler(
152151
})
153152

154153
const [createdOrg] = await db
155-
.select()
154+
.select(organizationColumns)
156155
.from(organization)
157156
.where(eq(organization.id, organizationId))
158157
.limit(1)

apps/sim/app/api/v1/admin/types.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,6 @@ export interface AdminOrganization {
358358
logo: string | null
359359
orgUsageLimit: string | null
360360
storageUsedBytes: number
361-
departedMemberUsage: string
362361
createdAt: string
363362
updatedAt: string
364363
}
@@ -370,15 +369,7 @@ export interface AdminOrganizationDetail extends AdminOrganization {
370369

371370
export type AdminOrganizationSource = Pick<
372371
DbOrganization,
373-
| 'id'
374-
| 'name'
375-
| 'slug'
376-
| 'logo'
377-
| 'orgUsageLimit'
378-
| 'storageUsedBytes'
379-
| 'departedMemberUsage'
380-
| 'createdAt'
381-
| 'updatedAt'
372+
'id' | 'name' | 'slug' | 'logo' | 'orgUsageLimit' | 'storageUsedBytes' | 'createdAt' | 'updatedAt'
382373
>
383374

384375
export function toAdminOrganization(dbOrg: AdminOrganizationSource): AdminOrganization {
@@ -389,7 +380,6 @@ export function toAdminOrganization(dbOrg: AdminOrganizationSource): AdminOrgani
389380
logo: dbOrg.logo,
390381
orgUsageLimit: dbOrg.orgUsageLimit,
391382
storageUsedBytes: dbOrg.storageUsedBytes,
392-
departedMemberUsage: dbOrg.departedMemberUsage,
393383
createdAt: dbOrg.createdAt.toISOString(),
394384
updatedAt: dbOrg.updatedAt.toISOString(),
395385
}
@@ -480,8 +470,7 @@ interface AdminUserBilling {
480470
billedOverageThisPeriod: string
481471
storageUsedBytes: number
482472
billingBlocked: boolean
483-
// Copilot usage (active per-period baselines)
484-
currentPeriodCopilotCost: string
473+
// Copilot usage
485474
lastPeriodCopilotCost: string | null
486475
}
487476

apps/sim/app/api/v1/admin/users/[id]/billing/route.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@
2020
*/
2121

2222
import { db } from '@sim/db'
23-
import { member, organization, subscription, user, userStats } from '@sim/db/schema'
23+
import {
24+
member,
25+
organization,
26+
subscription,
27+
user,
28+
userStats,
29+
userStatsColumns,
30+
} from '@sim/db/schema'
2431
import { createLogger } from '@sim/logger'
2532
import { generateShortId } from '@sim/utils/id'
2633
import { eq, or } from 'drizzle-orm'
@@ -78,7 +85,11 @@ export const GET = withRouteHandler(
7885
return notFoundResponse('User')
7986
}
8087

81-
const [stats] = await db.select().from(userStats).where(eq(userStats.userId, userId)).limit(1)
88+
const [stats] = await db
89+
.select(userStatsColumns)
90+
.from(userStats)
91+
.where(eq(userStats.userId, userId))
92+
.limit(1)
8293

8394
// Canonical current-period usage (attributed usage_log, refresh-adjusted)
8495
// comes from the same helper users see.
@@ -119,7 +130,6 @@ export const GET = withRouteHandler(
119130
billedOverageThisPeriod: stats?.billedOverageThisPeriod ?? '0',
120131
storageUsedBytes: stats?.storageUsedBytes ?? 0,
121132
billingBlocked: stats?.billingBlocked ?? false,
122-
currentPeriodCopilotCost: stats?.currentPeriodCopilotCost ?? '0',
123133
lastPeriodCopilotCost: stats?.lastPeriodCopilotCost ?? null,
124134
subscriptions: subscriptions.map(toAdminSubscription),
125135
organizationMemberships: memberOrgs.map((m) => ({
@@ -169,7 +179,7 @@ export const PATCH = withRouteHandler(
169179
}
170180

171181
const [existingStats] = await db
172-
.select()
182+
.select(userStatsColumns)
173183
.from(userStats)
174184
.where(eq(userStats.userId, userId))
175185
.limit(1)

0 commit comments

Comments
 (0)