@forgezero/agent 0.1.102 → 0.1.107

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
@@ -4634,7 +4634,7 @@ var VaultError, runtimeEnvironment = () => typeof process !== "undefined" && pro
4634
4634
  } catch {
4635
4635
  return;
4636
4636
  }
4637
- }, VERSION = "0.1.19";
4637
+ }, VERSION = "0.1.20";
4638
4638
  var init_dist = __esm(() => {
4639
4639
  init_identity();
4640
4640
  VaultError = class VaultError extends Error {
@@ -4810,9 +4810,9 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
4810
4810
  }
4811
4811
 
4812
4812
  // src/cli/index.ts
4813
- import { existsSync as existsSync13, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync16, renameSync as renameSync10, rmSync as rmSync8, writeFileSync as writeFileSync13 } from "fs";
4813
+ import { existsSync as existsSync13, lstatSync as lstatSync11, mkdirSync as mkdirSync13, readFileSync as readFileSync17, renameSync as renameSync10, rmSync as rmSync8, writeFileSync as writeFileSync13 } from "fs";
4814
4814
  import { randomBytes as randomBytes10 } from "crypto";
4815
- import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join13, resolve as resolve12 } from "path";
4815
+ import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join13, resolve as resolve13 } from "path";
4816
4816
 
4817
4817
  // src/process-input.ts
4818
4818
  async function writeAndCloseProcessInput(input, value) {
@@ -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.102";
4844
+ var VERSION2 = "0.1.107";
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],
@@ -12478,6 +12496,7 @@ function platformApiCredentialSpecs(options) {
12478
12496
  ];
