@forgezero/agent 0.1.52 → 0.1.55
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/README.md +118 -9
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +110 -69
- package/dist/community-rehearsal-host.js +40 -8
- package/dist/container-supervisor.d.ts +36 -0
- package/dist/definition.js +38 -6
- package/dist/deploy-actions.d.ts +87 -0
- package/dist/deploy-compiler.d.ts +26 -0
- package/dist/deploy-compiler.js +1050 -0
- package/dist/deploy-file.js +45 -13
- package/dist/deploy-plan-runner.d.ts +65 -0
- package/dist/deploy-plan-runner.js +1205 -0
- package/dist/deploy-plan.d.ts +41 -0
- package/dist/deploy-plan.js +925 -0
- package/dist/deploy-providers.d.ts +85 -0
- package/dist/deploy.d.ts +439 -0
- package/dist/deploy.js +169 -0
- package/dist/deployment.d.ts +5 -0
- package/dist/fz-agent.js +2116 -329
- package/dist/fz.js +1444 -313
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.js +167 -126
- package/dist/platform-bootstrap-runtime.js +1 -1
- package/dist/platform-fleet-verification.js +500 -121
- package/dist/provision.js +400 -21
- package/dist/software-helper.d.ts +5 -0
- package/dist/software-helper.js +400 -19
- package/dist/software.d.ts +1 -1
- package/dist/software.js +38 -6
- package/dist/version.d.ts +1 -1
- package/package.json +17 -1
package/dist/fz.js
CHANGED
|
@@ -4811,9 +4811,9 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
|
|
|
4811
4811
|
|
|
4812
4812
|
// src/cli/index.ts
|
|
4813
4813
|
init_dist();
|
|
4814
|
-
import { existsSync as
|
|
4814
|
+
import { existsSync as existsSync11, lstatSync as lstatSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync14, statSync as statSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync12 } from "fs";
|
|
4815
4815
|
import { randomBytes as randomBytes10 } from "crypto";
|
|
4816
|
-
import { dirname as
|
|
4816
|
+
import { basename as basename3, dirname as dirname12, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
|
|
4817
4817
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4818
4818
|
import { hostname } from "os";
|
|
4819
4819
|
|
|
@@ -4830,7 +4830,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
4830
4830
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
4831
4831
|
|
|
4832
4832
|
// src/version.ts
|
|
4833
|
-
var VERSION2 = "0.1.
|
|
4833
|
+
var VERSION2 = "0.1.55";
|
|
4834
4834
|
|
|
4835
4835
|
// src/software.ts
|
|
4836
4836
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -4840,6 +4840,7 @@ var OS_CATALOG = [
|
|
|
4840
4840
|
];
|
|
4841
4841
|
var SOFTWARE_CATALOG = [
|
|
4842
4842
|
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4843
|
+
{ id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4843
4844
|
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4844
4845
|
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4845
4846
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -4847,6 +4848,14 @@ var SOFTWARE_CATALOG = [
|
|
|
4847
4848
|
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4848
4849
|
{ id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
4849
4850
|
];
|
|
4851
|
+
var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
4852
|
+
"data-root": "/var/lib/docker",
|
|
4853
|
+
"storage-driver": "overlay2",
|
|
4854
|
+
"live-restore": true,
|
|
4855
|
+
"log-driver": "local",
|
|
4856
|
+
"log-opts": { "max-size": "10m", "max-file": "3" }
|
|
4857
|
+
}, null, 2)}
|
|
4858
|
+
`;
|
|
4850
4859
|
function validateSoftwareRequirements(value, _options = {}) {
|
|
4851
4860
|
if (!Array.isArray(value) || value.length > 32)
|
|
4852
4861
|
throw new Error("software requirements must be an array of at most 32 entries");
|
|
@@ -4858,7 +4867,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
4858
4867
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
4859
4868
|
throw new Error("software requirement contains an unknown field");
|
|
4860
4869
|
}
|
|
4861
|
-
if (!["bun", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
4870
|
+
if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
4862
4871
|
throw new Error("software requirement coordinate is invalid");
|
|
4863
4872
|
}
|
|
4864
4873
|
const requirement = { id: row.id, version: row.version };
|
|
@@ -5254,6 +5263,39 @@ var defaultHost = {
|
|
|
5254
5263
|
now: Date.now
|
|
5255
5264
|
};
|
|
5256
5265
|
|
|
5266
|
+
// src/container-supervisor.ts
|
|
5267
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5268
|
+
import { dirname as dirname2, resolve as resolve2, sep as sep2 } from "path";
|
|
5269
|
+
var defaultHost2 = {
|
|
5270
|
+
realpath: realpathSync2,
|
|
5271
|
+
exists: existsSync2,
|
|
5272
|
+
read: (path) => readFileSync2(path, "utf8"),
|
|
5273
|
+
write(path, content, mode) {
|
|
5274
|
+
mkdirSync2(dirname2(path), { recursive: true, mode: 493 });
|
|
5275
|
+
const next = `${path}.next`;
|
|
5276
|
+
writeFileSync2(next, content, { mode });
|
|
5277
|
+
renameSync2(next, path);
|
|
5278
|
+
},
|
|
5279
|
+
remove: (path) => rmSync2(path, { force: true }),
|
|
5280
|
+
list: (path) => existsSync2(path) ? readdirSync2(path) : [],
|
|
5281
|
+
mkdir: (path, mode) => mkdirSync2(path, { recursive: true, mode }),
|
|
5282
|
+
async exec(argv2) {
|
|
5283
|
+
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
|
|
5284
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
5285
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
5286
|
+
},
|
|
5287
|
+
async health(port, path, timeoutMs, method, expectedStatus) {
|
|
5288
|
+
try {
|
|
5289
|
+
const response = await fetch(`http://127.0.0.1:${port}${path}`, { method, redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
|
|
5290
|
+
return expectedStatus.includes(response.status);
|
|
5291
|
+
} catch {
|
|
5292
|
+
return false;
|
|
5293
|
+
}
|
|
5294
|
+
},
|
|
5295
|
+
sleep: (ms) => Bun.sleep(ms),
|
|
5296
|
+
now: Date.now
|
|
5297
|
+
};
|
|
5298
|
+
|
|
5257
5299
|
// src/software-helper.ts
|
|
5258
5300
|
var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
5259
5301
|
var SOFTWARE_HELPER_GROUP = "forgezero-software";
|
|
@@ -6236,17 +6278,17 @@ import { randomBytes as randomBytes5 } from "crypto";
|
|
|
6236
6278
|
import {
|
|
6237
6279
|
chmodSync,
|
|
6238
6280
|
copyFileSync,
|
|
6239
|
-
existsSync as
|
|
6281
|
+
existsSync as existsSync3,
|
|
6240
6282
|
lstatSync,
|
|
6241
|
-
mkdirSync as
|
|
6242
|
-
readFileSync as
|
|
6243
|
-
realpathSync as
|
|
6244
|
-
renameSync as
|
|
6245
|
-
rmSync as
|
|
6283
|
+
mkdirSync as mkdirSync3,
|
|
6284
|
+
readFileSync as readFileSync3,
|
|
6285
|
+
realpathSync as realpathSync3,
|
|
6286
|
+
renameSync as renameSync3,
|
|
6287
|
+
rmSync as rmSync3,
|
|
6246
6288
|
symlinkSync,
|
|
6247
|
-
writeFileSync as
|
|
6289
|
+
writeFileSync as writeFileSync3
|
|
6248
6290
|
} from "fs";
|
|
6249
|
-
import { dirname as
|
|
6291
|
+
import { dirname as dirname3 } from "path";
|
|
6250
6292
|
function parseAssignments(value) {
|
|
6251
6293
|
if (!value?.trim())
|
|
6252
6294
|
return;
|
|
@@ -6338,28 +6380,28 @@ var runProvisionOperation = async (operation) => {
|
|
|
6338
6380
|
}
|
|
6339
6381
|
if (operation.kind === "install-runtime") {
|
|
6340
6382
|
const release = `/opt/forgezero/agent/versions/${operation.version}`;
|
|
6341
|
-
|
|
6342
|
-
|
|
6383
|
+
mkdirSync3(`${release}/dist`, { recursive: true, mode: 493 });
|
|
6384
|
+
mkdirSync3(dirname3(operation.binary), { recursive: true, mode: 493 });
|
|
6343
6385
|
copyFileSync(operation.source, `${release}/dist/fz-agent.js`);
|
|
6344
6386
|
chmodSync(`${release}/dist/fz-agent.js`, 493);
|
|
6345
|
-
const gitSshSource = `${
|
|
6346
|
-
if (!
|
|
6387
|
+
const gitSshSource = `${dirname3(operation.source)}/fz-git-ssh.js`;
|
|
6388
|
+
if (!existsSync3(gitSshSource))
|
|
6347
6389
|
return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
|
|
6348
6390
|
copyFileSync(gitSshSource, `${release}/dist/fz-git-ssh.js`);
|
|
6349
6391
|
chmodSync(`${release}/dist/fz-git-ssh.js`, 493);
|
|
6350
6392
|
const pending = "/opt/forgezero/agent/current.next";
|
|
6351
|
-
|
|
6393
|
+
rmSync3(pending, { force: true });
|
|
6352
6394
|
symlinkSync(`versions/${operation.version}`, pending);
|
|
6353
|
-
|
|
6354
|
-
|
|
6395
|
+
renameSync3(pending, "/opt/forgezero/agent/current");
|
|
6396
|
+
rmSync3(operation.binary, { force: true });
|
|
6355
6397
|
symlinkSync("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
|
|
6356
6398
|
const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
|
|
6357
|
-
|
|
6399
|
+
rmSync3(gitSshBinary, { force: true });
|
|
6358
6400
|
symlinkSync("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
|
|
6359
6401
|
return { stdout: "", exitCode: 0 };
|
|
6360
6402
|
}
|
|
6361
6403
|
if (operation.kind === "ensure-seed") {
|
|
6362
|
-
if (
|
|
6404
|
+
if (existsSync3(operation.credential) && lstatSync(operation.credential).size > 0)
|
|
6363
6405
|
return { stdout: "", exitCode: 0 };
|
|
6364
6406
|
const seed = randomBytes5(32).toString("base64url");
|
|
6365
6407
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
|
|
@@ -6371,9 +6413,9 @@ var runProvisionOperation = async (operation) => {
|
|
|
6371
6413
|
const key = "/run/forgezero-git-deploy-key";
|
|
6372
6414
|
const publicKey = `${key}.pub`;
|
|
6373
6415
|
try {
|
|
6374
|
-
if (!
|
|
6375
|
-
|
|
6376
|
-
|
|
6416
|
+
if (!existsSync3(operation.credential) || lstatSync(operation.credential).size < 1) {
|
|
6417
|
+
rmSync3(key, { force: true });
|
|
6418
|
+
rmSync3(publicKey, { force: true });
|
|
6377
6419
|
let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
|
|
6378
6420
|
if (result.exitCode !== 0)
|
|
6379
6421
|
return result;
|
|
@@ -6382,8 +6424,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
6382
6424
|
return result;
|
|
6383
6425
|
chmodSync(operation.credential, 256);
|
|
6384
6426
|
}
|
|
6385
|
-
if (!
|
|
6386
|
-
if (!
|
|
6427
|
+
if (!existsSync3(operation.publicKey) || lstatSync(operation.publicKey).size < 1) {
|
|
6428
|
+
if (!existsSync3(key)) {
|
|
6387
6429
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
|
|
6388
6430
|
if (decrypted.exitCode !== 0)
|
|
6389
6431
|
return decrypted;
|
|
@@ -6391,25 +6433,25 @@ var runProvisionOperation = async (operation) => {
|
|
|
6391
6433
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
6392
6434
|
if (derived.exitCode !== 0)
|
|
6393
6435
|
return derived;
|
|
6394
|
-
|
|
6436
|
+
writeFileSync3(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
|
|
6395
6437
|
`, { mode: 292 });
|
|
6396
6438
|
}
|
|
6397
6439
|
return { stdout: "", exitCode: 0 };
|
|
6398
6440
|
} finally {
|
|
6399
|
-
|
|
6400
|
-
|
|
6441
|
+
rmSync3(key, { force: true });
|
|
6442
|
+
rmSync3(publicKey, { force: true });
|
|
6401
6443
|
}
|
|
6402
6444
|
}
|
|
6403
6445
|
if (operation.kind === "ensure-bootstrap-ssh-identity") {
|
|
6404
6446
|
const key = "/run/forgezero-bootstrap-ssh-key";
|
|
6405
6447
|
const generatedPublicKey = `${key}.pub`;
|
|
6406
6448
|
try {
|
|
6407
|
-
if (!
|
|
6408
|
-
|
|
6409
|
-
|
|
6449
|
+
if (!existsSync3(operation.credential) || lstatSync(operation.credential).size < 1) {
|
|
6450
|
+
rmSync3(key, { force: true });
|
|
6451
|
+
rmSync3(generatedPublicKey, { force: true });
|
|
6410
6452
|
let result;
|
|
6411
6453
|
if (operation.source) {
|
|
6412
|
-
const source =
|
|
6454
|
+
const source = existsSync3(operation.source) ? lstatSync(operation.source) : undefined;
|
|
6413
6455
|
if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
|
|
6414
6456
|
return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
|
|
6415
6457
|
}
|
|
@@ -6429,8 +6471,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
6429
6471
|
return result;
|
|
6430
6472
|
chmodSync(operation.credential, 256);
|
|
6431
6473
|
}
|
|
6432
|
-
if (!
|
|
6433
|
-
if (!
|
|
6474
|
+
if (!existsSync3(operation.publicKey) || lstatSync(operation.publicKey).size < 1) {
|
|
6475
|
+
if (!existsSync3(key)) {
|
|
6434
6476
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
|
|
6435
6477
|
if (decrypted.exitCode !== 0)
|
|
6436
6478
|
return decrypted;
|
|
@@ -6440,28 +6482,28 @@ var runProvisionOperation = async (operation) => {
|
|
|
6440
6482
|
if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
|
|
6441
6483
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
6442
6484
|
}
|
|
6443
|
-
|
|
6444
|
-
|
|
6485
|
+
mkdirSync3(dirname3(operation.publicKey), { recursive: true, mode: 493 });
|
|
6486
|
+
writeFileSync3(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
|
|
6445
6487
|
`, { mode: 292 });
|
|
6446
6488
|
chmodSync(operation.publicKey, 292);
|
|
6447
6489
|
}
|
|
6448
6490
|
if (operation.source)
|
|
6449
|
-
|
|
6491
|
+
rmSync3(operation.source, { force: true });
|
|
6450
6492
|
return { stdout: "", exitCode: 0 };
|
|
6451
6493
|
} finally {
|
|
6452
|
-
|
|
6453
|
-
|
|
6494
|
+
rmSync3(key, { force: true });
|
|
6495
|
+
rmSync3(generatedPublicKey, { force: true });
|
|
6454
6496
|
}
|
|
6455
6497
|
}
|
|
6456
6498
|
if (operation.kind === "ensure-enrolment") {
|
|
6457
|
-
if (
|
|
6499
|
+
if (existsSync3(operation.state) && lstatSync(operation.state).size > 0 || existsSync3(operation.credential) && lstatSync(operation.credential).size > 0)
|
|
6458
6500
|
return { stdout: "", exitCode: 0 };
|
|
6459
|
-
if (!
|
|
6501
|
+
if (!existsSync3(operation.source))
|
|
6460
6502
|
return { stdout: "enrolment source is missing", exitCode: 1 };
|
|
6461
6503
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
|
|
6462
6504
|
if (result.exitCode === 0) {
|
|
6463
6505
|
chmodSync(operation.credential, 256);
|
|
6464
|
-
|
|
6506
|
+
rmSync3(operation.source, { force: true });
|
|
6465
6507
|
}
|
|
6466
6508
|
return result;
|
|
6467
6509
|
}
|
|
@@ -6476,7 +6518,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
6476
6518
|
return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
|
|
6477
6519
|
}
|
|
6478
6520
|
if (operation.kind === "verify-file")
|
|
6479
|
-
return
|
|
6521
|
+
return existsSync3(operation.path) && lstatSync(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
|
|
6480
6522
|
if (operation.kind === "verify-egress") {
|
|
6481
6523
|
const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
|
|
6482
6524
|
if (active.exitCode !== 0)
|
|
@@ -6492,31 +6534,31 @@ var runProvisionOperation = async (operation) => {
|
|
|
6492
6534
|
if (operation.kind === "verify-resolved-stub") {
|
|
6493
6535
|
try {
|
|
6494
6536
|
const expected = "/run/systemd/resolve/stub-resolv.conf";
|
|
6495
|
-
return
|
|
6537
|
+
return realpathSync3("/etc/resolv.conf") === expected && realpathSync3(expected) === expected ? { stdout: expected, exitCode: 0 } : { stdout: "resolver stub mismatch", exitCode: 1 };
|
|
6496
6538
|
} catch {
|
|
6497
6539
|
return { stdout: "resolver stub missing", exitCode: 1 };
|
|
6498
6540
|
}
|
|
6499
6541
|
}
|
|
6500
6542
|
if (operation.kind === "install-warp") {
|
|
6501
|
-
const os =
|
|
6543
|
+
const os = readFileSync3("/etc/os-release", "utf8");
|
|
6502
6544
|
if (!/^ID=ubuntu$/m.test(os) || !/^VERSION_ID="?26\.04"?$/m.test(os))
|
|
6503
6545
|
return { stdout: "unsupported WARP host OS", exitCode: 1 };
|
|
6504
6546
|
const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
|
|
6505
6547
|
if (!response.ok)
|
|
6506
6548
|
return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6549
|
+
mkdirSync3("/usr/share/keyrings", { recursive: true, mode: 493 });
|
|
6550
|
+
mkdirSync3("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
|
|
6551
|
+
mkdirSync3("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
|
|
6510
6552
|
const key = "/run/cloudflare-warp-key.gpg";
|
|
6511
|
-
|
|
6553
|
+
writeFileSync3(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
|
|
6512
6554
|
let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
|
|
6513
|
-
|
|
6555
|
+
rmSync3(key, { force: true });
|
|
6514
6556
|
if (result.exitCode !== 0)
|
|
6515
6557
|
return result;
|
|
6516
6558
|
const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
|
|
6517
6559
|
if (!codename)
|
|
6518
6560
|
return { stdout: "Ubuntu codename missing", exitCode: 1 };
|
|
6519
|
-
|
|
6561
|
+
writeFileSync3("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
|
|
6520
6562
|
`, { mode: 420 });
|
|
6521
6563
|
result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
|
|
6522
6564
|
return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
|
|
@@ -9901,8 +9943,8 @@ async function runUnlock(args) {
|
|
|
9901
9943
|
}
|
|
9902
9944
|
|
|
9903
9945
|
// src/project-context.ts
|
|
9904
|
-
import { existsSync as
|
|
9905
|
-
import { dirname as
|
|
9946
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
9947
|
+
import { dirname as dirname4, join as join3, resolve as resolve3 } from "path";
|
|
9906
9948
|
var PROJECT_CONTEXT_VERSION = 1;
|
|
9907
9949
|
var GENERATED = "<!-- Generated by @forgezero/agent project context. Edit .forgezero/project.json, then run `fz project sync`. -->";
|
|
9908
9950
|
|
|
@@ -9981,14 +10023,14 @@ function defaultProjectContext(root = process.cwd()) {
|
|
|
9981
10023
|
let name = root.split("/").filter(Boolean).at(-1) ?? "project";
|
|
9982
10024
|
let verify = ["npm test"];
|
|
9983
10025
|
const manifestPath = join3(root, "package.json");
|
|
9984
|
-
if (
|
|
10026
|
+
if (existsSync4(manifestPath)) {
|
|
9985
10027
|
try {
|
|
9986
|
-
const pkg = JSON.parse(
|
|
10028
|
+
const pkg = JSON.parse(readFileSync4(manifestPath, "utf8"));
|
|
9987
10029
|
name = pkg.name ?? name;
|
|
9988
|
-
const runner =
|
|
10030
|
+
const runner = existsSync4(join3(root, "bun.lock")) ? "bun run" : "npm run";
|
|
9989
10031
|
verify = ["check", "test", "build"].filter((script) => pkg.scripts?.[script]).map((script) => `${runner} ${script}`);
|
|
9990
10032
|
if (verify.length === 0)
|
|
9991
|
-
verify = [
|
|
10033
|
+
verify = [existsSync4(join3(root, "bun.lock")) ? "bun test" : "npm test"];
|
|
9992
10034
|
} catch {}
|
|
9993
10035
|
}
|
|
9994
10036
|
return {
|
|
@@ -10083,22 +10125,22 @@ ${adapter("Cursor").replace(`${GENERATED}
|
|
|
10083
10125
|
];
|
|
10084
10126
|
}
|
|
10085
10127
|
var atomicWrite = (path, content) => {
|
|
10086
|
-
|
|
10128
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
10087
10129
|
const next = `${path}.${process.pid}.next`;
|
|
10088
|
-
|
|
10089
|
-
|
|
10130
|
+
writeFileSync4(next, content, { mode: 420 });
|
|
10131
|
+
renameSync4(next, path);
|
|
10090
10132
|
};
|
|
10091
10133
|
function initializeProjectContext(rootInput, manifestInput = defaultProjectContext(rootInput), options = {}) {
|
|
10092
|
-
const root =
|
|
10134
|
+
const root = resolve3(rootInput);
|
|
10093
10135
|
const manifest = parseProjectContext(manifestInput);
|
|
10094
10136
|
const manifestPath = join3(root, ".forgezero", "project.json");
|
|
10095
10137
|
const files = projectContextFiles(manifest);
|
|
10096
10138
|
const collisions = [manifestPath, ...files.map((file) => join3(root, file.path))].filter((path) => {
|
|
10097
|
-
if (!
|
|
10139
|
+
if (!existsSync4(path))
|
|
10098
10140
|
return false;
|
|
10099
10141
|
if (path === manifestPath)
|
|
10100
10142
|
return true;
|
|
10101
|
-
return !
|
|
10143
|
+
return !readFileSync4(path, "utf8").startsWith(GENERATED);
|
|
10102
10144
|
});
|
|
10103
10145
|
if (collisions.length && !options.force) {
|
|
10104
10146
|
throw new ProjectContextError(`refusing to replace existing project context: ${collisions.join(", ")}`);
|
|
@@ -10110,15 +10152,15 @@ function initializeProjectContext(rootInput, manifestInput = defaultProjectConte
|
|
|
10110
10152
|
return files;
|
|
10111
10153
|
}
|
|
10112
10154
|
function syncProjectContext(rootInput) {
|
|
10113
|
-
const root =
|
|
10155
|
+
const root = resolve3(rootInput);
|
|
10114
10156
|
const manifestPath = join3(root, ".forgezero", "project.json");
|
|
10115
|
-
if (!
|
|
10157
|
+
if (!existsSync4(manifestPath))
|
|
10116
10158
|
throw new ProjectContextError("No .forgezero/project.json. Run `fz project init`.");
|
|
10117
|
-
const manifest = parseProjectContext(JSON.parse(
|
|
10159
|
+
const manifest = parseProjectContext(JSON.parse(readFileSync4(manifestPath, "utf8")));
|
|
10118
10160
|
const files = projectContextFiles(manifest);
|
|
10119
10161
|
for (const file of files) {
|
|
10120
10162
|
const path = join3(root, file.path);
|
|
10121
|
-
if (
|
|
10163
|
+
if (existsSync4(path) && !readFileSync4(path, "utf8").startsWith(GENERATED)) {
|
|
10122
10164
|
throw new ProjectContextError(`refusing to replace non-generated adapter: ${file.path}`);
|
|
10123
10165
|
}
|
|
10124
10166
|
atomicWrite(path, file.content);
|
|
@@ -10126,30 +10168,30 @@ function syncProjectContext(rootInput) {
|
|
|
10126
10168
|
return files;
|
|
10127
10169
|
}
|
|
10128
10170
|
function checkProjectContext(rootInput) {
|
|
10129
|
-
const root =
|
|
10171
|
+
const root = resolve3(rootInput);
|
|
10130
10172
|
const manifestPath = join3(root, ".forgezero", "project.json");
|
|
10131
|
-
if (!
|
|
10173
|
+
if (!existsSync4(manifestPath))
|
|
10132
10174
|
return { ok: false, problems: ["missing .forgezero/project.json"] };
|
|
10133
10175
|
let manifest;
|
|
10134
10176
|
try {
|
|
10135
|
-
manifest = parseProjectContext(JSON.parse(
|
|
10177
|
+
manifest = parseProjectContext(JSON.parse(readFileSync4(manifestPath, "utf8")));
|
|
10136
10178
|
} catch (cause) {
|
|
10137
10179
|
return { ok: false, problems: [cause instanceof Error ? cause.message : String(cause)] };
|
|
10138
10180
|
}
|
|
10139
10181
|
const problems = [];
|
|
10140
10182
|
for (const source of manifest.truth) {
|
|
10141
|
-
if (!
|
|
10183
|
+
if (!existsSync4(join3(root, source.path)))
|
|
10142
10184
|
problems.push(`missing truth source: ${source.path}`);
|
|
10143
10185
|
}
|
|
10144
10186
|
for (const path of manifest.readFirst) {
|
|
10145
|
-
if (!
|
|
10187
|
+
if (!existsSync4(join3(root, path)))
|
|
10146
10188
|
problems.push(`missing read-first file: ${path}`);
|
|
10147
10189
|
}
|
|
10148
10190
|
for (const file of projectContextFiles(manifest)) {
|
|
10149
10191
|
const path = join3(root, file.path);
|
|
10150
|
-
if (!
|
|
10192
|
+
if (!existsSync4(path))
|
|
10151
10193
|
problems.push(`missing generated adapter: ${file.path}`);
|
|
10152
|
-
else if (
|
|
10194
|
+
else if (readFileSync4(path, "utf8") !== file.content)
|
|
10153
10195
|
problems.push(`drifted generated adapter: ${file.path}`);
|
|
10154
10196
|
}
|
|
10155
10197
|
return { ok: problems.length === 0, problems };
|
|
@@ -10157,7 +10199,7 @@ function checkProjectContext(rootInput) {
|
|
|
10157
10199
|
|
|
10158
10200
|
// src/deploy-file.ts
|
|
10159
10201
|
import { createHash } from "crypto";
|
|
10160
|
-
import { existsSync as
|
|
10202
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
10161
10203
|
import { basename, join as join4 } from "path";
|
|
10162
10204
|
var DEPLOY_FILE = ".fz/deploy.json";
|
|
10163
10205
|
var DEPLOY_TODO_PREFIX = "ForgeZero pipeline TODO:";
|
|
@@ -10178,17 +10220,17 @@ var safeName = (value) => {
|
|
|
10178
10220
|
};
|
|
10179
10221
|
function packageHints(root) {
|
|
10180
10222
|
const packagePath = join4(root, "package.json");
|
|
10181
|
-
if (!
|
|
10182
|
-
return { bun:
|
|
10223
|
+
if (!existsSync5(packagePath))
|
|
10224
|
+
return { bun: existsSync5(join4(root, "bun.lock")) };
|
|
10183
10225
|
try {
|
|
10184
|
-
const manifest = JSON.parse(
|
|
10226
|
+
const manifest = JSON.parse(readFileSync5(packagePath, "utf8"));
|
|
10185
10227
|
return {
|
|
10186
10228
|
name: typeof manifest.name === "string" ? manifest.name : undefined,
|
|
10187
10229
|
build: typeof manifest.scripts?.build === "string" ? ["bun", "run", "build"] : undefined,
|
|
10188
|
-
bun:
|
|
10230
|
+
bun: existsSync5(join4(root, "bun.lock")) || existsSync5(join4(root, "bun.lockb"))
|
|
10189
10231
|
};
|
|
10190
10232
|
} catch {
|
|
10191
|
-
return { bun:
|
|
10233
|
+
return { bun: existsSync5(join4(root, "bun.lock")) };
|
|
10192
10234
|
}
|
|
10193
10235
|
}
|
|
10194
10236
|
var blocker = (instruction) => ["fz-agent", "pipeline-todo", `${DEPLOY_TODO_PREFIX} ${instruction}`];
|
|
@@ -10225,9 +10267,9 @@ function defaultDeployFile(root, options = {}) {
|
|
|
10225
10267
|
}
|
|
10226
10268
|
function inspectDeployFile(root, options = {}) {
|
|
10227
10269
|
const path = join4(root, DEPLOY_FILE);
|
|
10228
|
-
if (!
|
|
10270
|
+
if (!existsSync5(path))
|
|
10229
10271
|
throw new Error(`${DEPLOY_FILE} does not exist; run \`fz deploy init\`.`);
|
|
10230
|
-
const raw = JSON.parse(
|
|
10272
|
+
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
10231
10273
|
const definition = parseDeployDefinition(raw, options);
|
|
10232
10274
|
const problems = definition.steps.filter((step2) => step2.exec.some((argument) => argument.includes(DEPLOY_TODO_PREFIX))).map((step2) => `${step2.name} still contains the safe initialization blocker`);
|
|
10233
10275
|
const profiles = Object.keys(definition.profiles).sort();
|
|
@@ -10251,17 +10293,1060 @@ function inspectDeployFile(root, options = {}) {
|
|
|
10251
10293
|
}
|
|
10252
10294
|
function initializeDeployFile(root, options = {}) {
|
|
10253
10295
|
const path = join4(root, DEPLOY_FILE);
|
|
10254
|
-
if (
|
|
10296
|
+
if (existsSync5(path) && !options.force) {
|
|
10255
10297
|
throw new Error(`${DEPLOY_FILE} already exists; use --force only when replacing it deliberately.`);
|
|
10256
10298
|
}
|
|
10257
10299
|
const raw = defaultDeployFile(root, options);
|
|
10258
10300
|
parseDeployDefinition(raw, { channel: options.channel });
|
|
10259
|
-
|
|
10260
|
-
|
|
10301
|
+
mkdirSync5(join4(root, ".fz"), { recursive: true });
|
|
10302
|
+
writeFileSync5(path, `${JSON.stringify(raw, null, 2)}
|
|
10261
10303
|
`, { mode: 420 });
|
|
10262
10304
|
return inspectDeployFile(root, { channel: options.channel });
|
|
10263
10305
|
}
|
|
10264
10306
|
|
|
10307
|
+
// src/deploy-compiler.ts
|
|
10308
|
+
import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
10309
|
+
import { dirname as dirname5, isAbsolute, relative, resolve as resolve4, sep as sep3 } from "path";
|
|
10310
|
+
import { pathToFileURL } from "url";
|
|
10311
|
+
|
|
10312
|
+
// src/deploy-plan.ts
|
|
10313
|
+
import { createHash as createHash2 } from "crypto";
|
|
10314
|
+
|
|
10315
|
+
// src/deploy-actions.ts
|
|
10316
|
+
var row = (value, where) => {
|
|
10317
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
10318
|
+
throw new Error(`${where} must be an object`);
|
|
10319
|
+
return value;
|
|
10320
|
+
};
|
|
10321
|
+
var exact = (value, keys, where) => {
|
|
10322
|
+
const unknown = Object.keys(value).filter((key) => !keys.includes(key));
|
|
10323
|
+
if (unknown.length)
|
|
10324
|
+
throw new Error(`${where} contains unknown field(s): ${unknown.join(", ")}`);
|
|
10325
|
+
for (const key of keys)
|
|
10326
|
+
if (value[key] === undefined)
|
|
10327
|
+
throw new Error(`${where} requires ${key}`);
|
|
10328
|
+
};
|
|
10329
|
+
var component = (value, components, where) => {
|
|
10330
|
+
if (typeof value !== "string" || !components[value])
|
|
10331
|
+
throw new Error(`${where}.component must name an existing component`);
|
|
10332
|
+
return components[value];
|
|
10333
|
+
};
|
|
10334
|
+
var exactArgv = (value, where) => {
|
|
10335
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 256 || value.some((entry) => typeof entry !== "string" || entry.length < 1 || entry.length > 16384 || entry.includes("\x00")))
|
|
10336
|
+
throw new Error(`${where} requires a bounded exact argv array`);
|
|
10337
|
+
const executable = value[0].split("/").at(-1).toLowerCase();
|
|
10338
|
+
if (["sh", "bash", "dash", "zsh", "ksh", "fish", "busybox", "env"].includes(executable))
|
|
10339
|
+
throw new Error(`${where} cannot invoke a command dispatcher`);
|
|
10340
|
+
};
|
|
10341
|
+
function validateBuiltInAction(definition, context, where) {
|
|
10342
|
+
if (!definition.uses.startsWith("forgezero."))
|
|
10343
|
+
return;
|
|
10344
|
+
const withValue = row(definition.with, `${where}.with`);
|
|
10345
|
+
if (definition.uses === "forgezero.software/ensure@1") {
|
|
10346
|
+
exact(withValue, ["requirements"], `${where}.with`);
|
|
10347
|
+
if (!Array.isArray(withValue.requirements) || withValue.requirements.length < 1 || withValue.requirements.length > 64 || withValue.requirements.some((name) => typeof name !== "string" || !context.requirements[name]))
|
|
10348
|
+
throw new Error(`${where}.with.requirements must name existing requirements`);
|
|
10349
|
+
return;
|
|
10350
|
+
}
|
|
10351
|
+
if (definition.uses === "forgezero.exec/argv@1") {
|
|
10352
|
+
const allowed = ["component", "argv", "credentials"];
|
|
10353
|
+
const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
|
|
10354
|
+
if (unknown.length)
|
|
10355
|
+
throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
|
|
10356
|
+
component(withValue.component, context.components, `${where}.with`);
|
|
10357
|
+
exactArgv(withValue.argv, `${where}.with.argv`);
|
|
10358
|
+
if (withValue.credentials !== undefined) {
|
|
10359
|
+
const bindings = row(withValue.credentials, `${where}.with.credentials`);
|
|
10360
|
+
for (const [environment, credentialName] of Object.entries(bindings))
|
|
10361
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(environment) || typeof credentialName !== "string" || !context.credentials[credentialName])
|
|
10362
|
+
throw new Error(`${where}.with.credentials contains an invalid binding`);
|
|
10363
|
+
}
|
|
10364
|
+
return;
|
|
10365
|
+
}
|
|
10366
|
+
if (["forgezero.oci/build@1", "forgezero.oci/pull@1"].includes(definition.uses)) {
|
|
10367
|
+
const allowed = definition.uses.endsWith("/build@1") ? ["component", "noCache"] : ["component"];
|
|
10368
|
+
const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
|
|
10369
|
+
if (unknown.length)
|
|
10370
|
+
throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
|
|
10371
|
+
const selected = component(withValue.component, context.components, `${where}.with`);
|
|
10372
|
+
if (selected.kind !== "application" || selected.runtime.kind !== "container")
|
|
10373
|
+
throw new Error(`${where}.with.component must use a container runtime`);
|
|
10374
|
+
if (withValue.noCache !== undefined && typeof withValue.noCache !== "boolean")
|
|
10375
|
+
throw new Error(`${where}.with.noCache must be a boolean`);
|
|
10376
|
+
return;
|
|
10377
|
+
}
|
|
10378
|
+
if (["forgezero.service/deploy@1", "forgezero.service/promote@1", "forgezero.health/http@1", "forgezero.storage/verify@1"].includes(definition.uses)) {
|
|
10379
|
+
const allowed = definition.uses === "forgezero.service/deploy@1" || definition.uses === "forgezero.service/promote@1" ? ["component", "imageDigest"] : ["component"];
|
|
10380
|
+
const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
|
|
10381
|
+
if (unknown.length)
|
|
10382
|
+
throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
|
|
10383
|
+
component(withValue.component, context.components, `${where}.with`);
|
|
10384
|
+
return;
|
|
10385
|
+
}
|
|
10386
|
+
throw new Error(`${where}.uses is an unknown ForgeZero action`);
|
|
10387
|
+
}
|
|
10388
|
+
// src/deploy-providers.ts
|
|
10389
|
+
var plain = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
10390
|
+
var exact2 = (row2, keys, provider) => {
|
|
10391
|
+
const unknown = Object.keys(row2).filter((key) => !keys.includes(key));
|
|
10392
|
+
if (unknown.length)
|
|
10393
|
+
throw new Error(`${provider} config contains unknown field(s): ${unknown.join(", ")}`);
|
|
10394
|
+
for (const key of keys)
|
|
10395
|
+
if (row2[key] === undefined)
|
|
10396
|
+
throw new Error(`${provider} config requires ${key}`);
|
|
10397
|
+
};
|
|
10398
|
+
var absolute = (value) => typeof value === "string" && /^\/(?:[A-Za-z0-9._-]+\/?)*$/.test(value) && !value.includes("..");
|
|
10399
|
+
function validateBuiltInProvider(requirement2, where) {
|
|
10400
|
+
const row2 = plain(requirement2.config);
|
|
10401
|
+
if (!requirement2.provider.startsWith("forgezero."))
|
|
10402
|
+
return;
|
|
10403
|
+
const expected = {
|
|
10404
|
+
"forgezero.bun": { capability: "runtime.javascript", versions: ["1.3.14"], keys: ["installScope"] },
|
|
10405
|
+
"forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
|
|
10406
|
+
"forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
|
|
10407
|
+
"forgezero.arangodb": { capability: "database.arangodb", versions: ["3.11.14"], keys: ["installScope", "runtime", "dataPath", "bind"] },
|
|
10408
|
+
"forgezero.cloudflared": { capability: "network.public-tunnel", versions: ["2026.7.3"], keys: ["installScope", "mode"] },
|
|
10409
|
+
"forgezero.warp": { capability: "network.private", versions: ["2026.6.822.0-min"], keys: ["installScope", "mode"] },
|
|
10410
|
+
"forgezero.ufw": { capability: "security.firewall", versions: ["ubuntu-26.04"], keys: ["installScope", "defaultIncoming", "defaultOutgoing"] },
|
|
10411
|
+
"forgezero.openssh": { capability: "transport.ssh", versions: ["ubuntu-26.04"], keys: ["installScope", "mode"] },
|
|
10412
|
+
"forgezero.systemd": { capability: "service.manager", versions: ["ubuntu-26.04"], keys: ["installScope", "credentials", "serviceSandbox"] }
|
|
10413
|
+
};
|
|
10414
|
+
const contract = expected[requirement2.provider];
|
|
10415
|
+
if (!contract)
|
|
10416
|
+
throw new Error(`${where} uses unknown built-in provider ${requirement2.provider}`);
|
|
10417
|
+
if (requirement2.contract !== 1 || requirement2.capability !== contract.capability || !contract.versions.includes(requirement2.version))
|
|
10418
|
+
throw new Error(`${where} provider coordinate is unsupported`);
|
|
10419
|
+
exact2(row2, contract.keys, requirement2.provider);
|
|
10420
|
+
if (row2.installScope !== "host")
|
|
10421
|
+
throw new Error(`${where}.config.installScope must be host`);
|
|
10422
|
+
if (requirement2.provider === "forgezero.docker") {
|
|
10423
|
+
if (row2.storageDriver !== "overlay2" || row2.dataRoot !== "/var/lib/docker" || row2.liveRestore !== true || row2.logDriver !== "local" || row2.defaultNetwork !== "bridge" || row2.rootless !== false)
|
|
10424
|
+
throw new Error(`${where} Docker config is unsupported by the fixed host strategy`);
|
|
10425
|
+
}
|
|
10426
|
+
if (requirement2.provider === "forgezero.nginx" && (typeof row2.websocket !== "boolean" || !Number.isInteger(row2.maximumBodyMiB) || Number(row2.maximumBodyMiB) < 1 || Number(row2.maximumBodyMiB) > 1024 || !(row2.workerProcesses === "auto" || Number.isInteger(row2.workerProcesses)) || !Number.isInteger(row2.workerConnections)))
|
|
10427
|
+
throw new Error(`${where} Nginx config is invalid`);
|
|
10428
|
+
if (requirement2.provider === "forgezero.arangodb" && (row2.runtime !== "native" || !absolute(row2.dataPath) || row2.bind !== "private"))
|
|
10429
|
+
throw new Error(`${where} ArangoDB config is invalid`);
|
|
10430
|
+
}
|
|
10431
|
+
|
|
10432
|
+
// src/deploy-plan.ts
|
|
10433
|
+
var DEPLOY_PLAN_FORMAT = "forgezero-deployment-plan";
|
|
10434
|
+
var DEPLOY_PLAN_VERSION = 1;
|
|
10435
|
+
|
|
10436
|
+
class DeploymentPlanError extends Error {
|
|
10437
|
+
constructor(message) {
|
|
10438
|
+
super(message);
|
|
10439
|
+
this.name = "DeploymentPlanError";
|
|
10440
|
+
}
|
|
10441
|
+
}
|
|
10442
|
+
var NAME3 = /^[a-z][A-Za-z0-9-]{0,62}$/;
|
|
10443
|
+
var COORDINATE = /^[a-z][a-z0-9.-]{0,127}$/;
|
|
10444
|
+
var ACTION = /^[a-z][a-z0-9.-]{0,127}\/[a-z][a-z0-9.-]{0,127}@[1-9][0-9]{0,5}$/;
|
|
10445
|
+
var VERSION3 = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,63}$/;
|
|
10446
|
+
var ABSOLUTE_PATH = /^\/(?:[A-Za-z0-9._-]+\/?)*$/;
|
|
10447
|
+
var HEALTH_PATH = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{0,255}$/;
|
|
10448
|
+
var fail2 = (where, message) => {
|
|
10449
|
+
throw new DeploymentPlanError(`${where} ${message}`);
|
|
10450
|
+
};
|
|
10451
|
+
var object = (value, where) => {
|
|
10452
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
10453
|
+
fail2(where, "must be an object.");
|
|
10454
|
+
return value;
|
|
10455
|
+
};
|
|
10456
|
+
var exact3 = (value, allowed, where) => {
|
|
10457
|
+
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
10458
|
+
if (unknown.length)
|
|
10459
|
+
fail2(where, `contains unknown field(s): ${unknown.join(", ")}.`);
|
|
10460
|
+
};
|
|
10461
|
+
var named = (value, where) => {
|
|
10462
|
+
if (typeof value !== "string" || !NAME3.test(value))
|
|
10463
|
+
fail2(where, "must be a lowercase typed name.");
|
|
10464
|
+
return value;
|
|
10465
|
+
};
|
|
10466
|
+
var boundedString = (value, where, maximum = 256) => {
|
|
10467
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maximum || value.includes("\x00"))
|
|
10468
|
+
fail2(where, `must be a non-empty string of at most ${maximum} characters.`);
|
|
10469
|
+
return value;
|
|
10470
|
+
};
|
|
10471
|
+
var integer = (value, where, minimum, maximum) => {
|
|
10472
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum)
|
|
10473
|
+
fail2(where, `must be an integer from ${minimum} to ${maximum}.`);
|
|
10474
|
+
return value;
|
|
10475
|
+
};
|
|
10476
|
+
var number = (value, where, minimum, maximum) => {
|
|
10477
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum)
|
|
10478
|
+
fail2(where, `must be a finite number from ${minimum} to ${maximum}.`);
|
|
10479
|
+
return value;
|
|
10480
|
+
};
|
|
10481
|
+
function canonicalJson(value) {
|
|
10482
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
10483
|
+
return JSON.stringify(value);
|
|
10484
|
+
if (typeof value === "number") {
|
|
10485
|
+
if (!Number.isFinite(value))
|
|
10486
|
+
throw new DeploymentPlanError("deployment data contains a non-finite number.");
|
|
10487
|
+
return JSON.stringify(value);
|
|
10488
|
+
}
|
|
10489
|
+
if (Array.isArray(value))
|
|
10490
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
10491
|
+
if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
10492
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
10493
|
+
}
|
|
10494
|
+
throw new DeploymentPlanError("deployment data must contain only plain JSON values.");
|
|
10495
|
+
}
|
|
10496
|
+
function deploymentSourceDigest(source) {
|
|
10497
|
+
return `sha256:${createHash2("sha256").update(canonicalJson(source)).digest("hex")}`;
|
|
10498
|
+
}
|
|
10499
|
+
function reference(value, where) {
|
|
10500
|
+
const row2 = object(value, where);
|
|
10501
|
+
exact3(row2, ["$ref"], where);
|
|
10502
|
+
const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
|
|
10503
|
+
if (!/^(inputs\.[a-z][A-Za-z0-9-]{0,62}|steps\.[a-z][A-Za-z0-9-]{0,62}\.(status|outputs\.[a-z][A-Za-z0-9-]{0,62})|components\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|targets\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|deployment\.[a-z][A-Za-z0-9-]{0,62})$/.test(coordinate)) {
|
|
10504
|
+
fail2(where, "contains an unsupported reference.");
|
|
10505
|
+
}
|
|
10506
|
+
return { $ref: coordinate };
|
|
10507
|
+
}
|
|
10508
|
+
function operand(value, where) {
|
|
10509
|
+
if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
|
|
10510
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
10511
|
+
fail2(where, "must be finite.");
|
|
10512
|
+
return value;
|
|
10513
|
+
}
|
|
10514
|
+
return reference(value, where);
|
|
10515
|
+
}
|
|
10516
|
+
function condition(value, where, depth = 0) {
|
|
10517
|
+
if (depth > 16)
|
|
10518
|
+
fail2(where, "is nested too deeply.");
|
|
10519
|
+
const row2 = object(value, where);
|
|
10520
|
+
if (Object.keys(row2).length !== 1)
|
|
10521
|
+
fail2(where, "must contain exactly one operator.");
|
|
10522
|
+
const [operator, body] = Object.entries(row2)[0];
|
|
10523
|
+
if (operator === "all" || operator === "any") {
|
|
10524
|
+
if (!Array.isArray(body) || body.length < 1 || body.length > 32)
|
|
10525
|
+
fail2(where, `${operator} must contain 1 to 32 conditions.`);
|
|
10526
|
+
const entries = body;
|
|
10527
|
+
return { [operator]: entries.map((entry, index) => condition(entry, `${where}.${operator}[${index}]`, depth + 1)) };
|
|
10528
|
+
}
|
|
10529
|
+
if (operator === "not")
|
|
10530
|
+
return { not: condition(body, `${where}.not`, depth + 1) };
|
|
10531
|
+
if (["equals", "notEquals", "greaterThan", "greaterThanOrEqual", "lessThan", "lessThanOrEqual", "contains", "startsWith"].includes(operator)) {
|
|
10532
|
+
if (!Array.isArray(body) || body.length !== 2)
|
|
10533
|
+
fail2(where, `${operator} must contain two operands.`);
|
|
10534
|
+
const entries = body;
|
|
10535
|
+
return { [operator]: [operand(entries[0], `${where}.${operator}[0]`), operand(entries[1], `${where}.${operator}[1]`)] };
|
|
10536
|
+
}
|
|
10537
|
+
if (operator === "exists")
|
|
10538
|
+
return { exists: reference(body, `${where}.exists`) };
|
|
10539
|
+
if (["succeeded", "failed", "changed"].includes(operator))
|
|
10540
|
+
return { [operator]: named(body, `${where}.${operator}`) };
|
|
10541
|
+
return fail2(where, `uses unsupported operator ${operator}.`);
|
|
10542
|
+
}
|
|
10543
|
+
function jsonValue(value, where, depth = 0) {
|
|
10544
|
+
if (depth > 16)
|
|
10545
|
+
fail2(where, "is nested too deeply.");
|
|
10546
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
10547
|
+
return value;
|
|
10548
|
+
if (typeof value === "number") {
|
|
10549
|
+
if (!Number.isFinite(value))
|
|
10550
|
+
fail2(where, "must be finite.");
|
|
10551
|
+
return value;
|
|
10552
|
+
}
|
|
10553
|
+
if (Array.isArray(value)) {
|
|
10554
|
+
if (value.length > 1000)
|
|
10555
|
+
fail2(where, "contains more than 1000 entries.");
|
|
10556
|
+
return value.map((entry, index) => jsonValue(entry, `${where}[${index}]`, depth + 1));
|
|
10557
|
+
}
|
|
10558
|
+
const row2 = object(value, where);
|
|
10559
|
+
if (Object.keys(row2).length > 1000)
|
|
10560
|
+
fail2(where, "contains more than 1000 fields.");
|
|
10561
|
+
return Object.fromEntries(Object.entries(row2).map(([key, entry]) => [key, jsonValue(entry, `${where}.${key}`, depth + 1)]));
|
|
10562
|
+
}
|
|
10563
|
+
function validateInput(value, where) {
|
|
10564
|
+
const row2 = object(value, where);
|
|
10565
|
+
const type = row2.type;
|
|
10566
|
+
const common = ["type", "required", "default"];
|
|
10567
|
+
if (row2.required !== undefined && typeof row2.required !== "boolean")
|
|
10568
|
+
fail2(`${where}.required`, "must be a boolean.");
|
|
10569
|
+
if (type === "string") {
|
|
10570
|
+
exact3(row2, [...common, "minimumLength", "maximumLength", "pattern"], where);
|
|
10571
|
+
if (row2.default !== undefined)
|
|
10572
|
+
boundedString(row2.default, `${where}.default`, 16384);
|
|
10573
|
+
if (row2.minimumLength !== undefined)
|
|
10574
|
+
integer(row2.minimumLength, `${where}.minimumLength`, 0, 16384);
|
|
10575
|
+
if (row2.maximumLength !== undefined)
|
|
10576
|
+
integer(row2.maximumLength, `${where}.maximumLength`, 1, 16384);
|
|
10577
|
+
if (row2.pattern !== undefined) {
|
|
10578
|
+
const pattern = boundedString(row2.pattern, `${where}.pattern`, 512);
|
|
10579
|
+
try {
|
|
10580
|
+
new RegExp(pattern);
|
|
10581
|
+
} catch {
|
|
10582
|
+
fail2(`${where}.pattern`, "must be a valid regular expression.");
|
|
10583
|
+
}
|
|
10584
|
+
}
|
|
10585
|
+
} else if (type === "integer" || type === "number") {
|
|
10586
|
+
exact3(row2, [...common, "minimum", "maximum"], where);
|
|
10587
|
+
if (row2.default !== undefined)
|
|
10588
|
+
number(row2.default, `${where}.default`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
|
10589
|
+
if (row2.minimum !== undefined)
|
|
10590
|
+
number(row2.minimum, `${where}.minimum`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
|
10591
|
+
if (row2.maximum !== undefined)
|
|
10592
|
+
number(row2.maximum, `${where}.maximum`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
|
10593
|
+
if (type === "integer" && row2.default !== undefined && !Number.isInteger(row2.default))
|
|
10594
|
+
fail2(`${where}.default`, "must be an integer.");
|
|
10595
|
+
} else if (type === "boolean") {
|
|
10596
|
+
exact3(row2, common, where);
|
|
10597
|
+
if (row2.default !== undefined && typeof row2.default !== "boolean")
|
|
10598
|
+
fail2(`${where}.default`, "must be a boolean.");
|
|
10599
|
+
} else if (type === "enum") {
|
|
10600
|
+
exact3(row2, [...common, "values"], where);
|
|
10601
|
+
if (!Array.isArray(row2.values) || row2.values.length < 1 || row2.values.length > 64)
|
|
10602
|
+
fail2(`${where}.values`, "must contain 1 to 64 values.");
|
|
10603
|
+
const values = row2.values.map((entry, index) => boundedString(entry, `${where}.values[${index}]`, 128));
|
|
10604
|
+
if (new Set(values).size !== values.length)
|
|
10605
|
+
fail2(`${where}.values`, "must be unique.");
|
|
10606
|
+
if (row2.default !== undefined && !values.includes(row2.default))
|
|
10607
|
+
fail2(`${where}.default`, "must be an enum value.");
|
|
10608
|
+
} else if (type === "hostname") {
|
|
10609
|
+
exact3(row2, common, where);
|
|
10610
|
+
if (row2.default !== undefined && !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(String(row2.default)))
|
|
10611
|
+
fail2(`${where}.default`, "must be a hostname.");
|
|
10612
|
+
} else
|
|
10613
|
+
fail2(`${where}.type`, "is unsupported.");
|
|
10614
|
+
return row2;
|
|
10615
|
+
}
|
|
10616
|
+
function validateRequirement(value, where) {
|
|
10617
|
+
const row2 = object(value, where);
|
|
10618
|
+
exact3(row2, ["capability", "provider", "contract", "version", "config", "when"], where);
|
|
10619
|
+
if (!COORDINATE.test(String(row2.capability)))
|
|
10620
|
+
fail2(`${where}.capability`, "is invalid.");
|
|
10621
|
+
if (!COORDINATE.test(String(row2.provider)))
|
|
10622
|
+
fail2(`${where}.provider`, "is invalid.");
|
|
10623
|
+
integer(row2.contract, `${where}.contract`, 1, 65535);
|
|
10624
|
+
if (typeof row2.version !== "string" || !VERSION3.test(row2.version))
|
|
10625
|
+
fail2(`${where}.version`, "is invalid.");
|
|
10626
|
+
jsonValue(row2.config, `${where}.config`);
|
|
10627
|
+
if (row2.when !== undefined)
|
|
10628
|
+
condition(row2.when, `${where}.when`);
|
|
10629
|
+
const requirement2 = row2;
|
|
10630
|
+
validateBuiltInProvider(requirement2, where);
|
|
10631
|
+
return requirement2;
|
|
10632
|
+
}
|
|
10633
|
+
function validateArgv(value, where) {
|
|
10634
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 256)
|
|
10635
|
+
fail2(where, "must contain 1 to 256 arguments.");
|
|
10636
|
+
let bytes = 0;
|
|
10637
|
+
const values = value.map((entry, index) => {
|
|
10638
|
+
const result = boundedString(entry, `${where}[${index}]`, 16384);
|
|
10639
|
+
bytes += Buffer.byteLength(result);
|
|
10640
|
+
return result;
|
|
10641
|
+
});
|
|
10642
|
+
if (bytes > 64 * 1024)
|
|
10643
|
+
fail2(where, "is larger than 64 KiB.");
|
|
10644
|
+
const executable = values[0].split("/").at(-1).toLowerCase();
|
|
10645
|
+
if (["sh", "bash", "dash", "zsh", "ksh", "fish", "busybox", "env"].includes(executable))
|
|
10646
|
+
fail2(`${where}[0]`, "may not invoke a command dispatcher.");
|
|
10647
|
+
return values;
|
|
10648
|
+
}
|
|
10649
|
+
function validateResources(value, where) {
|
|
10650
|
+
const row2 = object(value, where);
|
|
10651
|
+
exact3(row2, ["cpu", "memory", "pids", "io"], where);
|
|
10652
|
+
if (row2.cpu !== undefined) {
|
|
10653
|
+
const cpu = object(row2.cpu, `${where}.cpu`);
|
|
10654
|
+
exact3(cpu, ["limit", "weight", "pinning"], `${where}.cpu`);
|
|
10655
|
+
number(cpu.limit, `${where}.cpu.limit`, 0.01, 1024);
|
|
10656
|
+
if (cpu.weight !== undefined)
|
|
10657
|
+
integer(cpu.weight, `${where}.cpu.weight`, 1, 1e4);
|
|
10658
|
+
if (cpu.pinning !== undefined && !["automatic", "dedicated"].includes(String(cpu.pinning)))
|
|
10659
|
+
fail2(`${where}.cpu.pinning`, "is unsupported.");
|
|
10660
|
+
}
|
|
10661
|
+
if (row2.memory !== undefined) {
|
|
10662
|
+
const memory = object(row2.memory, `${where}.memory`);
|
|
10663
|
+
exact3(memory, ["limitMiB", "reservationMiB", "swap"], `${where}.memory`);
|
|
10664
|
+
integer(memory.limitMiB, `${where}.memory.limitMiB`, 16, 16777216);
|
|
10665
|
+
if (memory.reservationMiB !== undefined && integer(memory.reservationMiB, `${where}.memory.reservationMiB`, 1, memory.limitMiB) > memory.limitMiB)
|
|
10666
|
+
fail2(`${where}.memory.reservationMiB`, "must not exceed limitMiB.");
|
|
10667
|
+
if (memory.swap !== undefined && !["disabled", "bounded"].includes(String(memory.swap)))
|
|
10668
|
+
fail2(`${where}.memory.swap`, "is unsupported.");
|
|
10669
|
+
}
|
|
10670
|
+
if (row2.pids !== undefined) {
|
|
10671
|
+
const pids = object(row2.pids, `${where}.pids`);
|
|
10672
|
+
exact3(pids, ["limit"], `${where}.pids`);
|
|
10673
|
+
integer(pids.limit, `${where}.pids.limit`, 1, 1048576);
|
|
10674
|
+
}
|
|
10675
|
+
if (row2.io !== undefined) {
|
|
10676
|
+
const io = object(row2.io, `${where}.io`);
|
|
10677
|
+
exact3(io, ["weight", "readBps", "writeBps", "readIops", "writeIops"], `${where}.io`);
|
|
10678
|
+
for (const key of Object.keys(io))
|
|
10679
|
+
integer(io[key], `${where}.io.${key}`, 1, Number.MAX_SAFE_INTEGER);
|
|
10680
|
+
}
|
|
10681
|
+
}
|
|
10682
|
+
function validateStorage(value, where) {
|
|
10683
|
+
const row2 = object(value, where);
|
|
10684
|
+
if (row2.class === "ephemeral") {
|
|
10685
|
+
exact3(row2, ["class", "path", "type", "sizeMiB"], where);
|
|
10686
|
+
if (row2.type !== "tmpfs")
|
|
10687
|
+
fail2(`${where}.type`, "must be tmpfs.");
|
|
10688
|
+
integer(row2.sizeMiB, `${where}.sizeMiB`, 1, 1048576);
|
|
10689
|
+
} else if (row2.class === "persistent") {
|
|
10690
|
+
exact3(row2, ["class", "name", "path", "minimumFreePercent"], where);
|
|
10691
|
+
named(row2.name, `${where}.name`);
|
|
10692
|
+
if (row2.minimumFreePercent !== undefined)
|
|
10693
|
+
integer(row2.minimumFreePercent, `${where}.minimumFreePercent`, 1, 95);
|
|
10694
|
+
} else if (row2.class === "database") {
|
|
10695
|
+
exact3(row2, ["class", "name", "path", "minimumFreePercent", "minimumIops", "latencyTargetMs"], where);
|
|
10696
|
+
named(row2.name, `${where}.name`);
|
|
10697
|
+
integer(row2.minimumFreePercent, `${where}.minimumFreePercent`, 1, 95);
|
|
10698
|
+
if (row2.minimumIops !== undefined)
|
|
10699
|
+
integer(row2.minimumIops, `${where}.minimumIops`, 1, 1e8);
|
|
10700
|
+
if (row2.latencyTargetMs !== undefined)
|
|
10701
|
+
number(row2.latencyTargetMs, `${where}.latencyTargetMs`, 0.01, 60000);
|
|
10702
|
+
} else
|
|
10703
|
+
fail2(`${where}.class`, "is unsupported.");
|
|
10704
|
+
if (typeof row2.path !== "string" || !ABSOLUTE_PATH.test(row2.path) || row2.path.includes(".."))
|
|
10705
|
+
fail2(`${where}.path`, "must be a normalized absolute path.");
|
|
10706
|
+
}
|
|
10707
|
+
function validateNetwork(value, where) {
|
|
10708
|
+
const row2 = object(value, where);
|
|
10709
|
+
exact3(row2, ["ingress", "private", "public", "container"], where);
|
|
10710
|
+
if (row2.ingress !== undefined) {
|
|
10711
|
+
const ingress = object(row2.ingress, `${where}.ingress`);
|
|
10712
|
+
exact3(ingress, ["exposure", "stablePort"], `${where}.ingress`);
|
|
10713
|
+
if (!["loopback", "private", "public"].includes(String(ingress.exposure)))
|
|
10714
|
+
fail2(`${where}.ingress.exposure`, "is unsupported.");
|
|
10715
|
+
if (ingress.stablePort !== undefined)
|
|
10716
|
+
integer(ingress.stablePort, `${where}.ingress.stablePort`, 1, 65535);
|
|
10717
|
+
}
|
|
10718
|
+
if (row2.private !== undefined) {
|
|
10719
|
+
const privateNetwork = object(row2.private, `${where}.private`);
|
|
10720
|
+
exact3(privateNetwork, ["mode", "preferences"], `${where}.private`);
|
|
10721
|
+
if (!["private-lan", "cloudflare-warp", "auto"].includes(String(privateNetwork.mode)))
|
|
10722
|
+
fail2(`${where}.private.mode`, "is unsupported.");
|
|
10723
|
+
if (privateNetwork.preferences !== undefined && (!Array.isArray(privateNetwork.preferences) || privateNetwork.preferences.length < 1 || privateNetwork.preferences.length > 2 || privateNetwork.preferences.some((entry) => !["private-lan", "cloudflare-warp"].includes(String(entry))) || new Set(privateNetwork.preferences).size !== privateNetwork.preferences.length))
|
|
10724
|
+
fail2(`${where}.private.preferences`, "is invalid.");
|
|
10725
|
+
}
|
|
10726
|
+
if (row2.public !== undefined) {
|
|
10727
|
+
const publicNetwork = object(row2.public, `${where}.public`);
|
|
10728
|
+
exact3(publicNetwork, ["mode", "optional"], `${where}.public`);
|
|
10729
|
+
if (!["disabled", "cloudflare-tunnel"].includes(String(publicNetwork.mode)))
|
|
10730
|
+
fail2(`${where}.public.mode`, "is unsupported.");
|
|
10731
|
+
if (publicNetwork.optional !== undefined && typeof publicNetwork.optional !== "boolean")
|
|
10732
|
+
fail2(`${where}.public.optional`, "must be a boolean.");
|
|
10733
|
+
}
|
|
10734
|
+
if (row2.container !== undefined) {
|
|
10735
|
+
const container = object(row2.container, `${where}.container`);
|
|
10736
|
+
exact3(container, ["mode", "network"], `${where}.container`);
|
|
10737
|
+
if (!["bridge", "host"].includes(String(container.mode)))
|
|
10738
|
+
fail2(`${where}.container.mode`, "is unsupported.");
|
|
10739
|
+
if (container.network !== undefined)
|
|
10740
|
+
named(container.network, `${where}.container.network`);
|
|
10741
|
+
}
|
|
10742
|
+
}
|
|
10743
|
+
function validateComponent(value, where, targetNames, requirements) {
|
|
10744
|
+
const row2 = object(value, where);
|
|
10745
|
+
if (!targetNames.has(String(row2.target)))
|
|
10746
|
+
fail2(`${where}.target`, "must name an existing target.");
|
|
10747
|
+
if (row2.kind === "application") {
|
|
10748
|
+
exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
|
|
10749
|
+
const runtime = object(row2.runtime, `${where}.runtime`);
|
|
10750
|
+
const runtimeRequirement = requirements.get(String(runtime.requirement));
|
|
10751
|
+
if (!runtimeRequirement)
|
|
10752
|
+
fail2(`${where}.runtime.requirement`, "must name an existing requirement.");
|
|
10753
|
+
if (runtime.kind === "native") {
|
|
10754
|
+
exact3(runtime, ["kind", "provider", "requirement", "argv"], `${where}.runtime`);
|
|
10755
|
+
validateArgv(runtime.argv, `${where}.runtime.argv`);
|
|
10756
|
+
} else if (runtime.kind === "container") {
|
|
10757
|
+
exact3(runtime, ["kind", "provider", "requirement", "image", "entrypoint", "security"], `${where}.runtime`);
|
|
10758
|
+
const image = object(runtime.image, `${where}.runtime.image`);
|
|
10759
|
+
exact3(image, ["source"], `${where}.runtime.image`);
|
|
10760
|
+
const source = object(image.source, `${where}.runtime.image.source`);
|
|
10761
|
+
if (source.kind === "build") {
|
|
10762
|
+
exact3(source, ["kind", "context", "dockerfile"], `${where}.runtime.image.source`);
|
|
10763
|
+
boundedString(source.context, `${where}.runtime.image.source.context`, 512);
|
|
10764
|
+
boundedString(source.dockerfile, `${where}.runtime.image.source.dockerfile`, 512);
|
|
10765
|
+
} else if (source.kind === "registry") {
|
|
10766
|
+
exact3(source, ["kind", "reference"], `${where}.runtime.image.source`);
|
|
10767
|
+
if (!/^[-a-zA-Z0-9./:_@]+$/.test(boundedString(source.reference, `${where}.runtime.image.source.reference`, 512)))
|
|
10768
|
+
fail2(`${where}.runtime.image.source.reference`, "is invalid.");
|
|
10769
|
+
} else
|
|
10770
|
+
fail2(`${where}.runtime.image.source.kind`, "is unsupported.");
|
|
10771
|
+
if (runtime.entrypoint !== undefined)
|
|
10772
|
+
validateArgv(runtime.entrypoint, `${where}.runtime.entrypoint`);
|
|
10773
|
+
const security = object(runtime.security, `${where}.runtime.security`);
|
|
10774
|
+
exact3(security, ["privileged", "noNewPrivileges", "root", "dropCapabilities"], `${where}.runtime.security`);
|
|
10775
|
+
if (security.privileged !== false || security.noNewPrivileges !== true)
|
|
10776
|
+
fail2(`${where}.runtime.security`, "must disable privileged mode and enable noNewPrivileges.");
|
|
10777
|
+
if (!["read-only", "writable"].includes(String(security.root)))
|
|
10778
|
+
fail2(`${where}.runtime.security.root`, "is unsupported.");
|
|
10779
|
+
if (!Array.isArray(security.dropCapabilities) || !security.dropCapabilities.includes("ALL"))
|
|
10780
|
+
fail2(`${where}.runtime.security.dropCapabilities`, "must include ALL.");
|
|
10781
|
+
} else
|
|
10782
|
+
fail2(`${where}.runtime.kind`, "is unsupported.");
|
|
10783
|
+
if (!COORDINATE.test(String(runtime.provider)))
|
|
10784
|
+
fail2(`${where}.runtime.provider`, "is invalid.");
|
|
10785
|
+
if (runtimeRequirement && runtime.provider !== runtimeRequirement.provider)
|
|
10786
|
+
fail2(`${where}.runtime.provider`, "must match its requirement provider.");
|
|
10787
|
+
if (runtimeRequirement && runtime.kind === "container" && runtimeRequirement.capability !== "runtime.container")
|
|
10788
|
+
fail2(`${where}.runtime.requirement`, "must provide runtime.container.");
|
|
10789
|
+
if (runtimeRequirement && runtime.kind === "native" && !runtimeRequirement.capability.startsWith("runtime."))
|
|
10790
|
+
fail2(`${where}.runtime.requirement`, "must provide a runtime capability.");
|
|
10791
|
+
const service = object(row2.service, `${where}.service`);
|
|
10792
|
+
exact3(service, ["protocol", "port", "health", "websocket", "maximumConnections"], `${where}.service`);
|
|
10793
|
+
if (service.protocol !== "http")
|
|
10794
|
+
fail2(`${where}.service.protocol`, "must be http.");
|
|
10795
|
+
integer(service.port, `${where}.service.port`, 1, 65535);
|
|
10796
|
+
const health = object(service.health, `${where}.service.health`);
|
|
10797
|
+
exact3(health, ["protocol", "method", "path", "expectedStatus", "timeoutMs", "intervalMs", "attempts"], `${where}.service.health`);
|
|
10798
|
+
if (health.protocol !== "http" || !["GET", "HEAD"].includes(String(health.method)) || typeof health.path !== "string" || !HEALTH_PATH.test(health.path) || health.path.includes("..") || health.path.includes("//"))
|
|
10799
|
+
fail2(`${where}.service.health`, "is invalid.");
|
|
10800
|
+
if (!Array.isArray(health.expectedStatus) || health.expectedStatus.length < 1 || health.expectedStatus.length > 16 || health.expectedStatus.some((status) => !Number.isInteger(status) || status < 100 || status > 599))
|
|
10801
|
+
fail2(`${where}.service.health.expectedStatus`, "is invalid.");
|
|
10802
|
+
integer(health.timeoutMs, `${where}.service.health.timeoutMs`, 100, 300000);
|
|
10803
|
+
if (health.intervalMs !== undefined)
|
|
10804
|
+
integer(health.intervalMs, `${where}.service.health.intervalMs`, 100, 300000);
|
|
10805
|
+
if (health.attempts !== undefined)
|
|
10806
|
+
integer(health.attempts, `${where}.service.health.attempts`, 1, 1000);
|
|
10807
|
+
if (service.websocket !== undefined && typeof service.websocket !== "boolean")
|
|
10808
|
+
fail2(`${where}.service.websocket`, "must be a boolean.");
|
|
10809
|
+
if (service.maximumConnections !== undefined)
|
|
10810
|
+
integer(service.maximumConnections, `${where}.service.maximumConnections`, 1, 1e7);
|
|
10811
|
+
validateResources(row2.resources, `${where}.resources`);
|
|
10812
|
+
if (runtime.kind === "container") {
|
|
10813
|
+
const resources = object(row2.resources, `${where}.resources`);
|
|
10814
|
+
for (const required of ["cpu", "memory", "pids"])
|
|
10815
|
+
if (resources[required] === undefined)
|
|
10816
|
+
fail2(`${where}.resources.${required}`, "is required for a bounded container.");
|
|
10817
|
+
if (row2.network === undefined)
|
|
10818
|
+
fail2(`${where}.network`, "is required for a container.");
|
|
10819
|
+
const network = object(row2.network, `${where}.network`);
|
|
10820
|
+
if (network.container === undefined)
|
|
10821
|
+
fail2(`${where}.network.container`, "must select bridge or reviewed host networking.");
|
|
10822
|
+
}
|
|
10823
|
+
if (row2.storage !== undefined) {
|
|
10824
|
+
if (!Array.isArray(row2.storage) || row2.storage.length > 32)
|
|
10825
|
+
fail2(`${where}.storage`, "must contain at most 32 mounts.");
|
|
10826
|
+
row2.storage.forEach((entry, index) => validateStorage(entry, `${where}.storage[${index}]`));
|
|
10827
|
+
}
|
|
10828
|
+
if (row2.network !== undefined)
|
|
10829
|
+
validateNetwork(row2.network, `${where}.network`);
|
|
10830
|
+
const rollout = object(row2.rollout, `${where}.rollout`);
|
|
10831
|
+
exact3(rollout, ["strategy", "proxy", "drainMs", "automaticRollback"], `${where}.rollout`);
|
|
10832
|
+
if (!["direct", "blue-green", "rolling", "canary"].includes(String(rollout.strategy)))
|
|
10833
|
+
fail2(`${where}.rollout.strategy`, "is unsupported.");
|
|
10834
|
+
if (rollout.proxy !== undefined)
|
|
10835
|
+
named(rollout.proxy, `${where}.rollout.proxy`);
|
|
10836
|
+
if (rollout.strategy !== "direct") {
|
|
10837
|
+
const proxyRequirement = requirements.get(String(rollout.proxy));
|
|
10838
|
+
if (!proxyRequirement || proxyRequirement.capability !== "proxy.http")
|
|
10839
|
+
fail2(`${where}.rollout.proxy`, "must name a proxy.http requirement for managed rollout.");
|
|
10840
|
+
}
|
|
10841
|
+
if (rollout.drainMs !== undefined)
|
|
10842
|
+
integer(rollout.drainMs, `${where}.rollout.drainMs`, 0, 600000);
|
|
10843
|
+
if (rollout.automaticRollback !== undefined && typeof rollout.automaticRollback !== "boolean")
|
|
10844
|
+
fail2(`${where}.rollout.automaticRollback`, "must be a boolean.");
|
|
10845
|
+
} else if (row2.kind === "database") {
|
|
10846
|
+
exact3(row2, ["kind", "target", "runtime", "topology", "resources", "storage", "network"], where);
|
|
10847
|
+
const runtime = object(row2.runtime, `${where}.runtime`);
|
|
10848
|
+
exact3(runtime, ["provider", "requirement", "mode"], `${where}.runtime`);
|
|
10849
|
+
const databaseRequirement = requirements.get(String(runtime.requirement));
|
|
10850
|
+
if (!databaseRequirement)
|
|
10851
|
+
fail2(`${where}.runtime.requirement`, "must name an existing requirement.");
|
|
10852
|
+
if (databaseRequirement && (runtime.provider !== databaseRequirement.provider || databaseRequirement.capability !== "database.arangodb"))
|
|
10853
|
+
fail2(`${where}.runtime.requirement`, "must match a database.arangodb provider requirement.");
|
|
10854
|
+
if (databaseRequirement && databaseRequirement.config.runtime !== runtime.mode)
|
|
10855
|
+
fail2(`${where}.runtime.mode`, "must match the provider runtime mode.");
|
|
10856
|
+
if (runtime.mode !== "native")
|
|
10857
|
+
fail2(`${where}.runtime.mode`, "must be native in deployment plan v1.");
|
|
10858
|
+
const topology = object(row2.topology, `${where}.topology`);
|
|
10859
|
+
if (topology.mode === "standalone")
|
|
10860
|
+
exact3(topology, ["mode"], `${where}.topology`);
|
|
10861
|
+
else if (topology.mode === "cluster") {
|
|
10862
|
+
exact3(topology, ["mode", "bootstrapMembers", "replicationFactor", "writeConcern", "additionalJoiners"], `${where}.topology`);
|
|
10863
|
+
integer(topology.bootstrapMembers, `${where}.topology.bootstrapMembers`, 3, 9);
|
|
10864
|
+
integer(topology.replicationFactor, `${where}.topology.replicationFactor`, 1, 16);
|
|
10865
|
+
integer(topology.writeConcern, `${where}.topology.writeConcern`, 1, topology.replicationFactor);
|
|
10866
|
+
if (!["allowed", "disabled"].includes(String(topology.additionalJoiners)))
|
|
10867
|
+
fail2(`${where}.topology.additionalJoiners`, "is unsupported.");
|
|
10868
|
+
} else
|
|
10869
|
+
fail2(`${where}.topology.mode`, "is unsupported.");
|
|
10870
|
+
if (row2.resources !== undefined)
|
|
10871
|
+
validateResources(row2.resources, `${where}.resources`);
|
|
10872
|
+
validateStorage(row2.storage, `${where}.storage`);
|
|
10873
|
+
if (row2.storage.class !== "database")
|
|
10874
|
+
fail2(`${where}.storage.class`, "must be database.");
|
|
10875
|
+
validateNetwork(row2.network, `${where}.network`);
|
|
10876
|
+
} else
|
|
10877
|
+
fail2(`${where}.kind`, "is unsupported.");
|
|
10878
|
+
return row2;
|
|
10879
|
+
}
|
|
10880
|
+
function validateStep(value, where, targets, context) {
|
|
10881
|
+
const row2 = object(value, where);
|
|
10882
|
+
exact3(row2, ["uses", "with", "if", "scope", "timeoutMs", "retry", "failure", "compensate"], where);
|
|
10883
|
+
if (typeof row2.uses !== "string" || !ACTION.test(row2.uses))
|
|
10884
|
+
fail2(`${where}.uses`, "must be a versioned action coordinate.");
|
|
10885
|
+
jsonValue(row2.with, `${where}.with`);
|
|
10886
|
+
if (row2.if !== undefined)
|
|
10887
|
+
condition(row2.if, `${where}.if`);
|
|
10888
|
+
const scope = object(row2.scope, `${where}.scope`);
|
|
10889
|
+
if (scope.kind === "release-executor")
|
|
10890
|
+
exact3(scope, ["kind"], `${where}.scope`);
|
|
10891
|
+
else if (scope.kind === "elected-one") {
|
|
10892
|
+
exact3(scope, ["kind", "group"], `${where}.scope`);
|
|
10893
|
+
named(scope.group, `${where}.scope.group`);
|
|
10894
|
+
} else if (scope.kind === "each-target") {
|
|
10895
|
+
exact3(scope, ["kind", "target"], `${where}.scope`);
|
|
10896
|
+
if (!targets.has(String(scope.target)))
|
|
10897
|
+
fail2(`${where}.scope.target`, "must name an existing target.");
|
|
10898
|
+
} else if (scope.kind === "target-batches") {
|
|
10899
|
+
exact3(scope, ["kind", "target", "size"], `${where}.scope`);
|
|
10900
|
+
if (!targets.has(String(scope.target)))
|
|
10901
|
+
fail2(`${where}.scope.target`, "must name an existing target.");
|
|
10902
|
+
integer(scope.size, `${where}.scope.size`, 1, 32);
|
|
10903
|
+
} else
|
|
10904
|
+
fail2(`${where}.scope.kind`, "is unsupported.");
|
|
10905
|
+
if (row2.timeoutMs !== undefined)
|
|
10906
|
+
integer(row2.timeoutMs, `${where}.timeoutMs`, 1, 86400000);
|
|
10907
|
+
if (row2.retry !== undefined) {
|
|
10908
|
+
const retry = object(row2.retry, `${where}.retry`);
|
|
10909
|
+
exact3(retry, ["attempts", "backoff", "retryOn"], `${where}.retry`);
|
|
10910
|
+
integer(retry.attempts, `${where}.retry.attempts`, 1, 20);
|
|
10911
|
+
const backoff = object(retry.backoff, `${where}.retry.backoff`);
|
|
10912
|
+
exact3(backoff, ["kind", "initialMs", "maximumMs"], `${where}.retry.backoff`);
|
|
10913
|
+
if (!["fixed", "linear", "exponential"].includes(String(backoff.kind)))
|
|
10914
|
+
fail2(`${where}.retry.backoff.kind`, "is unsupported.");
|
|
10915
|
+
integer(backoff.initialMs, `${where}.retry.backoff.initialMs`, 0, 300000);
|
|
10916
|
+
integer(backoff.maximumMs, `${where}.retry.backoff.maximumMs`, backoff.initialMs, 3600000);
|
|
10917
|
+
if (retry.retryOn !== undefined && (!Array.isArray(retry.retryOn) || retry.retryOn.length > 8 || retry.retryOn.some((entry) => !["network", "timeout", "provider-unavailable", "rate-limited", "conflict"].includes(String(entry))) || new Set(retry.retryOn).size !== retry.retryOn.length))
|
|
10918
|
+
fail2(`${where}.retry.retryOn`, "is invalid.");
|
|
10919
|
+
}
|
|
10920
|
+
if (row2.failure !== undefined) {
|
|
10921
|
+
const failure = object(row2.failure, `${where}.failure`);
|
|
10922
|
+
exact3(failure, ["policy"], `${where}.failure`);
|
|
10923
|
+
if (!["continue", "stop", "rollback-stage", "rollback-deployment", "compensate", "manual-intervention"].includes(String(failure.policy)))
|
|
10924
|
+
fail2(`${where}.failure.policy`, "is unsupported.");
|
|
10925
|
+
}
|
|
10926
|
+
if (row2.compensate !== undefined) {
|
|
10927
|
+
const compensate = object(row2.compensate, `${where}.compensate`);
|
|
10928
|
+
exact3(compensate, ["uses", "with"], `${where}.compensate`);
|
|
10929
|
+
if (typeof compensate.uses !== "string" || !ACTION.test(compensate.uses))
|
|
10930
|
+
fail2(`${where}.compensate.uses`, "must be a versioned action coordinate.");
|
|
10931
|
+
jsonValue(compensate.with, `${where}.compensate.with`);
|
|
10932
|
+
if (row2.failure?.policy !== "compensate")
|
|
10933
|
+
fail2(`${where}.compensate`, "requires failure.policy compensate.");
|
|
10934
|
+
}
|
|
10935
|
+
const definition = row2;
|
|
10936
|
+
validateBuiltInAction(definition, context, where);
|
|
10937
|
+
if (definition.compensate)
|
|
10938
|
+
validateBuiltInAction({ uses: definition.compensate.uses, with: definition.compensate.with, scope: definition.scope }, context, `${where}.compensate`);
|
|
10939
|
+
return definition;
|
|
10940
|
+
}
|
|
10941
|
+
function topologicalStages(stages, workflow) {
|
|
10942
|
+
const names = Object.keys(stages);
|
|
10943
|
+
const nameSet = new Set(names);
|
|
10944
|
+
for (const name of names)
|
|
10945
|
+
for (const dependency of stages[name].dependsOn ?? []) {
|
|
10946
|
+
if (!nameSet.has(dependency))
|
|
10947
|
+
fail2(`workflows.${workflow}.stages.${name}.dependsOn`, `names missing stage ${dependency}.`);
|
|
10948
|
+
if (dependency === name)
|
|
10949
|
+
fail2(`workflows.${workflow}.stages.${name}.dependsOn`, "cannot depend on itself.");
|
|
10950
|
+
}
|
|
10951
|
+
const visiting = new Set;
|
|
10952
|
+
const visited = new Set;
|
|
10953
|
+
const ordered = [];
|
|
10954
|
+
const visit = (name) => {
|
|
10955
|
+
if (visiting.has(name))
|
|
10956
|
+
fail2(`workflows.${workflow}.stages`, "contains a dependency cycle.");
|
|
10957
|
+
if (visited.has(name))
|
|
10958
|
+
return;
|
|
10959
|
+
visiting.add(name);
|
|
10960
|
+
for (const dependency of stages[name].dependsOn ?? [])
|
|
10961
|
+
visit(dependency);
|
|
10962
|
+
visiting.delete(name);
|
|
10963
|
+
visited.add(name);
|
|
10964
|
+
ordered.push(name);
|
|
10965
|
+
};
|
|
10966
|
+
for (const name of names.sort())
|
|
10967
|
+
visit(name);
|
|
10968
|
+
return ordered;
|
|
10969
|
+
}
|
|
10970
|
+
function validateStage(value, where, targets, context) {
|
|
10971
|
+
const row2 = object(value, where);
|
|
10972
|
+
exact3(row2, ["dependsOn", "if", "strategy", "steps"], where);
|
|
10973
|
+
if (row2.dependsOn !== undefined && (!Array.isArray(row2.dependsOn) || row2.dependsOn.length > 64 || row2.dependsOn.some((entry) => typeof entry !== "string" || !NAME3.test(entry)) || new Set(row2.dependsOn).size !== row2.dependsOn.length))
|
|
10974
|
+
fail2(`${where}.dependsOn`, "is invalid.");
|
|
10975
|
+
if (row2.if !== undefined)
|
|
10976
|
+
condition(row2.if, `${where}.if`);
|
|
10977
|
+
const strategy = object(row2.strategy, `${where}.strategy`);
|
|
10978
|
+
exact3(strategy, ["mode", "maximumConcurrency", "batchSize", "minimumHealthy", "increments", "observationMs"], `${where}.strategy`);
|
|
10979
|
+
if (!["sequential", "parallel", "rolling", "blue-green", "canary"].includes(String(strategy.mode)))
|
|
10980
|
+
fail2(`${where}.strategy.mode`, "is unsupported.");
|
|
10981
|
+
if (strategy.maximumConcurrency !== undefined)
|
|
10982
|
+
integer(strategy.maximumConcurrency, `${where}.strategy.maximumConcurrency`, 1, 1024);
|
|
10983
|
+
if (strategy.batchSize !== undefined)
|
|
10984
|
+
integer(strategy.batchSize, `${where}.strategy.batchSize`, 1, 1024);
|
|
10985
|
+
if (strategy.minimumHealthy !== undefined)
|
|
10986
|
+
integer(strategy.minimumHealthy, `${where}.strategy.minimumHealthy`, 0, 1024);
|
|
10987
|
+
if (strategy.increments !== undefined && (!Array.isArray(strategy.increments) || strategy.increments.length < 1 || strategy.increments.length > 20 || strategy.increments.some((entry) => !Number.isInteger(entry) || entry < 1 || entry > 100) || strategy.increments.at(-1) !== 100))
|
|
10988
|
+
fail2(`${where}.strategy.increments`, "must end at 100 and contain bounded percentages.");
|
|
10989
|
+
if (strategy.observationMs !== undefined)
|
|
10990
|
+
integer(strategy.observationMs, `${where}.strategy.observationMs`, 1000, 86400000);
|
|
10991
|
+
if (strategy.mode === "canary" && strategy.increments === undefined)
|
|
10992
|
+
fail2(`${where}.strategy.increments`, "is required for canary mode.");
|
|
10993
|
+
if (strategy.mode === "rolling" && strategy.batchSize === undefined)
|
|
10994
|
+
fail2(`${where}.strategy.batchSize`, "is required for rolling mode.");
|
|
10995
|
+
const steps = object(row2.steps, `${where}.steps`);
|
|
10996
|
+
const entries = Object.entries(steps);
|
|
10997
|
+
if (entries.length < 1 || entries.length > 256)
|
|
10998
|
+
fail2(`${where}.steps`, "must contain 1 to 256 steps.");
|
|
10999
|
+
for (const [name, stepValue] of entries) {
|
|
11000
|
+
named(name, `${where}.steps key`);
|
|
11001
|
+
validateStep(stepValue, `${where}.steps.${name}`, targets, context);
|
|
11002
|
+
}
|
|
11003
|
+
return row2;
|
|
11004
|
+
}
|
|
11005
|
+
function compileDeployment(sourceValue) {
|
|
11006
|
+
const source = object(sourceValue, "deployment");
|
|
11007
|
+
exact3(source, ["apiVersion", "kind", "metadata", "spec"], "deployment");
|
|
11008
|
+
if (source.apiVersion !== "deploy.forgezero.net/v1")
|
|
11009
|
+
fail2("deployment.apiVersion", "must be deploy.forgezero.net/v1.");
|
|
11010
|
+
if (source.kind !== "Deployment")
|
|
11011
|
+
fail2("deployment.kind", "must be Deployment.");
|
|
11012
|
+
const metadata = object(source.metadata, "deployment.metadata");
|
|
11013
|
+
exact3(metadata, ["name", "description"], "deployment.metadata");
|
|
11014
|
+
const name = named(metadata.name, "deployment.metadata.name");
|
|
11015
|
+
if (metadata.description !== undefined)
|
|
11016
|
+
boundedString(metadata.description, "deployment.metadata.description", 1024);
|
|
11017
|
+
const spec = object(source.spec, "deployment.spec");
|
|
11018
|
+
exact3(spec, ["security", "inputs", "credentials", "targets", "requirements", "components", "workflows"], "deployment.spec");
|
|
11019
|
+
if (spec.security !== undefined) {
|
|
11020
|
+
const security = object(spec.security, "deployment.spec.security");
|
|
11021
|
+
exact3(security, ["attestation"], "deployment.spec.security");
|
|
11022
|
+
if (security.attestation !== undefined && !["required", "preferred", "disabled"].includes(String(security.attestation)))
|
|
11023
|
+
fail2("deployment.spec.security.attestation", "is unsupported.");
|
|
11024
|
+
}
|
|
11025
|
+
const inputs = object(spec.inputs ?? {}, "deployment.spec.inputs");
|
|
11026
|
+
if (Object.keys(inputs).length > 128)
|
|
11027
|
+
fail2("deployment.spec.inputs", "contains more than 128 inputs.");
|
|
11028
|
+
for (const [inputName, value] of Object.entries(inputs)) {
|
|
11029
|
+
named(inputName, "deployment.spec.inputs key");
|
|
11030
|
+
validateInput(value, `deployment.spec.inputs.${inputName}`);
|
|
11031
|
+
}
|
|
11032
|
+
const credentials = object(spec.credentials ?? {}, "deployment.spec.credentials");
|
|
11033
|
+
if (Object.keys(credentials).length > 128)
|
|
11034
|
+
fail2("deployment.spec.credentials", "contains more than 128 credentials.");
|
|
11035
|
+
for (const [credentialName, value] of Object.entries(credentials)) {
|
|
11036
|
+
named(credentialName, "deployment.spec.credentials key");
|
|
11037
|
+
const credential = object(value, `deployment.spec.credentials.${credentialName}`);
|
|
11038
|
+
exact3(credential, ["schema", "source"], `deployment.spec.credentials.${credentialName}`);
|
|
11039
|
+
if (!ACTION.test(String(credential.schema)))
|
|
11040
|
+
fail2(`deployment.spec.credentials.${credentialName}.schema`, "must be a versioned schema coordinate.");
|
|
11041
|
+
const credentialSource = object(credential.source, `deployment.spec.credentials.${credentialName}.source`);
|
|
11042
|
+
exact3(credentialSource, ["kind", "name", "fallback"], `deployment.spec.credentials.${credentialName}.source`);
|
|
11043
|
+
if (!["vault", "systemd"].includes(String(credentialSource.kind)))
|
|
11044
|
+
fail2(`deployment.spec.credentials.${credentialName}.source.kind`, "is unsupported.");
|
|
11045
|
+
named(credentialSource.name, `deployment.spec.credentials.${credentialName}.source.name`);
|
|
11046
|
+
if (credentialSource.fallback !== undefined && (credentialSource.kind !== "vault" || credentialSource.fallback !== "systemd"))
|
|
11047
|
+
fail2(`deployment.spec.credentials.${credentialName}.source.fallback`, "may only be systemd behind Vault.");
|
|
11048
|
+
}
|
|
11049
|
+
const targets = object(spec.targets, "deployment.spec.targets");
|
|
11050
|
+
const targetEntries = Object.entries(targets);
|
|
11051
|
+
if (targetEntries.length < 1 || targetEntries.length > 64)
|
|
11052
|
+
fail2("deployment.spec.targets", "must contain 1 to 64 targets.");
|
|
11053
|
+
const targetNames = new Set(targetEntries.map(([targetName]) => named(targetName, "deployment.spec.targets key")));
|
|
11054
|
+
for (const [targetName, value] of targetEntries) {
|
|
11055
|
+
const target = object(value, `deployment.spec.targets.${targetName}`);
|
|
11056
|
+
exact3(target, ["kind", "selector", "cardinality", "placement"], `deployment.spec.targets.${targetName}`);
|
|
11057
|
+
if (target.kind !== "compute")
|
|
11058
|
+
fail2(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
|
|
11059
|
+
const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
|
|
11060
|
+
exact3(selector, ["profiles", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
|
|
11061
|
+
if (!Array.isArray(selector.profiles) || selector.profiles.length < 1 || selector.profiles.length > 32 || selector.profiles.some((profile) => typeof profile !== "string" || !NAME3.test(profile)) || new Set(selector.profiles).size !== selector.profiles.length)
|
|
11062
|
+
fail2(`deployment.spec.targets.${targetName}.selector.profiles`, "is invalid.");
|
|
11063
|
+
if (selector.confidentialCompute !== undefined && !["required", "preferred", "disabled"].includes(String(selector.confidentialCompute)))
|
|
11064
|
+
fail2(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "is unsupported.");
|
|
11065
|
+
if (selector.labels !== undefined)
|
|
11066
|
+
jsonValue(selector.labels, `deployment.spec.targets.${targetName}.selector.labels`);
|
|
11067
|
+
const cardinality = object(target.cardinality, `deployment.spec.targets.${targetName}.cardinality`);
|
|
11068
|
+
exact3(cardinality, ["minimum", "desired", "maximum"], `deployment.spec.targets.${targetName}.cardinality`);
|
|
11069
|
+
const minimum = integer(cardinality.minimum, `deployment.spec.targets.${targetName}.cardinality.minimum`, 1, 1024);
|
|
11070
|
+
const maximum = integer(cardinality.maximum, `deployment.spec.targets.${targetName}.cardinality.maximum`, minimum, 1024);
|
|
11071
|
+
if (typeof cardinality.desired === "number")
|
|
11072
|
+
integer(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`, minimum, maximum);
|
|
11073
|
+
else
|
|
11074
|
+
reference(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`);
|
|
11075
|
+
if (target.placement !== undefined)
|
|
11076
|
+
jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
|
|
11077
|
+
}
|
|
11078
|
+
const requirements = object(spec.requirements ?? {}, "deployment.spec.requirements");
|
|
11079
|
+
if (Object.keys(requirements).length > 64)
|
|
11080
|
+
fail2("deployment.spec.requirements", "contains more than 64 requirements.");
|
|
11081
|
+
const requirementDefinitions = new Map;
|
|
11082
|
+
const requiredProviders = new Map;
|
|
11083
|
+
for (const [requirementName, value] of Object.entries(requirements)) {
|
|
11084
|
+
named(requirementName, "deployment.spec.requirements key");
|
|
11085
|
+
const requirementValue = validateRequirement(value, `deployment.spec.requirements.${requirementName}`);
|
|
11086
|
+
requirementDefinitions.set(requirementName, requirementValue);
|
|
11087
|
+
const key = `${requirementValue.provider}@${requirementValue.contract}:${requirementValue.capability}`;
|
|
11088
|
+
requiredProviders.set(key, { provider: requirementValue.provider, contract: requirementValue.contract, capability: requirementValue.capability });
|
|
11089
|
+
}
|
|
11090
|
+
const components = object(spec.components, "deployment.spec.components");
|
|
11091
|
+
const componentEntries = Object.entries(components);
|
|
11092
|
+
if (componentEntries.length < 1 || componentEntries.length > 128)
|
|
11093
|
+
fail2("deployment.spec.components", "must contain 1 to 128 components.");
|
|
11094
|
+
for (const [componentName, value] of componentEntries) {
|
|
11095
|
+
named(componentName, "deployment.spec.components key");
|
|
11096
|
+
validateComponent(value, `deployment.spec.components.${componentName}`, targetNames, requirementDefinitions);
|
|
11097
|
+
}
|
|
11098
|
+
const actionContext = {
|
|
11099
|
+
components,
|
|
11100
|
+
requirements,
|
|
11101
|
+
credentials
|
|
11102
|
+
};
|
|
11103
|
+
const workflows = object(spec.workflows, "deployment.spec.workflows");
|
|
11104
|
+
const workflowEntries = Object.entries(workflows);
|
|
11105
|
+
if (workflowEntries.length < 1 || workflowEntries.length > 32)
|
|
11106
|
+
fail2("deployment.spec.workflows", "must contain 1 to 32 workflows.");
|
|
11107
|
+
const plannedWorkflows = [];
|
|
11108
|
+
for (const [workflowName, value] of workflowEntries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
11109
|
+
named(workflowName, "deployment.spec.workflows key");
|
|
11110
|
+
const workflowValue = object(value, `deployment.spec.workflows.${workflowName}`);
|
|
11111
|
+
exact3(workflowValue, ["concurrency", "stages"], `deployment.spec.workflows.${workflowName}`);
|
|
11112
|
+
if (workflowValue.concurrency !== undefined) {
|
|
11113
|
+
const concurrency = object(workflowValue.concurrency, `deployment.spec.workflows.${workflowName}.concurrency`);
|
|
11114
|
+
exact3(concurrency, ["group", "limit"], `deployment.spec.workflows.${workflowName}.concurrency`);
|
|
11115
|
+
boundedString(concurrency.group, `deployment.spec.workflows.${workflowName}.concurrency.group`, 256);
|
|
11116
|
+
integer(concurrency.limit, `deployment.spec.workflows.${workflowName}.concurrency.limit`, 1, 1024);
|
|
11117
|
+
}
|
|
11118
|
+
const stages = object(workflowValue.stages, `deployment.spec.workflows.${workflowName}.stages`);
|
|
11119
|
+
if (Object.keys(stages).length < 1 || Object.keys(stages).length > 64)
|
|
11120
|
+
fail2(`deployment.spec.workflows.${workflowName}.stages`, "must contain 1 to 64 stages.");
|
|
11121
|
+
for (const [stageName, stageValue] of Object.entries(stages)) {
|
|
11122
|
+
named(stageName, `deployment.spec.workflows.${workflowName}.stages key`);
|
|
11123
|
+
validateStage(stageValue, `deployment.spec.workflows.${workflowName}.stages.${stageName}`, targetNames, actionContext);
|
|
11124
|
+
}
|
|
11125
|
+
const stepIds = new Set;
|
|
11126
|
+
const plannedStages = topologicalStages(stages, workflowName).map((stageName) => {
|
|
11127
|
+
const stageValue = stages[stageName];
|
|
11128
|
+
const plannedSteps = Object.entries(stageValue.steps).map(([stepName, stepValue]) => {
|
|
11129
|
+
if (stepIds.has(stepName))
|
|
11130
|
+
fail2(`deployment.spec.workflows.${workflowName}`, `contains duplicate step id ${stepName}.`);
|
|
11131
|
+
stepIds.add(stepName);
|
|
11132
|
+
return { id: stepName, ...stepValue };
|
|
11133
|
+
});
|
|
11134
|
+
return { id: stageName, ...stageValue.dependsOn ? { dependsOn: [...stageValue.dependsOn].sort() } : {}, ...stageValue.if ? { if: stageValue.if } : {}, strategy: stageValue.strategy, steps: plannedSteps };
|
|
11135
|
+
});
|
|
11136
|
+
plannedWorkflows.push({ id: workflowName, ...workflowValue.concurrency ? { concurrency: workflowValue.concurrency } : {}, stages: plannedStages });
|
|
11137
|
+
}
|
|
11138
|
+
const typedSource = source;
|
|
11139
|
+
return {
|
|
11140
|
+
format: DEPLOY_PLAN_FORMAT,
|
|
11141
|
+
version: DEPLOY_PLAN_VERSION,
|
|
11142
|
+
sourceDigest: deploymentSourceDigest(typedSource),
|
|
11143
|
+
name,
|
|
11144
|
+
...metadata.description ? { description: metadata.description } : {},
|
|
11145
|
+
requiredProviders: [...requiredProviders.values()].sort((left, right) => `${left.provider}:${left.contract}:${left.capability}`.localeCompare(`${right.provider}:${right.contract}:${right.capability}`)),
|
|
11146
|
+
spec: {
|
|
11147
|
+
...spec.security ? { security: spec.security } : {},
|
|
11148
|
+
inputs,
|
|
11149
|
+
credentials,
|
|
11150
|
+
targets,
|
|
11151
|
+
requirements,
|
|
11152
|
+
components,
|
|
11153
|
+
workflows: plannedWorkflows
|
|
11154
|
+
}
|
|
11155
|
+
};
|
|
11156
|
+
}
|
|
11157
|
+
function deploymentPlanDigest(plan) {
|
|
11158
|
+
return `sha256:${createHash2("sha256").update(canonicalJson(plan)).digest("hex")}`;
|
|
11159
|
+
}
|
|
11160
|
+
function parseDeploymentPlan(value) {
|
|
11161
|
+
const row2 = object(value, "plan");
|
|
11162
|
+
exact3(row2, ["format", "version", "sourceDigest", "name", "description", "requiredProviders", "spec"], "plan");
|
|
11163
|
+
if (row2.format !== DEPLOY_PLAN_FORMAT || row2.version !== DEPLOY_PLAN_VERSION)
|
|
11164
|
+
fail2("plan", "uses an unsupported format or version.");
|
|
11165
|
+
if (typeof row2.sourceDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(row2.sourceDigest))
|
|
11166
|
+
fail2("plan.sourceDigest", "is invalid.");
|
|
11167
|
+
named(row2.name, "plan.name");
|
|
11168
|
+
if (row2.description !== undefined)
|
|
11169
|
+
boundedString(row2.description, "plan.description", 1024);
|
|
11170
|
+
if (!Array.isArray(row2.requiredProviders) || row2.requiredProviders.length > 64)
|
|
11171
|
+
fail2("plan.requiredProviders", "must be a bounded array.");
|
|
11172
|
+
for (const [index, provider] of row2.requiredProviders.entries()) {
|
|
11173
|
+
const item = object(provider, `plan.requiredProviders[${index}]`);
|
|
11174
|
+
exact3(item, ["provider", "contract", "capability"], `plan.requiredProviders[${index}]`);
|
|
11175
|
+
if (!COORDINATE.test(String(item.provider)) || !COORDINATE.test(String(item.capability)))
|
|
11176
|
+
fail2(`plan.requiredProviders[${index}]`, "contains an invalid coordinate.");
|
|
11177
|
+
integer(item.contract, `plan.requiredProviders[${index}].contract`, 1, 65535);
|
|
11178
|
+
}
|
|
11179
|
+
const spec = object(row2.spec, "plan.spec");
|
|
11180
|
+
const workflows = spec.workflows;
|
|
11181
|
+
if (!Array.isArray(workflows))
|
|
11182
|
+
fail2("plan.spec.workflows", "must be an array.");
|
|
11183
|
+
const sourceWorkflows = {};
|
|
11184
|
+
for (const [workflowIndex, workflow] of workflows.entries()) {
|
|
11185
|
+
const item = object(workflow, `plan.spec.workflows[${workflowIndex}]`);
|
|
11186
|
+
exact3(item, ["id", "concurrency", "stages"], `plan.spec.workflows[${workflowIndex}]`);
|
|
11187
|
+
const workflowId = named(item.id, `plan.spec.workflows[${workflowIndex}].id`);
|
|
11188
|
+
if (sourceWorkflows[workflowId])
|
|
11189
|
+
fail2("plan.spec.workflows", `contains duplicate workflow ${workflowId}.`);
|
|
11190
|
+
if (!Array.isArray(item.stages))
|
|
11191
|
+
fail2(`plan.spec.workflows[${workflowIndex}].stages`, "must be an array.");
|
|
11192
|
+
const sourceStages = {};
|
|
11193
|
+
for (const [stageIndex, stage] of item.stages.entries()) {
|
|
11194
|
+
const stageItem = object(stage, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}]`);
|
|
11195
|
+
exact3(stageItem, ["id", "dependsOn", "if", "strategy", "steps"], `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}]`);
|
|
11196
|
+
const stageId = named(stageItem.id, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].id`);
|
|
11197
|
+
if (sourceStages[stageId])
|
|
11198
|
+
fail2(`plan.spec.workflows[${workflowIndex}].stages`, `contains duplicate stage ${stageId}.`);
|
|
11199
|
+
if (!Array.isArray(stageItem.steps))
|
|
11200
|
+
fail2(`plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps`, "must be an array.");
|
|
11201
|
+
const sourceSteps = {};
|
|
11202
|
+
for (const [stepIndex, step3] of stageItem.steps.entries()) {
|
|
11203
|
+
const stepItem = object(step3, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps[${stepIndex}]`);
|
|
11204
|
+
const stepId = named(stepItem.id, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps[${stepIndex}].id`);
|
|
11205
|
+
if (sourceSteps[stepId])
|
|
11206
|
+
fail2(`plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps`, `contains duplicate step ${stepId}.`);
|
|
11207
|
+
const { id: _id3, ...stepWithoutId } = stepItem;
|
|
11208
|
+
sourceSteps[stepId] = stepWithoutId;
|
|
11209
|
+
}
|
|
11210
|
+
const { id: _id2, steps: _steps, ...stageWithoutId } = stageItem;
|
|
11211
|
+
sourceStages[stageId] = { ...stageWithoutId, steps: sourceSteps };
|
|
11212
|
+
}
|
|
11213
|
+
const { id: _id, stages: _stages, ...workflowWithoutId } = item;
|
|
11214
|
+
sourceWorkflows[workflowId] = { ...workflowWithoutId, stages: sourceStages };
|
|
11215
|
+
}
|
|
11216
|
+
const { workflows: _workflows, ...specWithoutWorkflows } = spec;
|
|
11217
|
+
const reconstructed = {
|
|
11218
|
+
apiVersion: "deploy.forgezero.net/v1",
|
|
11219
|
+
kind: "Deployment",
|
|
11220
|
+
metadata: { name: row2.name, ...row2.description ? { description: row2.description } : {} },
|
|
11221
|
+
spec: { ...specWithoutWorkflows, workflows: sourceWorkflows }
|
|
11222
|
+
};
|
|
11223
|
+
const compiled = compileDeployment(reconstructed);
|
|
11224
|
+
return { ...compiled, sourceDigest: row2.sourceDigest };
|
|
11225
|
+
}
|
|
11226
|
+
|
|
11227
|
+
// src/deploy-compiler.ts
|
|
11228
|
+
var DEPLOY_SOURCE_FILE = "forgezero.deploy.ts";
|
|
11229
|
+
var DEPLOY_PLAN_FILE = ".fz/deploy.plan.json";
|
|
11230
|
+
function safeName2(value) {
|
|
11231
|
+
const result = value.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
11232
|
+
return /^[a-z]/.test(result) ? result : `app-${result || "service"}`;
|
|
11233
|
+
}
|
|
11234
|
+
function defaultTypeScriptDeployment(name) {
|
|
11235
|
+
return `import { actions, application, defineDeployment, input, providers, stage, target, workflow } from '@forgezero/agent/deploy';
|
|
11236
|
+
|
|
11237
|
+
export default defineDeployment({
|
|
11238
|
+
apiVersion: 'deploy.forgezero.net/v1',
|
|
11239
|
+
kind: 'Deployment',
|
|
11240
|
+
metadata: { name: '${safeName2(name)}' },
|
|
11241
|
+
spec: {
|
|
11242
|
+
security: { attestation: 'preferred' },
|
|
11243
|
+
inputs: { replicas: input.integer({ minimum: 1, maximum: 32, default: 1 }) },
|
|
11244
|
+
targets: {
|
|
11245
|
+
app: target.compute({
|
|
11246
|
+
selector: { profiles: ['app'] },
|
|
11247
|
+
cardinality: { minimum: 1, desired: input.ref('replicas'), maximum: 32 }
|
|
11248
|
+
})
|
|
11249
|
+
},
|
|
11250
|
+
requirements: { bun: providers.bun.require() },
|
|
11251
|
+
components: {
|
|
11252
|
+
app: application({
|
|
11253
|
+
target: 'app',
|
|
11254
|
+
runtime: { kind: 'native', provider: 'forgezero.bun', requirement: 'bun', argv: ['/usr/local/bin/bun', 'run', 'start'] },
|
|
11255
|
+
service: { protocol: 'http', port: 3000, health: { protocol: 'http', method: 'GET', path: '/health', expectedStatus: [200], timeoutMs: 5_000 } },
|
|
11256
|
+
resources: {},
|
|
11257
|
+
rollout: { strategy: 'direct' }
|
|
11258
|
+
})
|
|
11259
|
+
},
|
|
11260
|
+
workflows: {
|
|
11261
|
+
deploy: workflow({
|
|
11262
|
+
stages: {
|
|
11263
|
+
build: stage({ strategy: { mode: 'sequential' }, steps: {
|
|
11264
|
+
build: actions.exec.argv(
|
|
11265
|
+
{ component: 'app', argv: ['/usr/local/bin/fz-agent', 'pipeline-todo', 'replace with the project build argv'] },
|
|
11266
|
+
{ scope: { kind: 'each-target', target: 'app' }, timeoutMs: 600_000 }
|
|
11267
|
+
)
|
|
11268
|
+
} }),
|
|
11269
|
+
release: stage({ dependsOn: ['build'], strategy: { mode: 'sequential' }, steps: {
|
|
11270
|
+
start: actions.exec.argv(
|
|
11271
|
+
{ component: 'app', argv: ['/usr/local/bin/fz-agent', 'pipeline-todo', 'replace with the project release argv'] },
|
|
11272
|
+
{ scope: { kind: 'each-target', target: 'app' }, timeoutMs: 120_000 }
|
|
11273
|
+
)
|
|
11274
|
+
} }),
|
|
11275
|
+
verify: stage({ dependsOn: ['release'], strategy: { mode: 'parallel' }, steps: {
|
|
11276
|
+
health: actions.service.health({ component: 'app' }, { scope: { kind: 'each-target', target: 'app' }, timeoutMs: 30_000 })
|
|
11277
|
+
} })
|
|
11278
|
+
}
|
|
11279
|
+
})
|
|
11280
|
+
}
|
|
11281
|
+
}
|
|
11282
|
+
});
|
|
11283
|
+
`;
|
|
11284
|
+
}
|
|
11285
|
+
function initializeTypeScriptDeployment(rootValue, options) {
|
|
11286
|
+
const path = localPath(rootValue, DEPLOY_SOURCE_FILE, "deployment source");
|
|
11287
|
+
if (existsSync6(path) && !options.force)
|
|
11288
|
+
throw new Error(`${DEPLOY_SOURCE_FILE} already exists; use --force only when replacing it deliberately`);
|
|
11289
|
+
writeFileSync6(path, defaultTypeScriptDeployment(options.name), { mode: 420, flag: options.force ? "w" : "wx" });
|
|
11290
|
+
return path;
|
|
11291
|
+
}
|
|
11292
|
+
function inside(root, path) {
|
|
11293
|
+
const nested = relative(root, path);
|
|
11294
|
+
return nested === "" || !nested.startsWith(`..${sep3}`) && nested !== ".." && !isAbsolute(nested);
|
|
11295
|
+
}
|
|
11296
|
+
function localPath(rootValue, value, label) {
|
|
11297
|
+
const root = resolve4(rootValue);
|
|
11298
|
+
const path = resolve4(root, value);
|
|
11299
|
+
if (!inside(root, path))
|
|
11300
|
+
throw new Error(`${label} must remain inside the project root`);
|
|
11301
|
+
return path;
|
|
11302
|
+
}
|
|
11303
|
+
async function loadDeploymentSource(root, sourceFile = DEPLOY_SOURCE_FILE) {
|
|
11304
|
+
const source = localPath(root, sourceFile, "deployment source");
|
|
11305
|
+
if (!existsSync6(source))
|
|
11306
|
+
throw new Error(`${sourceFile} does not exist`);
|
|
11307
|
+
const status = lstatSync2(source);
|
|
11308
|
+
if (!status.isFile() || status.isSymbolicLink() || status.size > 2 * 1024 * 1024)
|
|
11309
|
+
throw new Error("deployment source must be one bounded regular file");
|
|
11310
|
+
const module = await import(`${pathToFileURL(source).href}?forgezero=${status.mtimeMs}`);
|
|
11311
|
+
if (module.default === undefined)
|
|
11312
|
+
throw new Error(`${sourceFile} must export one default deployment definition`);
|
|
11313
|
+
return module.default;
|
|
11314
|
+
}
|
|
11315
|
+
async function compileDeploymentProject(rootValue, options = {}) {
|
|
11316
|
+
const root = resolve4(rootValue);
|
|
11317
|
+
const sourceFile = options.sourceFile ?? DEPLOY_SOURCE_FILE;
|
|
11318
|
+
const outputFile = options.outputFile ?? DEPLOY_PLAN_FILE;
|
|
11319
|
+
const source = localPath(root, sourceFile, "deployment source");
|
|
11320
|
+
const output = localPath(root, outputFile, "deployment output");
|
|
11321
|
+
const plan = compileDeployment(await loadDeploymentSource(root, sourceFile));
|
|
11322
|
+
const bytes = `${canonicalJson(plan)}
|
|
11323
|
+
`;
|
|
11324
|
+
parseDeploymentPlan(JSON.parse(bytes));
|
|
11325
|
+
const previous = existsSync6(output) ? readFileSync6(output, "utf8") : undefined;
|
|
11326
|
+
const changed = previous !== bytes;
|
|
11327
|
+
if (options.write !== false && changed) {
|
|
11328
|
+
mkdirSync6(dirname5(output), { recursive: true, mode: 493 });
|
|
11329
|
+
const temporary = `${output}.new-${process.pid}`;
|
|
11330
|
+
try {
|
|
11331
|
+
writeFileSync6(temporary, bytes, { mode: 420, flag: "wx" });
|
|
11332
|
+
renameSync5(temporary, output);
|
|
11333
|
+
} finally {
|
|
11334
|
+
rmSync4(temporary, { force: true });
|
|
11335
|
+
}
|
|
11336
|
+
}
|
|
11337
|
+
return { source, output, plan, digest: deploymentPlanDigest(plan), changed };
|
|
11338
|
+
}
|
|
11339
|
+
function inspectCompiledDeployment(rootValue, outputFile = DEPLOY_PLAN_FILE) {
|
|
11340
|
+
const path = localPath(rootValue, outputFile, "deployment output");
|
|
11341
|
+
if (!existsSync6(path))
|
|
11342
|
+
throw new Error(`${outputFile} does not exist; run \`fz deploy compile\``);
|
|
11343
|
+
const status = lstatSync2(path);
|
|
11344
|
+
if (!status.isFile() || status.isSymbolicLink() || status.size > 4194304)
|
|
11345
|
+
throw new Error("deployment plan must be one bounded regular file");
|
|
11346
|
+
const plan = parseDeploymentPlan(JSON.parse(readFileSync6(path, "utf8")));
|
|
11347
|
+
return { path, plan, digest: deploymentPlanDigest(plan) };
|
|
11348
|
+
}
|
|
11349
|
+
|
|
10265
11350
|
// ../vault/dist/config.js
|
|
10266
11351
|
class ConfigError extends Error {
|
|
10267
11352
|
code;
|
|
@@ -10351,15 +11436,15 @@ function loadConfig(options = {}) {
|
|
|
10351
11436
|
// src/cli/session-store.ts
|
|
10352
11437
|
import {
|
|
10353
11438
|
chmodSync as chmodSync2,
|
|
10354
|
-
existsSync as
|
|
10355
|
-
lstatSync as
|
|
10356
|
-
mkdirSync as
|
|
10357
|
-
readFileSync as
|
|
10358
|
-
renameSync as
|
|
11439
|
+
existsSync as existsSync7,
|
|
11440
|
+
lstatSync as lstatSync3,
|
|
11441
|
+
mkdirSync as mkdirSync7,
|
|
11442
|
+
readFileSync as readFileSync7,
|
|
11443
|
+
renameSync as renameSync6,
|
|
10359
11444
|
unlinkSync,
|
|
10360
|
-
writeFileSync as
|
|
11445
|
+
writeFileSync as writeFileSync7
|
|
10361
11446
|
} from "fs";
|
|
10362
|
-
import { dirname as
|
|
11447
|
+
import { dirname as dirname6, join as join5 } from "path";
|
|
10363
11448
|
import { homedir } from "os";
|
|
10364
11449
|
var EMPTY2 = () => ({ version: 1, sessions: {} });
|
|
10365
11450
|
function canonicalApi(value) {
|
|
@@ -10381,9 +11466,9 @@ function defaultSessionPath(env = process.env) {
|
|
|
10381
11466
|
return join5(state, "forgezero", "sessions.json");
|
|
10382
11467
|
}
|
|
10383
11468
|
function assertPrivate(path, kind) {
|
|
10384
|
-
if (!
|
|
11469
|
+
if (!existsSync7(path))
|
|
10385
11470
|
return;
|
|
10386
|
-
const stat =
|
|
11471
|
+
const stat = lstatSync3(path);
|
|
10387
11472
|
if (stat.isSymbolicLink())
|
|
10388
11473
|
throw new Error(`Refusing symlinked CLI session ${kind}: ${path}`);
|
|
10389
11474
|
if (kind === "directory" ? !stat.isDirectory() : !stat.isFile()) {
|
|
@@ -10399,13 +11484,13 @@ function assertPrivate(path, kind) {
|
|
|
10399
11484
|
}
|
|
10400
11485
|
}
|
|
10401
11486
|
function parse(path) {
|
|
10402
|
-
if (!
|
|
11487
|
+
if (!existsSync7(path))
|
|
10403
11488
|
return EMPTY2();
|
|
10404
|
-
assertPrivate(
|
|
11489
|
+
assertPrivate(dirname6(path), "directory");
|
|
10405
11490
|
assertPrivate(path, "file");
|
|
10406
11491
|
let value;
|
|
10407
11492
|
try {
|
|
10408
|
-
value = JSON.parse(
|
|
11493
|
+
value = JSON.parse(readFileSync7(path, "utf8"));
|
|
10409
11494
|
} catch {
|
|
10410
11495
|
throw new Error(`CLI session file is not valid JSON: ${path}`);
|
|
10411
11496
|
}
|
|
@@ -10416,19 +11501,19 @@ function parse(path) {
|
|
|
10416
11501
|
return raw;
|
|
10417
11502
|
}
|
|
10418
11503
|
function persist(path, value) {
|
|
10419
|
-
const directory =
|
|
10420
|
-
if (!
|
|
10421
|
-
|
|
11504
|
+
const directory = dirname6(path);
|
|
11505
|
+
if (!existsSync7(directory))
|
|
11506
|
+
mkdirSync7(directory, { recursive: true, mode: 448 });
|
|
10422
11507
|
assertPrivate(directory, "directory");
|
|
10423
11508
|
if (process.platform !== "win32")
|
|
10424
11509
|
chmodSync2(directory, 448);
|
|
10425
11510
|
const temporary = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
10426
11511
|
try {
|
|
10427
|
-
|
|
11512
|
+
writeFileSync7(temporary, `${JSON.stringify(value, null, 2)}
|
|
10428
11513
|
`, { mode: 384, flag: "wx" });
|
|
10429
11514
|
if (process.platform !== "win32")
|
|
10430
11515
|
chmodSync2(temporary, 384);
|
|
10431
|
-
|
|
11516
|
+
renameSync6(temporary, path);
|
|
10432
11517
|
} finally {
|
|
10433
11518
|
try {
|
|
10434
11519
|
unlinkSync(temporary);
|
|
@@ -10465,18 +11550,18 @@ function removeSession(api, realm, path = defaultSessionPath()) {
|
|
|
10465
11550
|
}
|
|
10466
11551
|
|
|
10467
11552
|
// src/bootstrap.ts
|
|
10468
|
-
import { createHash as
|
|
11553
|
+
import { createHash as createHash3, createHmac, randomBytes as randomBytes7 } from "crypto";
|
|
10469
11554
|
import {
|
|
10470
11555
|
chmodSync as chmodSync3,
|
|
10471
|
-
existsSync as
|
|
10472
|
-
lstatSync as
|
|
10473
|
-
mkdirSync as
|
|
10474
|
-
readFileSync as
|
|
10475
|
-
renameSync as
|
|
10476
|
-
rmSync as
|
|
10477
|
-
writeFileSync as
|
|
11556
|
+
existsSync as existsSync8,
|
|
11557
|
+
lstatSync as lstatSync4,
|
|
11558
|
+
mkdirSync as mkdirSync8,
|
|
11559
|
+
readFileSync as readFileSync8,
|
|
11560
|
+
renameSync as renameSync7,
|
|
11561
|
+
rmSync as rmSync5,
|
|
11562
|
+
writeFileSync as writeFileSync8
|
|
10478
11563
|
} from "fs";
|
|
10479
|
-
import { dirname as
|
|
11564
|
+
import { dirname as dirname8 } from "path";
|
|
10480
11565
|
import { fileURLToPath } from "url";
|
|
10481
11566
|
|
|
10482
11567
|
// src/platform-bootstrap-runtime.ts
|
|
@@ -10854,7 +11939,7 @@ function planLocalOtlpProof(endpoint, collectorUnit) {
|
|
|
10854
11939
|
import { constants } from "fs";
|
|
10855
11940
|
import { randomBytes as randomBytes6, randomUUID } from "crypto";
|
|
10856
11941
|
import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "fs/promises";
|
|
10857
|
-
import { dirname as
|
|
11942
|
+
import { dirname as dirname7, join as join6, resolve as resolve5 } from "path";
|
|
10858
11943
|
import { isIP as isIP3 } from "net";
|
|
10859
11944
|
|
|
10860
11945
|
// src/cloudflare-edge.ts
|
|
@@ -11098,16 +12183,16 @@ async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
|
|
|
11098
12183
|
throw new Error(`${path} has an invalid size`);
|
|
11099
12184
|
}
|
|
11100
12185
|
async function readOwnerOnlyFile(path, maximumBytes) {
|
|
11101
|
-
const
|
|
12186
|
+
const absolute2 = resolve5(path);
|
|
11102
12187
|
let handle;
|
|
11103
12188
|
try {
|
|
11104
|
-
handle = await open(
|
|
11105
|
-
await assertOwnerOnlyHandle(
|
|
12189
|
+
handle = await open(absolute2, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
12190
|
+
await assertOwnerOnlyHandle(absolute2, handle, maximumBytes);
|
|
11106
12191
|
return await handle.readFile({ encoding: "utf8" });
|
|
11107
12192
|
} catch (cause) {
|
|
11108
|
-
if (cause instanceof Error && cause.message.startsWith(
|
|
12193
|
+
if (cause instanceof Error && cause.message.startsWith(absolute2))
|
|
11109
12194
|
throw cause;
|
|
11110
|
-
throw new Error(`cannot securely read owner-only file ${
|
|
12195
|
+
throw new Error(`cannot securely read owner-only file ${absolute2}`);
|
|
11111
12196
|
} finally {
|
|
11112
12197
|
await handle?.close();
|
|
11113
12198
|
}
|
|
@@ -11115,7 +12200,7 @@ async function readOwnerOnlyFile(path, maximumBytes) {
|
|
|
11115
12200
|
async function readOwnerApiToken(path) {
|
|
11116
12201
|
const token = (await readOwnerOnlyFile(path, 4096)).trim();
|
|
11117
12202
|
if (!TOKEN.test(token))
|
|
11118
|
-
throw new Error(`${
|
|
12203
|
+
throw new Error(`${resolve5(path)} must contain exactly one Cloudflare API token`);
|
|
11119
12204
|
return token;
|
|
11120
12205
|
}
|
|
11121
12206
|
async function readCloudflareBootstrapTokens(files) {
|
|
@@ -11308,7 +12393,7 @@ function planCloudflareBootstrap(input, outputPath) {
|
|
|
11308
12393
|
format: 1,
|
|
11309
12394
|
kind: "forgezero-cloudflare-bootstrap-plan",
|
|
11310
12395
|
mode: "attended-token-file",
|
|
11311
|
-
outputFile:
|
|
12396
|
+
outputFile: resolve5(outputPath),
|
|
11312
12397
|
coordinates,
|
|
11313
12398
|
operations: [
|
|
11314
12399
|
"prove CF_API_TOKEN can write, read and remove one namespaced nonce in the existing KV namespace",
|
|
@@ -11368,11 +12453,11 @@ async function readExistingOutput(path) {
|
|
|
11368
12453
|
parsed = JSON.parse(await readOwnerOnlyFile(path, 1048576));
|
|
11369
12454
|
} catch (cause) {
|
|
11370
12455
|
if (cause instanceof SyntaxError)
|
|
11371
|
-
throw new Error(`${
|
|
12456
|
+
throw new Error(`${resolve5(path)} is not valid bootstrap JSON`);
|
|
11372
12457
|
throw cause;
|
|
11373
12458
|
}
|
|
11374
12459
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11375
|
-
throw new Error(`${
|
|
12460
|
+
throw new Error(`${resolve5(path)} is not a ForgeZero Cloudflare bootstrap output`);
|
|
11376
12461
|
}
|
|
11377
12462
|
const output = parsed;
|
|
11378
12463
|
const unsupported = Object.keys(output).filter((key) => ![
|
|
@@ -11385,9 +12470,9 @@ async function readExistingOutput(path) {
|
|
|
11385
12470
|
"created"
|
|
11386
12471
|
].includes(key));
|
|
11387
12472
|
if (unsupported.length)
|
|
11388
|
-
throw new Error(`${
|
|
12473
|
+
throw new Error(`${resolve5(path)} contains unsupported field ${unsupported[0]}`);
|
|
11389
12474
|
if (output.format !== 1 || output.kind !== "forgezero-cloudflare-bootstrap" || !["edge-resources-provisioned", "complete"].includes(String(output.phase)) || typeof output.updatedAt !== "string" || Number.isNaN(Date.parse(output.updatedAt)) || !output.resources || typeof output.resources !== "object" || Array.isArray(output.resources)) {
|
|
11390
|
-
throw new Error(`${
|
|
12475
|
+
throw new Error(`${resolve5(path)} is not a ForgeZero Cloudflare bootstrap output`);
|
|
11391
12476
|
}
|
|
11392
12477
|
const resources = output.resources;
|
|
11393
12478
|
const unsupportedResource = Object.keys(resources).filter((key) => ![
|
|
@@ -11397,24 +12482,24 @@ async function readExistingOutput(path) {
|
|
|
11397
12482
|
"nodes"
|
|
11398
12483
|
].includes(key));
|
|
11399
12484
|
if (unsupportedResource.length) {
|
|
11400
|
-
throw new Error(`${
|
|
12485
|
+
throw new Error(`${resolve5(path)} resources contain unsupported field ${unsupportedResource[0]}`);
|
|
11401
12486
|
}
|
|
11402
12487
|
const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
|
|
11403
12488
|
if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !TOKEN.test(String(resources.apiToken ?? "")) || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
|
|
11404
|
-
throw new Error(`${
|
|
12489
|
+
throw new Error(`${resolve5(path)} has malformed Cloudflare bootstrap resources`);
|
|
11405
12490
|
}
|
|
11406
12491
|
if (coordinates.realtime) {
|
|
11407
12492
|
const realtime = resources.realtime;
|
|
11408
12493
|
if (!realtime || !REALTIME_SECRET.test(String(realtime.publishSecret ?? "")) || !REALTIME_SECRET.test(String(realtime.ticketSecret ?? "")) || realtime.publishSecret === realtime.ticketSecret) {
|
|
11409
|
-
throw new Error(`${
|
|
12494
|
+
throw new Error(`${resolve5(path)} has malformed Cloudflare realtime resources`);
|
|
11410
12495
|
}
|
|
11411
12496
|
} else if (resources.realtime !== undefined) {
|
|
11412
|
-
throw new Error(`${
|
|
12497
|
+
throw new Error(`${resolve5(path)} contains undeclared Cloudflare realtime resources`);
|
|
11413
12498
|
}
|
|
11414
12499
|
const seen = new Set;
|
|
11415
12500
|
for (const item of resources.nodes) {
|
|
11416
12501
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
11417
|
-
throw new Error(`${
|
|
12502
|
+
throw new Error(`${resolve5(path)} has a malformed Cloudflare node resource`);
|
|
11418
12503
|
}
|
|
11419
12504
|
const node = item;
|
|
11420
12505
|
const unknownNode = Object.keys(node).filter((key) => ![
|
|
@@ -11428,24 +12513,24 @@ async function readExistingOutput(path) {
|
|
|
11428
12513
|
].includes(key));
|
|
11429
12514
|
const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
|
|
11430
12515
|
if (unknownNode.length || !expected || seen.has(expected.nodeName) || node.hostname !== expected.hostname || node.service !== expected.service || node.tunnelName !== expected.tunnelName || !UUID.test(String(node.tunnelId ?? "")) || !CONNECTOR_TOKEN.test(String(node.connectorToken ?? ""))) {
|
|
11431
|
-
throw new Error(`${
|
|
12516
|
+
throw new Error(`${resolve5(path)} has a malformed or unbound Cloudflare node resource`);
|
|
11432
12517
|
}
|
|
11433
12518
|
if (expected.mesh) {
|
|
11434
12519
|
const mesh = node.mesh;
|
|
11435
12520
|
if (!mesh || mesh.connectorName !== expected.mesh.connectorName || JSON.stringify(mesh.routes) !== JSON.stringify(expected.mesh.routes) || mesh.highAvailability !== expected.mesh.highAvailability || !UUID.test(String(mesh.connectorId ?? "")) || !CONNECTOR_TOKEN.test(String(mesh.connectorToken ?? ""))) {
|
|
11436
|
-
throw new Error(`${
|
|
12521
|
+
throw new Error(`${resolve5(path)} has a malformed or unbound Cloudflare Mesh resource`);
|
|
11437
12522
|
}
|
|
11438
12523
|
} else if (node.mesh !== undefined) {
|
|
11439
|
-
throw new Error(`${
|
|
12524
|
+
throw new Error(`${resolve5(path)} contains an undeclared Cloudflare Mesh resource`);
|
|
11440
12525
|
}
|
|
11441
12526
|
seen.add(expected.nodeName);
|
|
11442
12527
|
}
|
|
11443
12528
|
if (output.phase === "complete" && resources.nodes.length !== coordinates.nodes.length) {
|
|
11444
|
-
throw new Error(`${
|
|
12529
|
+
throw new Error(`${resolve5(path)} completed output does not cover the declared node fleet`);
|
|
11445
12530
|
}
|
|
11446
12531
|
if (output.created !== undefined) {
|
|
11447
12532
|
if (!output.created || typeof output.created !== "object" || Array.isArray(output.created) || Object.keys(output.created).some((key) => key !== "nodes") || !Array.isArray(output.created.nodes) || output.created.nodes.some((node) => !node || typeof node !== "object" || Array.isArray(node) || Object.keys(node).some((key) => !["nodeName", "tunnel", "mesh"].includes(key)) || typeof node.nodeName !== "string" || typeof node.tunnel !== "boolean" || typeof node.mesh !== "boolean")) {
|
|
11448
|
-
throw new Error(`${
|
|
12533
|
+
throw new Error(`${resolve5(path)} has malformed Cloudflare creation evidence`);
|
|
11449
12534
|
}
|
|
11450
12535
|
}
|
|
11451
12536
|
output.coordinates = coordinates;
|
|
@@ -11455,7 +12540,7 @@ function cloudflareHostHandoffPath(checkpointPath, nodeName) {
|
|
|
11455
12540
|
const normalized = nodeName.trim().toLowerCase();
|
|
11456
12541
|
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(normalized))
|
|
11457
12542
|
throw new Error("Cloudflare host handoff node name is invalid");
|
|
11458
|
-
return join6(`${
|
|
12543
|
+
return join6(`${resolve5(checkpointPath)}.hosts`, `${normalized}.json`);
|
|
11459
12544
|
}
|
|
11460
12545
|
async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
11461
12546
|
let parsed;
|
|
@@ -11540,7 +12625,7 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
|
11540
12625
|
};
|
|
11541
12626
|
}
|
|
11542
12627
|
async function prepareOwnerOutputDirectory(absolutePath) {
|
|
11543
|
-
const directory =
|
|
12628
|
+
const directory = dirname7(absolutePath);
|
|
11544
12629
|
await mkdir(directory, { recursive: true, mode: 448 });
|
|
11545
12630
|
const metadata = await stat(directory);
|
|
11546
12631
|
const uid = ownerUid();
|
|
@@ -11550,9 +12635,9 @@ async function prepareOwnerOutputDirectory(absolutePath) {
|
|
|
11550
12635
|
return directory;
|
|
11551
12636
|
}
|
|
11552
12637
|
async function writeOwnerJson(path, output) {
|
|
11553
|
-
const
|
|
11554
|
-
const directory = await prepareOwnerOutputDirectory(
|
|
11555
|
-
const temporary = `${
|
|
12638
|
+
const absolute2 = resolve5(path);
|
|
12639
|
+
const directory = await prepareOwnerOutputDirectory(absolute2);
|
|
12640
|
+
const temporary = `${absolute2}.${randomUUID()}.tmp`;
|
|
11556
12641
|
let handle;
|
|
11557
12642
|
try {
|
|
11558
12643
|
handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
@@ -11561,8 +12646,8 @@ async function writeOwnerJson(path, output) {
|
|
|
11561
12646
|
await handle.sync();
|
|
11562
12647
|
await handle.close();
|
|
11563
12648
|
handle = undefined;
|
|
11564
|
-
await rename(temporary,
|
|
11565
|
-
await chmod(
|
|
12649
|
+
await rename(temporary, absolute2);
|
|
12650
|
+
await chmod(absolute2, 384);
|
|
11566
12651
|
const directoryHandle = await open(directory, constants.O_RDONLY);
|
|
11567
12652
|
try {
|
|
11568
12653
|
await directoryHandle.sync();
|
|
@@ -11611,7 +12696,7 @@ var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(r
|
|
|
11611
12696
|
var newRealtimeSecret = () => randomBytes6(48).toString("base64url");
|
|
11612
12697
|
async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch) {
|
|
11613
12698
|
const coordinates = validateCloudflareBootstrapCoordinates(input);
|
|
11614
|
-
const absoluteOutput =
|
|
12699
|
+
const absoluteOutput = resolve5(outputPath);
|
|
11615
12700
|
const existing = await readExistingOutput(absoluteOutput);
|
|
11616
12701
|
if (existing && !sameCoordinates(existing.coordinates, coordinates)) {
|
|
11617
12702
|
throw new Error("bootstrap output belongs to different Cloudflare coordinates; choose a different output file");
|
|
@@ -11806,8 +12891,8 @@ var acceptanceFetch = async (url, label, fetcher) => {
|
|
|
11806
12891
|
return response.status;
|
|
11807
12892
|
};
|
|
11808
12893
|
async function verifyCloudflareBootstrapAcceptance(checkpointPath, fetcher = fetch) {
|
|
11809
|
-
const
|
|
11810
|
-
const output = await readExistingOutput(
|
|
12894
|
+
const absolute2 = resolve5(checkpointPath);
|
|
12895
|
+
const output = await readExistingOutput(absolute2);
|
|
11811
12896
|
if (!output || output.phase !== "complete")
|
|
11812
12897
|
throw new Error("Cloudflare acceptance requires a completed owner checkpoint");
|
|
11813
12898
|
const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
|
|
@@ -11828,13 +12913,13 @@ async function verifyCloudflareBootstrapAcceptance(checkpointPath, fetcher = fet
|
|
|
11828
12913
|
return {
|
|
11829
12914
|
format: 1,
|
|
11830
12915
|
kind: "forgezero-cloudflare-bootstrap-acceptance",
|
|
11831
|
-
checkpointFile:
|
|
12916
|
+
checkpointFile: absolute2,
|
|
11832
12917
|
verifiedAt: new Date().toISOString(),
|
|
11833
12918
|
nodes
|
|
11834
12919
|
};
|
|
11835
12920
|
}
|
|
11836
12921
|
async function removeCloudflareBootstrapSecrets(checkpointPath, output) {
|
|
11837
|
-
const directory = `${
|
|
12922
|
+
const directory = `${resolve5(checkpointPath)}.hosts`;
|
|
11838
12923
|
let entries;
|
|
11839
12924
|
try {
|
|
11840
12925
|
const metadata = await lstat(directory);
|
|
@@ -11856,11 +12941,11 @@ async function removeCloudflareBootstrapSecrets(checkpointPath, output) {
|
|
|
11856
12941
|
await unlink(join6(directory, name));
|
|
11857
12942
|
await rmdir(directory);
|
|
11858
12943
|
}
|
|
11859
|
-
await unlink(
|
|
12944
|
+
await unlink(resolve5(checkpointPath));
|
|
11860
12945
|
}
|
|
11861
12946
|
async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
|
|
11862
|
-
const checkpointPath =
|
|
11863
|
-
const acceptancePath =
|
|
12947
|
+
const checkpointPath = resolve5(request.checkpointPath);
|
|
12948
|
+
const acceptancePath = resolve5(request.acceptancePath);
|
|
11864
12949
|
if (acceptancePath === checkpointPath || acceptancePath.startsWith(`${checkpointPath}.hosts/`)) {
|
|
11865
12950
|
throw new Error("Cloudflare acceptance evidence must be outside the secret checkpoint and handoff directory");
|
|
11866
12951
|
}
|
|
@@ -12496,7 +13581,7 @@ function bootstrapIdentity(config) {
|
|
|
12496
13581
|
};
|
|
12497
13582
|
}
|
|
12498
13583
|
function bootstrapIdentityDigest(config) {
|
|
12499
|
-
return
|
|
13584
|
+
return createHash3("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
|
|
12500
13585
|
}
|
|
12501
13586
|
function parseStoredState(raw) {
|
|
12502
13587
|
let value;
|
|
@@ -13130,11 +14215,11 @@ function strictBootstrapDocument(value) {
|
|
|
13130
14215
|
return value;
|
|
13131
14216
|
}
|
|
13132
14217
|
function readBootstrapConfig(path) {
|
|
13133
|
-
const metadata =
|
|
14218
|
+
const metadata = lstatSync4(path);
|
|
13134
14219
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
|
|
13135
14220
|
throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
|
|
13136
14221
|
}
|
|
13137
|
-
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(
|
|
14222
|
+
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync8(path, "utf8"))));
|
|
13138
14223
|
}
|
|
13139
14224
|
function localBootstrapHost() {
|
|
13140
14225
|
const execute = async (argv2, options = {}) => {
|
|
@@ -13152,19 +14237,19 @@ function localBootstrapHost() {
|
|
|
13152
14237
|
};
|
|
13153
14238
|
return {
|
|
13154
14239
|
uid: () => process.getuid?.() ?? -1,
|
|
13155
|
-
exists:
|
|
13156
|
-
read: (path) =>
|
|
14240
|
+
exists: existsSync8,
|
|
14241
|
+
read: (path) => readFileSync8(path, "utf8"),
|
|
13157
14242
|
write(path, content, mode) {
|
|
13158
|
-
|
|
14243
|
+
mkdirSync8(dirname8(path), { recursive: true, mode: 493 });
|
|
13159
14244
|
const temporary = `${path}.next.${process.pid}`;
|
|
13160
|
-
|
|
14245
|
+
writeFileSync8(temporary, content, { mode });
|
|
13161
14246
|
chmodSync3(temporary, mode);
|
|
13162
|
-
|
|
14247
|
+
renameSync7(temporary, path);
|
|
13163
14248
|
},
|
|
13164
|
-
mkdir: (path, mode) =>
|
|
13165
|
-
remove: (path) =>
|
|
14249
|
+
mkdir: (path, mode) => mkdirSync8(path, { recursive: true, mode }),
|
|
14250
|
+
remove: (path) => rmSync5(path, { force: true }),
|
|
13166
14251
|
inspect(path) {
|
|
13167
|
-
const value =
|
|
14252
|
+
const value = lstatSync4(path);
|
|
13168
14253
|
return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
|
|
13169
14254
|
},
|
|
13170
14255
|
exec: execute,
|
|
@@ -13186,7 +14271,7 @@ function localBootstrapHost() {
|
|
|
13186
14271
|
async installAgent(config, enrolTokenSourcePath) {
|
|
13187
14272
|
const capabilities = await readCapabilities(localRunner);
|
|
13188
14273
|
const deployRoot = config.deployRoot ?? "/opt/forgezero";
|
|
13189
|
-
const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) ||
|
|
14274
|
+
const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync8("/var/lib/forgezero/enrolment.json");
|
|
13190
14275
|
if (config.kind === "platform") {
|
|
13191
14276
|
const lifecycle = config.database.role === "none" ? {
|
|
13192
14277
|
apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
|
|
@@ -13198,8 +14283,8 @@ function localBootstrapHost() {
|
|
|
13198
14283
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
13199
14284
|
databasePorts: [8529]
|
|
13200
14285
|
};
|
|
13201
|
-
|
|
13202
|
-
|
|
14286
|
+
mkdirSync8(dirname8(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
14287
|
+
writeFileSync8(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
13203
14288
|
`, { mode: 256 });
|
|
13204
14289
|
}
|
|
13205
14290
|
const plan = planInstall({
|
|
@@ -13240,8 +14325,8 @@ function localBootstrapHost() {
|
|
|
13240
14325
|
} : {}
|
|
13241
14326
|
});
|
|
13242
14327
|
for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
13243
|
-
|
|
13244
|
-
|
|
14328
|
+
mkdirSync8(dirname8(unit.path), { recursive: true, mode: 493 });
|
|
14329
|
+
writeFileSync8(unit.path, unit.unit, { mode: 420 });
|
|
13245
14330
|
}
|
|
13246
14331
|
await applyPlan(plan, localRunner);
|
|
13247
14332
|
return plan;
|
|
@@ -13250,8 +14335,8 @@ function localBootstrapHost() {
|
|
|
13250
14335
|
}
|
|
13251
14336
|
|
|
13252
14337
|
// src/cli/cloudflare-bootstrap.ts
|
|
13253
|
-
import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as
|
|
13254
|
-
import { dirname as
|
|
14338
|
+
import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as readFileSync9 } from "fs";
|
|
14339
|
+
import { dirname as dirname9, resolve as resolve6 } from "path";
|
|
13255
14340
|
var exactKeys3 = (value, allowed, label) => {
|
|
13256
14341
|
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
13257
14342
|
if (unknown.length)
|
|
@@ -13263,22 +14348,22 @@ var record2 = (value, label) => {
|
|
|
13263
14348
|
return value;
|
|
13264
14349
|
};
|
|
13265
14350
|
function readOwnerConfig(path) {
|
|
13266
|
-
const
|
|
14351
|
+
const absolute2 = resolve6(path);
|
|
13267
14352
|
let descriptor;
|
|
13268
14353
|
try {
|
|
13269
|
-
descriptor = openSync(
|
|
14354
|
+
descriptor = openSync(absolute2, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
13270
14355
|
const metadata = fstatSync(descriptor);
|
|
13271
14356
|
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
13272
14357
|
if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size < 2 || metadata.size > 131072 || uid !== undefined && metadata.uid !== uid || (metadata.mode & 63) !== 0) {
|
|
13273
|
-
throw new Error(`${
|
|
14358
|
+
throw new Error(`${absolute2} must be one operator-owned 0600 regular file`);
|
|
13274
14359
|
}
|
|
13275
|
-
return JSON.parse(
|
|
14360
|
+
return JSON.parse(readFileSync9(descriptor, "utf8"));
|
|
13276
14361
|
} catch (cause) {
|
|
13277
14362
|
if (cause instanceof SyntaxError)
|
|
13278
|
-
throw new Error(`${
|
|
13279
|
-
if (cause instanceof Error && cause.message.startsWith(
|
|
14363
|
+
throw new Error(`${absolute2} is not valid Cloudflare bootstrap JSON`);
|
|
14364
|
+
if (cause instanceof Error && cause.message.startsWith(absolute2))
|
|
13280
14365
|
throw cause;
|
|
13281
|
-
throw new Error(`cannot securely read Cloudflare bootstrap config ${
|
|
14366
|
+
throw new Error(`cannot securely read Cloudflare bootstrap config ${absolute2}`);
|
|
13282
14367
|
} finally {
|
|
13283
14368
|
if (descriptor !== undefined)
|
|
13284
14369
|
closeSync(descriptor);
|
|
@@ -13286,7 +14371,7 @@ function readOwnerConfig(path) {
|
|
|
13286
14371
|
}
|
|
13287
14372
|
function readCloudflareBootstrapCommandConfig(path, mode) {
|
|
13288
14373
|
const input = record2(readOwnerConfig(path), "Cloudflare bootstrap config");
|
|
13289
|
-
const baseDirectory =
|
|
14374
|
+
const baseDirectory = dirname9(resolve6(path));
|
|
13290
14375
|
exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates", "tokenFiles"], "Cloudflare bootstrap config");
|
|
13291
14376
|
if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-request") {
|
|
13292
14377
|
throw new Error("Cloudflare bootstrap config format/kind is invalid");
|
|
@@ -13334,8 +14419,8 @@ function readCloudflareBootstrapCommandConfig(path, mode) {
|
|
|
13334
14419
|
}
|
|
13335
14420
|
}
|
|
13336
14421
|
tokenFiles = {
|
|
13337
|
-
tunnelTokenFile:
|
|
13338
|
-
apiTokenFile:
|
|
14422
|
+
tunnelTokenFile: resolve6(baseDirectory, source.tunnelTokenFile),
|
|
14423
|
+
apiTokenFile: resolve6(baseDirectory, source.apiTokenFile)
|
|
13339
14424
|
};
|
|
13340
14425
|
}
|
|
13341
14426
|
if (mode === "apply" && !tokenFiles) {
|
|
@@ -13344,7 +14429,7 @@ function readCloudflareBootstrapCommandConfig(path, mode) {
|
|
|
13344
14429
|
return {
|
|
13345
14430
|
mode,
|
|
13346
14431
|
coordinates,
|
|
13347
|
-
checkpointPath:
|
|
14432
|
+
checkpointPath: resolve6(baseDirectory, input.checkpointPath),
|
|
13348
14433
|
...tokenFiles ? { tokenFiles } : {}
|
|
13349
14434
|
};
|
|
13350
14435
|
}
|
|
@@ -13356,14 +14441,14 @@ async function runCloudflareBootstrapCommand(configPath, apply, dependencies = {
|
|
|
13356
14441
|
return evidence;
|
|
13357
14442
|
}
|
|
13358
14443
|
async function runCloudflareBootstrapVerificationCommand(checkpointPath, dependencies = {}) {
|
|
13359
|
-
const evidence = await (dependencies.verify ?? verifyCloudflareBootstrapAcceptance)(
|
|
14444
|
+
const evidence = await (dependencies.verify ?? verifyCloudflareBootstrapAcceptance)(resolve6(checkpointPath));
|
|
13360
14445
|
(dependencies.write ?? ((text3) => process.stdout.write(text3)))(`${JSON.stringify(evidence, null, 2)}
|
|
13361
14446
|
`);
|
|
13362
14447
|
return evidence;
|
|
13363
14448
|
}
|
|
13364
14449
|
function readCloudflareBootstrapFinalizeConfig(path) {
|
|
13365
14450
|
const input = record2(readOwnerConfig(path), "Cloudflare bootstrap finalize config");
|
|
13366
|
-
const baseDirectory =
|
|
14451
|
+
const baseDirectory = dirname9(resolve6(path));
|
|
13367
14452
|
exactKeys3(input, ["format", "kind", "checkpointPath", "acceptancePath"], "Cloudflare bootstrap finalize config");
|
|
13368
14453
|
if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-finalize") {
|
|
13369
14454
|
throw new Error("Cloudflare bootstrap finalize config format/kind is invalid");
|
|
@@ -13374,8 +14459,8 @@ function readCloudflareBootstrapFinalizeConfig(path) {
|
|
|
13374
14459
|
}
|
|
13375
14460
|
}
|
|
13376
14461
|
return {
|
|
13377
|
-
checkpointPath:
|
|
13378
|
-
acceptancePath:
|
|
14462
|
+
checkpointPath: resolve6(baseDirectory, input.checkpointPath),
|
|
14463
|
+
acceptancePath: resolve6(baseDirectory, input.acceptancePath)
|
|
13379
14464
|
};
|
|
13380
14465
|
}
|
|
13381
14466
|
async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies = {}) {
|
|
@@ -13387,31 +14472,31 @@ async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies =
|
|
|
13387
14472
|
}
|
|
13388
14473
|
|
|
13389
14474
|
// src/metal-bootstrap.ts
|
|
13390
|
-
import { createHash as
|
|
14475
|
+
import { createHash as createHash4, randomBytes as randomBytes8 } from "crypto";
|
|
13391
14476
|
import {
|
|
13392
14477
|
chmodSync as chmodSync4,
|
|
13393
14478
|
chownSync,
|
|
13394
14479
|
copyFileSync as copyFileSync2,
|
|
13395
|
-
existsSync as
|
|
13396
|
-
lstatSync as
|
|
13397
|
-
mkdirSync as
|
|
13398
|
-
readFileSync as
|
|
13399
|
-
realpathSync as
|
|
13400
|
-
renameSync as
|
|
14480
|
+
existsSync as existsSync9,
|
|
14481
|
+
lstatSync as lstatSync5,
|
|
14482
|
+
mkdirSync as mkdirSync10,
|
|
14483
|
+
readFileSync as readFileSync10,
|
|
14484
|
+
realpathSync as realpathSync4,
|
|
14485
|
+
renameSync as renameSync8,
|
|
13401
14486
|
statSync,
|
|
13402
14487
|
symlinkSync as symlinkSync2,
|
|
13403
14488
|
unlinkSync as unlinkSync2,
|
|
13404
|
-
writeFileSync as
|
|
14489
|
+
writeFileSync as writeFileSync10
|
|
13405
14490
|
} from "fs";
|
|
13406
|
-
import { dirname as
|
|
14491
|
+
import { dirname as dirname11, isAbsolute as isAbsolute3, join as join9, resolve as resolve7 } from "path";
|
|
13407
14492
|
import { isIP as isIP5 } from "net";
|
|
13408
14493
|
|
|
13409
14494
|
// src/metal-isolation.ts
|
|
13410
|
-
import { mkdirSync as
|
|
14495
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
13411
14496
|
import { join as join8 } from "path";
|
|
13412
14497
|
|
|
13413
14498
|
// src/metal-provision.ts
|
|
13414
|
-
import { dirname as
|
|
14499
|
+
import { dirname as dirname10, isAbsolute as isAbsolute2, join as join7 } from "path";
|
|
13415
14500
|
import { isIP as isIP4 } from "net";
|
|
13416
14501
|
|
|
13417
14502
|
// src/ubuntu.ts
|
|
@@ -13460,7 +14545,7 @@ function validateMetalProfile(profile) {
|
|
|
13460
14545
|
if (!Number.isInteger(profile.addressStart) || !Number.isInteger(profile.addressEnd) || profile.addressStart < 2 || profile.addressEnd > 254 || profile.addressStart > profile.addressEnd)
|
|
13461
14546
|
throw new MetalProvisionError("invalid guest address range");
|
|
13462
14547
|
for (const path of [profile.stateDir, profile.seedDir, profile.unitDir]) {
|
|
13463
|
-
if (!
|
|
14548
|
+
if (!isAbsolute2(path))
|
|
13464
14549
|
throw new MetalProvisionError("metal paths must be absolute");
|
|
13465
14550
|
}
|
|
13466
14551
|
new URL(profile.apiUrl);
|
|
@@ -13599,16 +14684,16 @@ async function applyMetalIsolation(profile, exec = defaultExec) {
|
|
|
13599
14684
|
validateMetalProfile(profile);
|
|
13600
14685
|
await requireGuestsInSlice(exec);
|
|
13601
14686
|
const unitDir = profile.unitDir;
|
|
13602
|
-
|
|
13603
|
-
|
|
14687
|
+
mkdirSync9(unitDir, { recursive: true });
|
|
14688
|
+
writeFileSync9(join8(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
|
|
13604
14689
|
for (const unit of ["system.slice", "user.slice"]) {
|
|
13605
14690
|
const directory = join8(unitDir, `${unit}.d`);
|
|
13606
|
-
|
|
13607
|
-
|
|
14691
|
+
mkdirSync9(directory, { recursive: true });
|
|
14692
|
+
writeFileSync9(join8(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
|
|
13608
14693
|
}
|
|
13609
14694
|
const initDirectory = join8(unitDir, "init.scope.d");
|
|
13610
|
-
|
|
13611
|
-
|
|
14695
|
+
mkdirSync9(initDirectory, { recursive: true });
|
|
14696
|
+
writeFileSync9(join8(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
|
|
13612
14697
|
await checked3(exec, ["systemctl", "daemon-reload"]);
|
|
13613
14698
|
await requireGuestsInSlice(exec);
|
|
13614
14699
|
const properties = [`AllowedCPUs=${profile.housekeepingCpus}`];
|
|
@@ -13727,7 +14812,7 @@ function validateMetalBootstrapConfig(config) {
|
|
|
13727
14812
|
throw new MetalBootstrapError("metal bootstrap uses fixed state, seed, and systemd unit directories");
|
|
13728
14813
|
}
|
|
13729
14814
|
const image = config.profile.images[Object.keys(config.profile.images)[0]];
|
|
13730
|
-
if (!image.path.startsWith("/var/lib/forgezero/images/") ||
|
|
14815
|
+
if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve7(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
|
|
13731
14816
|
throw new MetalBootstrapError("the pinned guest image must use the fixed image directory");
|
|
13732
14817
|
}
|
|
13733
14818
|
if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION2) {
|
|
@@ -13762,11 +14847,11 @@ function validateMetalBootstrapConfig(config) {
|
|
|
13762
14847
|
return config;
|
|
13763
14848
|
}
|
|
13764
14849
|
function validateOwnerOnlyPath(path, requireRootOwner) {
|
|
13765
|
-
if (!
|
|
14850
|
+
if (!isAbsolute3(path) || resolve7(path) !== path || path.includes("/../")) {
|
|
13766
14851
|
throw new MetalBootstrapError("private bootstrap paths must be canonical absolute paths");
|
|
13767
14852
|
}
|
|
13768
|
-
const metadata =
|
|
13769
|
-
if (!metadata.isFile() || metadata.isSymbolicLink() ||
|
|
14853
|
+
const metadata = lstatSync5(path);
|
|
14854
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || realpathSync4(path) !== path) {
|
|
13770
14855
|
throw new MetalBootstrapError("private bootstrap path must be a regular non-symlink file");
|
|
13771
14856
|
}
|
|
13772
14857
|
if ((metadata.mode & 63) !== 0)
|
|
@@ -13783,7 +14868,7 @@ function readMetalBootstrapConfig(path) {
|
|
|
13783
14868
|
throw new MetalBootstrapError("metal bootstrap config size is invalid");
|
|
13784
14869
|
let parsed;
|
|
13785
14870
|
try {
|
|
13786
|
-
parsed = JSON.parse(
|
|
14871
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
13787
14872
|
} catch {
|
|
13788
14873
|
throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
|
|
13789
14874
|
}
|
|
@@ -13816,15 +14901,15 @@ function planMetalBootstrap(config) {
|
|
|
13816
14901
|
};
|
|
13817
14902
|
}
|
|
13818
14903
|
var atomicWrite2 = (path, body, mode) => {
|
|
13819
|
-
|
|
14904
|
+
mkdirSync10(dirname11(path), { recursive: true, mode: 493 });
|
|
13820
14905
|
const temporary = `${path}.next-${process.pid}`;
|
|
13821
|
-
|
|
14906
|
+
writeFileSync10(temporary, body, { mode, flag: "wx" });
|
|
13822
14907
|
chmodSync4(temporary, mode);
|
|
13823
14908
|
chownSync(temporary, 0, 0);
|
|
13824
|
-
|
|
14909
|
+
renameSync8(temporary, path);
|
|
13825
14910
|
};
|
|
13826
14911
|
var validateAgentSourcePath = (source) => {
|
|
13827
|
-
if (!
|
|
14912
|
+
if (!isAbsolute3(source) || !lstatSync5(source).isFile() || lstatSync5(source).isSymbolicLink()) {
|
|
13828
14913
|
throw new MetalBootstrapError("published Agent source path must be an absolute regular non-symlink file");
|
|
13829
14914
|
}
|
|
13830
14915
|
};
|
|
@@ -13988,11 +15073,11 @@ WantedBy=multi-user.target
|
|
|
13988
15073
|
var installAgentBinary = (source, version) => {
|
|
13989
15074
|
validateAgentSourcePath(source);
|
|
13990
15075
|
const release = `/opt/forgezero/agent/versions/${version}/dist`;
|
|
13991
|
-
|
|
15076
|
+
mkdirSync10(release, { recursive: true, mode: 493 });
|
|
13992
15077
|
copyFileSync2(source, join9(release, "fz-agent.js"));
|
|
13993
15078
|
chmodSync4(join9(release, "fz-agent.js"), 493);
|
|
13994
15079
|
chownSync(join9(release, "fz-agent.js"), 0, 0);
|
|
13995
|
-
|
|
15080
|
+
mkdirSync10("/opt/forgezero/agent", { recursive: true, mode: 493 });
|
|
13996
15081
|
for (const [link, target] of [
|
|
13997
15082
|
["/opt/forgezero/agent/current.next", `versions/${version}`],
|
|
13998
15083
|
[AGENT_PATH, "/opt/forgezero/agent/current/dist/fz-agent.js"]
|
|
@@ -14002,7 +15087,7 @@ var installAgentBinary = (source, version) => {
|
|
|
14002
15087
|
} catch {}
|
|
14003
15088
|
symlinkSync2(target, link);
|
|
14004
15089
|
if (link.endsWith("current.next"))
|
|
14005
|
-
|
|
15090
|
+
renameSync8(link, "/opt/forgezero/agent/current");
|
|
14006
15091
|
}
|
|
14007
15092
|
};
|
|
14008
15093
|
var preflight = async (config, exec) => {
|
|
@@ -14024,10 +15109,10 @@ var preflight = async (config, exec) => {
|
|
|
14024
15109
|
]);
|
|
14025
15110
|
await runChecked(exec, ["/usr/sbin/vgs", config.profile.volumeGroup]);
|
|
14026
15111
|
await runChecked(exec, ["/usr/sbin/ip", "link", "show", config.profile.bridge]);
|
|
14027
|
-
if (!
|
|
15112
|
+
if (!existsSync9("/dev/kvm"))
|
|
14028
15113
|
throw new MetalBootstrapError("/dev/kvm is required");
|
|
14029
15114
|
if (config.profile.confidential) {
|
|
14030
|
-
if (!
|
|
15115
|
+
if (!existsSync9("/dev/sev"))
|
|
14031
15116
|
throw new MetalBootstrapError("/dev/sev is required by the confidential profile");
|
|
14032
15117
|
await runChecked(exec, ["/usr/bin/qemu-system-x86_64", "-object", "sev-snp-guest,help"]);
|
|
14033
15118
|
}
|
|
@@ -14039,16 +15124,16 @@ var assertSupportedMetalHost = () => {
|
|
|
14039
15124
|
if (process.platform !== "linux" || process.arch !== "x64") {
|
|
14040
15125
|
throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
|
|
14041
15126
|
}
|
|
14042
|
-
const release =
|
|
15127
|
+
const release = readFileSync10("/etc/os-release", "utf8");
|
|
14043
15128
|
if (!/^ID=ubuntu$/m.test(release) || !/^VERSION_ID="?26\.04"?$/m.test(release)) {
|
|
14044
15129
|
throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
|
|
14045
15130
|
}
|
|
14046
15131
|
};
|
|
14047
15132
|
var ensurePinnedGuestImage = async (config, exec) => {
|
|
14048
15133
|
const image = config.profile.images[SUPPORTED_GUEST_IMAGE.key];
|
|
14049
|
-
if (
|
|
15134
|
+
if (existsSync9(image.path))
|
|
14050
15135
|
return;
|
|
14051
|
-
|
|
15136
|
+
mkdirSync10(dirname11(image.path), { recursive: true, mode: 493 });
|
|
14052
15137
|
const temporary = `${image.path}.next-${process.pid}`;
|
|
14053
15138
|
try {
|
|
14054
15139
|
await runChecked(exec, [
|
|
@@ -14067,7 +15152,7 @@ var ensurePinnedGuestImage = async (config, exec) => {
|
|
|
14067
15152
|
throw new MetalBootstrapError("downloaded guest image digest mismatch");
|
|
14068
15153
|
chmodSync4(temporary, 292);
|
|
14069
15154
|
chownSync(temporary, 0, 0);
|
|
14070
|
-
|
|
15155
|
+
renameSync8(temporary, image.path);
|
|
14071
15156
|
} catch (cause) {
|
|
14072
15157
|
try {
|
|
14073
15158
|
unlinkSync2(temporary);
|
|
@@ -14080,11 +15165,11 @@ async function applyMetalBootstrap(config, options) {
|
|
|
14080
15165
|
if ((options.getuid ?? process.getuid)?.() !== 0)
|
|
14081
15166
|
throw new MetalBootstrapError("fz bootstrap metal --apply must run as root");
|
|
14082
15167
|
assertSupportedMetalHost();
|
|
14083
|
-
if (
|
|
15168
|
+
if (existsSync9(STATE_PATH2) && !options.repair)
|
|
14084
15169
|
throw new MetalBootstrapError("metal host is already initialized; use explicit repair");
|
|
14085
15170
|
if (config.agentSeedFile)
|
|
14086
15171
|
validateOwnerOnlyPath(config.agentSeedFile, true);
|
|
14087
|
-
if (config.agentSeedFile &&
|
|
15172
|
+
if (config.agentSeedFile && existsSync9(SEED_CREDENTIAL_PATH)) {
|
|
14088
15173
|
throw new MetalBootstrapError("repair refuses replacement seed material while the sealed metal identity exists");
|
|
14089
15174
|
}
|
|
14090
15175
|
validateAgentSourcePath(options.agentSourcePath);
|
|
@@ -14111,9 +15196,9 @@ async function applyMetalBootstrap(config, options) {
|
|
|
14111
15196
|
await preflight(config, exec);
|
|
14112
15197
|
await ensureAccount(exec);
|
|
14113
15198
|
installAgentBinary(options.agentSourcePath, config.profile.agentVersion);
|
|
14114
|
-
|
|
14115
|
-
|
|
14116
|
-
|
|
15199
|
+
mkdirSync10("/etc/forgezero/creds", { recursive: true, mode: 448 });
|
|
15200
|
+
mkdirSync10(config.profile.stateDir, { recursive: true, mode: 448 });
|
|
15201
|
+
mkdirSync10(config.profile.seedDir, { recursive: true, mode: 448 });
|
|
14117
15202
|
const persistedProfile = {
|
|
14118
15203
|
...config.profile,
|
|
14119
15204
|
metalHostname: config.metalHostname,
|
|
@@ -14122,8 +15207,8 @@ async function applyMetalBootstrap(config, options) {
|
|
|
14122
15207
|
};
|
|
14123
15208
|
atomicWrite2(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
|
|
14124
15209
|
`, 384);
|
|
14125
|
-
if (!
|
|
14126
|
-
const seed = config.agentSeedFile ?
|
|
15210
|
+
if (!existsSync9(SEED_CREDENTIAL_PATH)) {
|
|
15211
|
+
const seed = config.agentSeedFile ? readFileSync10(config.agentSeedFile, "utf8").trim() : randomBytes8(32).toString("base64url");
|
|
14127
15212
|
if (seed.length < 32 || /[\0\r\n]/.test(seed))
|
|
14128
15213
|
throw new MetalBootstrapError("metal Agent seed is invalid");
|
|
14129
15214
|
await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
|
|
@@ -14177,7 +15262,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
14177
15262
|
initializedAt: new Date().toISOString(),
|
|
14178
15263
|
role: "metal",
|
|
14179
15264
|
metalHostname: config.metalHostname,
|
|
14180
|
-
profileSha256:
|
|
15265
|
+
profileSha256: createHash4("sha256").update(JSON.stringify(config.profile)).digest("hex")
|
|
14181
15266
|
};
|
|
14182
15267
|
atomicWrite2(STATE_PATH2, `${JSON.stringify(state, null, 2)}
|
|
14183
15268
|
`, 384);
|
|
@@ -14185,7 +15270,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
14185
15270
|
}
|
|
14186
15271
|
var socketReady = (path) => {
|
|
14187
15272
|
try {
|
|
14188
|
-
return
|
|
15273
|
+
return lstatSync5(path).isSocket();
|
|
14189
15274
|
} catch {
|
|
14190
15275
|
return false;
|
|
14191
15276
|
}
|
|
@@ -14194,17 +15279,17 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
14194
15279
|
const problems = [];
|
|
14195
15280
|
let profileValid = false, imageVerified = false, metalHostname, profileSha256;
|
|
14196
15281
|
let profileMode = null;
|
|
14197
|
-
if (
|
|
15282
|
+
if (existsSync9(PROFILE_PATH)) {
|
|
14198
15283
|
try {
|
|
14199
15284
|
const metadata = statSync(PROFILE_PATH);
|
|
14200
15285
|
profileMode = metadata.mode & 511;
|
|
14201
15286
|
if (profileMode !== 384 || metadata.uid !== 0)
|
|
14202
15287
|
problems.push("metal profile is not root-owned mode 0600");
|
|
14203
|
-
const persisted = JSON.parse(
|
|
15288
|
+
const persisted = JSON.parse(readFileSync10(PROFILE_PATH, "utf8"));
|
|
14204
15289
|
const { metalHostname: profileHostname, hostTelemetryEndpoint, hostTelemetryUnit, ...profile } = persisted;
|
|
14205
15290
|
validateMetalProfile(profile);
|
|
14206
15291
|
profileValid = true;
|
|
14207
|
-
profileSha256 =
|
|
15292
|
+
profileSha256 = createHash4("sha256").update(JSON.stringify(profile)).digest("hex");
|
|
14208
15293
|
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(profileHostname) || hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
|
|
14209
15294
|
problems.push("persisted metal host coordinates are invalid");
|
|
14210
15295
|
}
|
|
@@ -14233,7 +15318,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
14233
15318
|
problems.push("local OTLP metrics receiver did not accept a proof request");
|
|
14234
15319
|
}
|
|
14235
15320
|
const image = profile.images[Object.keys(profile.images)[0]];
|
|
14236
|
-
if (
|
|
15321
|
+
if (existsSync9(image.path)) {
|
|
14237
15322
|
const digest = (await exec(["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
|
|
14238
15323
|
imageVerified = digest === image.sha256;
|
|
14239
15324
|
}
|
|
@@ -14244,9 +15329,9 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
14244
15329
|
}
|
|
14245
15330
|
} else
|
|
14246
15331
|
problems.push("metal profile is missing");
|
|
14247
|
-
if (
|
|
15332
|
+
if (existsSync9(STATE_PATH2)) {
|
|
14248
15333
|
try {
|
|
14249
|
-
const state = JSON.parse(
|
|
15334
|
+
const state = JSON.parse(readFileSync10(STATE_PATH2, "utf8"));
|
|
14250
15335
|
metalHostname = state.metalHostname;
|
|
14251
15336
|
if (state.role !== "metal" || !metalHostname || state.profileSha256 !== profileSha256) {
|
|
14252
15337
|
problems.push("metal initialized state does not bind the current profile");
|
|
@@ -14262,7 +15347,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
14262
15347
|
"forgezero-metal-agent-egress.service",
|
|
14263
15348
|
"forgezero-metal-agent.service"
|
|
14264
15349
|
]) {
|
|
14265
|
-
if (!
|
|
15350
|
+
if (!existsSync9(join9(UNIT_DIRECTORY, unit)))
|
|
14266
15351
|
units[unit] = "missing";
|
|
14267
15352
|
else
|
|
14268
15353
|
units[unit] = (await exec(["/usr/bin/systemctl", "is-active", "--quiet", unit])).exitCode === 0 ? "active" : "inactive";
|
|
@@ -14276,7 +15361,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
14276
15361
|
if (!updateSocketReady)
|
|
14277
15362
|
problems.push("Agent update helper socket is not ready");
|
|
14278
15363
|
return {
|
|
14279
|
-
initialized:
|
|
15364
|
+
initialized: existsSync9(STATE_PATH2),
|
|
14280
15365
|
profileValid,
|
|
14281
15366
|
profileMode,
|
|
14282
15367
|
imageVerified,
|
|
@@ -14289,8 +15374,8 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
14289
15374
|
}
|
|
14290
15375
|
|
|
14291
15376
|
// src/operator-bootstrap.ts
|
|
14292
|
-
import { createHash as
|
|
14293
|
-
import { lstatSync as
|
|
15377
|
+
import { createHash as createHash5, randomBytes as randomBytes9 } from "crypto";
|
|
15378
|
+
import { lstatSync as lstatSync6, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "fs";
|
|
14294
15379
|
import { isIP as isIP6 } from "net";
|
|
14295
15380
|
import { tmpdir } from "os";
|
|
14296
15381
|
import { basename as basename2, join as join10 } from "path";
|
|
@@ -14384,22 +15469,22 @@ var exactKeys5 = (value, keys, label) => {
|
|
|
14384
15469
|
var ownerFile = (path, limit, label) => {
|
|
14385
15470
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
14386
15471
|
throw new Error(`${label} path must be absolute`);
|
|
14387
|
-
const metadata =
|
|
15472
|
+
const metadata = lstatSync6(path);
|
|
14388
15473
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
14389
15474
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size < 1 || metadata.size > limit) {
|
|
14390
15475
|
throw new Error(`${label} must be an owner-only regular file with one link and at most ${limit} bytes`);
|
|
14391
15476
|
}
|
|
14392
|
-
return
|
|
15477
|
+
return readFileSync11(path);
|
|
14393
15478
|
};
|
|
14394
15479
|
var publicIdentity = (path) => {
|
|
14395
15480
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
14396
15481
|
throw new Error("SSH public-key path must be absolute");
|
|
14397
|
-
const metadata =
|
|
15482
|
+
const metadata = lstatSync6(path);
|
|
14398
15483
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
14399
15484
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || metadata.size > 16384) {
|
|
14400
15485
|
throw new Error("SSH public key must be a caller-owned regular file with one link");
|
|
14401
15486
|
}
|
|
14402
|
-
const value =
|
|
15487
|
+
const value = readFileSync11(path, "utf8").trim();
|
|
14403
15488
|
if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(value)) {
|
|
14404
15489
|
throw new Error("SSH public key is malformed");
|
|
14405
15490
|
}
|
|
@@ -14408,7 +15493,7 @@ var publicIdentity = (path) => {
|
|
|
14408
15493
|
var socketPath = (path) => {
|
|
14409
15494
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
14410
15495
|
throw new Error("SSH agent socket path must be absolute");
|
|
14411
|
-
const metadata =
|
|
15496
|
+
const metadata = lstatSync6(path);
|
|
14412
15497
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
14413
15498
|
if (!metadata.isSocket() || metadata.isSymbolicLink() || metadata.uid !== uid) {
|
|
14414
15499
|
throw new Error("SSH agent socket must be a caller-owned Unix socket");
|
|
@@ -14422,7 +15507,7 @@ var fingerprint = (key) => {
|
|
|
14422
15507
|
if (!bytes.length || bytes.toString("base64").replace(/=+$/, "") !== encoded.replace(/=+$/, "")) {
|
|
14423
15508
|
throw new Error("SSH host key is malformed");
|
|
14424
15509
|
}
|
|
14425
|
-
return `SHA256:${
|
|
15510
|
+
return `SHA256:${createHash5("sha256").update(bytes).digest("base64").replace(/=+$/, "")}`;
|
|
14426
15511
|
};
|
|
14427
15512
|
var validateHop = (value, label) => {
|
|
14428
15513
|
const hop = exactKeys5(value, ["address", "port", "user", "hostKey", "hostKeySha256"], label);
|
|
@@ -14603,7 +15688,7 @@ function writeKnownHosts(request, directory) {
|
|
|
14603
15688
|
if (request.target.jump)
|
|
14604
15689
|
lines.push(`${hostLabel(request.target.jump.address, request.target.jump.port)} ${request.target.jump.hostKey}`);
|
|
14605
15690
|
const path = join10(directory, "known_hosts");
|
|
14606
|
-
|
|
15691
|
+
writeFileSync11(path, `${lines.join(`
|
|
14607
15692
|
`)}
|
|
14608
15693
|
`, { mode: 384, flag: "wx" });
|
|
14609
15694
|
return path;
|
|
@@ -14678,7 +15763,7 @@ function stageConfig(config, directory) {
|
|
|
14678
15763
|
for (const [name, source] of secretSources(config)) {
|
|
14679
15764
|
const bytes = ownerFile(source, SECRET_LIMIT, name);
|
|
14680
15765
|
const local = join10(directory, name);
|
|
14681
|
-
|
|
15766
|
+
writeFileSync11(local, bytes, { mode: 384, flag: "wx" });
|
|
14682
15767
|
staged.push(name);
|
|
14683
15768
|
const remotePath = `${REMOTE_STAGE}/${name}`;
|
|
14684
15769
|
if (name === "cluster-code")
|
|
@@ -14693,7 +15778,7 @@ function stageConfig(config, directory) {
|
|
|
14693
15778
|
rewritten.cloudflareHandoff.handoffFile = remotePath;
|
|
14694
15779
|
}
|
|
14695
15780
|
const path = join10(directory, "platform-config.json");
|
|
14696
|
-
|
|
15781
|
+
writeFileSync11(path, `${JSON.stringify(rewritten, null, 2)}
|
|
14697
15782
|
`, { mode: 384, flag: "wx" });
|
|
14698
15783
|
return { path, files: staged };
|
|
14699
15784
|
}
|
|
@@ -14702,7 +15787,7 @@ function stageMetalConfig(config, directory) {
|
|
|
14702
15787
|
const files = [];
|
|
14703
15788
|
if (config.agentSeedFile) {
|
|
14704
15789
|
const name = "metal-agent-seed";
|
|
14705
|
-
|
|
15790
|
+
writeFileSync11(join10(directory, name), ownerFile(config.agentSeedFile, SECRET_LIMIT, name), {
|
|
14706
15791
|
mode: 384,
|
|
14707
15792
|
flag: "wx"
|
|
14708
15793
|
});
|
|
@@ -14710,7 +15795,7 @@ function stageMetalConfig(config, directory) {
|
|
|
14710
15795
|
files.push(name);
|
|
14711
15796
|
}
|
|
14712
15797
|
const path = join10(directory, "metal-config.json");
|
|
14713
|
-
|
|
15798
|
+
writeFileSync11(path, `${JSON.stringify(rewritten, null, 2)}
|
|
14714
15799
|
`, { mode: 384, flag: "wx" });
|
|
14715
15800
|
return { path, files };
|
|
14716
15801
|
}
|
|
@@ -14719,10 +15804,10 @@ async function verifiedBunArchive(directory, fetcher) {
|
|
|
14719
15804
|
if (!response.ok)
|
|
14720
15805
|
throw new Error("pinned Bun download failed");
|
|
14721
15806
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
14722
|
-
if (
|
|
15807
|
+
if (createHash5("sha256").update(bytes).digest("hex") !== BUN_RELEASE_SHA256)
|
|
14723
15808
|
throw new Error("pinned Bun checksum mismatch");
|
|
14724
15809
|
const path = join10(directory, "bun.zip");
|
|
14725
|
-
|
|
15810
|
+
writeFileSync11(path, bytes, { mode: 384, flag: "wx" });
|
|
14726
15811
|
return path;
|
|
14727
15812
|
}
|
|
14728
15813
|
async function installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options) {
|
|
@@ -14732,7 +15817,7 @@ async function installPackagedAgent(request, knownHosts, exec, directory, remote
|
|
|
14732
15817
|
[options.fzGitSshPath ?? fileURLToPath2(new URL("./fz-git-ssh.js", import.meta.url)), "fz-git-ssh.js"]
|
|
14733
15818
|
];
|
|
14734
15819
|
for (const [artifact] of artifacts) {
|
|
14735
|
-
if (!
|
|
15820
|
+
if (!readFileSync11(artifact).length)
|
|
14736
15821
|
throw new Error(`packaged artifact is empty: ${basename2(artifact)}`);
|
|
14737
15822
|
}
|
|
14738
15823
|
await remote(exec, request, knownHosts, ["/usr/bin/mkdir", "-m", "0700", remoteTemp], "remote staging");
|
|
@@ -14803,7 +15888,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
14803
15888
|
return;
|
|
14804
15889
|
});
|
|
14805
15890
|
}
|
|
14806
|
-
|
|
15891
|
+
rmSync6(directory, { recursive: true, force: true });
|
|
14807
15892
|
}
|
|
14808
15893
|
}
|
|
14809
15894
|
async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
@@ -14855,7 +15940,7 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
14855
15940
|
return;
|
|
14856
15941
|
});
|
|
14857
15942
|
}
|
|
14858
|
-
|
|
15943
|
+
rmSync6(directory, { recursive: true, force: true });
|
|
14859
15944
|
}
|
|
14860
15945
|
}
|
|
14861
15946
|
|
|
@@ -14910,7 +15995,7 @@ function platformGenesisBootstrapConfigs(template, nodes) {
|
|
|
14910
15995
|
}
|
|
14911
15996
|
|
|
14912
15997
|
// src/host-maintenance.ts
|
|
14913
|
-
import { lstatSync as
|
|
15998
|
+
import { lstatSync as lstatSync7, readFileSync as readFileSync12 } from "fs";
|
|
14914
15999
|
var ROOT = "/opt/forgezero";
|
|
14915
16000
|
var SLOT_FILE = `${ROOT}/.forge-slot`;
|
|
14916
16001
|
var SHARED_ENV = `${ROOT}/shared/.env`;
|
|
@@ -14918,9 +16003,9 @@ var JWT = "/etc/forgezero/creds/arangodb-jwt.cred";
|
|
|
14918
16003
|
var FZ = "/usr/local/bin/fz";
|
|
14919
16004
|
var localRuntime = () => ({
|
|
14920
16005
|
uid: () => process.getuid?.() ?? -1,
|
|
14921
|
-
read: (path) =>
|
|
16006
|
+
read: (path) => readFileSync12(path, "utf8"),
|
|
14922
16007
|
inspect(path) {
|
|
14923
|
-
const value =
|
|
16008
|
+
const value = lstatSync7(path);
|
|
14924
16009
|
return {
|
|
14925
16010
|
regular: value.isFile(),
|
|
14926
16011
|
symbolic: value.isSymbolicLink(),
|
|
@@ -15041,8 +16126,8 @@ async function applyHostMaintenance(request, runtime = localRuntime()) {
|
|
|
15041
16126
|
}
|
|
15042
16127
|
|
|
15043
16128
|
// src/cli/maintenance.ts
|
|
15044
|
-
import { existsSync as
|
|
15045
|
-
import { isAbsolute as
|
|
16129
|
+
import { existsSync as existsSync10, lstatSync as lstatSync8, readFileSync as readFileSync13, realpathSync as realpathSync5 } from "fs";
|
|
16130
|
+
import { isAbsolute as isAbsolute4, join as join11, relative as relative2, resolve as resolve8 } from "path";
|
|
15046
16131
|
var API_OPERATION_ENTRYPOINTS = {
|
|
15047
16132
|
"dev-reset": ["src", "server", "maintenance", "dev-reset.ts"],
|
|
15048
16133
|
"db-backup": ["src", "server", "maintenance", "snapshot-backup.ts"],
|
|
@@ -15112,12 +16197,12 @@ function unsupportedRepositoryCliOption(argv2) {
|
|
|
15112
16197
|
}
|
|
15113
16198
|
function manifestName(root) {
|
|
15114
16199
|
const manifestPath = join11(root, "package.json");
|
|
15115
|
-
if (!
|
|
16200
|
+
if (!existsSync10(manifestPath)) {
|
|
15116
16201
|
throw new Error(`No package.json exists at repository root ${root}.`);
|
|
15117
16202
|
}
|
|
15118
16203
|
let parsed;
|
|
15119
16204
|
try {
|
|
15120
|
-
parsed = JSON.parse(
|
|
16205
|
+
parsed = JSON.parse(readFileSync13(manifestPath, "utf8"));
|
|
15121
16206
|
} catch (cause) {
|
|
15122
16207
|
throw new Error(`Cannot read ${manifestPath}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
15123
16208
|
}
|
|
@@ -15128,21 +16213,21 @@ function manifestName(root) {
|
|
|
15128
16213
|
}
|
|
15129
16214
|
function checkedEntrypoint(root, parts) {
|
|
15130
16215
|
const candidate = join11(root, ...parts);
|
|
15131
|
-
if (!
|
|
16216
|
+
if (!existsSync10(candidate) || !lstatSync8(candidate).isFile()) {
|
|
15132
16217
|
throw new Error(`The reviewed operation entrypoint is missing: ${candidate}`);
|
|
15133
16218
|
}
|
|
15134
|
-
if (
|
|
16219
|
+
if (lstatSync8(candidate).isSymbolicLink()) {
|
|
15135
16220
|
throw new Error(`The reviewed operation entrypoint must not be a symbolic link: ${candidate}`);
|
|
15136
16221
|
}
|
|
15137
|
-
const actual =
|
|
15138
|
-
const within =
|
|
15139
|
-
if (within.startsWith("..") ||
|
|
16222
|
+
const actual = realpathSync5(candidate);
|
|
16223
|
+
const within = relative2(root, actual);
|
|
16224
|
+
if (within.startsWith("..") || isAbsolute4(within)) {
|
|
15140
16225
|
throw new Error(`The reviewed operation entrypoint escapes repository root ${root}.`);
|
|
15141
16226
|
}
|
|
15142
16227
|
return actual;
|
|
15143
16228
|
}
|
|
15144
16229
|
function resolveRepositoryOperation(operation, requestedRoot) {
|
|
15145
|
-
const root =
|
|
16230
|
+
const root = realpathSync5(resolve8(requestedRoot));
|
|
15146
16231
|
const name = manifestName(root);
|
|
15147
16232
|
if (operation in API_OPERATION_ENTRYPOINTS) {
|
|
15148
16233
|
const parts = API_OPERATION_ENTRYPOINTS[operation];
|
|
@@ -15155,7 +16240,7 @@ function resolveRepositoryOperation(operation, requestedRoot) {
|
|
|
15155
16240
|
};
|
|
15156
16241
|
}
|
|
15157
16242
|
if (name === "forgezero") {
|
|
15158
|
-
const apiRoot =
|
|
16243
|
+
const apiRoot = realpathSync5(join11(root, "api"));
|
|
15159
16244
|
if (manifestName(apiRoot) !== "@forgezero/api") {
|
|
15160
16245
|
throw new Error(`${apiRoot} is not the ForgeZero API package.`);
|
|
15161
16246
|
}
|
|
@@ -15443,7 +16528,7 @@ async function api(options, path, init) {
|
|
|
15443
16528
|
return { status: response.status, body };
|
|
15444
16529
|
}
|
|
15445
16530
|
function sleep(ms) {
|
|
15446
|
-
return new Promise((
|
|
16531
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
15447
16532
|
}
|
|
15448
16533
|
function browserCommand(url) {
|
|
15449
16534
|
if (process.platform === "darwin")
|
|
@@ -15528,7 +16613,7 @@ function requireSession() {
|
|
|
15528
16613
|
function readOwnerOnlySecret(path, label) {
|
|
15529
16614
|
let metadata;
|
|
15530
16615
|
try {
|
|
15531
|
-
metadata =
|
|
16616
|
+
metadata = lstatSync9(path);
|
|
15532
16617
|
} catch {
|
|
15533
16618
|
throw new Error(`The ${label} file ${path} is unreadable.`);
|
|
15534
16619
|
}
|
|
@@ -15542,7 +16627,7 @@ function readOwnerOnlySecret(path, label) {
|
|
|
15542
16627
|
if (uid !== undefined && uid !== 0 && metadata.uid !== uid) {
|
|
15543
16628
|
throw new Error(`The ${label} file ${path} is not owned by the current user.`);
|
|
15544
16629
|
}
|
|
15545
|
-
const value =
|
|
16630
|
+
const value = readFileSync14(path, "utf8").trim();
|
|
15546
16631
|
if (!value)
|
|
15547
16632
|
throw new Error(`The ${label} file ${path} is empty.`);
|
|
15548
16633
|
return value;
|
|
@@ -15666,13 +16751,13 @@ async function cmdRoutes(options, search) {
|
|
|
15666
16751
|
throw new Error("This API does not expose the CLI action catalog yet. Update the ForgeZero API first.");
|
|
15667
16752
|
}
|
|
15668
16753
|
const needle = search?.trim().toLowerCase();
|
|
15669
|
-
const
|
|
16754
|
+
const actions2 = needle ? body.actions.filter((action) => `${action.method} ${action.path} ${action.label} ${action.page}`.toLowerCase().includes(needle)) : body.actions;
|
|
15670
16755
|
if (options.json) {
|
|
15671
|
-
out.line(JSON.stringify({ api: options.api, realm: options.realm, actions }, null, 2));
|
|
16756
|
+
out.line(JSON.stringify({ api: options.api, realm: options.realm, actions: actions2 }, null, 2));
|
|
15672
16757
|
return 0;
|
|
15673
16758
|
}
|
|
15674
16759
|
out.line(`Authorized UI actions for ${options.realm}${needle ? ` matching "${search}"` : ""}:`);
|
|
15675
|
-
for (const action of
|
|
16760
|
+
for (const action of actions2) {
|
|
15676
16761
|
out.line(` ${action.method.padEnd(6)} ${action.path}${action.freshProof ? " [fresh proof]" : ""}`);
|
|
15677
16762
|
out.step(`${action.label} \xB7 page ${action.page}`);
|
|
15678
16763
|
}
|
|
@@ -15723,7 +16808,7 @@ async function cmdApi(options, args) {
|
|
|
15723
16808
|
let body = undefined;
|
|
15724
16809
|
if (options.data !== undefined && options.dataFile)
|
|
15725
16810
|
throw new Error("Use only one of --data or --data-file.");
|
|
15726
|
-
const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ?
|
|
16811
|
+
const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync14(options.dataFile, "utf8") : options.data;
|
|
15727
16812
|
if (encoded !== undefined)
|
|
15728
16813
|
body = JSON.parse(encoded);
|
|
15729
16814
|
const result = await api(options, `${url.pathname}${url.search}`, { method, body });
|
|
@@ -15857,22 +16942,22 @@ async function cmdAgent(options, args) {
|
|
|
15857
16942
|
out.line(auxiliary.unit);
|
|
15858
16943
|
}
|
|
15859
16944
|
out.line(" --- then ---");
|
|
15860
|
-
for (const
|
|
15861
|
-
out.step(`${
|
|
16945
|
+
for (const step3 of plan.steps)
|
|
16946
|
+
out.step(`${step3.command}`);
|
|
15862
16947
|
return 0;
|
|
15863
16948
|
}
|
|
15864
16949
|
try {
|
|
15865
|
-
|
|
16950
|
+
writeFileSync12(plan.unitPath, plan.unit, { mode: 420 });
|
|
15866
16951
|
out.ok(`Wrote ${plan.unitPath}`);
|
|
15867
16952
|
for (const auxiliary of plan.auxiliaryUnits) {
|
|
15868
|
-
|
|
15869
|
-
|
|
16953
|
+
mkdirSync11(dirname12(auxiliary.path), { recursive: true, mode: 493 });
|
|
16954
|
+
writeFileSync12(auxiliary.path, auxiliary.unit, { mode: 420 });
|
|
15870
16955
|
out.ok(`Wrote ${auxiliary.path}`);
|
|
15871
16956
|
}
|
|
15872
16957
|
if (options.enrol) {
|
|
15873
|
-
if (
|
|
16958
|
+
if (existsSync11(enrolTokenSourcePath)) {
|
|
15874
16959
|
const source = statSync2(enrolTokenSourcePath);
|
|
15875
|
-
const token =
|
|
16960
|
+
const token = readFileSync14(enrolTokenSourcePath, "utf8").trim();
|
|
15876
16961
|
if (!source.isFile() || (source.mode & 511) !== 384 || source.uid !== 0) {
|
|
15877
16962
|
throw new Error("The preloaded enrolment token must be a root-owned 0600 file in /run.");
|
|
15878
16963
|
}
|
|
@@ -15885,18 +16970,18 @@ async function cmdAgent(options, args) {
|
|
|
15885
16970
|
if (await prompt.exited !== 0 || !/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
|
|
15886
16971
|
throw new Error("A valid fze_ enrolment token was not provided.");
|
|
15887
16972
|
}
|
|
15888
|
-
|
|
16973
|
+
writeFileSync12(enrolTokenSourcePath, `${token}
|
|
15889
16974
|
`, { mode: 384, flag: "wx" });
|
|
15890
16975
|
}
|
|
15891
16976
|
}
|
|
15892
16977
|
const transcript = await applyPlan(plan, localRunner);
|
|
15893
|
-
for (const
|
|
15894
|
-
out.ok(
|
|
16978
|
+
for (const step3 of transcript)
|
|
16979
|
+
out.ok(step3.label);
|
|
15895
16980
|
out.ok("Agent service and socket verified");
|
|
15896
16981
|
out.line();
|
|
15897
16982
|
out.line(" Add this machine-specific PUBLIC key as a read-only deploy key:");
|
|
15898
16983
|
out.line();
|
|
15899
|
-
out.line(` ${
|
|
16984
|
+
out.line(` ${readFileSync14(gitPublicKeyPath, "utf8").trim()}`);
|
|
15900
16985
|
out.line();
|
|
15901
16986
|
return 0;
|
|
15902
16987
|
} catch (cause) {
|
|
@@ -15985,19 +17070,19 @@ function interactiveMetalBootstrap() {
|
|
|
15985
17070
|
});
|
|
15986
17071
|
}
|
|
15987
17072
|
function writeBootstrapConfig(path, config) {
|
|
15988
|
-
if (!
|
|
17073
|
+
if (!isAbsolute5(path) || resolve9(path) !== path)
|
|
15989
17074
|
throw new Error("--output must be a canonical absolute path");
|
|
15990
|
-
|
|
15991
|
-
|
|
17075
|
+
mkdirSync11(dirname12(path), { recursive: true, mode: 448 });
|
|
17076
|
+
writeFileSync12(path, `${JSON.stringify(config, null, 2)}
|
|
15992
17077
|
`, { mode: 384, flag: "wx" });
|
|
15993
17078
|
return path;
|
|
15994
17079
|
}
|
|
15995
17080
|
function genesisOutputDirectory(path) {
|
|
15996
|
-
if (!
|
|
17081
|
+
if (!isAbsolute5(path) || resolve9(path) !== path)
|
|
15997
17082
|
throw new Error("--output must be a canonical absolute directory");
|
|
15998
|
-
if (!
|
|
15999
|
-
|
|
16000
|
-
const metadata =
|
|
17083
|
+
if (!existsSync11(path))
|
|
17084
|
+
mkdirSync11(path, { recursive: true, mode: 448 });
|
|
17085
|
+
const metadata = lstatSync9(path);
|
|
16001
17086
|
const uid = process.getuid?.();
|
|
16002
17087
|
if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 63) !== 0 || uid !== undefined && uid !== 0 && metadata.uid !== uid) {
|
|
16003
17088
|
throw new Error("platform genesis output must be an owner-only directory owned by the current user");
|
|
@@ -16006,8 +17091,8 @@ function genesisOutputDirectory(path) {
|
|
|
16006
17091
|
}
|
|
16007
17092
|
function ensureGenesisClusterSecret(directory) {
|
|
16008
17093
|
const path = `${directory}/cluster-bootstrap.code`;
|
|
16009
|
-
if (!
|
|
16010
|
-
|
|
17094
|
+
if (!existsSync11(path))
|
|
17095
|
+
writeFileSync12(path, `${randomBytes10(32).toString("hex")}
|
|
16011
17096
|
`, { mode: 384, flag: "wx" });
|
|
16012
17097
|
if (!/^[a-f0-9]{64}$/i.test(readOwnerOnlySecret(path, "cluster bootstrap code"))) {
|
|
16013
17098
|
throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
|
|
@@ -16264,8 +17349,8 @@ async function cmdBootstrap(options, args) {
|
|
|
16264
17349
|
return 0;
|
|
16265
17350
|
}
|
|
16266
17351
|
const installedBootstrapKind = () => resolveInstalledBootstrapKind({
|
|
16267
|
-
metal:
|
|
16268
|
-
compute:
|
|
17352
|
+
metal: existsSync11(METAL_BOOTSTRAP_STATE_PATH),
|
|
17353
|
+
compute: existsSync11(BOOTSTRAP_STATE_PATH)
|
|
16269
17354
|
});
|
|
16270
17355
|
if (operation === "status") {
|
|
16271
17356
|
if (installedBootstrapKind() === "metal") {
|
|
@@ -16389,7 +17474,7 @@ async function cmdUnlock(options) {
|
|
|
16389
17474
|
if (options.key || options.userExplicit) {
|
|
16390
17475
|
throw new Error("SSH/user unlock coordinates are not accepted. SSH keys cannot open a WebAuthn-PRF custody envelope; use --phrase-file or --phrase-stdin.");
|
|
16391
17476
|
}
|
|
16392
|
-
const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ?
|
|
17477
|
+
const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync14(0, "utf8").trim() : "";
|
|
16393
17478
|
const phrase = phraseText ? phraseText.split(/\s+/) : [];
|
|
16394
17479
|
if (phrase.length !== 24) {
|
|
16395
17480
|
throw new Error(`Recovery phrase must contain exactly 24 words; received ${phrase.length}. Use --phrase-file or --phrase-stdin.`);
|
|
@@ -16605,7 +17690,7 @@ function projectFromCheckout(options) {
|
|
|
16605
17690
|
try {
|
|
16606
17691
|
return loadConfig({
|
|
16607
17692
|
cwd: options.projectRoot,
|
|
16608
|
-
readFile: (path) =>
|
|
17693
|
+
readFile: (path) => existsSync11(path) ? readFileSync14(path, "utf8") : undefined
|
|
16609
17694
|
}).config.project;
|
|
16610
17695
|
} catch (cause) {
|
|
16611
17696
|
throw new Error(`No --project was given and the checkout has no usable .fz/config.json: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
@@ -16653,24 +17738,69 @@ async function cmdDeploy(options, args) {
|
|
|
16653
17738
|
if (options.optionError)
|
|
16654
17739
|
throw new Error(options.optionError);
|
|
16655
17740
|
if (operation === "init") {
|
|
16656
|
-
|
|
16657
|
-
|
|
16658
|
-
|
|
16659
|
-
|
|
16660
|
-
|
|
16661
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
17741
|
+
if (options.deploySoftware.length > 0 || options.deployProfile !== "app" || options.requireAttestation) {
|
|
17742
|
+
const created = initializeDeployFile(options.projectRoot, {
|
|
17743
|
+
name: options.projectName,
|
|
17744
|
+
profile: options.deployProfile,
|
|
17745
|
+
software: softwareCoordinates(options.deploySoftware),
|
|
17746
|
+
requireAttestation: options.requireAttestation,
|
|
17747
|
+
channel: options.deployChannel,
|
|
17748
|
+
force: options.force
|
|
17749
|
+
});
|
|
17750
|
+
if (options.json)
|
|
17751
|
+
out.line(JSON.stringify(created.summary, null, 2));
|
|
17752
|
+
else
|
|
17753
|
+
out.ok(`Initialized legacy .fz/deploy.json (${created.summary.digest}).`);
|
|
17754
|
+
return 0;
|
|
17755
|
+
}
|
|
17756
|
+
initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(resolve9(options.projectRoot)), force: options.force });
|
|
17757
|
+
const compiled = await compileDeploymentProject(options.projectRoot);
|
|
16664
17758
|
if (options.json)
|
|
16665
|
-
out.line(JSON.stringify(
|
|
17759
|
+
out.line(JSON.stringify({ source: compiled.source, output: compiled.output, digest: compiled.digest }, null, 2));
|
|
16666
17760
|
else {
|
|
16667
|
-
out.ok(`Initialized
|
|
16668
|
-
out.warn("
|
|
17761
|
+
out.ok(`Initialized ${DEPLOY_SOURCE_FILE} and ${DEPLOY_PLAN_FILE} (${compiled.digest}).`);
|
|
17762
|
+
out.warn("Build and release use safe blockers until you replace them with project-specific typed actions.");
|
|
16669
17763
|
out.step("Run `fz deploy check`, commit the file, then push; the verified Git commit is the live sync.");
|
|
16670
17764
|
}
|
|
16671
17765
|
return 0;
|
|
16672
17766
|
}
|
|
17767
|
+
if (operation === "compile") {
|
|
17768
|
+
const compiled = await compileDeploymentProject(options.projectRoot);
|
|
17769
|
+
const problems = canonicalJson(compiled.plan).includes("pipeline-todo") ? ["typed plan contains safe initialization blockers"] : [];
|
|
17770
|
+
const summary = { source: compiled.source, output: compiled.output, digest: compiled.digest, changed: compiled.changed, ready: problems.length === 0, problems };
|
|
17771
|
+
if (options.json)
|
|
17772
|
+
out.line(JSON.stringify(summary, null, 2));
|
|
17773
|
+
else {
|
|
17774
|
+
out.ok(`Compiled ${DEPLOY_SOURCE_FILE} \u2192 ${DEPLOY_PLAN_FILE} (${compiled.digest}).`);
|
|
17775
|
+
for (const problem of problems)
|
|
17776
|
+
out.warn(problem);
|
|
17777
|
+
}
|
|
17778
|
+
return problems.length === 0 ? 0 : 1;
|
|
17779
|
+
}
|
|
16673
17780
|
if (operation === "check" || operation === "sync") {
|
|
17781
|
+
if (existsSync11(resolve9(options.projectRoot, DEPLOY_SOURCE_FILE))) {
|
|
17782
|
+
const expected = await compileDeploymentProject(options.projectRoot, { write: false });
|
|
17783
|
+
const actual = inspectCompiledDeployment(options.projectRoot);
|
|
17784
|
+
const current = canonicalJson(expected.plan) === canonicalJson(actual.plan);
|
|
17785
|
+
const problems = [
|
|
17786
|
+
...current ? [] : [`${DEPLOY_PLAN_FILE} is stale; run \`fz deploy compile\``],
|
|
17787
|
+
...canonicalJson(expected.plan).includes("pipeline-todo") ? ["typed plan contains safe initialization blockers"] : []
|
|
17788
|
+
];
|
|
17789
|
+
const summary = { source: expected.source, output: actual.path, digest: actual.digest, version: actual.plan.version, name: actual.plan.name, ready: problems.length === 0, problems };
|
|
17790
|
+
if (options.json)
|
|
17791
|
+
out.line(JSON.stringify(summary, null, 2));
|
|
17792
|
+
else {
|
|
17793
|
+
out.step(`${summary.name} \xB7 plan v${summary.version} \xB7 ${summary.digest}`);
|
|
17794
|
+
for (const problem of problems)
|
|
17795
|
+
out.fail(problem);
|
|
17796
|
+
if (summary.ready) {
|
|
17797
|
+
out.ok("TypeScript definition and canonical execution plan are current.");
|
|
17798
|
+
if (operation === "sync")
|
|
17799
|
+
out.step("Commit both files. ForgeZero executes only the reviewed plan.");
|
|
17800
|
+
}
|
|
17801
|
+
}
|
|
17802
|
+
return summary.ready ? 0 : 1;
|
|
17803
|
+
}
|
|
16674
17804
|
const inspected = inspectDeployFile(options.projectRoot, { channel: options.deployChannel });
|
|
16675
17805
|
if (options.json)
|
|
16676
17806
|
out.line(JSON.stringify(inspected.summary, null, 2));
|
|
@@ -16724,7 +17854,7 @@ async function cmdDeploy(options, args) {
|
|
|
16724
17854
|
branch: options.branch,
|
|
16725
17855
|
cloneUrl: required(options.cloneUrl, "--clone-url"),
|
|
16726
17856
|
sourceAuth,
|
|
16727
|
-
...options.knownHostsFile ? { knownHosts:
|
|
17857
|
+
...options.knownHostsFile ? { knownHosts: readFileSync14(options.knownHostsFile, "utf8") } : {},
|
|
16728
17858
|
projectKey
|
|
16729
17859
|
} });
|
|
16730
17860
|
const pipelineKey2 = String(created.pipelineKey);
|
|
@@ -16784,7 +17914,7 @@ async function cmdDeploy(options, args) {
|
|
|
16784
17914
|
out.line(JSON.stringify(await deploymentRequest(options, "/rotate", { method: "POST", body: { pipelineKey } }), null, 2));
|
|
16785
17915
|
return 0;
|
|
16786
17916
|
}
|
|
16787
|
-
out.fail("Usage: fz deploy init|check|sync|catalog|list|connect|attach|runs|release|enable|disable|target-enable|target-disable|rotate");
|
|
17917
|
+
out.fail("Usage: fz deploy init|compile|check|sync|catalog|list|connect|attach|runs|release|enable|disable|target-enable|target-disable|rotate");
|
|
16788
17918
|
return 2;
|
|
16789
17919
|
} catch (cause) {
|
|
16790
17920
|
out.fail(cause instanceof Error ? cause.message : String(cause));
|
|
@@ -16847,8 +17977,9 @@ function usage() {
|
|
|
16847
17977
|
fz project init Create vendor-neutral, Git-persisted AI context
|
|
16848
17978
|
fz project sync Regenerate Claude/Codex/Gemini/Copilot/Cursor adapters
|
|
16849
17979
|
fz project check Fail when truth sources or generated adapters drift
|
|
16850
|
-
fz deploy init Create
|
|
16851
|
-
fz deploy
|
|
17980
|
+
fz deploy init Create forgezero.deploy.ts and its fail-safe canonical plan
|
|
17981
|
+
fz deploy compile Compile TypeScript into .fz/deploy.plan.json
|
|
17982
|
+
fz deploy check Validate source, plan, actions, providers and topology
|
|
16852
17983
|
fz deploy sync Prove readiness and print the Git synchronization rule
|
|
16853
17984
|
fz deploy catalog List selectable tested OS/software coordinates
|
|
16854
17985
|
fz deploy list List pipelines for .fz/config.json's project
|