blob: d2c0dc46621feb9fc591b80d71f713ebb825cc08 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { randomUUID } from 'node:crypto';
import { chmod, lstat, mkdir, open, realpath, rename, rm } from 'node:fs/promises';
import { userInfo } from 'node:os';
import { dirname, isAbsolute, join, parse, posix, relative, resolve, sep, win32 } from 'node:path';
import { isDeepStrictEqual } from 'node:util';
import {
type StateRootOwner,
type StorageRootCapability,
assertStorageRootLease,
repairStorageRootAfterRemount,
resolveExistingStorageRoot,
tryAcquireStateRootOwner,
} from '@maka/storage/root-authority';
import {
readStableBoundedFile,
syncDirectory,
syncDirectoryChain,
} from '@maka/storage/stable-storage';
import { z } from 'zod';
import { isCanonicalRuntimeHostWebSocketPath } from '../protocol/websocket-path.js';
import { runtimeHostWebRtcStunPolicySchema } from '../webrtc-stun-policy.js';
import {
isProductReleaseVersion,
isSha512PackageIntegrity,
resolveRuntimeHostNpmDeploymentLayout,
} from './update-package-evidence.js';
import { isNodeError } from '../node-error.js';
export const RUNTIME_HOST_MANAGED_DEPLOYMENT_CONFIG_FILE = 'runtime-host-deployment.json';
const SCHEMA_VERSION = 1 as const;
const MAX_DOCUMENT_BYTES = 256 * 1024;
const ROOT_ID_PATTERN = /^[a-f0-9]{64}$/u;
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
const boundedText = (maximumBytes: number) =>
z
.string()
.min(1)
.refine(
(value) =>
Buffer.byteLength(value, 'utf8') <= maximumBytes && !/[\u0000-\u001f\u007f]/u.test(value),
);
const absolutePathSchema = boundedText(4_096).refine(isAbsolute);
const deploymentIdSchema = z.string().regex(UUID_PATTERN);
const configRevisionSchema = z.number().int().positive().safe();
export const runtimeHostSupervisorProviderSchema = z.enum([
'systemd_user',
'launch_agent',
'openrc_user',
'openrc_system',
'windows_task',
]);
export const runtimeHostReconciliationProviderSchema = z.enum([
'systemd_timer',
'launch_agent_timer',
'openrc_supervised_loop',
'windows_task_timer',
]);
const packageIdentitySchema = z
.object({
kind: z.literal('npm_registry'),
version: z.string().refine(isProductReleaseVersion),
integrity: z.string().refine(isSha512PackageIntegrity),
})
.strict();
const lifecycleSchema = z.discriminatedUnion('mode', [
z
.object({
mode: z.literal('on_demand'),
availability: z.literal('activation'),
})
.strict(),
z
.object({
mode: z.literal('supervised'),
provider: runtimeHostSupervisorProviderSchema,
availability: z.enum(['session', 'environment', 'machine']),
})
.strict(),
]);
const reconciliationSchema = z.discriminatedUnion('trigger', [
z.object({ trigger: z.literal('manual') }).strict(),
z.object({ trigger: z.literal('activation') }).strict(),
z
.object({
trigger: z.literal('scheduled'),
provider: runtimeHostReconciliationProviderSchema,
})
.strict(),
]);
const managedLaunchClaimSchema = z
.object({
deploymentId: deploymentIdSchema,
configRevision: configRevisionSchema,
})
.strict();
const deploymentTransitionOperationSchema = z.enum([
'install',
'legacy_migration',
'lifecycle_change',
'provider_change',
'configure',
'update',
'uninstall',
]);
const deploymentTransitionRecoverySchema = z.enum(['restore_from', 'complete_to']);
const deploymentAuthorityRootSchema = z
.object({
path: absolutePathSchema,
id: z.string().regex(ROOT_ID_PATTERN),
})
.strict();
const managedDeploymentConfigSchema = z
.object({
schemaVersion: z.literal(SCHEMA_VERSION),
state: z.literal('active'),
deploymentId: deploymentIdSchema,
configRevision: configRevisionSchema,
deploymentRoot: absolutePathSchema,
root: z
.object({
path: absolutePathSchema,
id: z.string().regex(ROOT_ID_PATTERN),
})
.strict(),
projectDirectoryRoots: z
.array(
z
.object({
label: boundedText(256),
path: absolutePathSchema,
})
.strict(),
)
.max(128),
launch: z
.object({
kind: z.literal('exact_package'),
nodePath: absolutePathSchema,
package: packageIdentitySchema,
})
.strict(),
listeners: z
.object({
localIpc: z.literal(true),
websocket: z
.object({
host: z.literal('127.0.0.1'),
port: z.number().int().min(0).max(65_535),
path: boundedText(2_048).refine(isCanonicalRuntimeHostWebSocketPath),
})
.strict()
.optional(),
directPeer: z
.object({
enabled: z.boolean(),
keyPath: absolutePathSchema,
peerId: boundedText(256),
listenAddresses: z.array(boundedText(2_048)).min(1).max(16),
coordinationRelays: z.array(boundedText(2_048)).max(16),
automaticRelayDiscovery: z.boolean().default(true),
webRtcStunPolicy: runtimeHostWebRtcStunPolicySchema.default({ kind: 'default' }),
})
.strict()
.optional(),
})
.strict(),
lifecycle: lifecycleSchema,
reconciliation: reconciliationSchema,
})
.strict()
.superRefine((value, context) => {
if (value.lifecycle.mode === 'on_demand' && value.reconciliation.trigger === 'scheduled') {
context.addIssue({
code: 'custom',
message: 'An on-demand deployment cannot use scheduled reconciliation',
path: ['reconciliation'],
});
}
if (value.lifecycle.mode === 'on_demand' && value.listeners.directPeer?.enabled === true) {
context.addIssue({
code: 'custom',
message: 'An on-demand deployment cannot enable a direct peer listener',
path: ['listeners', 'directPeer'],
});
}
if (value.lifecycle.mode === 'supervised' && value.reconciliation.trigger === 'activation') {
context.addIssue({
code: 'custom',
message: 'A supervised deployment cannot reconcile during Client activation',
path: ['reconciliation'],
});
}
if (value.lifecycle.mode === 'supervised' && value.reconciliation.trigger === 'scheduled') {
const expected =
value.lifecycle.provider === 'systemd_user'
? 'systemd_timer'
: value.lifecycle.provider === 'launch_agent'
? 'launch_agent_timer'
: value.lifecycle.provider === 'windows_task'
? 'windows_task_timer'
: 'openrc_supervised_loop';
if (value.reconciliation.provider !== expected) {
context.addIssue({
code: 'custom',
message: 'The reconciliation trigger does not match the persisted supervisor provider',
path: ['reconciliation', 'provider'],
});
}
}
});
const managedDeploymentTransitionSchema = z
.object({
schemaVersion: z.literal(SCHEMA_VERSION),
state: z.literal('transition'),
transactionId: deploymentIdSchema,
operation: deploymentTransitionOperationSchema,
recovery: deploymentTransitionRecoverySchema,
root: deploymentAuthorityRootSchema,
from: managedDeploymentConfigSchema.nullable(),
to: managedDeploymentConfigSchema.nullable(),
})
.strict()
.superRefine(validateDeploymentTransitionEndpoints);
const managedDeploymentBlockedSchema = z
.object({
schemaVersion: z.literal(SCHEMA_VERSION),
state: z.literal('blocked'),
transactionId: deploymentIdSchema,
operation: deploymentTransitionOperationSchema,
recovery: deploymentTransitionRecoverySchema,
root: deploymentAuthorityRootSchema,
from: managedDeploymentConfigSchema.nullable(),
to: managedDeploymentConfigSchema.nullable(),
reason: boundedText(1_024),
})
.strict()
.superRefine(validateDeploymentTransitionEndpoints);
const managedDeploymentAuthorityRecordSchema = z.union([
managedDeploymentConfigSchema,
managedDeploymentTransitionSchema,
managedDeploymentBlockedSchema,
]);
function validateDeploymentTransitionEndpoints(
value: {
readonly operation: z.infer<typeof deploymentTransitionOperationSchema>;
readonly root: z.infer<typeof deploymentAuthorityRootSchema>;
readonly from: RuntimeHostManagedDeploymentConfig | null;
readonly to: RuntimeHostManagedDeploymentConfig | null;
},
context: z.RefinementCtx,
): void {
const operationShapeValid =
(value.operation === 'install' && value.from === null && value.to !== null) ||
(value.operation === 'uninstall' && value.from !== null && value.to === null) ||
(value.operation === 'legacy_migration' &&
((value.from === null && value.to !== null) || (value.from !== null && value.to === null))) ||
((value.operation === 'lifecycle_change' ||
value.operation === 'provider_change' ||
value.operation === 'configure' ||
value.operation === 'update') &&
value.from !== null &&
value.to !== null &&
value.from.deploymentId === value.to.deploymentId &&
value.to.configRevision > value.from.configRevision);
const endpointsTargetAuthority = [value.from, value.to].every(
(config) =>
config === null || (config.root.id === value.root.id && config.root.path === value.root.path),
);
if (!operationShapeValid || !endpointsTargetAuthority) {
context.addIssue({
code: 'custom',
message: 'The managed deployment transition endpoints do not match its operation',
path: ['operation'],
});
}
}
export type RuntimeHostSupervisorProvider = z.infer<typeof runtimeHostSupervisorProviderSchema>;
export type RuntimeHostReconciliationProvider = z.infer<
typeof runtimeHostReconciliationProviderSchema
>;
export type RuntimeHostManagedDeploymentConfig = z.infer<typeof managedDeploymentConfigSchema>;
export type RuntimeHostManagedLaunchClaim = z.infer<typeof managedLaunchClaimSchema>;
export type RuntimeHostManagedDeploymentTransitionOperation = z.infer<
typeof deploymentTransitionOperationSchema
>;
export type RuntimeHostManagedDeploymentTransitionRecovery = z.infer<
typeof deploymentTransitionRecoverySchema
>;
export type RuntimeHostManagedDeploymentTransition = z.infer<
typeof managedDeploymentTransitionSchema
>;
export type RuntimeHostManagedDeploymentBlocked = z.infer<typeof managedDeploymentBlockedSchema>;
export type RuntimeHostManagedDeploymentAuthorityRecord =
| RuntimeHostManagedDeploymentConfig
| RuntimeHostManagedDeploymentTransition
| RuntimeHostManagedDeploymentBlocked;
export interface RuntimeHostManagedDeploymentTransitionInput {
readonly transactionId: string;
readonly operation: RuntimeHostManagedDeploymentTransitionOperation;
readonly recovery: RuntimeHostManagedDeploymentTransitionRecovery;
readonly expected?: RuntimeHostManagedDeploymentConfig;
readonly desired?: RuntimeHostManagedDeploymentConfig;
}
export interface RuntimeHostManagedDeploymentAuthorityOptions {
/** Explicitly accept a device-only root identity change at a known remount boundary. */
readonly repairRootAfterRemount?: true;
/** Test-only or embedding override. Production uses the account-local durable default. */
readonly authorityRoot?: string;
readonly homeDir?: string;
readonly platform?: NodeJS.Platform;
/** Pre-existing test/embedding durability anchor for an authorityRoot override. */
readonly durabilityBoundary?: string;
/** Test-only durability fault injection. */
readonly beforeDirectorySync?: (path: string) => void | Promise<void>;
}
export interface RuntimeHostManagedProcessLaunch {
readonly executablePath: string;
readonly entrypointPath: string;
}
export interface RuntimeHostManagedLaunchRequest {
readonly lifecycleMode: 'on_demand' | 'supervised';
readonly claim?: RuntimeHostManagedLaunchClaim;
readonly processLaunch: RuntimeHostManagedProcessLaunch;
}
export function currentRuntimeHostProcessLaunch(): RuntimeHostManagedProcessLaunch {
return {
executablePath: process.execPath,
entrypointPath: process.argv[1] ?? '',
};
}
export const RUNTIME_HOST_MANAGED_LAUNCH_REJECTIONS = [
'managed_root_requires_operator',
'deployment_record_missing',
'deployment_claim_mismatch',
'deployment_lifecycle_mismatch',
'deployment_launch_mismatch',
'deployment_record_invalid',
'deployment_transition_in_progress',
'deployment_needs_repair',
] as const;
export type RuntimeHostManagedLaunchRejection =
(typeof RUNTIME_HOST_MANAGED_LAUNCH_REJECTIONS)[number];
export class RuntimeHostManagedDeploymentError extends Error {
constructor(
readonly code:
| 'invalid_config'
| 'deployment_io_failed'
| 'deployment_commit_unknown'
| 'lifecycle_owner_exists'
| 'state_root_owned'
| 'deployment_transaction_mismatch'
| RuntimeHostManagedLaunchRejection,
message: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = 'RuntimeHostManagedDeploymentError';
}
}
export function decodeRuntimeHostManagedDeploymentConfig(
value: unknown,
): RuntimeHostManagedDeploymentConfig {
try {
return managedDeploymentConfigSchema.parse(value);
} catch (error) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host managed deployment config is invalid',
{ cause: error },
);
}
}
export function decodeRuntimeHostManagedDeploymentAuthorityRecord(
value: unknown,
): RuntimeHostManagedDeploymentAuthorityRecord {
try {
return managedDeploymentAuthorityRecordSchema.parse(
value,
) as RuntimeHostManagedDeploymentAuthorityRecord;
} catch (error) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host managed deployment authority record is invalid',
{ cause: error },
);
}
}
export function decodeRuntimeHostManagedLaunchClaim(value: unknown): RuntimeHostManagedLaunchClaim {
try {
return managedLaunchClaimSchema.parse(value);
} catch (error) {
throw new RuntimeHostManagedDeploymentError(
'deployment_claim_mismatch',
'The Runtime Host managed launch claim is invalid',
{ cause: error },
);
}
}
export function runtimeHostManagedLaunchClaim(
config: RuntimeHostManagedDeploymentConfig,
): RuntimeHostManagedLaunchClaim {
const canonical = decodeRuntimeHostManagedDeploymentConfig(config);
return {
deploymentId: canonical.deploymentId,
configRevision: canonical.configRevision,
};
}
export function resolveRuntimeHostManagedDeploymentAuthorityRoot(
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): string {
if (options.authorityRoot !== undefined) {
if (!isAbsolute(options.authorityRoot)) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host managed deployment authority root must be absolute',
);
}
return resolve(options.authorityRoot);
}
const homeDir = options.homeDir ?? userInfo().homedir;
const platform = options.platform ?? process.platform;
const accountPath = platform === 'win32' ? win32 : posix;
if (!accountPath.isAbsolute(homeDir)) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The OS account home must be absolute',
);
}
const segments =
platform === 'darwin'
? ['Library', 'Application Support', 'Maka', 'runtime-host-deployments']
: platform === 'win32'
? ['AppData', 'Local', 'Maka', 'runtime-host-deployments']
: ['.local', 'share', 'Maka', 'runtime-host-deployments'];
return accountPath.join(accountPath.normalize(homeDir), ...segments);
}
export function resolveRuntimeHostManagedDeploymentConfigPath(
rootId: string,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): string {
requireRootId(rootId);
return join(
resolveRuntimeHostManagedDeploymentAuthorityRoot(options),
rootId,
RUNTIME_HOST_MANAGED_DEPLOYMENT_CONFIG_FILE,
);
}
export async function readRuntimeHostManagedDeploymentConfig(
capability: StorageRootCapability<'interactive'>,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<RuntimeHostManagedDeploymentConfig | undefined> {
return readDeploymentConfigForCapability(
resolveRuntimeHostManagedDeploymentConfigPath(capability.rootId, options),
capability,
);
}
export async function readRuntimeHostManagedDeploymentAuthorityRecord(
capability: StorageRootCapability<'interactive'>,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<RuntimeHostManagedDeploymentAuthorityRecord | undefined> {
return readDeploymentAuthorityForCapability(
resolveRuntimeHostManagedDeploymentConfigPath(capability.rootId, options),
capability,
);
}
export async function assertRuntimeHostManagedDeploymentAuthorityDurablyAbsent(
owner: StateRootOwner<'interactive'>,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<void> {
if (owner.closed) {
throw new RuntimeHostManagedDeploymentError(
'state_root_owned',
'The State Root deployment owner is no longer active',
);
}
await assertStorageRootLease(owner.lease, 'interactive', 'write');
const path = resolveRuntimeHostManagedDeploymentConfigPath(owner.capability.rootId, options);
if ((await readDeploymentAuthorityForCapability(path, owner.capability)) !== undefined) {
throw new RuntimeHostManagedDeploymentError(
'lifecycle_owner_exists',
'Runtime Host lifecycle authority still exists',
);
}
await options.beforeDirectorySync?.(dirname(path));
await syncDirectory(dirname(path));
if ((await readDeploymentAuthorityForCapability(path, owner.capability)) !== undefined) {
throw new RuntimeHostManagedDeploymentError(
'lifecycle_owner_exists',
'Runtime Host lifecycle authority reappeared while confirming its removal',
);
}
}
export async function resolveRuntimeHostManagedDeployment(
rootId: string,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<{
readonly capability: StorageRootCapability<'interactive'>;
readonly config: RuntimeHostManagedDeploymentConfig;
}> {
const resolved = await resolveRuntimeHostManagedDeploymentAuthority(rootId, options);
if (!resolved) {
throw new RuntimeHostManagedDeploymentError(
'deployment_record_missing',
'The managed Runtime Host deployment is not installed',
);
}
const { capability, record } = resolved;
if (record.state !== 'active') {
throw transitionStateError(record);
}
return { capability, config: record };
}
export async function resolveRuntimeHostManagedDeploymentAuthority(
rootId: string,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<
| {
readonly capability: StorageRootCapability<'interactive'>;
readonly record: RuntimeHostManagedDeploymentAuthorityRecord;
}
| undefined
> {
requireRootId(rootId);
const path = resolveRuntimeHostManagedDeploymentConfigPath(rootId, options);
const value = await readBoundedJson(path);
if (value === undefined) return undefined;
const initial = decodeRuntimeHostManagedDeploymentAuthorityRecord(value);
if (initial.root.id !== rootId) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host managed deployment record has an invalid Root identity',
);
}
if (options.repairRootAfterRemount) {
await repairStorageRootAfterRemount({
path: initial.root.path,
kind: 'interactive',
expectedRootId: rootId,
});
}
const capability = await resolveExistingStorageRoot({
path: initial.root.path,
kind: 'interactive',
expectedRootId: rootId,
});
const record = await readRuntimeHostManagedDeploymentAuthorityRecord(capability, options);
if (!record) return undefined;
return { capability, record };
}
export async function claimRuntimeHostManagedDeployment(
capability: StorageRootCapability<'interactive'>,
config: RuntimeHostManagedDeploymentConfig,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<{
readonly kind: 'applied' | 'unchanged';
readonly config: RuntimeHostManagedDeploymentConfig;
readonly claim: RuntimeHostManagedLaunchClaim;
}> {
const canonical = decodeRuntimeHostManagedDeploymentConfig(config);
assertConfigTargetsCapability(canonical, capability);
const authorityRoot = resolveRuntimeHostManagedDeploymentAuthorityRoot(options);
const path = resolveRuntimeHostManagedDeploymentConfigPath(capability.rootId, options);
await prepareAuthorityDirectory(
dirname(path),
resolveAuthorityDurabilityBoundary(authorityRoot, options),
options,
);
const existing = await readDeploymentConfigForCapability(path, capability);
if (existing !== undefined) return existingDeploymentClaim(existing, canonical);
const owner = await tryAcquireStateRootOwner(capability);
if (!owner) {
const raced = await readDeploymentConfigForCapability(path, capability);
if (raced !== undefined) return existingDeploymentClaim(raced, canonical);
throw new RuntimeHostManagedDeploymentError(
'state_root_owned',
'The State Root must be retired before it can become managed',
);
}
try {
return await commitRuntimeHostManagedDeployment(owner, canonical, options);
} finally {
await owner.close();
}
}
export async function commitRuntimeHostManagedDeployment(
owner: StateRootOwner<'interactive'>,
config: RuntimeHostManagedDeploymentConfig,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<{
readonly kind: 'applied' | 'unchanged';
readonly config: RuntimeHostManagedDeploymentConfig;
readonly claim: RuntimeHostManagedLaunchClaim;
}> {
if (owner.closed) {
throw new RuntimeHostManagedDeploymentError(
'state_root_owned',
'The State Root deployment owner is no longer active',
);
}
const canonical = decodeRuntimeHostManagedDeploymentConfig(config);
assertConfigTargetsCapability(canonical, owner.capability);
await assertStorageRootLease(owner.lease, 'interactive', 'write');
const authorityRoot = resolveRuntimeHostManagedDeploymentAuthorityRoot(options);
const path = resolveRuntimeHostManagedDeploymentConfigPath(owner.capability.rootId, options);
await prepareAuthorityDirectory(
dirname(path),
resolveAuthorityDurabilityBoundary(authorityRoot, options),
options,
);
const current = await readDeploymentConfigForCapability(path, owner.capability);
if (current !== undefined) return existingDeploymentClaim(current, canonical);
await writePrivateJson(path, canonical, options.beforeDirectorySync);
return {
kind: 'applied',
config: canonical,
claim: runtimeHostManagedLaunchClaim(canonical),
};
}
export async function beginRuntimeHostManagedDeploymentTransition(
owner: StateRootOwner<'interactive'>,
input: RuntimeHostManagedDeploymentTransitionInput,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<{
readonly kind: 'applied' | 'unchanged';
readonly record: RuntimeHostManagedDeploymentTransition;
}> {
const transition = deploymentTransitionRecord(owner.capability, input);
await assertManagedDeploymentOwner(owner);
const path = await prepareManagedDeploymentAuthorityPath(owner.capability.rootId, options);
const current = await readDeploymentAuthorityForCapability(path, owner.capability);
if (current && current.state !== 'active') {
if (current.state === 'blocked') throw deploymentNeedsRepair(current);
if (isDeepStrictEqual(current, transition)) return { kind: 'unchanged', record: current };
throw deploymentTransactionMismatch('A different managed deployment transition is active');
}
const expected = input.expected && decodeRuntimeHostManagedDeploymentConfig(input.expected);
if (!isDeepStrictEqual(current, expected)) {
throw deploymentTransactionMismatch('The managed deployment changed before transition began');
}
await writePrivateJson(path, transition, options.beforeDirectorySync);
return { kind: 'applied', record: transition };
}
export async function commitRuntimeHostManagedDeploymentTransition(
owner: StateRootOwner<'interactive'>,
transactionId: string,
desired: RuntimeHostManagedDeploymentConfig | undefined,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<{
readonly kind: 'applied' | 'unchanged';
readonly config?: RuntimeHostManagedDeploymentConfig;
}> {
return finishRuntimeHostManagedDeploymentTransition(owner, transactionId, 'to', desired, options);
}
export async function rollbackRuntimeHostManagedDeploymentTransition(
owner: StateRootOwner<'interactive'>,
transactionId: string,
previous: RuntimeHostManagedDeploymentConfig | undefined,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<{
readonly kind: 'applied' | 'unchanged';
readonly config?: RuntimeHostManagedDeploymentConfig;
}> {
return finishRuntimeHostManagedDeploymentTransition(
owner,
transactionId,
'from',
previous,
options,
);
}
export async function blockRuntimeHostManagedDeploymentTransition(
owner: StateRootOwner<'interactive'>,
transactionId: string,
reason: string,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<{
readonly kind: 'applied' | 'unchanged';
readonly record: RuntimeHostManagedDeploymentBlocked;
}> {
await assertManagedDeploymentOwner(owner);
const path = await prepareManagedDeploymentAuthorityPath(owner.capability.rootId, options);
const current = await readDeploymentAuthorityForCapability(path, owner.capability);
if (!current || current.state === 'active' || current.transactionId !== transactionId) {
throw deploymentTransactionMismatch('The managed deployment transition is no longer current');
}
const record = decodeRuntimeHostManagedDeploymentAuthorityRecord({
...current,
state: 'blocked',
reason,
});
if (record.state !== 'blocked') {
throw deploymentTransactionMismatch('The managed deployment blocked record is invalid');
}
if (isDeepStrictEqual(current, record)) return { kind: 'unchanged', record };
await writePrivateJson(path, record, options.beforeDirectorySync);
return { kind: 'applied', record };
}
async function finishRuntimeHostManagedDeploymentTransition(
owner: StateRootOwner<'interactive'>,
transactionId: string,
endpoint: 'from' | 'to',
value: RuntimeHostManagedDeploymentConfig | undefined,
options: RuntimeHostManagedDeploymentAuthorityOptions,
): Promise<{
readonly kind: 'applied' | 'unchanged';
readonly config?: RuntimeHostManagedDeploymentConfig;
}> {
await assertManagedDeploymentOwner(owner);
const config = value && decodeRuntimeHostManagedDeploymentConfig(value);
if (config) assertConfigTargetsCapability(config, owner.capability);
const path = await prepareManagedDeploymentAuthorityPath(owner.capability.rootId, options);
const current = await readDeploymentAuthorityForCapability(path, owner.capability);
if (current === undefined) {
if (config === undefined) return { kind: 'unchanged' };
throw deploymentTransactionMismatch('The managed deployment transition record is missing');
}
if (current.state === 'active') {
if (config && isDeepStrictEqual(current, config)) {
return { kind: 'unchanged', config: current };
}
throw deploymentTransactionMismatch('The managed deployment transition is no longer current');
}
if (current.transactionId !== transactionId) {
throw deploymentTransactionMismatch('The managed deployment transaction identity changed');
}
if (!isDeepStrictEqual(current[endpoint], config ?? null)) {
throw deploymentTransactionMismatch('The managed deployment transition target changed');
}
if (config) await writePrivateJson(path, config, options.beforeDirectorySync);
else await removePrivateJson(path, options.beforeDirectorySync);
return { kind: 'applied', ...(config ? { config } : {}) };
}
function deploymentTransitionRecord(
capability: StorageRootCapability<'interactive'>,
input: RuntimeHostManagedDeploymentTransitionInput,
): RuntimeHostManagedDeploymentTransition {
const expected = input.expected && decodeRuntimeHostManagedDeploymentConfig(input.expected);
const desired = input.desired && decodeRuntimeHostManagedDeploymentConfig(input.desired);
if (expected) assertConfigTargetsCapability(expected, capability);
if (desired) assertConfigTargetsCapability(desired, capability);
const record = decodeRuntimeHostManagedDeploymentAuthorityRecord({
schemaVersion: SCHEMA_VERSION,
state: 'transition',
transactionId: input.transactionId,
operation: input.operation,
recovery: input.recovery,
root: { path: capability.canonicalPath, id: capability.rootId },
from: expected ?? null,
to: desired ?? null,
});
if (record.state !== 'transition') {
throw deploymentTransactionMismatch('The managed deployment transition is invalid');
}
return record;
}
async function assertManagedDeploymentOwner(owner: StateRootOwner<'interactive'>): Promise<void> {
if (owner.closed) {
throw new RuntimeHostManagedDeploymentError(
'state_root_owned',
'The State Root deployment owner is no longer active',
);
}
await assertStorageRootLease(owner.lease, 'interactive', 'write');
}
async function prepareManagedDeploymentAuthorityPath(
rootId: string,
options: RuntimeHostManagedDeploymentAuthorityOptions,
): Promise<string> {
const authorityRoot = resolveRuntimeHostManagedDeploymentAuthorityRoot(options);
const path = resolveRuntimeHostManagedDeploymentConfigPath(rootId, options);
await prepareAuthorityDirectory(
dirname(path),
resolveAuthorityDurabilityBoundary(authorityRoot, options),
options,
);
return path;
}
function deploymentTransactionMismatch(message: string): RuntimeHostManagedDeploymentError {
return new RuntimeHostManagedDeploymentError('deployment_transaction_mismatch', message);
}
function deploymentNeedsRepair(
record: RuntimeHostManagedDeploymentBlocked,
): RuntimeHostManagedDeploymentError {
return new RuntimeHostManagedDeploymentError(
'deployment_needs_repair',
`The managed deployment transaction ${record.transactionId} requires repair`,
);
}
function transitionStateError(
record: RuntimeHostManagedDeploymentTransition | RuntimeHostManagedDeploymentBlocked,
): RuntimeHostManagedDeploymentError {
return record.state === 'transition'
? new RuntimeHostManagedDeploymentError(
'deployment_transition_in_progress',
`The managed deployment transaction ${record.transactionId} is in progress`,
)
: deploymentNeedsRepair(record);
}
export interface RuntimeHostLaunchOwnership {
readonly owner: StateRootOwner<'interactive'>;
readonly managedConfig?: RuntimeHostManagedDeploymentConfig;
}
export async function tryAcquireRuntimeHostLaunch(
capability: StorageRootCapability<'interactive'>,
request: RuntimeHostManagedLaunchRequest,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<RuntimeHostLaunchOwnership | undefined> {
const canonicalClaim =
request.claim === undefined ? undefined : decodeRuntimeHostManagedLaunchClaim(request.claim);
const path = resolveRuntimeHostManagedDeploymentConfigPath(capability.rootId, options);
const owner = await tryAcquireStateRootOwner(capability);
if (!owner) return undefined;
try {
let config: RuntimeHostManagedDeploymentConfig | undefined;
try {
config = await readDeploymentConfigForCapability(path, capability);
} catch (error) {
if (
!(error instanceof RuntimeHostManagedDeploymentError) ||
error.code !== 'invalid_config'
) {
throw error;
}
throw new RuntimeHostManagedDeploymentError(
'deployment_record_invalid',
'The Runtime Host managed deployment record is invalid',
{ cause: error },
);
}
const rejection = runtimeHostManagedLaunchRejection(
config,
canonicalClaim,
request.lifecycleMode,
);
if (rejection !== undefined) {
throw new RuntimeHostManagedDeploymentError(
rejection,
managedLaunchRejectionMessage(rejection),
);
}
if (config && !(await matchesManagedProcessLaunch(config, request))) {
throw new RuntimeHostManagedDeploymentError(
'deployment_launch_mismatch',
managedLaunchRejectionMessage('deployment_launch_mismatch'),
);
}
return config === undefined ? { owner } : { owner, managedConfig: config };
} catch (error) {
await owner.close();
throw error;
}
}
export async function tryAcquireRuntimeHostLaunchOwner(
capability: StorageRootCapability<'interactive'>,
request: RuntimeHostManagedLaunchRequest,
options: RuntimeHostManagedDeploymentAuthorityOptions = {},
): Promise<StateRootOwner<'interactive'> | undefined> {
return (await tryAcquireRuntimeHostLaunch(capability, request, options))?.owner;
}
export function runtimeHostManagedLaunchRejection(
config: RuntimeHostManagedDeploymentConfig | undefined,
claim: RuntimeHostManagedLaunchClaim | undefined,
expectedLifecycleMode: 'on_demand' | 'supervised',
): RuntimeHostManagedLaunchRejection | undefined {
if (config === undefined) return claim === undefined ? undefined : 'deployment_record_missing';
if (claim === undefined) return 'managed_root_requires_operator';
if (!sameManagedLaunch(runtimeHostManagedLaunchClaim(config), claim)) {
return 'deployment_claim_mismatch';
}
return config.lifecycle.mode === expectedLifecycleMode
? undefined
: 'deployment_lifecycle_mismatch';
}
function sameManagedLaunch(
expected: RuntimeHostManagedLaunchClaim,
claim: RuntimeHostManagedLaunchClaim,
): boolean {
return (
expected.deploymentId === claim.deploymentId && expected.configRevision === claim.configRevision
);
}
function existingDeploymentClaim(
existing: RuntimeHostManagedDeploymentConfig,
requested: RuntimeHostManagedDeploymentConfig,
): {
readonly kind: 'unchanged';
readonly config: RuntimeHostManagedDeploymentConfig;
readonly claim: RuntimeHostManagedLaunchClaim;
} {
if (!isDeepStrictEqual(existing, requested)) {
throw new RuntimeHostManagedDeploymentError(
'lifecycle_owner_exists',
'The State Root already has a managed deployment',
);
}
return {
kind: 'unchanged',
config: existing,
claim: runtimeHostManagedLaunchClaim(existing),
};
}
async function readDeploymentConfigForCapability(
path: string,
capability: StorageRootCapability<'interactive'>,
): Promise<RuntimeHostManagedDeploymentConfig | undefined> {
const record = await readDeploymentAuthorityForCapability(path, capability);
if (record === undefined || record.state === 'active') return record;
throw transitionStateError(record);
}
async function readDeploymentAuthorityForCapability(
path: string,
capability: StorageRootCapability<'interactive'>,
): Promise<RuntimeHostManagedDeploymentAuthorityRecord | undefined> {
const value = await readBoundedJson(path);
if (value === undefined) return undefined;
const record = decodeRuntimeHostManagedDeploymentAuthorityRecord(value);
assertAuthorityTargetsCapability(record, capability);
return record;
}
function managedLaunchRejectionMessage(rejection: RuntimeHostManagedLaunchRejection): string {
switch (rejection) {
case 'managed_root_requires_operator':
return 'The State Root is managed and must be activated through its operator';
case 'deployment_record_missing':
return 'The managed Runtime Host launch has no deployment record';
case 'deployment_claim_mismatch':
return 'The Runtime Host launch does not match the managed deployment';
case 'deployment_lifecycle_mismatch':
return 'The Runtime Host launch path cannot honor the configured lifecycle';
case 'deployment_launch_mismatch':
return 'The Runtime Host process was not launched from the configured exact package';
case 'deployment_record_invalid':
return 'The Runtime Host managed deployment record is invalid';
case 'deployment_transition_in_progress':
return 'The Runtime Host managed deployment is changing and cannot be launched';
case 'deployment_needs_repair':
return 'The Runtime Host managed deployment requires repair before it can be launched';
}
}
async function matchesManagedProcessLaunch(
config: RuntimeHostManagedDeploymentConfig,
request: RuntimeHostManagedLaunchRequest,
): Promise<boolean> {
const layout = resolveRuntimeHostNpmDeploymentLayout(
config.deploymentRoot,
config.launch.package.integrity,
);
const expectedEntrypoint =
request.lifecycleMode === 'on_demand' ? layout.candidateEntrypoint : layout.cliPath;
const [actualExecutable, expectedExecutable, actualEntrypoint, canonicalExpectedEntrypoint] =
await Promise.all([
canonicalLaunchPath(request.processLaunch.executablePath),
canonicalLaunchPath(config.launch.nodePath),
canonicalLaunchPath(request.processLaunch.entrypointPath),
canonicalLaunchPath(expectedEntrypoint),
]);
return (
actualExecutable !== undefined &&
actualExecutable === expectedExecutable &&
actualEntrypoint !== undefined &&
actualEntrypoint === canonicalExpectedEntrypoint
);
}
async function canonicalLaunchPath(path: string): Promise<string | undefined> {
try {
return await realpath(path);
} catch (error) {
if (
isNodeError(error, 'ENOENT') ||
isNodeError(error, 'ENOTDIR') ||
isNodeError(error, 'ELOOP')
) {
return undefined;
}
throw deploymentIo('Unable to verify the Runtime Host managed launch path', error);
}
}
function assertConfigTargetsCapability(
config: RuntimeHostManagedDeploymentConfig,
capability: StorageRootCapability<'interactive'>,
): void {
if (
config.root.id !== capability.rootId ||
resolve(config.root.path) !== capability.canonicalPath
) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host managed deployment targets a different State Root',
);
}
}
function assertAuthorityTargetsCapability(
record: RuntimeHostManagedDeploymentAuthorityRecord,
capability: StorageRootCapability<'interactive'>,
): void {
if (
record.root.id !== capability.rootId ||
resolve(record.root.path) !== capability.canonicalPath
) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host managed deployment authority targets a different State Root',
);
}
}
async function readBoundedJson(path: string): Promise<unknown | undefined> {
let document: Buffer;
try {
document = await readStableBoundedFile({
path,
maxBytes: MAX_DOCUMENT_BYTES,
invalidFile: () =>
new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host managed deployment record must be one stable bounded regular file',
),
});
} catch (error) {
if (isNodeError(error, 'ENOENT')) return undefined;
if (error instanceof RuntimeHostManagedDeploymentError) throw error;
throw deploymentIo('Unable to inspect the Runtime Host managed deployment record', error);
}
let contents: string;
try {
contents = new TextDecoder('utf-8', { fatal: true }).decode(document);
} catch (error) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host managed deployment record is not valid UTF-8',
{ cause: error },
);
}
try {
return JSON.parse(contents) as unknown;
} catch (error) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host managed deployment record is not valid JSON',
{ cause: error },
);
}
}
async function writePrivateJson(
path: string,
value: unknown,
beforeDirectorySync?: (path: string) => void | Promise<void>,
): Promise<void> {
const contents = JSON.stringify(value, null, 2) + '\n';
if (Buffer.byteLength(contents, 'utf8') > MAX_DOCUMENT_BYTES) {
throw new RuntimeHostManagedDeploymentError(
'deployment_io_failed',
'The Runtime Host managed deployment record exceeds its size limit',
);
}
const temporaryPath = path + '.' + process.pid + '.' + randomUUID() + '.tmp';
let published = false;
try {
const handle = await open(temporaryPath, 'wx', 0o600);
try {
await handle.writeFile(contents, 'utf8');
await handle.sync();
} finally {
await handle.close();
}
if (process.platform !== 'win32') await chmod(temporaryPath, 0o600);
await rename(temporaryPath, path);
published = true;
await beforeDirectorySync?.(dirname(path));
await syncDirectory(dirname(path));
} catch (error) {
if (error instanceof RuntimeHostManagedDeploymentError) throw error;
if (published) {
throw new RuntimeHostManagedDeploymentError(
'deployment_commit_unknown',
'The Runtime Host managed deployment may have been persisted; re-read it before retrying',
{ cause: error },
);
}
throw deploymentIo('Unable to publish the Runtime Host managed deployment record', error);
} finally {
if (!published) await rm(temporaryPath, { force: true });
}
}
async function removePrivateJson(
path: string,
beforeDirectorySync?: (path: string) => void | Promise<void>,
): Promise<void> {
let removed = false;
try {
await rm(path);
removed = true;
await beforeDirectorySync?.(dirname(path));
await syncDirectory(dirname(path));
} catch (error) {
if (removed) {
throw new RuntimeHostManagedDeploymentError(
'deployment_commit_unknown',
'The Runtime Host managed deployment may have been removed; re-read it before retrying',
{ cause: error },
);
}
if (isNodeError(error, 'ENOENT')) return;
throw deploymentIo('Unable to remove the Runtime Host managed deployment record', error);
}
}
async function prepareAuthorityDirectory(
path: string,
durabilityBoundary: string,
options: RuntimeHostManagedDeploymentAuthorityOptions,
): Promise<void> {
try {
const boundaryMetadata = await lstat(durabilityBoundary);
if (!boundaryMetadata.isDirectory() || boundaryMetadata.isSymbolicLink()) {
throw new Error('Managed deployment durability boundary is not a directory');
}
await mkdir(path, { recursive: true, mode: 0o700 });
const metadata = await lstat(path);
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
throw new Error('Managed deployment authority path is not a directory');
}
if (process.platform !== 'win32') {
if (typeof process.getuid === 'function' && metadata.uid !== process.getuid()) {
throw new Error('Managed deployment authority path belongs to a different user');
}
await chmod(path, 0o700);
}
await syncDirectoryChain(path, durabilityBoundary, options.beforeDirectorySync);
} catch (error) {
throw deploymentIo('Unable to prepare the Runtime Host managed deployment authority', error);
}
}
function resolveAuthorityDurabilityBoundary(
authorityRoot: string,
options: RuntimeHostManagedDeploymentAuthorityOptions,
): string {
if (options.durabilityBoundary !== undefined && !isAbsolute(options.durabilityBoundary)) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host managed deployment durability boundary must be absolute',
);
}
const boundary = resolve(
options.durabilityBoundary ??
(options.authorityRoot === undefined
? (options.homeDir ?? userInfo().homedir)
: parse(authorityRoot).root),
);
const pathFromBoundary = relative(boundary, authorityRoot);
if (
pathFromBoundary === '..' ||
pathFromBoundary.startsWith(`..${sep}`) ||
isAbsolute(pathFromBoundary)
) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host managed deployment durability boundary must contain the authority root',
);
}
return boundary;
}
function requireRootId(rootId: string): void {
if (!ROOT_ID_PATTERN.test(rootId)) {
throw new RuntimeHostManagedDeploymentError(
'invalid_config',
'The Runtime Host State Root ID is invalid',
);
}
}
function deploymentIo(message: string, cause: unknown): RuntimeHostManagedDeploymentError {
return cause instanceof RuntimeHostManagedDeploymentError
? cause
: new RuntimeHostManagedDeploymentError('deployment_io_failed', message, {
cause,
});
}