@forgezero/agent 0.1.124 → 0.1.128

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.124";
920
+ var VERSION3 = "0.1.128";
921
921
 
922
922
  // src/agent-heartbeat.ts
923
923
  function readAgentHostMetrics() {
@@ -123,7 +123,9 @@ export interface EnrolledComputeBootstrapSecrets {
123
123
  }
124
124
  export type BootstrapSecrets = PlatformBootstrapSecrets | EnrolledComputeBootstrapSecrets;
125
125
  export declare function validateEnrolledComputeBootstrapSecrets(config: EnrolledComputeActivationConfig, input: unknown): EnrolledComputeBootstrapSecrets;
126
- export declare function validatePlatformBootstrapSecrets(config: PlatformBootstrapConfig, input: unknown): PlatformBootstrapSecrets;
126
+ export declare function validatePlatformBootstrapSecrets(config: PlatformBootstrapConfig, input: unknown, options?: {
127
+ allowStoredCloudflareCredentials?: boolean;
128
+ }): PlatformBootstrapSecrets;
127
129
  export interface BootstrapStep {
128
130
  id: string;
129
131
  label: string;
package/dist/bootstrap.js CHANGED
@@ -1460,7 +1460,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1460
1460
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1461
1461
 
1462
1462
  // src/version.ts
1463
- var VERSION = "0.1.124";
1463
+ var VERSION = "0.1.128";
1464
1464
 
1465
1465
  // src/software.ts
1466
1466
  var PINNED_BUN_VERSION = "1.3.14";
@@ -3805,7 +3805,7 @@ function validateEnrolledComputeBootstrapSecrets(config, input) {
3805
3805
  ...cloudflareConfigured ? validateCloudflareBootstrapSecretPair(source) : {}
3806
3806
  };
3807
3807
  }
3808
- function validatePlatformBootstrapSecrets(config, input) {
3808
+ function validatePlatformBootstrapSecrets(config, input, options = {}) {
3809
3809
  if (!input || typeof input !== "object" || Array.isArray(input))
3810
3810
  throw new Error("bootstrap credential input must be an object");
3811
3811
  const source = input;
@@ -3833,7 +3833,8 @@ function validatePlatformBootstrapSecrets(config, input) {
3833
3833
  throw new Error("backup credential is malformed");
3834
3834
  }
3835
3835
  const cloudflareConfigured = Boolean(config.cloudflareHandoff || config.runtime.environment.cloudflare);
3836
- if (cloudflareConfigured !== Boolean(cloudflareTunnelToken && cloudflareApiToken)) {
3836
+ const cloudflareCredentialsSupplied = Boolean(cloudflareTunnelToken || cloudflareApiToken);
3837
+ if (!cloudflareConfigured && cloudflareCredentialsSupplied || cloudflareConfigured && !cloudflareCredentialsSupplied && !options.allowStoredCloudflareCredentials || Boolean(cloudflareTunnelToken) !== Boolean(cloudflareApiToken)) {
3837
3838
  throw new Error("Cloudflare configuration requires attended CF_TUNNEL_TOKEN and CF_API_TOKEN together");
3838
3839
  }
3839
3840
  if (cloudflareTunnelToken)
@@ -4759,7 +4760,9 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
4759
4760
  const platformPrivate = config.kind === "platform" ? (() => {
4760
4761
  if (!secrets)
4761
4762
  throw new Error("platform apply requires attended credentials on stdin");
4762
- const checked4 = validatePlatformBootstrapSecrets(config, secrets);
4763
+ const checked4 = validatePlatformBootstrapSecrets(config, secrets, {
4764
+ allowStoredCloudflareCredentials: Boolean(installed?.cloudflare && config.cloudflareHandoff && !cloudflare)
4765
+ });
4763
4766
  return {
4764
4767
  root: checked4.clusterBootstrapCode,
4765
4768
  email: checked4.emailSecret,
@@ -9,6 +9,8 @@ export declare const COMMUNITY_REHEARSAL_COORDINATOR_PORT = 18529;
9
9
  export declare const COMMUNITY_REHEARSAL_DATABASE_PORT_RANGE = "18528:18539";
10
10
  export declare const COMMUNITY_REHEARSAL_API_PORT = 18787;
11
11
  export declare const COMMUNITY_REHEARSAL_FIREWALL_COMMENT = "fz-community-rehearsal";
12
+ export declare const COMMUNITY_REHEARSAL_EGRESS_TABLE = "forgezero_agent_egress";
13
+ export declare const COMMUNITY_REHEARSAL_EGRESS_CHAIN = "output";
12
14
  export interface CommunityRehearsalNode {
13
15
  name: string;
14
16
  address: string;
@@ -48,6 +50,11 @@ export type CommunityHostOperation = {
48
50
  path: typeof COMMUNITY_DATABASE_ROOT | typeof COMMUNITY_REHEARSAL_ROOT | typeof COMMUNITY_CREDENTIAL_PARENT;
49
51
  } | {
50
52
  kind: 'clear-firewall';
53
+ } | {
54
+ kind: 'prepare-egress';
55
+ addresses: readonly string[];
56
+ } | {
57
+ kind: 'clear-egress';
51
58
  } | {
52
59
  kind: 'write';
53
60
  path: string;
@@ -75,5 +82,7 @@ export declare function planCommunityRehearsalPrepare(request: Extract<Community
75
82
  /** Remove only the dedicated rehearsal units, credentials, release and data. */
76
83
  export declare function planCommunityRehearsalCleanup(): CommunityHostOperation[];
77
84
  export declare function communityRehearsalFirewallRuleNumbers(status: string): number[];
85
+ export declare function communityRehearsalEgressRuleHandles(ruleset: string): number[];
86
+ export declare function communityRehearsalEgressRuleArguments(uid: string, addresses: readonly string[]): readonly (readonly string[])[];
78
87
  /** Execute only the dedicated four-node rehearsal operation selected by a typed request. */
79
88
  export declare function runCommunityRehearsalHost(request: CommunityRehearsalHostRequest): Promise<Record<string, unknown>>;
@@ -577,6 +577,8 @@ var COMMUNITY_REHEARSAL_COORDINATOR_PORT = 18529;
577
577
  var COMMUNITY_REHEARSAL_DATABASE_PORT_RANGE = "18528:18539";
578
578
  var COMMUNITY_REHEARSAL_API_PORT = 18787;
579
579
  var COMMUNITY_REHEARSAL_FIREWALL_COMMENT = "fz-community-rehearsal";
580
+ var COMMUNITY_REHEARSAL_EGRESS_TABLE = "forgezero_agent_egress";
581
+ var COMMUNITY_REHEARSAL_EGRESS_CHAIN = "output";
580
582
  var RELEASE = /^[a-f0-9]{64}$/;
581
583
  var SAFE_NAME = /^[a-z][a-z0-9-]{0,62}$/;
582
584
  async function ensureCommunityRehearsalArango(execute = executeSoftwareOperation) {
@@ -740,6 +742,7 @@ function planCommunityRehearsalPrepare(request) {
740
742
  { kind: "exec", argv: ["/usr/bin/systemctl", "stop", "forgezero-community-api.service"], accepted: [0, 5] },
741
743
  { kind: "exec", argv: ["/usr/bin/systemctl", "stop", "forgezero-rehearsal-db.service"], accepted: [0, 5] },
742
744
  { kind: "clear-firewall" },
745
+ { kind: "prepare-egress", addresses: topology.map(({ address }) => address) },
743
746
  ...topology.map(({ address }) => ({
744
747
  kind: "exec",
745
748
  argv: [
@@ -802,6 +805,7 @@ function planCommunityRehearsalCleanup() {
802
805
  { kind: "remove-tree", path: COMMUNITY_DATABASE_ROOT },
803
806
  { kind: "remove-tree", path: COMMUNITY_REHEARSAL_ROOT },
804
807
  { kind: "remove-tree", path: COMMUNITY_CREDENTIAL_PARENT },
808
+ { kind: "clear-egress" },
805
809
  { kind: "clear-firewall" },
806
810
  { kind: "exec", argv: ["/usr/bin/systemctl", "daemon-reload"] },
807
811
  { kind: "exec", argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-community-api.service", "forgezero-rehearsal-db.service"], accepted: [0, 1, 5] }
@@ -824,6 +828,10 @@ async function applyOperations(operations) {
824
828
  rmSync2(operation.path, { recursive: true, force: true });
825
829
  else if (operation.kind === "clear-firewall")
826
830
  await clearCommunityRehearsalFirewall();
831
+ else if (operation.kind === "prepare-egress")
832
+ await prepareCommunityRehearsalEgress(operation.addresses);
833
+ else if (operation.kind === "clear-egress")
834
+ await clearCommunityRehearsalEgress();
827
835
  else if (operation.kind === "write") {
828
836
  mkdirSync2(dirname2(operation.path), { recursive: true });
829
837
  writeFileSync2(operation.path, operation.content, { mode: operation.mode });
@@ -854,6 +862,49 @@ function communityRehearsalFirewallRuleNumbers(status) {
854
862
  return match ? [Number(match[1])] : [];
855
863
  }).filter((number) => Number.isSafeInteger(number) && number > 0).sort((left, right) => right - left);
856
864
  }
865
+ function communityRehearsalEgressRuleHandles(ruleset) {
866
+ return ruleset.split(/\r?\n/).flatMap((line) => {
867
+ if (!line.includes(`comment "${COMMUNITY_REHEARSAL_FIREWALL_COMMENT}"`))
868
+ return [];
869
+ const match = line.match(/# handle (\d+)\s*$/);
870
+ return match ? [Number(match[1])] : [];
871
+ }).filter((number) => Number.isSafeInteger(number) && number > 0).sort((left, right) => right - left);
872
+ }
873
+ function communityRehearsalEgressRuleArguments(uid, addresses) {
874
+ if (!/^\d+$/.test(uid) || addresses.some((address) => isIP(address) !== 4) || new Set(addresses).size !== addresses.length) {
875
+ throw new Error("rehearsal egress arguments require one uid and unique IPv4 addresses");
876
+ }
877
+ return addresses.flatMap((address) => [
878
+ [
879
+ "meta",
880
+ "skuid",
881
+ uid,
882
+ "ct",
883
+ "state",
884
+ "established,related",
885
+ "ip",
886
+ "daddr",
887
+ address,
888
+ "accept",
889
+ "comment",
890
+ COMMUNITY_REHEARSAL_FIREWALL_COMMENT
891
+ ],
892
+ [
893
+ "meta",
894
+ "skuid",
895
+ uid,
896
+ "ip",
897
+ "daddr",
898
+ address,
899
+ "tcp",
900
+ "dport",
901
+ String(COMMUNITY_REHEARSAL_COORDINATOR_PORT),
902
+ "accept",
903
+ "comment",
904
+ COMMUNITY_REHEARSAL_FIREWALL_COMMENT
905
+ ]
906
+ ]);
907
+ }
857
908
  async function clearCommunityRehearsalFirewall() {
858
909
  const status = await exec(["/usr/sbin/ufw", "status", "numbered"]);
859
910
  if (status.exitCode !== 0)
@@ -865,6 +916,71 @@ async function clearCommunityRehearsalFirewall() {
865
916
  }
866
917
  }
867
918
  }
919
+ var rehearsalEgressRules = async () => {
920
+ const result = await exec([
921
+ "/usr/sbin/nft",
922
+ "-a",
923
+ "list",
924
+ "chain",
925
+ "inet",
926
+ COMMUNITY_REHEARSAL_EGRESS_TABLE,
927
+ COMMUNITY_REHEARSAL_EGRESS_CHAIN
928
+ ]);
929
+ if (result.exitCode === 0)
930
+ return { exists: true, output: result.output };
931
+ if (/No such file or directory|does not exist/i.test(result.output))
932
+ return { exists: false, output: result.output };
933
+ throw new Error(`unable to inspect rehearsal egress rules: ${result.output.trim()}`);
934
+ };
935
+ async function clearCommunityRehearsalEgress() {
936
+ const listed = await rehearsalEgressRules();
937
+ if (!listed.exists)
938
+ return;
939
+ for (const handle of communityRehearsalEgressRuleHandles(listed.output)) {
940
+ const removed = await exec([
941
+ "/usr/sbin/nft",
942
+ "delete",
943
+ "rule",
944
+ "inet",
945
+ COMMUNITY_REHEARSAL_EGRESS_TABLE,
946
+ COMMUNITY_REHEARSAL_EGRESS_CHAIN,
947
+ "handle",
948
+ String(handle)
949
+ ]);
950
+ if (removed.exitCode !== 0) {
951
+ throw new Error(`unable to remove rehearsal egress rule ${handle}: ${removed.output.trim()}`);
952
+ }
953
+ }
954
+ }
955
+ async function prepareCommunityRehearsalEgress(addresses) {
956
+ await clearCommunityRehearsalEgress();
957
+ if (!(await rehearsalEgressRules()).exists)
958
+ return;
959
+ const identity = await exec(["/usr/bin/id", "-u", "forgezero"]);
960
+ const uid = identity.output.trim();
961
+ if (identity.exitCode !== 0 || !/^\d+$/.test(uid))
962
+ throw new Error("unable to resolve the forgezero service uid");
963
+ for (const rule of communityRehearsalEgressRuleArguments(uid, addresses)) {
964
+ const inserted = await exec([
965
+ "/usr/sbin/nft",
966
+ "insert",
967
+ "rule",
968
+ "inet",
969
+ COMMUNITY_REHEARSAL_EGRESS_TABLE,
970
+ COMMUNITY_REHEARSAL_EGRESS_CHAIN,
971
+ ...rule
972
+ ]);
973
+ if (inserted.exitCode !== 0) {
974
+ await clearCommunityRehearsalEgress();
975
+ throw new Error(`unable to install a rehearsal egress rule: ${inserted.output.trim()}`);
976
+ }
977
+ }
978
+ const installed = communityRehearsalEgressRuleHandles((await rehearsalEgressRules()).output);
979
+ if (installed.length !== addresses.length * 2) {
980
+ await clearCommunityRehearsalEgress();
981
+ throw new Error("rehearsal egress rule verification failed");
982
+ }
983
+ }
868
984
  var responseFor = async (request) => {
869
985
  const node = request.topology.find((entry) => entry.name === request.node);
870
986
  const base = `http://${node.address}:${COMMUNITY_REHEARSAL_API_PORT}`;
@@ -953,10 +1069,14 @@ export {
953
1069
  parseCommunityRehearsalHostRequest,
954
1070
  ensureCommunityRehearsalArango,
955
1071
  communityRehearsalFirewallRuleNumbers,
1072
+ communityRehearsalEgressRuleHandles,
1073
+ communityRehearsalEgressRuleArguments,
956
1074
  COMMUNITY_REHEARSAL_VERSION,
957
1075
  COMMUNITY_REHEARSAL_STARTER_PORT,
958
1076
  COMMUNITY_REHEARSAL_ROOT,
959
1077
  COMMUNITY_REHEARSAL_FIREWALL_COMMENT,
1078
+ COMMUNITY_REHEARSAL_EGRESS_TABLE,
1079
+ COMMUNITY_REHEARSAL_EGRESS_CHAIN,
960
1080
  COMMUNITY_REHEARSAL_DATABASE_PORT_RANGE,
961
1081
  COMMUNITY_REHEARSAL_COORDINATOR_PORT,
962
1082
  COMMUNITY_REHEARSAL_API_PORT,