@forgezero/agent 0.1.102 → 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.
@@ -917,7 +917,7 @@ async function postSignedNode(options, path, body) {
917
917
  }
918
918
 
919
919
  // src/version.ts
920
- var VERSION3 = "0.1.102";
920
+ var VERSION3 = "0.1.103";
921
921
 
922
922
  // src/agent-heartbeat.ts
923
923
  function readAgentHostMetrics() {
@@ -115,6 +115,10 @@ export interface PlatformBootstrapSecrets {
115
115
  backupS3Secret?: string;
116
116
  cloudflareTunnelToken?: string;
117
117
  cloudflareApiToken?: string;
118
+ githubClientSecret?: string;
119
+ /** One-line base64 form accepted by prompts/env; decoded before systemd sealing. */
120
+ githubPrivateKeyBase64?: string;
121
+ githubWebhookSecret?: string;
118
122
  }
119
123
  export interface EnrolledComputeBootstrapSecrets {
120
124
  enrolmentToken: string;
package/dist/bootstrap.js CHANGED
@@ -1386,7 +1386,7 @@ async function buildBootstrapBundle(input, exec = run) {
1386
1386
  }
1387
1387
 
1388
1388
  // src/bootstrap.ts
1389
- import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
1389
+ import { createHash as createHash2, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes3 } from "crypto";
1390
1390
  import {
1391
1391
  chmodSync as chmodSync3,
1392
1392
  existsSync as existsSync5,
@@ -1420,7 +1420,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1420
1420
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1421
1421
 
1422
1422
  // src/version.ts
1423
- var VERSION = "0.1.102";
1423
+ var VERSION = "0.1.103";
1424
1424
 
1425
1425
  // src/software.ts
1426
1426
  var PINNED_BUN_VERSION = "1.3.14";
@@ -3090,10 +3090,16 @@ var httpsOrigin = (name, raw) => {
3090
3090
  };
3091
3091
  var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
3092
3092
  function validatePlatformInitialInventory(value) {
3093
- 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)) {
3093
+ 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)) {
3094
3094
  throw new Error("initial platform inventory coordinates are invalid");
3095
3095
  }
3096
3096
  safeAtom("initialInventory.region.label", value.region.label);
3097
+ if (value.metalIdentity !== undefined) {
3098
+ const identity = value.metalIdentity;
3099
+ 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))) {
3100
+ throw new Error("initial platform Metal identity is invalid");
3101
+ }
3102
+ }
3097
3103
  if (value.region.city !== undefined)
3098
3104
  safeAtom("initialInventory.region.city", value.region.city);
3099
3105
  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)))
@@ -3128,6 +3134,7 @@ function validatePlatformInitialInventory(value) {
3128
3134
  }