12479
12497
  return [
12480
12498
  { name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
12499
+ { name: "arangodb-root-password", encryptedPath: "/etc/forgezero/creds/arangodb-root-password.cred", required: true },
12481
12500
  { name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
12482
12501
  ...optional.filter(([, present]) => present).map(([name]) => ({
12483
12502
  name,
@@ -14061,6 +14080,46 @@ async function buildBootstrapBundle(input, exec = run) {
14061
14080
  throw cause;
14062
14081
  }
14063
14082
  }
14083
+ async function refreshBootstrapBundle(input, exec = run) {
14084
+ const outputPath = resolve6(input.outputPath);
14085
+ const manifestPath = `${outputPath}.json`;
14086
+ if (existsSync8(outputPath) !== existsSync8(manifestPath)) {
14087
+ throw new Error("bootstrap bundle and manifest must either both exist or both be absent");
14088
+ }
14089
+ const candidatePath = `${outputPath}.candidate.${process.pid}.${randomBytes6(6).toString("hex")}`;
14090
+ const candidate = await buildBootstrapBundle({ ...input, outputPath: candidatePath }, exec);
14091
+ try {
14092
+ if (!existsSync8(outputPath)) {
14093
+ renameSync7(candidate.bundlePath, outputPath);
14094
+ renameSync7(candidate.manifestPath, manifestPath);
14095
+ return readBootstrapBundle(outputPath);
14096
+ }
14097
+ const current2 = await readBootstrapBundle(outputPath);
14098
+ if (current2.manifest.branch === candidate.manifest.branch && current2.manifest.revision === candidate.manifest.revision) {
14099
+ rmSync5(candidate.bundlePath, { force: true });
14100
+ rmSync5(candidate.manifestPath, { force: true });
14101
+ return current2;
14102
+ }
14103
+ const archivePath = `${outputPath}.before-${current2.manifest.revision.slice(0, 7)}-${Date.now()}`;
14104
+ const archiveManifestPath = `${archivePath}.json`;
14105
+ renameSync7(outputPath, archivePath);
14106
+ renameSync7(manifestPath, archiveManifestPath);
14107
+ try {
14108
+ renameSync7(candidate.bundlePath, outputPath);
14109
+ renameSync7(candidate.manifestPath, manifestPath);
14110
+ return readBootstrapBundle(outputPath);
14111
+ } catch (cause) {
14112
+ rmSync5(outputPath, { force: true });
14113
+ rmSync5(manifestPath, { force: true });
14114
+ renameSync7(archivePath, outputPath);
14115
+ renameSync7(archiveManifestPath, manifestPath);
14116
+ throw cause;
14117
+ }
14118
+ } finally {
14119
+ rmSync5(candidate.bundlePath, { force: true });
14120
+ rmSync5(candidate.manifestPath, { force: true });
14121
+ }
14122
+ }
14064
14123
 
14065
14124
  // src/bootstrap.ts
14066
14125
  var PLATFORM_BOOTSTRAP_PROFILES = [
@@ -14102,7 +14161,17 @@ function validatePlatformBootstrapSecrets(config, input) {
14102
14161
  if (!input || typeof input !== "object" || Array.isArray(input))
14103
14162
  throw new Error("bootstrap credential input must be an object");
14104
14163
  const source = input;
14105
- const allowed = ["clusterBootstrapCode", "emailSecret", "enrolmentToken", "backupS3Secret", "cloudflareTunnelToken", "cloudflareApiToken"];
14164
+ const allowed = [
14165
+ "clusterBootstrapCode",
14166
+ "emailSecret",
14167
+ "enrolmentToken",
14168
+ "backupS3Secret",
14169
+ "cloudflareTunnelToken",
14170
+ "cloudflareApiToken",
14171
+ "githubClientSecret",
14172
+ "githubPrivateKeyBase64",
14173
+ "githubWebhookSecret"
14174
+ ];
14106
14175
  const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
14107
14176
  if (unknown.length)
14108
14177
  throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
@@ -14112,6 +14181,9 @@ function validatePlatformBootstrapSecrets(config, input) {
14112
14181
  const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
14113
14182
  const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
14114
14183
  const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
14184
+ const githubClientSecret = typeof source.githubClientSecret === "string" ? source.githubClientSecret.trim() : undefined;
14185
+ const githubPrivateKeyBase64 = typeof source.githubPrivateKeyBase64 === "string" ? source.githubPrivateKeyBase64.trim() : undefined;
14186
+ const githubWebhookSecret = typeof source.githubWebhookSecret === "string" ? source.githubWebhookSecret.trim() : undefined;
14115
14187
  if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
14116
14188
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
14117
14189
  if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
@@ -14131,12 +14203,29 @@ function validatePlatformBootstrapSecrets(config, input) {
14131
14203
  }
14132
14204
  if (cloudflareTunnelToken)
14133
14205
  validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
14206
+ const githubConfigured = Boolean(config.runtime.environment.githubApp);
14207
+ if (githubConfigured !== Boolean(githubClientSecret && githubPrivateKeyBase64 && githubWebhookSecret)) {
14208
+ throw new Error("GitHub App coordinates require client secret, private key and webhook secret together");
14209
+ }
14210
+ if (githubConfigured) {
14211
+ if (githubClientSecret.length < 20 || githubClientSecret.length > 512 || /[\r\n\0]/.test(githubClientSecret) || githubWebhookSecret.length < 32 || githubWebhookSecret.length > 512 || /[\r\n\0]/.test(githubWebhookSecret)) {
14212
+ throw new Error("GitHub App client or webhook secret is malformed");
14213
+ }
14214
+ try {
14215
+ const pem = Buffer.from(githubPrivateKeyBase64, "base64").toString("utf8");
14216
+ if (createPrivateKey(pem).asymmetricKeyType !== "rsa")
14217
+ throw new Error("not RSA");
14218
+ } catch {
14219
+ throw new Error("GitHub App private key must be a base64-encoded RSA private key");
14220
+ }
14221
+ }
14134
14222
  return {
14135
14223
  clusterBootstrapCode,
14136
14224
  emailSecret,
14137
14225
  ...enrolmentToken ? { enrolmentToken } : {},
14138
14226
  ...backupS3Secret ? { backupS3Secret } : {},
14139
- ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
14227
+ ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {},
14228
+ ...githubConfigured ? { githubClientSecret, githubPrivateKeyBase64, githubWebhookSecret } : {}
14140
14229
  };
14141
14230
  }
14142
14231
  var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
@@ -14151,6 +14240,7 @@ var STATE_PATH = BOOTSTRAP_STATE_PATH;
14151
14240
  var INTENT_PATH = "/var/lib/forgezero/bootstrap.intent.json";
14152
14241
  var CREDS = "/etc/forgezero/creds";
14153
14242
  var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
14243
+ var ARANGO_ROOT_CREDENTIAL = `${CREDS}/arangodb-root-password.cred`;
14154
14244
  var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
14155
14245
  var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
14156
14246
  var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
@@ -14436,7 +14526,7 @@ WantedBy=multi-user.target
14436
14526
  function databaseVerifyUnit(config) {
14437
14527
  const { address } = config.database;
14438
14528
  return `[Unit]
14439
- Description=Verify ForgeZero ArangoDB Community 3.11.14 writable Coordinator
14529
+ Description=Secure and verify ForgeZero ArangoDB Community 3.11.14 writable Coordinator
14440
14530
  Requires=forgezero-db.service
14441
14531
  After=forgezero-db.service
14442
14532
  PartOf=forgezero-db.service
@@ -14446,7 +14536,9 @@ Type=oneshot
14446
14536
  User=arangodb
14447
14537
  Group=arangodb
14448
14538
  LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
14449
- ExecStart=/usr/bin/arangosh --server.endpoint tcp://${unitEscape(address)}:8529 --server.jwt-secret-keyfile %d/arangodb-jwt --javascript.execute-string 'const c=require("@arangodb").db._connection;let last;let ok=false;for(let i=0;i<90;i++){try{const v=c.GET("/_api/version?details=true");const s=c.GET("/_admin/status");const m=c.GET("/_admin/server/mode");if(!v.error&&v.version==="3.11.14"&&v.details&&v.details.license==="community"&&!s.error&&s.serverInfo&&s.serverInfo.role==="COORDINATOR"&&!m.error&&m.mode==="default"){ok=true;break;}last={v,s,m};}catch(e){last=String(e);}require("internal").wait(2);}if(!ok)throw new Error("writable Community Coordinator verification failed: "+JSON.stringify(last));'
14539
+ LoadCredentialEncrypted=arangodb-root-password:${ARANGO_ROOT_CREDENTIAL}
14540
+ ExecStart=/usr/bin/arangosh --server.endpoint tcp://${unitEscape(address)}:8529 --server.jwt-secret-keyfile %d/arangodb-jwt --javascript.execute-string 'const c=require("@arangodb").db._connection;let last;let ok=false;for(let i=0;i<90;i++){try{const v=c.GET("/_api/version?details=true");const s=c.GET("/_admin/status");const m=c.GET("/_admin/server/mode");if(!v.error&&v.version==="3.11.14"&&v.details&&v.details.license==="community"&&!s.error&&s.serverInfo&&s.serverInfo.role==="COORDINATOR"&&!m.error&&m.mode==="default"){ok=true;break;}last={v,s,m};}catch(e){last=String(e);}require("internal").wait(2);}if(!ok)throw new Error("writable Community Coordinator verification failed: "+JSON.stringify(last));const password=require("fs").read("%d/arangodb-root-password").trim();if(password.length<48)throw new Error("ArangoDB root credential is malformed");require("@arangodb/users").update("root",password,true);'
14541
+ ExecStart=/usr/local/bin/fz-agent database-auth-verify --endpoint=http://${unitEscape(address)}:8529
14450
14542
  RemainAfterExit=yes
14451
14543
  TimeoutStartSec=200
14452
14544
  NoNewPrivileges=true
@@ -14688,6 +14780,12 @@ function approvedPlatformRepairIdentityDigests(config) {
14688
14780
  delete beforeAttestation.runtime.environment.initialInventory.attestation;
14689
14781
  prior.push(beforeAttestation);
14690
14782
  }
14783
+ const metalIdentity = config.runtime.environment.initialInventory?.metalIdentity;
14784
+ if (metalIdentity && config.enrolment.source === "genesis-derived") {
14785
+ const beforeMetalIdentity = structuredClone(config);
14786
+ delete beforeMetalIdentity.runtime.environment.initialInventory.metalIdentity;
14787
+ prior.push(beforeMetalIdentity);
14788
+ }
14691
14789
  if (config.database.role === "joiner" && config.enrolment.source === "genesis-derived" && config.runtime.environment.custodianEmail) {
14692
14790
  const beforeReplicaCustodianEmail = structuredClone(config);
14693
14791
  delete beforeReplicaCustodianEmail.runtime.environment.custodianEmail;
@@ -14907,6 +15005,11 @@ async function bootstrapStatus(host = localBootstrapHost()) {
14907
15005
  if (nginx.exitCode !== 0)
14908
15006
  problems.push("nginx configuration is invalid");
14909
15007
  if (state.databaseRole !== "none") {
15008
+ for (const credential of [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL]) {
15009
+ services[credential] = host.exists(credential);
15010
+ if (!services[credential])
15011
+ problems.push(`${credential} is missing`);
15012
+ }
14910
15013
  const unitPath = "/etc/systemd/system/forgezero-db.service";
14911
15014
  const expectsNoAgency = state.databaseAgency === "none";
14912
15015
  const unitHasNoAgency = host.exists(unitPath) && host.read(unitPath).includes("--cluster.start-agent=false");
@@ -15045,7 +15148,10 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15045
15148
  enrolmentToken: checked4.enrolmentToken,
15046
15149
  backup: checked4.backupS3Secret,
15047
15150
  cloudflareTunnelToken: checked4.cloudflareTunnelToken,
15048
- cloudflareApiToken: checked4.cloudflareApiToken
15151
+ cloudflareApiToken: checked4.cloudflareApiToken,
15152
+ githubClientSecret: checked4.githubClientSecret,
15153
+ githubPrivateKey: checked4.githubPrivateKeyBase64 ? Buffer.from(checked4.githubPrivateKeyBase64, "base64").toString("utf8") : undefined,
15154
+ githubWebhookSecret: checked4.githubWebhookSecret
15049
15155
  };
15050
15156
  })() : undefined;
15051
15157
  const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
@@ -15138,11 +15244,17 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15138
15244
  if (!host.exists(JWT_CREDENTIAL)) {
15139
15245
  await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
15140
15246
  }
15247
+ if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
15248
+ await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
15249
+ }
15141
15250
  await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
15142
15251
  await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
15143
15252
  const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
15144
15253
  for (const [name, source] of Object.entries({
15145
15254
  ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
15255
+ "fz_github.clientSecret": platformPrivate.githubClientSecret,
15256
+ "fz_github.privateKey": platformPrivate.githubPrivateKey,
15257
+ "fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
15146
15258
  "backup.s3.secretAccessKey": platformPrivate.backup
15147
15259
  })) {
15148
15260
  if (source) {
@@ -15159,6 +15271,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15159
15271
  const credentials = platformApiCredentialSpecs({
15160
15272
  emailProvider: runtime.environment.email?.provider,
15161
15273
  cloudflareKv: cloudflareConfigured,
15274
+ githubApp: Boolean(runtime.environment.githubApp),
15162
15275
  realtime: Boolean(runtime.environment.realtime)
15163
15276
  });
15164
15277
  const units = renderPlatformApiUnits({
@@ -15417,7 +15530,11 @@ function strictBootstrapDocument(value) {
15417
15530
  ], "runtime environment");
15418
15531
  const environment = runtime.environment;
15419
15532
  if (environment.initialInventory !== undefined) {
15420
- const inventory = exactKeys2(environment.initialInventory, ["metalHostname", "region", "computes", "attestation", "deployment"], "initial inventory");
15533
+ const inventory = exactKeys2(environment.initialInventory, ["metalHostname", "metalIdentity", "region", "computes", "attestation", "deployment"], "initial inventory");
15534
+ if (inventory.metalIdentity !== undefined) {
15535
+ const identity = exactKeys2(inventory.metalIdentity, ["nodeKey", "publicKeys"], "initial Metal identity");
15536
+ exactKeys2(identity.publicKeys, ["ed25519", "mlDsa"], "initial Metal public keys");
15537
+ }
15421
15538
  exactKeys2(inventory.region, ["key", "label", "country", "city", "confidentialCapable"], "initial inventory region");
15422
15539
  if (inventory.attestation !== undefined) {
15423
15540
  const attestation = exactKeys2(inventory.attestation, ["measurement", "tcbFloor"], "initial attestation evidence");
@@ -16156,7 +16273,7 @@ var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dc
16156
16273
 
16157
16274
  class MetalBootstrapError extends Error {
16158
16275
  }
16159
- function normalizeMetalPublicIdentity(value) {
16276
+ function parseMetalPublicIdentity(value) {
16160
16277
  let parsed;
16161
16278
  try {
16162
16279
  parsed = JSON.parse(value);
@@ -16171,7 +16288,13 @@ function normalizeMetalPublicIdentity(value) {
16171
16288
  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)) {
16172
16289
  throw new MetalBootstrapError("metal public identity proof was invalid");
16173
16290
  }
16174
- return JSON.stringify(parsed);
16291
+ return {
16292
+ nodeKey: identity.nodeKey,
16293
+ publicKeys: { ed25519: keys.ed25519, mlDsa: keys.mlDsa }
16294
+ };
16295
+ }
16296
+ function normalizeMetalPublicIdentity(value) {
16297
+ return JSON.stringify(parseMetalPublicIdentity(value));
16175
16298
  }
16176
16299
  var defaultExec2 = async (argv2, stdin) => {
16177
16300
  const child = Bun.spawn([...argv2], {
@@ -17061,7 +17184,7 @@ function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudfla
17061
17184
  return writeOwnerJson2(path, request, "operator fleet request");
17062
17185
  }
17063
17186
  var validateMetalRequestValue = (value, options = {}) => {
17064
- const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
17187
+ const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "metalIdentityEvidenceFile", "genesis"], "operator metal request");
17065
17188
  if (root.kind !== "metal-remote")
17066
17189
  throw new Error("operator metal bootstrap kind must be metal-remote");
17067
17190
  const targetValue = exactKeys5(root.target, ["address", "port", "user", "hostKey", "hostKeySha256", "identityPublicKeyFile", "agentSocket"], "operator metal target");
@@ -17080,6 +17203,12 @@ var validateMetalRequestValue = (value, options = {}) => {
17080
17203
  if (typeof root.metalConfigFile !== "string")
17081
17204
  throw new Error("metalConfigFile must be an absolute path");
17082
17205
  canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
17206
+ if (root.metalIdentityEvidenceFile !== undefined && typeof root.metalIdentityEvidenceFile !== "string") {
17207
+ throw new Error("metalIdentityEvidenceFile must be an absolute path");
17208
+ }
17209
+ if (typeof root.metalIdentityEvidenceFile === "string") {
17210
+ canonicalOutputPath(root.metalIdentityEvidenceFile, "metalIdentityEvidenceFile");
17211
+ }
17083
17212
  if (options.validateMetalConfig !== false)
17084
17213
  readMetalBootstrapConfig(root.metalConfigFile);
17085
17214
  const genesis = exactKeys5(root.genesis, ["nodes", "rehearsalNode", "sshPublicKeyFiles"], "operator metal genesis");
@@ -17128,6 +17257,7 @@ var validateMetalRequestValue = (value, options = {}) => {
17128
17257
  return {
17129
17258
  kind: "metal-remote",
17130
17259
  metalConfigFile: root.metalConfigFile,
17260
+ ...typeof root.metalIdentityEvidenceFile === "string" ? { metalIdentityEvidenceFile: root.metalIdentityEvidenceFile } : {},
17131
17261
  target: {
17132
17262
  ...target,
17133
17263
  identityPublicKeyFile: targetValue.identityPublicKeyFile,
@@ -17148,6 +17278,25 @@ function writeOperatorMetalBootstrapRequest(path, request) {
17148
17278
  validateMetalRequestValue(request);
17149
17279
  return writeOwnerJson2(path, request, "operator metal request");
17150
17280
  }
17281
+ function readOperatorMetalIdentityEvidence(path, request) {
17282
+ const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal identity evidence")));
17283
+ const root = exactKeys5(value, ["format", "kind", "metalHostname", "metalHostKeySha256", "nodeKey", "publicKeys"], "operator metal identity evidence");
17284
+ 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") {
17285
+ throw new Error("operator metal identity evidence is malformed");
17286
+ }
17287
+ const identity = parseMetalPublicIdentity(JSON.stringify({ nodeKey: root.nodeKey, publicKeys: root.publicKeys }));
17288
+ const config = readMetalBootstrapConfig(request.metalConfigFile, { allowHistoricalRelease: true });
17289
+ if (root.metalHostname !== config.metalHostname || root.metalHostKeySha256 !== request.target.hostKeySha256) {
17290
+ throw new Error("operator metal identity evidence does not match the reviewed Metal target");
17291
+ }
17292
+ return {
17293
+ format: 1,
17294
+ kind: "forgezero-operator-metal-identity",
17295
+ metalHostname: root.metalHostname,
17296
+ metalHostKeySha256: root.metalHostKeySha256,
17297
+ ...identity
17298
+ };
17299
+ }
17151
17300
  var validateGuestHostKeyEvidenceValue = (value, request) => {
17152
17301
  const root = exactKeys5(value, ["format", "kind", "metalHostKeySha256", "nodes"], "operator guest host-key evidence");
17153
17302
  if (root.format !== 1 || root.kind !== "forgezero-operator-guest-host-keys" || typeof root.metalHostKeySha256 !== "string" || !Array.isArray(root.nodes)) {
@@ -17833,6 +17982,25 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
17833
17982
  `${REMOTE_STAGE}/metal-config.json`,
17834
17983
  "--apply"
17835
17984
  ], "remote typed metal bootstrap");
17985
+ if (request.metalIdentityEvidenceFile) {
17986
+ let applied;
17987
+ try {
17988
+ applied = JSON.parse(output);
17989
+ } catch {
17990
+ throw new Error("remote Metal bootstrap did not return identity evidence");
17991
+ }
17992
+ const publicIdentity2 = applied?.publicIdentity;
17993
+ if (typeof publicIdentity2 !== "string")
17994
+ throw new Error("remote Metal bootstrap omitted its public identity");
17995
+ const identity = parseMetalPublicIdentity(publicIdentity2);
17996
+ writeOwnerJson2(request.metalIdentityEvidenceFile, {
17997
+ format: 1,
17998
+ kind: "forgezero-operator-metal-identity",
17999
+ metalHostname: config.metalHostname,
18000
+ metalHostKeySha256: request.target.hostKeySha256,
18001
+ ...identity
18002
+ }, "operator metal identity evidence");
18003
+ }
17836
18004
  return { plan, output };
17837
18005
  } finally {
17838
18006
  if (knownHosts) {
@@ -17886,7 +18054,7 @@ async function applyAttendedPlatformBootstrap(fleetRequest, cloudflareRequest, a
17886
18054
  }
17887
18055
 
17888
18056
  // src/platform-genesis-config.ts
17889
- function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment, attestation) {
18057
+ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment, attestation, metalIdentity) {
17890
18058
  const fleet = validatePlatformGenesisGuests(guests);
17891
18059
  if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(metalHostname)) {
17892
18060
  throw new Error("platform genesis requires the reviewed metal hostname");
@@ -17902,6 +18070,7 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
17902
18070
  const coordinators = fleet.map((guest) => `http://${guest.address}:8529`);
17903
18071
  const initialInventory = {
17904
18072
  metalHostname,
18073
+ ...metalIdentity ? { metalIdentity: structuredClone(metalIdentity) } : {},
17905
18074
  region: {
17906
18075
  key: template.runtime.environment.nodeRegion,
17907
18076
  label: region.label,
@@ -18065,6 +18234,7 @@ function forgeZeroLaunchTemplate(profile, guests, bundle, owner) {
18065
18234
  otlpTraceSampleRatio: 0.1,
18066
18235
  custodianEmail: owner.custodianEmail,
18067
18236
  email: owner.email,
18237
+ ...owner.githubApp ? { githubApp: owner.githubApp } : {},
18068
18238
  deployProfile: profile.environment
18069
18239
  },
18070
18240
  serviceUser: "forgezero-api",
@@ -18421,6 +18591,92 @@ function planAppBuild(input) {
18421
18591
  };
18422
18592
  }
18423
18593
 
18594
+ // src/platform-launch-env.ts
18595
+ import { lstatSync as lstatSync10, readFileSync as readFileSync16 } from "fs";
18596
+ import { resolve as resolve12 } from "path";
18597
+ var booleanValue = (values, name, fallback) => {
18598
+ const raw = values.get(name)?.trim().toLowerCase();
18599
+ if (raw === undefined || raw === "")
18600
+ return fallback;
18601
+ if (raw === "true")
18602
+ return true;
18603
+ if (raw === "false")
18604
+ return false;
18605
+ throw new Error(`${name} must be true or false`);
18606
+ };
18607
+ function readPlatformLaunchEnvironment(path) {
18608
+ const absolute2 = resolve12(path);
18609
+ const stat2 = lstatSync10(absolute2);
18610
+ if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1 || typeof process.getuid === "function" && stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0) {
18611
+ throw new Error(`bootstrap secret environment must be one owner-only regular file: ${absolute2}`);
18612
+ }
18613
+ const values = new Map;
18614
+ for (const [index, raw] of readFileSync16(absolute2, "utf8").split(/\r?\n/).entries()) {
18615
+ const line = raw.trim();
18616
+ if (!line || line.startsWith("#"))
18617
+ continue;
18618
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
18619
+ if (!match)
18620
+ throw new Error(`invalid environment assignment at ${absolute2}:${index + 1}`);
18621
+ if (values.has(match[1]))
18622
+ throw new Error(`duplicate environment assignment ${match[1]} at ${absolute2}:${index + 1}`);
18623
+ let value = match[2].trim();
18624
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
18625
+ value = value.slice(1, -1);
18626
+ }
18627
+ values.set(match[1], value);
18628
+ }
18629
+ const requiredValue = (name) => {
18630
+ const value = values.get(name)?.trim();
18631
+ if (!value)
18632
+ throw new Error(`bootstrap secret environment is missing ${name}`);
18633
+ return value;
18634
+ };
18635
+ const provider = requiredValue("FZ_EMAIL_PROVIDER");
18636
+ if (provider !== "smtp" && provider !== "jetemail")
18637
+ throw new Error("FZ_EMAIL_PROVIDER must be smtp or jetemail");
18638
+ const smtpPort = provider === "smtp" ? Number(requiredValue("FZ_SMTP_PORT")) : undefined;
18639
+ if (smtpPort !== undefined && (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535)) {
18640
+ throw new Error("FZ_SMTP_PORT must be an integer from 1 through 65535");
18641
+ }
18642
+ const githubEnabled = booleanValue(values, "FZ_GITHUB_ENABLED", false);
18643
+ const githubApp = githubEnabled ? {
18644
+ clientId: requiredValue("FZ_GITHUB_CLIENT_ID"),
18645
+ appId: requiredValue("FZ_GITHUB_APP_ID"),
18646
+ slug: requiredValue("FZ_GITHUB_APP_SLUG")
18647
+ } : undefined;
18648
+ const owner = provider === "jetemail" ? {
18649
+ custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
18650
+ email: {
18651
+ provider,
18652
+ from: requiredValue("FZ_EMAIL_FROM"),
18653
+ eu: booleanValue(values, "FZ_JETEMAIL_EU", false)
18654
+ },
18655
+ ...githubApp ? { githubApp } : {}
18656
+ } : {
18657
+ custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
18658
+ email: {
18659
+ provider,
18660
+ from: requiredValue("FZ_EMAIL_FROM"),
18661
+ host: requiredValue("FZ_SMTP_HOST"),
18662
+ port: smtpPort,
18663
+ user: requiredValue("FZ_SMTP_USER")
18664
+ },
18665
+ ...githubApp ? { githubApp } : {}
18666
+ };
18667
+ return {
18668
+ owner,
18669
+ emailSecret: requiredValue(provider === "jetemail" ? "FZ_JETEMAIL_API_KEY" : "FZ_SMTP_PASSWORD"),
18670
+ cloudflareTunnelToken: requiredValue("CF_TUNNEL_TOKEN"),
18671
+ cloudflareApiToken: requiredValue("CF_API_TOKEN"),
18672
+ ...githubEnabled ? {
18673
+ githubClientSecret: requiredValue("FZ_GITHUB_CLIENT_SECRET"),
18674
+ githubPrivateKeyBase64: requiredValue("FZ_GITHUB_PRIVATE_KEY_BASE64"),
18675
+ githubWebhookSecret: requiredValue("FZ_GITHUB_WEBHOOK_SECRET")
18676
+ } : {}
18677
+ };
18678
+ }
18679
+
18424
18680
  // src/cli/index.ts
18425
18681
  var DEFAULT_MODE = (THRESHOLD_MODES.find((mode) => mode.threshold === 1 && mode.total === 1) ?? THRESHOLD_MODES[0]).id;
18426
18682
  var PACKAGED_AGENT_BIN2 = operatorPackagedAgentPath();
@@ -18733,7 +18989,7 @@ async function api(options, path, init) {
18733
18989
  return { status: response.status, body };
18734
18990
  }
18735
18991
  function sleep(ms) {
18736
- return new Promise((resolve13) => setTimeout(resolve13, ms));
18992
+ return new Promise((resolve14) => setTimeout(resolve14, ms));
18737
18993
  }
18738
18994
  function browserCommand(url) {
18739
18995
  if (process.platform === "darwin")
@@ -18818,7 +19074,7 @@ function requireSession() {
18818
19074
  function readOwnerOnlySecret(path, label) {
18819
19075
  let metadata;
18820
19076
  try {
18821
- metadata = lstatSync10(path);
19077
+ metadata = lstatSync11(path);
18822
19078
  } catch {
18823
19079
  throw new Error(`The ${label} file ${path} is unreadable.`);
18824
19080
  }
@@ -18832,7 +19088,7 @@ function readOwnerOnlySecret(path, label) {
18832
19088
  if (uid !== undefined && uid !== 0 && metadata.uid !== uid) {
18833
19089
  throw new Error(`The ${label} file ${path} is not owned by the current user.`);
18834
19090
  }
18835
- const value = readFileSync16(path, "utf8").trim();
19091
+ const value = readFileSync17(path, "utf8").trim();
18836
19092
  if (!value)
18837
19093
  throw new Error(`The ${label} file ${path} is empty.`);
18838
19094
  return value;
@@ -19013,7 +19269,7 @@ async function cmdApi(options, args) {
19013
19269
  let body = undefined;
19014
19270
  if (options.data !== undefined && options.dataFile)
19015
19271
  throw new Error("Use only one of --data or --data-file.");
19016
- const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync16(options.dataFile, "utf8") : options.data;
19272
+ const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync17(options.dataFile, "utf8") : options.data;
19017
19273
  if (encoded !== undefined)
19018
19274
  body = JSON.parse(encoded);
19019
19275
  const result = await api(options, `${url.pathname}${url.search}`, { method, body });
@@ -19192,7 +19448,7 @@ async function cmdAgent(options, args) {
19192
19448
  out.line();
19193
19449
  out.line(" Compatibility SSH deploy public key:");
19194
19450
  out.line();
19195
- out.line(` ${readFileSync16(gitIdentity.gitPublicKeyPath, "utf8").trim()}`);
19451
+ out.line(` ${readFileSync17(gitIdentity.gitPublicKeyPath, "utf8").trim()}`);
19196
19452
  out.line();
19197
19453
  }
19198
19454
  return 0;
@@ -19232,65 +19488,6 @@ async function bootstrapSecret(question) {
19232
19488
  throw new Error(`${question} was not provided`);
19233
19489
  return value;
19234
19490
  }
19235
- function readPlatformLaunchEnvironment(path) {
19236
- const absolute2 = resolve12(path);
19237
- const stat2 = lstatSync10(absolute2);
19238
- if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1 || typeof process.getuid === "function" && stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0) {
19239
- throw new Error(`bootstrap secret environment must be one owner-only regular file: ${absolute2}`);
19240
- }
19241
- const values = new Map;
19242
- for (const [index, raw] of readFileSync16(absolute2, "utf8").split(/\r?\n/).entries()) {
19243
- const line = raw.trim();
19244
- if (!line || line.startsWith("#"))
19245
- continue;
19246
- const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
19247
- if (!match)
19248
- throw new Error(`invalid environment assignment at ${absolute2}:${index + 1}`);
19249
- if (values.has(match[1]))
19250
- throw new Error(`duplicate environment assignment ${match[1]} at ${absolute2}:${index + 1}`);
19251
- let value = match[2].trim();
19252
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
19253
- value = value.slice(1, -1);
19254
- }
19255
- values.set(match[1], value);
19256
- }
19257
- const requiredValue = (name) => {
19258
- const value = values.get(name)?.trim();
19259
- if (!value)
19260
- throw new Error(`bootstrap secret environment is missing ${name}`);
19261
- return value;
19262
- };
19263
- const provider = requiredValue("FZ_EMAIL_PROVIDER");
19264
- if (provider !== "smtp" && provider !== "jetemail")
19265
- throw new Error("FZ_EMAIL_PROVIDER must be smtp or jetemail");
19266
- const smtpPort = provider === "smtp" ? Number(requiredValue("FZ_SMTP_PORT")) : undefined;
19267
- if (smtpPort !== undefined && (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535)) {
19268
- throw new Error("FZ_SMTP_PORT must be an integer from 1 through 65535");
19269
- }
19270
- const owner = provider === "jetemail" ? {
19271
- custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
19272
- email: {
19273
- provider,
19274
- from: requiredValue("FZ_EMAIL_FROM"),
19275
- eu: (values.get("FZ_JETEMAIL_EU") ?? "false").trim().toLowerCase() === "true"
19276
- }
19277
- } : {
19278
- custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
19279
- email: {
19280
- provider,
19281
- from: requiredValue("FZ_EMAIL_FROM"),
19282
- host: requiredValue("FZ_SMTP_HOST"),
19283
- port: smtpPort,
19284
- user: requiredValue("FZ_SMTP_USER")
19285
- }
19286
- };
19287
- return {
19288
- owner,
19289
- emailSecret: requiredValue(provider === "jetemail" ? "FZ_JETEMAIL_API_KEY" : "FZ_SMTP_PASSWORD"),
19290
- cloudflareTunnelToken: requiredValue("CF_TUNNEL_TOKEN"),
19291
- cloudflareApiToken: requiredValue("CF_API_TOKEN")
19292
- };
19293
- }
19294
19491
  async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapCode) {
19295
19492
  return validatePlatformBootstrapSecrets(config, {
19296
19493
  clusterBootstrapCode: generatedClusterBootstrapCode ?? await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
@@ -19300,6 +19497,11 @@ async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapC
19300
19497
  ...config.cloudflareHandoff || config.runtime.environment.cloudflare ? {
19301
19498
  cloudflareTunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
19302
19499
  cloudflareApiToken: await bootstrapSecret("CF_API_TOKEN")
19500
+ } : {},
19501
+ ...config.runtime.environment.githubApp ? {
19502
+ githubClientSecret: await bootstrapSecret("GitHub App client secret"),
19503
+ githubPrivateKeyBase64: await bootstrapSecret("GitHub App RSA private key (base64)"),
19504
+ githubWebhookSecret: await bootstrapSecret("GitHub App webhook secret")
19303
19505
  } : {}
19304
19506
  });
19305
19507
  }
@@ -19361,7 +19563,7 @@ function interactiveMetalBootstrap() {
19361
19563
  });
19362
19564
  }
19363
19565
  function writeBootstrapConfig(path, config) {
19364
- if (!isAbsolute7(path) || resolve12(path) !== path)
19566
+ if (!isAbsolute7(path) || resolve13(path) !== path)
19365
19567
  throw new Error("--output must be a canonical absolute path");
19366
19568
  mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
19367
19569
  writeFileSync13(path, `${JSON.stringify(config, null, 2)}
@@ -19425,11 +19627,11 @@ async function interactiveCloudflareBootstrap() {
19425
19627
  });
19426
19628
  }
19427
19629
  function genesisOutputDirectory(path) {
19428
- if (!isAbsolute7(path) || resolve12(path) !== path)
19630
+ if (!isAbsolute7(path) || resolve13(path) !== path)
19429
19631
  throw new Error("--output must be a canonical absolute directory");
19430
19632
  if (!existsSync13(path))
19431
19633
  mkdirSync13(path, { recursive: true, mode: 448 });
19432
- const metadata = lstatSync10(path);
19634
+ const metadata = lstatSync11(path);
19433
19635
  const uid = process.getuid?.();
19434
19636
  if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 63) !== 0 || uid !== undefined && uid !== 0 && metadata.uid !== uid) {
19435
19637
  throw new Error("platform genesis output must be an owner-only directory owned by the current user");
@@ -19481,6 +19683,7 @@ function writeMetalOperatorFiles(directory) {
19481
19683
  const remoteRequest = writeOperatorMetalBootstrapRequest(join13(output, "metal-remote.json"), {
19482
19684
  kind: "metal-remote",
19483
19685
  metalConfigFile: metalConfig,
19686
+ metalIdentityEvidenceFile: join13(output, "metal-identity.json"),
19484
19687
  target: { ...target, ...identity },
19485
19688
  genesis: { ...interactiveGenesisGuests(), sshPublicKeyFiles: [identity.identityPublicKeyFile] }
19486
19689
  });
@@ -19501,6 +19704,10 @@ function writePlatformGenesisFleet(directory, preset) {
19501
19704
  const output = genesisOutputDirectory(directory);
19502
19705
  const metalRequestFile = preset?.metalRequestFile ?? bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
19503
19706
  const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile, { validateMetalConfig: false });
19707
+ if (!metalRequest.metalIdentityEvidenceFile) {
19708
+ throw new Error("the reviewed Metal request predates automatic identity seeding; rerun Metal apply with a generated request");
19709
+ }
19710
+ const metalIdentity = readOperatorMetalIdentityEvidence(metalRequest.metalIdentityEvidenceFile, metalRequest);
19504
19711
  const guestHostKeysFile = preset?.guestHostKeysFile ?? bootstrapAnswer("Owner-only guest host-key evidence file");
19505
19712
  const guestHostKeys = readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
19506
19713
  const fleet = metalRequest.genesis.nodes;
@@ -19545,7 +19752,7 @@ function writePlatformGenesisFleet(directory, preset) {
19545
19752
  branch: preset.profile.branch,
19546
19753
  revision: preset.bundle.manifest.revision,
19547
19754
  bundleSha256: preset.bundle.manifest.sha256
19548
- } : undefined, preset?.profile.attestation).map((config) => {
19755
+ } : undefined, preset?.profile.attestation, { nodeKey: metalIdentity.nodeKey, publicKeys: { ...metalIdentity.publicKeys } }).map((config) => {
19549
19756
  const path = `${output}/${config.computeReference}-platform.json`;
19550
19757
  if (preset)
19551
19758
  writeDerivedLaunchFile(path, (temporary) => writeBootstrapConfig(temporary, config));
@@ -19599,10 +19806,16 @@ function forgeZeroLaunchOwnerInput() {
19599
19806
  throw new Error("Bootstrap email provider must be smtp or jetemail");
19600
19807
  }
19601
19808
  const from = bootstrapAnswer("Bootstrap email From address");
19809
+ const githubApp = bootstrapBoolean("Configure the platform GitHub App now?", "no") ? {
19810
+ clientId: bootstrapAnswer("GitHub App client ID"),
19811
+ appId: bootstrapAnswer("GitHub App ID"),
19812
+ slug: bootstrapAnswer("GitHub App slug")
19813
+ } : undefined;
19602
19814
  if (provider === "jetemail") {
19603
19815
  return {
19604
19816
  custodianEmail,
19605
- email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") }
19817
+ email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") },
19818
+ ...githubApp ? { githubApp } : {}
19606
19819
  };
19607
19820
  }
19608
19821
  return {
@@ -19613,7 +19826,8 @@ function forgeZeroLaunchOwnerInput() {
19613
19826
  host: bootstrapAnswer("SMTP host"),
19614
19827
  port: bootstrapNumber("SMTP port", "587"),
19615
19828
  user: bootstrapAnswer("SMTP user")
19616
- }
19829
+ },
19830
+ ...githubApp ? { githubApp } : {}
19617
19831
  };
19618
19832
  }
19619
19833
  async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, projectRoot, owner) {
@@ -19628,14 +19842,11 @@ async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, proj
19628
19842
  if (!existsSync13(path))
19629
19843
  throw new Error(`${label} is missing at ${path}`);
19630
19844
  }
19631
- const root = resolve12(projectRoot);
19845
+ const root = resolve13(projectRoot);
19632
19846
  const nestedApi = join13(root, "api");
19633
19847
  const repositoryRoot = existsSync13(join13(nestedApi, "package.json")) ? nestedApi : root;
19634
19848
  const bundlePath = join13(base, "api.bundle");
19635
- const bundle = existsSync13(bundlePath) || existsSync13(`${bundlePath}.json`) ? await readBootstrapBundle(bundlePath) : await buildBootstrapBundle({ repositoryRoot, outputPath: bundlePath, branch: profile.branch });
19636
- if (bundle.manifest.branch !== profile.branch) {
19637
- throw new Error(`existing bootstrap bundle is for ${bundle.manifest.branch}, expected ${profile.branch}`);
19638
- }
19849
+ const bundle = await refreshBootstrapBundle({ repositoryRoot, outputPath: bundlePath, branch: profile.branch });
19639
19850
  return writePlatformGenesisFleet(outputDirectory, {
19640
19851
  profile,
19641
19852
  metalRequestFile,
@@ -19798,7 +20009,12 @@ async function runAttendedPlatformFleet(fleet, apply, generatedClusterBootstrapC
19798
20009
  clusterBootstrapCode: generatedClusterBootstrapCode,
19799
20010
  emailSecret: environmentInput.emailSecret,
19800
20011
  cloudflareTunnelToken: environmentInput.cloudflareTunnelToken,
19801
- cloudflareApiToken: environmentInput.cloudflareApiToken
20012
+ cloudflareApiToken: environmentInput.cloudflareApiToken,
20013
+ ...environmentInput.githubClientSecret ? {
20014
+ githubClientSecret: environmentInput.githubClientSecret,
20015
+ githubPrivateKeyBase64: environmentInput.githubPrivateKeyBase64,
20016
+ githubWebhookSecret: environmentInput.githubWebhookSecret
20017
+ } : {}
19802
20018
  }) : await promptPlatformBootstrapSecrets(firstConfig, generatedClusterBootstrapCode);
19803
20019
  const cloudflare = await resolveCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, {
19804
20020
  tunnelToken: secrets.cloudflareTunnelToken,
@@ -20112,7 +20328,7 @@ async function cmdUnlock(options) {
20112
20328
  if (options.key || options.userExplicit) {
20113
20329
  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.");
20114
20330
  }
20115
- const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync16(0, "utf8").trim() : "";
20331
+ const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync17(0, "utf8").trim() : "";
20116
20332
  const phrase = phraseText ? phraseText.split(/\s+/) : [];
20117
20333
  if (phrase.length !== 24) {
20118
20334
  throw new Error(`Recovery phrase must contain exactly 24 words; received ${phrase.length}. Use --phrase-file or --phrase-stdin.`);
@@ -20345,7 +20561,7 @@ function projectFromCheckout(options) {
20345
20561
  try {
20346
20562
  return loadConfig({
20347
20563
  cwd: options.projectRoot,
20348
- readFile: (path) => existsSync13(path) ? readFileSync16(path, "utf8") : undefined
20564
+ readFile: (path) => existsSync13(path) ? readFileSync17(path, "utf8") : undefined
20349
20565
  }).config.project;
20350
20566
  } catch (cause) {
20351
20567
  throw new Error(`No --project was given and the checkout has no usable .fz/config.json: ${cause instanceof Error ? cause.message : String(cause)}`);
@@ -20412,7 +20628,7 @@ async function cmdDeploy(options, args) {
20412
20628
  throw new Error("typed runtime initialization selects its tested software and attestation from --runtime/--isolation; do not combine --software or --attestation");
20413
20629
  }
20414
20630
  initializeTypeScriptDeployment(options.projectRoot, {
20415
- name: options.projectName ?? basename3(resolve12(options.projectRoot)),
20631
+ name: options.projectName ?? basename3(resolve13(options.projectRoot)),
20416
20632
  force: options.force,
20417
20633
  runtime: options.deployRuntime,
20418
20634
  operatingSystem: options.deployOperatingSystem,
@@ -20467,7 +20683,7 @@ async function cmdDeploy(options, args) {
20467
20683
  return problems.length === 0 ? 0 : 1;
20468
20684
  }
20469
20685
  if (operation === "check" || operation === "sync") {
20470
- if (existsSync13(resolve12(options.projectRoot, DEPLOY_SOURCE_FILE))) {
20686
+ if (existsSync13(resolve13(options.projectRoot, DEPLOY_SOURCE_FILE))) {
20471
20687
  const expected = await compileDeploymentProject(options.projectRoot, { write: false });
20472
20688
  const actual = inspectCompiledDeployment(options.projectRoot);
20473
20689
  const current2 = canonicalJson(expected.plan) === canonicalJson(actual.plan);
@@ -20543,7 +20759,7 @@ async function cmdDeploy(options, args) {
20543
20759
  branch: options.branch,
20544
20760
  cloneUrl: required(options.cloneUrl, "--clone-url"),
20545
20761
  sourceAuth,
20546
- ...options.knownHostsFile ? { knownHosts: readFileSync16(options.knownHostsFile, "utf8") } : {},
20762
+ ...options.knownHostsFile ? { knownHosts: readFileSync17(options.knownHostsFile, "utf8") } : {},
20547
20763
  projectKey
20548
20764
  } });
20549
20765
  const pipelineKey2 = String(created.pipelineKey);