@forgezero/agent 0.1.30 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fz-agent.js CHANGED
@@ -4934,7 +4934,7 @@ function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8")
4934
4934
  architecture
4935
4935
  };
4936
4936
  }
4937
- function validateSoftwareRequirements(value, options = {}) {
4937
+ function validateSoftwareRequirements(value, _options = {}) {
4938
4938
  if (!Array.isArray(value) || value.length > 32)
4939
4939
  throw new Error("software requirements must be an array of at most 32 entries");
4940
4940
  const seen = new Set;
@@ -4953,8 +4953,8 @@ function validateSoftwareRequirements(value, options = {}) {
4953
4953
  throw new Error(`duplicate software requirement: ${requirement.id}`);
4954
4954
  seen.add(requirement.id);
4955
4955
  const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
4956
- if (!catalog || catalog.status === "retired" || options.channel !== "development" && catalog.status !== "active") {
4957
- throw new Error(`software requirement is not available for ${options.channel ?? "production"}: ${requirement.id}@${requirement.version}`);
4956
+ if (!catalog || catalog.status !== "active") {
4957
+ throw new Error(`software requirement is not active: ${requirement.id}@${requirement.version}`);
4958
4958
  }
4959
4959
  return requirement;
4960
4960
  });
@@ -4989,6 +4989,7 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
4989
4989
 
4990
4990
  // src/definition.ts
4991
4991
  var PIPELINE_VERSION = 2;
4992
+ var DEPLOY_SCHEMA_URL = "https://www.forgezero.net/schemas/deploy-v2.json";
4992
4993
 
4993
4994
  class DefinitionError extends Error {
4994
4995
  constructor(message) {
@@ -5026,15 +5027,18 @@ var RESERVED_STEP_ENV = new Set([
5026
5027
  "GIT_SSH",
5027
5028
  "GIT_SSH_COMMAND"
5028
5029
  ]);
5029
- function parseDeployDefinition(value) {
5030
+ function parseDeployDefinition(value, options = {}) {
5030
5031
  const root = record(value, "pipeline");
5031
5032
  exactKeys(root, ["$schema", "version", "name", "requireAttestation", "profiles", "steps"], "pipeline");
5032
- if (root.$schema !== undefined && (typeof root.$schema !== "string" || !root.$schema.startsWith("https://"))) {
5033
- throw new DefinitionError("pipeline.$schema must be an HTTPS URL.");
5033
+ if (root.$schema !== undefined && root.$schema !== DEPLOY_SCHEMA_URL) {
5034
+ throw new DefinitionError(`pipeline.$schema must be ${DEPLOY_SCHEMA_URL}.`);
5034
5035
  }
5035
5036
  if (root.version !== PIPELINE_VERSION) {
5036
5037
  throw new DefinitionError(`pipeline.version must be ${PIPELINE_VERSION}.`);
5037
5038
  }
5039
+ if (root.requireAttestation !== undefined && typeof root.requireAttestation !== "boolean") {
5040
+ throw new DefinitionError("pipeline.requireAttestation must be a boolean.");
5041
+ }
5038
5042
  const rawProfiles = record(root.profiles, "pipeline.profiles");
5039
5043
  const profileEntries = Object.entries(rawProfiles);
5040
5044
  if (profileEntries.length === 0 || profileEntries.length > 32) {
@@ -5044,15 +5048,15 @@ function parseDeployDefinition(value) {
5044
5048
  throw new DefinitionError("pipeline.steps must contain at least one step.");
5045
5049
  }
5046
5050
  const profiles = {};
5047
- for (const [name, raw] of profileEntries) {
5048
- if (!NAME.test(name))
5049
- throw new DefinitionError(`pipeline profile name is invalid: ${name}.`);
5050
- const profile = record(raw, `profiles.${name}`);
5051
- exactKeys(profile, ["software"], `profiles.${name}`);
5051
+ for (const [name2, raw] of profileEntries) {
5052
+ if (!NAME.test(name2))
5053
+ throw new DefinitionError(`pipeline profile name is invalid: ${name2}.`);
5054
+ const profile = record(raw, `profiles.${name2}`);
5055
+ exactKeys(profile, ["software"], `profiles.${name2}`);
5052
5056
  if (!Array.isArray(profile.software)) {
5053
- throw new DefinitionError(`profiles.${name}.software must be an array.`);
5057
+ throw new DefinitionError(`profiles.${name2}.software must be an array.`);
5054
5058
  }
5055
- profiles[name] = { software: validateSoftwareRequirements(profile.software) };
5059
+ profiles[name2] = { software: validateSoftwareRequirements(profile.software, options) };
5056
5060
  }
5057
5061
  const phases = new Set(["build", "release", "migrate", "health"]);
5058
5062
  const steps = root.steps.map((raw, index) => {
@@ -5064,9 +5068,12 @@ function parseDeployDefinition(value) {
5064
5068
  if (step.scope !== "target" && step.scope !== "release") {
5065
5069
  throw new DefinitionError(`steps[${index}].scope must be target or release.`);
5066
5070
  }
5071
+ if (step.always !== undefined && typeof step.always !== "boolean") {
5072
+ throw new DefinitionError(`steps[${index}].always must be a boolean.`);
5073
+ }
5067
5074
  let selectedProfiles;
5068
5075
  if (step.profiles !== undefined) {
5069
- if (!Array.isArray(step.profiles) || step.profiles.length === 0 || step.profiles.some((name) => typeof name !== "string" || !Object.hasOwn(profiles, name))) {
5076
+ if (!Array.isArray(step.profiles) || step.profiles.length === 0 || step.profiles.some((name2) => typeof name2 !== "string" || !Object.hasOwn(profiles, name2))) {
5070
5077
  throw new DefinitionError(`steps[${index}].profiles must name existing profiles.`);
5071
5078
  }
5072
5079
  selectedProfiles = [...step.profiles];
@@ -5074,13 +5081,13 @@ function parseDeployDefinition(value) {
5074
5081
  throw new DefinitionError(`steps[${index}].profiles must not contain duplicates.`);
5075
5082
  }
5076
5083
  }
5077
- if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((name) => typeof name !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name)))) {
5084
+ if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((name2) => typeof name2 !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name2)))) {
5078
5085
  throw new DefinitionError(`steps[${index}].secrets must contain names only.`);
5079
5086
  }
5080
5087
  if (Array.isArray(step.secrets) && new Set(step.secrets).size !== step.secrets.length) {
5081
5088
  throw new DefinitionError(`steps[${index}].secrets must not contain duplicates.`);
5082
5089
  }
5083
- if (Array.isArray(step.secrets) && step.secrets.some((name) => RESERVED_STEP_ENV.has(String(name)))) {
5090
+ if (Array.isArray(step.secrets) && step.secrets.some((name2) => RESERVED_STEP_ENV.has(String(name2)))) {
5084
5091
  throw new DefinitionError(`steps[${index}].secrets may not replace process-control environment variables.`);
5085
5092
  }
5086
5093
  const timeoutMs = step.timeoutMs === undefined ? undefined : Number(step.timeoutMs);
@@ -5091,11 +5098,11 @@ function parseDeployDefinition(value) {
5091
5098
  if (step.when !== undefined) {
5092
5099
  const conditions = record(step.when, `steps[${index}].when`);
5093
5100
  when = {};
5094
- for (const [name, expected] of Object.entries(conditions)) {
5095
- if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || typeof expected !== "string" || expected.length === 0) {
5101
+ for (const [name2, expected] of Object.entries(conditions)) {
5102
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(name2) || typeof expected !== "string" || expected.length === 0) {
5096
5103
  throw new DefinitionError(`steps[${index}].when must map environment names to non-empty strings.`);
5097
5104
  }
5098
- when[name] = expected;
5105
+ when[name2] = expected;
5099
5106
  }
5100
5107
  if (Object.keys(when).length === 0)
5101
5108
  throw new DefinitionError(`steps[${index}].when must not be empty.`);
@@ -5115,15 +5122,32 @@ function parseDeployDefinition(value) {
5115
5122
  if (new Set(steps.map((step) => step.name)).size !== steps.length) {
5116
5123
  throw new DefinitionError("pipeline.steps must have unique names.");
5117
5124
  }
5125
+ const name = text(root.name, "pipeline.name");
5126
+ if (name.length > 120)
5127
+ throw new DefinitionError("pipeline.name must be at most 120 characters.");
5118
5128
  return {
5119
5129
  version: PIPELINE_VERSION,
5120
- name: text(root.name, "pipeline.name"),
5130
+ name,
5121
5131
  requireAttestation: root.requireAttestation === true,
5122
5132
  profiles,
5123
5133
  steps
5124
5134
  };
5125
5135
  }
5126
5136
 
5137
+ // src/deploy-file.ts
5138
+ import { createHash } from "crypto";
5139
+ var stable = (value) => {
5140
+ if (Array.isArray(value))
5141
+ return `[${value.map(stable).join(",")}]`;
5142
+ if (value && typeof value === "object") {
5143
+ return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stable(entry)}`).join(",")}}`;
5144
+ }
5145
+ return JSON.stringify(value);
5146
+ };
5147
+ function deployDefinitionDigest(definition) {
5148
+ return `sha256:${createHash("sha256").update(stable(definition)).digest("hex")}`;
5149
+ }
5150
+
5127
5151
  // src/pipeline.ts
5128
5152
  class PipelineError extends Error {
5129
5153
  code;
@@ -5365,6 +5389,7 @@ function createDeploymentManager(options) {
5365
5389
  throw new DeploymentError("SOURCE_FAILED", `Git checked out ${head}, not requested ${request.revision}.`);
5366
5390
  }
5367
5391
  const definition = parseDeployDefinition(readDefinition(join(release, ".fz", "deploy.json")));
5392
+ const definitionDigest = deployDefinitionDigest(definition);
5368
5393
  const selectedProfile = definition.profiles[options.profile];
5369
5394
  if (!selectedProfile)
5370
5395
  throw new DeploymentError("PIPELINE_FAILED", `pipeline profile does not exist: ${options.profile}.`);
@@ -5414,6 +5439,9 @@ function createDeploymentManager(options) {
5414
5439
  repository: options.repository,
5415
5440
  branch: options.branch,
5416
5441
  revision: head,
5442
+ definitionDigest,
5443
+ definitionVersion: definition.version,
5444
+ profile: options.profile,
5417
5445
  release,
5418
5446
  ok: true,
5419
5447
  phases
@@ -5677,7 +5705,10 @@ async function pullDeploymentOnce(options) {
5677
5705
  claimToken: claim.claimToken,
5678
5706
  ok: true,
5679
5707
  detail: `Deployed ${result.revision}.`,
5680
- release: result.release
5708
+ release: result.release,
5709
+ definitionDigest: result.definitionDigest,
5710
+ definitionVersion: result.definitionVersion,
5711
+ profile: result.profile
5681
5712
  });
5682
5713
  return { status: "deployed", claim, result };
5683
5714
  }
@@ -6096,7 +6127,7 @@ import { chmodSync as chmodSync5, existsSync as existsSync6, unlinkSync as unlin
6096
6127
  import { connect as connect2, createServer as createServer3 } from "net";
6097
6128
 
6098
6129
  // src/metal-provision.ts
6099
- import { createHash } from "crypto";
6130
+ import { createHash as createHash2 } from "crypto";
6100
6131
  import {
6101
6132
  existsSync as existsSync5,
6102
6133
  mkdirSync as mkdirSync3,
@@ -6249,8 +6280,8 @@ function membersOfLinuxList(value, label) {
6249
6280
  throw new MetalProvisionError(`${label} list overlaps itself`);
6250
6281
  return members;
6251
6282
  }
6252
- var guestNameFor = (computeKey) => `fzg-${createHash("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
6253
- var tapNameFor = (computeKey) => `fzt${createHash("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
6283
+ var guestNameFor = (computeKey) => `fzg-${createHash2("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
6284
+ var tapNameFor = (computeKey) => `fzt${createHash2("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
6254
6285
  var macForAddress = (address) => {
6255
6286
  const octets = address.split(".").map(Number);
6256
6287
  if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
@@ -6332,7 +6363,7 @@ function allocateAddress(profile, computeKey, rows) {
6332
6363
  return existing.address;
6333
6364
  const used = new Set(rows.map((row) => row.address));
6334
6365
  const width = profile.addressEnd - profile.addressStart + 1;
6335
- const start = createHash("sha256").update(computeKey).digest().readUInt16BE(0) % width;
6366
+ const start = createHash2("sha256").update(computeKey).digest().readUInt16BE(0) % width;
6336
6367
  for (let offset = 0;offset < width; offset += 1) {
6337
6368
  const last = profile.addressStart + (start + offset) % width;
6338
6369
  const address = `${profile.subnetPrefix}.${last}`;
@@ -7547,7 +7578,7 @@ function materializeWarpMdm(options) {
7547
7578
  }
7548
7579
 
7549
7580
  // src/agent-update.ts
7550
- import { createHash as createHash2, timingSafeEqual, randomUUID as randomUUID2 } from "crypto";
7581
+ import { createHash as createHash3, timingSafeEqual, randomUUID as randomUUID2 } from "crypto";
7551
7582
  import {
7552
7583
  chmodSync as chmodSync9,
7553
7584
  existsSync as existsSync10,
@@ -7665,7 +7696,7 @@ async function stageAgentRelease(releaseInput, options) {
7665
7696
  throw new Error("agent update tarball is empty or exceeds the size limit");
7666
7697
  }
7667
7698
  const expected = Buffer.from(release.integrity.slice("sha512-".length), "base64");
7668
- const actual = createHash2("sha512").update(bytes).digest();
7699
+ const actual = createHash3("sha512").update(bytes).digest();
7669
7700
  if (!timingSafeEqual(actual, expected))
7670
7701
  throw new Error("agent update integrity mismatch");
7671
7702
  writeFileSync6(archive, bytes, { mode: 384, flag: "wx" });
@@ -7907,7 +7938,7 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
7907
7938
  import { readFileSync as readFileSync7 } from "fs";
7908
7939
 
7909
7940
  // src/version.ts
7910
- var VERSION2 = "0.1.30";
7941
+ var VERSION2 = "0.1.31";
7911
7942
 
7912
7943
  // src/agent-heartbeat.ts
7913
7944
  var unquote = (value) => value.replace(/^['"]|['"]$/g, "");