3129
3135
  return {
3130
3136
  metalHostname: value.metalHostname,
3137
+ ...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
3131
3138
  region: { ...value.region },
3132
3139
  computes: value.computes.map((compute, index) => ({
3133
3140
  ...computes[index],
@@ -3181,6 +3188,11 @@ function validatePlatformSharedEnvironment(input) {
3181
3188
  } else if (input.email !== undefined) {
3182
3189
  throw new Error("Bootstrap email provider must be smtp or jetemail.");
3183
3190
  }
3191
+ if (input.githubApp) {
3192
+ 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)) {
3193
+ throw new Error("GitHub App client id, app id or slug is malformed.");
3194
+ }
3195
+ }
3184
3196
  boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
3185
3197
  if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
3186
3198
  throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
@@ -3311,6 +3323,9 @@ function renderPlatformSharedEnvironment(input) {
3311
3323
  FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
3312
3324
  FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
3313
3325
  FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
3326
+ FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
3327
+ FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
3328
+ FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
3314
3329
  FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
3315
3330
  };
3316
3331
  return `# Generated by fz bootstrap platform. Non-secret coordinates only.
@@ -3322,6 +3337,9 @@ function platformApiCredentialSpecs(options) {
3322
3337
  const optional = [
3323
3338
  ["fz_smtp.password", options.emailProvider === "smtp"],
3324
3339
  ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
3340
+ ["fz_github.clientSecret", options.githubApp],
3341
+ ["fz_github.privateKey", options.githubApp],
3342
+ ["fz_github.webhookSecret", options.githubApp],
3325
3343
  ["CF_API_TOKEN", options.cloudflareKv],
3326
3344
  ["CF_TUNNEL_TOKEN", options.cloudflareKv],
3327
3345
  ["REALTIME_PUBLISH_SECRET", options.realtime],
@@ -3717,7 +3735,17 @@ function validatePlatformBootstrapSecrets(config, input) {
3717
3735
  if (!input || typeof input !== "object" || Array.isArray(input))
3718
3736
  throw new Error("bootstrap credential input must be an object");
3719
3737
  const source = input;
3720
- const allowed = ["clusterBootstrapCode", "emailSecret", "enrolmentToken", "backupS3Secret", "cloudflareTunnelToken", "cloudflareApiToken"];
3738
+ const allowed = [
3739
+ "clusterBootstrapCode",
3740
+ "emailSecret",
3741
+ "enrolmentToken",
3742
+ "backupS3Secret",
3743
+ "cloudflareTunnelToken",
3744
+ "cloudflareApiToken",
3745
+ "githubClientSecret",
3746
+ "githubPrivateKeyBase64",
3747
+ "githubWebhookSecret"
3748
+ ];
3721
3749
  const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
3722
3750
  if (unknown.length)
3723
3751
  throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
@@ -3727,6 +3755,9 @@ function validatePlatformBootstrapSecrets(config, input) {
3727
3755
  const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
3728
3756
  const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
3729
3757
  const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
3758
+ const githubClientSecret = typeof source.githubClientSecret === "string" ? source.githubClientSecret.trim() : undefined;
3759
+ const githubPrivateKeyBase64 = typeof source.githubPrivateKeyBase64 === "string" ? source.githubPrivateKeyBase64.trim() : undefined;
3760
+ const githubWebhookSecret = typeof source.githubWebhookSecret === "string" ? source.githubWebhookSecret.trim() : undefined;
3730
3761
  if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
3731
3762
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
3732
3763
  if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
@@ -3746,12 +3777,29 @@ function validatePlatformBootstrapSecrets(config, input) {
3746
3777
  }
3747
3778
  if (cloudflareTunnelToken)
3748
3779
  validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
3780
+ const githubConfigured = Boolean(config.runtime.environment.githubApp);
3781
+ if (githubConfigured !== Boolean(githubClientSecret && githubPrivateKeyBase64 && githubWebhookSecret)) {
3782
+ throw new Error("GitHub App coordinates require client secret, private key and webhook secret together");
3783
+ }
3784
+ if (githubConfigured) {
3785
+ if (githubClientSecret.length < 20 || githubClientSecret.length > 512 || /[\r\n\0]/.test(githubClientSecret) || githubWebhookSecret.length < 32 || githubWebhookSecret.length > 512 || /[\r\n\0]/.test(githubWebhookSecret)) {
3786
+ throw new Error("GitHub App client or webhook secret is malformed");
3787
+ }
3788
+ try {
3789
+ const pem = Buffer.from(githubPrivateKeyBase64, "base64").toString("utf8");
3790
+ if (createPrivateKey(pem).asymmetricKeyType !== "rsa")
3791
+ throw new Error("not RSA");
3792
+ } catch {
3793
+ throw new Error("GitHub App private key must be a base64-encoded RSA private key");
3794
+ }
3795
+ }
3749
3796
  return {
3750
3797
  clusterBootstrapCode,
3751
3798
  emailSecret,
3752
3799
  ...enrolmentToken ? { enrolmentToken } : {},
3753
3800
  ...backupS3Secret ? { backupS3Secret } : {},
3754
- ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
3801
+ ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {},
3802
+ ...githubConfigured ? { githubClientSecret, githubPrivateKeyBase64, githubWebhookSecret } : {}
3755
3803
  };
3756
3804
  }
3757
3805
  var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
@@ -4303,6 +4351,12 @@ function approvedPlatformRepairIdentityDigests(config) {
4303
4351
  delete beforeAttestation.runtime.environment.initialInventory.attestation;
4304
4352
  prior.push(beforeAttestation);
4305
4353
  }
4354
+ const metalIdentity = config.runtime.environment.initialInventory?.metalIdentity;
4355
+ if (metalIdentity && config.enrolment.source === "genesis-derived") {
4356
+ const beforeMetalIdentity = structuredClone(config);
4357
+ delete beforeMetalIdentity.runtime.environment.initialInventory.metalIdentity;
4358
+ prior.push(beforeMetalIdentity);
4359
+ }
4306
4360
  if (config.database.role === "joiner" && config.enrolment.source === "genesis-derived" && config.runtime.environment.custodianEmail) {
4307
4361
  const beforeReplicaCustodianEmail = structuredClone(config);
4308
4362
  delete beforeReplicaCustodianEmail.runtime.environment.custodianEmail;
@@ -4660,7 +4714,10 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4660
4714
  enrolmentToken: checked4.enrolmentToken,
4661
4715
  backup: checked4.backupS3Secret,
4662
4716
  cloudflareTunnelToken: checked4.cloudflareTunnelToken,
4663
- cloudflareApiToken: checked4.cloudflareApiToken
4717
+ cloudflareApiToken: checked4.cloudflareApiToken,
4718
+ githubClientSecret: checked4.githubClientSecret,
4719
+ githubPrivateKey: checked4.githubPrivateKeyBase64 ? Buffer.from(checked4.githubPrivateKeyBase64, "base64").toString("utf8") : undefined,
4720
+ githubWebhookSecret: checked4.githubWebhookSecret
4664
4721
  };
4665
4722
  })() : undefined;
4666
4723
  const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
@@ -4758,6 +4815,9 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4758
4815
  const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
4759
4816
  for (const [name, source] of Object.entries({
4760
4817
  ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
4818
+ "fz_github.clientSecret": platformPrivate.githubClientSecret,
4819
+ "fz_github.privateKey": platformPrivate.githubPrivateKey,
4820
+ "fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
4761
4821
  "backup.s3.secretAccessKey": platformPrivate.backup
4762
4822
  })) {
4763
4823
  if (source) {
@@ -4774,6 +4834,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4774
4834
  const credentials = platformApiCredentialSpecs({
4775
4835
  emailProvider: runtime.environment.email?.provider,
4776
4836
  cloudflareKv: cloudflareConfigured,
4837
+ githubApp: Boolean(runtime.environment.githubApp),
4777
4838
  realtime: Boolean(runtime.environment.realtime)
4778
4839
  });
4779
4840
  const units = renderPlatformApiUnits({
package/dist/fz-agent.js CHANGED
@@ -9496,7 +9496,7 @@ async function writeAndCloseProcessInput(input, value) {
9496
9496
  }
9497
9497
 
9498
9498
  // src/version.ts
9499
- var VERSION2 = "0.1.102";
9499
+ var VERSION2 = "0.1.103";
9500
9500
 
9501
9501
  // src/ssh-bootstrap.ts
9502
9502
  class SshBootstrapError extends Error {
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.102";
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,12 @@ 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
+ }
14691
14745
  if (config.database.role === "joiner" && config.enrolment.source === "genesis-derived" && config.runtime.environment.custodianEmail) {
14692
14746
  const beforeReplicaCustodianEmail = structuredClone(config);
14693
14747
  delete beforeReplicaCustodianEmail.runtime.environment.custodianEmail;
@@ -15045,7 +15099,10 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15045
15099
  enrolmentToken: checked4.enrolmentToken,
15046
15100
  backup: checked4.backupS3Secret,
15047
15101
  cloudflareTunnelToken: checked4.cloudflareTunnelToken,
15048
- 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
15049
15106
  };
15050
15107
  })() : undefined;
15051
15108
  const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
@@ -15143,6 +15200,9 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15143
15200
  const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
15144
15201
  for (const [name, source] of Object.entries({
15145
15202
  ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
15203
+ "fz_github.clientSecret": platformPrivate.githubClientSecret,
15204
+ "fz_github.privateKey": platformPrivate.githubPrivateKey,
15205
+ "fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
15146
15206
  "backup.s3.secretAccessKey": platformPrivate.backup
15147
15207
  })) {
15148
15208
  if (source) {
@@ -15159,6 +15219,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
15159
15219
  const credentials = platformApiCredentialSpecs({
15160
15220
  emailProvider: runtime.environment.email?.provider,
15161
15221
  cloudflareKv: cloudflareConfigured,
15222
+ githubApp: Boolean(runtime.environment.githubApp),
15162
15223
  realtime: Boolean(runtime.environment.realtime)
15163
15224
  });
15164
15225
  const units = renderPlatformApiUnits({
@@ -16156,7 +16217,7 @@ var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dc
16156
16217
 
16157
16218
  class MetalBootstrapError extends Error {
16158
16219
  }
16159
- function normalizeMetalPublicIdentity(value) {
16220
+ function parseMetalPublicIdentity(value) {
16160
16221
  let parsed;
16161
16222
  try {
16162
16223
  parsed = JSON.parse(value);
@@ -16171,7 +16232,13 @@ function normalizeMetalPublicIdentity(value) {
16171
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)) {
16172
16233
  throw new MetalBootstrapError("metal public identity proof was invalid");
16173
16234
  }
16174
- 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));
16175
16242
  }
16176
16243
  var defaultExec2 = async (argv2, stdin) => {
16177
16244
  const child = Bun.spawn([...argv2], {
@@ -17061,7 +17128,7 @@ function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudfla
17061
17128
  return writeOwnerJson2(path, request, "operator fleet request");
17062
17129
  }
17063
17130
  var validateMetalRequestValue = (value, options = {}) => {
17064
- const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
17131
+ const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "metalIdentityEvidenceFile", "genesis"], "operator metal request");
17065
17132
  if (root.kind !== "metal-remote")
17066
17133
  throw new Error("operator metal bootstrap kind must be metal-remote");
17067
17134
  const targetValue = exactKeys5(root.target, ["address", "port", "user", "hostKey", "hostKeySha256", "identityPublicKeyFile", "agentSocket"], "operator metal target");
@@ -17080,6 +17147,12 @@ var validateMetalRequestValue = (value, options = {}) => {
17080
17147
  if (typeof root.metalConfigFile !== "string")
17081
17148
  throw new Error("metalConfigFile must be an absolute path");
17082
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
+ }
17083
17156
  if (options.validateMetalConfig !== false)
17084
17157
  readMetalBootstrapConfig(root.metalConfigFile);
17085
17158
  const genesis = exactKeys5(root.genesis, ["nodes", "rehearsalNode", "sshPublicKeyFiles"], "operator metal genesis");
@@ -17128,6 +17201,7 @@ var validateMetalRequestValue = (value, options = {}) => {
17128
17201
  return {
17129
17202
  kind: "metal-remote",
17130
17203
  metalConfigFile: root.metalConfigFile,
17204
+ ...typeof root.metalIdentityEvidenceFile === "string" ? { metalIdentityEvidenceFile: root.metalIdentityEvidenceFile } : {},
17131
17205
  target: {
17132
17206
  ...target,
17133
17207
  identityPublicKeyFile: targetValue.identityPublicKeyFile,
@@ -17148,6 +17222,25 @@ function writeOperatorMetalBootstrapRequest(path, request) {
17148
17222
  validateMetalRequestValue(request);
17149
17223
  return writeOwnerJson2(path, request, "operator metal request");
17150
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
+ }
17151
17244
  var validateGuestHostKeyEvidenceValue = (value, request) => {
17152
17245
  const root = exactKeys5(value, ["format", "kind", "metalHostKeySha256", "nodes"], "operator guest host-key evidence");
17153
17246
  if (root.format !== 1 || root.kind !== "forgezero-operator-guest-host-keys" || typeof root.metalHostKeySha256 !== "string" || !Array.isArray(root.nodes)) {
@@ -17833,6 +17926,25 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
17833
17926
  `${REMOTE_STAGE}/metal-config.json`,
17834
17927
  "--apply"
17835
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
+ }
17836
17948
  return { plan, output };
17837
17949
  } finally {
17838
17950
  if (knownHosts) {
@@ -17886,7 +17998,7 @@ async function applyAttendedPlatformBootstrap(fleetRequest, cloudflareRequest, a
17886
17998
  }
17887
17999
 
17888
18000
  // src/platform-genesis-config.ts
17889
- function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment, attestation) {
18001
+ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment, attestation, metalIdentity) {
17890
18002
  const fleet = validatePlatformGenesisGuests(guests);
17891
18003
  if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(metalHostname)) {
17892
18004
  throw new Error("platform genesis requires the reviewed metal hostname");
@@ -17902,6 +18014,7 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
17902
18014
  const coordinators = fleet.map((guest) => `http://${guest.address}:8529`);
17903
18015
  const initialInventory = {
17904
18016
  metalHostname,
18017
+ ...metalIdentity ? { metalIdentity: structuredClone(metalIdentity) } : {},
17905
18018
  region: {
17906
18019
  key: template.runtime.environment.nodeRegion,
17907
18020
  label: region.label,
@@ -18065,6 +18178,7 @@ function forgeZeroLaunchTemplate(profile, guests, bundle, owner) {
18065
18178
  otlpTraceSampleRatio: 0.1,
18066
18179
  custodianEmail: owner.custodianEmail,
18067
18180
  email: owner.email,
18181
+ ...owner.githubApp ? { githubApp: owner.githubApp } : {},
18068
18182
  deployProfile: profile.environment
18069
18183
  },
18070
18184
  serviceUser: "forgezero-api",
@@ -19267,13 +19381,20 @@ function readPlatformLaunchEnvironment(path) {
19267
19381
  if (smtpPort !== undefined && (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535)) {
19268
19382
  throw new Error("FZ_SMTP_PORT must be an integer from 1 through 65535");
19269
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;
19270
19390
  const owner = provider === "jetemail" ? {
19271
19391
  custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
19272
19392
  email: {
19273
19393
  provider,
19274
19394
  from: requiredValue("FZ_EMAIL_FROM"),
19275
19395
  eu: (values.get("FZ_JETEMAIL_EU") ?? "false").trim().toLowerCase() === "true"
19276
- }
19396
+ },
19397
+ ...githubApp ? { githubApp } : {}
19277
19398
  } : {
19278
19399
  custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
19279
19400
  email: {
@@ -19282,13 +19403,19 @@ function readPlatformLaunchEnvironment(path) {
19282
19403
  host: requiredValue("FZ_SMTP_HOST"),
19283
19404
  port: smtpPort,
19284
19405
  user: requiredValue("FZ_SMTP_USER")
19285
- }
19406
+ },
19407
+ ...githubApp ? { githubApp } : {}
19286
19408
  };
19287
19409
  return {
19288
19410
  owner,
19289
19411
  emailSecret: requiredValue(provider === "jetemail" ? "FZ_JETEMAIL_API_KEY" : "FZ_SMTP_PASSWORD"),
19290
19412
  cloudflareTunnelToken: requiredValue("CF_TUNNEL_TOKEN"),
19291
- 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
+ } : {}
19292
19419
  };
19293
19420
  }
19294
19421
  async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapCode) {
@@ -19300,6 +19427,11 @@ async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapC
19300
19427
  ...config.cloudflareHandoff || config.runtime.environment.cloudflare ? {
19301
19428
  cloudflareTunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
19302
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")
19303
19435
  } : {}
19304
19436
  });
19305
19437
  }
