@forgezero/agent 0.1.101 → 0.1.103

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.js CHANGED
@@ -4841,7 +4841,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4841
4841
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4842
4842
 
4843
4843
  // src/version.ts
4844
- var VERSION2 = "0.1.101";
4844
+ var VERSION2 = "0.1.103";
4845
4845
 
4846
4846
  // src/software.ts
4847
4847
  var PINNED_BUN_VERSION = "1.3.14";
@@ -12058,7 +12058,7 @@ function removeSession(api, realm, path = defaultSessionPath()) {
12058
12058
  }
12059
12059
 
12060
12060
  // src/bootstrap.ts
12061
- import { createHash as createHash4, createHmac as createHmac2, randomBytes as randomBytes7 } from "crypto";
12061
+ import { createHash as createHash4, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes7 } from "crypto";
12062
12062
  import {
12063
12063
  chmodSync as chmodSync4,
12064
12064
  existsSync as existsSync9,
@@ -12239,10 +12239,16 @@ var httpsOrigin = (name, raw) => {
12239
12239
  };
12240
12240
  var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
12241
12241
  function validatePlatformInitialInventory(value) {
12242
- if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes", "attestation", "deployment"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
12242
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "metalIdentity", "region", "computes", "attestation", "deployment"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
12243
12243
  throw new Error("initial platform inventory coordinates are invalid");
12244
12244
  }
12245
12245
  safeAtom("initialInventory.region.label", value.region.label);
12246
+ if (value.metalIdentity !== undefined) {
12247
+ const identity = value.metalIdentity;
12248
+ if (!identity || typeof identity !== "object" || Array.isArray(identity) || Object.keys(identity).some((key) => !["nodeKey", "publicKeys"].includes(key)) || !/^[A-Za-z0-9_-]{43}$/.test(identity.nodeKey) || !identity.publicKeys || identity.publicKeys.ed25519 !== identity.nodeKey || !/^[A-Za-z0-9_-]{1,8192}$/.test(identity.publicKeys.mlDsa) || Object.keys(identity.publicKeys).some((key) => !["ed25519", "mlDsa"].includes(key))) {
12249
+ throw new Error("initial platform Metal identity is invalid");
12250
+ }
12251
+ }
12246
12252
  if (value.region.city !== undefined)
12247
12253
  safeAtom("initialInventory.region.city", value.region.city);
12248
12254
  if (value.deployment !== undefined && (!value.deployment || typeof value.deployment !== "object" || Array.isArray(value.deployment) || Object.keys(value.deployment).some((key) => !["source", "branch", "revision", "bundleSha256"].includes(key)) || value.deployment.source !== "bootstrap-bundle" || !["dev", "main"].includes(value.deployment.branch) || !/^[a-f0-9]{40}$/.test(value.deployment.revision) || !/^[a-f0-9]{64}$/.test(value.deployment.bundleSha256)))
@@ -12277,6 +12283,7 @@ function validatePlatformInitialInventory(value) {
12277
12283
  }
12278
12284
  return {
12279
12285
  metalHostname: value.metalHostname,
12286
+ ...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
12280
12287
  region: { ...value.region },
12281
12288
  computes: value.computes.map((compute, index) => ({
12282
12289
  ...computes[index],
@@ -12330,6 +12337,11 @@ function validatePlatformSharedEnvironment(input) {
12330
12337
  } else if (input.email !== undefined) {
12331
12338
  throw new Error("Bootstrap email provider must be smtp or jetemail.");
12332
12339
  }
12340
+ if (input.githubApp) {
12341
+ if (!/^(?:Iv1\.[A-Fa-f0-9]{16}|Ov23li[A-Za-z0-9]{14,})$/.test(input.githubApp.clientId) || !/^[1-9][0-9]{0,19}$/.test(input.githubApp.appId) || !/^[a-z0-9][a-z0-9-]{0,99}$/.test(input.githubApp.slug)) {
12342
+ throw new Error("GitHub App client id, app id or slug is malformed.");
12343
+ }
12344
+ }
12333
12345
  boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
12334
12346
  if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
12335
12347
  throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
@@ -12460,6 +12472,9 @@ function renderPlatformSharedEnvironment(input) {
12460
12472
  FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
12461
12473
  FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
12462
12474
  FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
12475
+ FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
12476
+ FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
12477
+ FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
12463
12478
  FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
12464
12479
  };
12465
12480
  return `# Generated by fz bootstrap platform. Non-secret coordinates only.
@@ -12471,6 +12486,9 @@ function platformApiCredentialSpecs(options) {
12471
12486
  const optional = [
12472
12487
  ["fz_smtp.password", options.emailProvider === "smtp"],
12473
12488
  ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
12489
+ ["fz_github.clientSecret", options.githubApp],
12490
+ ["fz_github.privateKey", options.githubApp],
12491
+ ["fz_github.webhookSecret", options.githubApp],
12474
12492
  ["CF_API_TOKEN", options.cloudflareKv],
12475
12493
  ["CF_TUNNEL_TOKEN", options.cloudflareKv],
12476
12494
  ["REALTIME_PUBLISH_SECRET", options.realtime],
@@ -14102,7 +14120,17 @@ function validatePlatformBootstrapSecrets(config, input) {
14102
14120
  if (!input || typeof input !== "object" || Array.isArray(input))
14103
14121
  throw new Error("bootstrap credential input must be an object");
14104
14122
  const source = input;
14105
- const allowed = ["clusterBootstrapCode", "emailSecret", "enrolmentToken", "backupS3Secret", "cloudflareTunnelToken", "cloudflareApiToken"];
14123
+ const allowed = [
14124
+ "clusterBootstrapCode",
14125
+ "emailSecret",
14126
+ "enrolmentToken",
14127
+ "backupS3Secret",
14128
+ "cloudflareTunnelToken",
14129
+ "cloudflareApiToken",
14130
+ "githubClientSecret",
14131
+ "githubPrivateKeyBase64",
14132
+ "githubWebhookSecret"
14133
+ ];
14106
14134
  const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
14107
14135
  if (unknown.length)
14108
14136
  throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
@@ -14112,6 +14140,9 @@ function validatePlatformBootstrapSecrets(config, input) {
14112
14140
  const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
14113
14141
  const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
14114
14142
  const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
14143
+ const githubClientSecret = typeof source.githubClientSecret === "string" ? source.githubClientSecret.trim() : undefined;
14144
+ const githubPrivateKeyBase64 = typeof source.githubPrivateKeyBase64 === "string" ? source.githubPrivateKeyBase64.trim() : undefined;
14145
+ const githubWebhookSecret = typeof source.githubWebhookSecret === "string" ? source.githubWebhookSecret.trim() : undefined;
14115
14146
  if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
14116
14147
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
14117
14148
  if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
@@ -14131,12 +14162,29 @@ function validatePlatformBootstrapSecrets(config, input) {
14131
14162
  }
14132
14163
  if (cloudflareTunnelToken)
14133
14164
  validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
14165
+ const githubConfigured = Boolean(config.runtime.environment.githubApp);
14166
+ if (githubConfigured !== Boolean(githubClientSecret && githubPrivateKeyBase64 && githubWebhookSecret)) {
14167
+ throw new Error("GitHub App coordinates require client secret, private key and webhook secret together");
14168
+ }
14169
+ if (githubConfigured) {
14170
+ if (githubClientSecret.length < 20 || githubClientSecret.length > 512 || /[\r\n\0]/.test(githubClientSecret) || githubWebhookSecret.length < 32 || githubWebhookSecret.length > 512 || /[\r\n\0]/.test(githubWebhookSecret)) {
14171
+ throw new Error("GitHub App client or webhook secret is malformed");
14172
+ }
14173
+ try {
14174
+ const pem = Buffer.from(githubPrivateKeyBase64, "base64").toString("utf8");
14175
+ if (createPrivateKey(pem).asymmetricKeyType !== "rsa")
14176
+ throw new Error("not RSA");
14177
+ } catch {
14178
+ throw new Error("GitHub App private key must be a base64-encoded RSA private key");
14179
+ }
14180
+ }
14134
14181
  return {
14135
14182
  clusterBootstrapCode,
14136
14183
  emailSecret,
14137
14184
  ...enrolmentToken ? { enrolmentToken } : {},
14138
14185
  ...backupS3Secret ? { backupS3Secret } : {},
14139
- ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
14186
+ ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {},
14187
+ ...githubConfigured ? { githubClientSecret, githubPrivateKeyBase64, githubWebhookSecret } : {}
14140
14188
  };
14141
14189
  }
14142
14190
  var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
@@ -14688,6 +14736,17 @@ function approvedPlatformRepairIdentityDigests(config) {
14688
14736
  delete beforeAttestation.runtime.environment.initialInventory.attestation;
14689
14737
  prior.push(beforeAttestation);
14690
14738
  }
14739
+ const metalIdentity = config.runtime.environment.initialInventory?.metalIdentity;
14740
+ if (metalIdentity && config.enrolment.source === "genesis-derived") {
14741
+ const beforeMetalIdentity = structuredClone(config);
14742
+ delete beforeMetalIdentity.runtime.environment.initialInventory.metalIdentity;
14743
+ prior.push(beforeMetalIdentity);
14744
+ }
14745
+ if (config.database.role === "joiner" && config.enrolment.source === "genesis-derived" && config.runtime.environment.custodianEmail) {
14746
+ const beforeReplicaCustodianEmail = structuredClone(config);
14747
+ delete beforeReplicaCustodianEmail.runtime.environment.custodianEmail;
14748
+ prior.push(beforeReplicaCustodianEmail);
14749
+ }
14691
14750
  const telemetryMigration = config.environment === "development" && config.telemetryEndpoint === LOCAL_DEBUG_OTEL_EXPORT && config.runtime.environment.agentOtlpEndpoint === "http://127.0.0.1:4318";
14692
14751
  if (telemetryMigration) {
14693
14752
  const beforeTelemetry = structuredClone(config);
@@ -15040,7 +15099,10 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15040
15099
  enrolmentToken: checked4.enrolmentToken,
15041
15100
  backup: checked4.backupS3Secret,
15042
15101
  cloudflareTunnelToken: checked4.cloudflareTunnelToken,
15043
- cloudflareApiToken: checked4.cloudflareApiToken
15102
+ cloudflareApiToken: checked4.cloudflareApiToken,
15103
+ githubClientSecret: checked4.githubClientSecret,
15104
+ githubPrivateKey: checked4.githubPrivateKeyBase64 ? Buffer.from(checked4.githubPrivateKeyBase64, "base64").toString("utf8") : undefined,
15105
+ githubWebhookSecret: checked4.githubWebhookSecret
15044
15106
  };
15045
15107
  })() : undefined;
15046
15108
  const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
@@ -15138,6 +15200,9 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15138
15200
  const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
15139
15201
  for (const [name, source] of Object.entries({
15140
15202
  ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
15203
+ "fz_github.clientSecret": platformPrivate.githubClientSecret,
15204
+ "fz_github.privateKey": platformPrivate.githubPrivateKey,
15205
+ "fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
15141
15206
  "backup.s3.secretAccessKey": platformPrivate.backup
15142
15207
  })) {
15143
15208
  if (source) {
@@ -15154,6 +15219,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15154
15219
  const credentials = platformApiCredentialSpecs({
15155
15220
  emailProvider: runtime.environment.email?.provider,
15156
15221
  cloudflareKv: cloudflareConfigured,
15222
+ githubApp: Boolean(runtime.environment.githubApp),
15157
15223
  realtime: Boolean(runtime.environment.realtime)
15158
15224
  });
15159
15225
  const units = renderPlatformApiUnits({
@@ -16151,7 +16217,7 @@ var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dc
16151
16217
 
16152
16218
  class MetalBootstrapError extends Error {
16153
16219
  }
16154
- function normalizeMetalPublicIdentity(value) {
16220
+ function parseMetalPublicIdentity(value) {
16155
16221
  let parsed;
16156
16222
  try {
16157
16223
  parsed = JSON.parse(value);
@@ -16166,7 +16232,13 @@ function normalizeMetalPublicIdentity(value) {
16166
16232
  if (Object.keys(identity).some((key) => !["nodeKey", "publicKeys"].includes(key)) || typeof identity.nodeKey !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(identity.nodeKey) || !keys || Object.keys(keys).some((key) => !["ed25519", "mlDsa"].includes(key)) || keys.ed25519 !== identity.nodeKey || typeof keys.mlDsa !== "string" || !/^[A-Za-z0-9_-]{1,8192}$/.test(keys.mlDsa)) {
16167
16233
  throw new MetalBootstrapError("metal public identity proof was invalid");
16168
16234
  }
16169
- return JSON.stringify(parsed);
16235
+ return {
16236
+ nodeKey: identity.nodeKey,
16237
+ publicKeys: { ed25519: keys.ed25519, mlDsa: keys.mlDsa }
16238
+ };
16239
+ }
16240
+ function normalizeMetalPublicIdentity(value) {
16241
+ return JSON.stringify(parseMetalPublicIdentity(value));
16170
16242
  }
16171
16243
  var defaultExec2 = async (argv2, stdin) => {
16172
16244
  const child = Bun.spawn([...argv2], {
@@ -17056,7 +17128,7 @@ function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudfla
17056
17128
  return writeOwnerJson2(path, request, "operator fleet request");
17057
17129
  }
17058
17130
  var validateMetalRequestValue = (value, options = {}) => {
17059
- const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
17131
+ const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "metalIdentityEvidenceFile", "genesis"], "operator metal request");
17060
17132
  if (root.kind !== "metal-remote")
17061
17133
  throw new Error("operator metal bootstrap kind must be metal-remote");
17062
17134
  const targetValue = exactKeys5(root.target, ["address", "port", "user", "hostKey", "hostKeySha256", "identityPublicKeyFile", "agentSocket"], "operator metal target");
@@ -17075,6 +17147,12 @@ var validateMetalRequestValue = (value, options = {}) => {
17075
17147
  if (typeof root.metalConfigFile !== "string")
17076
17148
  throw new Error("metalConfigFile must be an absolute path");
17077
17149
  canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
17150
+ if (root.metalIdentityEvidenceFile !== undefined && typeof root.metalIdentityEvidenceFile !== "string") {
17151
+ throw new Error("metalIdentityEvidenceFile must be an absolute path");
17152
+ }
17153
+ if (typeof root.metalIdentityEvidenceFile === "string") {
17154
+ canonicalOutputPath(root.metalIdentityEvidenceFile, "metalIdentityEvidenceFile");
17155
+ }
17078
17156
  if (options.validateMetalConfig !== false)
17079
17157
  readMetalBootstrapConfig(root.metalConfigFile);
17080
17158
  const genesis = exactKeys5(root.genesis, ["nodes", "rehearsalNode", "sshPublicKeyFiles"], "operator metal genesis");
@@ -17123,6 +17201,7 @@ var validateMetalRequestValue = (value, options = {}) => {
17123
17201
  return {
17124
17202
  kind: "metal-remote",
17125
17203
  metalConfigFile: root.metalConfigFile,
17204
+ ...typeof root.metalIdentityEvidenceFile === "string" ? { metalIdentityEvidenceFile: root.metalIdentityEvidenceFile } : {},
17126
17205
  target: {
17127
17206
  ...target,
17128
17207
  identityPublicKeyFile: targetValue.identityPublicKeyFile,
@@ -17143,6 +17222,25 @@ function writeOperatorMetalBootstrapRequest(path, request) {
17143
17222
  validateMetalRequestValue(request);
17144
17223
  return writeOwnerJson2(path, request, "operator metal request");
17145
17224
  }
17225
+ function readOperatorMetalIdentityEvidence(path, request) {
17226
+ const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal identity evidence")));
17227
+ const root = exactKeys5(value, ["format", "kind", "metalHostname", "metalHostKeySha256", "nodeKey", "publicKeys"], "operator metal identity evidence");
17228
+ if (root.format !== 1 || root.kind !== "forgezero-operator-metal-identity" || typeof root.metalHostname !== "string" || typeof root.metalHostKeySha256 !== "string" || typeof root.nodeKey !== "string" || !root.publicKeys || typeof root.publicKeys !== "object") {
17229
+ throw new Error("operator metal identity evidence is malformed");
17230
+ }
17231
+ const identity = parseMetalPublicIdentity(JSON.stringify({ nodeKey: root.nodeKey, publicKeys: root.publicKeys }));
17232
+ const config = readMetalBootstrapConfig(request.metalConfigFile, { allowHistoricalRelease: true });
17233
+ if (root.metalHostname !== config.metalHostname || root.metalHostKeySha256 !== request.target.hostKeySha256) {
17234
+ throw new Error("operator metal identity evidence does not match the reviewed Metal target");
17235
+ }
17236
+ return {
17237
+ format: 1,
17238
+ kind: "forgezero-operator-metal-identity",
17239
+ metalHostname: root.metalHostname,
17240
+ metalHostKeySha256: root.metalHostKeySha256,
17241
+ ...identity
17242
+ };
17243
+ }
17146
17244
  var validateGuestHostKeyEvidenceValue = (value, request) => {
17147
17245
  const root = exactKeys5(value, ["format", "kind", "metalHostKeySha256", "nodes"], "operator guest host-key evidence");
17148
17246
  if (root.format !== 1 || root.kind !== "forgezero-operator-guest-host-keys" || typeof root.metalHostKeySha256 !== "string" || !Array.isArray(root.nodes)) {
@@ -17828,6 +17926,25 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
17828
17926
  `${REMOTE_STAGE}/metal-config.json`,
17829
17927
  "--apply"
17830
17928
  ], "remote typed metal bootstrap");
17929
+ if (request.metalIdentityEvidenceFile) {
17930
+ let applied;
17931
+ try {
17932
+ applied = JSON.parse(output);
17933
+ } catch {
17934
+ throw new Error("remote Metal bootstrap did not return identity evidence");
17935
+ }
17936
+ const publicIdentity2 = applied?.publicIdentity;
17937
+ if (typeof publicIdentity2 !== "string")
17938
+ throw new Error("remote Metal bootstrap omitted its public identity");
17939
+ const identity = parseMetalPublicIdentity(publicIdentity2);
17940
+ writeOwnerJson2(request.metalIdentityEvidenceFile, {
17941
+ format: 1,
17942
+ kind: "forgezero-operator-metal-identity",
17943
+ metalHostname: config.metalHostname,
17944
+ metalHostKeySha256: request.target.hostKeySha256,
17945
+ ...identity
17946
+ }, "operator metal identity evidence");
17947
+ }
17831
17948
  return { plan, output };
17832
17949
  } finally {
17833
17950
  if (knownHosts) {
@@ -17881,7 +17998,7 @@ async function applyAttendedPlatformBootstrap(fleetRequest, cloudflareRequest, a
17881
17998
  }
17882
17999
 
17883
18000
  // src/platform-genesis-config.ts
17884
- function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment, attestation) {
18001
+ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment, attestation, metalIdentity) {
17885
18002
  const fleet = validatePlatformGenesisGuests(guests);
17886
18003
  if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(metalHostname)) {
17887
18004
  throw new Error("platform genesis requires the reviewed metal hostname");
@@ -17897,6 +18014,7 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
17897
18014
  const coordinators = fleet.map((guest) => `http://${guest.address}:8529`);
17898
18015
  const initialInventory = {
17899
18016
  metalHostname,
18017
+ ...metalIdentity ? { metalIdentity: structuredClone(metalIdentity) } : {},
17900
18018
  region: {
17901
18019
  key: template.runtime.environment.nodeRegion,
17902
18020
  label: region.label,
@@ -17946,7 +18064,7 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
17946
18064
  seedSyncPeers: fleet.filter((_, peer) => peer !== index).flatMap((peer) => [config.runtime.bluePort, config.runtime.greenPort].map((port) => `ws://${peer.address}:${port}/_internal/forgezero/seed-mesh/v2`)),
17947
18065
  seedSyncMembers: nodes.map(({ nodeHostname }) => nodeHostname),
17948
18066
  initialInventory,
17949
- custodianEmail: index === 0 ? template.runtime.environment.custodianEmail : undefined
18067
+ custodianEmail: template.runtime.environment.custodianEmail
17950
18068
  };
17951
18069
  return validateBootstrapConfig(config);
17952
18070
  });
@@ -18060,6 +18178,7 @@ function forgeZeroLaunchTemplate(profile, guests, bundle, owner) {
18060
18178
  otlpTraceSampleRatio: 0.1,
18061
18179
  custodianEmail: owner.custodianEmail,
18062
18180
  email: owner.email,
18181
+ ...owner.githubApp ? { githubApp: owner.githubApp } : {},
18063
18182
  deployProfile: profile.environment
18064
18183
  },
18065
18184
  serviceUser: "forgezero-api",
@@ -19262,13 +19381,20 @@ function readPlatformLaunchEnvironment(path) {
19262
19381
  if (smtpPort !== undefined && (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535)) {
19263
19382
  throw new Error("FZ_SMTP_PORT must be an integer from 1 through 65535");
19264
19383
  }
19384
+ const githubEnabled = (values.get("FZ_GITHUB_ENABLED") ?? "false").trim().toLowerCase() === "true";
19385
+ const githubApp = githubEnabled ? {
19386
+ clientId: requiredValue("FZ_GITHUB_CLIENT_ID"),
19387
+ appId: requiredValue("FZ_GITHUB_APP_ID"),
19388
+ slug: requiredValue("FZ_GITHUB_APP_SLUG")
19389
+ } : undefined;
19265
19390
  const owner = provider === "jetemail" ? {
19266
19391
  custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
19267
19392
  email: {
19268
19393
  provider,
19269
19394
  from: requiredValue("FZ_EMAIL_FROM"),
19270
19395
  eu: (values.get("FZ_JETEMAIL_EU") ?? "false").trim().toLowerCase() === "true"
19271
- }
19396
+ },
19397
+ ...githubApp ? { githubApp } : {}
19272
19398
  } : {
19273
19399
  custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
19274
19400
  email: {
@@ -19277,13 +19403,19 @@ function readPlatformLaunchEnvironment(path) {
19277
19403
  host: requiredValue("FZ_SMTP_HOST"),
19278
19404
  port: smtpPort,
19279
19405
  user: requiredValue("FZ_SMTP_USER")
19280
- }
19406
+ },
19407
+ ...githubApp ? { githubApp } : {}
19281
19408
  };
19282
19409
  return {
19283
19410
  owner,
19284
19411
  emailSecret: requiredValue(provider === "jetemail" ? "FZ_JETEMAIL_API_KEY" : "FZ_SMTP_PASSWORD"),
19285
19412
  cloudflareTunnelToken: requiredValue("CF_TUNNEL_TOKEN"),
19286
- cloudflareApiToken: requiredValue("CF_API_TOKEN")
19413
+ cloudflareApiToken: requiredValue("CF_API_TOKEN"),
19414
+ ...githubEnabled ? {
19415
+ githubClientSecret: requiredValue("FZ_GITHUB_CLIENT_SECRET"),
19416
+ githubPrivateKeyBase64: requiredValue("FZ_GITHUB_PRIVATE_KEY_BASE64"),
19417
+ githubWebhookSecret: requiredValue("FZ_GITHUB_WEBHOOK_SECRET")
19418
+ } : {}
19287
19419
  };
19288
19420
  }
19289
19421
  async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapCode) {
@@ -19295,6 +19427,11 @@ async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapC
19295
19427
  ...config.cloudflareHandoff || config.runtime.environment.cloudflare ? {
19296
19428
  cloudflareTunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
19297
19429
  cloudflareApiToken: await bootstrapSecret("CF_API_TOKEN")
19430
+ } : {},
19431
+ ...config.runtime.environment.githubApp ? {
19432
+ githubClientSecret: await bootstrapSecret("GitHub App client secret"),
19433
+ githubPrivateKeyBase64: await bootstrapSecret("GitHub App RSA private key (base64)"),
19434
+ githubWebhookSecret: await bootstrapSecret("GitHub App webhook secret")
19298
19435
  } : {}
19299
19436
  });
19300
19437
  }
