witnora 0.12.0 → 0.12.1
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/dist/onboard.js +72 -4
- package/package.json +1 -1
package/dist/onboard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
-
import { DEFAULT_WITNORA_SERVER } from "./credentials.js";
|
|
4
|
+
import { DEFAULT_WITNORA_SERVER, saveConnection } from "./credentials.js";
|
|
5
5
|
import { authorizeProjectConnection } from "./device-authorization.js";
|
|
6
6
|
import { verifyControlPlaneConnection } from "./control-plane.js";
|
|
7
7
|
import { inferPrivateCapabilities, runPrivateCapabilityDiscovery } from "./private-discovery.js";
|
|
@@ -38,6 +38,7 @@ export async function runOnboard(options) {
|
|
|
38
38
|
}
|
|
39
39
|
const attemptId = `install-${Date.now()}-${randomSuffix()}`;
|
|
40
40
|
const generatedFiles = [];
|
|
41
|
+
let gatewayMigration;
|
|
41
42
|
await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, { status: "installing", attemptId });
|
|
42
43
|
try {
|
|
43
44
|
generatedFiles.push(...await generateRepositoryConfig(repositoryPath, repository.template, repository.name));
|
|
@@ -85,7 +86,21 @@ export async function runOnboard(options) {
|
|
|
85
86
|
generatedFiles.push(...gateway.generatedFiles);
|
|
86
87
|
}
|
|
87
88
|
else {
|
|
88
|
-
|
|
89
|
+
const binding = await inspectGatewayBinding(gatewayState.directory, token.projectId, server);
|
|
90
|
+
if (binding.matches) {
|
|
91
|
+
await saveConnection(binding.connectionName, { server, projectId: token.projectId, apiKey: token.apiKey }, { configHome: options.configHome });
|
|
92
|
+
output("\nExisting customer-owned Gateway matches this project; verifying and reusing it.\n");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
await assertGatewayStopped(requestFetch, binding.host, binding.port, binding.projectId, token.projectId);
|
|
96
|
+
gatewayMigration = await archiveGateway(gatewayState.directory, repositoryPath, binding.projectId);
|
|
97
|
+
output(`\nExisting Gateway belongs to project ${binding.projectId}; archived it at ${gatewayMigration.archiveDirectory}.\n`);
|
|
98
|
+
const gateway = await initializeCustomerGateway({
|
|
99
|
+
projectId: token.projectId, server, repository: repositoryPath, authorization: token,
|
|
100
|
+
fetch: requestFetch, configHome: options.configHome, output,
|
|
101
|
+
});
|
|
102
|
+
generatedFiles.push(...gateway.generatedFiles);
|
|
103
|
+
}
|
|
89
104
|
}
|
|
90
105
|
generatedFiles.push(...await generateAutopilotFiles(repositoryPath, repository.name));
|
|
91
106
|
const doctor = await doctorCustomerGateway({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
|
|
@@ -102,17 +117,70 @@ export async function runOnboard(options) {
|
|
|
102
117
|
output("The self-test remains isolated. Start the generated Gateway and run the agent normally; the first source-signed, server-reconciled Gateway run completes onboarding.\n");
|
|
103
118
|
return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
|
|
104
119
|
repositoryKind: repository.kind, generatedFiles, receiptPath, discovery, setupPlanId: setupPlan.id,
|
|
105
|
-
gatewayDirectory: join(repositoryPath, ".witnora", "gateway") };
|
|
120
|
+
gatewayDirectory: join(repositoryPath, ".witnora", "gateway"), gatewayArchiveDirectory: gatewayMigration?.archiveDirectory };
|
|
106
121
|
}
|
|
107
122
|
catch (error) {
|
|
108
123
|
const diagnosis = error instanceof Error ? error.message : String(error);
|
|
109
124
|
await rollbackGeneratedFiles(generatedFiles);
|
|
125
|
+
if (gatewayMigration)
|
|
126
|
+
await restoreGateway(gatewayMigration);
|
|
110
127
|
await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, {
|
|
111
128
|
status: "failed", attemptId, generatedFiles: relativeGeneratedFiles(repositoryPath, generatedFiles), diagnosis, rolledBack: true,
|
|
112
129
|
}).catch(() => undefined);
|
|
113
130
|
throw new Error(`Witnora Setup Autopilot rolled back this install attempt: ${diagnosis}`);
|
|
114
131
|
}
|
|
115
132
|
}
|
|
133
|
+
async function inspectGatewayBinding(directory, projectId, server) {
|
|
134
|
+
let value;
|
|
135
|
+
try {
|
|
136
|
+
value = JSON.parse(await readFile(join(directory, "gateway.json"), "utf8"));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new Error("Existing Gateway configuration is not valid JSON. Preserve it and repair the setup in Advanced mode.");
|
|
140
|
+
}
|
|
141
|
+
const existingProjectId = typeof value.projectId === "string" ? value.projectId.trim() : "";
|
|
142
|
+
const existingServer = typeof value.server === "string" ? normalizeServer(value.server) : "";
|
|
143
|
+
const connectionName = typeof value.connectionName === "string" ? value.connectionName.trim() : "";
|
|
144
|
+
const host = typeof value.host === "string" && value.host.trim() ? value.host.trim() : "127.0.0.1";
|
|
145
|
+
const port = Number(value.port);
|
|
146
|
+
if (!existingProjectId || !existingServer || !connectionName || !Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
147
|
+
throw new Error("Existing Gateway configuration is missing a valid project, server, host, or port. Preserve it and repair the setup in Advanced mode.");
|
|
148
|
+
}
|
|
149
|
+
return { matches: existingProjectId === projectId && existingServer === server, projectId: existingProjectId, server: existingServer, connectionName, host, port };
|
|
150
|
+
}
|
|
151
|
+
async function assertGatewayStopped(requestFetch, host, port, oldProjectId, newProjectId) {
|
|
152
|
+
try {
|
|
153
|
+
const response = await requestFetch(`http://${host}:${port}/healthz`, { signal: AbortSignal.timeout(800) });
|
|
154
|
+
if (!response.ok)
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`The running Gateway belongs to project ${oldProjectId}, not ${newProjectId}. Stop it with Ctrl+C, then run onboard again so Witnora can preserve the old evidence and bind this repository to the new project.`);
|
|
161
|
+
}
|
|
162
|
+
async function archiveGateway(gatewayDirectory, repositoryPath, projectId) {
|
|
163
|
+
const archiveRoot = join(repositoryPath, ".witnora", "gateway-archive");
|
|
164
|
+
const archiveDirectory = join(archiveRoot, `${Date.now()}-${safePathSegment(projectId)}-${randomSuffix()}`);
|
|
165
|
+
await mkdir(archiveRoot, { recursive: true });
|
|
166
|
+
const archiveIgnore = join(archiveRoot, ".gitignore");
|
|
167
|
+
if (!await exists(archiveIgnore))
|
|
168
|
+
await writeFile(archiveIgnore, "*\n!.gitignore\n");
|
|
169
|
+
try {
|
|
170
|
+
await rename(gatewayDirectory, archiveDirectory);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
throw new Error(`Could not archive the Gateway for project ${projectId}. Stop any running Gateway with Ctrl+C and retry. ${error instanceof Error ? error.message : String(error)}`);
|
|
174
|
+
}
|
|
175
|
+
return { gatewayDirectory, archiveDirectory };
|
|
176
|
+
}
|
|
177
|
+
async function restoreGateway(migration) {
|
|
178
|
+
await rm(migration.gatewayDirectory, { recursive: true, force: true });
|
|
179
|
+
await rename(migration.archiveDirectory, migration.gatewayDirectory);
|
|
180
|
+
}
|
|
181
|
+
function safePathSegment(value) {
|
|
182
|
+
return (value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-|-$/g, "") || "unknown-project").slice(0, 80);
|
|
183
|
+
}
|
|
116
184
|
async function generateAutopilotFiles(repositoryPath, subject) {
|
|
117
185
|
const files = new Map([
|
|
118
186
|
[".witnora/setup/policy.json", `${JSON.stringify({ schemaVersion: "witnora.setup_policy.v0.1", subject, unknownCapabilities: "pending_confirmation", defaultDecision: "deny", environment: "sandbox" }, null, 2)}\n`],
|