@@ -19481,6 +19613,7 @@ function writeMetalOperatorFiles(directory) {
19481
19613
  const remoteRequest = writeOperatorMetalBootstrapRequest(join13(output, "metal-remote.json"), {
19482
19614
  kind: "metal-remote",
19483
19615
  metalConfigFile: metalConfig,
19616
+ metalIdentityEvidenceFile: join13(output, "metal-identity.json"),
19484
19617
  target: { ...target, ...identity },
19485
19618
  genesis: { ...interactiveGenesisGuests(), sshPublicKeyFiles: [identity.identityPublicKeyFile] }
19486
19619
  });
@@ -19501,6 +19634,10 @@ function writePlatformGenesisFleet(directory, preset) {
19501
19634
  const output = genesisOutputDirectory(directory);
19502
19635
  const metalRequestFile = preset?.metalRequestFile ?? bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
19503
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);
19504
19641
  const guestHostKeysFile = preset?.guestHostKeysFile ?? bootstrapAnswer("Owner-only guest host-key evidence file");
19505
19642
  const guestHostKeys = readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
19506
19643
  const fleet = metalRequest.genesis.nodes;
@@ -19545,7 +19682,7 @@ function writePlatformGenesisFleet(directory, preset) {
19545
19682
  branch: preset.profile.branch,
19546
19683
  revision: preset.bundle.manifest.revision,
19547
19684
  bundleSha256: preset.bundle.manifest.sha256
19548
- } : undefined, preset?.profile.attestation).map((config) => {
19685
+ } : undefined, preset?.profile.attestation, { nodeKey: metalIdentity.nodeKey, publicKeys: { ...metalIdentity.publicKeys } }).map((config) => {
19549
19686
  const path = `${output}/${config.computeReference}-platform.json`;
19550
19687
  if (preset)
19551
19688
  writeDerivedLaunchFile(path, (temporary) => writeBootstrapConfig(temporary, config));
@@ -19599,10 +19736,16 @@ function forgeZeroLaunchOwnerInput() {
19599
19736
  throw new Error("Bootstrap email provider must be smtp or jetemail");
19600
19737
  }
19601
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;
19602
19744
  if (provider === "jetemail") {
19603
19745
  return {
19604
19746
  custodianEmail,
19605
- email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") }
19747
+ email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") },
19748
+ ...githubApp ? { githubApp } : {}
19606
19749
  };
19607
19750
  }
19608
19751
  return {
@@ -19613,7 +19756,8 @@ function forgeZeroLaunchOwnerInput() {
19613
19756
  host: bootstrapAnswer("SMTP host"),
19614
19757
  port: bootstrapNumber("SMTP port", "587"),
19615
19758
  user: bootstrapAnswer("SMTP user")
19616
- }
19759
+ },
19760
+ ...githubApp ? { githubApp } : {}
19617
19761
  };
19618
19762
  }
