@forgezero/agent 0.1.103 → 0.1.108

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.
@@ -1384,9 +1384,49 @@ async function buildBootstrapBundle(input, exec = run) {
1384
1384
  throw cause;
1385
1385
  }
1386
1386
  }
1387
+ async function refreshBootstrapBundle(input, exec = run) {
1388
+ const outputPath = resolve2(input.outputPath);
1389
+ const manifestPath = `${outputPath}.json`;
1390
+ if (existsSync(outputPath) !== existsSync(manifestPath)) {
1391
+ throw new Error("bootstrap bundle and manifest must either both exist or both be absent");
1392
+ }
1393
+ const candidatePath = `${outputPath}.candidate.${process.pid}.${randomBytes(6).toString("hex")}`;
1394
+ const candidate = await buildBootstrapBundle({ ...input, outputPath: candidatePath }, exec);
1395
+ try {
1396
+ if (!existsSync(outputPath)) {
1397
+ renameSync(candidate.bundlePath, outputPath);
1398
+ renameSync(candidate.manifestPath, manifestPath);
1399
+ return readBootstrapBundle(outputPath);
1400
+ }
1401
+ const current = await readBootstrapBundle(outputPath);
1402
+ if (current.manifest.branch === candidate.manifest.branch && current.manifest.revision === candidate.manifest.revision) {
1403
+ rmSync(candidate.bundlePath, { force: true });
1404
+ rmSync(candidate.manifestPath, { force: true });
1405
+ return current;
1406
+ }
1407
+ const archivePath = `${outputPath}.before-${current.manifest.revision.slice(0, 7)}-${Date.now()}`;
1408
+ const archiveManifestPath = `${archivePath}.json`;
1409
+ renameSync(outputPath, archivePath);
1410
+ renameSync(manifestPath, archiveManifestPath);
1411
+ try {
1412
+ renameSync(candidate.bundlePath, outputPath);
1413
+ renameSync(candidate.manifestPath, manifestPath);
1414
+ return readBootstrapBundle(outputPath);
1415
+ } catch (cause) {
1416
+ rmSync(outputPath, { force: true });
1417
+ rmSync(manifestPath, { force: true });
1418
+ renameSync(archivePath, outputPath);
1419
+ renameSync(archiveManifestPath, manifestPath);
1420
+ throw cause;
1421
+ }
1422
+ } finally {
1423
+ rmSync(candidate.bundlePath, { force: true });
1424
+ rmSync(candidate.manifestPath, { force: true });
1425
+ }
1426
+ }
1387
1427
 
1388
1428
  // src/bootstrap.ts
1389
- import { createHash as createHash2, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes3 } from "crypto";
1429
+ import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
1390
1430
  import {
1391
1431
  chmodSync as chmodSync3,
1392
1432
  existsSync as existsSync5,
@@ -1420,7 +1460,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1420
1460
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1421
1461
 
1422
1462
  // src/version.ts
1423
- var VERSION = "0.1.103";
1463
+ var VERSION = "0.1.108";
1424
1464
 
1425
1465
  // src/software.ts
1426
1466
  var PINNED_BUN_VERSION = "1.3.14";
@@ -3188,10 +3228,8 @@ function validatePlatformSharedEnvironment(input) {
3188
3228
  } else if (input.email !== undefined) {
3189
3229
  throw new Error("Bootstrap email provider must be smtp or jetemail.");
3190
3230
  }
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
- }
3231
+ if (!input.githubOAuth || !/^(?:Iv1\.[A-Fa-f0-9]{16}|Ov23li[A-Za-z0-9]{14,})$/.test(input.githubOAuth.clientId)) {
3232
+ throw new Error("GitHub OAuth client id is malformed.");
3195
3233
  }
3196
3234
  boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
3197
3235
  if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
@@ -3323,9 +3361,7 @@ function renderPlatformSharedEnvironment(input) {
3323
3361
  FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
3324
3362
  FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
3325
3363
  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 ?? "",
3364
+ FZ_GITHUB_OAUTH_CLIENT_ID: value.githubOAuth.clientId,
3329
3365
  FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
3330
3366
  };
