mirror of
https://gh-proxy.org/https://github.com/klinsmaya/workdsh.git
synced 2026-11-09 18:07:02 +08:00
feat(desktop): create safe web profiles
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
openSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
renameSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
@@ -18,8 +19,10 @@ import {
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import {
|
||||
PROFILE_TEMPLATES,
|
||||
initProfile,
|
||||
readProfileManifest,
|
||||
resolveProfileDir,
|
||||
writeProfileManifest,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
|
||||
const BIN_NAME = 'dsh-plugin-desktop'
|
||||
@@ -33,6 +36,7 @@ const STATE_VERSION = 1
|
||||
const MAX_STATE_BYTES = 4 * 1024
|
||||
const STATE_DIRECTORY_MODE = 0o700
|
||||
const STATE_FILE_MODE = 0o600
|
||||
const MAX_PROFILE_NAME_BYTES = 255
|
||||
|
||||
/** One discovered or lazily available DSH profile. */
|
||||
export interface DesktopProfileSummary {
|
||||
@@ -93,10 +97,15 @@ function defaultState(): DesktopProfileStateV1 {
|
||||
|
||||
/** Reject profile names that cannot safely cross the persisted state boundary. */
|
||||
export function assertDesktopProfileName(name: string): void {
|
||||
resolveProfileDir(name, '/')
|
||||
if (name.length > 255 || /[\0-\x1f\x7f]/.test(name)) {
|
||||
if (typeof name !== 'string' || name.length === 0
|
||||
|| name.includes('/') || name.includes('\\') || name === '.' || name === '..'
|
||||
|| name === 'node_modules' || Buffer.byteLength(name, 'utf8') > MAX_PROFILE_NAME_BYTES
|
||||
|| /[\0-\x1f\x7f-\x9f]/.test(name)
|
||||
|| /[<>:"|?*]/.test(name) || /[. ]$/.test(name)
|
||||
|| /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?$/i.test(name)) {
|
||||
throw new Error(`${BIN_NAME}: invalid desktop profile name ${JSON.stringify(name)}`)
|
||||
}
|
||||
resolveProfileDir(name, '/')
|
||||
}
|
||||
|
||||
/** Validate a manifest bundle list without trusting arbitrary JSON values. */
|
||||
@@ -156,6 +165,50 @@ function virtualProfile(name: typeof DEFAULT_PROFILE_NAME | typeof WEB_PROFILE_N
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a safe Web profile using only the shipped template.
|
||||
*
|
||||
* The profile is initialized in a sibling staging directory and published with
|
||||
* one rename, so a failed initialization never leaves a partially initialized
|
||||
* target visible to discovery.
|
||||
*/
|
||||
export function createDesktopWebProfile(home: string, name: string): DesktopProfileSummary {
|
||||
assertDesktopProfileName(name)
|
||||
const template = PROFILE_TEMPLATES.web
|
||||
if (template === undefined) {
|
||||
throw new Error(`${BIN_NAME}: installed dsh-app-boot has no web profile template`)
|
||||
}
|
||||
const target = resolveProfileDir(name, home)
|
||||
const profilesDir = dirname(target)
|
||||
mkdirSync(profilesDir, { recursive: true })
|
||||
try {
|
||||
lstatSync(target)
|
||||
throw new Error(`${BIN_NAME}: profile ${JSON.stringify(name)} already exists`)
|
||||
} catch (cause) {
|
||||
if ((cause as NodeJS.ErrnoException).code !== 'ENOENT') throw cause
|
||||
}
|
||||
|
||||
const staging = join(profilesDir, `.${basename(target)}.creating-${process.pid}-${randomUUID()}`)
|
||||
try {
|
||||
initProfile(staging, [...template])
|
||||
const manifest = readProfileManifest(BIN_NAME, staging)
|
||||
writeProfileManifest(staging, { ...manifest, name: `dsh-profile-${name}` })
|
||||
// The target is checked again immediately before publication. `renameSync`
|
||||
// is atomic on the same filesystem; an existing target is never replaced.
|
||||
try {
|
||||
lstatSync(target)
|
||||
throw new Error(`${BIN_NAME}: profile ${JSON.stringify(name)} already exists`)
|
||||
} catch (cause) {
|
||||
if ((cause as NodeJS.ErrnoException).code !== 'ENOENT') throw cause
|
||||
}
|
||||
renameSync(staging, target)
|
||||
} catch (cause) {
|
||||
rmSync(staging, { recursive: true, force: true })
|
||||
throw cause
|
||||
}
|
||||
return existingProfile(name, home)
|
||||
}
|
||||
|
||||
/** Deterministic profile order with the product defaults first. */
|
||||
function compareProfiles(left: DesktopProfileSummary, right: DesktopProfileSummary): number {
|
||||
const priority = (name: string): number => name === DEFAULT_PROFILE_NAME ? 0 : name === WEB_PROFILE_NAME ? 1 : 2
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface DesktopCurrentProfile {
|
||||
export interface DesktopProfiles {
|
||||
/** Immutable identity of the profile backing this Cordis generation. */
|
||||
readonly current: DesktopCurrentProfile
|
||||
/** Create a safe Web profile without selecting or restarting it. */
|
||||
create(name: string): DesktopProfileSummary
|
||||
/** Re-read the available profile manifests without changing them. */
|
||||
list(): readonly DesktopProfileSummary[]
|
||||
/** Persist a compatible profile and request an orderly application restart. */
|
||||
@@ -32,6 +34,8 @@ declare module '@deepseek-ai/cordis' {
|
||||
export interface DesktopProfileServiceBootstrap {
|
||||
/** Profile that produced the current Cordis generation. */
|
||||
readonly current: DesktopCurrentProfile
|
||||
/** Create a safe Web profile without selecting or restarting it. */
|
||||
create?: (name: string) => DesktopProfileSummary
|
||||
/** Re-read the available profile manifests without changing them. */
|
||||
list(): readonly DesktopProfileSummary[]
|
||||
/** Persist one validated profile as pending for the next startup. */
|
||||
@@ -81,6 +85,15 @@ export class DesktopProfileService extends Service implements DesktopProfiles {
|
||||
return this.fixedCurrent
|
||||
}
|
||||
|
||||
/** Create a profile through the launcher-owned implementation. */
|
||||
create(name: string): DesktopProfileSummary {
|
||||
this.assertActive()
|
||||
if (this.bootstrap.create === undefined) {
|
||||
throw new Error('dsh-plugin-desktop: profile creation is unavailable')
|
||||
}
|
||||
return this.bootstrap.create(name)
|
||||
}
|
||||
|
||||
/** Re-read profile discovery through the launcher-owned implementation. */
|
||||
list(): readonly DesktopProfileSummary[] {
|
||||
this.assertActive()
|
||||
|
||||
@@ -15,6 +15,7 @@ import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
assertDesktopProfileName,
|
||||
beginDesktopProfileStartup,
|
||||
createDesktopWebProfile,
|
||||
listDesktopProfiles,
|
||||
markDesktopProfileFailed,
|
||||
markDesktopProfileHealthy,
|
||||
@@ -50,6 +51,43 @@ describe('desktop profile discovery', () => {
|
||||
expect(() => assertDesktopProfileName('团队 profile')).not.toThrow()
|
||||
expect(() => assertDesktopProfileName('profile\nname')).toThrow('invalid desktop profile name')
|
||||
expect(() => assertDesktopProfileName('../outside')).toThrow()
|
||||
expect(() => assertDesktopProfileName('CON.txt')).toThrow('invalid desktop profile name')
|
||||
expect(() => assertDesktopProfileName('name.')).toThrow('invalid desktop profile name')
|
||||
expect(() => assertDesktopProfileName('name ')).toThrow('invalid desktop profile name')
|
||||
expect(() => assertDesktopProfileName('é'.repeat(128))).toThrow('invalid desktop profile name')
|
||||
})
|
||||
|
||||
it('creates a Web profile from the shipped template and publishes all files together', () => {
|
||||
const home = temporaryRoot()
|
||||
|
||||
expect(createDesktopWebProfile(home, 'work')).toEqual(expect.objectContaining({
|
||||
name: 'work',
|
||||
dir: join(home, 'profiles', 'work'),
|
||||
exists: true,
|
||||
bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'],
|
||||
webCapable: true,
|
||||
}))
|
||||
expect(readFileSync(join(home, 'profiles', 'work', 'package.json'), 'utf8'))
|
||||
.toContain('"name": "dsh-profile-work"')
|
||||
expect(existsSync(join(home, 'profiles', 'work', 'cordis.patch.yml'))).toBe(true)
|
||||
expect(existsSync(join(home, 'profiles', 'work', 'pnpm-workspace.yaml'))).toBe(true)
|
||||
expect(readdirSync(join(home, 'profiles')).filter(name => name.includes('.work.creating-'))).toEqual([])
|
||||
})
|
||||
|
||||
it('does not overwrite an existing profile or leave a target after failure', () => {
|
||||
const home = temporaryRoot()
|
||||
const existing = writeProfile(home, 'work', ['@deepseek-ai/dsh-base'])
|
||||
const before = readFileSync(join(existing, 'package.json'), 'utf8')
|
||||
|
||||
expect(() => createDesktopWebProfile(home, 'work')).toThrow('already exists')
|
||||
expect(readFileSync(join(existing, 'package.json'), 'utf8')).toBe(before)
|
||||
expect(readdirSync(join(home, 'profiles')).filter(name => name.includes('.work.creating-'))).toEqual([])
|
||||
|
||||
const blockedHome = join(home, 'blocked-home')
|
||||
mkdirSync(blockedHome, { recursive: true })
|
||||
writeFileSync(join(blockedHome, 'profiles'), 'not a directory')
|
||||
expect(() => createDesktopWebProfile(blockedHome, 'work')).toThrow()
|
||||
expect(existsSync(join(blockedHome, 'profiles', 'work'))).toBe(false)
|
||||
})
|
||||
|
||||
it('lists lazy defaults and existing profiles without creating or changing manifests', () => {
|
||||
|
||||
@@ -77,6 +77,24 @@ describe('desktop profile service', () => {
|
||||
expect(list).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('creates a profile through the launcher without selecting it', async () => {
|
||||
const create = vi.fn(() => WORK)
|
||||
const mounted = await mount(createBootstrap({ create }))
|
||||
|
||||
expect(mounted.service.create('work')).toEqual(WORK)
|
||||
expect(create).toHaveBeenCalledWith('work')
|
||||
expect(mounted.service.current.name).toBe('desktop')
|
||||
})
|
||||
|
||||
it('rejects profile creation through a retained service after disposal', async () => {
|
||||
const create = vi.fn(() => WORK)
|
||||
const mounted = await mount(createBootstrap({ create }))
|
||||
await mounted.dispose()
|
||||
|
||||
expect(() => mounted.service.create('work')).toThrow('desktopProfiles service disposed')
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing when the current profile is selected', async () => {
|
||||
const persistSelection = vi.fn()
|
||||
const requestRestart = vi.fn()
|
||||
|
||||
@@ -21,6 +21,7 @@ describe('desktop profiles Host plugin', () => {
|
||||
} as unknown as DesktopRuntime
|
||||
const profiles: DesktopProfiles = {
|
||||
current: { name: 'desktop', dir: '/profiles/desktop' },
|
||||
create: vi.fn(),
|
||||
list: () => [
|
||||
{ name: 'desktop', dir: '/profiles/desktop', exists: true, bundles: [], webCapable: true },
|
||||
{ name: '工作 profile', dir: '/profiles/work', exists: true, bundles: [], webCapable: true },
|
||||
|
||||
Reference in New Issue
Block a user