19619
19763
  async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, projectRoot, owner) {
@@ -19798,7 +19942,12 @@ async function runAttendedPlatformFleet(fleet, apply, generatedClusterBootstrapC
19798
19942
  clusterBootstrapCode: generatedClusterBootstrapCode,
19799
19943
  emailSecret: environmentInput.emailSecret,
19800
19944
  cloudflareTunnelToken: environmentInput.cloudflareTunnelToken,
19801
- cloudflareApiToken: environmentInput.cloudflareApiToken
19945
+ cloudflareApiToken: environmentInput.cloudflareApiToken,
19946
+ ...environmentInput.githubClientSecret ? {
19947
+ githubClientSecret: environmentInput.githubClientSecret,
19948
+ githubPrivateKeyBase64: environmentInput.githubPrivateKeyBase64,
19949
+ githubWebhookSecret: environmentInput.githubWebhookSecret
19950
+ } : {}
19802
19951
  }) : await promptPlatformBootstrapSecrets(firstConfig, generatedClusterBootstrapCode);
19803
19952
  const cloudflare = await resolveCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, {
19804
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.102";
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;
@@ -1386,7 +1386,7 @@ async function buildBootstrapBundle(input, exec = run) {
1386
1386
  }
1387
1387
 
1388
1388
  // src/bootstrap.ts
1389
- import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
1389
+ import { createHash as createHash2, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes3 } from "crypto";
1390
1390
  import {
1391
1391
  chmodSync as chmodSync3,
1392
1392
  existsSync as existsSync5,
@@ -1420,7 +1420,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1420
1420
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1421
1421
 
1422
1422
  // src/version.ts
1423
- var VERSION = "0.1.102";
1423
+ var VERSION = "0.1.103";
1424
1424
 
1425
1425
  // src/software.ts
1426
1426
  var PINNED_BUN_VERSION = "1.3.14";
@@ -3090,10 +3090,16 @@ var httpsOrigin = (name, raw) => {
3090
3090
  };
3091
3091
  var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
3092
3092
  function validatePlatformInitialInventory(value) {
3093
- 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)) {
3093
+ 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)) {
3094
3094
  throw new Error("initial platform inventory coordinates are invalid");
3095
3095
  }
3096
3096
  safeAtom("initialInventory.region.label", value.region.label);
3097
+ if (value.metalIdentity !== undefined) {
3098
+ const identity = value.metalIdentity;
3099
+ 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))) {
3100
+ throw new Error("initial platform Metal identity is invalid");
3101
+ }
3102
+ }
3097
3103
  if (value.region.city !== undefined)
3098
3104
  safeAtom("initialInventory.region.city", value.region.city);
3099
3105
  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)))
@@ -3128,6 +3134,7 @@ function validatePlatformInitialInventory(value) {
3128
3134
  }