3331
3367
  return `# Generated by fz bootstrap platform. Non-secret coordinates only.
@@ -3337,9 +3373,7 @@ function platformApiCredentialSpecs(options) {
3337
3373
  const optional = [
3338
3374
  ["fz_smtp.password", options.emailProvider === "smtp"],
3339
3375
  ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
3340
- ["fz_github.clientSecret", options.githubApp],
3341
- ["fz_github.privateKey", options.githubApp],
3342
- ["fz_github.webhookSecret", options.githubApp],
3376
+ ["fz_oauth.github.clientSecret", options.githubOAuth],
3343
3377
  ["CF_API_TOKEN", options.cloudflareKv],
3344
3378
  ["CF_TUNNEL_TOKEN", options.cloudflareKv],
3345
3379
  ["REALTIME_PUBLISH_SECRET", options.realtime],
@@ -3347,6 +3381,7 @@ function platformApiCredentialSpecs(options) {
3347
3381
  ];
3348
3382
  return [
3349
3383
  { name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
3384
+ { name: "arangodb-root-password", encryptedPath: "/etc/forgezero/creds/arangodb-root-password.cred", required: true },
3350
3385
  { name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
3351
3386
  ...optional.filter(([, present]) => present).map(([name]) => ({
3352
3387
  name,
@@ -3742,9 +3777,7 @@ function validatePlatformBootstrapSecrets(config, input) {
3742
3777
  "backupS3Secret",
3743
3778
  "cloudflareTunnelToken",
3744
3779
  "cloudflareApiToken",
3745
- "githubClientSecret",
3746
- "githubPrivateKeyBase64",
3747
- "githubWebhookSecret"
3780
+ "githubOAuthClientSecret"
3748
3781
  ];
3749
3782
  const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
3750
3783
  if (unknown.length)
@@ -3755,9 +3788,7 @@ function validatePlatformBootstrapSecrets(config, input) {
3755
3788
  const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
3756
3789
  const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
3757
3790
  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;
3791
+ const githubOAuthClientSecret = typeof source.githubOAuthClientSecret === "string" ? source.githubOAuthClientSecret.trim() : "";
3761
3792
  if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
3762
3793
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
3763
3794
  if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
@@ -3777,21 +3808,8 @@ function validatePlatformBootstrapSecrets(config, input) {
3777
3808
  }
3778
3809
  if (cloudflareTunnelToken)
3779
3810
  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
- }
3811
+ if (githubOAuthClientSecret.length < 20 || githubOAuthClientSecret.length > 512 || /[\r\n\0]/.test(githubOAuthClientSecret)) {
3812
+ throw new Error("GitHub OAuth client secret is malformed");
3795
3813
  }
3796
3814
  return {
3797
3815
  clusterBootstrapCode,
@@ -3799,7 +3817,7 @@ function validatePlatformBootstrapSecrets(config, input) {
3799
3817
  ...enrolmentToken ? { enrolmentToken } : {},
3800
3818
  ...backupS3Secret ? { backupS3Secret } : {},
3801
3819
  ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {},
3802
- ...githubConfigured ? { githubClientSecret, githubPrivateKeyBase64, githubWebhookSecret } : {}
3820
+ githubOAuthClientSecret
3803
3821
  };
3804
3822
  }
3805
3823
  var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
@@ -3814,6 +3832,7 @@ var STATE_PATH = BOOTSTRAP_STATE_PATH;
3814
3832
  var INTENT_PATH = "/var/lib/forgezero/bootstrap.intent.json";
3815
3833
  var CREDS = "/etc/forgezero/creds";
3816
3834
  var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
3835
+ var ARANGO_ROOT_CREDENTIAL = `${CREDS}/arangodb-root-password.cred`;
3817
3836
  var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
3818
3837
  var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
3819
3838
  var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
@@ -4099,7 +4118,7 @@ WantedBy=multi-user.target
4099
4118
  function databaseVerifyUnit(config) {
4100
4119
  const { address } = config.database;
4101
4120
  return `[Unit]
4102
- Description=Verify ForgeZero ArangoDB Community 3.11.14 writable Coordinator
4121
+ Description=Secure and verify ForgeZero ArangoDB Community 3.11.14 writable Coordinator
4103
4122
  Requires=forgezero-db.service
4104
4123
  After=forgezero-db.service
4105
4124
  PartOf=forgezero-db.service
@@ -4109,7 +4128,9 @@ Type=oneshot
4109
4128
  User=arangodb
4110
4129
  Group=arangodb
4111
4130
  LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
4112
- 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));'
4131
+ LoadCredentialEncrypted=arangodb-root-password:${ARANGO_ROOT_CREDENTIAL}
4132
+ 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);'
4133
+ ExecStart=/usr/local/bin/fz-agent database-auth-verify --endpoint=http://${unitEscape(address)}:8529
4113
4134
  RemainAfterExit=yes
4114
4135
  TimeoutStartSec=200
4115
4136
  NoNewPrivileges=true
