Read a store's config from the registry record
`GET /api/stores` records now carry a `config` field — a store's `config.json` moved into the record that already said what the store is — and the client applies it directly. Installing a store no longer depends on a second repository existing and staying reachable, and a store can be configured from the site's admin alone. The order is registry config, then a repository's `config.json`, then the engine's defaults. The middle one is why nothing has to move at once: a registry whose stores have not been migrated is read exactly as before. The window cannot supply a config. It is handed stores to show and hands one back to install, but only as an identity: `RegistryStoreDtoMapper.toModel` drops the config and `StoreProvisioningService` reads the record again from the registry first. A config decides where files are written and, through `paths.subfolder`, which subtree the store may later delete from — not a decision the renderer gets to make, for the same reason a `GameDto` carries no paths. Tested by installing from a record carrying `subfolder: "ATTACKER"` and `install_root: "/tmp/pwned"` and finding neither on disk. A store that has left the registry, or a registry that cannot be re-read, still installs: it falls back to the engine's defaults rather than refusing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,12 +16,20 @@ export class RegistryStoreDtoMapper {
|
||||
return stores.map((store: RegistryStore): RegistryStoreDto => this.toDto(store))
|
||||
}
|
||||
|
||||
/** The window hands a record straight back when asking for an install. */
|
||||
/**
|
||||
* The window hands a record back when asking for an install — as an identity only.
|
||||
*
|
||||
* There is no `config` here on purpose. A store's config decides where files are
|
||||
* written and which subtree the store may delete from, so it must not be something
|
||||
* the window can supply; `StoreProvisioningService` reads the real record from the
|
||||
* registry instead. That is the same rule as `GameDto` carrying no paths.
|
||||
*/
|
||||
public toModel (dto: RegistryStoreDto): RegistryStore {
|
||||
return {
|
||||
name: dto.name,
|
||||
catalogUrl: dto.catalogUrl,
|
||||
storeRepositoryUrl: dto.storeRepositoryUrl
|
||||
storeRepositoryUrl: dto.storeRepositoryUrl,
|
||||
config: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,44 @@ export class StoreProvisioningService {
|
||||
}
|
||||
|
||||
public async installStore (
|
||||
store: RegistryStore,
|
||||
chosen: RegistryStore,
|
||||
progress?: EngineProgressListener
|
||||
): Promise<InstalledStore> {
|
||||
const store = await this.resolveFromRegistry(chosen, progress)
|
||||
const home = this.stores.resolveDefaultHome(deriveStoreId(store))
|
||||
const installed = await this.installer.installEngine(home, store, progress)
|
||||
return this.selection.adoptStore(installed)
|
||||
}
|
||||
|
||||
/**
|
||||
* The registry's own record for the store that was chosen.
|
||||
*
|
||||
* The window is handed stores to display and hands one back to install, but what it
|
||||
* hands back is not what gets used. A store's config decides where files are written
|
||||
* and which subtree the store may later delete from, so it cannot be something the
|
||||
* window supplies — the choice is treated as an identity, a name and a catalog, and
|
||||
* the record behind it is read again here.
|
||||
*
|
||||
* A store that has since left the registry, or a registry that cannot be reached, is
|
||||
* not a reason to refuse the install: it proceeds on the engine's defaults, which is
|
||||
* what a store with no config gets anyway.
|
||||
*/
|
||||
private async resolveFromRegistry (
|
||||
chosen: RegistryStore,
|
||||
progress?: EngineProgressListener
|
||||
): Promise<RegistryStore> {
|
||||
try {
|
||||
const listed = await this.registry.listStores()
|
||||
const found = listed.find((store: RegistryStore): boolean =>
|
||||
store.catalogUrl === chosen.catalogUrl && store.name === chosen.name)
|
||||
if (found !== undefined) return found
|
||||
progress?.onLog?.(
|
||||
`${chosen.name} is no longer in the registry — installing on the engine's defaults`)
|
||||
} catch (error: unknown) {
|
||||
const reason = error instanceof Error ? error.message : String(error)
|
||||
progress?.onLog?.(
|
||||
`the registry could not be read again (${reason}) — installing on the engine's defaults`)
|
||||
}
|
||||
return { ...chosen, config: null }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user