@@ -19476,6 +19613,7 @@ function writeMetalOperatorFiles(directory) {
19476
19613
  const remoteRequest = writeOperatorMetalBootstrapRequest(join13(output, "metal-remote.json"), {
19477
19614
  kind: "metal-remote",
19478
19615
  metalConfigFile: metalConfig,
19616
+ metalIdentityEvidenceFile: join13(output, "metal-identity.json"),
19479
19617
  target: { ...target, ...identity },
19480
19618
  genesis: { ...interactiveGenesisGuests(), sshPublicKeyFiles: [identity.identityPublicKeyFile] }
19481
19619
  });
@@ -19496,6 +19634,10 @@ function writePlatformGenesisFleet(directory, preset) {
19496
19634
  const output = genesisOutputDirectory(directory);
19497
19635
  const metalRequestFile = preset?.metalRequestFile ?? bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
19498
19636
  const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile, { validateMetalConfig: false });
19637
+ if (!metalRequest.metalIdentityEvidenceFile) {
19638
+ throw new Error("the reviewed Metal request predates automatic identity seeding; rerun Metal apply with a generated request");
19639
+ }
19640
+ const metalIdentity = readOperatorMetalIdentityEvidence(metalRequest.metalIdentityEvidenceFile, metalRequest);
19499
19641
  const guestHostKeysFile = preset?.guestHostKeysFile ?? bootstrapAnswer("Owner-only guest host-key evidence file");
19500
19642
  const guestHostKeys = readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
19501
19643
  const fleet = metalRequest.genesis.nodes;
@@ -19540,7 +19682,7 @@ function writePlatformGenesisFleet(directory, preset) {
19540
19682
  branch: preset.profile.branch,
19541
19683
  revision: preset.bundle.manifest.revision,
19542
19684
  bundleSha256: preset.bundle.manifest.sha256
19543
- } : undefined, preset?.profile.attestation).map((config) => {
19685
+ } : undefined, preset?.profile.attestation, { nodeKey: metalIdentity.nodeKey, publicKeys: { ...metalIdentity.publicKeys } }).map((config) => {
19544
19686
  const path = `${output}/${config.computeReference}-platform.json`;
19545
19687
  if (preset)
19546
19688
  writeDerivedLaunchFile(path, (temporary) => writeBootstrapConfig(temporary, config));
@@ -19594,10 +19736,16 @@ function forgeZeroLaunchOwnerInput() {
19594
19736
  throw new Error("Bootstrap email provider must be smtp or jetemail");
19595
19737
  }
19596
19738
  const from = bootstrapAnswer("Bootstrap email From address");
19739
+ const githubApp = bootstrapBoolean("Configure the platform GitHub App now?", "no") ? {
19740
+ clientId: bootstrapAnswer("GitHub App client ID"),
19741
+ appId: bootstrapAnswer("GitHub App ID"),
19742
+ slug: bootstrapAnswer("GitHub App slug")
19743
+ } : undefined;
19597
19744
  if (provider === "jetemail") {
19598
19745
  return {
19599
19746
  custodianEmail,
19600
- email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") }
19747
+ email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") },
19748
+ ...githubApp ? { githubApp } : {}
19601
19749
  };
19602
19750
  }
19603
19751
  return {
@@ -19608,7 +19756,8 @@ function forgeZeroLaunchOwnerInput() {
19608
19756
  host: bootstrapAnswer("SMTP host"),
19609
19757
  port: bootstrapNumber("SMTP port", "587"),
19610
19758
  user: bootstrapAnswer("SMTP user")
19611
- }
19759
+ },
19760
+ ...githubApp ? { githubApp } : {}
19612
19761
  };
19613
19762
  }
