mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
fix(mac): declare terminal helper protected-folder access
This commit is contained in:
@@ -18,6 +18,10 @@ const {
|
||||
verifyPackagedNodePtyJobOwnership
|
||||
} = require('./scripts/verify-packaged-node-pty-job-ownership.cjs')
|
||||
const { verifySkillsCliRuntime } = require('./scripts/verify-skills-cli-runtime.cjs')
|
||||
const {
|
||||
extendMacElectronNodeHostInfoPlist,
|
||||
macTerminalProtectedFolderUsageDescriptions
|
||||
} = require('./mac-electron-node-host-privacy.cjs')
|
||||
|
||||
// Why: dev-channel builds must carry the *release* identity — same bundle id,
|
||||
// Developer ID signature, and notarization ticket — or Squirrel.Mac refuses to
|
||||
@@ -241,6 +245,9 @@ module.exports = {
|
||||
throw new Error(`Missing packaged resources directory: ${resourcesDir}`)
|
||||
}
|
||||
if (context.electronPlatformName === 'darwin') {
|
||||
const appPath = join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`)
|
||||
// afterPack runs before signing seals the nested helper bundle.
|
||||
extendMacElectronNodeHostInfoPlist(appPath, context.packager.appInfo.productFilename)
|
||||
const architectureByEnum = { 1: 'x64', 3: 'arm64' }
|
||||
const architecture = architectureByEnum[context.arch]
|
||||
if (!architecture) {
|
||||
@@ -377,6 +384,7 @@ module.exports = {
|
||||
entitlements: 'resources/build/entitlements.mac.plist',
|
||||
entitlementsInherit: 'resources/build/entitlements.mac.plist',
|
||||
extendInfo: {
|
||||
...macTerminalProtectedFolderUsageDescriptions,
|
||||
NSAppleEventsUsageDescription:
|
||||
'Orca allows terminal-launched developer tools to automate local apps when you request it.',
|
||||
NSBluetoothAlwaysUsageDescription:
|
||||
@@ -391,11 +399,7 @@ module.exports = {
|
||||
NSMicrophoneUsageDescription: "Application requests access to the device's microphone.",
|
||||
NSAudioCaptureUsageDescription:
|
||||
'Orca allows terminal-launched developer tools to capture desktop audio when you request it.',
|
||||
NSBonjourServices: ['_http._tcp', '_https._tcp'],
|
||||
NSDocumentsFolderUsageDescription:
|
||||
"Application requests access to the user's Documents folder.",
|
||||
NSDownloadsFolderUsageDescription:
|
||||
"Application requests access to the user's Downloads folder."
|
||||
NSBonjourServices: ['_http._tcp', '_https._tcp']
|
||||
},
|
||||
// Why: local macOS validation builds should launch without Apple release
|
||||
// credentials. Hardened runtime + notarization stay enabled only on the
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const { existsSync } = require('node:fs')
|
||||
const { join } = require('node:path')
|
||||
|
||||
// The detached terminal daemon executes as Electron's generic Node helper, so
|
||||
// protected-folder prompts are owned by this nested bundle rather than Orca.app.
|
||||
const macTerminalProtectedFolderUsageDescriptions = {
|
||||
NSDesktopFolderUsageDescription:
|
||||
'Orca allows terminal-launched developer tools to access files on your Desktop when you request it.',
|
||||
NSDocumentsFolderUsageDescription:
|
||||
'Orca allows terminal-launched developer tools to access files in Documents when you request it.',
|
||||
NSDownloadsFolderUsageDescription:
|
||||
'Orca allows terminal-launched developer tools to access files in Downloads when you request it.'
|
||||
}
|
||||
|
||||
function extendMacElectronNodeHostInfoPlist(appPath, productFilename) {
|
||||
const infoPlistPath = join(
|
||||
appPath,
|
||||
'Contents',
|
||||
'Frameworks',
|
||||
`${productFilename} Helper.app`,
|
||||
'Contents',
|
||||
'Info.plist'
|
||||
)
|
||||
if (!existsSync(infoPlistPath)) {
|
||||
throw new Error(`Missing Electron Node host Info.plist: ${infoPlistPath}`)
|
||||
}
|
||||
for (const [key, value] of Object.entries(macTerminalProtectedFolderUsageDescriptions)) {
|
||||
execFileSync('/usr/bin/plutil', ['-replace', key, '-string', value, infoPlistPath])
|
||||
}
|
||||
return infoPlistPath
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extendMacElectronNodeHostInfoPlist,
|
||||
macTerminalProtectedFolderUsageDescriptions
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const electronBuilderConfig = require('../electron-builder.config.cjs')
|
||||
const {
|
||||
extendMacElectronNodeHostInfoPlist,
|
||||
macTerminalProtectedFolderUsageDescriptions
|
||||
} = require('../mac-electron-node-host-privacy.cjs')
|
||||
|
||||
async function createInfoPlist(path, bundleId) {
|
||||
await mkdir(join(path, 'Contents'), { recursive: true })
|
||||
await writeFile(
|
||||
join(path, 'Contents', 'Info.plist'),
|
||||
[
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
||||
'<plist version="1.0">',
|
||||
'<dict>',
|
||||
' <key>CFBundleIdentifier</key>',
|
||||
` <string>${bundleId}</string>`,
|
||||
'</dict>',
|
||||
'</plist>'
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
|
||||
describe('macOS Electron Node host privacy', () => {
|
||||
it('keeps protected-folder purposes identical in the main app and terminal host', () => {
|
||||
expect(electronBuilderConfig.mac.extendInfo).toMatchObject(
|
||||
macTerminalProtectedFolderUsageDescriptions
|
||||
)
|
||||
})
|
||||
|
||||
it.skipIf(process.platform !== 'darwin')(
|
||||
'adds protected-folder purposes only to the generic Electron helper',
|
||||
async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-electron-node-host-privacy-'))
|
||||
const appPath = join(root, 'Orca.app')
|
||||
const genericHelper = join(appPath, 'Contents', 'Frameworks', 'Orca Helper.app')
|
||||
const rendererHelper = join(appPath, 'Contents', 'Frameworks', 'Orca Helper (Renderer).app')
|
||||
try {
|
||||
await createInfoPlist(genericHelper, 'com.stablyai.orca.helper')
|
||||
await createInfoPlist(rendererHelper, 'com.stablyai.orca.helper.Renderer')
|
||||
|
||||
const updatedPath = extendMacElectronNodeHostInfoPlist(appPath, 'Orca')
|
||||
|
||||
expect(updatedPath).toBe(join(genericHelper, 'Contents', 'Info.plist'))
|
||||
const genericInfo = JSON.parse(
|
||||
execFileSync('/usr/bin/plutil', ['-convert', 'json', '-o', '-', updatedPath], {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
)
|
||||
const rendererInfo = JSON.parse(
|
||||
execFileSync(
|
||||
'/usr/bin/plutil',
|
||||
['-convert', 'json', '-o', '-', join(rendererHelper, 'Contents', 'Info.plist')],
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
)
|
||||
expect(genericInfo.CFBundleIdentifier).toBe('com.stablyai.orca.helper')
|
||||
expect(genericInfo).toMatchObject(macTerminalProtectedFolderUsageDescriptions)
|
||||
expect(rendererInfo).not.toMatchObject(macTerminalProtectedFolderUsageDescriptions)
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('fails packaging when the generic Electron helper is missing', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-electron-node-host-missing-'))
|
||||
try {
|
||||
expect(() => extendMacElectronNodeHostInfoPlist(join(root, 'Orca.app'), 'Orca')).toThrow(
|
||||
/Missing Electron Node host Info\.plist/
|
||||
)
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user