blob: ad5564f8a89afb3e61e4c5f968fda69e185cad4f [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 { createHash, randomUUID } from 'node:crypto';
import { realpath, stat } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join, posix, resolve, win32 } from 'node:path';
import { isDeepStrictEqual } from 'node:util';
import {
claimLocalHostProcessDeployment,
handoffLocalHostProcessDeployment,
readLocalHostDeploymentRecord,
resolveRuntimeHostManagedDeploymentAuthority,
type LocalHostDeploymentAuthorityOptions,
type LocalHostProcessDeploymentClaimAdapter,
type LocalHostProcessDeploymentClaimResult,
type LocalHostProcessDeploymentHandoffAdapter,
type LocalHostProcessDeploymentHandoffResult,
type RuntimeHostInstallationOwner,
} from '@maka/runtime-host/operator';
import { compareProductReleaseVersions } from '@maka/runtime-host/operator/update-package-evidence';
import {
connectExistingRuntimeHost,
forceTerminateObservedRegisteredRuntimeHost,
type RuntimeHostProcessIdentity,
} from '@maka/runtime-host/client';
import {
INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
RUNTIME_HOST_PROTOCOL_VERSION,
type HostRegistration,
} from '@maka/runtime-host/protocol';
import {
resolveRuntimeHostNpmGlobalInstallation,
type RuntimeHostNpmGlobalInstallation,
} from './runtime-host-cli-installation.js';
import {
openRuntimeHostPackageDeployment,
prepareRuntimeHostPackageDeployment,
resolveRuntimeHostPackageCliPath,
type RuntimeHostPackageDeployment,
} from './runtime-host-package-deployment.js';
import {
RuntimeHostUpdatePackageError,
withRuntimeHostRegistryUpdatePackage,
} from './runtime-host-update-package.js';
import type { RuntimeHostUpdateCandidate } from './runtime-host-update-discovery.js';
import { resolveRuntimeHostRegistryUpdateCandidate } from './runtime-host-update-discovery.js';
import {
launchRuntimeHostTargetActivator,
type RuntimeHostTargetActivation,
type RuntimeHostTargetActivationInput,
} from './runtime-host-local-target-activation.js';
import { launchRuntimeHostLocalSourceRetirement } from './runtime-host-local-source-retirement.js';
const ROOT_ID = /^[a-f0-9]{64}$/u;
const CANDIDATE_RELATIVE_PATH = [
'node_modules',
'@maka',
'runtime-host',
'dist',
'execution-candidate-main.js',
] as const;
export interface RuntimeHostLocalStagedDeployment extends RuntimeHostPackageDeployment {
readonly candidateEntrypoint: string;
/** Transaction-scoped launch fence, not artifact identity or owner authority. */
readonly launchGeneration: string;
}
export class RuntimeHostLocalHandoffError extends Error {
constructor(
readonly code:
| 'installed_release_mismatch'
| 'root_changed'
| 'selected_target_observation_conflict'
| 'source_owner_mismatch'
| 'unsupported_downgrade',
message: string,
) {
super(message);
this.name = 'RuntimeHostLocalHandoffError';
}
}
export interface RuntimeHostLocalDeploymentPathOptions {
readonly homeDir?: string;
readonly platform?: NodeJS.Platform;
}
interface RuntimeHostLocalHandoffDeps {
readonly resolveInstallation: typeof resolveRuntimeHostNpmGlobalInstallation;
readonly withPackage: typeof withRuntimeHostRegistryUpdatePackage;
readonly prepareDeployment: typeof prepareRuntimeHostPackageDeployment;
readonly readRecord: typeof readLocalHostDeploymentRecord;
readonly claim: typeof claimLocalHostProcessDeployment;
readonly handoff: typeof handoffLocalHostProcessDeployment;
}
interface RuntimeHostLocalRestartDeps extends RuntimeHostLocalHandoffDeps {
readonly resolveManagedAuthority: typeof resolveRuntimeHostManagedDeploymentAuthority;
readonly resolveCandidate: typeof resolveRuntimeHostRegistryUpdateCandidate;
readonly connectExisting: typeof connectExistingRuntimeHost;
readonly activateTarget: (
input: RuntimeHostTargetActivationInput,
) => Promise<RuntimeHostTargetActivation>;
readonly retireSource: typeof launchRuntimeHostLocalSourceRetirement;
readonly terminateObservedHost: typeof forceTerminateObservedRegisteredRuntimeHost;
}
export type RuntimeHostLocalProcessLifecycleAdapter = Omit<
LocalHostProcessDeploymentHandoffAdapter<RuntimeHostLocalStagedDeployment>,
'stageTarget'
> &
Pick<
LocalHostProcessDeploymentClaimAdapter<RuntimeHostLocalStagedDeployment>,
'prepareUnownedHostCutover'
>;
export interface RuntimeHostNpmGlobalReconciliationRequest {
readonly rootId: string;
readonly transactionId: string;
readonly target: RuntimeHostUpdateCandidate;
readonly activeWorkPolicy: 'refuse_active_work' | 'interrupt_active_work';
readonly installationOptions?: Parameters<typeof resolveRuntimeHostNpmGlobalInstallation>[0];
readonly deploymentPathOptions?: RuntimeHostLocalDeploymentPathOptions;
}
export type RuntimeHostNpmGlobalRestartResult =
| { readonly kind: 'changed' }
| LocalHostProcessDeploymentClaimResult
| LocalHostProcessDeploymentHandoffResult
| {
readonly kind: 'operator_required';
readonly reason: 'service_host' | 'unowned_host';
};
/**
* Explicitly restarts one local ephemeral Host from the exact artifact matching
* the installed npm-global CLI. A durable selected source may retire its exact
* Host through its own compatible client; released Hosts without that helper
* retain only the bounded true-idle takeover adapter.
*/
export async function restartRuntimeHostNpmGlobalDeployment(
input: {
readonly rootPath: string;
readonly registration: HostRegistration;
readonly processIdentity?: RuntimeHostProcessIdentity;
readonly installationOptions?: Parameters<typeof resolveRuntimeHostNpmGlobalInstallation>[0];
readonly deploymentPathOptions?: RuntimeHostLocalDeploymentPathOptions;
readonly activeWorkPolicy?: 'refuse_active_work' | 'interrupt_active_work';
readonly signal?: AbortSignal;
readonly expectedInstallation?: RuntimeHostNpmGlobalInstallation;
},
authorityOptions: LocalHostDeploymentAuthorityOptions = {},
overrides: Partial<RuntimeHostLocalRestartDeps> = {},
): Promise<RuntimeHostNpmGlobalRestartResult> {
input.signal?.throwIfAborted();
if (input.registration.lifecycleMode !== 'ephemeral') {
return {
kind: 'operator_required',
reason: input.registration.lifecycleMode === 'service' ? 'service_host' : 'unowned_host',
};
}
const deps: RuntimeHostLocalRestartDeps = {
resolveInstallation: resolveRuntimeHostNpmGlobalInstallation,
withPackage: withRuntimeHostRegistryUpdatePackage,
prepareDeployment: prepareRuntimeHostPackageDeployment,
readRecord: readLocalHostDeploymentRecord,
resolveManagedAuthority: resolveRuntimeHostManagedDeploymentAuthority,
claim: claimLocalHostProcessDeployment,
handoff: handoffLocalHostProcessDeployment,
resolveCandidate: resolveRuntimeHostRegistryUpdateCandidate,
connectExisting: connectExistingRuntimeHost,
activateTarget: launchRuntimeHostTargetActivator,
retireSource: launchRuntimeHostLocalSourceRetirement,
terminateObservedHost: forceTerminateObservedRegisteredRuntimeHost,
...overrides,
};
if (await deps.resolveManagedAuthority(input.registration.rootId)) {
return { kind: 'operator_required', reason: 'unowned_host' };
}
const installation = await deps.resolveInstallation(input.installationOptions);
if (input.expectedInstallation && !isDeepStrictEqual(installation, input.expectedInstallation)) {
return { kind: 'changed' };
}
const current = await deps.readRecord(input.registration.rootId, authorityOptions);
const sourceOwner = current?.state.kind === 'handoff' ? current.state.from : current?.state.owner;
if (sourceOwner && !sameOwner(sourceOwner, installation.owner)) {
throw new RuntimeHostLocalHandoffError(
'source_owner_mismatch',
'External npm replacement can reconcile only the same durable npm-global installation owner',
);
}
const target = await deps.resolveCandidate({
kind: 'exact',
version: installation.observedRelease.version,
});
if (
current?.state.kind === 'owned' &&
current.state.owner.kind === installation.owner.kind &&
current.state.owner.installationId === installation.owner.installationId &&
current.state.selected.kind === target.kind &&
current.state.selected.version === target.version &&
current.state.selected.integrity === target.integrity
) {
throw new RuntimeHostLocalHandoffError(
'selected_target_observation_conflict',
'The active Runtime Host conflicts with the deployment already committed for this installation',
);
}
if (
current &&
compareProductReleaseVersions(target.version, current.state.selected.version) < 0
) {
throw new RuntimeHostLocalHandoffError(
'unsupported_downgrade',
`Downgrading the local Runtime Host from ${current.state.selected.version} to ${target.version} is not supported`,
);
}
const transactionId = restartTransactionId(input.registration.rootId, installation.owner, target);
const activeWorkPolicy = input.activeWorkPolicy ?? 'refuse_active_work';
const staged = await stageRuntimeHostNpmGlobalDeploymentTarget(
{
rootId: input.registration.rootId,
owner: installation.owner,
target,
transactionId,
},
input.deploymentPathOptions,
deps,
);
const selected = current?.state.selected;
const sourceIdentity = selected && !sameDeployment(selected, target) ? selected : undefined;
const source = sourceIdentity
? await openRuntimeHostNpmGlobalStagedDeployment(
{
rootId: input.registration.rootId,
owner: installation.owner,
target: sourceIdentity,
transactionId: `${transactionId}:source`,
},
input.deploymentPathOptions,
)
: undefined;
const sourceSupportsRetirement = source ? await hasSourceRetirementHelper(source) : false;
let targetActivator: RuntimeHostTargetActivation | undefined;
const activateExactTarget = async (
rootId: string,
stagedTarget: RuntimeHostLocalStagedDeployment,
inheritableAuthorityLeaseFd: number,
takeoverHostEpoch?: string,
): Promise<'target_present' | 'active_work' | 'operator_required'> => {
const activated = await deps.activateTarget({
rootPath: input.rootPath,
rootId,
staged: stagedTarget,
ownerInstallationId: installation.owner.installationId,
target,
inheritableAuthorityLeaseFd,
...(takeoverHostEpoch ? { takeoverHostEpoch } : {}),
});
if (activated.kind !== 'ready') return activated.kind;
targetActivator = activated;
return 'target_present';
};
const prepare = async (
rootId: string,
selectedDeployment: RuntimeHostUpdateCandidate | undefined,
stagedTarget: RuntimeHostLocalStagedDeployment,
inheritableAuthorityLeaseFd: number,
): Promise<{ readonly kind: 'target_present' | 'active_work' }> => {
input.signal?.throwIfAborted();
if (rootId !== input.registration.rootId) {
throw new RuntimeHostLocalHandoffError(
'root_changed',
'The local Runtime Host State Root changed before restart',
);
}
if (await deps.resolveManagedAuthority(rootId)) {
throw new Error('The managed Runtime Host requires its operator to perform replacement');
}
const observed = await deps.connectExisting({
rootPath: input.rootPath,
protocol: { min: RUNTIME_HOST_PROTOCOL_VERSION, max: RUNTIME_HOST_PROTOCOL_VERSION },
compositionId: INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
clientInstanceId: randomUUID(),
});
const registration = observed.registration;
if (registration?.rootId !== undefined && registration.rootId !== rootId) {
if (observed.kind === 'connected') await observed.connection.close().catch(() => undefined);
throw new RuntimeHostLocalHandoffError(
'root_changed',
'The local Runtime Host State Root changed during restart',
);
}
if (observed.kind === 'connected') await observed.connection.close();
if (!registration) {
const activated = await activateExactTarget(
rootId,
stagedTarget,
inheritableAuthorityLeaseFd,
);
if (activated !== 'target_present') {
throw new Error('The exact target could not start after the source Host disappeared');
}
return { kind: 'target_present' };
}
if (registration.lifecycleMode !== 'ephemeral') {
throw new Error('The observed Runtime Host requires its operator to perform replacement');
}
if (registration.generation === stagedTarget.launchGeneration) {
const activated = await activateExactTarget(
rootId,
stagedTarget,
inheritableAuthorityLeaseFd,
);
if (activated !== 'target_present') {
throw new Error('The exact staged Runtime Host could not be reattached for recovery');
}
return { kind: 'target_present' };
}
if (registration.hostEpoch !== input.registration.hostEpoch) {
throw new Error('The observed Runtime Host changed after restart confirmation');
}
if (
source &&
sourceSupportsRetirement &&
selectedDeployment &&
sourceIdentity &&
sameDeployment(selectedDeployment, sourceIdentity)
) {
const retired = await deps.retireSource({
sourceCliPath: source.cliPath,
rootPath: input.rootPath,
expectedRootId: rootId,
expectedHostEpoch: registration.hostEpoch,
activeWorkPolicy,
inheritableAuthorityLeaseFd,
...(input.signal ? { signal: input.signal } : {}),
});
if (retired === 'active_work') return { kind: 'active_work' };
if (retired === 'operator_required') {
throw new Error('The observed Runtime Host requires its operator to perform replacement');
}
const activated = await activateExactTarget(
rootId,
stagedTarget,
inheritableAuthorityLeaseFd,
registration.hostEpoch,
);
if (activated !== 'target_present') {
throw new Error('The exact target did not activate after source retirement began');
}
return { kind: 'target_present' };
}
// Older source packages cannot negotiate retirement. Explicit consent may
// use Desktop's exact-process recovery, inside this existing deployment
// transaction and only after staging the verified successor.
if (activeWorkPolicy === 'interrupt_active_work' && input.processIdentity) {
input.signal?.throwIfAborted();
const stopped = await deps.terminateObservedHost(
{ rootPath: input.rootPath, registration: input.registration },
{ processIdentity: input.processIdentity, isCurrent: () => !input.signal?.aborted },
);
if (!stopped)
throw new Error('The observed Runtime Host changed before it could be stopped safely');
}
const activated = await activateExactTarget(
rootId,
stagedTarget,
inheritableAuthorityLeaseFd,
registration.hostEpoch,
);
if (activated === 'active_work') return { kind: 'active_work' };
if (activated === 'operator_required') {
throw new Error('The observed Runtime Host requires its operator to perform replacement');
}
return { kind: 'target_present' };
};
const unreachable = async (): Promise<never> => {
throw new Error('Local restart must converge through its exact target activator');
};
let result:
| LocalHostProcessDeploymentClaimResult
| LocalHostProcessDeploymentHandoffResult
| undefined;
try {
result = await reconcilePreparedRuntimeHostNpmGlobalDeployment(
{
rootId: input.registration.rootId,
transactionId,
target,
activeWorkPolicy,
installation,
staged,
...(input.deploymentPathOptions
? { deploymentPathOptions: input.deploymentPathOptions }
: {}),
},
{
prepareUnownedHostCutover: (rootId, _target, stagedTarget, _policy, leaseFd) =>
prepare(rootId, undefined, stagedTarget, leaseFd),
prepareHostCutover: (rootId, selectedDeployment, _target, stagedTarget, _policy, leaseFd) =>
prepare(rootId, selectedDeployment, stagedTarget, leaseFd),
observeWriterRelease: unreachable,
activateTarget: unreachable,
async verifyTargetReady() {
if (targetActivator?.kind !== 'ready') {
throw new Error('Exact restarted Runtime Host Ready evidence is unavailable');
}
},
async finalizeTarget() {
const observedInstallation = await deps.resolveInstallation(input.installationOptions);
if (
observedInstallation.owner.installationId !== installation.owner.installationId ||
observedInstallation.observedRelease.version !== target.version
) {
throw new RuntimeHostLocalHandoffError(
'installed_release_mismatch',
'The installed Maka release changed again before local Host ownership committed',
);
}
},
},
authorityOptions,
deps,
);
return result;
} finally {
if (targetActivator) {
const settlement = targetActivator.settle();
if (result?.kind === 'completed') await settlement;
else await settlement.catch(() => undefined);
}
}
}
/**
* Resolves the persistent npm-global owner, stages the exact registry target,
* and delegates the only authority mutation to the shared local handoff.
* Lifecycle policy and process control stay in the caller-provided adapter.
*/
export async function reconcileRuntimeHostNpmGlobalDeployment(
request: RuntimeHostNpmGlobalReconciliationRequest,
lifecycle: RuntimeHostLocalProcessLifecycleAdapter,
authorityOptions: LocalHostDeploymentAuthorityOptions = {},
overrides: Partial<RuntimeHostLocalHandoffDeps> = {},
): Promise<LocalHostProcessDeploymentClaimResult | LocalHostProcessDeploymentHandoffResult> {
const deps: RuntimeHostLocalHandoffDeps = {
resolveInstallation: resolveRuntimeHostNpmGlobalInstallation,
withPackage: withRuntimeHostRegistryUpdatePackage,
prepareDeployment: prepareRuntimeHostPackageDeployment,
readRecord: readLocalHostDeploymentRecord,
claim: claimLocalHostProcessDeployment,
handoff: handoffLocalHostProcessDeployment,
...overrides,
};
const installation = await deps.resolveInstallation(request.installationOptions);
if (installation.observedRelease.version !== request.target.version) {
throw new RuntimeHostLocalHandoffError(
'installed_release_mismatch',
`The installed Maka release changed from ${request.target.version} to ${installation.observedRelease.version} before local Host reconciliation`,
);
}
const staged = await stageRuntimeHostNpmGlobalDeploymentTarget(
{
rootId: request.rootId,
owner: installation.owner,
target: request.target,
transactionId: request.transactionId,
},
request.deploymentPathOptions,
deps,
);
return reconcilePreparedRuntimeHostNpmGlobalDeployment(
{ ...request, installation, staged },
lifecycle,
authorityOptions,
deps,
);
}
export async function reconcilePreparedRuntimeHostNpmGlobalDeployment(
request: RuntimeHostNpmGlobalReconciliationRequest & {
readonly installation: RuntimeHostNpmGlobalInstallation;
readonly staged: RuntimeHostLocalStagedDeployment;
/** Captured owner observation, not permission to follow a later owner. */
readonly expectedOwner?: {
readonly revision: string;
readonly owner: RuntimeHostInstallationOwner;
};
},
lifecycle: RuntimeHostLocalProcessLifecycleAdapter,
authorityOptions: LocalHostDeploymentAuthorityOptions = {},
overrides: Pick<RuntimeHostLocalHandoffDeps, 'readRecord' | 'claim' | 'handoff'> = {
readRecord: readLocalHostDeploymentRecord,
claim: claimLocalHostProcessDeployment,
handoff: handoffLocalHostProcessDeployment,
},
): Promise<LocalHostProcessDeploymentClaimResult | LocalHostProcessDeploymentHandoffResult> {
const stageTarget = async (target: RuntimeHostUpdateCandidate, transactionId: string) => {
if (
transactionId !== request.transactionId ||
target.kind !== request.target.kind ||
target.version !== request.target.version ||
target.integrity !== request.target.integrity
) {
throw new RuntimeHostLocalHandoffError(
'selected_target_observation_conflict',
'The prepared local Host target does not match its owner transaction',
);
}
return request.staged;
};
const current = await overrides.readRecord(request.rootId, authorityOptions);
if (
request.expectedOwner &&
(current?.state.kind !== 'owned' ||
current.revision !== request.expectedOwner.revision ||
!sameOwner(current.state.owner, request.expectedOwner.owner))
) {
throw new RuntimeHostLocalHandoffError(
'source_owner_mismatch',
'The observed local Host owner changed before update reconciliation',
);
}
if (!current) {
return overrides.claim(
{
rootId: request.rootId,
transactionId: request.transactionId,
owner: request.installation.owner,
target: request.target,
activeWorkPolicy: request.activeWorkPolicy,
},
{ ...lifecycle, stageTarget },
authorityOptions,
);
}
return overrides.handoff(
{
rootId: request.rootId,
// The same captured CAS token is checked again by begin_handoff under
// the authority lock. Never substitute a newly observed owner revision.
expectedRevision: request.expectedOwner?.revision ?? current.revision,
transactionId:
current.state.kind === 'handoff' ? current.state.transactionId : request.transactionId,
from:
request.expectedOwner?.owner ??
(current.state.kind === 'handoff' ? current.state.from : current.state.owner),
to: request.installation.owner,
target: request.target,
activeWorkPolicy: request.activeWorkPolicy,
},
{
...lifecycle,
stageTarget,
},
authorityOptions,
);
}
export async function stageRuntimeHostNpmGlobalDeploymentTarget(
input: {
readonly rootId: string;
readonly owner: RuntimeHostInstallationOwner & { readonly kind: 'cli' };
readonly target: RuntimeHostUpdateCandidate;
readonly transactionId: string;
},
pathOptions: RuntimeHostLocalDeploymentPathOptions = {},
overrides: Pick<RuntimeHostLocalHandoffDeps, 'withPackage' | 'prepareDeployment'> = {
withPackage: withRuntimeHostRegistryUpdatePackage,
prepareDeployment: prepareRuntimeHostPackageDeployment,
},
): Promise<RuntimeHostLocalStagedDeployment> {
return overrides.withPackage(input.target, async (sourcePackageRoot) => {
return prepareRuntimeHostNpmGlobalStagedDeployment(
{ ...input, sourcePackageRoot },
pathOptions,
overrides.prepareDeployment,
);
});
}
export async function prepareRuntimeHostNpmGlobalStagedDeployment(
input: {
readonly rootId: string;
readonly owner: RuntimeHostInstallationOwner & { readonly kind: 'cli' };
readonly target: RuntimeHostUpdateCandidate;
readonly transactionId: string;
readonly sourcePackageRoot: string;
},
pathOptions: RuntimeHostLocalDeploymentPathOptions = {},
prepareDeployment: typeof prepareRuntimeHostPackageDeployment = prepareRuntimeHostPackageDeployment,
): Promise<RuntimeHostLocalStagedDeployment> {
const deploymentRoot = resolveRuntimeHostLocalCliDeploymentRoot(
input.rootId,
input.owner,
pathOptions,
);
const staged = await prepareDeployment({
deploymentRoot,
sourcePackageRoot: input.sourcePackageRoot,
version: input.target.version,
packageIntegrity: input.target.integrity,
});
const candidateEntrypoint = await requireCandidateEntrypoint(staged.packageRoot);
return {
...staged,
candidateEntrypoint,
launchGeneration: launchGeneration(input.transactionId, input.target),
};
}
export async function openRuntimeHostNpmGlobalStagedDeployment(
input: {
readonly rootId: string;
readonly owner: RuntimeHostInstallationOwner & { readonly kind: 'cli' };
readonly target: RuntimeHostUpdateCandidate;
readonly transactionId: string;
},
pathOptions: RuntimeHostLocalDeploymentPathOptions = {},
): Promise<RuntimeHostLocalStagedDeployment> {
const deploymentRoot = resolveRuntimeHostLocalCliDeploymentRoot(
input.rootId,
input.owner,
pathOptions,
);
const staged = await openRuntimeHostPackageDeployment({
deploymentRoot,
cliPath: resolveRuntimeHostPackageCliPath(
deploymentRoot,
input.target.version,
input.target.integrity,
),
version: input.target.version,
});
const candidateEntrypoint = await requireCandidateEntrypoint(staged.packageRoot);
return {
...staged,
candidateEntrypoint,
launchGeneration: launchGeneration(input.transactionId, input.target),
};
}
export function resolveRuntimeHostLocalCliDeploymentRoot(
rootId: string,
owner: RuntimeHostInstallationOwner & { readonly kind: 'cli' },
options: RuntimeHostLocalDeploymentPathOptions = {},
): string {
if (!ROOT_ID.test(rootId) || owner.kind !== 'cli' || owner.installationId.length === 0) {
throw new TypeError('Invalid local Runtime Host CLI deployment identity');
}
const platform = options.platform ?? process.platform;
const path = platform === 'win32' ? win32 : posix;
const accountHome = path.normalize(options.homeDir ?? homedir());
if (!path.isAbsolute(accountHome)) {
throw new TypeError('The OS account home must be absolute');
}
const dataRoot =
platform === 'darwin'
? path.join(accountHome, 'Library', 'Application Support')
: platform === 'win32'
? path.join(accountHome, 'AppData', 'Local')
: path.join(accountHome, '.local', 'share');
const ownerKey = createHash('sha256').update(owner.installationId).digest('hex');
return path.join(dataRoot, 'Maka', 'runtime-host-deployments', 'cli', ownerKey, rootId);
}
async function requireCandidateEntrypoint(packageRoot: string): Promise<string> {
const requested = join(packageRoot, ...CANDIDATE_RELATIVE_PATH);
let candidate: string;
try {
candidate = await realpath(requested);
if (!(await stat(candidate)).isFile()) throw new Error('Not a file');
} catch (cause) {
throw invalidStagedPackage('The staged Maka package has no Runtime Host candidate', cause);
}
if (candidate !== resolve(requested)) {
throw invalidStagedPackage('The staged Runtime Host candidate is redirected');
}
return candidate;
}
/** Read-only capability check against the same verified source package the transaction uses. */
export async function runtimeHostNpmGlobalSourceRetirementAvailable(input: {
readonly rootId: string;
readonly owner: RuntimeHostNpmGlobalInstallation['owner'];
readonly source: RuntimeHostUpdateCandidate;
readonly deploymentPathOptions?: RuntimeHostLocalDeploymentPathOptions;
}): Promise<boolean> {
const source = await openRuntimeHostNpmGlobalStagedDeployment(
{
rootId: input.rootId,
owner: input.owner,
target: input.source,
transactionId: 'source-capability-probe',
},
input.deploymentPathOptions,
);
return hasSourceRetirementHelper(source);
}
async function hasSourceRetirementHelper(
source: RuntimeHostLocalStagedDeployment,
): Promise<boolean> {
try {
return (
await stat(join(dirname(source.cliPath), 'runtime-host-local-source-retirement.js'))
).isFile();
} catch (error) {
if (isNodeError(error, 'ENOENT')) return false;
throw error;
}
}
function sameDeployment(
left: RuntimeHostUpdateCandidate,
right: RuntimeHostUpdateCandidate,
): boolean {
return (
left.kind === right.kind && left.version === right.version && left.integrity === right.integrity
);
}
function sameOwner(
left: RuntimeHostInstallationOwner,
right: RuntimeHostInstallationOwner,
): boolean {
return left.kind === right.kind && left.installationId === right.installationId;
}
function launchGeneration(transactionId: string, target: RuntimeHostUpdateCandidate): string {
return `npm-global-handoff:${createHash('sha256')
.update(transactionId)
.update('\0')
.update(target.version)
.update('\0')
.update(target.integrity)
.digest('hex')}`;
}
function restartTransactionId(
rootId: string,
owner: RuntimeHostInstallationOwner,
target: RuntimeHostUpdateCandidate,
): string {
return `npm-global-restart:${createHash('sha256')
.update(rootId)
.update('\0')
.update(owner.kind)
.update('\0')
.update(owner.installationId)
.update('\0')
.update(target.version)
.update('\0')
.update(target.integrity)
.digest('hex')}`;
}
function invalidStagedPackage(message: string, cause?: unknown): RuntimeHostUpdatePackageError {
return new RuntimeHostUpdatePackageError('invalid_package', message, { cause });
}
function isNodeError(error: unknown, code: string): error is NodeJS.ErrnoException {
return error instanceof Error && 'code' in error && error.code === code;
}