993 lines
36 KiB
TypeScript
Raw Permalink Normal View History

import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { randomBytes } from 'node:crypto';
type JsonRecord = Record<string, unknown>;
export interface IHiveCastNatsConfig {
readonly url: string;
readonly wsUrl?: string;
readonly leafnodeUrl?: string;
}
export interface IHiveCastNatsCredentials {
readonly accountSeed?: string;
readonly jwt?: string;
readonly seed?: string;
}
export interface IHiveCastHostLinkRecord {
readonly version: 1;
readonly cloudUrl: string;
readonly deviceRegistryRoot?: string;
readonly hostLinkId?: string;
readonly hostId?: string;
readonly principalId?: string;
readonly hostName?: string;
readonly deviceSlug?: string;
readonly authorityCoordinator?: boolean;
readonly spaceId?: string;
readonly routeKey?: string;
readonly publicNamespace?: string;
readonly authorityRoot: string;
readonly linkedAt: string;
readonly credentials: {
readonly hostLinkTokenRef?: string;
readonly registryTokenRef?: string;
readonly natsCredentialRef: string;
readonly natsLeafnodeCredentialRef?: string;
};
}
interface ILocalOwnerTransportSnapshot {
readonly root: string;
readonly authorityRoot?: string;
readonly nats: JsonRecord;
}
export interface IHiveCastLocalOwnerTransport {
readonly root: string;
readonly authorityRoot?: string;
}
interface ILinkedAuthorityTransportSnapshot {
readonly authorityRoot: string;
readonly nats: JsonRecord;
readonly leafnode?: {
readonly url: string;
readonly credentialsRef: string;
};
readonly authorityCoordinator?: boolean;
readonly routeKey?: string;
readonly publicNamespace?: string;
readonly spaceId?: string;
}
export interface IHiveCastLinkPaths {
readonly matrixHome: string;
readonly credentialsDir: string;
readonly installIdentityPath: string;
readonly linkPath: string;
readonly natsCredentialsPath: string;
readonly natsLeafnodeCredentialsPath: string;
readonly hostLinkTokenPath: string;
readonly hostCredentialsPath: string;
readonly hostConfigPath: string;
}
export interface IHiveCastInstallIdentity {
readonly version: 1;
readonly installId: string;
readonly hostName?: string;
readonly createdAt: string;
readonly updatedAt: string;
}
export interface ISaveHiveCastHostLinkOptions {
readonly cwd: string;
readonly matrixHome?: string;
readonly cloudUrl: string;
readonly deviceRegistryRoot?: string;
readonly authorityRoot: string;
readonly nats: IHiveCastNatsConfig;
readonly natsCredentials: IHiveCastNatsCredentials;
readonly hostLinkToken?: string;
readonly hostLinkId?: string;
readonly hostId?: string;
readonly principalId?: string;
readonly hostName?: string;
readonly deviceSlug?: string;
readonly authorityCoordinator?: boolean;
readonly spaceId?: string;
readonly routeKey?: string;
readonly publicNamespace?: string;
readonly linkedAt?: string;
}
export interface IHiveCastLinkStatus {
readonly linked: boolean;
readonly matrixHome: string;
readonly linkPath: string;
readonly natsCredentialsPath: string;
readonly hostConfigPath: string;
readonly natsCredentialsPresent: boolean;
readonly hostConfigPresent: boolean;
readonly hostConfigLinked: boolean;
readonly hostLinkId?: string;
readonly authorityRoot?: string;
readonly routeKey?: string;
readonly publicNamespace?: string;
readonly spaceId?: string;
readonly authorityCoordinator?: boolean;
readonly cloudUrl?: string;
readonly linkedAt?: string;
}
export interface IRemoveHiveCastHostLinkResult {
readonly removed: boolean;
readonly hostConfigReset: boolean;
readonly paths: IHiveCastLinkPaths;
}
export interface IRemoveHiveCastHostLinkOptions {
readonly preserveCredentials?: boolean;
}
const DEFAULT_MATRIX_HOME = path.join(os.homedir(), '.matrix');
const HIVECAST_NATS_JSON_CREDENTIAL_REF = 'file:credentials/hivecast-nats.json';
const HIVECAST_NATS_LEAFNODE_CREDENTIAL_REF = 'file:credentials/hivecast-nats.creds';
const HIVECAST_DEFAULT_RUNTIME_KEYS = [
'SYSTEM',
'GATEWAY',
'INFERENCE',
'AGENTS',
'WEB',
'EDGE',
'DIRECTOR',
'CHAT',
'INFERENCE-SETTINGS',
'FLOWPAD',
'SMITHERS',
'HOST-CONTROL',
'HOST-OPS',
] as const;
function asRecord(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function optionalString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
}
function expandHome(value: string): string {
if (value === '~') {
return os.homedir();
}
return value.startsWith('~/') ? path.join(os.homedir(), value.slice(2)) : value;
}
export function resolveHiveCastMatrixHome(cwd: string, matrixHome?: string): string {
const raw = matrixHome
?? process.env.MATRIX_HOST_HOME
?? process.env.MATRIX_HOME
?? DEFAULT_MATRIX_HOME;
const expanded = expandHome(raw);
return path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(cwd, expanded);
}
export function hiveCastLinkPaths(cwd: string, matrixHome?: string): IHiveCastLinkPaths {
const resolvedHome = resolveHiveCastMatrixHome(cwd, matrixHome);
const credentialsDir = path.join(resolvedHome, 'credentials');
return {
matrixHome: resolvedHome,
credentialsDir,
installIdentityPath: path.join(credentialsDir, 'hivecast-install.json'),
linkPath: path.join(credentialsDir, 'hivecast-link.json'),
natsCredentialsPath: path.join(credentialsDir, 'hivecast-nats.json'),
natsLeafnodeCredentialsPath: path.join(credentialsDir, 'hivecast-nats.creds'),
hostLinkTokenPath: path.join(credentialsDir, 'hivecast-host-link-token'),
hostCredentialsPath: path.join(resolvedHome, 'credentials.json'),
hostConfigPath: path.join(resolvedHome, 'host.json'),
};
}
export function ensureHiveCastInstallIdentity(options: {
readonly cwd: string;
readonly matrixHome?: string;
readonly hostName?: string;
}): { readonly identity: IHiveCastInstallIdentity; readonly paths: IHiveCastLinkPaths } {
const paths = hiveCastLinkPaths(options.cwd, options.matrixHome);
const existing = readJsonFile(paths.installIdentityPath);
const existingInstallId = optionalString(existing?.installId);
const now = new Date().toISOString();
const hostName = optionalString(options.hostName);
if (existingInstallId) {
const existingHostName = optionalString(existing?.hostName);
const nextHostName = hostName ?? existingHostName;
const identity: IHiveCastInstallIdentity = {
version: 1,
installId: existingInstallId,
...(nextHostName ? { hostName: nextHostName } : {}),
createdAt: optionalString(existing?.createdAt) ?? now,
updatedAt: hostName && hostName !== existingHostName ? now : (optionalString(existing?.updatedAt) ?? now),
};
if (hostName && hostName !== existingHostName) {
writePrivateJson(paths.installIdentityPath, identity);
}
return { identity, paths };
}
const identity: IHiveCastInstallIdentity = {
version: 1,
installId: `install_${randomBytes(10).toString('hex')}`,
...(hostName ? { hostName } : {}),
createdAt: now,
updatedAt: now,
};
writePrivateJson(paths.installIdentityPath, identity);
return { identity, paths };
}
function localOwnerRootToken(installId: string): string {
const token = installId
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
if (!token) {
throw new Error('HIVECAST_INSTALL_IDENTITY_INVALID: installId cannot produce a local owner root');
}
return token;
}
function localOwnerAuthorityRoot(matrixHome: string): string {
const { identity } = ensureHiveCastInstallIdentity({ cwd: process.cwd(), matrixHome });
return `local-${localOwnerRootToken(identity.installId)}`;
}
function runtimeScopeToken(installId: string): string {
const token = installId
.toUpperCase()
.replace(/[^A-Z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
if (!token) {
throw new Error('HIVECAST_INSTALL_IDENTITY_INVALID: installId cannot produce a runtime scope');
}
return token;
}
function installRuntimeScope(matrixHome: string): string {
const { identity } = ensureHiveCastInstallIdentity({ cwd: process.cwd(), matrixHome });
return runtimeScopeToken(identity.installId);
}
function writePrivateJson(filePath: string, payload: object): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
try {
fs.chmodSync(filePath, 0o600);
} catch {
}
}
function writePrivateText(filePath: string, payload: string): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, payload, 'utf8');
try {
fs.chmodSync(filePath, 0o600);
} catch {
}
}
function natsUserCredentialsFile(credentials: IHiveCastNatsCredentials): string | undefined {
const jwt = optionalString(credentials.jwt);
const seed = optionalString(credentials.seed);
if (!jwt || !seed) {
return undefined;
}
return [
'-----BEGIN NATS USER JWT-----',
jwt,
'------END NATS USER JWT------',
'',
'-----BEGIN USER NKEY SEED-----',
seed,
'------END USER NKEY SEED------',
'',
].join('\n');
}
function ownerForPath(filePath: string): { readonly uid: number; readonly gid: number } {
const stat = fs.statSync(filePath);
return { uid: stat.uid, gid: stat.gid };
}
function alignOwnership(filePath: string, owner: { readonly uid: number; readonly gid: number }): void {
if (typeof process.getuid !== 'function' || process.getuid() !== 0) {
return;
}
fs.chownSync(filePath, owner.uid, owner.gid);
}
function readJsonFile(filePath: string): JsonRecord | null {
if (!fs.existsSync(filePath)) {
return null;
}
try {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
return asRecord(parsed);
} catch {
return null;
}
}
function buildHostConfig(
existing: JsonRecord | null,
matrixHome: string,
localOwnerTransportOverride?: ILocalOwnerTransportSnapshot,
linkedAuthorityTransport?: ILinkedAuthorityTransportSnapshot,
): JsonRecord {
const currentHttp = asRecord(existing?.http);
const currentRuntimeStorage = asRecord(existing?.runtimeStorage);
const currentPackageStorage = asRecord(existing?.packageStorage);
const currentAuth = asRecord(existing?.auth);
const localOwnerTransport = readLocalOwnerTransportSnapshotForHome(matrixHome, existing)
?? localOwnerTransportOverride
?? captureLocalOwnerTransport(matrixHome, existing);
const generatedLocalOwnerRoot = localOwnerAuthorityRoot(matrixHome);
const generatedLocalOwnerTransport = {
root: generatedLocalOwnerRoot,
authorityRoot: generatedLocalOwnerRoot,
addressRoot: generatedLocalOwnerRoot,
nats: defaultLocalOwnerNatsTransport(),
};
const localOwnerHostTransport = localOwnerTransport
? localOwnerHostConfigTransport(localOwnerTransport)
: generatedLocalOwnerTransport;
const hostTransport = linkedAuthorityTransport
? linkedAuthorityTransport.authorityCoordinator === false
? linkedLeafHostConfigTransport(linkedAuthorityTransport, localOwnerHostTransport)
: linkedAuthorityHostConfigTransport(linkedAuthorityTransport)
: localOwnerHostTransport;
const nextConfig: JsonRecord = {
...(existing ?? {}),
kind: optionalString(existing?.kind) ?? 'MatrixHostConfig',
version: typeof existing?.version === 'number' ? existing.version : 1,
home: matrixHome,
...(localOwnerTransport ? { hivecastLocalOwnerTransport: localOwnerTransport } : {}),
http: currentHttp ?? {
host: '127.0.0.1',
port: 3100,
},
transport: hostTransport,
auth: currentAuth ?? {
mode: 'local-client',
},
runtimeStorage: currentRuntimeStorage ?? {
recordsDir: 'runtimes',
logsDir: 'logs/runtimes',
},
packageStorage: currentPackageStorage ?? {
globalDir: 'packages/global',
systemDir: 'packages/system',
},
};
if (linkedAuthorityTransport?.authorityCoordinator === false && linkedAuthorityTransport.leafnode) {
nextConfig.natsTopology = linkedLeafNatsTopology(hostTransport.nats as JsonRecord, linkedAuthorityTransport.leafnode);
} else {
delete nextConfig.natsTopology;
}
return nextConfig;
}
function linkedLeafNatsTopology(
localNats: JsonRecord,
leafnode: { readonly url: string; readonly credentialsRef: string },
): JsonRecord {
const clientUrl = optionalString(localNats.url);
const wsUrl = optionalString(localNats.wsUrl);
return {
mode: 'leafnode',
...(clientUrl ? { clientUrl } : {}),
...(wsUrl ? { wsUrl } : {}),
leafnodes: [{
url: leafnode.url,
credentialsRef: leafnode.credentialsRef,
}],
};
}
function assertLinkedHostConfigConverged(
record: IHiveCastHostLinkRecord,
hostConfig: JsonRecord,
): void {
const transport = asRecord(hostConfig.transport);
const root = optionalString(transport?.root);
const authorityRoot = optionalString(transport?.authorityRoot);
const addressRoot = optionalString(transport?.addressRoot);
for (const [field, actual] of [
['transport.root', root],
['transport.authorityRoot', authorityRoot],
['transport.addressRoot', addressRoot],
] as const) {
if (actual !== record.authorityRoot) {
throw new Error(
`HIVECAST_LINK_ROOT_CONVERGENCE_FAILED: expected host config ${field} ${record.authorityRoot}, got ${actual ?? '(missing)'}`,
);
}
}
}
function linkedAuthorityHostConfigTransport(snapshot: ILinkedAuthorityTransportSnapshot): JsonRecord {
return {
root: snapshot.authorityRoot,
authorityRoot: snapshot.authorityRoot,
addressRoot: snapshot.authorityRoot,
...(snapshot.spaceId ? { spaceId: snapshot.spaceId } : {}),
...(snapshot.routeKey ? { routeKey: snapshot.routeKey } : {}),
...(snapshot.publicNamespace ? { publicNamespace: snapshot.publicNamespace } : {}),
nats: { ...snapshot.nats },
};
}
function linkedLeafHostConfigTransport(
snapshot: ILinkedAuthorityTransportSnapshot,
localOwnerTransport: JsonRecord,
): JsonRecord {
return {
root: snapshot.authorityRoot,
authorityRoot: snapshot.authorityRoot,
addressRoot: snapshot.authorityRoot,
...(snapshot.spaceId ? { spaceId: snapshot.spaceId } : {}),
...(snapshot.routeKey ? { routeKey: snapshot.routeKey } : {}),
...(snapshot.publicNamespace ? { publicNamespace: snapshot.publicNamespace } : {}),
nats: localNatsTransportForLinkedLeaf(snapshot, localOwnerTransport),
};
}
function localNatsTransportForLinkedLeaf(
snapshot: ILinkedAuthorityTransportSnapshot,
localOwnerTransport: JsonRecord,
): JsonRecord {
const localNats = asRecord(localOwnerTransport.nats);
return stripNatsCredentialFields(localNats ?? snapshot.nats);
}
function defaultLocalOwnerNatsTransport(): JsonRecord {
return {
mode: 'external',
url: 'nats://127.0.0.1:4222',
wsUrl: 'ws://127.0.0.1:4223',
port: 4222,
wsPort: 4223,
dataDir: 'nats/host-default',
pidFile: 'nats/host-default/nats-server.pid',
};
}
function stripNatsCredentialFields(nats: JsonRecord): JsonRecord {
const {
credentialsRef: _credentialsRef,
credentials: _credentials,
natsCredentialRef: _natsCredentialRef,
...rest
} = nats;
return rest;
}
function linkedAuthorityNatsTransport(
nats: IHiveCastNatsConfig,
natsCredentialsPath: string,
): JsonRecord {
const url = optionalString(nats.url);
if (!url) {
throw new Error('HIVECAST_LINK_AUTHORITY_NATS_MISSING: linked Device setup requires nats.url from the setup exchange');
}
if (/^wss?:\/\//i.test(url)) {
throw new Error(
`HIVECAST_LINK_AUTHORITY_NATS_WS_ONLY: linked Device setup requires a server NATS URL for runtimes, got ${url}`,
);
}
const wsUrl = optionalString(nats.wsUrl);
return {
mode: 'external',
url,
...(wsUrl ? { wsUrl } : {}),
credentialsRef: `file:${natsCredentialsPath}`,
};
}
function localOwnerHostConfigTransport(snapshot: ILocalOwnerTransportSnapshot): JsonRecord {
return {
root: snapshot.root,
authorityRoot: snapshot.authorityRoot ?? snapshot.root,
addressRoot: snapshot.root,
nats: { ...snapshot.nats },
};
}
function captureLocalOwnerTransport(matrixHome: string, existing: JsonRecord | null): ILocalOwnerTransportSnapshot | undefined {
return readLocalOwnerTransportSnapshotFromTransportForHome(matrixHome, asRecord(existing?.transport));
}
function captureLocalOwnerTransportFromCurrentNats(matrixHome: string, existing: JsonRecord | null): ILocalOwnerTransportSnapshot | undefined {
const transport = asRecord(existing?.transport);
const nats = asRecord(transport?.nats);
if (!nats) return undefined;
return readLocalOwnerTransportSnapshotFromTransportForHome(matrixHome, {
root: localOwnerAuthorityRoot(matrixHome),
nats: { ...nats },
});
}
function readLocalOwnerTransportSnapshot(config: JsonRecord | null): ILocalOwnerTransportSnapshot | undefined {
const snapshot = asRecord(config?.hivecastLocalOwnerTransport);
if (!snapshot) return undefined;
const root = optionalString(snapshot.root);
const nats = asRecord(snapshot.nats);
if (!root || !nats) return undefined;
return readLocalOwnerTransportSnapshotFromTransport({
root,
...(optionalString(snapshot.authorityRoot) ? { authorityRoot: optionalString(snapshot.authorityRoot) } : {}),
nats: { ...nats },
});
}
function readLocalOwnerTransportSnapshotForHome(
matrixHome: string,
config: JsonRecord | null,
): ILocalOwnerTransportSnapshot | undefined {
const snapshot = readLocalOwnerTransportSnapshot(config);
if (snapshot) return snapshot;
const saved = asRecord(config?.hivecastLocalOwnerTransport);
if (!saved) return undefined;
return readLocalOwnerTransportSnapshotFromTransportForHome(matrixHome, saved);
}
function readLocalOwnerTransportSnapshotFromStatus(matrixHome: string): ILocalOwnerTransportSnapshot | undefined {
const status = readJsonFile(path.join(matrixHome, 'host.status.json'));
if (optionalString(status?.status) !== 'running') return undefined;
return readLocalOwnerTransportSnapshotFromTransportForHome(matrixHome, asRecord(status?.transport));
}
function readLocalOwnerTransportSnapshotFromRuntimeRecords(matrixHome: string): ILocalOwnerTransportSnapshot | undefined {
const runtimesDir = path.join(matrixHome, 'runtimes');
if (!fs.existsSync(runtimesDir)) return undefined;
const localScope = localDefaultRuntimeScope(matrixHome);
const preferredRuntimeIds = HIVECAST_DEFAULT_RUNTIME_KEYS.map((key) => `RUNTIME-HOST-${localScope}-${key}`);
for (const runtimeId of preferredRuntimeIds) {
const snapshot = readLocalOwnerTransportSnapshotFromRuntimeRecord(matrixHome, path.join(runtimesDir, runtimeId, 'runtime.json'));
if (snapshot) return snapshot;
}
for (const entry of fs.readdirSync(runtimesDir, { withFileTypes: true })) {
if (!entry.isDirectory() || !entry.name.startsWith(`RUNTIME-HOST-${localScope}-`)) continue;
const snapshot = readLocalOwnerTransportSnapshotFromRuntimeRecord(matrixHome, path.join(runtimesDir, entry.name, 'runtime.json'));
if (snapshot) return snapshot;
}
return undefined;
}
function readLocalOwnerTransportSnapshotFromRuntimeRecord(
matrixHome: string,
recordPath: string,
): ILocalOwnerTransportSnapshot | undefined {
const record = readJsonFile(recordPath);
const metadata = asRecord(record?.metadata);
return readLocalOwnerTransportSnapshotFromTransportForHome(matrixHome, asRecord(metadata?.transport));
}
function readLocalOwnerTransportSnapshotFromTransportForHome(
_matrixHome: string,
transport: JsonRecord | null,
): ILocalOwnerTransportSnapshot | undefined {
return readLocalOwnerTransportSnapshotFromTransport(transport);
}
function readLocalOwnerTransportSnapshotFromTransport(transport: JsonRecord | null): ILocalOwnerTransportSnapshot | undefined {
if (!transport) return undefined;
if (
optionalString(transport.routeKey)
|| optionalString(transport.publicNamespace)
|| optionalString(transport.spaceId)
) {
return undefined;
}
const root = optionalString(transport.root);
if (!root) return undefined;
const nats = asRecord(transport.nats);
if (!nats || optionalString(nats.credentialsRef)) return undefined;
const mode = optionalString(nats.mode);
if (mode !== 'embedded' && mode !== 'external') return undefined;
const natsPort = positivePort(nats.port) ?? loopbackUrlPort(optionalString(nats.url));
const natsWsPort = positivePort(nats.wsPort) ?? loopbackUrlPort(optionalString(nats.wsUrl));
const maxPayloadBytes = positiveInteger(nats.maxPayloadBytes);
if (!natsPort || !natsWsPort) return undefined;
const natsUrl = mode === 'external'
? optionalString(nats.url) ?? `nats://127.0.0.1:${natsPort}`
: undefined;
const natsWsUrl = mode === 'external'
? optionalString(nats.wsUrl) ?? `ws://127.0.0.1:${natsWsPort}`
: undefined;
const authorityRoot = optionalString(transport.authorityRoot);
return {
root,
...(authorityRoot && authorityRoot !== root ? { authorityRoot } : {}),
nats: {
mode,
...(natsUrl ? { url: natsUrl } : {}),
...(natsWsUrl ? { wsUrl: natsWsUrl } : {}),
port: natsPort,
wsPort: natsWsPort,
...(maxPayloadBytes ? { maxPayloadBytes } : {}),
dataDir: optionalString(nats.dataDir) ?? 'nats/host-default',
pidFile: optionalString(nats.pidFile) ?? 'nats/host-default/nats-server.pid',
...(optionalString(nats.binaryPath) ? { binaryPath: optionalString(nats.binaryPath) } : {}),
},
};
}
export function readHiveCastLocalOwnerTransport(
cwd: string,
matrixHome?: string,
): IHiveCastLocalOwnerTransport | null {
const paths = hiveCastLinkPaths(cwd, matrixHome);
const config = readJsonFile(paths.hostConfigPath);
const snapshot = readLocalOwnerTransportSnapshot(config)
?? readLocalOwnerTransportSnapshotForHome(paths.matrixHome, config)
?? captureLocalOwnerTransport(paths.matrixHome, config)
?? readLocalOwnerTransportSnapshotFromStatus(paths.matrixHome)
?? readLocalOwnerTransportSnapshotFromRuntimeRecords(paths.matrixHome);
if (!snapshot) return null;
return {
root: snapshot.root,
...(snapshot.authorityRoot ? { authorityRoot: snapshot.authorityRoot } : {}),
};
}
function positivePort(value: unknown): number | undefined {
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
return undefined;
}
return value;
}
function positiveInteger(value: unknown): number | undefined {
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
return undefined;
}
return value;
}
function loopbackUrlPort(raw: string | undefined): number | undefined {
if (!raw) return undefined;
try {
const parsed = new URL(raw);
if (!['127.0.0.1', 'localhost', '[::1]', '::1'].includes(parsed.hostname)) {
return undefined;
}
const port = Number.parseInt(parsed.port, 10);
return Number.isFinite(port) && port > 0 ? port : undefined;
} catch {
return undefined;
}
}
function localDefaultRuntimeScope(matrixHome: string): string {
return installRuntimeScope(matrixHome);
}
function activeDefaultRuntimeScope(matrixHome: string): string {
return localDefaultRuntimeScope(matrixHome);
}
function defaultRuntimeIdsForScope(scope: string): Set<string> {
return new Set(HIVECAST_DEFAULT_RUNTIME_KEYS.map((key) => `RUNTIME-HOST-${scope}-${key}`));
}
function isHiveCastDefaultRuntimeId(runtimeId: string): boolean {
return runtimeId.startsWith('RUNTIME-HOST-')
&& HIVECAST_DEFAULT_RUNTIME_KEYS.some((key) => runtimeId.endsWith(`-${key}`));
}
function updateHiveCastDefaultRuntimePolicies(matrixHome: string, activeScope: string): void {
const runtimesDir = path.join(matrixHome, 'runtimes');
if (!fs.existsSync(runtimesDir)) return;
const activeIds = defaultRuntimeIdsForScope(activeScope);
for (const entry of fs.readdirSync(runtimesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const runtimeId = entry.name;
if (!isHiveCastDefaultRuntimeId(runtimeId)) continue;
const recordPath = path.join(runtimesDir, runtimeId, 'runtime.json');
const record = readJsonFile(recordPath);
if (!record) continue;
const recordOwner = ownerForPath(recordPath);
const active = activeIds.has(runtimeId);
const nextRecord = {
...record,
startup: active ? 'auto' : 'manual',
restart: active ? 'always' : 'never',
updatedAt: new Date().toISOString(),
metadata: {
...(asRecord(record.metadata) ?? {}),
hivecastDefaultRuntimePolicy: active ? 'active' : 'inactive',
hivecastDefaultRuntimePolicyUpdatedAt: new Date().toISOString(),
},
};
writePrivateJson(recordPath, nextRecord);
alignOwnership(recordPath, recordOwner);
}
}
export function saveHiveCastHostLink(options: ISaveHiveCastHostLinkOptions): {
readonly record: IHiveCastHostLinkRecord;
readonly paths: IHiveCastLinkPaths;
} {
const paths = hiveCastLinkPaths(options.cwd, options.matrixHome);
fs.mkdirSync(paths.matrixHome, { recursive: true });
const homeOwner = ownerForPath(paths.matrixHome);
fs.mkdirSync(paths.credentialsDir, { recursive: true });
alignOwnership(paths.credentialsDir, homeOwner);
writePrivateJson(paths.natsCredentialsPath, options.natsCredentials);
alignOwnership(paths.natsCredentialsPath, homeOwner);
writePrivateJson(paths.hostCredentialsPath, options.natsCredentials);
alignOwnership(paths.hostCredentialsPath, homeOwner);
const leafnodeUrl = optionalString(options.nats.leafnodeUrl);
const leafnodeCredentialBody = natsUserCredentialsFile(options.natsCredentials);
if (options.authorityCoordinator !== true && leafnodeUrl) {
if (!leafnodeCredentialBody) {
throw new Error('HIVECAST_LINK_LEAFNODE_CREDENTIALS_MISSING: linked Device leaf-node transport requires jwt+seed credentials');
}
writePrivateText(paths.natsLeafnodeCredentialsPath, leafnodeCredentialBody);
alignOwnership(paths.natsLeafnodeCredentialsPath, homeOwner);
} else {
fs.rmSync(paths.natsLeafnodeCredentialsPath, { force: true });
}
if (options.hostLinkToken) {
writePrivateText(paths.hostLinkTokenPath, `${options.hostLinkToken}\n`);
alignOwnership(paths.hostLinkTokenPath, homeOwner);
} else {
fs.rmSync(paths.hostLinkTokenPath, { force: true });
}
const record: IHiveCastHostLinkRecord = {
version: 1,
cloudUrl: options.cloudUrl,
...(options.deviceRegistryRoot ? { deviceRegistryRoot: options.deviceRegistryRoot } : {}),
...(options.hostLinkId ? { hostLinkId: options.hostLinkId } : {}),
...(options.hostId ? { hostId: options.hostId } : {}),
...(options.principalId ? { principalId: options.principalId } : {}),
...(options.hostName ? { hostName: options.hostName } : {}),
...(options.deviceSlug ? { deviceSlug: options.deviceSlug } : {}),
authorityCoordinator: options.authorityCoordinator === true,
...(options.spaceId ? { spaceId: options.spaceId } : {}),
...(options.routeKey ? { routeKey: options.routeKey } : {}),
...(options.publicNamespace ? { publicNamespace: options.publicNamespace } : {}),
authorityRoot: options.authorityRoot,
linkedAt: options.linkedAt ?? new Date().toISOString(),
credentials: {
...(options.hostLinkToken ? { hostLinkTokenRef: 'file:credentials/hivecast-host-link-token' } : {}),
natsCredentialRef: HIVECAST_NATS_JSON_CREDENTIAL_REF,
...(options.authorityCoordinator !== true && leafnodeUrl
? { natsLeafnodeCredentialRef: HIVECAST_NATS_LEAFNODE_CREDENTIAL_REF }
: {}),
},
};
writePrivateJson(paths.linkPath, record);
alignOwnership(paths.linkPath, homeOwner);
const existingHostConfig = readJsonFile(paths.hostConfigPath);
const localOwnerTransport = readLocalOwnerTransportSnapshotForHome(paths.matrixHome, existingHostConfig)
?? captureLocalOwnerTransportFromCurrentNats(paths.matrixHome, existingHostConfig)
?? readLocalOwnerTransportSnapshotFromStatus(paths.matrixHome)
?? readLocalOwnerTransportSnapshotFromRuntimeRecords(paths.matrixHome)
?? captureLocalOwnerTransport(paths.matrixHome, existingHostConfig);
const linkedAuthorityTransport: ILinkedAuthorityTransportSnapshot = {
authorityRoot: options.authorityRoot,
...(options.routeKey ? { routeKey: options.routeKey } : {}),
...(options.publicNamespace ? { publicNamespace: options.publicNamespace } : {}),
...(options.spaceId ? { spaceId: options.spaceId } : {}),
authorityCoordinator: options.authorityCoordinator === true,
...(options.authorityCoordinator !== true && leafnodeUrl
? { leafnode: { url: leafnodeUrl, credentialsRef: HIVECAST_NATS_LEAFNODE_CREDENTIAL_REF } }
: {}),
nats: linkedAuthorityNatsTransport(options.nats, paths.natsCredentialsPath),
};
const hostConfig = buildHostConfig(
existingHostConfig,
paths.matrixHome,
localOwnerTransport,
linkedAuthorityTransport,
);
assertLinkedHostConfigConverged(record, hostConfig);
writePrivateJson(paths.hostConfigPath, hostConfig);
alignOwnership(paths.hostConfigPath, homeOwner);
updateHiveCastDefaultRuntimePolicies(paths.matrixHome, activeDefaultRuntimeScope(paths.matrixHome));
return { record, paths };
}
export function readHiveCastHostLink(cwd: string, matrixHome?: string): {
readonly record: IHiveCastHostLinkRecord | null;
readonly paths: IHiveCastLinkPaths;
} {
const paths = hiveCastLinkPaths(cwd, matrixHome);
const parsed = readJsonFile(paths.linkPath);
if (!parsed || parsed.version !== 1) {
return { record: null, paths };
}
const authorityRoot = optionalString(parsed.authorityRoot);
const cloudUrl = optionalString(parsed.cloudUrl);
const credentials = asRecord(parsed.credentials);
const natsCredentialRef = optionalString(credentials?.natsCredentialRef);
const linkedAt = optionalString(parsed.linkedAt);
if (!authorityRoot || !cloudUrl || !natsCredentialRef || !linkedAt) {
return { record: null, paths };
}
const hostId = optionalString(parsed.hostId);
const deviceRegistryRoot = optionalString(parsed.deviceRegistryRoot);
const hostLinkId = optionalString(parsed.hostLinkId);
const principalId = optionalString(parsed.principalId);
const hostName = optionalString(parsed.hostName);
const deviceSlug = optionalString(parsed.deviceSlug);
const authorityCoordinator = typeof parsed.authorityCoordinator === 'boolean'
? parsed.authorityCoordinator
: undefined;
const spaceId = optionalString(parsed.spaceId);
const routeKey = optionalString(parsed.routeKey);
const publicNamespace = optionalString(parsed.publicNamespace);
const hostLinkTokenRef = optionalString(credentials?.hostLinkTokenRef);
const registryTokenRef = optionalString(credentials?.registryTokenRef);
const natsLeafnodeCredentialRef = optionalString(credentials?.natsLeafnodeCredentialRef);
return {
paths,
record: {
version: 1,
cloudUrl,
...(deviceRegistryRoot ? { deviceRegistryRoot } : {}),
...(hostLinkId ? { hostLinkId } : {}),
...(hostId ? { hostId } : {}),
...(principalId ? { principalId } : {}),
...(hostName ? { hostName } : {}),
...(deviceSlug ? { deviceSlug } : {}),
...(typeof authorityCoordinator === 'boolean' ? { authorityCoordinator } : {}),
...(spaceId ? { spaceId } : {}),
...(routeKey ? { routeKey } : {}),
...(publicNamespace ? { publicNamespace } : {}),
authorityRoot,
linkedAt,
credentials: {
natsCredentialRef,
...(hostLinkTokenRef ? { hostLinkTokenRef } : {}),
...(registryTokenRef ? { registryTokenRef } : {}),
...(natsLeafnodeCredentialRef ? { natsLeafnodeCredentialRef } : {}),
},
},
};
}
export function removeHiveCastCredentialFiles(
paths: Pick<IHiveCastLinkPaths, 'natsCredentialsPath' | 'natsLeafnodeCredentialsPath' | 'hostCredentialsPath' | 'hostLinkTokenPath'>,
): boolean {
const existed = fs.existsSync(paths.natsCredentialsPath)
|| fs.existsSync(paths.natsLeafnodeCredentialsPath)
|| fs.existsSync(paths.hostCredentialsPath)
|| fs.existsSync(paths.hostLinkTokenPath);
fs.rmSync(paths.natsCredentialsPath, { force: true });
fs.rmSync(paths.natsLeafnodeCredentialsPath, { force: true });
fs.rmSync(paths.hostCredentialsPath, { force: true });
fs.rmSync(paths.hostLinkTokenPath, { force: true });
return existed;
}
export function removeHiveCastHostLink(
cwd: string,
matrixHome?: string,
options: IRemoveHiveCastHostLinkOptions = {},
): IRemoveHiveCastHostLinkResult {
const paths = hiveCastLinkPaths(cwd, matrixHome);
const existed = fs.existsSync(paths.linkPath)
|| fs.existsSync(paths.natsCredentialsPath)
|| fs.existsSync(paths.natsLeafnodeCredentialsPath)
|| fs.existsSync(paths.hostLinkTokenPath)
|| fs.existsSync(paths.hostCredentialsPath)
|| isHostConfigLinked(paths.hostConfigPath);
fs.rmSync(paths.linkPath, { force: true });
updateHiveCastDefaultRuntimePolicies(paths.matrixHome, localDefaultRuntimeScope(paths.matrixHome));
const hostConfigReset = resetHostConfigToLocalOwner(paths.hostConfigPath);
if (options.preserveCredentials !== true) {
removeHiveCastCredentialFiles(paths);
}
return { removed: existed, hostConfigReset, paths };
}
export function readHiveCastLinkStatus(cwd: string, matrixHome?: string): IHiveCastLinkStatus {
const { record, paths } = readHiveCastHostLink(cwd, matrixHome);
const hostConfigLinked = Boolean(record) && fs.existsSync(paths.hostConfigPath);
return {
linked: Boolean(record),
matrixHome: paths.matrixHome,
linkPath: paths.linkPath,
natsCredentialsPath: paths.natsCredentialsPath,
hostConfigPath: paths.hostConfigPath,
natsCredentialsPresent: fs.existsSync(paths.natsCredentialsPath),
hostConfigPresent: fs.existsSync(paths.hostConfigPath),
hostConfigLinked,
...(record?.hostLinkId ? { hostLinkId: record.hostLinkId } : {}),
...(record?.authorityRoot ? { authorityRoot: record.authorityRoot } : {}),
...(record?.routeKey ? { routeKey: record.routeKey } : {}),
...(record?.publicNamespace ? { publicNamespace: record.publicNamespace } : {}),
...(record?.spaceId ? { spaceId: record.spaceId } : {}),
...(typeof record?.authorityCoordinator === 'boolean' ? { authorityCoordinator: record.authorityCoordinator } : {}),
...(record?.cloudUrl ? { cloudUrl: record.cloudUrl } : {}),
...(record?.linkedAt ? { linkedAt: record.linkedAt } : {}),
};
}
function isHostConfigLinked(hostConfigPath: string): boolean {
const config = readJsonFile(hostConfigPath);
const transport = asRecord(config?.transport);
if (!transport) return false;
const nats = asRecord(transport.nats);
const root = optionalString(transport.root);
const authorityRoot = optionalString(transport.authorityRoot);
const matrixHome = optionalString(config?.home) ?? path.dirname(hostConfigPath);
const localOwnerTransport = readLocalOwnerTransportSnapshotForHome(matrixHome, config);
const localOwnerRoot = localOwnerTransport?.root;
const hasCloudAuthority = Boolean(
authorityRoot
&& authorityRoot !== root,
) && (localOwnerRoot ? authorityRoot !== localOwnerRoot : true);
const externalNatsIsHiveCast = nats?.mode === 'external'
&& (
Boolean(optionalString(nats.credentialsRef))
|| (!loopbackUrlPort(optionalString(nats.url)) && !loopbackUrlPort(optionalString(nats.wsUrl)))
);
return Boolean(
optionalString(transport.routeKey)
|| optionalString(transport.publicNamespace)
|| optionalString(transport.spaceId)
|| hasCloudAuthority
|| externalNatsIsHiveCast,
);
}
function resetHostConfigToLocalOwner(hostConfigPath: string): boolean {
const config = readJsonFile(hostConfigPath);
if (!config) return false;
const transport = asRecord(config.transport);
if (!transport) return false;
const currentNats = asRecord(transport.nats);
const matrixHome = optionalString(config.home) ?? path.dirname(hostConfigPath);
const localOwnerTransport = readLocalOwnerTransportSnapshotForHome(matrixHome, config)
?? readLocalOwnerTransportSnapshotFromRuntimeRecords(matrixHome)
?? readLocalOwnerTransportSnapshotFromStatus(matrixHome);
const nextTransport: JsonRecord = { ...transport };
const localRoot = localOwnerAuthorityRoot(matrixHome);
nextTransport.root = localOwnerTransport?.root ?? localRoot;
if (localOwnerTransport?.authorityRoot) {
nextTransport.authorityRoot = localOwnerTransport.authorityRoot;
} else {
delete nextTransport.authorityRoot;
}
nextTransport.addressRoot = localOwnerTransport?.root ?? localRoot;
delete nextTransport.routeKey;
delete nextTransport.publicNamespace;
delete nextTransport.spaceId;
nextTransport.nats = localOwnerTransport?.nats
?? (currentNats && currentNats.mode !== 'external' ? currentNats : defaultLocalOwnerNatsTransport());
const nextConfig: JsonRecord = {
...config,
transport: nextTransport,
};
delete nextConfig.natsTopology;
writePrivateJson(hostConfigPath, nextConfig);
return true;
}