19614
19763
  async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, projectRoot, owner) {
@@ -19793,7 +19942,12 @@ async function runAttendedPlatformFleet(fleet, apply, generatedClusterBootstrapC
19793
19942
  clusterBootstrapCode: generatedClusterBootstrapCode,
19794
19943
  emailSecret: environmentInput.emailSecret,
19795
19944
  cloudflareTunnelToken: environmentInput.cloudflareTunnelToken,
19796
- cloudflareApiToken: environmentInput.cloudflareApiToken
19945
+ cloudflareApiToken: environmentInput.cloudflareApiToken,
19946
+ ...environmentInput.githubClientSecret ? {
19947
+ githubClientSecret: environmentInput.githubClientSecret,
19948
+ githubPrivateKeyBase64: environmentInput.githubPrivateKeyBase64,
19949
+ githubWebhookSecret: environmentInput.githubWebhookSecret
19950
+ } : {}
19797
19951
  }) : await promptPlatformBootstrapSecrets(firstConfig, generatedClusterBootstrapCode);
19798
19952
  const cloudflare = await resolveCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, {
19799
19953
  tunnelToken: secrets.cloudflareTunnelToken,
@@ -43,6 +43,14 @@ export interface MetalBootstrapApplyOptions {
43
43
  }
44
44
  export declare class MetalBootstrapError extends Error {
45
45
  }
46
+ export interface MetalPublicIdentity {
47
+ nodeKey: string;
48
+ publicKeys: {
49
+ ed25519: string;
50
+ mlDsa: string;
51
+ };
52
+ }
53
+ export declare function parseMetalPublicIdentity(value: string): MetalPublicIdentity;
46
54
  export declare function normalizeMetalPublicIdentity(value: string): string;
47
55
  export interface MetalBootstrapValidationOptions {
48
56
  /**
@@ -366,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
366
366
  }
367
367
 
368
368
  // src/version.ts
369
- var VERSION = "0.1.101";
369
+ var VERSION = "0.1.103";
370
370
 
371
371
  // src/otel-collector.ts
372
372
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -543,7 +543,7 @@ var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dc
543
543
 
544
544
  class MetalBootstrapError extends Error {
545
545
  }
546
- function normalizeMetalPublicIdentity(value) {
546
+ function parseMetalPublicIdentity(value) {
547
547
  let parsed;
548
548
  try {
549
549
  parsed = JSON.parse(value);
@@ -558,7 +558,13 @@ function normalizeMetalPublicIdentity(value) {
558
558
  if (Object.keys(identity).some((key) => !["nodeKey", "publicKeys"].includes(key)) || typeof identity.nodeKey !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(identity.nodeKey) || !keys || Object.keys(keys).some((key) => !["ed25519", "mlDsa"].includes(key)) || keys.ed25519 !== identity.nodeKey || typeof keys.mlDsa !== "string" || !/^[A-Za-z0-9_-]{1,8192}$/.test(keys.mlDsa)) {
559
559
  throw new MetalBootstrapError("metal public identity proof was invalid");
560
560
  }
561
- return JSON.stringify(parsed);
561
+ return {
562
+ nodeKey: identity.nodeKey,
563
+ publicKeys: { ed25519: keys.ed25519, mlDsa: keys.mlDsa }
564
+ };
565
+ }
566
+ function normalizeMetalPublicIdentity(value) {
567
+ return JSON.stringify(parseMetalPublicIdentity(value));
562
568
  }
563
569
  var defaultExec2 = async (argv, stdin) => {
564
570
  const child = Bun.spawn([...argv], {
@@ -1258,6 +1264,7 @@ export {
1258
1264
  renderMetalUnits,
1259
1265
  readMetalBootstrapConfig,
1260
1266
  planMetalBootstrap,
1267
+ parseMetalPublicIdentity,
1261
1268
  normalizeMetalPublicIdentity,
1262
1269
  metalBootstrapStatus,
1263
1270
  applyMetalBootstrap,
@@ -1,4 +1,5 @@
1
1
  import { type PlatformBootstrapConfig, type PlatformBootstrapSecrets } from './bootstrap';
2
+ import { type MetalPublicIdentity } from './metal-bootstrap';
2
3
  import { type PlatformGenesisGuest } from './platform-genesis';
3
4
  export interface OperatorSshHop {
4
5
  address: string;
@@ -20,12 +21,20 @@ export interface OperatorMetalBootstrapRequest {
20
21
  kind: 'metal-remote';
21
22
  target: OperatorPlatformBootstrapRequest['target'];
22
23
  metalConfigFile: string;
24
+ /** Secret-free, pinned-host identity evidence written automatically after apply. */
25
+ metalIdentityEvidenceFile?: string;
23
26
  genesis: {
24
27
  nodes: readonly [PlatformGenesisGuest, PlatformGenesisGuest, PlatformGenesisGuest];
25
28
  rehearsalNode?: PlatformGenesisGuest;
26
29
  sshPublicKeyFiles: string[];
27
30
  };
28
31
  }
32
+ export interface OperatorMetalIdentityEvidence extends MetalPublicIdentity {
33
+ format: 1;
34
+ kind: 'forgezero-operator-metal-identity';
35
+ metalHostname: string;
36
+ metalHostKeySha256: string;
37
+ }
29
38
  export interface OperatorGuestHostKeyEvidence {
30
39
  format: 1;
31
40
  kind: 'forgezero-operator-guest-host-keys';
@@ -139,6 +148,7 @@ export declare function readOperatorMetalBootstrapRequest(path: string, options?
139
148
  validateMetalConfig?: boolean;
140
149
  }): OperatorMetalBootstrapRequest;
141
150
  export declare function writeOperatorMetalBootstrapRequest(path: string, request: OperatorMetalBootstrapRequest): string;
151
+ export declare function readOperatorMetalIdentityEvidence(path: string, request: OperatorMetalBootstrapRequest): OperatorMetalIdentityEvidence;
142
152
  export declare function readOperatorGuestHostKeyEvidence(path: string, request?: OperatorMetalBootstrapRequest): OperatorGuestHostKeyEvidence;
143
153
  export declare function writeOperatorGuestHostKeyEvidence(path: string, evidence: OperatorGuestHostKeyEvidence, request?: OperatorMetalBootstrapRequest): string;
144
154
  export declare function planOperatorMetalBootstrap(request: OperatorMetalBootstrapRequest, mode: OperatorMetalBootstrapMode): OperatorMetalBootstrapPlan;