3129
3135
  return {
3130
3136
  metalHostname: value.metalHostname,
3137
+ ...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
3131
3138
  region: { ...value.region },
3132
3139
  computes: value.computes.map((compute, index) => ({
3133
3140
  ...computes[index],
@@ -3181,6 +3188,11 @@ function validatePlatformSharedEnvironment(input) {
3181
3188
  } else if (input.email !== undefined) {
3182
3189
  throw new Error("Bootstrap email provider must be smtp or jetemail.");
3183
3190
  }
3191
+ if (input.githubApp) {
3192
+ 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)) {
3193
+ throw new Error("GitHub App client id, app id or slug is malformed.");
3194
+ }
3195
+ }
3184
3196
  boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
3185
3197
  if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
3186
3198
  throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
@@ -3311,6 +3323,9 @@ function renderPlatformSharedEnvironment(input) {
3311
3323
  FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
3312
3324
  FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
3313
3325
  FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
3326
+ FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
3327
+ FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
3328
+ FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
3314
3329
  FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
3315
3330
  };
3316
3331
  return `# Generated by fz bootstrap platform. Non-secret coordinates only.
@@ -3322,6 +3337,9 @@ function platformApiCredentialSpecs(options) {
3322
3337
  const optional = [
3323
3338
  ["fz_smtp.password", options.emailProvider === "smtp"],
3324
3339
  ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
3340
+ ["fz_github.clientSecret", options.githubApp],
3341
+ ["fz_github.privateKey", options.githubApp],
3342
+ ["fz_github.webhookSecret", options.githubApp],
3325
3343
  ["CF_API_TOKEN", options.cloudflareKv],
3326
3344
  ["CF_TUNNEL_TOKEN", options.cloudflareKv],
3327
3345
  ["REALTIME_PUBLISH_SECRET", options.realtime],
@@ -3717,7 +3735,17 @@ function validatePlatformBootstrapSecrets(config, input) {
3717
3735
  if (!input || typeof input !== "object" || Array.isArray(input))
3718
3736
  throw new Error("bootstrap credential input must be an object");
3719
3737
  const source = input;
3720
- const allowed = ["clusterBootstrapCode", "emailSecret", "enrolmentToken", "backupS3Secret", "cloudflareTunnelToken", "cloudflareApiToken"];
3738
+ const allowed = [
3739
+ "clusterBootstrapCode",
3740
+ "emailSecret",
3741
+ "enrolmentToken",
3742
+ "backupS3Secret",
3743
+ "cloudflareTunnelToken",
3744
+ "cloudflareApiToken",
3745
+ "githubClientSecret",
3746
+ "githubPrivateKeyBase64",
3747
+ "githubWebhookSecret"
3748
+ ];
3721
3749
  const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
3722
3750
  if (unknown.length)
3723
3751
  throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
@@ -3727,6 +3755,9 @@ function validatePlatformBootstrapSecrets(config, input) {
3727
3755
  const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
3728
3756
  const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
3729
3757
  const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
3758
+ const githubClientSecret = typeof source.githubClientSecret === "string" ? source.githubClientSecret.trim() : undefined;
3759
+ const githubPrivateKeyBase64 = typeof source.githubPrivateKeyBase64 === "string" ? source.githubPrivateKeyBase64.trim() : undefined;
3760
+ const githubWebhookSecret = typeof source.githubWebhookSecret === "string" ? source.githubWebhookSecret.trim() : undefined;
3730
3761
  if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
3731
3762
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
3732
3763
  if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
@@ -3746,12 +3777,29 @@ function validatePlatformBootstrapSecrets(config, input) {
3746
3777
  }
3747
3778
  if (cloudflareTunnelToken)
3748
3779
  validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
3780
+ const githubConfigured = Boolean(config.runtime.environment.githubApp);
3781
+ if (githubConfigured !== Boolean(githubClientSecret && githubPrivateKeyBase64 && githubWebhookSecret)) {
3782
+ throw new Error("GitHub App coordinates require client secret, private key and webhook secret together");
3783
+ }
3784
+ if (githubConfigured) {
3785
+ if (githubClientSecret.length < 20 || githubClientSecret.length > 512 || /[\r\n\0]/.test(githubClientSecret) || githubWebhookSecret.length < 32 || githubWebhookSecret.length > 512 || /[\r\n\0]/.test(githubWebhookSecret)) {
3786
+ throw new Error("GitHub App client or webhook secret is malformed");
3787
+ }
3788
+ try {
3789
+ const pem = Buffer.from(githubPrivateKeyBase64, "base64").toString("utf8");
3790
+ if (createPrivateKey(pem).asymmetricKeyType !== "rsa")
3791
+ throw new Error("not RSA");
3792
+ } catch {
3793
+ throw new Error("GitHub App private key must be a base64-encoded RSA private key");
3794
+ }
3795
+ }
3749
3796
  return {
3750
3797
  clusterBootstrapCode,
3751
3798
  emailSecret,
3752
3799
  ...enrolmentToken ? { enrolmentToken } : {},
3753
3800
  ...backupS3Secret ? { backupS3Secret } : {},
3754
- ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
3801
+ ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {},
3802
+ ...githubConfigured ? { githubClientSecret, githubPrivateKeyBase64, githubWebhookSecret } : {}
3755
3803
  };
3756
3804
  }
3757
3805
  var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
@@ -4303,6 +4351,12 @@ function approvedPlatformRepairIdentityDigests(config) {
4303
4351
  delete beforeAttestation.runtime.environment.initialInventory.attestation;
4304
4352
  prior.push(beforeAttestation);
4305
4353
  }
4354
+ const metalIdentity = config.runtime.environment.initialInventory?.metalIdentity;
4355
+ if (metalIdentity && config.enrolment.source === "genesis-derived") {
4356
+ const beforeMetalIdentity = structuredClone(config);
4357
+ delete beforeMetalIdentity.runtime.environment.initialInventory.metalIdentity;
4358
+ prior.push(beforeMetalIdentity);
4359
+ }
4306
4360
  if (config.database.role === "joiner" && config.enrolment.source === "genesis-derived" && config.runtime.environment.custodianEmail) {
4307
4361
  const beforeReplicaCustodianEmail = structuredClone(config);
4308
4362
  delete beforeReplicaCustodianEmail.runtime.environment.custodianEmail;
@@ -4660,7 +4714,10 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4660
4714
  enrolmentToken: checked4.enrolmentToken,
4661
4715
  backup: checked4.backupS3Secret,
4662
4716
  cloudflareTunnelToken: checked4.cloudflareTunnelToken,
4663
- cloudflareApiToken: checked4.cloudflareApiToken
4717
+ cloudflareApiToken: checked4.cloudflareApiToken,
4718
+ githubClientSecret: checked4.githubClientSecret,
4719
+ githubPrivateKey: checked4.githubPrivateKeyBase64 ? Buffer.from(checked4.githubPrivateKeyBase64, "base64").toString("utf8") : undefined,
4720
+ githubWebhookSecret: checked4.githubWebhookSecret
4664
4721
  };
4665
4722
  })() : undefined;
4666
4723
  const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
@@ -4758,6 +4815,9 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4758
4815
  const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
4759
4816
  for (const [name, source] of Object.entries({
4760
4817
  ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
4818
+ "fz_github.clientSecret": platformPrivate.githubClientSecret,
4819
+ "fz_github.privateKey": platformPrivate.githubPrivateKey,
4820
+ "fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
4761
4821
  "backup.s3.secretAccessKey": platformPrivate.backup
4762
4822
  })) {
4763
4823
  if (source) {
@@ -4774,6 +4834,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4774
4834
  const credentials = platformApiCredentialSpecs({
4775
4835
  emailProvider: runtime.environment.email?.provider,
4776
4836
  cloudflareKv: cloudflareConfigured,
4837
+ githubApp: Boolean(runtime.environment.githubApp),
4777
4838
  realtime: Boolean(runtime.environment.realtime)
4778
4839
  });
4779
4840
  const units = renderPlatformApiUnits({
@@ -5501,7 +5562,7 @@ var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dc
5501
5562
 
5502
5563
  class MetalBootstrapError extends Error {
5503
5564
  }
5504
- function normalizeMetalPublicIdentity(value) {
5565
+ function parseMetalPublicIdentity(value) {
5505
5566
  let parsed;
5506
5567
  try {
5507
5568
  parsed = JSON.parse(value);
@@ -5516,7 +5577,13 @@ function normalizeMetalPublicIdentity(value) {
5516
5577
  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)) {
5517
5578
  throw new MetalBootstrapError("metal public identity proof was invalid");
5518
5579
  }
5519
- return JSON.stringify(parsed);
5580
+ return {
5581
+ nodeKey: identity.nodeKey,
5582
+ publicKeys: { ed25519: keys.ed25519, mlDsa: keys.mlDsa }
5583
+ };
5584
+ }
5585
+ function normalizeMetalPublicIdentity(value) {
5586
+ return JSON.stringify(parseMetalPublicIdentity(value));
5520
5587
  }
5521
5588
  var defaultExec2 = async (argv, stdin) => {
5522
5589
  const child = Bun.spawn([...argv], {
@@ -6412,7 +6479,7 @@ function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudfla
6412
6479
  return writeOwnerJson2(path, request, "operator fleet request");
6413
6480
  }
6414
6481
  var validateMetalRequestValue = (value, options = {}) => {
6415
- const root = exactKeys3(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
6482
+ const root = exactKeys3(value, ["kind", "target", "metalConfigFile", "metalIdentityEvidenceFile", "genesis"], "operator metal request");
6416
6483
  if (root.kind !== "metal-remote")
6417
6484
  throw new Error("operator metal bootstrap kind must be metal-remote");
6418
6485
  const targetValue = exactKeys3(root.target, ["address", "port", "user", "hostKey", "hostKeySha256", "identityPublicKeyFile", "agentSocket"], "operator metal target");
@@ -6431,6 +6498,12 @@ var validateMetalRequestValue = (value, options = {}) => {
6431
6498
  if (typeof root.metalConfigFile !== "string")
6432
6499
  throw new Error("metalConfigFile must be an absolute path");
6433
6500
  canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
6501
+ if (root.metalIdentityEvidenceFile !== undefined && typeof root.metalIdentityEvidenceFile !== "string") {
6502
+ throw new Error("metalIdentityEvidenceFile must be an absolute path");
6503
+ }
6504
+ if (typeof root.metalIdentityEvidenceFile === "string") {
6505
+ canonicalOutputPath(root.metalIdentityEvidenceFile, "metalIdentityEvidenceFile");
6506
+ }
6434
6507
  if (options.validateMetalConfig !== false)
6435
6508
  readMetalBootstrapConfig(root.metalConfigFile);
6436
6509
  const genesis = exactKeys3(root.genesis, ["nodes", "rehearsalNode", "sshPublicKeyFiles"], "operator metal genesis");
@@ -6479,6 +6552,7 @@ var validateMetalRequestValue = (value, options = {}) => {
6479
6552
  return {
6480
6553
  kind: "metal-remote",
6481
6554
  metalConfigFile: root.metalConfigFile,
6555
+ ...typeof root.metalIdentityEvidenceFile === "string" ? { metalIdentityEvidenceFile: root.metalIdentityEvidenceFile } : {},
6482
6556
  target: {
6483
6557
  ...target,
6484
6558
  identityPublicKeyFile: targetValue.identityPublicKeyFile,
@@ -6499,6 +6573,25 @@ function writeOperatorMetalBootstrapRequest(path, request) {
6499
6573
  validateMetalRequestValue(request);
6500
6574
  return writeOwnerJson2(path, request, "operator metal request");
6501
6575
  }
6576
+ function readOperatorMetalIdentityEvidence(path, request) {
6577
+ const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal identity evidence")));
6578
+ const root = exactKeys3(value, ["format", "kind", "metalHostname", "metalHostKeySha256", "nodeKey", "publicKeys"], "operator metal identity evidence");
6579
+ 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") {
6580
+ throw new Error("operator metal identity evidence is malformed");
6581
+ }
6582
+ const identity = parseMetalPublicIdentity(JSON.stringify({ nodeKey: root.nodeKey, publicKeys: root.publicKeys }));
6583
+ const config = readMetalBootstrapConfig(request.metalConfigFile, { allowHistoricalRelease: true });
6584
+ if (root.metalHostname !== config.metalHostname || root.metalHostKeySha256 !== request.target.hostKeySha256) {
6585
+ throw new Error("operator metal identity evidence does not match the reviewed Metal target");
6586
+ }
6587
+ return {
6588
+ format: 1,
6589
+ kind: "forgezero-operator-metal-identity",
6590
+ metalHostname: root.metalHostname,
6591
+ metalHostKeySha256: root.metalHostKeySha256,
6592
+ ...identity
6593
+ };
6594
+ }
6502
6595
  var validateGuestHostKeyEvidenceValue = (value, request) => {
6503
6596
  const root = exactKeys3(value, ["format", "kind", "metalHostKeySha256", "nodes"], "operator guest host-key evidence");
6504
6597
  if (root.format !== 1 || root.kind !== "forgezero-operator-guest-host-keys" || typeof root.metalHostKeySha256 !== "string" || !Array.isArray(root.nodes)) {
@@ -7279,6 +7372,25 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
7279
7372
  `${REMOTE_STAGE}/metal-config.json`,
7280
7373
  "--apply"
7281
7374
  ], "remote typed metal bootstrap");
7375
+ if (request.metalIdentityEvidenceFile) {
7376
+ let applied;
7377
+ try {
7378
+ applied = JSON.parse(output);
7379
+ } catch {
7380
+ throw new Error("remote Metal bootstrap did not return identity evidence");
7381
+ }
7382
+ const publicIdentity2 = applied?.publicIdentity;
7383
+ if (typeof publicIdentity2 !== "string")
7384
+ throw new Error("remote Metal bootstrap omitted its public identity");
7385
+ const identity = parseMetalPublicIdentity(publicIdentity2);
7386
+ writeOwnerJson2(request.metalIdentityEvidenceFile, {
7387
+ format: 1,
7388
+ kind: "forgezero-operator-metal-identity",
7389
+ metalHostname: config.metalHostname,
7390
+ metalHostKeySha256: request.target.hostKeySha256,
7391
+ ...identity
7392
+ }, "operator metal identity evidence");
7393
+ }
7282
7394
  return { plan, output };
7283
7395
  } finally {
7284
7396
  if (knownHosts) {
@@ -7299,6 +7411,7 @@ export {
7299
7411
  redactOperatorSecretDiagnostic,
7300
7412
  readOperatorPlatformBootstrapRequest,
7301
7413
  readOperatorPlatformBootstrapFleetRequest,
7414
+ readOperatorMetalIdentityEvidence,
7302
7415
  readOperatorMetalBootstrapRequest,
7303
7416
  readOperatorGuestHostKeyEvidence,
7304
7417
  planOperatorPlatformFleetBootstrap,
@@ -19,6 +19,14 @@ export interface PlatformInitialInventoryCompute {
19
19
  }
20
20
  export interface PlatformInitialInventory {
21
21
  metalHostname: string;
22
+ /** Hybrid public identity proved by the pinned Metal bootstrap transport. */
23
+ metalIdentity?: {
24
+ nodeKey: string;
25
+ publicKeys: {
26
+ ed25519: string;
27
+ mlDsa: string;
28
+ };
29
+ };
22
30
  region: {
23
31
  key: string;
24
32
  label: string;
@@ -60,6 +68,16 @@ export type PlatformBootstrapEmail = {
60
68
  from: string;
61
69
  eu: boolean;
62
70
  };
71
+ /**
72
+ * Public coordinates for one GitHub App. Its client secret, RSA private key and
73
+ * webhook secret are attended inputs sealed as systemd credentials and later
74
+ * imported into the unlocked platform Vault.
75
+ */
76
+ export interface PlatformBootstrapGitHubApp {
77
+ clientId: string;
78
+ appId: string;
79
+ slug: string;
80
+ }
63
81
  export interface PlatformSharedEnvironment {
64
82
  softwareProfile: PlatformSoftwareProfile;
65
83
  databaseRole: PlatformDatabaseRole;
@@ -91,6 +109,7 @@ export interface PlatformSharedEnvironment {
91
109
  otlpTraceSampleRatio: number;
92
110
  custodianEmail?: string;
93
111
  email?: PlatformBootstrapEmail;
112
+ githubApp?: PlatformBootstrapGitHubApp;
94
113
  backup?: {
95
114
  endpoint: string;
96
115
  region: string;
@@ -118,12 +137,13 @@ export declare function validatePlatformSharedEnvironment(input: PlatformSharedE
118
137
  /** Render only non-secret runtime coordinates. Passwords/tokens have no field in this contract. */
119
138
  export declare function renderPlatformSharedEnvironment(input: PlatformSharedEnvironment): string;
120
139
  export interface SystemdCredentialSpec {
121
- name: 'arangodb-jwt' | 'seed-sync-root' | 'fz_smtp.password' | 'fz_jetemail.apiKey' | 'CF_API_TOKEN' | 'CF_TUNNEL_TOKEN' | 'REALTIME_PUBLISH_SECRET' | 'REALTIME_TICKET_SECRET';
140
+ name: 'arangodb-jwt' | 'seed-sync-root' | 'fz_smtp.password' | 'fz_jetemail.apiKey' | 'fz_github.clientSecret' | 'fz_github.privateKey' | 'fz_github.webhookSecret' | 'CF_API_TOKEN' | 'CF_TUNNEL_TOKEN' | 'REALTIME_PUBLISH_SECRET' | 'REALTIME_TICKET_SECRET';
122
141
  encryptedPath: string;
123
142
  required: boolean;
124
143
  }
125
144
  export declare function platformApiCredentialSpecs(options: {
126
145
  emailProvider?: PlatformBootstrapEmail['provider'];
146
+ githubApp: boolean;
127
147
  cloudflareKv: boolean;
128
148
  realtime: boolean;
129
149
  }): SystemdCredentialSpec[];
@@ -730,10 +730,16 @@ var httpsOrigin = (name, raw) => {
730
730
  };
731
731
  var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
732
732
  function validatePlatformInitialInventory(value) {
733
- 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)) {
733
+ 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)) {
734
734
  throw new Error("initial platform inventory coordinates are invalid");
735
735
  }
736
736
  safeAtom("initialInventory.region.label", value.region.label);
737
+ if (value.metalIdentity !== undefined) {
738
+ const identity = value.metalIdentity;
739
+ 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))) {
740
+ throw new Error("initial platform Metal identity is invalid");
741
+ }
742
+ }
737
743
  if (value.region.city !== undefined)
738
744
  safeAtom("initialInventory.region.city", value.region.city);
739
745
  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)))
@@ -768,6 +774,7 @@ function validatePlatformInitialInventory(value) {
768
774
  }
769
775
  return {
770
776
  metalHostname: value.metalHostname,
777
+ ...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
771
778
  region: { ...value.region },
772
779
  computes: value.computes.map((compute, index) => ({
773
780
  ...computes[index],
@@ -821,6 +828,11 @@ function validatePlatformSharedEnvironment(input) {
821
828
  } else if (input.email !== undefined) {
822
829
  throw new Error("Bootstrap email provider must be smtp or jetemail.");
823
830
  }
831
+ if (input.githubApp) {
832
+ 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)) {
833
+ throw new Error("GitHub App client id, app id or slug is malformed.");
834
+ }
835
+ }
824
836
  boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
825
837
  if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
826
838
  throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
@@ -951,6 +963,9 @@ function renderPlatformSharedEnvironment(input) {
951
963
  FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
952
964
  FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
953
965
  FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
966
+ FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
967
+ FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
968
+ FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
954
969
  FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
955
970
  };
956
971
  return `# Generated by fz bootstrap platform. Non-secret coordinates only.
@@ -962,6 +977,9 @@ function platformApiCredentialSpecs(options) {
962
977
  const optional = [
963
978
  ["fz_smtp.password", options.emailProvider === "smtp"],
964
979
  ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
980
+ ["fz_github.clientSecret", options.githubApp],
981
+ ["fz_github.privateKey", options.githubApp],
982
+ ["fz_github.webhookSecret", options.githubApp],
965
983
  ["CF_API_TOKEN", options.cloudflareKv],
966
984
  ["CF_TUNNEL_TOKEN", options.cloudflareKv],
967
985
  ["REALTIME_PUBLISH_SECRET", options.realtime],
@@ -2908,7 +2908,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2908
2908
  }
2909
2909
 
2910
2910
  // src/version.ts
2911
- var VERSION3 = "0.1.102";
2911
+ var VERSION3 = "0.1.103";
2912
2912
 
2913
2913
  // src/egress-policy.ts
2914
2914
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -4142,10 +4142,16 @@ var httpsOrigin = (name, raw) => {
4142
4142
  };
4143
4143
  var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
4144
4144
  function validatePlatformInitialInventory(value) {
4145
- 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)) {
4145
+ 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)) {
4146
4146
  throw new Error("initial platform inventory coordinates are invalid");
4147
4147
  }
4148
4148
  safeAtom("initialInventory.region.label", value.region.label);
4149
+ if (value.metalIdentity !== undefined) {
4150
+ const identity = value.metalIdentity;
4151
+ 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))) {
4152
+ throw new Error("initial platform Metal identity is invalid");
4153
+ }
4154
+ }
4149
4155
  if (value.region.city !== undefined)