@@ -4576,6 +4597,11 @@ async function bootstrapStatus(host = localBootstrapHost()) {
4576
4597
  if (nginx.exitCode !== 0)
4577
4598
  problems.push("nginx configuration is invalid");
4578
4599
  if (state.databaseRole !== "none") {
4600
+ for (const credential of [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL]) {
4601
+ services[credential] = host.exists(credential);
4602
+ if (!services[credential])
4603
+ problems.push(`${credential} is missing`);
4604
+ }
4579
4605
  const unitPath = "/etc/systemd/system/forgezero-db.service";
4580
4606
  const expectsNoAgency = state.databaseAgency === "none";
4581
4607
  const unitHasNoAgency = host.exists(unitPath) && host.read(unitPath).includes("--cluster.start-agent=false");
@@ -4715,9 +4741,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4715
4741
  backup: checked4.backupS3Secret,
4716
4742
  cloudflareTunnelToken: checked4.cloudflareTunnelToken,
4717
4743
  cloudflareApiToken: checked4.cloudflareApiToken,
4718
- githubClientSecret: checked4.githubClientSecret,
4719
- githubPrivateKey: checked4.githubPrivateKeyBase64 ? Buffer.from(checked4.githubPrivateKeyBase64, "base64").toString("utf8") : undefined,
4720
- githubWebhookSecret: checked4.githubWebhookSecret
4744
+ githubOAuthClientSecret: checked4.githubOAuthClientSecret
4721
4745
  };
4722
4746
  })() : undefined;
4723
4747
  const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
@@ -4810,14 +4834,15 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4810
4834
  if (!host.exists(JWT_CREDENTIAL)) {
4811
4835
  await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
4812
4836
  }
4837
+ if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
4838
+ await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
4839
+ }
4813
4840
  await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
4814
4841
  await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
4815
4842
  const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
