* fix(desktop): refuse writes where a written file would run The fs scope let the webview write, create folders, and delete anywhere. Documents may still be saved anywhere, but the global scope now denies login items and startup folders, PowerShell profiles, and the global package and executable folders where coding agents and OpenPencil's companions live, and writes to the MCP discovery files agents trust. requireLiteralLeadingDot keeps hidden files and folders, such as shell profiles and agent settings, out of ** on Windows as Tauri already does on macOS and Linux. A native test saves a document and is refused a LaunchAgents file, a home dotfile, and the discovery file. * fix(ui): draw segmented controls at panel field height Panel fields moved to 24px when sizing tokens became plain utilities, but segmented control items stayed 22px inside a 2px padding, so the Typography and resizing controls stood 2px taller than the fields beside them. The panel foundation story renders its inputs at the panel size and checks 24px. * test(storybook): run every story and its play function No test ran the play functions, and five had gone stale: the layer tree example labelled a wrapper with the same name as its row, the chat composer's label gained an ellipsis, the MCP failure story queried a test id attribute the app does not use, and the property primitives story still collapsed sections whose titles are static now. bun run test:storybook now renders every story and fails on a story or play function that throws. * fix(desktop): deny protected folders themselves and writable opens of the discovery files Each protected folder is denied alongside its contents, so a recursive remove cannot target the folder itself, and the MCP discovery files are denied to open as well as write, since opening with truncate would empty them. The native test opens the discovery file for writing without truncating, and removes only files it created. The story test waits for storyFinished, which follows afterEach, and judges exceptions and non-accessibility reports. * test(desktop): run the file scope check only on macOS Its protected paths are macOS ones, so other platforms skip it rather than pass for another reason. * docs: note that documents opened from hidden folders can still be saved
49 lines
2.1 KiB
TypeScript
49 lines
2.1 KiB
TypeScript
import { strict as assert } from 'node:assert'
|
|
import { existsSync, mkdtempSync, rmSync } from 'node:fs'
|
|
import { homedir, tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
|
|
import { invokeNative, writeNativeFile } from '#tests/helpers/tauri/invoke'
|
|
|
|
const NAME = 'openpencil-native-test-scope.txt'
|
|
const DISCOVERY = join(homedir(), 'Library', 'Application Support', 'OpenPencil', 'mcp.json')
|
|
|
|
describe('desktop file scope', () => {
|
|
// The protected paths below are macOS ones; other platforms are not claimed.
|
|
before(function () {
|
|
if (process.platform !== 'darwin') this.skip()
|
|
})
|
|
|
|
it('saves documents but not where a written file would run', async () => {
|
|
const documents = mkdtempSync(join(tmpdir(), 'openpencil-scope-'))
|
|
const refused = [
|
|
join(homedir(), 'Library', 'LaunchAgents', NAME),
|
|
join(homedir(), `.${NAME}`),
|
|
join(homedir(), '.openpencil', 'mcp.json')
|
|
]
|
|
// Only files this test would have created are removed afterwards.
|
|
const created = refused.filter((path) => !existsSync(path))
|
|
try {
|
|
assert.equal(await writeNativeFile(join(documents, 'design.fig'), 'design'), null)
|
|
for (const path of refused) {
|
|
const error = await writeNativeFile(path, 'not allowed')
|
|
assert.match(error ?? '', /forbidden|not allowed/i, `${path} was writable`)
|
|
}
|
|
} finally {
|
|
rmSync(documents, { recursive: true, force: true })
|
|
for (const path of created) if (existsSync(path)) rmSync(path)
|
|
}
|
|
})
|
|
|
|
it('refuses to open the MCP discovery file for writing', async () => {
|
|
// The scope is checked before the file is opened, so a refusal does not depend on the file
|
|
// existing; opening without truncating changes nothing even if the scope let it through.
|
|
const opened = await invokeNative<number>('plugin:fs|open', {
|
|
path: DISCOVERY,
|
|
options: { write: true }
|
|
}).then((rid) => rid, String)
|
|
if (typeof opened === 'number') await invokeNative('plugin:resources|close', { rid: opened })
|
|
assert.match(String(opened), /forbidden|not allowed/i, `${DISCOVERY} opened for writing`)
|
|
})
|
|
})
|