4150
4156
  safeAtom("initialInventory.region.city", value.region.city);
4151
4157
  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)))
@@ -4180,6 +4186,7 @@ function validatePlatformInitialInventory(value) {
4180
4186
  }
4181
4187
  return {
4182
4188
  metalHostname: value.metalHostname,
4189
+ ...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
4183
4190
  region: { ...value.region },
4184
4191
  computes: value.computes.map((compute, index) => ({
4185
4192
  ...computes[index],
@@ -4233,6 +4240,11 @@ function validatePlatformSharedEnvironment(input) {
4233
4240
  } else if (input.email !== undefined) {
4234
4241
  throw new Error("Bootstrap email provider must be smtp or jetemail.");
4235
4242
  }
4243
+ if (input.githubApp) {
4244
+ 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)) {
4245
+ throw new Error("GitHub App client id, app id or slug is malformed.");
4246
+ }
4247
+ }
4236
4248
  boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
4237
4249
  if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
4238
4250
  throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
@@ -4363,6 +4375,9 @@ function renderPlatformSharedEnvironment(input) {
4363
4375
  FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
4364
4376
  FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
4365
4377
  FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
4378
+ FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
4379
+ FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
4380
+ FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
4366
4381
  FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
4367
4382
  };
4368
4383
  return `# Generated by fz bootstrap platform. Non-secret coordinates only.
@@ -4374,6 +4389,9 @@ function platformApiCredentialSpecs(options) {
4374
4389
  const optional = [
4375
4390
  ["fz_smtp.password", options.emailProvider === "smtp"],
4376
4391
  ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
4392
+ ["fz_github.clientSecret", options.githubApp],
4393
+ ["fz_github.privateKey", options.githubApp],
4394
+ ["fz_github.webhookSecret", options.githubApp],
4377
4395
  ["CF_API_TOKEN", options.cloudflareKv],
4378
4396
  ["CF_TUNNEL_TOKEN", options.cloudflareKv],
4379
4397
  ["REALTIME_PUBLISH_SECRET", options.realtime],
@@ -4727,7 +4745,7 @@ function planLocalOtlpProof(endpoint, collectorUnit) {
4727
4745
  import { lstatSync as lstatSync5 } from "node:fs";
4728
4746
 
4729
4747
  // src/bootstrap.ts
4730
- import { createHash as createHash7, createHmac as createHmac2, randomBytes as randomBytes3 } from "node:crypto";
4748
+ import { createHash as createHash7, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes3 } from "node:crypto";
4731
4749
  import {
4732
4750
  chmodSync as chmodSync9,
4733
4751
  existsSync as existsSync11,
@@ -15,4 +15,4 @@ export declare function platformGenesisBootstrapConfigs(template: PlatformBootst
15
15
  branch: 'dev' | 'main';
16
16
  revision: string;
17
17
  bundleSha256: string;
18
- }, attestation?: import('./platform-bootstrap-runtime').PlatformInitialInventory['attestation']): readonly PlatformBootstrapConfig[];
18
+ }, attestation?: import('./platform-bootstrap-runtime').PlatformInitialInventory['attestation'], metalIdentity?: NonNullable<import('./platform-bootstrap-runtime').PlatformInitialInventory['metalIdentity']>): readonly PlatformBootstrapConfig[];
@@ -45,6 +45,11 @@ export interface ForgeZeroLaunchOwnerInput {
45
45
  from: string;
46
46
  eu: boolean;
47
47
  };
48
+ githubApp?: {
49
+ clientId: string;
50
+ appId: string;
51
+ slug: string;
52
+ };
48
53
  }
49
54
  /**
50
55
  * ForgeZero's own genesis public coordinates. They are reviewed source data,
package/dist/provision.js CHANGED
@@ -2908,7 +2908,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2908
2908
  }
2909
2909
 
2910
2910
  // src/version.ts
2911
- var VERSION3 = "0.1.102";
2911
+ var VERSION3 = "0.1.103";
2912
2912
 
2913
2913
  // src/egress-policy.ts
2914
2914
  import { realpathSync as realpathSync3 } from "node:fs";
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** One package version shared by both public binaries. Pinned to package.json by tests. */
2
- export declare const VERSION = "0.1.102";
2
+ export declare const VERSION = "0.1.103";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.102",
3
+ "version": "0.1.103",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",