4816
4843
  for (const [name, source] of Object.entries({
4817
4844
  ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
4818
- "fz_github.clientSecret": platformPrivate.githubClientSecret,
4819
- "fz_github.privateKey": platformPrivate.githubPrivateKey,
4820
- "fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
4845
+ "fz_oauth.github.clientSecret": platformPrivate.githubOAuthClientSecret,
4821
4846
  "backup.s3.secretAccessKey": platformPrivate.backup
4822
4847
  })) {
4823
4848
  if (source) {
@@ -4834,7 +4859,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4834
4859
  const credentials = platformApiCredentialSpecs({
4835
4860
  emailProvider: runtime.environment.email?.provider,
4836
4861
  cloudflareKv: cloudflareConfigured,
4837
- githubApp: Boolean(runtime.environment.githubApp),
4862
+ githubOAuth: true,
4838
4863
  realtime: Boolean(runtime.environment.realtime)
4839
4864
  });
4840
4865
  const units = renderPlatformApiUnits({
@@ -5082,6 +5107,7 @@ function strictBootstrapDocument(value) {
5082
5107
  "agentOtlpEndpoint",
5083
5108
  "custodianEmail",
5084
5109
  "email",
5110
+ "githubOAuth",
5085
5111
  "deployProfile",
5086
5112
  "otlpFlushIntervalMs",
5087
5113
  "otlpTraceSampleRatio",
@@ -5092,8 +5118,13 @@ function strictBootstrapDocument(value) {
5092
5118
  "databaseReadPreferredCoordinators"
5093
5119
  ], "runtime environment");
5094
5120
  const environment = runtime.environment;
5121
+ exactKeys(environment.githubOAuth, ["clientId"], "GitHub OAuth config");
5095
5122
  if (environment.initialInventory !== undefined) {
5096
- const inventory = exactKeys(environment.initialInventory, ["metalHostname", "region", "computes", "attestation", "deployment"], "initial inventory");
5123
+ const inventory = exactKeys(environment.initialInventory, ["metalHostname", "metalIdentity", "region", "computes", "attestation", "deployment"], "initial inventory");
5124
+ if (inventory.metalIdentity !== undefined) {
5125
+ const identity = exactKeys(inventory.metalIdentity, ["nodeKey", "publicKeys"], "initial Metal identity");
5126
+ exactKeys(identity.publicKeys, ["ed25519", "mlDsa"], "initial Metal public keys");
5127
+ }
5097
5128
  exactKeys(inventory.region, ["key", "label", "country", "city", "confidentialCapable"], "initial inventory region");
5098
5129
  if (inventory.attestation !== undefined) {
5099
5130
  const attestation = exactKeys(inventory.attestation, ["measurement", "tcbFloor"], "initial attestation evidence");
@@ -73,10 +73,8 @@ export type PlatformBootstrapEmail = {
73
73
  * webhook secret are attended inputs sealed as systemd credentials and later
74
74
  * imported into the unlocked platform Vault.
75
75
  */
76
- export interface PlatformBootstrapGitHubApp {
76
+ export interface PlatformBootstrapGitHubOAuth {
77
77
  clientId: string;
78
- appId: string;
79
- slug: string;
80
78
  }
81
79
  export interface PlatformSharedEnvironment {
82
80
  softwareProfile: PlatformSoftwareProfile;
@@ -109,7 +107,7 @@ export interface PlatformSharedEnvironment {
109
107
  otlpTraceSampleRatio: number;
110
108
  custodianEmail?: string;
111
109
  email?: PlatformBootstrapEmail;
112
- githubApp?: PlatformBootstrapGitHubApp;
110
+ githubOAuth: PlatformBootstrapGitHubOAuth;
113
111
  backup?: {
114
112
  endpoint: string;
115
113
  region: string;
@@ -137,13 +135,13 @@ export declare function validatePlatformSharedEnvironment(input: PlatformSharedE
137
135
  /** Render only non-secret runtime coordinates. Passwords/tokens have no field in this contract. */
138
136
  export declare function renderPlatformSharedEnvironment(input: PlatformSharedEnvironment): string;
139
137
  export interface SystemdCredentialSpec {
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';
138
+ name: 'arangodb-jwt' | 'arangodb-root-password' | 'seed-sync-root' | 'fz_smtp.password' | 'fz_jetemail.apiKey' | 'fz_oauth.github.clientSecret' | 'CF_API_TOKEN' | 'CF_TUNNEL_TOKEN' | 'REALTIME_PUBLISH_SECRET' | 'REALTIME_TICKET_SECRET';
141
139
  encryptedPath: string;
142
140
  required: boolean;
143
141
  }
144
142
  export declare function platformApiCredentialSpecs(options: {
145
143
  emailProvider?: PlatformBootstrapEmail['provider'];
146
- githubApp: boolean;
144
+ githubOAuth: boolean;
147
145
  cloudflareKv: boolean;
148
146
  realtime: boolean;
149
147
  }): SystemdCredentialSpec[];
@@ -828,10 +828,8 @@ function validatePlatformSharedEnvironment(input) {
828
828
  } else if (input.email !== undefined) {
829
829
  throw new Error("Bootstrap email provider must be smtp or jetemail.");
830
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
- }
831
+ if (!input.githubOAuth || !/^(?:Iv1\.[A-Fa-f0-9]{16}|Ov23li[A-Za-z0-9]{14,})$/.test(input.githubOAuth.clientId)) {
832
+ throw new Error("GitHub OAuth client id is malformed.");
835
833
  }
836
834
  boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
837
835
  if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
@@ -963,9 +961,7 @@ function renderPlatformSharedEnvironment(input) {
963
961
  FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
964
962
  FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
965
963
  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 ?? "",
964
+ FZ_GITHUB_OAUTH_CLIENT_ID: value.githubOAuth.clientId,
969
965
  FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
970
966
  };
971
967
  return `# Generated by fz bootstrap platform. Non-secret coordinates only.
@@ -977,9 +973,7 @@ function platformApiCredentialSpecs(options) {
977
973
  const optional = [
978
974
  ["fz_smtp.password", options.emailProvider === "smtp"],
979
975
  ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
980
- ["fz_github.clientSecret", options.githubApp],
981
- ["fz_github.privateKey", options.githubApp],
982
- ["fz_github.webhookSecret", options.githubApp],
976
+ ["fz_oauth.github.clientSecret", options.githubOAuth],
983
977
  ["CF_API_TOKEN", options.cloudflareKv],
984
978
  ["CF_TUNNEL_TOKEN", options.cloudflareKv],
985
979
  ["REALTIME_PUBLISH_SECRET", options.realtime],
@@ -987,6 +981,7 @@ function platformApiCredentialSpecs(options) {
987
981
  ];
988
982
  return [
989
983
  { name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
984
+ { name: "arangodb-root-password", encryptedPath: "/etc/forgezero/creds/arangodb-root-password.cred", required: true },
990
985
  { name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
991
986
  ...optional.filter(([, present]) => present).map(([name]) => ({
992
987
  name,
@@ -1869,7 +1869,7 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
1869
1869
  // src/deployment-connectivity.ts
1870
1870
  import { createHash as createHash3 } from "node:crypto";
1871
1871
  import { mkdirSync as mkdirSync4, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "node:fs";
1872
- import { createConnection } from "node:net";
1872
+ import { createConnection, isIP } from "node:net";
1873
1873
  import { dirname as dirname5 } from "node:path";
1874
1874
 
1875
1875
  // src/process-input.ts
@@ -1956,21 +1956,99 @@ function validate(request) {
1956
1956
  }
1957
1957
  } else if (request.capabilities.private)
1958
1958
  throw new Error("unexpected private deployment capability");
1959
+ if (request.firewallPolicy) {
1960
+ const policy = request.firewallPolicy;
1961
+ if (!/^sha256:[a-f0-9]{64}$/.test(policy.generation) || !Array.isArray(policy.rules) || policy.rules.length > 256) {
1962
+ throw new Error("deployment firewall policy is malformed");
1963
+ }
1964
+ let expanded = 0;
1965
+ for (const rule of policy.rules) {
1966
+ expanded += rule.sourceAddresses.length;
1967
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(rule.ruleKey) || !["allow", "deny"].includes(rule.action) || !["tcp", "udp"].includes(rule.protocol) || rule.sourceAddresses.length < 1 || rule.sourceAddresses.length > 1024 || new Set(rule.sourceAddresses).size !== rule.sourceAddresses.length || rule.sourceAddresses.some((address) => isIP(address) !== 4) || !Number.isSafeInteger(rule.portFrom) || rule.portFrom < 1 || rule.portFrom > 65535 || !Number.isSafeInteger(rule.portTo) || rule.portTo < rule.portFrom || rule.portTo > 65535 || !Number.isSafeInteger(rule.priority) || rule.priority < 0 || rule.priority > 1e6) {
1968
+ throw new Error("deployment firewall policy is malformed");
1969
+ }
1970
+ }
1971
+ if (expanded > 2048)
1972
+ throw new Error("deployment firewall policy expands beyond its rule limit");
1973
+ }
1974
+ }
1975
+ async function replaceTaggedUfwRules(host, comment) {
1976
+ const status = await checked2(host, ["/usr/sbin/ufw", "status", "numbered"], "firewall inventory");
1977
+ const numbers = status.split(`
1978
+ `).flatMap((line) => {
1979
+ if (!line.includes(comment))
1980
+ return [];
1981
+ const match = line.match(/^\s*\[\s*(\d{1,6})\]/);
1982
+ return match ? [Number(match[1])] : [];
1983
+ }).filter((value) => Number.isSafeInteger(value) && value > 0).sort((left, right) => right - left);
1984
+ for (const number of numbers)
1985
+ await checked2(host, ["/usr/sbin/ufw", "--force", "delete", String(number)], `stale firewall rule ${number}`);
1959
1986
  }
1960
1987
  async function applyDeploymentConnectivity(request, host = defaultHost) {
1961
1988
  validate(request);
1962
1989
  const id = idFor(request.key);
1963
1990
  const evidence = { key: request.key };
1991
+ if (request.firewallPolicy) {
1992
+ const policy = request.firewallPolicy;
1993
+ const comment = `fz-policy-${id}`;
1994
+ await replaceTaggedUfwRules(host, comment);
1995
+ const ordered = [...policy.rules].sort((left, right) => left.priority - right.priority || (left.action === right.action ? left.ruleKey.localeCompare(right.ruleKey) : left.action === "deny" ? -1 : 1));
1996
+ const commands = [];
1997
+ for (const rule of ordered)
1998
+ for (const source of rule.sourceAddresses)
1999
+ commands.push([
2000
+ "/usr/sbin/ufw",
2001
+ "insert",
2002
+ "1",
2003
+ rule.action,
2004
+ "from",
2005
+ source,
2006
+ "to",
2007
+ "any",
2008
+ "port",
2009
+ rule.portFrom === rule.portTo ? String(rule.portFrom) : `${rule.portFrom}:${rule.portTo}`,
2010
+ "proto",
2011
+ rule.protocol,
2012
+ "comment",
2013
+ comment
2014
+ ]);
2015
+ const ranges = new Map;
2016
+ for (const rule of ordered)
2017
+ ranges.set(`${rule.protocol}:${rule.portFrom}:${rule.portTo}`, rule);
2018
+ for (const range of ranges.values())
2019
+ commands.push([
2020
+ "/usr/sbin/ufw",
2021
+ "insert",
2022
+ "1",
2023
+ "deny",
2024
+ "to",
2025
+ "any",
2026
+ "port",
2027
+ range.portFrom === range.portTo ? String(range.portFrom) : `${range.portFrom}:${range.portTo}`,
2028
+ "proto",
2029
+ range.protocol,
2030
+ "comment",
2031
+ comment
2032
+ ]);
2033
+ for (const command2 of commands.toReversed())
2034
+ await checked2(host, command2, "deployment firewall rule");
2035
+ evidence.firewall = { generation: policy.generation, rules: commands.length, active: true };
2036
+ }
1964
2037
  const topology = request.topology;
1965
2038
  if (topology) {
1966
- const values = [topology.localRelayAddress, ...topology.localPeerAddresses, ...topology.remoteRelayAddresses];
2039
+ const values = [
2040
+ topology.localRelayAddress,
2041
+ ...topology.localPeerAddresses,
2042
+ ...topology.remoteRelayAddresses,
2043
+ ...topology.remoteMemberAddresses
2044
+ ];
1967
2045
  const identities = [
1968
2046
  topology.nodeIdentity,
1969
2047
  ...topology.localPeerIdentities,
1970
2048
  ...topology.remoteRelayIdentities,
1971
2049
  ...topology.memberIdentities
1972
2050
  ];
1973
- if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => !/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) || identities.some((value) => !/^[A-Za-z0-9.-]{1,253}$/.test(value)) || topology.localPeerIdentities.length !== topology.localPeerAddresses.length || topology.remoteRelayIdentities.length !== topology.remoteRelayAddresses.length || topology.memberIdentities.length < 1 || topology.memberIdentities.length > 1024 || new Set(topology.memberIdentities).size !== topology.memberIdentities.length || !topology.memberIdentities.includes(topology.nodeIdentity) || !/^sha256:[a-f0-9]{64}$/.test(topology.generation) || (topology.role === "member" ? topology.remoteRelayAddresses.length !== 0 : topology.remoteSiteCidrs.length !== topology.remoteRelayAddresses.length) || topology.remoteSiteCidrs.some((value) => !/^(?:\d{1,3}\.){3}0\/24$/.test(value)) || new Set(topology.remoteSiteCidrs).size !== topology.remoteSiteCidrs.length || !Number.isSafeInteger(topology.healthPort) || topology.healthPort < 1 || topology.healthPort > 65535 || topology.routedTcpPorts.length < 1 || topology.routedTcpPorts.length > 64 || new Set(topology.routedTcpPorts).size !== topology.routedTcpPorts.length || !topology.routedTcpPorts.includes(topology.healthPort) || topology.routedTcpPorts.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535) || topology.role === "member" && topology.transport !== "private-lan") {
2051
+ if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => isIP(value) !== 4) || identities.some((value) => !/^[A-Za-z0-9.-]{1,253}$/.test(value)) || topology.localPeerIdentities.length !== topology.localPeerAddresses.length || topology.remoteRelayIdentities.length !== topology.remoteRelayAddresses.length || topology.remoteMemberAddresses.length > 1024 || new Set(topology.remoteMemberAddresses).size !== topology.remoteMemberAddresses.length || topology.memberIdentities.length < 1 || topology.memberIdentities.length > 1024 || new Set(topology.memberIdentities).size !== topology.memberIdentities.length || !topology.memberIdentities.includes(topology.nodeIdentity) || !/^sha256:[a-f0-9]{64}$/.test(topology.generation) || (topology.role === "member" ? topology.remoteRelayAddresses.length !== 0 : topology.remoteSiteCidrs.length !== topology.remoteRelayAddresses.length) || topology.remoteSiteCidrs.some((value) => !/^(?:\d{1,3}\.){3}0\/24$/.test(value)) || new Set(topology.remoteSiteCidrs).size !== topology.remoteSiteCidrs.length || !Number.isSafeInteger(topology.healthPort) || topology.healthPort < 1 || topology.healthPort > 65535 || topology.routedTcpPorts.length < 1 || topology.routedTcpPorts.length > 64 || new Set(topology.routedTcpPorts).size !== topology.routedTcpPorts.length || !topology.routedTcpPorts.includes(topology.healthPort) || topology.routedTcpPorts.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535) || topology.role === "member" && topology.transport !== "private-lan") {
1974
2052
  throw new Error("deployment topology is malformed");
1975
2053
  }
1976
2054
  if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
@@ -2097,28 +2175,46 @@ ${forwarding}${noOp}${starts}${starts ? `
2097
2175
  [Install]
2098
2176
  WantedBy=multi-user.target
2099
2177
  `, 420);
2100
- for (const source of [...new Set([topology.siteCidr, ...topology.remoteSiteCidrs])]) {
2178
+ const firewallComment = `fz-topology-${id}`;
2179
+ await replaceTaggedUfwRules(host, firewallComment);
2180
+ for (const source of [...new Set([...topology.localPeerAddresses, ...topology.remoteMemberAddresses])]) {
2101
2181
  for (const port of topology.routedTcpPorts) {
2102
- await checked2(host, ["/usr/sbin/ufw", "allow", "from", source, "to", "any", "port", String(port), "proto", "tcp"], `private service ${source}:${port}`);
2182
+ await checked2(host, [
2183
+ "/usr/sbin/ufw",
2184
+ "allow",
2185
+ "from",
2186
+ source,
2187
+ "to",
2188
+ "any",
2189
+ "port",
2190
+ String(port),
2191
+ "proto",
2192
+ "tcp",
2193
+ "comment",
2194
+ firewallComment
2195
+ ], `private service ${source}:${port}`);
2103
2196
  }
2104
2197
  }
2105
2198
  if (topology.role !== "member")
2106
- for (const remoteCidr of topology.remoteSiteCidrs) {
2107
- for (const port of topology.routedTcpPorts) {
2108
- await checked2(host, [
2109
- "/usr/sbin/ufw",
2110
- "route",
2111
- "allow",
2112
- "proto",
2113
- "tcp",
2114
- "from",
2115
- topology.siteCidr,
2116
- "to",
2117
- remoteCidr,
2118
- "port",
2119
- String(port)
2120
- ], `private routed service ${topology.siteCidr}->${remoteCidr}:${port}`);
2121
- }
2199
+ for (const remoteAddress of topology.remoteMemberAddresses) {
2200
+ for (const source of topology.localPeerAddresses)
2201
+ for (const port of topology.routedTcpPorts) {
2202
+ await checked2(host, [
2203
+ "/usr/sbin/ufw",
2204
+ "route",
2205
+ "allow",
2206
+ "proto",
2207
+ "tcp",
2208
+ "from",
2209
+ source,
2210
+ "to",
2211
+ remoteAddress,
2212
+ "port",
2213
+ String(port),
2214
+ "comment",
2215
+ firewallComment
2216
+ ], `private routed service ${source}->${remoteAddress}:${port}`);
2217
+ }
2122
2218
  }
2123
2219
  await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
2124
2220
  await checked2(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
@@ -2908,7 +3004,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2908
3004
  }
2909
3005
 
2910
3006
  // src/version.ts
2911
- var VERSION3 = "0.1.103";
3007
+ var VERSION3 = "0.1.108";
2912
3008
 
2913
3009
  // src/egress-policy.ts
2914
3010
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -2971,7 +3067,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
2971
3067
  }
2972
3068
 
2973
3069
  // src/provision.ts
2974
- import { isIP } from "node:net";
3070
+ import { isIP as isIP2 } from "node:net";
2975
3071
  function atLeast(version, floor) {
2976
3072
  const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
2977
3073
  const got = parse(version);
@@ -3451,7 +3547,7 @@ function agentUnit(options) {
3451
3547
  throw new Error("compute telemetry endpoint must be an absolute collector URL");
3452
3548
  }
3453
3549
  const localCollector = endpoint.protocol === "http:" && endpoint.hostname === "127.0.0.1" && endpoint.port === "4318" && endpoint.pathname === "/";
3454
- const publicCollector = endpoint.protocol === "https:" && !endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash && isIP(endpoint.hostname) === 0 && endpoint.hostname.includes(".") && endpoint.hostname !== "localhost" && !endpoint.hostname.endsWith(".local");
3550
+ const publicCollector = endpoint.protocol === "https:" && !endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash && isIP2(endpoint.hostname) === 0 && endpoint.hostname.includes(".") && endpoint.hostname !== "localhost" && !endpoint.hostname.endsWith(".local");
3455
3551
  if (!localCollector && !publicCollector || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
3456
3552
  throw new Error("compute telemetry endpoint must be the supervised loopback collector or credential-free public HTTPS");
3457
3553
  }
@@ -4240,10 +4336,8 @@ function validatePlatformSharedEnvironment(input) {
4240
4336
  } else if (input.email !== undefined) {
4241
4337
  throw new Error("Bootstrap email provider must be smtp or jetemail.");
4242
4338
  }
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
- }
4339
+ if (!input.githubOAuth || !/^(?:Iv1\.[A-Fa-f0-9]{16}|Ov23li[A-Za-z0-9]{14,})$/.test(input.githubOAuth.clientId)) {
4340
+ throw new Error("GitHub OAuth client id is malformed.");
4247
4341
  }
4248
4342
  boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
4249
4343
  if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
@@ -4375,9 +4469,7 @@ function renderPlatformSharedEnvironment(input) {
4375
4469
  FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
4376
4470
  FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
4377
4471
  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 ?? "",
4472
+ FZ_GITHUB_OAUTH_CLIENT_ID: value.githubOAuth.clientId,
4381
4473
  FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
4382
4474
  };
4383
4475
  return `# Generated by fz bootstrap platform. Non-secret coordinates only.
@@ -4389,9 +4481,7 @@ function platformApiCredentialSpecs(options) {
4389
4481
  const optional = [
4390
4482
  ["fz_smtp.password", options.emailProvider === "smtp"],
4391
4483
  ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
4392
- ["fz_github.clientSecret", options.githubApp],
4393
- ["fz_github.privateKey", options.githubApp],
4394
- ["fz_github.webhookSecret", options.githubApp],
4484
+ ["fz_oauth.github.clientSecret", options.githubOAuth],
4395
4485
  ["CF_API_TOKEN", options.cloudflareKv],
4396
4486
  ["CF_TUNNEL_TOKEN", options.cloudflareKv],
4397
4487
  ["REALTIME_PUBLISH_SECRET", options.realtime],
@@ -4399,6 +4489,7 @@ function platformApiCredentialSpecs(options) {
4399
4489
  ];
4400
4490
  return [
4401
4491
  { name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
4492
+ { name: "arangodb-root-password", encryptedPath: "/etc/forgezero/creds/arangodb-root-password.cred", required: true },
4402
4493
  { name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
4403
4494
  ...optional.filter(([, present]) => present).map(([name]) => ({
4404
4495
  name,
@@ -4745,7 +4836,7 @@ function planLocalOtlpProof(endpoint, collectorUnit) {
4745
4836
  import { lstatSync as lstatSync5 } from "node:fs";
4746
4837
 
4747
4838
  // src/bootstrap.ts
4748
- import { createHash as createHash7, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes3 } from "node:crypto";
4839
+ import { createHash as createHash7, createHmac as createHmac2, randomBytes as randomBytes3 } from "node:crypto";
4749
4840
  import {
4750
4841
  chmodSync as chmodSync9,
4751
4842
  existsSync as existsSync11,
@@ -5078,10 +5169,10 @@ import { constants } from "node:fs";
5078
5169
  import { createHmac, randomUUID as randomUUID4 } from "node:crypto";
5079
5170
  import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "node:fs/promises";
5080
5171
  import { dirname as dirname10, join as join6, resolve as resolve5 } from "node:path";
5081
- import { isIP as isIP3 } from "node:net";
5172
+ import { isIP as isIP4 } from "node:net";
5082
5173
 
5083
5174
  // src/cloudflare-edge.ts
5084
- import { isIP as isIP2 } from "node:net";
5175
+ import { isIP as isIP3 } from "node:net";
5085
5176
 
5086
5177
  // src/otel-collector.ts
5087
5178
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -5146,6 +5237,7 @@ var BOOTSTRAP_STATE_PATH = "/var/lib/forgezero/bootstrap.json";
5146
5237
  var STATE_PATH = BOOTSTRAP_STATE_PATH;
5147
5238
  var CREDS = "/etc/forgezero/creds";
5148
5239
  var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
5240
+ var ARANGO_ROOT_CREDENTIAL = `${CREDS}/arangodb-root-password.cred`;
5149
5241
  var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
5150
5242
  var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
5151
5243
  var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
@@ -5307,6 +5399,11 @@ async function bootstrapStatus(host = localBootstrapHost()) {
5307
5399
  if (nginx3.exitCode !== 0)
5308
5400
  problems.push("nginx configuration is invalid");
5309
5401
  if (state.databaseRole !== "none") {
5402
+ for (const credential of [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL]) {
5403
+ services[credential] = host.exists(credential);
5404
+ if (!services[credential])
5405
+ problems.push(`${credential} is missing`);
5406
+ }
5310
5407
  const unitPath = "/etc/systemd/system/forgezero-db.service";
5311
5408
  const expectsNoAgency = state.databaseAgency === "none";
5312
5409
  const unitHasNoAgency = host.exists(unitPath) && host.read(unitPath).includes("--cluster.start-agent=false");
@@ -0,0 +1,17 @@
1
+ import type { ForgeZeroLaunchOwnerInput } from './platform-launch-profile';
2
+ export interface PlatformLaunchEnvironmentInput {
3
+ owner: ForgeZeroLaunchOwnerInput;
4
+ emailSecret: string;
5
+ cloudflareTunnelToken: string;
6
+ cloudflareApiToken: string;
7
+ githubOAuthClientSecret: string;
8
+ }
9
+ /**
10
+ * Read the development-only attended-launch adapter.
11
+ *
12
+ * The file may contain the API's ordinary non-secret environment as well; this
13
+ * reader selects only the fixed bootstrap coordinates below. Secret values are
14
+ * returned to the caller for immediate systemd-creds sealing and are never
15
+ * copied into the API environment rendered on a target.
16
+ */
17
+ export declare function readPlatformLaunchEnvironment(path: string): PlatformLaunchEnvironmentInput;