import { randomUUID } from 'node:crypto' import { lstat, mkdir, readFile, readdir, realpath, rename, rmdir, unlink } from 'node:fs/promises' import { isAbsolute, join, relative, resolve, sep } from 'node:path' import { isDeepStrictEqual } from 'node:util' import { z } from 'zod' import { runtimeExtensionActionSchema, runtimeExtensionCatalogEntrySchema, runtimeExtensionIdSchema, runtimeExtensionInstalledStateSchema, runtimeExtensionStartupFailureCode, type RuntimeExtensionAction, type RuntimeExtensionCatalogEntry, type RuntimeExtensionConfiguration, type RuntimeExtensionExactPackage, type RuntimeExtensionInstalledState, type RuntimeExtensionMarketplaceInstalledState, type RuntimeExtensionMarketplaceSnapshot } from '../../shared/runtime-extension-contracts' import { isMissingFileError, writeJsonFileAtomically } from '../settings-file-utils' const managedDirectoryName = 'runtime-extensions' const stateFileName = 'store.json' const journalFileName = '.mutation-journal.json' const unjournaledStagingDirectoryPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}(?:-previous|-removed)?$/iu const version1StoredStateSchema = z .object({ version: z.literal(1), installed: z.array(runtimeExtensionInstalledStateSchema) }) .strict() const storedStateSchema = z .object({ version: z.literal(2), marketplaceEnabled: z.boolean(), installed: z.array(runtimeExtensionInstalledStateSchema) }) .strict() const storedStateFileSchema = z.union([ storedStateSchema, version1StoredStateSchema ]) type StoredState = z.infer const mutationJournalSchema = z .object({ version: z.literal(1), kind: z.enum(['install', 'remove']), extensionId: runtimeExtensionIdSchema, temporaryId: runtimeExtensionIdSchema, before: storedStateSchema, after: storedStateSchema }) .strict() type MutationJournal = z.infer export interface RuntimeExtensionCatalog { list(): Promise } export interface RuntimeExtensionStoreDependencies { catalog: RuntimeExtensionCatalog install(input: { entry: RuntimeExtensionCatalogEntry destinationDirectory: string }): Promise<{ entrypoint: string integrity?: string }> now?: () => Date temporaryId?: () => string } export interface EnabledRuntimeExtension { id: string entrypoint: string configuration: RuntimeExtensionConfiguration } export type RuntimeExtensionApplyResult = { snapshot: RuntimeExtensionMarketplaceSnapshot changed: boolean } function emptyState(): StoredState { return { version: 2, marketplaceEnabled: false, installed: [] } } function compareIds( left: { id: string }, right: { id: string } ): number { return left.id.localeCompare(right.id, 'en') } function packagesEqual( left: RuntimeExtensionExactPackage, right: RuntimeExtensionExactPackage ): boolean { return left.name === right.name && left.version === right.version } function marketplaceInstalledState( extension: RuntimeExtensionInstalledState ): RuntimeExtensionMarketplaceInstalledState { return { id: extension.id, package: extension.package, installedAt: extension.installedAt, enabled: extension.enabled, configuration: extension.configuration, ...(extension.integrity ? { integrity: extension.integrity } : {}), ...(extension.lastError ? { lastError: extension.lastError } : {}) } } export class RuntimeExtensionStore { readonly managedRoot: string private readonly statePath: string private readonly journalPath: string private state?: StoredState private stateLoad?: Promise private canonicalRoot?: string private mutationQueue: Promise = Promise.resolve() private catalog: RuntimeExtensionCatalogEntry[] = [] private catalogError?: string constructor( userDataPath: string, private readonly dependencies: RuntimeExtensionStoreDependencies ) { if (!isAbsolute(userDataPath)) { throw new Error('GoodBuddy userData path must be absolute') } this.managedRoot = resolve(userDataPath, managedDirectoryName) this.statePath = join(this.managedRoot, stateFileName) this.journalPath = join(this.managedRoot, journalFileName) } async getSnapshot(): Promise { const state = await this.load() if (!state.marketplaceEnabled) { this.catalog = [] this.catalogError = undefined return this.marketplaceSnapshot(state) } try { await this.loadCatalog() } catch (error) { this.catalog = [] this.catalogError = error instanceof Error && error.message.trim() ? error.message.trim().slice(0, 1_000) : 'Extension catalog is unavailable.' } return this.marketplaceSnapshot(state) } async apply( action: RuntimeExtensionAction ): Promise { return (await this.applyWithResult(action)).snapshot } async applyWithResult( action: RuntimeExtensionAction ): Promise { const parsed = runtimeExtensionActionSchema.parse(action) const changed = await this.serialize(async () => { await this.reconcileMutationJournal() switch (parsed.type) { case 'set-marketplace-enabled': return this.setMarketplaceEnabled(parsed.enabled) case 'install': await this.install(parsed.extensionId, parsed.package) return true case 'set-enabled': return this.setEnabled(parsed.extensionId, parsed.enabled) case 'remove': await this.remove(parsed.extensionId) return true case 'configure': return this.configure( parsed.extensionId, parsed.configuration ) } }) return { snapshot: this.marketplaceSnapshot(await this.load()), changed } } async getEnabledExtensions(): Promise { const state = await this.load() return state.installed .filter((extension) => extension.enabled) .sort(compareIds) .map(({ id, entrypoint, configuration }) => ({ id, entrypoint, configuration })) } markStartupFailed(ids: readonly string[]): Promise { const parsedIds = z.array(runtimeExtensionIdSchema).parse(ids) return this.serialize(async () => { const failed = new Set(parsedIds) const state = await this.load() const installed = state.installed.map((extension) => failed.has(extension.id) ? { ...extension, enabled: false, lastError: runtimeExtensionStartupFailureCode } : extension ) if ( installed.some( (extension, index) => extension !== state.installed[index] ) ) { await this.persistAndSet({ ...state, installed }) } }) } private serialize(operation: () => Promise): Promise { const result = this.mutationQueue.then(operation) this.mutationQueue = result.then( () => undefined, () => undefined ) return result } private load(): Promise { if (this.state) { return Promise.resolve(this.state) } if (!this.stateLoad) { this.stateLoad = this.readState().finally(() => { this.stateLoad = undefined }) } return this.stateLoad } private async readState(): Promise { await this.initialize() try { const status = await lstat(this.statePath) if ( !status.isFile() || status.isSymbolicLink() || status.nlink > 1 ) { throw new Error('Extension store state must be a regular file') } await this.assertExistingPathContained(this.statePath) const stored = storedStateFileSchema.parse( JSON.parse(await readFile(this.statePath, 'utf8')) as unknown ) const parsed: StoredState = stored.version === 1 ? { version: 2, marketplaceEnabled: stored.installed.length > 0, installed: stored.installed } : stored for (const extension of parsed.installed) { this.assertExtensionEntrypoint(extension) } if (stored.version === 1) { await this.persist(parsed) } this.state = parsed } catch (error) { if (!isMissingFileError(error)) { throw error } this.state = emptyState() await this.persist(this.state) } return this.state } private async initialize(): Promise { await mkdir(this.managedRoot, { recursive: true, mode: 0o700 }) const status = await lstat(this.managedRoot) if (!status.isDirectory() || status.isSymbolicLink()) { throw new Error('Extension managed root must be a real directory') } this.canonicalRoot = await realpath(this.managedRoot) await this.createManagedDirectory('extensions') await this.createManagedDirectory('.staging') await this.reconcileMutationJournal() await this.cleanupUnjournaledStagingDirectories() } private async loadCatalog(): Promise { const catalog = (await this.dependencies.catalog.list()).map((entry) => runtimeExtensionCatalogEntrySchema.parse(entry) ) const ids = new Set() for (const entry of catalog) { if (ids.has(entry.id)) { throw new Error(`Duplicate extension catalog ID: ${entry.id}`) } ids.add(entry.id) } this.catalog = catalog.sort(compareIds) this.catalogError = undefined return this.catalog } private async install( extensionId: string, requestedPackage: RuntimeExtensionExactPackage ): Promise { const state = await this.load() if (!state.marketplaceEnabled) { throw new Error('The DSH plugin marketplace is disabled') } const catalog = await this.loadCatalog() const entry = catalog.find( (candidate) => candidate.id === extensionId && packagesEqual(candidate.package, requestedPackage) ) if (!entry) { throw new Error('The exact extension package is not in the catalog') } const temporaryId = this.dependencies.temporaryId?.() ?? randomUUID() runtimeExtensionIdSchema.parse(temporaryId) const stagedDirectory = await this.createFreshManagedDirectory( '.staging', temporaryId ) const backupDirectory = this.managedPath( '.staging', `${temporaryId}-previous` ) const finalDirectory = this.extensionDirectory(extensionId) try { if (await this.pathExists(backupDirectory)) { throw new Error( 'Extension upgrade backup path already exists' ) } const installedPackage = await this.dependencies.install({ entry, destinationDirectory: stagedDirectory }) await this.resolveEntrypoint( stagedDirectory, installedPackage.entrypoint ) const entrypoint = resolve( finalDirectory, installedPackage.entrypoint ) const existing = state.installed.find( (extension) => extension.id === extensionId ) const installed: RuntimeExtensionInstalledState = { id: extensionId, package: entry.package, entrypoint, installedAt: ( this.dependencies.now?.() ?? new Date() ).toISOString(), enabled: existing?.enabled ?? true, configuration: existing?.configuration ?? {}, ...(installedPackage.integrity ? { integrity: installedPackage.integrity } : {}) } const nextState = this.replaceInstalled(state, installed) const journal: MutationJournal = { version: 1, kind: 'install', extensionId, temporaryId, before: state, after: nextState } await this.writeMutationJournal(journal) if (await this.pathExists(finalDirectory)) { await this.assertRealManagedDirectory(finalDirectory) } if (await this.pathExists(finalDirectory)) { await rename(finalDirectory, backupDirectory) } await rename(stagedDirectory, finalDirectory) await this.persistAndSet(nextState) await this.removeManagedTree(backupDirectory) await this.clearMutationJournal() } catch (error) { this.state = undefined await this.reconcileMutationJournal().catch(() => undefined) throw error } finally { await this.removeManagedTree(stagedDirectory).catch(() => undefined) } } private async setEnabled( extensionId: string, enabled: boolean ): Promise { const state = await this.load() const extension = this.requireInstalled(state, extensionId) if ( extension.enabled === enabled && (!enabled || !extension.lastError) ) { return false } const updated = { ...extension, enabled, ...(enabled ? { lastError: undefined } : {}) } await this.persistAndSet(this.replaceInstalled(state, updated)) return true } private async setMarketplaceEnabled( enabled: boolean ): Promise { const state = await this.load() if (state.marketplaceEnabled === enabled) { return false } await this.persistAndSet({ ...state, marketplaceEnabled: enabled }) if (!enabled) { this.catalog = [] this.catalogError = undefined } return true } private async configure( extensionId: string, configuration: RuntimeExtensionConfiguration ): Promise { const state = await this.load() const extension = this.requireInstalled(state, extensionId) if (isDeepStrictEqual(extension.configuration, configuration)) { return false } await this.persistAndSet( this.replaceInstalled(state, { ...extension, configuration }) ) return true } private marketplaceSnapshot( state: StoredState ): RuntimeExtensionMarketplaceSnapshot { return { marketplaceEnabled: state.marketplaceEnabled, catalog: this.catalog, installed: [...state.installed] .sort(compareIds) .map(marketplaceInstalledState), ...(this.catalogError ? { catalogError: this.catalogError } : {}) } } private async remove(extensionId: string): Promise { const state = await this.load() this.requireInstalled(state, extensionId) const finalDirectory = this.extensionDirectory(extensionId) const trashDirectory = this.managedPath( '.staging', `${randomUUID()}-removed` ) const temporaryId = trashDirectory .slice(trashDirectory.lastIndexOf(sep) + 1) .replace(/-removed$/u, '') runtimeExtensionIdSchema.parse(temporaryId) if (await this.pathExists(trashDirectory)) { throw new Error('Extension removal staging path already exists') } const nextState: StoredState = { ...state, installed: state.installed.filter( (extension) => extension.id !== extensionId ) } await this.writeMutationJournal({ version: 1, kind: 'remove', extensionId, temporaryId, before: state, after: nextState }) if (await this.pathExists(finalDirectory)) { await this.assertRealManagedDirectory(finalDirectory) await rename(finalDirectory, trashDirectory) } try { await this.persistAndSet(nextState) } catch (error) { this.state = undefined await this.reconcileMutationJournal().catch(() => undefined) throw error } await this.removeManagedTree(trashDirectory) await this.clearMutationJournal() } private requireInstalled( state: StoredState, extensionId: string ): RuntimeExtensionInstalledState { const extension = state.installed.find( (candidate) => candidate.id === extensionId ) if (!extension) { throw new Error(`Extension is not installed: ${extensionId}`) } return extension } private replaceInstalled( state: StoredState, extension: RuntimeExtensionInstalledState ): StoredState { return storedStateSchema.parse({ ...state, installed: state.installed .filter((candidate) => candidate.id !== extension.id) .concat(extension) .sort(compareIds) }) } private async persistAndSet(state: StoredState): Promise { await this.persist(state) this.state = state } private persist(state: StoredState): Promise { return writeJsonFileAtomically( this.statePath, storedStateSchema.parse(state) ) } private writeMutationJournal(journal: MutationJournal): Promise { return writeJsonFileAtomically( this.journalPath, mutationJournalSchema.parse(journal) ) } private async clearMutationJournal(): Promise { await unlink(this.journalPath).catch((error: unknown) => { if (!isMissingFileError(error)) { throw error } }) } private async reconcileMutationJournal(): Promise { let journal: MutationJournal try { const status = await lstat(this.journalPath) if ( !status.isFile() || status.isSymbolicLink() || status.nlink > 1 ) { throw new Error( 'Extension mutation journal must be a regular file' ) } await this.assertExistingPathContained(this.journalPath) journal = mutationJournalSchema.parse( JSON.parse(await readFile(this.journalPath, 'utf8')) as unknown ) } catch (error) { if (isMissingFileError(error)) { return } throw error } const stateStatus = await lstat(this.statePath) if ( !stateStatus.isFile() || stateStatus.isSymbolicLink() || stateStatus.nlink > 1 ) { throw new Error('Extension store state must be a regular file') } await this.assertExistingPathContained(this.statePath) const stored = storedStateFileSchema.parse( JSON.parse(await readFile(this.statePath, 'utf8')) as unknown ) if (stored.version !== 2) { throw new Error( 'Extension mutation journal requires current store state' ) } const committed = isDeepStrictEqual(stored, journal.after) const rolledBack = isDeepStrictEqual(stored, journal.before) if (!committed && !rolledBack) { throw new Error( 'Extension mutation journal does not match store state' ) } const finalDirectory = this.extensionDirectory(journal.extensionId) const stagedDirectory = this.managedPath( '.staging', journal.temporaryId ) const auxiliaryDirectory = this.managedPath( '.staging', journal.kind === 'install' ? `${journal.temporaryId}-previous` : `${journal.temporaryId}-removed` ) if (journal.kind === 'install') { if (committed) { await this.assertInstalledStateOnDisk( this.requireInstalled(journal.after, journal.extensionId) ) await this.removeManagedTree(auxiliaryDirectory) await this.removeManagedTree(stagedDirectory) } else { const hadPrevious = journal.before.installed.some( (extension) => extension.id === journal.extensionId ) if (await this.pathExists(auxiliaryDirectory)) { await this.assertRealManagedDirectory(auxiliaryDirectory) await this.removeManagedTree(finalDirectory) await rename(auxiliaryDirectory, finalDirectory) } else if (!hadPrevious) { await this.removeManagedTree(finalDirectory) } if (hadPrevious) { await this.assertInstalledStateOnDisk( this.requireInstalled( journal.before, journal.extensionId ) ) } await this.removeManagedTree(stagedDirectory) } } else if (committed) { await this.removeManagedTree(finalDirectory) await this.removeManagedTree(auxiliaryDirectory) } else { if (await this.pathExists(auxiliaryDirectory)) { await this.assertRealManagedDirectory(auxiliaryDirectory) await this.removeManagedTree(finalDirectory) await rename(auxiliaryDirectory, finalDirectory) } await this.assertInstalledStateOnDisk( this.requireInstalled(journal.before, journal.extensionId) ) } await this.clearMutationJournal() this.state = stored } private async assertRealManagedDirectory(path: string): Promise { this.assertContained(this.managedRoot, path) const status = await lstat(path) if (!status.isDirectory() || status.isSymbolicLink()) { throw new Error( 'Extension package path must be a real directory' ) } await this.assertExistingPathContained(path) } private async cleanupUnjournaledStagingDirectories(): Promise { const stagingDirectory = this.managedPath('.staging') await this.assertRealManagedDirectory(stagingDirectory) const entries = await readdir(stagingDirectory, { withFileTypes: true }) for (const entry of entries) { if ( !entry.isDirectory() || entry.isSymbolicLink() || !unjournaledStagingDirectoryPattern.test(entry.name) ) { continue } const directory = this.managedPath('.staging', entry.name) await this.assertRealManagedDirectory(directory) await this.removeManagedTree(directory) } } private async assertInstalledStateOnDisk( extension: RuntimeExtensionInstalledState ): Promise { this.assertExtensionEntrypoint(extension) const directory = this.extensionDirectory(extension.id) await this.assertRealManagedDirectory(directory) const relativeEntrypoint = relative( directory, extension.entrypoint ).split(sep).join('/') await this.resolveEntrypoint(directory, relativeEntrypoint) } private assertExtensionEntrypoint( extension: RuntimeExtensionInstalledState ): void { if (!isAbsolute(extension.entrypoint)) { throw new Error('Installed extension entrypoint must be absolute') } const directory = this.extensionDirectory(extension.id) this.assertContained(directory, extension.entrypoint) } private extensionDirectory(extensionId: string): string { runtimeExtensionIdSchema.parse(extensionId) return this.managedPath('extensions', extensionId) } private managedPath(...segments: string[]): string { const path = resolve(this.managedRoot, ...segments) this.assertContained(this.managedRoot, path) return path } private assertContained(root: string, path: string): void { const relativePath = relative(root, path) if ( relativePath === '' || (!relativePath.startsWith(`..${sep}`) && relativePath !== '..' && !isAbsolute(relativePath)) ) { return } throw new Error('Extension path escapes the managed root') } private async assertExistingPathContained(path: string): Promise { this.assertContained(this.managedRoot, path) const root = this.canonicalRoot ?? (await realpath(this.managedRoot)) this.assertContained(root, await realpath(path)) } private async createManagedDirectory( ...segments: string[] ): Promise { let directory = this.managedRoot await this.assertExistingPathContained(directory) for (const segment of segments) { directory = join(directory, segment) this.assertContained(this.managedRoot, directory) await mkdir(directory, { recursive: true, mode: 0o700 }) const status = await lstat(directory) if (!status.isDirectory() || status.isSymbolicLink()) { throw new Error('Extension managed path must be a real directory') } await this.assertExistingPathContained(directory) } return directory } private async createFreshManagedDirectory( ...segments: string[] ): Promise { const leaf = segments.at(-1) if (!leaf) { throw new Error('A managed directory name is required') } const parent = await this.createManagedDirectory(...segments.slice(0, -1)) const directory = join(parent, leaf) this.assertContained(this.managedRoot, directory) await mkdir(directory, { mode: 0o700 }) await this.assertExistingPathContained(directory) return directory } private async pathExists(path: string): Promise { this.assertContained(this.managedRoot, path) try { await lstat(path) return true } catch (error) { if (isMissingFileError(error)) { return false } throw error } } private async resolveEntrypoint( root: string, relativeEntrypoint: string ): Promise { if ( !relativeEntrypoint || relativeEntrypoint.includes('\\') || relativeEntrypoint.startsWith('/') || /^[A-Za-z]:/u.test(relativeEntrypoint) || relativeEntrypoint .split('/') .some((part) => part === '' || part === '.' || part === '..') ) { throw new Error( 'Extension installer returned an invalid entrypoint' ) } const entrypoint = resolve(root, relativeEntrypoint) this.assertContained(root, entrypoint) const [canonicalRoot, canonicalEntrypoint] = await Promise.all([ realpath(root), realpath(entrypoint) ]) this.assertContained(canonicalRoot, canonicalEntrypoint) const status = await lstat(canonicalEntrypoint) if (!status.isFile()) { throw new Error('Extension entrypoint is not a regular file') } return canonicalEntrypoint } private async removeManagedTree(path: string): Promise { this.assertContained(this.managedRoot, path) let status try { status = await lstat(path) } catch (error) { if (isMissingFileError(error)) { return } throw error } if (status.isDirectory() && !status.isSymbolicLink()) { for (const entry of await readdir(path)) { await this.removeManagedTree(join(path, entry)) } await rmdir(path) } else { await unlink(path) } } }