mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
644 lines
20 KiB
TypeScript
644 lines
20 KiB
TypeScript
import path from 'node:path';
|
|
|
|
import { readPrivateLocalCommandFile } from '@qinglong/local-command-file';
|
|
import { MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE } from '@qinglong/runtime-core/plugin-package-install';
|
|
import { MAX_PLUGIN_PACKAGE_RECOVERY_PAGES } from '@qinglong/runtime-core/plugin-package-recovery';
|
|
import {
|
|
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGES,
|
|
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGE_SIZE,
|
|
} from '@qinglong/runtime-core/plugin-package-task-publication';
|
|
|
|
import type { LocalApplicationProfile } from '../application-runtime/contract';
|
|
|
|
export const LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA =
|
|
'qinglong/local-application-process@v1' as const;
|
|
export const LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V2 =
|
|
'qinglong/local-application-process@v2' as const;
|
|
export const LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V3 =
|
|
'qinglong/local-application-process@v3' as const;
|
|
export const LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4 =
|
|
'qinglong/local-application-process@v4' as const;
|
|
|
|
const MAX_PATH_BYTES = 4_096;
|
|
const INSTANCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
|
|
|
export interface LocalApplicationProcessAdoptedStorageConfig {
|
|
readonly mode?: 'adopted';
|
|
readonly sourcePath: string;
|
|
readonly targetPath: string;
|
|
readonly recoveryPath: string;
|
|
readonly manifestPath: string;
|
|
readonly activationPath: string;
|
|
readonly expectedActivationDigest: string;
|
|
readonly busyTimeoutMs?: number;
|
|
}
|
|
|
|
export interface LocalApplicationProcessFreshStorageConfig {
|
|
readonly mode: 'fresh';
|
|
readonly databasePath: string;
|
|
readonly busyTimeoutMs?: number;
|
|
}
|
|
|
|
export type LocalApplicationProcessStorageConfig =
|
|
| LocalApplicationProcessAdoptedStorageConfig
|
|
| LocalApplicationProcessFreshStorageConfig;
|
|
|
|
export interface LocalApplicationProcessRuntimeConfig {
|
|
readonly receiptRoot: string;
|
|
readonly artifactRoot: string;
|
|
readonly secretKeyringPath: string;
|
|
}
|
|
|
|
export interface LocalApplicationProcessCutoverConfig {
|
|
readonly cutoverId: string;
|
|
readonly commitmentPath: string;
|
|
readonly expectedCommitmentDigest: string;
|
|
}
|
|
|
|
export interface LocalApplicationProcessLegacyDataApplicationConfig {
|
|
readonly commitPath: string;
|
|
readonly expectedCommitDigest: string;
|
|
readonly expectedReceiptDigest: string;
|
|
}
|
|
|
|
export type LocalApplicationProcessPluginPackageRecoverySourceConfig =
|
|
| Readonly<{ mode: 'disabled' }>
|
|
| Readonly<{
|
|
mode: 'materialized_catalog';
|
|
catalogRoot: string;
|
|
bundleRoot: string;
|
|
publisherTrustFilePath: string;
|
|
}>;
|
|
|
|
export interface LocalApplicationProcessPluginPackageConfig {
|
|
readonly stagingRoot: string;
|
|
readonly activationRoot: string;
|
|
readonly recoverySource: LocalApplicationProcessPluginPackageRecoverySourceConfig;
|
|
readonly pageSize?: number;
|
|
readonly maxPages?: number;
|
|
readonly taskPublicationPageSize?: number;
|
|
readonly taskPublicationMaxPages?: number;
|
|
}
|
|
|
|
export type LocalApplicationProcessAiConfig =
|
|
| Readonly<{ deployment: 'excluded' }>
|
|
| Readonly<{
|
|
deployment: 'installed';
|
|
maxConcurrent?: number;
|
|
recoveryLimit?: number;
|
|
drainTimeoutMs?: number;
|
|
drainPollMs?: number;
|
|
}>;
|
|
|
|
export interface LocalApplicationProcessConfig {
|
|
readonly schema:
|
|
| typeof LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA
|
|
| typeof LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V2
|
|
| typeof LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V3
|
|
| typeof LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4;
|
|
readonly instanceId: string;
|
|
readonly profile: LocalApplicationProfile;
|
|
readonly storage: Readonly<LocalApplicationProcessStorageConfig>;
|
|
readonly runtime: Readonly<LocalApplicationProcessRuntimeConfig>;
|
|
readonly pluginPackages: Readonly<LocalApplicationProcessPluginPackageConfig>;
|
|
readonly ai: LocalApplicationProcessAiConfig;
|
|
readonly cutover?: Readonly<LocalApplicationProcessCutoverConfig>;
|
|
readonly legacyDataApplication?: Readonly<LocalApplicationProcessLegacyDataApplicationConfig>;
|
|
}
|
|
|
|
export class LocalApplicationProcessConfigError extends TypeError {
|
|
readonly code = 'QL3_LOCAL_APPLICATION_PROCESS_CONFIG_INVALID';
|
|
|
|
constructor(message: string, options?: ErrorOptions) {
|
|
super(
|
|
`Local application process configuration is invalid: ${message}`,
|
|
options,
|
|
);
|
|
this.name = 'LocalApplicationProcessConfigError';
|
|
}
|
|
}
|
|
|
|
function record(value: unknown, label: string): Record<string, unknown> {
|
|
if (
|
|
!value ||
|
|
typeof value !== 'object' ||
|
|
Array.isArray(value) ||
|
|
(Object.getPrototypeOf(value) !== Object.prototype &&
|
|
Object.getPrototypeOf(value) !== null)
|
|
) {
|
|
throw new LocalApplicationProcessConfigError(`${label} must be an object`);
|
|
}
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function exactKeys(
|
|
value: Record<string, unknown>,
|
|
expected: readonly string[],
|
|
label: string,
|
|
): void {
|
|
const actual = Object.keys(value).sort();
|
|
const canonical = [...expected].sort();
|
|
if (
|
|
actual.length !== canonical.length ||
|
|
actual.some((key, index) => key !== canonical[index])
|
|
) {
|
|
throw new LocalApplicationProcessConfigError(`${label} shape is invalid`);
|
|
}
|
|
}
|
|
|
|
function absolutePath(value: unknown, label: string): string {
|
|
if (
|
|
typeof value !== 'string' ||
|
|
value.length === 0 ||
|
|
Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES ||
|
|
value.includes('\0') ||
|
|
!path.isAbsolute(value) ||
|
|
path.normalize(value) !== value ||
|
|
path.parse(value).root === value
|
|
) {
|
|
throw new LocalApplicationProcessConfigError(
|
|
`${label} must be a normalized bounded absolute non-root path`,
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function optionalInteger(
|
|
value: unknown,
|
|
minimum: number,
|
|
maximum: number,
|
|
label: string,
|
|
): number | undefined {
|
|
if (value === undefined) return undefined;
|
|
if (
|
|
!Number.isSafeInteger(value) ||
|
|
(value as number) < minimum ||
|
|
(value as number) > maximum
|
|
) {
|
|
throw new LocalApplicationProcessConfigError(`${label} is invalid`);
|
|
}
|
|
return value as number;
|
|
}
|
|
|
|
function storageConfig(
|
|
value: unknown,
|
|
schema:
|
|
| typeof LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA
|
|
| typeof LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V2
|
|
| typeof LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V3
|
|
| typeof LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4,
|
|
): Readonly<LocalApplicationProcessStorageConfig> {
|
|
const storage = record(value, 'storage');
|
|
if (schema !== LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA) {
|
|
if (storage.mode === 'fresh') {
|
|
const optionalKeys = Object.hasOwn(storage, 'busyTimeoutMs')
|
|
? ['busyTimeoutMs']
|
|
: [];
|
|
exactKeys(
|
|
storage,
|
|
['databasePath', 'mode', ...optionalKeys],
|
|
'fresh storage',
|
|
);
|
|
const busyTimeoutMs = optionalInteger(
|
|
storage.busyTimeoutMs,
|
|
100,
|
|
30_000,
|
|
'busyTimeoutMs',
|
|
);
|
|
return Object.freeze({
|
|
mode: 'fresh' as const,
|
|
databasePath: absolutePath(storage.databasePath, 'databasePath'),
|
|
...(busyTimeoutMs === undefined ? {} : { busyTimeoutMs }),
|
|
});
|
|
}
|
|
if (storage.mode !== 'adopted') {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'storage mode must be fresh or adopted',
|
|
);
|
|
}
|
|
}
|
|
const optionalKeys = Object.hasOwn(storage, 'busyTimeoutMs')
|
|
? ['busyTimeoutMs']
|
|
: [];
|
|
exactKeys(
|
|
storage,
|
|
[
|
|
'activationPath',
|
|
'expectedActivationDigest',
|
|
'manifestPath',
|
|
'recoveryPath',
|
|
'sourcePath',
|
|
'targetPath',
|
|
...(schema !== LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA ? ['mode'] : []),
|
|
...optionalKeys,
|
|
],
|
|
'storage',
|
|
);
|
|
const expectedActivationDigest = storage.expectedActivationDigest;
|
|
if (
|
|
typeof expectedActivationDigest !== 'string' ||
|
|
!DIGEST_PATTERN.test(expectedActivationDigest)
|
|
) {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'expectedActivationDigest is invalid',
|
|
);
|
|
}
|
|
const busyTimeoutMs = optionalInteger(
|
|
storage.busyTimeoutMs,
|
|
100,
|
|
30_000,
|
|
'busyTimeoutMs',
|
|
);
|
|
const result: LocalApplicationProcessStorageConfig = {
|
|
...(schema !== LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA
|
|
? { mode: 'adopted' as const }
|
|
: {}),
|
|
sourcePath: absolutePath(storage.sourcePath, 'sourcePath'),
|
|
targetPath: absolutePath(storage.targetPath, 'targetPath'),
|
|
recoveryPath: absolutePath(storage.recoveryPath, 'recoveryPath'),
|
|
manifestPath: absolutePath(storage.manifestPath, 'manifestPath'),
|
|
activationPath: absolutePath(storage.activationPath, 'activationPath'),
|
|
expectedActivationDigest,
|
|
...(busyTimeoutMs === undefined ? {} : { busyTimeoutMs }),
|
|
};
|
|
const authorityPaths = [
|
|
result.sourcePath,
|
|
result.targetPath,
|
|
result.recoveryPath,
|
|
result.manifestPath,
|
|
result.activationPath,
|
|
];
|
|
if (new Set(authorityPaths).size !== authorityPaths.length) {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'storage authority paths must be distinct',
|
|
);
|
|
}
|
|
return Object.freeze(result);
|
|
}
|
|
|
|
function cutoverConfig(
|
|
value: unknown,
|
|
): Readonly<LocalApplicationProcessCutoverConfig> {
|
|
const cutover = record(value, 'cutover');
|
|
exactKeys(
|
|
cutover,
|
|
['commitmentPath', 'cutoverId', 'expectedCommitmentDigest'],
|
|
'cutover',
|
|
);
|
|
if (
|
|
typeof cutover.cutoverId !== 'string' ||
|
|
!INSTANCE_ID_PATTERN.test(cutover.cutoverId)
|
|
) {
|
|
throw new LocalApplicationProcessConfigError('cutoverId is invalid');
|
|
}
|
|
if (
|
|
typeof cutover.expectedCommitmentDigest !== 'string' ||
|
|
!DIGEST_PATTERN.test(cutover.expectedCommitmentDigest)
|
|
) {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'expectedCommitmentDigest is invalid',
|
|
);
|
|
}
|
|
return Object.freeze({
|
|
cutoverId: cutover.cutoverId,
|
|
commitmentPath: absolutePath(cutover.commitmentPath, 'commitmentPath'),
|
|
expectedCommitmentDigest: cutover.expectedCommitmentDigest,
|
|
});
|
|
}
|
|
|
|
function legacyDataApplicationConfig(
|
|
value: unknown,
|
|
): Readonly<LocalApplicationProcessLegacyDataApplicationConfig> {
|
|
const application = record(value, 'legacyDataApplication');
|
|
exactKeys(
|
|
application,
|
|
['commitPath', 'expectedCommitDigest', 'expectedReceiptDigest'],
|
|
'legacyDataApplication',
|
|
);
|
|
if (
|
|
typeof application.expectedCommitDigest !== 'string' ||
|
|
!DIGEST_PATTERN.test(application.expectedCommitDigest) ||
|
|
typeof application.expectedReceiptDigest !== 'string' ||
|
|
!DIGEST_PATTERN.test(application.expectedReceiptDigest)
|
|
) {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'legacyDataApplication digest is invalid',
|
|
);
|
|
}
|
|
return Object.freeze({
|
|
commitPath: absolutePath(application.commitPath, 'commitPath'),
|
|
expectedCommitDigest: application.expectedCommitDigest,
|
|
expectedReceiptDigest: application.expectedReceiptDigest,
|
|
});
|
|
}
|
|
|
|
function runtimeConfig(
|
|
value: unknown,
|
|
): Readonly<LocalApplicationProcessRuntimeConfig> {
|
|
const runtime = record(value, 'runtime');
|
|
exactKeys(
|
|
runtime,
|
|
['artifactRoot', 'receiptRoot', 'secretKeyringPath'],
|
|
'runtime',
|
|
);
|
|
const result = {
|
|
receiptRoot: absolutePath(runtime.receiptRoot, 'receiptRoot'),
|
|
artifactRoot: absolutePath(runtime.artifactRoot, 'artifactRoot'),
|
|
secretKeyringPath: absolutePath(
|
|
runtime.secretKeyringPath,
|
|
'secretKeyringPath',
|
|
),
|
|
};
|
|
if (new Set(Object.values(result)).size !== Object.keys(result).length) {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'runtime authority paths must be distinct',
|
|
);
|
|
}
|
|
return Object.freeze(result);
|
|
}
|
|
|
|
function pluginPackageConfig(
|
|
value: unknown,
|
|
): Readonly<LocalApplicationProcessPluginPackageConfig> {
|
|
const pluginPackages = record(value, 'pluginPackages');
|
|
const optionalKeys = [
|
|
'maxPages',
|
|
'pageSize',
|
|
'taskPublicationMaxPages',
|
|
'taskPublicationPageSize',
|
|
].filter((key) => Object.hasOwn(pluginPackages, key));
|
|
exactKeys(
|
|
pluginPackages,
|
|
['activationRoot', 'recoverySource', 'stagingRoot', ...optionalKeys],
|
|
'pluginPackages',
|
|
);
|
|
const stagingRoot = absolutePath(pluginPackages.stagingRoot, 'stagingRoot');
|
|
const activationRoot = absolutePath(
|
|
pluginPackages.activationRoot,
|
|
'activationRoot',
|
|
);
|
|
if (stagingRoot === activationRoot) {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'Plugin Package authority roots must be distinct',
|
|
);
|
|
}
|
|
const recoverySourceValue = record(
|
|
pluginPackages.recoverySource,
|
|
'Plugin Package recovery source',
|
|
);
|
|
let recoverySource: LocalApplicationProcessPluginPackageRecoverySourceConfig;
|
|
if (recoverySourceValue.mode === 'disabled') {
|
|
exactKeys(
|
|
recoverySourceValue,
|
|
['mode'],
|
|
'disabled Plugin Package recovery source',
|
|
);
|
|
recoverySource = Object.freeze({ mode: 'disabled' as const });
|
|
} else if (recoverySourceValue.mode === 'materialized_catalog') {
|
|
exactKeys(
|
|
recoverySourceValue,
|
|
['bundleRoot', 'catalogRoot', 'mode', 'publisherTrustFilePath'],
|
|
'materialized Plugin Package recovery source',
|
|
);
|
|
const catalogRoot = absolutePath(
|
|
recoverySourceValue.catalogRoot,
|
|
'Plugin Package catalogRoot',
|
|
);
|
|
const publisherTrustFilePath = absolutePath(
|
|
recoverySourceValue.publisherTrustFilePath,
|
|
'Plugin Package publisherTrustFilePath',
|
|
);
|
|
const bundleRoot = absolutePath(
|
|
recoverySourceValue.bundleRoot,
|
|
'Plugin Package bundleRoot',
|
|
);
|
|
if (
|
|
new Set([
|
|
stagingRoot,
|
|
activationRoot,
|
|
catalogRoot,
|
|
bundleRoot,
|
|
publisherTrustFilePath,
|
|
]).size !== 5
|
|
) {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'Plugin Package recovery authorities must be distinct',
|
|
);
|
|
}
|
|
recoverySource = Object.freeze({
|
|
mode: 'materialized_catalog' as const,
|
|
catalogRoot,
|
|
bundleRoot,
|
|
publisherTrustFilePath,
|
|
});
|
|
} else {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'Plugin Package recovery source mode is invalid',
|
|
);
|
|
}
|
|
const pageSize = optionalInteger(
|
|
pluginPackages.pageSize,
|
|
1,
|
|
MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE,
|
|
'Plugin Package recovery pageSize',
|
|
);
|
|
const maxPages = optionalInteger(
|
|
pluginPackages.maxPages,
|
|
1,
|
|
MAX_PLUGIN_PACKAGE_RECOVERY_PAGES,
|
|
'Plugin Package recovery maxPages',
|
|
);
|
|
const taskPublicationPageSize = optionalInteger(
|
|
pluginPackages.taskPublicationPageSize,
|
|
1,
|
|
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGE_SIZE,
|
|
'Plugin Package Task publication pageSize',
|
|
);
|
|
const taskPublicationMaxPages = optionalInteger(
|
|
pluginPackages.taskPublicationMaxPages,
|
|
1,
|
|
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGES,
|
|
'Plugin Package Task publication maxPages',
|
|
);
|
|
return Object.freeze({
|
|
stagingRoot,
|
|
activationRoot,
|
|
recoverySource,
|
|
...(pageSize === undefined ? {} : { pageSize }),
|
|
...(maxPages === undefined ? {} : { maxPages }),
|
|
...(taskPublicationPageSize === undefined
|
|
? {}
|
|
: { taskPublicationPageSize }),
|
|
...(taskPublicationMaxPages === undefined
|
|
? {}
|
|
: { taskPublicationMaxPages }),
|
|
});
|
|
}
|
|
|
|
function aiConfig(value: unknown): LocalApplicationProcessAiConfig {
|
|
const ai = record(value, 'ai');
|
|
if (ai.deployment === 'excluded') {
|
|
exactKeys(ai, ['deployment'], 'excluded AI deployment');
|
|
return Object.freeze({ deployment: 'excluded' as const });
|
|
}
|
|
if (ai.deployment !== 'installed') {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'AI deployment must be excluded or installed',
|
|
);
|
|
}
|
|
const optionalKeys = [
|
|
'drainPollMs',
|
|
'drainTimeoutMs',
|
|
'maxConcurrent',
|
|
'recoveryLimit',
|
|
].filter((key) => Object.hasOwn(ai, key));
|
|
exactKeys(ai, ['deployment', ...optionalKeys], 'installed AI deployment');
|
|
const maxConcurrent = optionalInteger(
|
|
ai.maxConcurrent,
|
|
1,
|
|
64,
|
|
'AI maxConcurrent',
|
|
);
|
|
const recoveryLimit = optionalInteger(
|
|
ai.recoveryLimit,
|
|
1,
|
|
128,
|
|
'AI recoveryLimit',
|
|
);
|
|
const drainTimeoutMs = optionalInteger(
|
|
ai.drainTimeoutMs,
|
|
100,
|
|
60_000,
|
|
'AI drainTimeoutMs',
|
|
);
|
|
const drainPollMs = optionalInteger(
|
|
ai.drainPollMs,
|
|
10,
|
|
1_000,
|
|
'AI drainPollMs',
|
|
);
|
|
return Object.freeze({
|
|
deployment: 'installed' as const,
|
|
...(maxConcurrent === undefined ? {} : { maxConcurrent }),
|
|
...(recoveryLimit === undefined ? {} : { recoveryLimit }),
|
|
...(drainTimeoutMs === undefined ? {} : { drainTimeoutMs }),
|
|
...(drainPollMs === undefined ? {} : { drainPollMs }),
|
|
});
|
|
}
|
|
|
|
export function normalizeLocalApplicationProcessConfig(
|
|
value: unknown,
|
|
): Readonly<LocalApplicationProcessConfig> {
|
|
const config = record(value, 'configuration');
|
|
if (
|
|
config.schema !== LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA &&
|
|
config.schema !== LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V2 &&
|
|
config.schema !== LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V3 &&
|
|
config.schema !== LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4
|
|
) {
|
|
throw new LocalApplicationProcessConfigError('schema is invalid');
|
|
}
|
|
exactKeys(
|
|
config,
|
|
[
|
|
'ai',
|
|
...(config.schema === LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V3 ||
|
|
config.schema === LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4
|
|
? ['cutover']
|
|
: []),
|
|
'instanceId',
|
|
...(config.schema === LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4
|
|
? ['legacyDataApplication']
|
|
: []),
|
|
'pluginPackages',
|
|
'profile',
|
|
'runtime',
|
|
'schema',
|
|
'storage',
|
|
],
|
|
'configuration',
|
|
);
|
|
if (
|
|
typeof config.instanceId !== 'string' ||
|
|
!INSTANCE_ID_PATTERN.test(config.instanceId)
|
|
) {
|
|
throw new LocalApplicationProcessConfigError('instanceId is invalid');
|
|
}
|
|
if (config.profile !== 'edge' && config.profile !== 'standalone') {
|
|
throw new LocalApplicationProcessConfigError('profile is invalid');
|
|
}
|
|
if (
|
|
(config.schema === LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V3 ||
|
|
config.schema === LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4) &&
|
|
record(config.storage, 'storage').mode !== 'adopted'
|
|
) {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'v3 configuration requires adopted storage',
|
|
);
|
|
}
|
|
const normalized = {
|
|
schema: config.schema,
|
|
instanceId: config.instanceId,
|
|
profile: config.profile,
|
|
storage: storageConfig(config.storage, config.schema),
|
|
runtime: runtimeConfig(config.runtime),
|
|
pluginPackages: pluginPackageConfig(config.pluginPackages),
|
|
ai: aiConfig(config.ai),
|
|
...(config.schema === LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V3 ||
|
|
config.schema === LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4
|
|
? { cutover: cutoverConfig(config.cutover) }
|
|
: {}),
|
|
...(config.schema === LOCAL_APPLICATION_PROCESS_CONFIG_SCHEMA_V4
|
|
? {
|
|
legacyDataApplication: legacyDataApplicationConfig(
|
|
config.legacyDataApplication,
|
|
),
|
|
}
|
|
: {}),
|
|
} as const;
|
|
const authorityPaths = [
|
|
...(normalized.storage.mode === 'fresh'
|
|
? [normalized.storage.databasePath]
|
|
: [
|
|
normalized.storage.sourcePath,
|
|
normalized.storage.targetPath,
|
|
normalized.storage.recoveryPath,
|
|
normalized.storage.manifestPath,
|
|
normalized.storage.activationPath,
|
|
]),
|
|
normalized.runtime.receiptRoot,
|
|
normalized.runtime.artifactRoot,
|
|
normalized.runtime.secretKeyringPath,
|
|
normalized.pluginPackages.stagingRoot,
|
|
normalized.pluginPackages.activationRoot,
|
|
...(normalized.cutover === undefined
|
|
? []
|
|
: [normalized.cutover.commitmentPath]),
|
|
...(normalized.legacyDataApplication === undefined
|
|
? []
|
|
: [normalized.legacyDataApplication.commitPath]),
|
|
...(normalized.pluginPackages.recoverySource.mode === 'materialized_catalog'
|
|
? [
|
|
normalized.pluginPackages.recoverySource.catalogRoot,
|
|
normalized.pluginPackages.recoverySource.bundleRoot,
|
|
normalized.pluginPackages.recoverySource.publisherTrustFilePath,
|
|
]
|
|
: []),
|
|
];
|
|
if (new Set(authorityPaths).size !== authorityPaths.length) {
|
|
throw new LocalApplicationProcessConfigError(
|
|
'process authority paths must be distinct',
|
|
);
|
|
}
|
|
return Object.freeze(normalized);
|
|
}
|
|
|
|
export function loadLocalApplicationProcessConfig(
|
|
configFilePath: string,
|
|
): Readonly<LocalApplicationProcessConfig> {
|
|
return normalizeLocalApplicationProcessConfig(
|
|
readPrivateLocalCommandFile(configFilePath),
|
|
);
|
|
}
|