@aopslabs/aops 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/NOTICE +7 -0
- package/README.md +203 -0
- package/assets/agent-assets/core/SKILL.md +49 -0
- package/assets/agent-assets/core/references/agentspace/SKILL.md +78 -0
- package/assets/agent-assets/core/references/aops-cli-core/SKILL.md +108 -0
- package/assets/agent-assets/core/references/chatv3/SKILL.md +48 -0
- package/assets/agent-assets/core/references/collaborative-work/SKILL.md +85 -0
- package/assets/agent-assets/core/references/discuss/SKILL.md +66 -0
- package/assets/agent-assets/core/references/docman/SKILL.md +69 -0
- package/assets/agent-assets/core/references/hosted-chat/SKILL.md +64 -0
- package/assets/agent-assets/core/references/mission/SKILL.md +54 -0
- package/assets/agent-assets/core/references/projectman/SKILL.md +86 -0
- package/assets/agent-assets/core/references/sys/SKILL.md +36 -0
- package/assets/agent-assets/core/references/view/SKILL.md +58 -0
- package/assets/agent-assets/core/references/working-disciplines/SKILL.md +69 -0
- package/assets/agent-assets/core/user-guides/agent-assets.md +81 -0
- package/assets/agent-assets/core/user-guides/agentspace.md +1098 -0
- package/assets/agent-assets/core/user-guides/aops-cli.md +1107 -0
- package/assets/agent-assets/core/user-guides/aops-system.md +63 -0
- package/assets/agent-assets/core/user-guides/chatv3.md +113 -0
- package/assets/agent-assets/core/user-guides/docman.md +162 -0
- package/assets/agent-assets/core/user-guides/projectman.md +212 -0
- package/assets/agent-assets/core/user-guides/sys.md +94 -0
- package/assets/agent-assets/core/user-guides/working-disciplines.md +588 -0
- package/assets/agent-assets/gateway/aops/SKILL.md +14 -0
- package/assets/r6-remote-session-baseline.json +19 -0
- package/assets/skills/aops-install/SKILL.md +231 -0
- package/assets/skills/aops-install/agents/openai.yaml +4 -0
- package/dist/commands/activity.js +352 -0
- package/dist/commands/agent-profile.js +653 -0
- package/dist/commands/agent.js +710 -0
- package/dist/commands/agents-md.js +125 -0
- package/dist/commands/api.js +84 -0
- package/dist/commands/archive.js +873 -0
- package/dist/commands/artifact.js +535 -0
- package/dist/commands/assets.js +1414 -0
- package/dist/commands/auth/device-login.js +1 -0
- package/dist/commands/auth/legacy-export.js +177 -0
- package/dist/commands/auth/login.js +426 -0
- package/dist/commands/chat.js +1363 -0
- package/dist/commands/chatv3.js +1684 -0
- package/dist/commands/checkpoint.js +549 -0
- package/dist/commands/commercial-artifact.js +475 -0
- package/dist/commands/commercial-license.js +594 -0
- package/dist/commands/community-auth.js +62 -0
- package/dist/commands/community-cockpit.js +185 -0
- package/dist/commands/community-console.js +109 -0
- package/dist/commands/community-doctor.js +335 -0
- package/dist/commands/community-server.js +2062 -0
- package/dist/commands/community-setup.js +284 -0
- package/dist/commands/discuss.js +1448 -0
- package/dist/commands/doc.js +5219 -0
- package/dist/commands/experience.js +922 -0
- package/dist/commands/host.js +1127 -0
- package/dist/commands/init.js +118 -0
- package/dist/commands/memory.js +3129 -0
- package/dist/commands/mission.js +562 -0
- package/dist/commands/official-catalog.js +134 -0
- package/dist/commands/plan.js +84 -0
- package/dist/commands/playbook.js +415 -0
- package/dist/commands/pm/context.js +6 -0
- package/dist/commands/pm/index.js +701 -0
- package/dist/commands/pm/projectman.js +6266 -0
- package/dist/commands/project.js +1080 -0
- package/dist/commands/prompt.js +1024 -0
- package/dist/commands/repo-sync.js +1907 -0
- package/dist/commands/resource.js +527 -0
- package/dist/commands/skill.js +1211 -0
- package/dist/commands/start-disciplines.js +510 -0
- package/dist/commands/start.js +1343 -0
- package/dist/commands/target.js +309 -0
- package/dist/commands/version.js +42 -0
- package/dist/commands/view.js +1852 -0
- package/dist/launchers/cockpit.js +28 -0
- package/dist/launchers/options.js +74 -0
- package/dist/launchers/server.js +41 -0
- package/dist/lib/agent-assets/envelope.js +83 -0
- package/dist/lib/agent-assets/gateway.js +15 -0
- package/dist/lib/agent-assets/guards.js +23 -0
- package/dist/lib/agent-assets/hosted-discovery.js +148 -0
- package/dist/lib/agent-assets/hosted-package-input.js +154 -0
- package/dist/lib/agent-assets/legacy-pointer-migration.js +677 -0
- package/dist/lib/agent-assets/native-fs.js +589 -0
- package/dist/lib/agent-assets/package-manifest.js +324 -0
- package/dist/lib/agent-assets/portable-path.js +67 -0
- package/dist/lib/agent-assets/release-input.js +177 -0
- package/dist/lib/agent-assets/roots.js +45 -0
- package/dist/lib/agent-assets/runtime-binding-reader.js +545 -0
- package/dist/lib/agent-assets/runtime-targets.js +50 -0
- package/dist/lib/agent-assets/store-reader.js +1212 -0
- package/dist/lib/agent-assets/store-types.js +1 -0
- package/dist/lib/agent-assets/store-writer.js +1484 -0
- package/dist/lib/agent-assets/types.js +1 -0
- package/dist/lib/agent-assets/unicode-case-folding-v15-1.generated.js +1548 -0
- package/dist/lib/commercial-final-readiness.js +504 -0
- package/dist/lib/commercial-lifecycle-smoke-isolation.js +236 -0
- package/dist/lib/commercial-managed-postgres-docker-adapter.js +274 -0
- package/dist/lib/commercial-setup-readiness-adapter.js +179 -0
- package/dist/lib/community-client-contract.js +164 -0
- package/dist/lib/community-cockpit-lifecycle.js +601 -0
- package/dist/lib/community-command-abort.js +59 -0
- package/dist/lib/community-diagnostic.js +237 -0
- package/dist/lib/community-docker-adapter.js +498 -0
- package/dist/lib/community-home.js +31 -0
- package/dist/lib/community-instance-contract.js +148 -0
- package/dist/lib/community-instance-layout.js +97 -0
- package/dist/lib/community-lifecycle.js +1631 -0
- package/dist/lib/community-migration-snapshot.js +770 -0
- package/dist/lib/community-native-application-recovery.js +425 -0
- package/dist/lib/community-native-child.js +240 -0
- package/dist/lib/community-native-database-recovery.js +456 -0
- package/dist/lib/community-native-lifecycle.js +2458 -0
- package/dist/lib/community-native-migration.js +339 -0
- package/dist/lib/community-native-postgres.js +596 -0
- package/dist/lib/community-operation-journal.js +773 -0
- package/dist/lib/community-operation-lock.js +596 -0
- package/dist/lib/community-release-resolver.js +291 -0
- package/dist/lib/community-release-verifier.js +475 -0
- package/dist/lib/community-repo-discovery.js +252 -0
- package/dist/lib/community-setup-server-env.js +203 -0
- package/dist/lib/external-url.js +110 -0
- package/dist/lib/official-catalog-gateway.js +108 -0
- package/dist/lib/official-catalog.js +364 -0
- package/dist/lib/server-bootstrap.js +240 -0
- package/dist/lib/setup-agent-assets-bridge.js +176 -0
- package/dist/lib/setup-agent-assets-release.js +52 -0
- package/dist/lib/setup-external-postgres.js +108 -0
- package/dist/lib/setup-init-orchestrator.js +990 -0
- package/dist/lib/setup-install-guide.js +52 -0
- package/dist/lib/setup-installer-release.js +47 -0
- package/dist/lib/setup-local-postgres.js +329 -0
- package/dist/lib/setup-official-catalog-bridge.js +148 -0
- package/dist/lib/setup-readiness.js +888 -0
- package/dist/lib/tui-launcher.js +59 -0
- package/dist/main.js +150 -0
- package/dist/seeds/agents-md.js +54 -0
- package/dist/utils/agent-gateway.js +275 -0
- package/dist/utils/agents-md.js +281 -0
- package/dist/utils/api.js +255 -0
- package/dist/utils/chatv3-session-store.js +185 -0
- package/dist/utils/command.js +67 -0
- package/dist/utils/community-host-runtime.js +376 -0
- package/dist/utils/config.js +1015 -0
- package/dist/utils/discussion-workspace.js +552 -0
- package/dist/utils/experience-workspace.js +326 -0
- package/dist/utils/guide-paths.js +67 -0
- package/dist/utils/hosted-sugar.js +56 -0
- package/dist/utils/hosted-workspace.js +229 -0
- package/dist/utils/memory-workspace.js +537 -0
- package/dist/utils/operator-runtime.js +199 -0
- package/dist/utils/password-crypto.js +39 -0
- package/dist/utils/playbook-workspace.js +479 -0
- package/dist/utils/project-context.js +255 -0
- package/dist/utils/prompts.js +70 -0
- package/dist/utils/refresh-race.js +28 -0
- package/dist/utils/repo-config.js +109 -0
- package/dist/utils/repo-first-projectman.js +194 -0
- package/dist/utils/repo-first-storage.js +13 -0
- package/dist/utils/repo-first-view.js +305 -0
- package/dist/utils/session-guidance.js +278 -0
- package/dist/utils/session-state.js +137 -0
- package/dist/utils/table.js +39 -0
- package/dist/utils/transient-fs.js +49 -0
- package/dist/utils/workspace-root.js +1 -0
- package/launchers/aops-cockpit.ps1 +15 -0
- package/launchers/aops-cockpit.sh +16 -0
- package/launchers/aops-server.ps1 +15 -0
- package/launchers/aops-server.sh +16 -0
- package/native/tui/win32-x64/aops-tui.exe +0 -0
- package/package.json +81 -0
|
@@ -0,0 +1,2458 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { parseEnv } from 'node:util';
|
|
8
|
+
import { COMMUNITY_NATIVE_CHILD_PROTOCOL, COMMUNITY_NATIVE_CONTROL_PROTOCOL, } from './community-native-child.js';
|
|
9
|
+
import { resolveCommunityInstallPaths } from './community-lifecycle.js';
|
|
10
|
+
import { COMMUNITY_NATIVE_POSTGRES_CONTRACT_PATH, assertCommunityNativePostgresInstanceState, assertCommunityNativePostgresState, buildCommunityNativePostgresUrl, setupCommunityNativePostgres, startCommunityNativePostgres, stopCommunityNativePostgres, } from './community-native-postgres.js';
|
|
11
|
+
import { COMMUNITY_NATIVE_MIGRATION_POLICY_PATH, assertCommunityNativeMigrationReceiptV1, planCommunityNativeMigration, runCommunityNativeMigration, } from './community-native-migration.js';
|
|
12
|
+
import { createCommunityExternalSnapshotAttestationV1, writeCommunityExternalSnapshotAttestationV1, } from './community-migration-snapshot.js';
|
|
13
|
+
import { createCommunityNativeApplicationReferenceV1, readCommunityNativeApplicationUpdate, sameCommunityNativeApplicationContent, writeCommunityNativeApplicationRollbackOutcome, writeCommunityNativeApplicationRollbackPrepared, writeCommunityNativeApplicationUpdateOutcome, writeCommunityNativeApplicationUpdatePrepared, } from './community-native-application-recovery.js';
|
|
14
|
+
const PUBLIC_ROOT_NAME = 'aops-community-distribution';
|
|
15
|
+
const PUBLIC_SERVER_PACKAGE_NAME = '@aopslabs/aops-server';
|
|
16
|
+
const PUBLIC_PACKAGE_MANAGER = /^pnpm@(11\.[0-9]+\.[0-9]+)$/;
|
|
17
|
+
const RELEASE_VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?(?:\+[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?$/;
|
|
18
|
+
const INSTANCE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
|
|
19
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/;
|
|
20
|
+
const RAW_SHA256 = /^[a-f0-9]{64}$/;
|
|
21
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
22
|
+
const MAX_CONFIG_BYTES = 65_536;
|
|
23
|
+
const MAX_TLS_ROOT_CERT_BYTES = 1_048_576;
|
|
24
|
+
const MIN_EXACT_SECRET_REDACTION_LENGTH = 8;
|
|
25
|
+
const DEFAULT_READY_TIMEOUT_MS = 30_000;
|
|
26
|
+
const DEFAULT_STOP_TIMEOUT_MS = 15_000;
|
|
27
|
+
const READY_POLL_MS = 250;
|
|
28
|
+
const MAX_LOG_TAIL_BYTES = 4 * 1024 * 1024;
|
|
29
|
+
const MAX_INVENTORY_FILES = 50_000;
|
|
30
|
+
const MAX_INVENTORY_BYTES = 2 * 1024 * 1024 * 1024;
|
|
31
|
+
const SOURCE_INVENTORY_EXCLUDED_DIRECTORIES = new Set([
|
|
32
|
+
'.aops', '.git', '.pnpm-store', '.svelte-kit', '.turbo', 'build', 'coverage', 'dist', 'node_modules',
|
|
33
|
+
]);
|
|
34
|
+
const RUNTIME_INVENTORY_EXCLUDED_DIRECTORIES = new Set([
|
|
35
|
+
'.aops', '.git', '.pnpm-store', '.svelte-kit', '.turbo', 'coverage', 'node_modules',
|
|
36
|
+
]);
|
|
37
|
+
const CHECKOUT_REQUIRED_SOURCE_PATHS = Object.freeze([
|
|
38
|
+
'apps/aops-cli/package.json',
|
|
39
|
+
'apps/aops-cockpit-v2/package.json',
|
|
40
|
+
'apps/aops-server/package.json',
|
|
41
|
+
'apps/aops-server/scripts/community-host.mjs',
|
|
42
|
+
COMMUNITY_NATIVE_MIGRATION_POLICY_PATH,
|
|
43
|
+
COMMUNITY_NATIVE_POSTGRES_CONTRACT_PATH,
|
|
44
|
+
'pnpm-lock.yaml',
|
|
45
|
+
'pnpm-workspace.yaml',
|
|
46
|
+
]);
|
|
47
|
+
const CHECKOUT_BUILD_PATHS = Object.freeze({
|
|
48
|
+
hostEntry: 'apps/aops-server/scripts/community-host.mjs',
|
|
49
|
+
handlerEntry: 'apps/aops-server/build/handler.js',
|
|
50
|
+
cockpitIndex: 'apps/aops-cockpit-v2/dist/index.html',
|
|
51
|
+
});
|
|
52
|
+
const PACKAGE_REQUIRED_SOURCE_PATHS = Object.freeze([
|
|
53
|
+
'community-postgres.json',
|
|
54
|
+
'package.json',
|
|
55
|
+
'runtime/agentspace-host-adapter.mjs',
|
|
56
|
+
'runtime/agentspace-tooling.mjs',
|
|
57
|
+
'runtime/docman-host-adapter.mjs',
|
|
58
|
+
'runtime/docman-policy.mjs',
|
|
59
|
+
'runtime/docman-tooling.mjs',
|
|
60
|
+
'runtime/projectman-host-adapter.mjs',
|
|
61
|
+
'runtime/scope-context.mjs',
|
|
62
|
+
'scripts/community-host.mjs',
|
|
63
|
+
'scripts/community-migration-policy-package-v1.json',
|
|
64
|
+
]);
|
|
65
|
+
const PACKAGE_BUILD_PATHS = Object.freeze({
|
|
66
|
+
hostEntry: 'scripts/community-host.mjs',
|
|
67
|
+
handlerEntry: 'build/handler.js',
|
|
68
|
+
cockpitIndex: 'cockpit/index.html',
|
|
69
|
+
});
|
|
70
|
+
function sha256(content) {
|
|
71
|
+
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
72
|
+
}
|
|
73
|
+
function throwIfNativeAborted(signal) {
|
|
74
|
+
if (signal?.aborted)
|
|
75
|
+
throw new Error('community_operation_aborted');
|
|
76
|
+
}
|
|
77
|
+
function hashFile(filePath) {
|
|
78
|
+
const stats = lstatSync(filePath);
|
|
79
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
80
|
+
throw new Error(`community_native_required_file_invalid:${path.basename(filePath)}`);
|
|
81
|
+
}
|
|
82
|
+
return sha256(readFileSync(filePath));
|
|
83
|
+
}
|
|
84
|
+
function isWithin(root, candidate) {
|
|
85
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
86
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
87
|
+
}
|
|
88
|
+
function safeRegularFile(root, relativePath) {
|
|
89
|
+
const candidate = path.resolve(root, relativePath);
|
|
90
|
+
if (!isWithin(root, candidate))
|
|
91
|
+
throw new Error(`community_native_source_path_escape:${relativePath}`);
|
|
92
|
+
let stats;
|
|
93
|
+
try {
|
|
94
|
+
stats = lstatSync(candidate);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new Error(`community_native_source_file_missing:${relativePath}`);
|
|
98
|
+
}
|
|
99
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
100
|
+
throw new Error(`community_native_source_file_invalid:${relativePath}`);
|
|
101
|
+
}
|
|
102
|
+
const realRoot = realpathSync(root);
|
|
103
|
+
const realCandidate = realpathSync(candidate);
|
|
104
|
+
if (!isWithin(realRoot, realCandidate))
|
|
105
|
+
throw new Error(`community_native_source_realpath_escape:${relativePath}`);
|
|
106
|
+
return realCandidate;
|
|
107
|
+
}
|
|
108
|
+
function codepointCompare(left, right) {
|
|
109
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
110
|
+
}
|
|
111
|
+
function inventoryCommunityNativeTree(root, excludedDirectories) {
|
|
112
|
+
const canonicalRoot = realpathSync(root);
|
|
113
|
+
const records = [];
|
|
114
|
+
let byteLength = 0;
|
|
115
|
+
const visit = (directory, prefix) => {
|
|
116
|
+
const directoryStats = lstatSync(directory);
|
|
117
|
+
if (!directoryStats.isDirectory() || directoryStats.isSymbolicLink() || realpathSync(directory) !== directory) {
|
|
118
|
+
throw new Error(`community_native_inventory_directory_unsafe:${prefix || '.'}`);
|
|
119
|
+
}
|
|
120
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) => codepointCompare(left.name, right.name))) {
|
|
121
|
+
if (entry.isDirectory() && excludedDirectories.has(entry.name))
|
|
122
|
+
continue;
|
|
123
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
124
|
+
const absolutePath = path.join(directory, entry.name);
|
|
125
|
+
const stats = lstatSync(absolutePath);
|
|
126
|
+
if (stats.isSymbolicLink())
|
|
127
|
+
throw new Error(`community_native_inventory_link_forbidden:${relativePath}`);
|
|
128
|
+
if (stats.isDirectory()) {
|
|
129
|
+
visit(absolutePath, relativePath);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!stats.isFile() || stats.size < 0) {
|
|
133
|
+
throw new Error(`community_native_inventory_entry_invalid:${relativePath}`);
|
|
134
|
+
}
|
|
135
|
+
const canonicalFile = realpathSync(absolutePath);
|
|
136
|
+
if (!isWithin(canonicalRoot, canonicalFile)) {
|
|
137
|
+
throw new Error(`community_native_inventory_path_escape:${relativePath}`);
|
|
138
|
+
}
|
|
139
|
+
const content = readFileSync(canonicalFile);
|
|
140
|
+
byteLength += content.byteLength;
|
|
141
|
+
records.push({ path: relativePath.replace(/\\/g, '/'), byteLength: content.byteLength, sha256: sha256(content) });
|
|
142
|
+
if (records.length > MAX_INVENTORY_FILES || byteLength > MAX_INVENTORY_BYTES) {
|
|
143
|
+
throw new Error('community_native_inventory_bounds_exceeded');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
visit(canonicalRoot, '');
|
|
148
|
+
return Object.freeze({
|
|
149
|
+
fileCount: records.length,
|
|
150
|
+
byteLength,
|
|
151
|
+
sha256: sha256(JSON.stringify(records)),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function atomicTextWrite(targetPath, content) {
|
|
155
|
+
const parent = path.dirname(targetPath);
|
|
156
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
157
|
+
const tempPath = path.join(parent, `.${path.basename(targetPath)}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`);
|
|
158
|
+
let descriptor;
|
|
159
|
+
try {
|
|
160
|
+
descriptor = openSync(tempPath, 'wx', 0o600);
|
|
161
|
+
writeFileSync(descriptor, content, { encoding: 'utf8' });
|
|
162
|
+
fsyncSync(descriptor);
|
|
163
|
+
closeSync(descriptor);
|
|
164
|
+
descriptor = undefined;
|
|
165
|
+
renameSync(tempPath, targetPath);
|
|
166
|
+
fsyncDirectoryBestEffortOnWindows(parent);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
if (descriptor !== undefined)
|
|
170
|
+
closeSync(descriptor);
|
|
171
|
+
rmSync(tempPath, { force: true });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function atomicJsonWrite(targetPath, value) {
|
|
175
|
+
atomicTextWrite(targetPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
176
|
+
}
|
|
177
|
+
function fsyncDirectoryBestEffortOnWindows(directory) {
|
|
178
|
+
let descriptor;
|
|
179
|
+
try {
|
|
180
|
+
descriptor = openSync(directory, 'r');
|
|
181
|
+
fsyncSync(descriptor);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
const code = error.code;
|
|
185
|
+
if (process.platform !== 'win32' || !['EACCES', 'EBADF', 'EINVAL', 'EISDIR', 'EPERM'].includes(String(code))) {
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
if (descriptor !== undefined)
|
|
191
|
+
closeSync(descriptor);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function readJson(targetPath, code) {
|
|
195
|
+
try {
|
|
196
|
+
return JSON.parse(readFileSync(targetPath, 'utf8'));
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
throw new Error(code);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function exactKeys(input, expected, code) {
|
|
203
|
+
const actual = Object.keys(input).sort();
|
|
204
|
+
const wanted = [...expected].sort();
|
|
205
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
206
|
+
throw new Error(code);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function validPort(value) {
|
|
210
|
+
return Number.isSafeInteger(value) && Number(value) >= 1 && Number(value) <= 65_535;
|
|
211
|
+
}
|
|
212
|
+
export function resolveCommunityNativeCockpitPort(serverPort) {
|
|
213
|
+
if (!validPort(serverPort))
|
|
214
|
+
throw new Error('community_native_server_port_invalid');
|
|
215
|
+
const candidate = serverPort <= 65_513 ? serverPort + 22 : serverPort - 22;
|
|
216
|
+
if (!validPort(candidate) || candidate === serverPort) {
|
|
217
|
+
throw new Error('community_native_cockpit_port_invalid');
|
|
218
|
+
}
|
|
219
|
+
return candidate;
|
|
220
|
+
}
|
|
221
|
+
export function resolveCommunityNativePaths(input = {}) {
|
|
222
|
+
const ociPaths = resolveCommunityInstallPaths(input);
|
|
223
|
+
return {
|
|
224
|
+
dataRoot: ociPaths.dataRoot,
|
|
225
|
+
instanceRoot: ociPaths.instanceRoot,
|
|
226
|
+
runtimeRoot: ociPaths.runtimeRoot,
|
|
227
|
+
statePath: path.join(ociPaths.runtimeRoot, 'native-state.json'),
|
|
228
|
+
processPath: path.join(ociPaths.runtimeRoot, 'native-process.json'),
|
|
229
|
+
identityPath: path.join(ociPaths.runtimeRoot, 'native-identity.json'),
|
|
230
|
+
controlPath: path.join(ociPaths.runtimeRoot, 'native-control.json'),
|
|
231
|
+
secretPath: path.join(ociPaths.runtimeRoot, 'native-secrets.env'),
|
|
232
|
+
migrationReceiptPath: path.join(ociPaths.runtimeRoot, 'native-migration-receipt.json'),
|
|
233
|
+
migrationEvidenceRoot: path.join(ociPaths.runtimeRoot, 'native-migration-evidence'),
|
|
234
|
+
applicationUpdateRoot: path.join(ociPaths.runtimeRoot, 'native-application-updates'),
|
|
235
|
+
databaseRestoreRoot: path.join(ociPaths.runtimeRoot, 'native-database-restores'),
|
|
236
|
+
backupRoot: ociPaths.backupRoot,
|
|
237
|
+
logRoot: path.join(ociPaths.instanceRoot, 'logs'),
|
|
238
|
+
logPath: path.join(ociPaths.instanceRoot, 'logs', 'native-server.log'),
|
|
239
|
+
ociStatePath: ociPaths.statePath,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function resolveLegacyCommunityNativeMigrationPaths(paths) {
|
|
243
|
+
return {
|
|
244
|
+
intentPath: path.join(paths.runtimeRoot, 'native-migration-intent.json'),
|
|
245
|
+
receiptRoot: path.join(paths.runtimeRoot, 'native-migration-receipts'),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function samePhysicalPath(left, right) {
|
|
249
|
+
return path.relative(path.resolve(left), path.resolve(right)) === '' &&
|
|
250
|
+
path.relative(path.resolve(right), path.resolve(left)) === '';
|
|
251
|
+
}
|
|
252
|
+
function assertPlainCanonicalDirectory(candidate, code) {
|
|
253
|
+
const stats = lstatSync(candidate);
|
|
254
|
+
if (!stats.isDirectory() || stats.isSymbolicLink() || !samePhysicalPath(realpathSync.native(candidate), candidate)) {
|
|
255
|
+
throw new Error(code);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function assertPlainCanonicalFile(candidate, expectedParent, code) {
|
|
259
|
+
const stats = lstatSync(candidate);
|
|
260
|
+
if (!stats.isFile() || stats.isSymbolicLink() || !samePhysicalPath(realpathSync.native(candidate), candidate) ||
|
|
261
|
+
!samePhysicalPath(path.dirname(candidate), expectedParent)) {
|
|
262
|
+
throw new Error(code);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function captureCommunityNativeLocalFile(targetPath, expectedParent) {
|
|
266
|
+
if (!existsSync(targetPath))
|
|
267
|
+
return { targetPath, content: null };
|
|
268
|
+
assertPlainCanonicalFile(targetPath, expectedParent, 'community_native_runtime_file_unsafe');
|
|
269
|
+
return { targetPath, content: readFileSync(targetPath, 'utf8') };
|
|
270
|
+
}
|
|
271
|
+
function captureCommunityNativeSetupFiles(paths) {
|
|
272
|
+
return [
|
|
273
|
+
paths.statePath,
|
|
274
|
+
paths.migrationReceiptPath,
|
|
275
|
+
paths.processPath,
|
|
276
|
+
paths.identityPath,
|
|
277
|
+
paths.controlPath,
|
|
278
|
+
].map((targetPath) => captureCommunityNativeLocalFile(targetPath, paths.runtimeRoot));
|
|
279
|
+
}
|
|
280
|
+
function restoreCommunityNativeLocalFile(snapshot, expectedParent) {
|
|
281
|
+
if (snapshot.content === null) {
|
|
282
|
+
if (!existsSync(snapshot.targetPath))
|
|
283
|
+
return;
|
|
284
|
+
assertPlainCanonicalFile(snapshot.targetPath, expectedParent, 'community_native_runtime_file_unsafe');
|
|
285
|
+
rmSync(snapshot.targetPath, { force: true });
|
|
286
|
+
fsyncDirectoryBestEffortOnWindows(expectedParent);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (existsSync(snapshot.targetPath)) {
|
|
290
|
+
assertPlainCanonicalFile(snapshot.targetPath, expectedParent, 'community_native_runtime_file_unsafe');
|
|
291
|
+
}
|
|
292
|
+
atomicTextWrite(snapshot.targetPath, snapshot.content);
|
|
293
|
+
}
|
|
294
|
+
function restoreCommunityNativeSetupFiles(paths, snapshots) {
|
|
295
|
+
for (const snapshot of snapshots)
|
|
296
|
+
restoreCommunityNativeLocalFile(snapshot, paths.runtimeRoot);
|
|
297
|
+
}
|
|
298
|
+
function removeNewCommunityNativeSecret(paths) {
|
|
299
|
+
if (!existsSync(paths.secretPath))
|
|
300
|
+
return;
|
|
301
|
+
assertPlainCanonicalFile(paths.secretPath, paths.runtimeRoot, 'community_native_runtime_file_unsafe');
|
|
302
|
+
rmSync(paths.secretPath, { force: true });
|
|
303
|
+
fsyncDirectoryBestEffortOnWindows(paths.runtimeRoot);
|
|
304
|
+
}
|
|
305
|
+
function removeCommunityNativeLaunchArtifactsForRefresh(paths) {
|
|
306
|
+
for (const targetPath of [paths.processPath, paths.identityPath, paths.controlPath]) {
|
|
307
|
+
if (!existsSync(targetPath))
|
|
308
|
+
continue;
|
|
309
|
+
assertPlainCanonicalFile(targetPath, paths.runtimeRoot, 'community_native_runtime_file_unsafe');
|
|
310
|
+
rmSync(targetPath, { force: true });
|
|
311
|
+
}
|
|
312
|
+
fsyncDirectoryBestEffortOnWindows(paths.runtimeRoot);
|
|
313
|
+
}
|
|
314
|
+
export function assertCommunityNativePathLayout(paths, options = {}) {
|
|
315
|
+
const legacyMigration = resolveLegacyCommunityNativeMigrationPaths(paths);
|
|
316
|
+
const instanceExists = existsSync(paths.instanceRoot);
|
|
317
|
+
if (!instanceExists) {
|
|
318
|
+
if (options.requireInstanceRoot === true)
|
|
319
|
+
throw new Error('community_native_instance_root_missing');
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
assertPlainCanonicalDirectory(paths.instanceRoot, 'community_native_instance_root_unsafe');
|
|
323
|
+
if (existsSync(paths.runtimeRoot)) {
|
|
324
|
+
if (!samePhysicalPath(path.dirname(paths.runtimeRoot), paths.instanceRoot)) {
|
|
325
|
+
throw new Error('community_native_runtime_root_unsafe');
|
|
326
|
+
}
|
|
327
|
+
assertPlainCanonicalDirectory(paths.runtimeRoot, 'community_native_runtime_root_unsafe');
|
|
328
|
+
}
|
|
329
|
+
if (existsSync(paths.logRoot)) {
|
|
330
|
+
if (!samePhysicalPath(path.dirname(paths.logRoot), paths.instanceRoot)) {
|
|
331
|
+
throw new Error('community_native_log_root_unsafe');
|
|
332
|
+
}
|
|
333
|
+
assertPlainCanonicalDirectory(paths.logRoot, 'community_native_log_root_unsafe');
|
|
334
|
+
}
|
|
335
|
+
for (const [candidate, code, expectedParent] of [
|
|
336
|
+
[legacyMigration.receiptRoot, 'community_native_migration_receipt_root_unsafe', paths.runtimeRoot],
|
|
337
|
+
[paths.migrationEvidenceRoot, 'community_native_migration_evidence_root_unsafe', paths.runtimeRoot],
|
|
338
|
+
[paths.applicationUpdateRoot, 'community_native_application_update_root_unsafe', paths.runtimeRoot],
|
|
339
|
+
[paths.databaseRestoreRoot, 'community_native_database_restore_root_unsafe', paths.runtimeRoot],
|
|
340
|
+
[paths.backupRoot, 'community_native_backup_root_unsafe', paths.instanceRoot],
|
|
341
|
+
]) {
|
|
342
|
+
if (existsSync(candidate)) {
|
|
343
|
+
if (!samePhysicalPath(path.dirname(candidate), expectedParent))
|
|
344
|
+
throw new Error(code);
|
|
345
|
+
assertPlainCanonicalDirectory(candidate, code);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const candidate of [
|
|
349
|
+
paths.statePath,
|
|
350
|
+
paths.processPath,
|
|
351
|
+
paths.identityPath,
|
|
352
|
+
paths.controlPath,
|
|
353
|
+
paths.secretPath,
|
|
354
|
+
paths.migrationReceiptPath,
|
|
355
|
+
legacyMigration.intentPath,
|
|
356
|
+
]) {
|
|
357
|
+
if (existsSync(candidate)) {
|
|
358
|
+
if (!existsSync(paths.runtimeRoot))
|
|
359
|
+
throw new Error('community_native_runtime_root_unsafe');
|
|
360
|
+
assertPlainCanonicalFile(candidate, paths.runtimeRoot, 'community_native_runtime_file_unsafe');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const postgresSecretPath = path.join(paths.runtimeRoot, 'native-postgres.env');
|
|
364
|
+
if (existsSync(postgresSecretPath)) {
|
|
365
|
+
if (!existsSync(paths.runtimeRoot))
|
|
366
|
+
throw new Error('community_native_runtime_root_unsafe');
|
|
367
|
+
assertPlainCanonicalFile(postgresSecretPath, paths.runtimeRoot, 'community_native_postgres_secret_invalid');
|
|
368
|
+
}
|
|
369
|
+
if (existsSync(paths.logPath)) {
|
|
370
|
+
if (!existsSync(paths.logRoot))
|
|
371
|
+
throw new Error('community_native_log_root_unsafe');
|
|
372
|
+
assertPlainCanonicalFile(paths.logPath, paths.logRoot, 'community_native_log_file_invalid');
|
|
373
|
+
}
|
|
374
|
+
if (existsSync(paths.ociStatePath)) {
|
|
375
|
+
assertPlainCanonicalFile(paths.ociStatePath, paths.instanceRoot, 'community_native_oci_state_unsafe');
|
|
376
|
+
}
|
|
377
|
+
assertLegacyCommunityNativeMigrationArtifactsSafe(paths);
|
|
378
|
+
}
|
|
379
|
+
function parseNativeState(value, paths) {
|
|
380
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
381
|
+
throw new Error('community_native_state_invalid');
|
|
382
|
+
const input = value;
|
|
383
|
+
const requiredStateKeys = [
|
|
384
|
+
'schemaVersion', 'runtime', 'instanceName', 'installId', 'profile', 'createdAt', 'updatedAt',
|
|
385
|
+
'source', 'build', 'postgres', 'server',
|
|
386
|
+
];
|
|
387
|
+
const stateKeys = Object.keys(input);
|
|
388
|
+
if (requiredStateKeys.some((key) => !stateKeys.includes(key)) ||
|
|
389
|
+
stateKeys.some((key) => !requiredStateKeys.includes(key) && key !== 'cockpit'))
|
|
390
|
+
throw new Error('community_native_state_schema_invalid');
|
|
391
|
+
if (input.schemaVersion !== 1 || input.runtime !== 'native' ||
|
|
392
|
+
!['native-external-postgres', 'native-container-postgres'].includes(String(input.profile)) ||
|
|
393
|
+
!INSTANCE_NAME.test(String(input.instanceName)) ||
|
|
394
|
+
!UUID.test(String(input.installId)) || Number.isNaN(Date.parse(String(input.createdAt))) ||
|
|
395
|
+
Number.isNaN(Date.parse(String(input.updatedAt))))
|
|
396
|
+
throw new Error('community_native_state_schema_invalid');
|
|
397
|
+
exactKeys(input.source, [
|
|
398
|
+
'root', 'packageManager', 'releaseVersion', 'packageSha256', 'lockfileSha256',
|
|
399
|
+
'sourceFileCount', 'sourceInventorySha256', 'sourceFingerprint',
|
|
400
|
+
], 'community_native_state_source_invalid');
|
|
401
|
+
exactKeys(input.build, [
|
|
402
|
+
'completedAt', 'hostEntry', 'handlerEntry', 'cockpitIndex', 'hostEntrySha256',
|
|
403
|
+
'handlerEntrySha256', 'cockpitIndexSha256', 'runtimeFileCount', 'runtimeInventorySha256',
|
|
404
|
+
'buildFingerprint',
|
|
405
|
+
], 'community_native_state_build_invalid');
|
|
406
|
+
const serverKeys = Object.keys(input.server ?? {});
|
|
407
|
+
if (!['host', 'port'].every((key) => serverKeys.includes(key)) ||
|
|
408
|
+
serverKeys.some((key) => !['host', 'port', 'exposure', 'publicPort', 'authProvider'].includes(key)))
|
|
409
|
+
throw new Error('community_native_state_server_invalid');
|
|
410
|
+
const exposure = input.server.exposure ?? 'loopback';
|
|
411
|
+
const publicPort = input.server.publicPort ?? input.server.port;
|
|
412
|
+
const cockpit = input.cockpit ?? {
|
|
413
|
+
host: '127.0.0.1',
|
|
414
|
+
port: resolveCommunityNativeCockpitPort(publicPort),
|
|
415
|
+
};
|
|
416
|
+
const authProvider = input.server.authProvider ?? 'trusted-local';
|
|
417
|
+
const expectedHost = exposure === 'authenticated-network' ? '0.0.0.0' : '127.0.0.1';
|
|
418
|
+
if (!path.isAbsolute(input.source.root) || !PUBLIC_PACKAGE_MANAGER.test(input.source.packageManager) ||
|
|
419
|
+
!RELEASE_VERSION.test(String(input.source.releaseVersion)) ||
|
|
420
|
+
!Number.isSafeInteger(input.source.sourceFileCount) || input.source.sourceFileCount < 1 ||
|
|
421
|
+
![input.source.packageSha256, input.source.lockfileSha256, input.source.sourceInventorySha256,
|
|
422
|
+
input.source.sourceFingerprint].every((item) => SHA256.test(String(item))) ||
|
|
423
|
+
Number.isNaN(Date.parse(String(input.build.completedAt))) ||
|
|
424
|
+
![input.build.hostEntry, input.build.handlerEntry, input.build.cockpitIndex].every((item) => path.isAbsolute(String(item))) ||
|
|
425
|
+
!Number.isSafeInteger(input.build.runtimeFileCount) || input.build.runtimeFileCount < 1 ||
|
|
426
|
+
![input.build.hostEntrySha256, input.build.handlerEntrySha256, input.build.cockpitIndexSha256,
|
|
427
|
+
input.build.runtimeInventorySha256, input.build.buildFingerprint]
|
|
428
|
+
.every((item) => SHA256.test(String(item))) ||
|
|
429
|
+
input.server.host !== expectedHost || !validPort(input.server.port) ||
|
|
430
|
+
!['loopback', 'authenticated-network', 'container'].includes(exposure) ||
|
|
431
|
+
!['trusted-local', 'authv2-jwt-session'].includes(authProvider) || !validPort(publicPort) ||
|
|
432
|
+
(exposure === 'container' && input.server.port !== 5900) ||
|
|
433
|
+
(exposure !== 'container' && publicPort !== input.server.port) ||
|
|
434
|
+
(exposure === 'authenticated-network' && authProvider !== 'authv2-jwt-session') ||
|
|
435
|
+
(exposure === 'container' && authProvider !== 'trusted-local') ||
|
|
436
|
+
!cockpit || typeof cockpit !== 'object' || Array.isArray(cockpit) ||
|
|
437
|
+
Object.keys(cockpit).sort().join(',') !== 'host,port' ||
|
|
438
|
+
cockpit.host !== '127.0.0.1' || !validPort(cockpit.port) || cockpit.port === publicPort)
|
|
439
|
+
throw new Error('community_native_state_schema_invalid');
|
|
440
|
+
const buildPathMatchesKnownLayout = [CHECKOUT_BUILD_PATHS, PACKAGE_BUILD_PATHS].some((buildPaths) => samePhysicalPath(input.build.hostEntry, path.join(input.source.root, buildPaths.hostEntry)) &&
|
|
441
|
+
samePhysicalPath(input.build.handlerEntry, path.join(input.source.root, buildPaths.handlerEntry)) &&
|
|
442
|
+
samePhysicalPath(input.build.cockpitIndex, path.join(input.source.root, buildPaths.cockpitIndex)));
|
|
443
|
+
if (!buildPathMatchesKnownLayout)
|
|
444
|
+
throw new Error('community_native_state_build_path_mismatch');
|
|
445
|
+
if (input.profile === 'native-external-postgres') {
|
|
446
|
+
exactKeys(input.postgres, ['mode', 'configRef', 'tlsPolicy'], 'community_native_state_postgres_invalid');
|
|
447
|
+
if (input.postgres.mode !== 'external' || !path.isAbsolute(input.postgres.configRef) ||
|
|
448
|
+
!['disable', 'require', 'verify-full'].includes(input.postgres.tlsPolicy))
|
|
449
|
+
throw new Error('community_native_state_postgres_invalid');
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
const postgres = assertCommunityNativePostgresInstanceState({
|
|
453
|
+
state: assertCommunityNativePostgresState(input.postgres),
|
|
454
|
+
instanceName: String(input.instanceName),
|
|
455
|
+
instanceRoot: paths.instanceRoot,
|
|
456
|
+
});
|
|
457
|
+
if (postgres.mode !== 'container')
|
|
458
|
+
throw new Error('community_native_state_postgres_invalid');
|
|
459
|
+
void buildCommunityNativePostgresUrl(postgres);
|
|
460
|
+
}
|
|
461
|
+
input.server = { ...input.server, exposure, publicPort, authProvider };
|
|
462
|
+
input.cockpit = { host: '127.0.0.1', port: cockpit.port };
|
|
463
|
+
return input;
|
|
464
|
+
}
|
|
465
|
+
function parseNativeProcess(value, paths) {
|
|
466
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
467
|
+
throw new Error('community_native_process_invalid');
|
|
468
|
+
const input = value;
|
|
469
|
+
const required = [
|
|
470
|
+
'schemaVersion', 'protocol', 'instanceName', 'launchId', 'pid', 'mode', 'status', 'startedAt',
|
|
471
|
+
'updatedAt', 'sourceFingerprint', 'identityPath', 'controlPath', 'logPath',
|
|
472
|
+
];
|
|
473
|
+
const optional = ['hostPid', 'exitCode', 'signal', 'failure'];
|
|
474
|
+
const keys = Object.keys(input);
|
|
475
|
+
if (required.some((key) => !keys.includes(key)) || keys.some((key) => !required.includes(key) && !optional.includes(key))) {
|
|
476
|
+
throw new Error('community_native_process_schema_invalid');
|
|
477
|
+
}
|
|
478
|
+
if (input.schemaVersion !== 1 || input.protocol !== COMMUNITY_NATIVE_CHILD_PROTOCOL ||
|
|
479
|
+
!INSTANCE_NAME.test(String(input.instanceName)) || !UUID.test(String(input.launchId)) ||
|
|
480
|
+
!Number.isSafeInteger(input.pid) || input.pid < 1 ||
|
|
481
|
+
(input.hostPid !== undefined && (!Number.isSafeInteger(input.hostPid) || input.hostPid < 1)) ||
|
|
482
|
+
!['foreground', 'detached'].includes(input.mode) ||
|
|
483
|
+
!['starting', 'running', 'exited', 'failed'].includes(input.status) ||
|
|
484
|
+
Number.isNaN(Date.parse(String(input.startedAt))) || Number.isNaN(Date.parse(String(input.updatedAt))) ||
|
|
485
|
+
!SHA256.test(String(input.sourceFingerprint)) || !path.isAbsolute(input.identityPath) ||
|
|
486
|
+
!path.isAbsolute(input.controlPath) ||
|
|
487
|
+
(input.logPath !== null && !path.isAbsolute(input.logPath)))
|
|
488
|
+
throw new Error('community_native_process_schema_invalid');
|
|
489
|
+
const expectedLogPath = input.mode === 'detached' ? paths.logPath : null;
|
|
490
|
+
if (!samePhysicalPath(input.identityPath, paths.identityPath) ||
|
|
491
|
+
!samePhysicalPath(input.controlPath, paths.controlPath) ||
|
|
492
|
+
(expectedLogPath === null ? input.logPath !== null :
|
|
493
|
+
input.logPath === null || !samePhysicalPath(input.logPath, expectedLogPath)))
|
|
494
|
+
throw new Error('community_native_process_path_mismatch');
|
|
495
|
+
return input;
|
|
496
|
+
}
|
|
497
|
+
export function readCommunityNativeState(paths) {
|
|
498
|
+
assertCommunityNativePathLayout(paths, { requireInstanceRoot: true });
|
|
499
|
+
return parseNativeState(readJson(paths.statePath, 'community_native_state_json_invalid'), paths);
|
|
500
|
+
}
|
|
501
|
+
export function readCommunityNativeProcess(paths) {
|
|
502
|
+
assertCommunityNativePathLayout(paths, { requireInstanceRoot: true });
|
|
503
|
+
if (!existsSync(paths.processPath))
|
|
504
|
+
return null;
|
|
505
|
+
return parseNativeProcess(readJson(paths.processPath, 'community_native_process_json_invalid'), paths);
|
|
506
|
+
}
|
|
507
|
+
export function readCommunityNativeMigrationReceipt(paths, state) {
|
|
508
|
+
assertCommunityNativePathLayout(paths, { requireInstanceRoot: true });
|
|
509
|
+
if (!existsSync(paths.migrationReceiptPath))
|
|
510
|
+
return null;
|
|
511
|
+
let receipt;
|
|
512
|
+
try {
|
|
513
|
+
receipt = assertCommunityNativeMigrationReceiptV1(readJson(paths.migrationReceiptPath, 'community_native_migration_receipt_json_invalid'));
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
if (state && (receipt.instanceName !== state.instanceName ||
|
|
519
|
+
receipt.installId !== state.installId ||
|
|
520
|
+
receipt.sourceFingerprint !== state.source.sourceFingerprint)) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
return receipt;
|
|
524
|
+
}
|
|
525
|
+
function assertLegacyCommunityNativeMigrationArtifactsSafe(paths) {
|
|
526
|
+
const legacy = resolveLegacyCommunityNativeMigrationPaths(paths);
|
|
527
|
+
let intentPath = null;
|
|
528
|
+
if (existsSync(legacy.intentPath)) {
|
|
529
|
+
assertPlainCanonicalFile(legacy.intentPath, paths.runtimeRoot, 'community_native_migration_intent_unsafe');
|
|
530
|
+
intentPath = legacy.intentPath;
|
|
531
|
+
}
|
|
532
|
+
let receiptRoot = null;
|
|
533
|
+
if (existsSync(legacy.receiptRoot)) {
|
|
534
|
+
if (!samePhysicalPath(path.dirname(legacy.receiptRoot), paths.runtimeRoot)) {
|
|
535
|
+
throw new Error('community_native_migration_receipt_root_unsafe');
|
|
536
|
+
}
|
|
537
|
+
assertPlainCanonicalDirectory(legacy.receiptRoot, 'community_native_migration_receipt_root_unsafe');
|
|
538
|
+
for (const entry of readdirSync(legacy.receiptRoot, { withFileTypes: true })) {
|
|
539
|
+
const entryPath = path.join(legacy.receiptRoot, entry.name);
|
|
540
|
+
if (!/^[a-f0-9]{64}\.json$/.test(entry.name) || !entry.isFile() || entry.isSymbolicLink()) {
|
|
541
|
+
throw new Error('community_native_migration_history_receipt_unsafe');
|
|
542
|
+
}
|
|
543
|
+
assertPlainCanonicalFile(entryPath, legacy.receiptRoot, 'community_native_migration_history_receipt_unsafe');
|
|
544
|
+
}
|
|
545
|
+
receiptRoot = legacy.receiptRoot;
|
|
546
|
+
}
|
|
547
|
+
return { intentPath, receiptRoot };
|
|
548
|
+
}
|
|
549
|
+
function retireLegacyCommunityNativeMigrationArtifacts(paths) {
|
|
550
|
+
const legacy = assertLegacyCommunityNativeMigrationArtifactsSafe(paths);
|
|
551
|
+
if (legacy.intentPath)
|
|
552
|
+
rmSync(legacy.intentPath, { force: true });
|
|
553
|
+
if (legacy.receiptRoot)
|
|
554
|
+
rmSync(legacy.receiptRoot, { recursive: true, force: true });
|
|
555
|
+
if (legacy.intentPath || legacy.receiptRoot)
|
|
556
|
+
fsyncDirectoryBestEffortOnWindows(paths.runtimeRoot);
|
|
557
|
+
}
|
|
558
|
+
export function persistCommunityNativeMigrationReceipt(params) {
|
|
559
|
+
assertCommunityNativePathLayout(params.paths, { requireInstanceRoot: true });
|
|
560
|
+
if (params.receipt.instanceName !== params.state.instanceName ||
|
|
561
|
+
params.receipt.installId !== params.state.installId ||
|
|
562
|
+
params.receipt.sourceFingerprint !== params.state.source.sourceFingerprint) {
|
|
563
|
+
throw new Error('community_native_migration_receipt_identity_mismatch');
|
|
564
|
+
}
|
|
565
|
+
atomicJsonWrite(params.paths.migrationReceiptPath, params.receipt);
|
|
566
|
+
retireLegacyCommunityNativeMigrationArtifacts(params.paths);
|
|
567
|
+
}
|
|
568
|
+
export function inspectCommunityNativeInstall(input = {}) {
|
|
569
|
+
const paths = resolveCommunityNativePaths(input);
|
|
570
|
+
const candidates = [
|
|
571
|
+
paths.statePath,
|
|
572
|
+
paths.processPath,
|
|
573
|
+
paths.identityPath,
|
|
574
|
+
paths.controlPath,
|
|
575
|
+
paths.secretPath,
|
|
576
|
+
paths.migrationReceiptPath,
|
|
577
|
+
];
|
|
578
|
+
const installationCandidates = candidates.filter((candidate) => candidate !== paths.migrationReceiptPath);
|
|
579
|
+
try {
|
|
580
|
+
assertCommunityNativePathLayout(paths);
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
return {
|
|
584
|
+
status: 'partial',
|
|
585
|
+
paths,
|
|
586
|
+
error: error instanceof Error ? error.message : 'community_native_path_layout_invalid',
|
|
587
|
+
presentFiles: [],
|
|
588
|
+
missingFiles: candidates,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
const presentFiles = candidates.filter(existsSync);
|
|
592
|
+
const missingFiles = candidates.filter((entry) => !existsSync(entry));
|
|
593
|
+
if (existsSync(paths.ociStatePath) && existsSync(paths.statePath)) {
|
|
594
|
+
return { status: 'runtime-conflict', paths, error: 'community_instance_runtime_conflict', presentFiles, missingFiles };
|
|
595
|
+
}
|
|
596
|
+
if (!existsSync(paths.statePath)) {
|
|
597
|
+
if (!installationCandidates.some(existsSync)) {
|
|
598
|
+
return { status: 'not-installed', paths, presentFiles, missingFiles };
|
|
599
|
+
}
|
|
600
|
+
return { status: 'partial', paths, error: 'community_native_state_missing', presentFiles, missingFiles };
|
|
601
|
+
}
|
|
602
|
+
if (!existsSync(paths.secretPath)) {
|
|
603
|
+
return { status: 'partial', paths, error: 'community_native_secret_file_missing', presentFiles, missingFiles };
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
readNativeSecret(paths);
|
|
607
|
+
const state = readCommunityNativeState(paths);
|
|
608
|
+
return {
|
|
609
|
+
status: 'installed',
|
|
610
|
+
paths,
|
|
611
|
+
state,
|
|
612
|
+
process: readCommunityNativeProcess(paths),
|
|
613
|
+
migration: readCommunityNativeMigrationReceipt(paths, state) ?? undefined,
|
|
614
|
+
presentFiles,
|
|
615
|
+
missingFiles,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
return {
|
|
620
|
+
status: 'partial',
|
|
621
|
+
paths,
|
|
622
|
+
error: error instanceof Error ? error.message : 'community_native_state_invalid',
|
|
623
|
+
presentFiles,
|
|
624
|
+
missingFiles,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function sourcePackage(root) {
|
|
629
|
+
const packagePath = safeRegularFile(root, 'package.json');
|
|
630
|
+
try {
|
|
631
|
+
return JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
throw new Error('community_native_source_package_json_invalid');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function sourceLayout(root, manifest = sourcePackage(root)) {
|
|
638
|
+
if (manifest.name === PUBLIC_ROOT_NAME && manifest.private === true &&
|
|
639
|
+
typeof manifest.version === 'string' && RELEASE_VERSION.test(manifest.version) &&
|
|
640
|
+
typeof manifest.packageManager === 'string' && PUBLIC_PACKAGE_MANAGER.test(manifest.packageManager)) {
|
|
641
|
+
return {
|
|
642
|
+
kind: 'checkout',
|
|
643
|
+
requiredSourcePaths: CHECKOUT_REQUIRED_SOURCE_PATHS,
|
|
644
|
+
lockfilePath: 'pnpm-lock.yaml',
|
|
645
|
+
buildPaths: CHECKOUT_BUILD_PATHS,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
if (manifest.name === PUBLIC_SERVER_PACKAGE_NAME && manifest.private !== true &&
|
|
649
|
+
typeof manifest.version === 'string' && RELEASE_VERSION.test(manifest.version) &&
|
|
650
|
+
typeof manifest.packageManager === 'string' && PUBLIC_PACKAGE_MANAGER.test(manifest.packageManager)) {
|
|
651
|
+
return {
|
|
652
|
+
kind: 'npm-package',
|
|
653
|
+
requiredSourcePaths: PACKAGE_REQUIRED_SOURCE_PATHS,
|
|
654
|
+
lockfilePath: 'package.json',
|
|
655
|
+
buildPaths: PACKAGE_BUILD_PATHS,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
throw new Error('community_native_public_source_required');
|
|
659
|
+
}
|
|
660
|
+
function isPackagedCommunityServerSource(source) {
|
|
661
|
+
return sourceLayout(source.root).kind === 'npm-package';
|
|
662
|
+
}
|
|
663
|
+
export function isCommunityNativeNpmPackageSource(sourceRoot) {
|
|
664
|
+
const resolved = path.resolve(sourceRoot);
|
|
665
|
+
return sourceLayout(realpathSync(resolved)).kind === 'npm-package';
|
|
666
|
+
}
|
|
667
|
+
export function resolveCommunityNativeDefaultSourceRoot(fallbackRoot = process.cwd(), moduleUrl = import.meta.url) {
|
|
668
|
+
try {
|
|
669
|
+
const require = createRequire(moduleUrl);
|
|
670
|
+
const packageJsonPath = require.resolve(`${PUBLIC_SERVER_PACKAGE_NAME}/package.json`);
|
|
671
|
+
const packageRoot = path.dirname(packageJsonPath);
|
|
672
|
+
sourceLayout(packageRoot);
|
|
673
|
+
return packageRoot;
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
return path.resolve(fallbackRoot);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
export function inspectCommunityNativeSource(sourceRoot = process.cwd()) {
|
|
680
|
+
const resolved = path.resolve(sourceRoot);
|
|
681
|
+
let rootStats;
|
|
682
|
+
try {
|
|
683
|
+
rootStats = lstatSync(resolved);
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
throw new Error('community_native_source_root_invalid');
|
|
687
|
+
}
|
|
688
|
+
if (!rootStats.isDirectory() || rootStats.isSymbolicLink())
|
|
689
|
+
throw new Error('community_native_source_root_invalid');
|
|
690
|
+
const root = realpathSync(resolved);
|
|
691
|
+
const manifest = sourcePackage(root);
|
|
692
|
+
const layout = sourceLayout(root, manifest);
|
|
693
|
+
for (const relativePath of layout.requiredSourcePaths)
|
|
694
|
+
safeRegularFile(root, relativePath);
|
|
695
|
+
const packageSha256 = hashFile(path.join(root, 'package.json'));
|
|
696
|
+
const lockfileSha256 = hashFile(path.join(root, layout.lockfilePath));
|
|
697
|
+
const postgresProfileSha256 = hashFile(path.join(root, COMMUNITY_NATIVE_POSTGRES_CONTRACT_PATH));
|
|
698
|
+
const sourceInventory = inventoryCommunityNativeTree(root, SOURCE_INVENTORY_EXCLUDED_DIRECTORIES);
|
|
699
|
+
return {
|
|
700
|
+
root,
|
|
701
|
+
packageManager: manifest.packageManager,
|
|
702
|
+
releaseVersion: manifest.version,
|
|
703
|
+
packageSha256,
|
|
704
|
+
lockfileSha256,
|
|
705
|
+
sourceFileCount: sourceInventory.fileCount,
|
|
706
|
+
sourceInventorySha256: sourceInventory.sha256,
|
|
707
|
+
sourceFingerprint: sha256(JSON.stringify({
|
|
708
|
+
root,
|
|
709
|
+
packageManager: manifest.packageManager,
|
|
710
|
+
releaseVersion: manifest.version,
|
|
711
|
+
packageSha256,
|
|
712
|
+
lockfileSha256,
|
|
713
|
+
postgresProfileSha256,
|
|
714
|
+
sourceFileCount: sourceInventory.fileCount,
|
|
715
|
+
sourceInventorySha256: sourceInventory.sha256,
|
|
716
|
+
})),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function inspectCommunityNativeBuild(source, completedAt, currentSource = inspectCommunityNativeSource(source.root)) {
|
|
720
|
+
if (currentSource.packageSha256 !== source.packageSha256 ||
|
|
721
|
+
currentSource.lockfileSha256 !== source.lockfileSha256 ||
|
|
722
|
+
currentSource.sourceFileCount !== source.sourceFileCount ||
|
|
723
|
+
currentSource.sourceInventorySha256 !== source.sourceInventorySha256 ||
|
|
724
|
+
currentSource.sourceFingerprint !== source.sourceFingerprint)
|
|
725
|
+
throw new Error('community_native_source_changed_during_build');
|
|
726
|
+
const layout = sourceLayout(source.root);
|
|
727
|
+
const hostEntry = safeRegularFile(source.root, layout.buildPaths.hostEntry);
|
|
728
|
+
const handlerEntry = safeRegularFile(source.root, layout.buildPaths.handlerEntry);
|
|
729
|
+
const cockpitIndex = safeRegularFile(source.root, layout.buildPaths.cockpitIndex);
|
|
730
|
+
const hostEntrySha256 = hashFile(hostEntry);
|
|
731
|
+
const handlerEntrySha256 = hashFile(handlerEntry);
|
|
732
|
+
const cockpitIndexSha256 = hashFile(cockpitIndex);
|
|
733
|
+
const runtimeInventory = inventoryCommunityNativeTree(source.root, RUNTIME_INVENTORY_EXCLUDED_DIRECTORIES);
|
|
734
|
+
return {
|
|
735
|
+
completedAt,
|
|
736
|
+
hostEntry,
|
|
737
|
+
handlerEntry,
|
|
738
|
+
cockpitIndex,
|
|
739
|
+
hostEntrySha256,
|
|
740
|
+
handlerEntrySha256,
|
|
741
|
+
cockpitIndexSha256,
|
|
742
|
+
runtimeFileCount: runtimeInventory.fileCount,
|
|
743
|
+
runtimeInventorySha256: runtimeInventory.sha256,
|
|
744
|
+
buildFingerprint: sha256(JSON.stringify({
|
|
745
|
+
sourceFingerprint: source.sourceFingerprint,
|
|
746
|
+
hostEntrySha256,
|
|
747
|
+
handlerEntrySha256,
|
|
748
|
+
cockpitIndexSha256,
|
|
749
|
+
runtimeFileCount: runtimeInventory.fileCount,
|
|
750
|
+
runtimeInventorySha256: runtimeInventory.sha256,
|
|
751
|
+
})),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
export function assertCommunityNativeApplicationCurrent(state) {
|
|
755
|
+
const source = inspectCommunityNativeSource(state.source.root);
|
|
756
|
+
const sourceFields = [
|
|
757
|
+
'root', 'packageManager', 'releaseVersion', 'packageSha256', 'lockfileSha256',
|
|
758
|
+
'sourceFileCount', 'sourceInventorySha256', 'sourceFingerprint',
|
|
759
|
+
];
|
|
760
|
+
if (sourceFields.some((field) => source[field] !== state.source[field])) {
|
|
761
|
+
throw new Error('community_native_prior_application_source_drift');
|
|
762
|
+
}
|
|
763
|
+
const build = inspectCommunityNativeBuild(state.source, state.build.completedAt, source);
|
|
764
|
+
const buildFields = [
|
|
765
|
+
'completedAt', 'hostEntry', 'handlerEntry', 'cockpitIndex', 'hostEntrySha256',
|
|
766
|
+
'handlerEntrySha256', 'cockpitIndexSha256', 'runtimeFileCount',
|
|
767
|
+
'runtimeInventorySha256', 'buildFingerprint',
|
|
768
|
+
];
|
|
769
|
+
if (buildFields.some((field) => build[field] !== state.build[field])) {
|
|
770
|
+
throw new Error('community_native_prior_application_build_drift');
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function compareCommunityReleaseVersions(left, right) {
|
|
774
|
+
const parse = (value) => {
|
|
775
|
+
const buildIndex = value.indexOf('+');
|
|
776
|
+
const withoutBuild = buildIndex === -1 ? value : value.slice(0, buildIndex);
|
|
777
|
+
const prereleaseIndex = withoutBuild.indexOf('-');
|
|
778
|
+
const core = prereleaseIndex === -1 ? withoutBuild : withoutBuild.slice(0, prereleaseIndex);
|
|
779
|
+
const prerelease = prereleaseIndex === -1 ? '' : withoutBuild.slice(prereleaseIndex + 1);
|
|
780
|
+
return {
|
|
781
|
+
core: core.split('.').map((part) => BigInt(part)),
|
|
782
|
+
prerelease: prerelease ? prerelease.split('.') : [],
|
|
783
|
+
};
|
|
784
|
+
};
|
|
785
|
+
const a = parse(left);
|
|
786
|
+
const b = parse(right);
|
|
787
|
+
for (let index = 0; index < 3; index += 1) {
|
|
788
|
+
if (a.core[index] !== b.core[index])
|
|
789
|
+
return a.core[index] < b.core[index] ? -1 : 1;
|
|
790
|
+
}
|
|
791
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
792
|
+
if (a.prerelease.length === b.prerelease.length)
|
|
793
|
+
return 0;
|
|
794
|
+
return a.prerelease.length === 0 ? 1 : -1;
|
|
795
|
+
}
|
|
796
|
+
const count = Math.max(a.prerelease.length, b.prerelease.length);
|
|
797
|
+
for (let index = 0; index < count; index += 1) {
|
|
798
|
+
const leftPart = a.prerelease[index];
|
|
799
|
+
const rightPart = b.prerelease[index];
|
|
800
|
+
if (leftPart === undefined || rightPart === undefined)
|
|
801
|
+
return leftPart === undefined ? -1 : 1;
|
|
802
|
+
if (leftPart === rightPart)
|
|
803
|
+
continue;
|
|
804
|
+
const leftNumeric = /^\d+$/.test(leftPart);
|
|
805
|
+
const rightNumeric = /^\d+$/.test(rightPart);
|
|
806
|
+
if (leftNumeric && rightNumeric)
|
|
807
|
+
return BigInt(leftPart) < BigInt(rightPart) ? -1 : 1;
|
|
808
|
+
if (leftNumeric !== rightNumeric)
|
|
809
|
+
return leftNumeric ? -1 : 1;
|
|
810
|
+
return leftPart < rightPart ? -1 : 1;
|
|
811
|
+
}
|
|
812
|
+
return 0;
|
|
813
|
+
}
|
|
814
|
+
export function reconcileCommunityNativePriorApplication(params) {
|
|
815
|
+
const { state, selectedSource, contract } = params;
|
|
816
|
+
if (state.instanceName !== contract.instanceId)
|
|
817
|
+
throw new Error('community_native_instance_mismatch');
|
|
818
|
+
if (state.profile !== contract.profile) {
|
|
819
|
+
throw new Error(`community_native_profile_change_refused:${state.profile}:to:${contract.profile}:run_server_reset`);
|
|
820
|
+
}
|
|
821
|
+
if (state.profile === 'native-external-postgres') {
|
|
822
|
+
if (contract.postgres.mode !== 'external' || !params.selectedExternalConfigRef) {
|
|
823
|
+
throw new Error('community_native_external_postgres_contract_required');
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
else if (contract.postgres.mode !== 'container') {
|
|
827
|
+
throw new Error('community_native_container_postgres_contract_required');
|
|
828
|
+
}
|
|
829
|
+
if (!samePhysicalPath(state.source.root, selectedSource.root)) {
|
|
830
|
+
return 'application-source-adopted';
|
|
831
|
+
}
|
|
832
|
+
try {
|
|
833
|
+
assertCommunityNativeApplicationCurrent(state);
|
|
834
|
+
return 'current';
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
const code = error instanceof Error ? error.message : '';
|
|
838
|
+
if (code !== 'community_native_prior_application_source_drift' &&
|
|
839
|
+
code !== 'community_native_prior_application_build_drift') {
|
|
840
|
+
throw error;
|
|
841
|
+
}
|
|
842
|
+
const selectedLayout = sourceLayout(selectedSource.root);
|
|
843
|
+
if (selectedLayout.kind === 'checkout' && code === 'community_native_prior_application_build_drift' &&
|
|
844
|
+
state.source.sourceFingerprint === selectedSource.sourceFingerprint) {
|
|
845
|
+
return 'same-source-rebuild';
|
|
846
|
+
}
|
|
847
|
+
if (selectedLayout.kind === 'checkout') {
|
|
848
|
+
return 'in-place-checkout-refresh';
|
|
849
|
+
}
|
|
850
|
+
const comparison = compareCommunityReleaseVersions(selectedSource.releaseVersion, state.source.releaseVersion);
|
|
851
|
+
if (comparison < 0) {
|
|
852
|
+
throw new Error(`community_native_application_downgrade_refused:${state.source.releaseVersion}:to:${selectedSource.releaseVersion}`);
|
|
853
|
+
}
|
|
854
|
+
if (comparison > 0)
|
|
855
|
+
return 'application-source-adopted';
|
|
856
|
+
throw error;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function nodePackageIdentity(scriptPath) {
|
|
860
|
+
try {
|
|
861
|
+
const packageJson = JSON.parse(readFileSync(path.resolve(path.dirname(scriptPath), '..', 'package.json'), 'utf8'));
|
|
862
|
+
return typeof packageJson.name === 'string' && typeof packageJson.version === 'string'
|
|
863
|
+
? { name: packageJson.name, version: packageJson.version }
|
|
864
|
+
: null;
|
|
865
|
+
}
|
|
866
|
+
catch {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function sourcePinsPnpm11(cwd) {
|
|
871
|
+
try {
|
|
872
|
+
const manifest = JSON.parse(readFileSync(path.join(path.resolve(cwd), 'package.json'), 'utf8'));
|
|
873
|
+
return typeof manifest.packageManager === 'string' && /^pnpm@11\./.test(manifest.packageManager);
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function pnpmScriptCandidates(env) {
|
|
880
|
+
const candidates = [];
|
|
881
|
+
if (env.npm_execpath)
|
|
882
|
+
candidates.push(path.resolve(env.npm_execpath));
|
|
883
|
+
const roots = [env.PNPM_HOME, ...(String(env.PATH ?? '').split(path.delimiter))]
|
|
884
|
+
.filter((entry) => Boolean(entry?.trim()));
|
|
885
|
+
for (const root of roots) {
|
|
886
|
+
const resolved = path.resolve(root);
|
|
887
|
+
const packageRoots = [
|
|
888
|
+
path.join(resolved, 'node_modules', 'pnpm'),
|
|
889
|
+
path.resolve(resolved, '..', 'lib', 'node_modules', 'pnpm'),
|
|
890
|
+
];
|
|
891
|
+
if (path.basename(resolved).toLowerCase() === '.bin') {
|
|
892
|
+
packageRoots.push(path.resolve(resolved, '..', 'pnpm'));
|
|
893
|
+
}
|
|
894
|
+
for (const packageRoot of packageRoots) {
|
|
895
|
+
candidates.push(path.join(packageRoot, 'bin', 'pnpm.mjs'), path.join(packageRoot, 'bin', 'pnpm.cjs'));
|
|
896
|
+
}
|
|
897
|
+
for (const corepackRoot of [
|
|
898
|
+
path.join(resolved, 'node_modules', 'corepack'),
|
|
899
|
+
path.resolve(resolved, '..', 'lib', 'node_modules', 'corepack'),
|
|
900
|
+
path.resolve(resolved, '..', 'node_modules', 'corepack'),
|
|
901
|
+
]) {
|
|
902
|
+
candidates.push(path.join(corepackRoot, 'dist', 'pnpm.js'));
|
|
903
|
+
}
|
|
904
|
+
if (process.platform !== 'win32') {
|
|
905
|
+
for (const name of ['pnpm', 'pnpm.cjs', 'pnpm.mjs']) {
|
|
906
|
+
const executable = path.join(resolved, name);
|
|
907
|
+
try {
|
|
908
|
+
candidates.push(realpathSync(executable));
|
|
909
|
+
}
|
|
910
|
+
catch { }
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return [...new Set(candidates)];
|
|
915
|
+
}
|
|
916
|
+
export function buildCommunityPnpmInvocation(cwd, args, env = process.env) {
|
|
917
|
+
const pinnedBySource = sourcePinsPnpm11(cwd);
|
|
918
|
+
for (const candidate of pnpmScriptCandidates(env)) {
|
|
919
|
+
if (!existsSync(candidate) || !lstatSync(candidate).isFile())
|
|
920
|
+
continue;
|
|
921
|
+
const identity = nodePackageIdentity(candidate);
|
|
922
|
+
if (identity?.name === 'pnpm' && identity.version.startsWith('11.')) {
|
|
923
|
+
return { command: process.execPath, args: [candidate, ...args], cwd };
|
|
924
|
+
}
|
|
925
|
+
if (identity?.name === 'corepack' && path.basename(candidate) === 'pnpm.js' && pinnedBySource) {
|
|
926
|
+
return { command: process.execPath, args: [candidate, ...args], cwd };
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
throw new Error('community_pnpm11_not_resolvable:run_via_pnpm_11_or_set_PNPM_HOME');
|
|
930
|
+
}
|
|
931
|
+
export function loadExternalPostgresUrl(configRef, tlsPolicy) {
|
|
932
|
+
const resolved = path.resolve(configRef);
|
|
933
|
+
let stats;
|
|
934
|
+
try {
|
|
935
|
+
stats = lstatSync(resolved);
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
throw new Error('community_native_postgres_config_file_invalid');
|
|
939
|
+
}
|
|
940
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.size < 1 || stats.size > MAX_CONFIG_BYTES) {
|
|
941
|
+
throw new Error('community_native_postgres_config_file_invalid');
|
|
942
|
+
}
|
|
943
|
+
let parsed;
|
|
944
|
+
try {
|
|
945
|
+
parsed = parseEnv(readFileSync(resolved, 'utf8'));
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
throw new Error('community_native_postgres_config_parse_failed');
|
|
949
|
+
}
|
|
950
|
+
const value = String(parsed.AOPS_PG_URL ?? '').trim();
|
|
951
|
+
let url;
|
|
952
|
+
try {
|
|
953
|
+
url = new URL(value);
|
|
954
|
+
}
|
|
955
|
+
catch {
|
|
956
|
+
throw new Error('community_native_postgres_url_invalid');
|
|
957
|
+
}
|
|
958
|
+
if ((url.protocol !== 'postgres:' && url.protocol !== 'postgresql:') ||
|
|
959
|
+
!url.hostname || !url.username || !url.password || !url.pathname.slice(1))
|
|
960
|
+
throw new Error('community_native_postgres_url_invalid');
|
|
961
|
+
if (url.search || url.hash)
|
|
962
|
+
throw new Error('community_native_postgres_url_options_refused');
|
|
963
|
+
const rootCertInput = String(parsed.AOPS_PG_SSL_ROOT_CERT ?? '').trim();
|
|
964
|
+
if (tlsPolicy === 'disable') {
|
|
965
|
+
if (rootCertInput)
|
|
966
|
+
throw new Error('community_native_postgres_tls_root_cert_not_allowed');
|
|
967
|
+
url.searchParams.set('sslmode', 'disable');
|
|
968
|
+
return url.toString();
|
|
969
|
+
}
|
|
970
|
+
if (tlsPolicy === 'require') {
|
|
971
|
+
if (rootCertInput)
|
|
972
|
+
throw new Error('community_native_postgres_tls_root_cert_requires_verify_full');
|
|
973
|
+
url.searchParams.set('sslmode', 'require');
|
|
974
|
+
url.searchParams.set('uselibpqcompat', 'true');
|
|
975
|
+
return url.toString();
|
|
976
|
+
}
|
|
977
|
+
if (tlsPolicy !== 'verify-full')
|
|
978
|
+
throw new Error('community_native_postgres_tls_policy_invalid');
|
|
979
|
+
url.searchParams.set('sslmode', 'verify-full');
|
|
980
|
+
if (rootCertInput) {
|
|
981
|
+
if (path.isAbsolute(rootCertInput)) {
|
|
982
|
+
throw new Error('community_native_postgres_tls_root_cert_invalid');
|
|
983
|
+
}
|
|
984
|
+
const rootCertPath = path.resolve(path.dirname(resolved), rootCertInput);
|
|
985
|
+
if (!isWithin(path.dirname(resolved), rootCertPath)) {
|
|
986
|
+
throw new Error('community_native_postgres_tls_root_cert_invalid');
|
|
987
|
+
}
|
|
988
|
+
let rootCertStats;
|
|
989
|
+
try {
|
|
990
|
+
rootCertStats = lstatSync(rootCertPath);
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
throw new Error('community_native_postgres_tls_root_cert_invalid');
|
|
994
|
+
}
|
|
995
|
+
if (!rootCertStats.isFile() || rootCertStats.isSymbolicLink() ||
|
|
996
|
+
rootCertStats.size < 1 || rootCertStats.size > MAX_TLS_ROOT_CERT_BYTES)
|
|
997
|
+
throw new Error('community_native_postgres_tls_root_cert_invalid');
|
|
998
|
+
const realRootCertPath = realpathSync(rootCertPath);
|
|
999
|
+
if (!isWithin(realpathSync(path.dirname(resolved)), realRootCertPath)) {
|
|
1000
|
+
throw new Error('community_native_postgres_tls_root_cert_invalid');
|
|
1001
|
+
}
|
|
1002
|
+
url.searchParams.set('sslrootcert', realRootCertPath);
|
|
1003
|
+
}
|
|
1004
|
+
return url.toString();
|
|
1005
|
+
}
|
|
1006
|
+
export function loadExternalServerAuthConfig(configRef, expectedAuthProvider) {
|
|
1007
|
+
const resolved = path.resolve(configRef);
|
|
1008
|
+
let stats;
|
|
1009
|
+
try {
|
|
1010
|
+
stats = lstatSync(resolved);
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
throw new Error('community_native_postgres_config_file_invalid');
|
|
1014
|
+
}
|
|
1015
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.size < 1 || stats.size > MAX_CONFIG_BYTES) {
|
|
1016
|
+
throw new Error('community_native_postgres_config_file_invalid');
|
|
1017
|
+
}
|
|
1018
|
+
let parsed;
|
|
1019
|
+
try {
|
|
1020
|
+
parsed = parseEnv(readFileSync(resolved, 'utf8'));
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
throw new Error('community_native_postgres_config_parse_failed');
|
|
1024
|
+
}
|
|
1025
|
+
const configuredProvider = String(parsed.AOPS_AUTH_PROVIDER ?? 'trusted-local').trim().toLowerCase();
|
|
1026
|
+
if (configuredProvider === 'authv2-jwt-session' || configuredProvider === 'remote-session') {
|
|
1027
|
+
throw new Error('community_native_legacy_auth_cutover_required:repair_the_private_env_with_trusted-local_and_reconfigure_the_installation');
|
|
1028
|
+
}
|
|
1029
|
+
if (configuredProvider !== 'trusted-local') {
|
|
1030
|
+
throw new Error('community_native_auth_provider_invalid');
|
|
1031
|
+
}
|
|
1032
|
+
if (configuredProvider !== expectedAuthProvider) {
|
|
1033
|
+
throw new Error('community_native_auth_provider_state_mismatch');
|
|
1034
|
+
}
|
|
1035
|
+
return Object.freeze({ authProvider: 'trusted-local' });
|
|
1036
|
+
}
|
|
1037
|
+
async function resolveInstalledMigrationPlanningContext(input, dependencies = {}) {
|
|
1038
|
+
throwIfNativeAborted(input.signal);
|
|
1039
|
+
const inspection = inspectCommunityNativeInstall(input);
|
|
1040
|
+
if (inspection.status !== 'installed' || !inspection.state) {
|
|
1041
|
+
throw new Error(`community_native_migration_plan_requires_install:${inspection.status}:${inspection.error ?? 'run_server_setup'}`);
|
|
1042
|
+
}
|
|
1043
|
+
assertCommunityNativePathLayout(inspection.paths, { requireInstanceRoot: true });
|
|
1044
|
+
const sourceBefore = inspectCommunityNativeSource(inspection.state.source.root);
|
|
1045
|
+
const postgres = inspection.state.postgres;
|
|
1046
|
+
const repoUrl = postgres.mode === 'external'
|
|
1047
|
+
? loadExternalPostgresUrl(postgres.configRef, postgres.tlsPolicy)
|
|
1048
|
+
: (() => {
|
|
1049
|
+
assertCommunityNativePostgresInstanceState({
|
|
1050
|
+
state: postgres,
|
|
1051
|
+
instanceName: inspection.state.instanceName,
|
|
1052
|
+
instanceRoot: inspection.paths.instanceRoot,
|
|
1053
|
+
});
|
|
1054
|
+
return buildCommunityNativePostgresUrl(postgres);
|
|
1055
|
+
})();
|
|
1056
|
+
const result = await (dependencies.planMigration ?? planCommunityNativeMigration)({
|
|
1057
|
+
sourceRoot: inspection.state.source.root,
|
|
1058
|
+
repoUrl,
|
|
1059
|
+
signal: input.signal,
|
|
1060
|
+
});
|
|
1061
|
+
throwIfNativeAborted(input.signal);
|
|
1062
|
+
const sourceAfter = inspectCommunityNativeSource(inspection.state.source.root);
|
|
1063
|
+
if (sourceAfter.sourceFingerprint !== sourceBefore.sourceFingerprint) {
|
|
1064
|
+
throw new Error('community_native_source_drift_during_migration_plan');
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
paths: inspection.paths,
|
|
1068
|
+
state: inspection.state,
|
|
1069
|
+
planning: result.planning,
|
|
1070
|
+
policy: result.policy,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
export async function planCommunityNativeInstalledMigration(input = {}, dependencies = {}) {
|
|
1074
|
+
const context = await resolveInstalledMigrationPlanningContext(input, dependencies);
|
|
1075
|
+
return {
|
|
1076
|
+
schemaVersion: 1,
|
|
1077
|
+
instanceName: context.state.instanceName,
|
|
1078
|
+
profile: context.state.profile,
|
|
1079
|
+
evidencePath: context.planning.requiresSnapshotEvidence
|
|
1080
|
+
? path.join(context.paths.migrationEvidenceRoot, `${context.state.profile === 'native-external-postgres' ? 'external' : 'managed'}-${context.planning.acceptedPlanSha256}.json`)
|
|
1081
|
+
: null,
|
|
1082
|
+
planning: context.planning,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
export async function attestCommunityNativeExternalSnapshot(params, dependencies = {}) {
|
|
1086
|
+
if (!RAW_SHA256.test(params.expectedPlanSha256)) {
|
|
1087
|
+
throw new Error('community_external_snapshot_expected_plan_sha256_invalid');
|
|
1088
|
+
}
|
|
1089
|
+
if (params.preview === true && params.apply === true) {
|
|
1090
|
+
throw new Error('community_external_snapshot_mode_conflict:choose_--preview_or_--apply');
|
|
1091
|
+
}
|
|
1092
|
+
if (params.preview !== true && params.apply !== true) {
|
|
1093
|
+
throw new Error('community_external_snapshot_mode_required:use_--preview_or_--apply');
|
|
1094
|
+
}
|
|
1095
|
+
if (params.apply === true && params.confirmExternalRecoveryOwner !== true) {
|
|
1096
|
+
throw new Error('community_external_snapshot_recovery_owner_confirmation_required');
|
|
1097
|
+
}
|
|
1098
|
+
const context = await resolveInstalledMigrationPlanningContext(params, dependencies);
|
|
1099
|
+
if (context.state.profile !== 'native-external-postgres') {
|
|
1100
|
+
throw new Error('community_external_snapshot_attestation_requires_external_postgres');
|
|
1101
|
+
}
|
|
1102
|
+
if (context.planning.acceptedPlanSha256 !== params.expectedPlanSha256) {
|
|
1103
|
+
throw new Error(`community_external_snapshot_plan_mismatch:expected=${params.expectedPlanSha256}:actual=${context.planning.acceptedPlanSha256}`);
|
|
1104
|
+
}
|
|
1105
|
+
const evidencePath = path.join(context.paths.migrationEvidenceRoot, `external-${context.planning.acceptedPlanSha256}.json`);
|
|
1106
|
+
const createdAt = (dependencies.now ?? (() => new Date()))();
|
|
1107
|
+
const evidenceParams = {
|
|
1108
|
+
planning: context.planning,
|
|
1109
|
+
policy: context.policy,
|
|
1110
|
+
provider: params.provider,
|
|
1111
|
+
snapshotRef: params.snapshotRef,
|
|
1112
|
+
snapshotDigest: params.snapshotDigest ?? null,
|
|
1113
|
+
attestedBy: params.attestedBy,
|
|
1114
|
+
restoreInstructionsRef: params.restoreInstructionsRef,
|
|
1115
|
+
now: () => createdAt,
|
|
1116
|
+
};
|
|
1117
|
+
const evidence = params.apply === true
|
|
1118
|
+
? (dependencies.writeExternalAttestation ?? writeCommunityExternalSnapshotAttestationV1)({
|
|
1119
|
+
...evidenceParams,
|
|
1120
|
+
evidenceRoot: context.paths.migrationEvidenceRoot,
|
|
1121
|
+
}).evidence
|
|
1122
|
+
: createCommunityExternalSnapshotAttestationV1(evidenceParams);
|
|
1123
|
+
return {
|
|
1124
|
+
schemaVersion: 1,
|
|
1125
|
+
instanceName: context.state.instanceName,
|
|
1126
|
+
profile: context.state.profile,
|
|
1127
|
+
applied: params.apply === true,
|
|
1128
|
+
evidencePath,
|
|
1129
|
+
evidence,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function childEnvironment(params) {
|
|
1133
|
+
const env = { ...(params.env ?? process.env) };
|
|
1134
|
+
for (const key of Object.keys(env)) {
|
|
1135
|
+
const upper = key.toUpperCase();
|
|
1136
|
+
if (/^PG[A-Z0-9_]+$/i.test(key) ||
|
|
1137
|
+
/(?:^|_)(?:AUTH|COOKIE|DATABASE|ENCRYPT|JWT|PASSWORD|PG|POSTGRES|REPO|SECRET|SESSION|SQLITE|TOKEN)(?:_|$)/i.test(key) ||
|
|
1138
|
+
/^(?:AOPS|PROJECTMAN|DOCMAN|AGENTSPACE|CHATV3|AUTHV2|KANBAN|SPRINT|TASKER|FILEMAN|SYS)_/i.test(key) ||
|
|
1139
|
+
/_(?:REPO|PG|SQLITE)_URL$/i.test(key) ||
|
|
1140
|
+
/^NODE_/i.test(key) || /^DYLD_/i.test(key) ||
|
|
1141
|
+
['DATABASE_URL', 'DEV_PG_URL', 'HOST', 'PORT', 'ORIGIN', 'LD_PRELOAD', 'LD_LIBRARY_PATH',
|
|
1142
|
+
'SSLKEYLOGFILE', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
|
|
1143
|
+
'ADDRESS_HEADER', 'XFF_DEPTH', 'PROTOCOL_HEADER', 'HOST_HEADER'].includes(upper))
|
|
1144
|
+
delete env[key];
|
|
1145
|
+
}
|
|
1146
|
+
const child = {
|
|
1147
|
+
...env,
|
|
1148
|
+
NODE_ENV: 'production',
|
|
1149
|
+
AOPS_DB_BOOTSTRAP_MODE: 'explicit',
|
|
1150
|
+
AOPS_PG_URL: params.postgresUrl,
|
|
1151
|
+
AOPS_RELEASE_VERSION: params.state.source.releaseVersion,
|
|
1152
|
+
CHATV3_SERVER_KEY_ID: 'k1',
|
|
1153
|
+
CHATV3_SERVER_KEY_SECRET: params.chatv3ServerKeySecret,
|
|
1154
|
+
AOPS_NATIVE_LAUNCH_ID: params.launchId,
|
|
1155
|
+
AOPS_NATIVE_STARTED_AT: params.startedAt,
|
|
1156
|
+
AOPS_NATIVE_SOURCE_FINGERPRINT: params.state.source.sourceFingerprint,
|
|
1157
|
+
AOPS_NATIVE_HOST_ENTRY: params.state.build.hostEntry,
|
|
1158
|
+
AOPS_NATIVE_IDENTITY_PATH: params.paths.identityPath,
|
|
1159
|
+
AOPS_NATIVE_CONTROL_PATH: params.paths.controlPath,
|
|
1160
|
+
AOPS_NATIVE_PORT: String(params.state.server.port),
|
|
1161
|
+
COMMUNITY_PORT: String(params.state.server.port),
|
|
1162
|
+
AOPS_COMMUNITY_HOST_MODE: params.state.server.exposure,
|
|
1163
|
+
AOPS_COMMUNITY_PUBLIC_PORT: String(params.state.server.publicPort),
|
|
1164
|
+
AOPS_COMMUNITY_COCKPIT_PORT: String(params.state.cockpit.port),
|
|
1165
|
+
AOPS_AUTH_PROVIDER: params.authConfig.authProvider,
|
|
1166
|
+
};
|
|
1167
|
+
return child;
|
|
1168
|
+
}
|
|
1169
|
+
function readNativeSecret(paths) {
|
|
1170
|
+
try {
|
|
1171
|
+
const stats = lstatSync(paths.secretPath);
|
|
1172
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.size < 1 || stats.size > 4_096) {
|
|
1173
|
+
throw new Error('invalid');
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
throw new Error('community_native_secret_file_invalid');
|
|
1178
|
+
}
|
|
1179
|
+
let parsed;
|
|
1180
|
+
try {
|
|
1181
|
+
parsed = parseEnv(readFileSync(paths.secretPath, 'utf8'));
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
throw new Error('community_native_secret_file_invalid');
|
|
1185
|
+
}
|
|
1186
|
+
const secret = String(parsed.CHATV3_SERVER_KEY_SECRET ?? '').trim();
|
|
1187
|
+
if (parsed.CHATV3_SERVER_KEY_ID !== 'k1' || !/^[A-Za-z0-9_-]{32,}$/.test(secret) ||
|
|
1188
|
+
Object.keys(parsed).some((key) => !['CHATV3_SERVER_KEY_ID', 'CHATV3_SERVER_KEY_SECRET'].includes(key)))
|
|
1189
|
+
throw new Error('community_native_secret_file_invalid');
|
|
1190
|
+
return secret;
|
|
1191
|
+
}
|
|
1192
|
+
function ensureNativeSecret(paths, createSecret) {
|
|
1193
|
+
if (existsSync(paths.secretPath))
|
|
1194
|
+
return readNativeSecret(paths);
|
|
1195
|
+
const secret = createSecret();
|
|
1196
|
+
if (!/^[A-Za-z0-9_-]{32,}$/.test(secret))
|
|
1197
|
+
throw new Error('community_native_generated_secret_invalid');
|
|
1198
|
+
mkdirSync(path.dirname(paths.secretPath), { recursive: true, mode: 0o700 });
|
|
1199
|
+
writeFileSync(paths.secretPath, `CHATV3_SERVER_KEY_ID=k1\nCHATV3_SERVER_KEY_SECRET=${secret}\n`, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
1200
|
+
return secret;
|
|
1201
|
+
}
|
|
1202
|
+
function parseChildIdentity(paths, processRecord) {
|
|
1203
|
+
const value = readJson(paths.identityPath, 'community_native_child_identity_json_invalid');
|
|
1204
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1205
|
+
throw new Error('community_native_child_identity_invalid');
|
|
1206
|
+
exactKeys(value, [
|
|
1207
|
+
'schemaVersion', 'protocol', 'launchId', 'pid', 'hostPid', 'startedAt',
|
|
1208
|
+
'sourceFingerprint', 'hostEntry', 'controlPath', 'port',
|
|
1209
|
+
], 'community_native_child_identity_schema_invalid');
|
|
1210
|
+
const identity = value;
|
|
1211
|
+
if (identity.schemaVersion !== 1 || identity.protocol !== COMMUNITY_NATIVE_CHILD_PROTOCOL ||
|
|
1212
|
+
identity.launchId !== processRecord.launchId || identity.pid !== processRecord.pid ||
|
|
1213
|
+
!Number.isSafeInteger(identity.hostPid) || identity.hostPid < 1 ||
|
|
1214
|
+
identity.startedAt !== processRecord.startedAt || identity.sourceFingerprint !== processRecord.sourceFingerprint ||
|
|
1215
|
+
path.resolve(identity.controlPath) !== path.resolve(paths.controlPath) ||
|
|
1216
|
+
path.resolve(processRecord.controlPath) !== path.resolve(paths.controlPath))
|
|
1217
|
+
throw new Error('community_native_child_identity_mismatch');
|
|
1218
|
+
return identity;
|
|
1219
|
+
}
|
|
1220
|
+
function removeIdentityIfExact(paths, processRecord) {
|
|
1221
|
+
if (!existsSync(paths.identityPath))
|
|
1222
|
+
return;
|
|
1223
|
+
parseChildIdentity(paths, processRecord);
|
|
1224
|
+
rmSync(paths.identityPath, { force: true });
|
|
1225
|
+
}
|
|
1226
|
+
async function stopSpawnedNative(params) {
|
|
1227
|
+
writeStopRequest(params.paths, params.processRecord.launchId, params.now);
|
|
1228
|
+
let identity = null;
|
|
1229
|
+
const observe = () => {
|
|
1230
|
+
if (existsSync(params.paths.identityPath)) {
|
|
1231
|
+
const current = parseChildIdentity(params.paths, params.processRecord);
|
|
1232
|
+
if (current.hostEntry !== params.state.build.hostEntry || current.port !== params.state.server.port ||
|
|
1233
|
+
current.sourceFingerprint !== params.state.source.sourceFingerprint)
|
|
1234
|
+
throw new Error('community_native_child_identity_mismatch');
|
|
1235
|
+
identity = current;
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
supervisorAlive: params.runtime.processExists(params.processRecord.pid),
|
|
1239
|
+
hostAlive: identity ? params.runtime.processExists(identity.hostPid) : false,
|
|
1240
|
+
};
|
|
1241
|
+
};
|
|
1242
|
+
const pollCount = Math.max(1, Math.ceil(params.timeoutMs / READY_POLL_MS));
|
|
1243
|
+
for (let attempt = 0; attempt < pollCount; attempt += 1) {
|
|
1244
|
+
const live = observe();
|
|
1245
|
+
if (!live.supervisorAlive && !live.hostAlive) {
|
|
1246
|
+
removeIdentityIfExact(params.paths, params.processRecord);
|
|
1247
|
+
removeControlIfExact(params.paths, params.processRecord.launchId);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
await params.runtime.sleep(READY_POLL_MS);
|
|
1251
|
+
}
|
|
1252
|
+
const live = observe();
|
|
1253
|
+
if (live.hostAlive) {
|
|
1254
|
+
throw new Error('community_native_child_cleanup_timeout:verified_host_left_running');
|
|
1255
|
+
}
|
|
1256
|
+
if (live.supervisorAlive)
|
|
1257
|
+
await params.handle.terminate();
|
|
1258
|
+
if (params.runtime.processExists(params.processRecord.pid)) {
|
|
1259
|
+
throw new Error('community_native_child_cleanup_timeout:supervisor_still_running');
|
|
1260
|
+
}
|
|
1261
|
+
removeIdentityIfExact(params.paths, params.processRecord);
|
|
1262
|
+
removeControlIfExact(params.paths, params.processRecord.launchId);
|
|
1263
|
+
}
|
|
1264
|
+
async function waitUntilReady(params) {
|
|
1265
|
+
const deadline = Date.now() + params.timeoutMs;
|
|
1266
|
+
const childState = {};
|
|
1267
|
+
void params.handle.wait.then((value) => { childState.exit = value; }, (error) => { childState.failure = error; });
|
|
1268
|
+
try {
|
|
1269
|
+
while (Date.now() < deadline) {
|
|
1270
|
+
throwIfNativeAborted(params.signal);
|
|
1271
|
+
if (childState.failure)
|
|
1272
|
+
throw new Error('community_native_child_launch_failed');
|
|
1273
|
+
if (childState.exit) {
|
|
1274
|
+
throw new Error(`community_native_child_exited_before_ready:${childState.exit.exitCode ?? 'signal'}:${childState.exit.signal ?? 'none'}`);
|
|
1275
|
+
}
|
|
1276
|
+
if (existsSync(params.paths.identityPath)) {
|
|
1277
|
+
try {
|
|
1278
|
+
const identity = parseChildIdentity(params.paths, params.processRecord);
|
|
1279
|
+
if (identity.hostEntry !== params.state.build.hostEntry || identity.port !== params.state.server.port ||
|
|
1280
|
+
identity.sourceFingerprint !== params.state.source.sourceFingerprint)
|
|
1281
|
+
throw new Error('community_native_child_identity_mismatch');
|
|
1282
|
+
const healthy = await params.runtime.health(`http://127.0.0.1:${params.state.server.port}/api/health`, params.signal);
|
|
1283
|
+
throwIfNativeAborted(params.signal);
|
|
1284
|
+
if (healthy)
|
|
1285
|
+
return identity;
|
|
1286
|
+
}
|
|
1287
|
+
catch (error) {
|
|
1288
|
+
if (params.signal?.aborted)
|
|
1289
|
+
throw new Error('community_operation_aborted');
|
|
1290
|
+
if (error instanceof Error && /identity_mismatch/.test(error.message))
|
|
1291
|
+
throw error;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
await params.runtime.sleep(READY_POLL_MS, params.signal);
|
|
1295
|
+
throwIfNativeAborted(params.signal);
|
|
1296
|
+
}
|
|
1297
|
+
throw new Error('community_native_child_ready_timeout');
|
|
1298
|
+
}
|
|
1299
|
+
catch (error) {
|
|
1300
|
+
await stopSpawnedNative({ ...params, timeoutMs: DEFAULT_STOP_TIMEOUT_MS });
|
|
1301
|
+
throw error;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
function writeProcess(paths, record) {
|
|
1305
|
+
assertCommunityNativePathLayout(paths, { requireInstanceRoot: true });
|
|
1306
|
+
atomicJsonWrite(paths.processPath, record);
|
|
1307
|
+
}
|
|
1308
|
+
export function resolveCommunityNativeChildEntry(moduleUrl = import.meta.url) {
|
|
1309
|
+
const moduleDirectory = path.dirname(fileURLToPath(moduleUrl));
|
|
1310
|
+
return path.join(path.basename(moduleDirectory) === 'launchers' ? path.dirname(moduleDirectory) : moduleDirectory, 'community-native-child.js');
|
|
1311
|
+
}
|
|
1312
|
+
function defaultRunBuild(invocation) {
|
|
1313
|
+
return new Promise((resolve, reject) => {
|
|
1314
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
1315
|
+
cwd: invocation.cwd,
|
|
1316
|
+
env: process.env,
|
|
1317
|
+
shell: false,
|
|
1318
|
+
windowsHide: true,
|
|
1319
|
+
stdio: 'inherit',
|
|
1320
|
+
signal: invocation.signal,
|
|
1321
|
+
});
|
|
1322
|
+
child.once('error', reject);
|
|
1323
|
+
child.once('exit', (exitCode, signal) => resolve({ exitCode, signal }));
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
async function defaultSpawnChild(params) {
|
|
1327
|
+
mkdirSync(path.dirname(params.logPath), { recursive: true, mode: 0o700 });
|
|
1328
|
+
const logRootStats = lstatSync(path.dirname(params.logPath));
|
|
1329
|
+
if (!logRootStats.isDirectory() || logRootStats.isSymbolicLink()) {
|
|
1330
|
+
throw new Error('community_native_log_root_invalid');
|
|
1331
|
+
}
|
|
1332
|
+
if (existsSync(params.logPath)) {
|
|
1333
|
+
const logStats = lstatSync(params.logPath);
|
|
1334
|
+
if (!logStats.isFile() || logStats.isSymbolicLink())
|
|
1335
|
+
throw new Error('community_native_log_file_invalid');
|
|
1336
|
+
}
|
|
1337
|
+
let logFd;
|
|
1338
|
+
if (params.mode === 'detached')
|
|
1339
|
+
logFd = openSync(params.logPath, 'a', 0o600);
|
|
1340
|
+
const child = spawn(process.execPath, [params.childEntry], {
|
|
1341
|
+
cwd: params.cwd,
|
|
1342
|
+
env: params.env,
|
|
1343
|
+
detached: params.mode === 'detached',
|
|
1344
|
+
windowsHide: params.mode === 'detached',
|
|
1345
|
+
stdio: params.mode === 'foreground' ? 'inherit' : ['ignore', logFd, logFd],
|
|
1346
|
+
});
|
|
1347
|
+
try {
|
|
1348
|
+
await new Promise((resolve, reject) => {
|
|
1349
|
+
child.once('spawn', resolve);
|
|
1350
|
+
child.once('error', reject);
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
finally {
|
|
1354
|
+
if (logFd !== undefined)
|
|
1355
|
+
closeSync(logFd);
|
|
1356
|
+
}
|
|
1357
|
+
if (!child.pid)
|
|
1358
|
+
throw new Error('community_native_child_pid_missing');
|
|
1359
|
+
const wait = new Promise((resolve, reject) => {
|
|
1360
|
+
child.once('error', reject);
|
|
1361
|
+
child.once('exit', (exitCode, signal) => resolve({ exitCode, signal }));
|
|
1362
|
+
});
|
|
1363
|
+
let termination;
|
|
1364
|
+
return {
|
|
1365
|
+
pid: child.pid,
|
|
1366
|
+
wait,
|
|
1367
|
+
detach: () => child.unref(),
|
|
1368
|
+
terminate: () => {
|
|
1369
|
+
termination ??= (async () => {
|
|
1370
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
1371
|
+
return;
|
|
1372
|
+
try {
|
|
1373
|
+
child.kill('SIGTERM');
|
|
1374
|
+
}
|
|
1375
|
+
catch { }
|
|
1376
|
+
let forceTimer;
|
|
1377
|
+
await Promise.race([
|
|
1378
|
+
wait.then(() => undefined),
|
|
1379
|
+
new Promise((resolve) => {
|
|
1380
|
+
forceTimer = setTimeout(() => {
|
|
1381
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
1382
|
+
try {
|
|
1383
|
+
child.kill('SIGKILL');
|
|
1384
|
+
}
|
|
1385
|
+
catch { }
|
|
1386
|
+
}
|
|
1387
|
+
resolve();
|
|
1388
|
+
}, 5_000);
|
|
1389
|
+
}),
|
|
1390
|
+
]);
|
|
1391
|
+
if (forceTimer)
|
|
1392
|
+
clearTimeout(forceTimer);
|
|
1393
|
+
await wait.then(() => undefined);
|
|
1394
|
+
})();
|
|
1395
|
+
return termination;
|
|
1396
|
+
},
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
export const communityNativeRuntime = {
|
|
1400
|
+
runBuild: defaultRunBuild,
|
|
1401
|
+
migrate: runCommunityNativeMigration,
|
|
1402
|
+
spawnChild: defaultSpawnChild,
|
|
1403
|
+
async health(url, signal) {
|
|
1404
|
+
try {
|
|
1405
|
+
const timeout = AbortSignal.timeout(2_000);
|
|
1406
|
+
const response = await fetch(url, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
|
|
1407
|
+
return response.ok;
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
if (signal?.aborted)
|
|
1411
|
+
throw new Error('community_operation_aborted');
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
},
|
|
1415
|
+
processExists(pid) {
|
|
1416
|
+
try {
|
|
1417
|
+
process.kill(pid, 0);
|
|
1418
|
+
return true;
|
|
1419
|
+
}
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'EPERM');
|
|
1422
|
+
}
|
|
1423
|
+
},
|
|
1424
|
+
sleep: (milliseconds, signal) => new Promise((resolve, reject) => {
|
|
1425
|
+
if (signal?.aborted) {
|
|
1426
|
+
reject(new Error('community_operation_aborted'));
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const timer = setTimeout(() => {
|
|
1430
|
+
signal?.removeEventListener('abort', onAbort);
|
|
1431
|
+
resolve();
|
|
1432
|
+
}, milliseconds);
|
|
1433
|
+
const onAbort = () => {
|
|
1434
|
+
clearTimeout(timer);
|
|
1435
|
+
signal?.removeEventListener('abort', onAbort);
|
|
1436
|
+
reject(new Error('community_operation_aborted'));
|
|
1437
|
+
};
|
|
1438
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1439
|
+
}),
|
|
1440
|
+
};
|
|
1441
|
+
function parseControlRequest(paths) {
|
|
1442
|
+
const value = readJson(paths.controlPath, 'community_native_control_json_invalid');
|
|
1443
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1444
|
+
throw new Error('community_native_control_invalid');
|
|
1445
|
+
exactKeys(value, [
|
|
1446
|
+
'schemaVersion', 'protocol', 'launchId', 'command', 'requestedAt',
|
|
1447
|
+
], 'community_native_control_schema_invalid');
|
|
1448
|
+
const request = value;
|
|
1449
|
+
if (request.schemaVersion !== 1 || request.protocol !== COMMUNITY_NATIVE_CONTROL_PROTOCOL ||
|
|
1450
|
+
!UUID.test(request.launchId) || request.command !== 'stop' ||
|
|
1451
|
+
Number.isNaN(Date.parse(request.requestedAt)))
|
|
1452
|
+
throw new Error('community_native_control_schema_invalid');
|
|
1453
|
+
return request;
|
|
1454
|
+
}
|
|
1455
|
+
function removeControlIfExact(paths, launchId) {
|
|
1456
|
+
if (!existsSync(paths.controlPath))
|
|
1457
|
+
return;
|
|
1458
|
+
const request = parseControlRequest(paths);
|
|
1459
|
+
if (request.launchId !== launchId)
|
|
1460
|
+
throw new Error('community_native_control_launch_mismatch');
|
|
1461
|
+
rmSync(paths.controlPath, { force: true });
|
|
1462
|
+
}
|
|
1463
|
+
async function observeInstalledNative(params) {
|
|
1464
|
+
const record = params.processRecord;
|
|
1465
|
+
const identityPresent = existsSync(params.paths.identityPath);
|
|
1466
|
+
if (!record) {
|
|
1467
|
+
return {
|
|
1468
|
+
runtimeState: identityPresent ? 'identity-conflict' : 'stopped',
|
|
1469
|
+
recoverable: !identityPresent,
|
|
1470
|
+
process: null,
|
|
1471
|
+
identity: null,
|
|
1472
|
+
supervisorAlive: false,
|
|
1473
|
+
hostAlive: false,
|
|
1474
|
+
health: 'not-checked',
|
|
1475
|
+
reason: identityPresent ? 'community_native_identity_without_process_record' : null,
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
const supervisorAlive = params.runtime.processExists(record.pid);
|
|
1479
|
+
let identity = null;
|
|
1480
|
+
if (identityPresent) {
|
|
1481
|
+
try {
|
|
1482
|
+
identity = parseChildIdentity(params.paths, record);
|
|
1483
|
+
}
|
|
1484
|
+
catch (error) {
|
|
1485
|
+
return {
|
|
1486
|
+
runtimeState: 'identity-conflict',
|
|
1487
|
+
recoverable: false,
|
|
1488
|
+
process: record,
|
|
1489
|
+
identity: null,
|
|
1490
|
+
supervisorAlive,
|
|
1491
|
+
hostAlive: record.hostPid ? params.runtime.processExists(record.hostPid) : false,
|
|
1492
|
+
health: 'not-checked',
|
|
1493
|
+
reason: error instanceof Error ? error.message : 'community_native_child_identity_invalid',
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
const hostPid = identity?.hostPid ?? record.hostPid;
|
|
1498
|
+
const hostAlive = hostPid ? params.runtime.processExists(hostPid) : false;
|
|
1499
|
+
const activeRecord = record.status === 'starting' || record.status === 'running';
|
|
1500
|
+
if (!activeRecord && !identityPresent && !supervisorAlive && !hostAlive &&
|
|
1501
|
+
record.instanceName === params.state.instanceName) {
|
|
1502
|
+
return {
|
|
1503
|
+
runtimeState: 'stopped', recoverable: true, process: record, identity: null,
|
|
1504
|
+
supervisorAlive: false, hostAlive: false, health: 'not-checked', reason: null,
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
if (record.instanceName !== params.state.instanceName ||
|
|
1508
|
+
record.sourceFingerprint !== params.state.source.sourceFingerprint ||
|
|
1509
|
+
(identity && (identity.sourceFingerprint !== params.state.source.sourceFingerprint ||
|
|
1510
|
+
!samePhysicalPath(identity.hostEntry, params.state.build.hostEntry) ||
|
|
1511
|
+
identity.port !== params.state.server.port))) {
|
|
1512
|
+
return {
|
|
1513
|
+
runtimeState: 'identity-conflict', recoverable: false, process: record, identity,
|
|
1514
|
+
supervisorAlive, hostAlive, health: 'not-checked',
|
|
1515
|
+
reason: 'community_native_process_state_identity_mismatch',
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
if (!activeRecord) {
|
|
1519
|
+
if (!identityPresent && !supervisorAlive && !hostAlive) {
|
|
1520
|
+
return {
|
|
1521
|
+
runtimeState: 'stopped', recoverable: true, process: record, identity: null,
|
|
1522
|
+
supervisorAlive: false, hostAlive: false, health: 'not-checked', reason: null,
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
if (identity && !supervisorAlive && !hostAlive) {
|
|
1526
|
+
return {
|
|
1527
|
+
runtimeState: 'stopped', recoverable: true, process: record, identity,
|
|
1528
|
+
supervisorAlive: false, hostAlive: false, health: 'not-checked',
|
|
1529
|
+
reason: 'community_native_terminal_identity_stale',
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
return {
|
|
1533
|
+
runtimeState: 'identity-conflict', recoverable: false, process: record, identity,
|
|
1534
|
+
supervisorAlive, hostAlive, health: 'not-checked',
|
|
1535
|
+
reason: 'community_native_terminal_record_with_live_or_unowned_process',
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
if (!identity) {
|
|
1539
|
+
if (!supervisorAlive && !hostAlive) {
|
|
1540
|
+
return {
|
|
1541
|
+
runtimeState: 'crashed', recoverable: true, process: record, identity: null,
|
|
1542
|
+
supervisorAlive: false, hostAlive: false, health: 'not-checked',
|
|
1543
|
+
reason: 'community_native_process_disappeared_without_identity',
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
return {
|
|
1547
|
+
runtimeState: 'identity-conflict', recoverable: false, process: record, identity: null,
|
|
1548
|
+
supervisorAlive, hostAlive, health: 'not-checked',
|
|
1549
|
+
reason: 'community_native_live_process_without_matching_identity',
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
if (!supervisorAlive && !hostAlive) {
|
|
1553
|
+
return {
|
|
1554
|
+
runtimeState: 'crashed', recoverable: true, process: record, identity,
|
|
1555
|
+
supervisorAlive: false, hostAlive: false, health: 'not-checked',
|
|
1556
|
+
reason: 'community_native_process_disappeared',
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
if (!supervisorAlive && hostAlive) {
|
|
1560
|
+
return {
|
|
1561
|
+
runtimeState: 'orphaned', recoverable: false, process: record, identity,
|
|
1562
|
+
supervisorAlive, hostAlive, health: 'not-checked', reason: 'community_native_host_orphaned',
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
if (supervisorAlive && !hostAlive) {
|
|
1566
|
+
return {
|
|
1567
|
+
runtimeState: 'unhealthy', recoverable: false, process: record, identity,
|
|
1568
|
+
supervisorAlive, hostAlive, health: 'unhealthy', reason: 'community_native_host_process_missing',
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
const healthy = await params.runtime.health(`http://127.0.0.1:${params.state.server.port}/api/health`);
|
|
1572
|
+
return {
|
|
1573
|
+
runtimeState: healthy ? 'running' : record.status === 'starting' ? 'starting' : 'unhealthy',
|
|
1574
|
+
recoverable: false, process: record, identity,
|
|
1575
|
+
supervisorAlive, hostAlive, health: healthy ? 'healthy' : 'unhealthy',
|
|
1576
|
+
reason: healthy ? null : 'community_native_health_failed',
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
export async function inspectCommunityNativeRuntime(params = {}) {
|
|
1580
|
+
const inspection = inspectCommunityNativeInstall({ instanceName: params.instanceName, dataRoot: params.dataRoot });
|
|
1581
|
+
if (inspection.status !== 'installed' || !inspection.state) {
|
|
1582
|
+
throw new Error(`community_native_not_installed:${inspection.status}:${inspection.error ?? 'run_server_setup'}`);
|
|
1583
|
+
}
|
|
1584
|
+
return observeInstalledNative({
|
|
1585
|
+
paths: inspection.paths,
|
|
1586
|
+
state: inspection.state,
|
|
1587
|
+
processRecord: inspection.process ?? null,
|
|
1588
|
+
runtime: params.runtime ?? communityNativeRuntime,
|
|
1589
|
+
now: params.now ?? (() => new Date()),
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
function writeStopRequest(paths, launchId, now) {
|
|
1593
|
+
if (existsSync(paths.controlPath)) {
|
|
1594
|
+
const existing = parseControlRequest(paths);
|
|
1595
|
+
if (existing.launchId === launchId)
|
|
1596
|
+
return;
|
|
1597
|
+
throw new Error('community_native_control_launch_mismatch');
|
|
1598
|
+
}
|
|
1599
|
+
atomicJsonWrite(paths.controlPath, {
|
|
1600
|
+
schemaVersion: 1,
|
|
1601
|
+
protocol: COMMUNITY_NATIVE_CONTROL_PROTOCOL,
|
|
1602
|
+
launchId,
|
|
1603
|
+
command: 'stop',
|
|
1604
|
+
requestedAt: now().toISOString(),
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
export async function stopCommunityNativeInstall(params = {}) {
|
|
1608
|
+
throwIfNativeAborted(params.signal);
|
|
1609
|
+
const inspection = inspectCommunityNativeInstall({ instanceName: params.instanceName, dataRoot: params.dataRoot });
|
|
1610
|
+
if (inspection.status !== 'installed' || !inspection.state) {
|
|
1611
|
+
throw new Error(`community_native_not_installed:${inspection.status}:${inspection.error ?? 'run_server_setup'}`);
|
|
1612
|
+
}
|
|
1613
|
+
const runtime = params.runtime ?? communityNativeRuntime;
|
|
1614
|
+
const now = params.now ?? (() => new Date());
|
|
1615
|
+
const observed = await observeInstalledNative({
|
|
1616
|
+
paths: inspection.paths,
|
|
1617
|
+
state: inspection.state,
|
|
1618
|
+
processRecord: inspection.process ?? null,
|
|
1619
|
+
runtime,
|
|
1620
|
+
now,
|
|
1621
|
+
});
|
|
1622
|
+
if (observed.runtimeState === 'stopped' || observed.runtimeState === 'crashed') {
|
|
1623
|
+
if (inspection.state.postgres.mode === 'container') {
|
|
1624
|
+
await stopCommunityNativePostgres({
|
|
1625
|
+
state: inspection.state.postgres,
|
|
1626
|
+
instanceName: inspection.state.instanceName,
|
|
1627
|
+
instanceRoot: inspection.paths.instanceRoot,
|
|
1628
|
+
runtime: params.postgresRuntime,
|
|
1629
|
+
signal: params.signal,
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
return { status: 'already-stopped', state: inspection.state, process: observed.process };
|
|
1633
|
+
}
|
|
1634
|
+
if (!observed.process || !observed.identity || !observed.supervisorAlive ||
|
|
1635
|
+
!['starting', 'running', 'unhealthy'].includes(observed.runtimeState)) {
|
|
1636
|
+
throw new Error(`community_native_stop_refused:${observed.runtimeState}:${observed.reason ?? 'identity_not_verified'}`);
|
|
1637
|
+
}
|
|
1638
|
+
writeStopRequest(inspection.paths, observed.process.launchId, now);
|
|
1639
|
+
const deadline = Date.now() + (params.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS);
|
|
1640
|
+
while (Date.now() < deadline) {
|
|
1641
|
+
throwIfNativeAborted(params.signal);
|
|
1642
|
+
if (existsSync(inspection.paths.identityPath))
|
|
1643
|
+
parseChildIdentity(inspection.paths, observed.process);
|
|
1644
|
+
const supervisorAlive = runtime.processExists(observed.process.pid);
|
|
1645
|
+
const hostAlive = runtime.processExists(observed.identity.hostPid);
|
|
1646
|
+
if (!supervisorAlive && !hostAlive && !existsSync(inspection.paths.identityPath)) {
|
|
1647
|
+
removeControlIfExact(inspection.paths, observed.process.launchId);
|
|
1648
|
+
const stopped = {
|
|
1649
|
+
...observed.process,
|
|
1650
|
+
status: 'exited',
|
|
1651
|
+
updatedAt: now().toISOString(),
|
|
1652
|
+
exitCode: 0,
|
|
1653
|
+
signal: null,
|
|
1654
|
+
};
|
|
1655
|
+
delete stopped.failure;
|
|
1656
|
+
writeProcess(inspection.paths, stopped);
|
|
1657
|
+
if (inspection.state.postgres.mode === 'container') {
|
|
1658
|
+
await stopCommunityNativePostgres({
|
|
1659
|
+
state: inspection.state.postgres,
|
|
1660
|
+
instanceName: inspection.state.instanceName,
|
|
1661
|
+
instanceRoot: inspection.paths.instanceRoot,
|
|
1662
|
+
runtime: params.postgresRuntime,
|
|
1663
|
+
signal: params.signal,
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
return { status: 'stopped', state: inspection.state, process: stopped };
|
|
1667
|
+
}
|
|
1668
|
+
await runtime.sleep(READY_POLL_MS, params.signal);
|
|
1669
|
+
throwIfNativeAborted(params.signal);
|
|
1670
|
+
}
|
|
1671
|
+
throw new Error('community_native_stop_timeout:no_unverified_pid_kill');
|
|
1672
|
+
}
|
|
1673
|
+
export function redactNativeLog(content, paths, state) {
|
|
1674
|
+
const exactSecrets = [];
|
|
1675
|
+
try {
|
|
1676
|
+
exactSecrets.push(readNativeSecret(paths));
|
|
1677
|
+
}
|
|
1678
|
+
catch { }
|
|
1679
|
+
if (state.postgres.mode === 'external') {
|
|
1680
|
+
try {
|
|
1681
|
+
const parsed = parseEnv(readFileSync(state.postgres.configRef, 'utf8'));
|
|
1682
|
+
for (const key of ['JWT_SECRET', 'JWT_ENC_SECRET', 'AOPS_ADMIN_KEY']) {
|
|
1683
|
+
const value = String(parsed[key] ?? '').trim();
|
|
1684
|
+
if (value)
|
|
1685
|
+
exactSecrets.push(value);
|
|
1686
|
+
}
|
|
1687
|
+
if (parsed.AOPS_PG_URL) {
|
|
1688
|
+
const rawUrl = String(parsed.AOPS_PG_URL);
|
|
1689
|
+
exactSecrets.push(rawUrl);
|
|
1690
|
+
try {
|
|
1691
|
+
const url = new URL(rawUrl);
|
|
1692
|
+
if (url.password) {
|
|
1693
|
+
exactSecrets.push(url.password);
|
|
1694
|
+
try {
|
|
1695
|
+
exactSecrets.push(decodeURIComponent(url.password));
|
|
1696
|
+
}
|
|
1697
|
+
catch { }
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
catch { }
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
catch { }
|
|
1704
|
+
}
|
|
1705
|
+
else {
|
|
1706
|
+
try {
|
|
1707
|
+
const rawUrl = buildCommunityNativePostgresUrl(state.postgres);
|
|
1708
|
+
exactSecrets.push(rawUrl);
|
|
1709
|
+
const url = new URL(rawUrl);
|
|
1710
|
+
if (url.password) {
|
|
1711
|
+
exactSecrets.push(url.password);
|
|
1712
|
+
try {
|
|
1713
|
+
exactSecrets.push(decodeURIComponent(url.password));
|
|
1714
|
+
}
|
|
1715
|
+
catch { }
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
catch { }
|
|
1719
|
+
}
|
|
1720
|
+
let redacted = content;
|
|
1721
|
+
for (const secret of exactSecrets
|
|
1722
|
+
.filter((value) => value.length >= MIN_EXACT_SECRET_REDACTION_LENGTH)
|
|
1723
|
+
.sort((left, right) => right.length - left.length)) {
|
|
1724
|
+
redacted = redacted.split(secret).join('[REDACTED]');
|
|
1725
|
+
}
|
|
1726
|
+
return redacted
|
|
1727
|
+
.replace(/postgres(?:ql)?:\/\/[^\s"']+/gi, 'postgresql://[REDACTED]')
|
|
1728
|
+
.replace(/(CHATV3_SERVER_KEY_SECRET\s*[=:]\s*)[^\s]+/gi, '$1[REDACTED]')
|
|
1729
|
+
.replace(/(["']?(?:JWT_SECRET|JWT_ENC_SECRET|AOPS_ADMIN_KEY)["']?\s*[=:]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^,;\s}\]]+)/gi, '$1[REDACTED]')
|
|
1730
|
+
.replace(/((?:password|pwd)\s*[=:]\s*)[^;\s]+/gi, '$1[REDACTED]');
|
|
1731
|
+
}
|
|
1732
|
+
export function readCommunityNativeLogs(params = {}) {
|
|
1733
|
+
const inspection = inspectCommunityNativeInstall({ instanceName: params.instanceName, dataRoot: params.dataRoot });
|
|
1734
|
+
if (inspection.status !== 'installed' || !inspection.state) {
|
|
1735
|
+
throw new Error(`community_native_not_installed:${inspection.status}:${inspection.error ?? 'run_server_setup'}`);
|
|
1736
|
+
}
|
|
1737
|
+
const tail = params.tail ?? 100;
|
|
1738
|
+
if (!Number.isSafeInteger(tail) || tail < 1 || tail > 10_000)
|
|
1739
|
+
throw new Error('community_logs_tail_invalid');
|
|
1740
|
+
if (!existsSync(inspection.paths.logPath)) {
|
|
1741
|
+
return { content: '', lineCount: 0, truncated: false, logPath: inspection.paths.logPath };
|
|
1742
|
+
}
|
|
1743
|
+
const stats = lstatSync(inspection.paths.logPath);
|
|
1744
|
+
if (!stats.isFile() || stats.isSymbolicLink())
|
|
1745
|
+
throw new Error('community_native_log_file_invalid');
|
|
1746
|
+
const realLog = realpathSync(inspection.paths.logPath);
|
|
1747
|
+
if (!isWithin(realpathSync(inspection.paths.instanceRoot), realLog))
|
|
1748
|
+
throw new Error('community_native_log_path_escape');
|
|
1749
|
+
const byteLength = Math.min(stats.size, MAX_LOG_TAIL_BYTES);
|
|
1750
|
+
const start = stats.size - byteLength;
|
|
1751
|
+
const buffer = Buffer.alloc(byteLength);
|
|
1752
|
+
const fd = openSync(realLog, 'r');
|
|
1753
|
+
try {
|
|
1754
|
+
if (byteLength > 0)
|
|
1755
|
+
readSync(fd, buffer, 0, byteLength, start);
|
|
1756
|
+
}
|
|
1757
|
+
finally {
|
|
1758
|
+
closeSync(fd);
|
|
1759
|
+
}
|
|
1760
|
+
let content = buffer.toString('utf8');
|
|
1761
|
+
let truncated = start > 0;
|
|
1762
|
+
if (truncated) {
|
|
1763
|
+
const firstNewline = content.indexOf('\n');
|
|
1764
|
+
content = firstNewline >= 0 ? content.slice(firstNewline + 1) : '';
|
|
1765
|
+
}
|
|
1766
|
+
const lines = content.replace(/\r\n/g, '\n').split('\n');
|
|
1767
|
+
if (lines.at(-1) === '')
|
|
1768
|
+
lines.pop();
|
|
1769
|
+
const selected = lines.slice(-tail);
|
|
1770
|
+
truncated ||= selected.length < lines.length;
|
|
1771
|
+
const safe = redactNativeLog(selected.join('\n'), inspection.paths, inspection.state);
|
|
1772
|
+
return { content: safe, lineCount: selected.length, truncated, logPath: inspection.paths.logPath };
|
|
1773
|
+
}
|
|
1774
|
+
async function runBuildChecked(runtime, invocation, step) {
|
|
1775
|
+
throwIfNativeAborted(invocation.signal);
|
|
1776
|
+
let result;
|
|
1777
|
+
try {
|
|
1778
|
+
result = await runtime.runBuild(invocation);
|
|
1779
|
+
}
|
|
1780
|
+
catch (error) {
|
|
1781
|
+
if (invocation.signal?.aborted)
|
|
1782
|
+
throw new Error('community_operation_aborted');
|
|
1783
|
+
throw error;
|
|
1784
|
+
}
|
|
1785
|
+
throwIfNativeAborted(invocation.signal);
|
|
1786
|
+
if (result.exitCode !== 0)
|
|
1787
|
+
throw new Error(`community_native_${step}_failed:${result.exitCode ?? result.signal ?? 'unknown'}`);
|
|
1788
|
+
}
|
|
1789
|
+
export function resolveCommunityNativeLaunchMode(input) {
|
|
1790
|
+
if (input.foreground === true && input.detach === true)
|
|
1791
|
+
throw new Error('community_native_launch_mode_conflict');
|
|
1792
|
+
return input.foreground === true ? 'foreground' : 'detached';
|
|
1793
|
+
}
|
|
1794
|
+
async function launchInstalledNative(params) {
|
|
1795
|
+
throwIfNativeAborted(params.signal);
|
|
1796
|
+
assertCommunityNativePathLayout(params.paths, { requireInstanceRoot: true });
|
|
1797
|
+
const priorProcess = readCommunityNativeProcess(params.paths);
|
|
1798
|
+
const observed = await observeInstalledNative({
|
|
1799
|
+
paths: params.paths,
|
|
1800
|
+
state: params.state,
|
|
1801
|
+
processRecord: priorProcess,
|
|
1802
|
+
runtime: params.runtime,
|
|
1803
|
+
now: params.now,
|
|
1804
|
+
});
|
|
1805
|
+
if (observed.runtimeState !== 'stopped' && observed.runtimeState !== 'crashed') {
|
|
1806
|
+
throw new Error(`community_native_process_active:${observed.runtimeState}:${observed.reason ?? 'run_server_status'}`);
|
|
1807
|
+
}
|
|
1808
|
+
const packagedSource = isPackagedCommunityServerSource(params.state.source);
|
|
1809
|
+
const currentSource = inspectCommunityNativeSource(params.state.source.root);
|
|
1810
|
+
if (packagedSource && currentSource.sourceFingerprint !== params.state.source.sourceFingerprint) {
|
|
1811
|
+
throw new Error('community_native_source_drift:run_server_setup_--apply');
|
|
1812
|
+
}
|
|
1813
|
+
const currentBuild = inspectCommunityNativeBuild(packagedSource ? params.state.source : currentSource, params.state.build.completedAt, currentSource);
|
|
1814
|
+
if (packagedSource && currentBuild.buildFingerprint !== params.state.build.buildFingerprint) {
|
|
1815
|
+
throw new Error('community_native_build_drift:run_server_setup_--apply');
|
|
1816
|
+
}
|
|
1817
|
+
const buildFields = [
|
|
1818
|
+
'completedAt', 'hostEntry', 'handlerEntry', 'cockpitIndex', 'hostEntrySha256',
|
|
1819
|
+
'handlerEntrySha256', 'cockpitIndexSha256', 'runtimeFileCount',
|
|
1820
|
+
'runtimeInventorySha256', 'buildFingerprint',
|
|
1821
|
+
];
|
|
1822
|
+
if (packagedSource && buildFields.some((field) => currentBuild[field] !== params.state.build[field])) {
|
|
1823
|
+
throw new Error('community_native_build_identity_drift:run_server_setup_--apply');
|
|
1824
|
+
}
|
|
1825
|
+
const postgres = params.state.postgres;
|
|
1826
|
+
params.progress?.({
|
|
1827
|
+
stage: 'postgres',
|
|
1828
|
+
message: postgres.mode === 'external'
|
|
1829
|
+
? 'Connecting to the selected PostgreSQL database...'
|
|
1830
|
+
: 'Starting and checking the AOPS-managed PostgreSQL database...',
|
|
1831
|
+
});
|
|
1832
|
+
const postgresUrl = postgres.mode === 'external'
|
|
1833
|
+
? loadExternalPostgresUrl(postgres.configRef, postgres.tlsPolicy)
|
|
1834
|
+
: await (async () => {
|
|
1835
|
+
await startCommunityNativePostgres({
|
|
1836
|
+
state: postgres,
|
|
1837
|
+
instanceName: params.state.instanceName,
|
|
1838
|
+
instanceRoot: params.paths.instanceRoot,
|
|
1839
|
+
runtime: params.postgresRuntime,
|
|
1840
|
+
signal: params.signal,
|
|
1841
|
+
});
|
|
1842
|
+
return buildCommunityNativePostgresUrl(postgres);
|
|
1843
|
+
})();
|
|
1844
|
+
if (params.state.server.authProvider !== 'trusted-local') {
|
|
1845
|
+
throw new Error('community_native_legacy_auth_cutover_required:run_aops_setup_server-env_--auth_trusted-local_then_reconfigure_without_reset');
|
|
1846
|
+
}
|
|
1847
|
+
const authConfig = postgres.mode === 'external'
|
|
1848
|
+
? loadExternalServerAuthConfig(postgres.configRef, params.state.server.authProvider)
|
|
1849
|
+
: Object.freeze({ authProvider: 'trusted-local' });
|
|
1850
|
+
throwIfNativeAborted(params.signal);
|
|
1851
|
+
params.progress?.({
|
|
1852
|
+
stage: 'migration-plan',
|
|
1853
|
+
message: 'Inspecting the PostgreSQL schema and planning migrations...',
|
|
1854
|
+
});
|
|
1855
|
+
const migration = assertCommunityNativeMigrationReceiptV1(await params.runtime.migrate({
|
|
1856
|
+
sourceRoot: params.state.source.root,
|
|
1857
|
+
repoUrl: postgresUrl,
|
|
1858
|
+
instanceName: params.state.instanceName,
|
|
1859
|
+
installId: params.state.installId,
|
|
1860
|
+
sourceFingerprint: params.state.source.sourceFingerprint,
|
|
1861
|
+
requiredPlanSha256: params.requiredMigrationPlanSha256,
|
|
1862
|
+
requiredAction: params.requiredMigrationAction,
|
|
1863
|
+
onPlanAccepted: (context) => {
|
|
1864
|
+
const pendingMigrationCount = context.migrationPlan.pendingMigrations.length;
|
|
1865
|
+
params.progress?.({
|
|
1866
|
+
stage: 'migration-apply',
|
|
1867
|
+
message: context.migrationPlan.action === 'migrate'
|
|
1868
|
+
? `Applying and verifying ${pendingMigrationCount} PostgreSQL migration${pendingMigrationCount === 1 ? '' : 's'}...`
|
|
1869
|
+
: 'No pending migrations; verifying the current PostgreSQL schema...',
|
|
1870
|
+
migrationAction: context.migrationPlan.action,
|
|
1871
|
+
pendingMigrationCount,
|
|
1872
|
+
});
|
|
1873
|
+
},
|
|
1874
|
+
signal: params.signal,
|
|
1875
|
+
}));
|
|
1876
|
+
throwIfNativeAborted(params.signal);
|
|
1877
|
+
if (migration.instanceName !== params.state.instanceName ||
|
|
1878
|
+
migration.installId !== params.state.installId ||
|
|
1879
|
+
migration.sourceFingerprint !== params.state.source.sourceFingerprint) {
|
|
1880
|
+
throw new Error('community_native_migration_receipt_identity_mismatch');
|
|
1881
|
+
}
|
|
1882
|
+
const sourceAfterMigration = inspectCommunityNativeSource(params.state.source.root);
|
|
1883
|
+
const expectedSourceFingerprint = packagedSource
|
|
1884
|
+
? params.state.source.sourceFingerprint
|
|
1885
|
+
: currentSource.sourceFingerprint;
|
|
1886
|
+
if (sourceAfterMigration.sourceFingerprint !== expectedSourceFingerprint) {
|
|
1887
|
+
throw new Error('community_native_source_drift_during_migration:run_server_setup_--apply');
|
|
1888
|
+
}
|
|
1889
|
+
const buildAfterMigration = inspectCommunityNativeBuild(packagedSource ? params.state.source : sourceAfterMigration, params.state.build.completedAt, sourceAfterMigration);
|
|
1890
|
+
const expectedBuild = packagedSource ? params.state.build : currentBuild;
|
|
1891
|
+
if (buildAfterMigration.buildFingerprint !== expectedBuild.buildFingerprint ||
|
|
1892
|
+
buildFields.some((field) => buildAfterMigration[field] !== expectedBuild[field])) {
|
|
1893
|
+
throw new Error('community_native_build_drift_during_migration:run_server_setup_--apply');
|
|
1894
|
+
}
|
|
1895
|
+
persistCommunityNativeMigrationReceipt({ paths: params.paths, state: params.state, receipt: migration });
|
|
1896
|
+
const chatv3ServerKeySecret = readNativeSecret(params.paths);
|
|
1897
|
+
throwIfNativeAborted(params.signal);
|
|
1898
|
+
params.progress?.({
|
|
1899
|
+
stage: 'server-start',
|
|
1900
|
+
message: 'Starting the local AOPS server process...',
|
|
1901
|
+
});
|
|
1902
|
+
rmSync(params.paths.identityPath, { force: true });
|
|
1903
|
+
rmSync(params.paths.controlPath, { force: true });
|
|
1904
|
+
mkdirSync(params.paths.logRoot, { recursive: true, mode: 0o700 });
|
|
1905
|
+
const launchId = params.createId();
|
|
1906
|
+
const startedAt = params.now().toISOString();
|
|
1907
|
+
const handle = await params.runtime.spawnChild({
|
|
1908
|
+
childEntry: resolveCommunityNativeChildEntry(),
|
|
1909
|
+
cwd: params.state.source.root,
|
|
1910
|
+
env: childEnvironment({
|
|
1911
|
+
state: params.state,
|
|
1912
|
+
paths: params.paths,
|
|
1913
|
+
launchId,
|
|
1914
|
+
startedAt,
|
|
1915
|
+
postgresUrl,
|
|
1916
|
+
chatv3ServerKeySecret,
|
|
1917
|
+
authConfig,
|
|
1918
|
+
env: params.env,
|
|
1919
|
+
}),
|
|
1920
|
+
mode: params.mode,
|
|
1921
|
+
logPath: params.paths.logPath,
|
|
1922
|
+
});
|
|
1923
|
+
let processRecord = {
|
|
1924
|
+
schemaVersion: 1,
|
|
1925
|
+
protocol: COMMUNITY_NATIVE_CHILD_PROTOCOL,
|
|
1926
|
+
instanceName: params.state.instanceName,
|
|
1927
|
+
launchId,
|
|
1928
|
+
pid: handle.pid,
|
|
1929
|
+
mode: params.mode,
|
|
1930
|
+
status: 'starting',
|
|
1931
|
+
startedAt,
|
|
1932
|
+
updatedAt: startedAt,
|
|
1933
|
+
sourceFingerprint: params.state.source.sourceFingerprint,
|
|
1934
|
+
identityPath: params.paths.identityPath,
|
|
1935
|
+
controlPath: params.paths.controlPath,
|
|
1936
|
+
logPath: params.mode === 'detached' ? params.paths.logPath : null,
|
|
1937
|
+
};
|
|
1938
|
+
writeProcess(params.paths, processRecord);
|
|
1939
|
+
try {
|
|
1940
|
+
params.progress?.({
|
|
1941
|
+
stage: 'server-health',
|
|
1942
|
+
message: 'Waiting for the AOPS server health check...',
|
|
1943
|
+
});
|
|
1944
|
+
const identity = await waitUntilReady({
|
|
1945
|
+
paths: params.paths,
|
|
1946
|
+
state: params.state,
|
|
1947
|
+
processRecord,
|
|
1948
|
+
handle,
|
|
1949
|
+
runtime: params.runtime,
|
|
1950
|
+
timeoutMs: params.readyTimeoutMs,
|
|
1951
|
+
now: params.now,
|
|
1952
|
+
signal: params.signal,
|
|
1953
|
+
});
|
|
1954
|
+
processRecord = { ...processRecord, hostPid: identity.hostPid };
|
|
1955
|
+
}
|
|
1956
|
+
catch (error) {
|
|
1957
|
+
const liveAfterFailure = params.runtime.processExists(processRecord.pid) || (() => {
|
|
1958
|
+
try {
|
|
1959
|
+
return existsSync(params.paths.identityPath) &&
|
|
1960
|
+
params.runtime.processExists(parseChildIdentity(params.paths, processRecord).hostPid);
|
|
1961
|
+
}
|
|
1962
|
+
catch {
|
|
1963
|
+
return false;
|
|
1964
|
+
}
|
|
1965
|
+
})();
|
|
1966
|
+
processRecord = {
|
|
1967
|
+
...processRecord,
|
|
1968
|
+
status: liveAfterFailure ? 'starting' : 'failed',
|
|
1969
|
+
updatedAt: params.now().toISOString(),
|
|
1970
|
+
failure: error instanceof Error ? error.message.slice(0, 300) : 'community_native_child_launch_failed',
|
|
1971
|
+
};
|
|
1972
|
+
writeProcess(params.paths, processRecord);
|
|
1973
|
+
throw error;
|
|
1974
|
+
}
|
|
1975
|
+
processRecord = { ...processRecord, status: 'running', updatedAt: params.now().toISOString() };
|
|
1976
|
+
writeProcess(params.paths, processRecord);
|
|
1977
|
+
if (params.mode === 'detached') {
|
|
1978
|
+
handle.detach();
|
|
1979
|
+
return { paths: params.paths, state: params.state, process: processRecord, migration, mode: params.mode };
|
|
1980
|
+
}
|
|
1981
|
+
return {
|
|
1982
|
+
paths: params.paths,
|
|
1983
|
+
state: params.state,
|
|
1984
|
+
process: processRecord,
|
|
1985
|
+
migration,
|
|
1986
|
+
mode: params.mode,
|
|
1987
|
+
waitForExit: async () => {
|
|
1988
|
+
let aborted = params.signal?.aborted === true;
|
|
1989
|
+
let onAbort;
|
|
1990
|
+
let exit;
|
|
1991
|
+
if (params.signal && !aborted) {
|
|
1992
|
+
const outcome = await Promise.race([
|
|
1993
|
+
handle.wait.then((value) => ({ kind: 'exit', value })),
|
|
1994
|
+
new Promise((resolve) => {
|
|
1995
|
+
onAbort = () => resolve({ kind: 'abort' });
|
|
1996
|
+
params.signal.addEventListener('abort', onAbort, { once: true });
|
|
1997
|
+
}),
|
|
1998
|
+
]);
|
|
1999
|
+
if (onAbort)
|
|
2000
|
+
params.signal.removeEventListener('abort', onAbort);
|
|
2001
|
+
if (outcome.kind === 'abort') {
|
|
2002
|
+
aborted = true;
|
|
2003
|
+
await stopSpawnedNative({
|
|
2004
|
+
paths: params.paths,
|
|
2005
|
+
state: params.state,
|
|
2006
|
+
processRecord,
|
|
2007
|
+
handle,
|
|
2008
|
+
runtime: params.runtime,
|
|
2009
|
+
timeoutMs: DEFAULT_STOP_TIMEOUT_MS,
|
|
2010
|
+
now: params.now,
|
|
2011
|
+
});
|
|
2012
|
+
exit = await handle.wait;
|
|
2013
|
+
}
|
|
2014
|
+
else {
|
|
2015
|
+
exit = outcome.value;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
else if (aborted) {
|
|
2019
|
+
await stopSpawnedNative({
|
|
2020
|
+
paths: params.paths,
|
|
2021
|
+
state: params.state,
|
|
2022
|
+
processRecord,
|
|
2023
|
+
handle,
|
|
2024
|
+
runtime: params.runtime,
|
|
2025
|
+
timeoutMs: DEFAULT_STOP_TIMEOUT_MS,
|
|
2026
|
+
now: params.now,
|
|
2027
|
+
});
|
|
2028
|
+
exit = await handle.wait;
|
|
2029
|
+
}
|
|
2030
|
+
else {
|
|
2031
|
+
exit = await handle.wait;
|
|
2032
|
+
}
|
|
2033
|
+
writeProcess(params.paths, {
|
|
2034
|
+
...processRecord,
|
|
2035
|
+
status: !aborted && exit.exitCode === 0 ? 'exited' : 'failed',
|
|
2036
|
+
updatedAt: params.now().toISOString(),
|
|
2037
|
+
exitCode: exit.exitCode,
|
|
2038
|
+
signal: exit.signal,
|
|
2039
|
+
...(!aborted && exit.exitCode === 0
|
|
2040
|
+
? {}
|
|
2041
|
+
: { failure: aborted ? 'community_operation_aborted' : `community_native_child_exit:${exit.exitCode ?? exit.signal ?? 'unknown'}` }),
|
|
2042
|
+
});
|
|
2043
|
+
if (aborted)
|
|
2044
|
+
throw new Error('community_operation_aborted');
|
|
2045
|
+
if (exit.exitCode !== 0)
|
|
2046
|
+
throw new Error(`community_native_child_failed:${exit.exitCode ?? exit.signal ?? 'unknown'}`);
|
|
2047
|
+
return exit;
|
|
2048
|
+
},
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
export async function setupCommunityNativeInstall(params) {
|
|
2052
|
+
if (params.contract.runtime !== 'native')
|
|
2053
|
+
throw new Error('community_native_contract_required');
|
|
2054
|
+
if (params.contract.profile !== 'native-external-postgres' &&
|
|
2055
|
+
params.contract.profile !== 'native-container-postgres')
|
|
2056
|
+
throw new Error('community_native_postgres_contract_required');
|
|
2057
|
+
const runtime = params.runtime ?? communityNativeRuntime;
|
|
2058
|
+
throwIfNativeAborted(params.signal);
|
|
2059
|
+
params.progress?.({
|
|
2060
|
+
stage: 'runtime',
|
|
2061
|
+
message: 'Validating the installed AOPS server runtime...',
|
|
2062
|
+
});
|
|
2063
|
+
const now = params.now ?? (() => new Date());
|
|
2064
|
+
const createId = params.createId ?? randomUUID;
|
|
2065
|
+
const paths = resolveCommunityNativePaths({ instanceName: params.contract.instanceId, dataRoot: params.dataRoot });
|
|
2066
|
+
assertCommunityNativePathLayout(paths);
|
|
2067
|
+
mkdirSync(paths.instanceRoot, { recursive: true, mode: 0o700 });
|
|
2068
|
+
assertCommunityNativePathLayout(paths, { requireInstanceRoot: true });
|
|
2069
|
+
if (existsSync(paths.ociStatePath))
|
|
2070
|
+
throw new Error('community_instance_runtime_conflict:oci_already_installed');
|
|
2071
|
+
const inspection = inspectCommunityNativeInstall({ instanceName: params.contract.instanceId, dataRoot: params.dataRoot });
|
|
2072
|
+
if (inspection.status === 'partial' || inspection.status === 'runtime-conflict') {
|
|
2073
|
+
throw new Error(`community_native_install_${inspection.status}:${inspection.error ?? 'unknown'}:run_doctor`);
|
|
2074
|
+
}
|
|
2075
|
+
const defaultSourceRoot = resolveCommunityNativeDefaultSourceRoot();
|
|
2076
|
+
const source = inspectCommunityNativeSource(params.sourceRoot ?? defaultSourceRoot);
|
|
2077
|
+
let configRef;
|
|
2078
|
+
if (params.contract.profile === 'native-external-postgres') {
|
|
2079
|
+
if (params.contract.postgres.mode !== 'external' || !params.contract.postgres.configRef ||
|
|
2080
|
+
!params.contract.postgres.tlsPolicy)
|
|
2081
|
+
throw new Error('community_native_external_postgres_contract_required');
|
|
2082
|
+
const configInput = path.resolve(params.contract.postgres.configRef);
|
|
2083
|
+
loadExternalPostgresUrl(configInput, params.contract.postgres.tlsPolicy);
|
|
2084
|
+
configRef = realpathSync(configInput);
|
|
2085
|
+
}
|
|
2086
|
+
else if (params.contract.postgres.mode !== 'container') {
|
|
2087
|
+
throw new Error('community_native_container_postgres_contract_required');
|
|
2088
|
+
}
|
|
2089
|
+
let priorApplicationReconciliation = 'current';
|
|
2090
|
+
if (inspection.status === 'installed' && inspection.state) {
|
|
2091
|
+
const observed = await observeInstalledNative({
|
|
2092
|
+
paths: inspection.paths,
|
|
2093
|
+
state: inspection.state,
|
|
2094
|
+
processRecord: inspection.process ?? null,
|
|
2095
|
+
runtime,
|
|
2096
|
+
now,
|
|
2097
|
+
});
|
|
2098
|
+
if (observed.runtimeState !== 'stopped' && observed.runtimeState !== 'crashed') {
|
|
2099
|
+
throw new Error(`community_native_process_active:${observed.runtimeState}:run_server_status_before_setup`);
|
|
2100
|
+
}
|
|
2101
|
+
priorApplicationReconciliation = reconcileCommunityNativePriorApplication({
|
|
2102
|
+
state: inspection.state,
|
|
2103
|
+
selectedSource: source,
|
|
2104
|
+
contract: params.contract,
|
|
2105
|
+
selectedExternalConfigRef: configRef,
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
if (!isPackagedCommunityServerSource(source)) {
|
|
2109
|
+
await runBuildChecked(runtime, { ...buildCommunityPnpmInvocation(source.root, ['install', '--frozen-lockfile'], params.env), signal: params.signal }, 'install');
|
|
2110
|
+
await runBuildChecked(runtime, { ...buildCommunityPnpmInvocation(source.root, ['run', 'build'], params.env), signal: params.signal }, 'build');
|
|
2111
|
+
}
|
|
2112
|
+
throwIfNativeAborted(params.signal);
|
|
2113
|
+
const updatedAt = now().toISOString();
|
|
2114
|
+
const build = inspectCommunityNativeBuild(source, updatedAt);
|
|
2115
|
+
const existing = inspection.status === 'installed' ? inspection.state : undefined;
|
|
2116
|
+
if (existing && existing.instanceName !== params.contract.instanceId)
|
|
2117
|
+
throw new Error('community_native_instance_mismatch');
|
|
2118
|
+
if (existing && existing.profile !== params.contract.profile) {
|
|
2119
|
+
throw new Error(`community_native_profile_change_refused:${existing.profile}:to:${params.contract.profile}:run_server_reset`);
|
|
2120
|
+
}
|
|
2121
|
+
mkdirSync(paths.runtimeRoot, { recursive: true, mode: 0o700 });
|
|
2122
|
+
const baseState = {
|
|
2123
|
+
schemaVersion: 1,
|
|
2124
|
+
runtime: 'native',
|
|
2125
|
+
instanceName: params.contract.instanceId,
|
|
2126
|
+
installId: existing?.installId ?? createId(),
|
|
2127
|
+
createdAt: existing?.createdAt ?? updatedAt,
|
|
2128
|
+
updatedAt,
|
|
2129
|
+
source,
|
|
2130
|
+
build,
|
|
2131
|
+
server: {
|
|
2132
|
+
host: params.contract.server.host,
|
|
2133
|
+
port: params.contract.server.port,
|
|
2134
|
+
exposure: params.contract.server.exposure,
|
|
2135
|
+
publicPort: params.contract.server.publicPort,
|
|
2136
|
+
authProvider: params.contract.server.authProvider,
|
|
2137
|
+
},
|
|
2138
|
+
cockpit: {
|
|
2139
|
+
host: '127.0.0.1',
|
|
2140
|
+
port: resolveCommunityNativeCockpitPort(params.contract.server.publicPort),
|
|
2141
|
+
},
|
|
2142
|
+
};
|
|
2143
|
+
const state = params.contract.profile === 'native-external-postgres'
|
|
2144
|
+
? {
|
|
2145
|
+
...baseState,
|
|
2146
|
+
profile: 'native-external-postgres',
|
|
2147
|
+
postgres: {
|
|
2148
|
+
mode: 'external',
|
|
2149
|
+
configRef: configRef,
|
|
2150
|
+
tlsPolicy: params.contract.postgres.tlsPolicy,
|
|
2151
|
+
},
|
|
2152
|
+
}
|
|
2153
|
+
: {
|
|
2154
|
+
...baseState,
|
|
2155
|
+
profile: 'native-container-postgres',
|
|
2156
|
+
postgres: await setupCommunityNativePostgres({
|
|
2157
|
+
sourceRoot: source.root,
|
|
2158
|
+
instanceRoot: paths.instanceRoot,
|
|
2159
|
+
runtimeRoot: paths.runtimeRoot,
|
|
2160
|
+
instanceName: params.contract.instanceId,
|
|
2161
|
+
runtime: params.postgresRuntime,
|
|
2162
|
+
createSecret: params.createPostgresSecret,
|
|
2163
|
+
readyTimeoutMs: params.readyTimeoutMs,
|
|
2164
|
+
signal: params.signal,
|
|
2165
|
+
}),
|
|
2166
|
+
};
|
|
2167
|
+
let applicationUpdatePrepared = null;
|
|
2168
|
+
const sameSourceIdentity = existing !== undefined &&
|
|
2169
|
+
samePhysicalPath(existing.source.root, state.source.root) &&
|
|
2170
|
+
existing.source.sourceFingerprint === state.source.sourceFingerprint;
|
|
2171
|
+
const inPlaceCheckoutRefresh = priorApplicationReconciliation === 'in-place-checkout-refresh';
|
|
2172
|
+
const selectedCheckout = sourceLayout(state.source.root).kind === 'checkout';
|
|
2173
|
+
let retryableInPlaceRefresh = false;
|
|
2174
|
+
let refreshLaunchArtifacts = false;
|
|
2175
|
+
if (existing) {
|
|
2176
|
+
const priorReference = createCommunityNativeApplicationReferenceV1(existing);
|
|
2177
|
+
const targetReference = createCommunityNativeApplicationReferenceV1(state);
|
|
2178
|
+
const applicationContentChanged = !sameCommunityNativeApplicationContent(priorReference, targetReference);
|
|
2179
|
+
refreshLaunchArtifacts = applicationContentChanged;
|
|
2180
|
+
if (applicationContentChanged && sameSourceIdentity) {
|
|
2181
|
+
if (!selectedCheckout)
|
|
2182
|
+
throw new Error('community_native_prior_application_build_drift');
|
|
2183
|
+
retryableInPlaceRefresh = true;
|
|
2184
|
+
}
|
|
2185
|
+
if (applicationContentChanged && !sameSourceIdentity) {
|
|
2186
|
+
if (params.requireApplicationUpdate === true && !inPlaceCheckoutRefresh) {
|
|
2187
|
+
applicationUpdatePrepared = writeCommunityNativeApplicationUpdatePrepared({
|
|
2188
|
+
paths,
|
|
2189
|
+
updateId: createId(),
|
|
2190
|
+
prior: existing,
|
|
2191
|
+
target: state,
|
|
2192
|
+
now,
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
else if (samePhysicalPath(existing.source.root, state.source.root)) {
|
|
2196
|
+
retryableInPlaceRefresh = true;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
if (params.requireApplicationUpdate === true && !applicationUpdatePrepared) {
|
|
2201
|
+
throw new Error(existing
|
|
2202
|
+
? 'community_native_application_update_content_unchanged'
|
|
2203
|
+
: 'community_native_application_update_requires_existing_install');
|
|
2204
|
+
}
|
|
2205
|
+
const setupFileSnapshots = captureCommunityNativeSetupFiles(paths);
|
|
2206
|
+
const nativeSecretExisted = existsSync(paths.secretPath);
|
|
2207
|
+
let launch;
|
|
2208
|
+
try {
|
|
2209
|
+
if (refreshLaunchArtifacts)
|
|
2210
|
+
removeCommunityNativeLaunchArtifactsForRefresh(paths);
|
|
2211
|
+
ensureNativeSecret(paths, params.createSecret ?? (() => randomBytes(32).toString('base64url')));
|
|
2212
|
+
atomicJsonWrite(paths.statePath, state);
|
|
2213
|
+
throwIfNativeAborted(params.signal);
|
|
2214
|
+
launch = await launchInstalledNative({
|
|
2215
|
+
paths,
|
|
2216
|
+
state,
|
|
2217
|
+
mode: params.mode ?? 'detached',
|
|
2218
|
+
runtime,
|
|
2219
|
+
postgresRuntime: params.postgresRuntime,
|
|
2220
|
+
now,
|
|
2221
|
+
createId,
|
|
2222
|
+
readyTimeoutMs: params.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS,
|
|
2223
|
+
progress: params.progress,
|
|
2224
|
+
env: params.env,
|
|
2225
|
+
signal: params.signal,
|
|
2226
|
+
});
|
|
2227
|
+
if (applicationUpdatePrepared) {
|
|
2228
|
+
writeCommunityNativeApplicationUpdateOutcome({
|
|
2229
|
+
paths,
|
|
2230
|
+
updateId: applicationUpdatePrepared.updateId,
|
|
2231
|
+
receipt: launch.migration,
|
|
2232
|
+
now,
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
catch (error) {
|
|
2237
|
+
let restoreFailure;
|
|
2238
|
+
if (!launch) {
|
|
2239
|
+
try {
|
|
2240
|
+
restoreCommunityNativeSetupFiles(paths, setupFileSnapshots);
|
|
2241
|
+
if (retryableInPlaceRefresh && existing) {
|
|
2242
|
+
atomicJsonWrite(paths.statePath, state);
|
|
2243
|
+
removeCommunityNativeLaunchArtifactsForRefresh(paths);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
catch (restoreError) {
|
|
2247
|
+
restoreFailure = restoreError;
|
|
2248
|
+
}
|
|
2249
|
+
if (!nativeSecretExisted) {
|
|
2250
|
+
try {
|
|
2251
|
+
removeNewCommunityNativeSecret(paths);
|
|
2252
|
+
}
|
|
2253
|
+
catch (secretRestoreError) {
|
|
2254
|
+
restoreFailure = restoreFailure
|
|
2255
|
+
? new AggregateError([restoreFailure, secretRestoreError])
|
|
2256
|
+
: secretRestoreError;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
if (applicationUpdatePrepared) {
|
|
2261
|
+
let succeededOutcomeDurable = false;
|
|
2262
|
+
try {
|
|
2263
|
+
succeededOutcomeDurable = readCommunityNativeApplicationUpdate(paths, applicationUpdatePrepared.updateId)
|
|
2264
|
+
.outcome?.status === 'community-native-application-update-succeeded';
|
|
2265
|
+
}
|
|
2266
|
+
catch { }
|
|
2267
|
+
if (!succeededOutcomeDurable) {
|
|
2268
|
+
writeCommunityNativeApplicationUpdateOutcome({
|
|
2269
|
+
paths,
|
|
2270
|
+
updateId: applicationUpdatePrepared.updateId,
|
|
2271
|
+
receipt: launch?.migration,
|
|
2272
|
+
error,
|
|
2273
|
+
now,
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
if (restoreFailure) {
|
|
2278
|
+
throw new Error('community_native_setup_local_state_restore_failed', {
|
|
2279
|
+
cause: new AggregateError([error, restoreFailure]),
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
throw error;
|
|
2283
|
+
}
|
|
2284
|
+
if (!launch)
|
|
2285
|
+
throw new Error('community_native_application_launch_missing');
|
|
2286
|
+
return {
|
|
2287
|
+
status: existing ? 'refreshed' : 'created',
|
|
2288
|
+
paths,
|
|
2289
|
+
state,
|
|
2290
|
+
launch,
|
|
2291
|
+
applicationUpdate: applicationUpdatePrepared
|
|
2292
|
+
? readCommunityNativeApplicationUpdate(paths, applicationUpdatePrepared.updateId)
|
|
2293
|
+
: null,
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
export async function rollbackCommunityNativeApplication(params) {
|
|
2297
|
+
throwIfNativeAborted(params.signal);
|
|
2298
|
+
const runtime = params.runtime ?? communityNativeRuntime;
|
|
2299
|
+
const now = params.now ?? (() => new Date());
|
|
2300
|
+
const createId = params.createId ?? randomUUID;
|
|
2301
|
+
const inspection = inspectCommunityNativeInstall({ instanceName: params.instanceName, dataRoot: params.dataRoot });
|
|
2302
|
+
if (inspection.status !== 'installed' || !inspection.state) {
|
|
2303
|
+
throw new Error(`community_native_not_installed:${inspection.status}:${inspection.error ?? 'run_server_setup'}`);
|
|
2304
|
+
}
|
|
2305
|
+
assertCommunityNativeApplicationCurrent(inspection.state);
|
|
2306
|
+
const observed = await observeInstalledNative({
|
|
2307
|
+
paths: inspection.paths,
|
|
2308
|
+
state: inspection.state,
|
|
2309
|
+
processRecord: inspection.process ?? null,
|
|
2310
|
+
runtime,
|
|
2311
|
+
now,
|
|
2312
|
+
});
|
|
2313
|
+
if (observed.runtimeState !== 'stopped' && observed.runtimeState !== 'crashed') {
|
|
2314
|
+
throw new Error(`community_native_process_active:${observed.runtimeState}:run_server_stop_before_rollback`);
|
|
2315
|
+
}
|
|
2316
|
+
const update = readCommunityNativeApplicationUpdate(inspection.paths, params.updateId);
|
|
2317
|
+
if (update.prepared.instanceName !== inspection.state.instanceName ||
|
|
2318
|
+
update.prepared.installId !== inspection.state.installId) {
|
|
2319
|
+
throw new Error('community_native_application_update_instance_mismatch');
|
|
2320
|
+
}
|
|
2321
|
+
if (update.rollbackPrepared || update.rollbackOutcome) {
|
|
2322
|
+
throw new Error('community_native_application_rollback_already_recorded');
|
|
2323
|
+
}
|
|
2324
|
+
const activeReference = createCommunityNativeApplicationReferenceV1(inspection.state);
|
|
2325
|
+
if (!sameCommunityNativeApplicationContent(activeReference, update.prepared.target)) {
|
|
2326
|
+
throw new Error('community_native_application_update_target_not_active');
|
|
2327
|
+
}
|
|
2328
|
+
const source = inspectCommunityNativeSource(params.sourceRoot);
|
|
2329
|
+
if (!isPackagedCommunityServerSource(source)) {
|
|
2330
|
+
await runBuildChecked(runtime, { ...buildCommunityPnpmInvocation(source.root, ['install', '--frozen-lockfile'], params.env), signal: params.signal }, 'rollback_install');
|
|
2331
|
+
await runBuildChecked(runtime, { ...buildCommunityPnpmInvocation(source.root, ['run', 'build'], params.env), signal: params.signal }, 'rollback_build');
|
|
2332
|
+
}
|
|
2333
|
+
throwIfNativeAborted(params.signal);
|
|
2334
|
+
const updatedAt = now().toISOString();
|
|
2335
|
+
const build = inspectCommunityNativeBuild(source, updatedAt);
|
|
2336
|
+
const candidateState = inspection.state.profile === 'native-external-postgres'
|
|
2337
|
+
? {
|
|
2338
|
+
...inspection.state,
|
|
2339
|
+
updatedAt,
|
|
2340
|
+
source,
|
|
2341
|
+
build,
|
|
2342
|
+
}
|
|
2343
|
+
: {
|
|
2344
|
+
...inspection.state,
|
|
2345
|
+
updatedAt,
|
|
2346
|
+
source,
|
|
2347
|
+
build,
|
|
2348
|
+
};
|
|
2349
|
+
const candidateReference = createCommunityNativeApplicationReferenceV1(candidateState);
|
|
2350
|
+
if (!sameCommunityNativeApplicationContent(candidateReference, update.prepared.prior)) {
|
|
2351
|
+
throw new Error('community_native_application_rollback_source_content_mismatch');
|
|
2352
|
+
}
|
|
2353
|
+
const postgresUrl = candidateState.profile === 'native-external-postgres'
|
|
2354
|
+
? loadExternalPostgresUrl(candidateState.postgres.configRef, candidateState.postgres.tlsPolicy)
|
|
2355
|
+
: await (async () => {
|
|
2356
|
+
await startCommunityNativePostgres({
|
|
2357
|
+
state: candidateState.postgres,
|
|
2358
|
+
instanceName: candidateState.instanceName,
|
|
2359
|
+
instanceRoot: inspection.paths.instanceRoot,
|
|
2360
|
+
runtime: params.postgresRuntime,
|
|
2361
|
+
signal: params.signal,
|
|
2362
|
+
});
|
|
2363
|
+
return buildCommunityNativePostgresUrl(candidateState.postgres);
|
|
2364
|
+
})();
|
|
2365
|
+
const databasePlan = await (params.planMigration ?? planCommunityNativeMigration)({
|
|
2366
|
+
sourceRoot: candidateState.source.root,
|
|
2367
|
+
repoUrl: postgresUrl,
|
|
2368
|
+
signal: params.signal,
|
|
2369
|
+
});
|
|
2370
|
+
throwIfNativeAborted(params.signal);
|
|
2371
|
+
if (databasePlan.planning.migrationPlan.action !== 'verify-only') {
|
|
2372
|
+
throw new Error(`community_native_database_restore_required:update=${params.updateId}:action=${databasePlan.planning.migrationPlan.action}`);
|
|
2373
|
+
}
|
|
2374
|
+
const rollbackId = createId();
|
|
2375
|
+
writeCommunityNativeApplicationRollbackPrepared({
|
|
2376
|
+
paths: inspection.paths,
|
|
2377
|
+
updateId: params.updateId,
|
|
2378
|
+
rollbackId,
|
|
2379
|
+
candidate: candidateReference,
|
|
2380
|
+
databasePlan: {
|
|
2381
|
+
action: 'verify-only',
|
|
2382
|
+
acceptedPlanSha256: databasePlan.planning.acceptedPlanSha256,
|
|
2383
|
+
sourceMigrationStateFingerprintSha256: databasePlan.planning.sourceFingerprintSha256,
|
|
2384
|
+
stateFingerprintSha256: databasePlan.planning.stateFingerprintSha256,
|
|
2385
|
+
},
|
|
2386
|
+
now,
|
|
2387
|
+
});
|
|
2388
|
+
let launch;
|
|
2389
|
+
try {
|
|
2390
|
+
atomicJsonWrite(inspection.paths.statePath, candidateState);
|
|
2391
|
+
launch = await launchInstalledNative({
|
|
2392
|
+
paths: inspection.paths,
|
|
2393
|
+
state: candidateState,
|
|
2394
|
+
mode: params.mode ?? 'detached',
|
|
2395
|
+
runtime,
|
|
2396
|
+
postgresRuntime: params.postgresRuntime,
|
|
2397
|
+
now,
|
|
2398
|
+
createId,
|
|
2399
|
+
readyTimeoutMs: params.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS,
|
|
2400
|
+
env: params.env,
|
|
2401
|
+
requiredMigrationPlanSha256: databasePlan.planning.acceptedPlanSha256,
|
|
2402
|
+
requiredMigrationAction: 'verify-only',
|
|
2403
|
+
signal: params.signal,
|
|
2404
|
+
});
|
|
2405
|
+
writeCommunityNativeApplicationRollbackOutcome({
|
|
2406
|
+
paths: inspection.paths,
|
|
2407
|
+
updateId: params.updateId,
|
|
2408
|
+
rollbackId,
|
|
2409
|
+
receipt: launch.migration,
|
|
2410
|
+
now,
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
catch (error) {
|
|
2414
|
+
let succeededOutcomeDurable = false;
|
|
2415
|
+
try {
|
|
2416
|
+
succeededOutcomeDurable = readCommunityNativeApplicationUpdate(inspection.paths, params.updateId)
|
|
2417
|
+
.rollbackOutcome?.status === 'community-native-application-rolled-back';
|
|
2418
|
+
}
|
|
2419
|
+
catch { }
|
|
2420
|
+
if (!succeededOutcomeDurable) {
|
|
2421
|
+
writeCommunityNativeApplicationRollbackOutcome({
|
|
2422
|
+
paths: inspection.paths,
|
|
2423
|
+
updateId: params.updateId,
|
|
2424
|
+
rollbackId,
|
|
2425
|
+
receipt: launch?.migration,
|
|
2426
|
+
error,
|
|
2427
|
+
now,
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
throw error;
|
|
2431
|
+
}
|
|
2432
|
+
if (!launch)
|
|
2433
|
+
throw new Error('community_native_application_rollback_launch_missing');
|
|
2434
|
+
return {
|
|
2435
|
+
paths: inspection.paths,
|
|
2436
|
+
state: candidateState,
|
|
2437
|
+
launch,
|
|
2438
|
+
applicationUpdate: readCommunityNativeApplicationUpdate(inspection.paths, params.updateId),
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
export async function startCommunityNativeInstall(params) {
|
|
2442
|
+
const inspection = inspectCommunityNativeInstall({ instanceName: params.instanceName, dataRoot: params.dataRoot });
|
|
2443
|
+
if (inspection.status !== 'installed' || !inspection.state) {
|
|
2444
|
+
throw new Error(`community_native_not_installed:${inspection.status}:${inspection.error ?? 'run_server_setup'}`);
|
|
2445
|
+
}
|
|
2446
|
+
return launchInstalledNative({
|
|
2447
|
+
paths: inspection.paths,
|
|
2448
|
+
state: inspection.state,
|
|
2449
|
+
mode: params.mode ?? 'detached',
|
|
2450
|
+
runtime: params.runtime ?? communityNativeRuntime,
|
|
2451
|
+
postgresRuntime: params.postgresRuntime,
|
|
2452
|
+
now: params.now ?? (() => new Date()),
|
|
2453
|
+
createId: params.createId ?? randomUUID,
|
|
2454
|
+
readyTimeoutMs: params.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS,
|
|
2455
|
+
env: params.env,
|
|
2456
|
+
signal: params.signal,
|
|
2457
|
+
});
|
|
2458
|
+
}
|