@elgora/cli 7.4.0 → 7.5.0

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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +144 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,6 +21,10 @@ Or run one command without installing:
21
21
  npx @elgora/cli --help
22
22
  ```
23
23
 
24
+ Every command refuses to start on a Node outside the `engines` range the
25
+ package declares, and says which range it needs. `--version` and `help` still
26
+ answer.
27
+
24
28
  ## Commands
25
29
 
26
30
  - `poster:publish-fund` publishes approved `bounty_challenge.md` bytes and
package/dist/cli.js CHANGED
@@ -1142,8 +1142,12 @@ async function openAesKeyForRecipient(input) {
1142
1142
  }
1143
1143
  }
1144
1144
  if (!matched) {
1145
+ const wanted = [...new Set(wrappedKeys.map((key) => key.key_id))].sort();
1146
+ const configured = [
1147
+ ...new Set(privateKeys.map((key) => key.key_id))
1148
+ ].sort();
1145
1149
  throw new Error(
1146
- "no matching Guardian private key for private artifact envelope"
1150
+ `no matching Guardian private key for private artifact envelope: this envelope was sealed for key_id ${JSON.stringify(wanted)}, but ELGORA_GUARDIAN_PRIVATE_KEYS_JSON configures key_id ${JSON.stringify(configured)}. key_id must be this Guardian's own registered roster name (the name passed to roster:add-guardian), not its account address.`
1147
1151
  );
1148
1152
  }
1149
1153
  throw new Error("matching Guardian private key could not open wrapped key", {
@@ -6902,6 +6906,70 @@ function resolveChainId(selector) {
6902
6906
  );
6903
6907
  }
6904
6908
 
6909
+ // src/version.ts
6910
+ var CLI_VERSION = true ? "7.5.0" : "0.0.0-dev";
6911
+ var CLI_NODE_ENGINES = true ? ">=24 <25" : ">=24 <25";
6912
+
6913
+ // src/node-engines.ts
6914
+ function unsupportedNodeMessage(running = process.versions.node, range = CLI_NODE_ENGINES) {
6915
+ if (satisfiesRange(running, range)) return null;
6916
+ return `elgora-cli needs Node ${range}; this is Node ${running}. Install a Node inside that range and run the command again. --version and help still answer.`;
6917
+ }
6918
+ function satisfiesRange(version, range) {
6919
+ const running = parseVersion(version);
6920
+ if (!running) return false;
6921
+ return range.split("||").some(
6922
+ (alternative) => alternative.trim().split(/\s+/).filter(Boolean).every((comparator) => satisfiesComparator(running, comparator))
6923
+ );
6924
+ }
6925
+ function satisfiesComparator(running, comparator) {
6926
+ const match = /^(>=|<=|>|<|=|\^|~)?v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?$/.exec(
6927
+ comparator
6928
+ );
6929
+ if (!match) return false;
6930
+ const operator = match[1] ?? "=";
6931
+ const major = Number(match[2]);
6932
+ const minorRaw = match[3];
6933
+ const patchRaw = match[4];
6934
+ const wildcard = minorRaw === void 0 || minorRaw === "x" || minorRaw === "*" || patchRaw === void 0 || patchRaw === "x" || patchRaw === "*";
6935
+ const bound = [
6936
+ major,
6937
+ minorRaw && !["x", "*"].includes(minorRaw) ? Number(minorRaw) : 0,
6938
+ patchRaw && !["x", "*"].includes(patchRaw) ? Number(patchRaw) : 0
6939
+ ];
6940
+ const order = compareVersions(running, bound);
6941
+ switch (operator) {
6942
+ case ">":
6943
+ return order > 0;
6944
+ case ">=":
6945
+ return order >= 0;
6946
+ case "<":
6947
+ return order < 0;
6948
+ case "<=":
6949
+ return order <= 0;
6950
+ case "^":
6951
+ return order >= 0 && running[0] === major;
6952
+ case "~": {
6953
+ const anyMinor = minorRaw === void 0 || minorRaw === "x" || minorRaw === "*";
6954
+ return order >= 0 && running[0] === major && (anyMinor || running[1] === bound[1]);
6955
+ }
6956
+ default:
6957
+ return wildcard ? running[0] === major && (minorRaw === void 0 || ["x", "*"].includes(minorRaw) || running[1] === bound[1]) : order === 0;
6958
+ }
6959
+ }
6960
+ function parseVersion(version) {
6961
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
6962
+ if (!match) return null;
6963
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
6964
+ }
6965
+ function compareVersions(left, right) {
6966
+ for (let index = 0; index < 3; index += 1) {
6967
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
6968
+ if (difference !== 0) return difference;
6969
+ }
6970
+ return 0;
6971
+ }
6972
+
6905
6973
  // src/poster-open-winning-submission.ts
6906
6974
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
6907
6975
  import path2 from "path";
@@ -7276,6 +7344,11 @@ function loadPublishFundConfig(env, apiBaseUrlOverride) {
7276
7344
  };
7277
7345
  }
7278
7346
 
7347
+ // src/required-env.ts
7348
+ function missingRequiredEnv(required4, env) {
7349
+ return required4.filter((envName) => !env[envName]?.trim());
7350
+ }
7351
+
7279
7352
  // src/roster-add-guardian.ts
7280
7353
  import { isAddress as isAddress4 } from "viem";
7281
7354
  import { privateKeyToAccount as privateKeyToAccount9 } from "viem/accounts";
@@ -7312,6 +7385,11 @@ async function main10(env = process.env, argv = process.argv.slice(2)) {
7312
7385
  `${args.guardianAccount} is already on the Guardian roster`
7313
7386
  );
7314
7387
  }
7388
+ if (roster.some((guardian) => guardian.name === args.name)) {
7389
+ throw new Error(
7390
+ `"${args.name}" is already the name of a Guardian on the roster; names must be unique`
7391
+ );
7392
+ }
7315
7393
  const contractArgs = [
7316
7394
  args.name,
7317
7395
  args.guardianAccount,
@@ -7395,6 +7473,14 @@ function parseRosterAddGuardianArgs(argv) {
7395
7473
  const guardianAccount = positional[1]?.trim();
7396
7474
  const encryptionPublicKey = positional[2]?.trim();
7397
7475
  if (!name) throw usageError2();
7476
+ if (!KEY_ID_RE.test(name)) {
7477
+ throw new Error(
7478
+ // A Solver's CLI seals private artifacts against this exact string as
7479
+ // the Guardian's key_id, so it can't contain spaces or punctuation the
7480
+ // wire format rejects — no display-only rendering happens in between.
7481
+ `name must match ${KEY_ID_RE.source} (it becomes this Guardian's private-artifact key_id)`
7482
+ );
7483
+ }
7398
7484
  if (!guardianAccount || !isAddress4(guardianAccount)) {
7399
7485
  throw new Error("guardian_account must be a valid EVM address");
7400
7486
  }
@@ -7928,6 +8014,23 @@ function requirePinnedGuardianRosterHash(hubGuardianRosterHash, apiGuardianRoste
7928
8014
  );
7929
8015
  }
7930
8016
  }
8017
+ function requireUsableGuardianNames(roster) {
8018
+ const seen = /* @__PURE__ */ new Map();
8019
+ for (const guardian of roster) {
8020
+ if (!KEY_ID_RE.test(guardian.name)) {
8021
+ throw new Error(
8022
+ `Guardian ${guardian.account} has roster name ${JSON.stringify(guardian.name)}, which cannot be used as a private-artifact key_id (must match ${KEY_ID_RE.source}). This roster cannot receive private submissions until its admin corrects the name.`
8023
+ );
8024
+ }
8025
+ const collidingAccount = seen.get(guardian.name);
8026
+ if (collidingAccount) {
8027
+ throw new Error(
8028
+ `Guardians ${collidingAccount} and ${guardian.account} share the roster name ${JSON.stringify(guardian.name)}. Names must be unique on the pinned roster so each Guardian's key_id resolves to exactly one recipient.`
8029
+ );
8030
+ }
8031
+ seen.set(guardian.name, guardian.account);
8032
+ }
8033
+ }
7931
8034
  function requireMatchingSubmitTransaction(actual, expected) {
7932
8035
  if (actual.chain_id !== expected.chain_id || actual.to.toLowerCase() !== expected.to.toLowerCase() || actual.value !== expected.value || actual.data.toLowerCase() !== expected.data.toLowerCase() || actual.function_name !== expected.function_name || actual.args.bounty_id !== expected.args.bounty_id || actual.args.submission_commitment.toLowerCase() !== expected.args.submission_commitment.toLowerCase() || actual.args.guardian_roster_hash.toLowerCase() !== expected.args.guardian_roster_hash.toLowerCase() || actual.args.extra_data !== expected.args.extra_data) {
7933
8036
  throw new Error(
@@ -7949,9 +8052,10 @@ function requireOpenSubmittableBounty(bountyFacts, bountyId) {
7949
8052
  async function buildSolverSubmissionEnvelope(input) {
7950
8053
  const solver = input.solver.toLowerCase();
7951
8054
  const bountyId = input.bountyId.toString();
8055
+ requireUsableGuardianNames(input.guardianRoster);
7952
8056
  const guardianRecipients = input.guardianRoster.map(
7953
8057
  (guardian) => ({
7954
- key_id: guardian.account.toLowerCase(),
8058
+ key_id: guardian.name,
7955
8059
  suite: PRIVATE_ARTIFACT_HPKE_SUITE,
7956
8060
  public_key: guardian.encryptionPublicKey
7957
8061
  })
@@ -8215,19 +8319,19 @@ function usageError4() {
8215
8319
  return new Error(VERIFICATION_RECORD_USAGE);
8216
8320
  }
8217
8321
 
8218
- // src/version.ts
8219
- var CLI_VERSION = true ? "7.4.0" : "0.0.0-dev";
8220
-
8221
8322
  // src/cli.ts
8222
- var dotenvPath = path4.resolve(process.env.DOTENV_CONFIG_PATH ?? ".env");
8223
- var dotenvResult = loadDotenv({
8224
- quiet: true,
8225
- ...process.env.DOTENV_CONFIG_PATH ? { path: process.env.DOTENV_CONFIG_PATH } : void 0
8226
- });
8227
- process.stderr.write(
8228
- `Env: ${dotenvPath} (${dotenvResult.error ? "not found, using process env only" : "loaded"})
8323
+ if (process.env.DOTENV_CONFIG_PATH) {
8324
+ const dotenvPath = path4.resolve(process.env.DOTENV_CONFIG_PATH);
8325
+ const dotenvResult = loadDotenv({ path: dotenvPath, quiet: true });
8326
+ process.stderr.write(
8327
+ `Env: ${dotenvPath} (${dotenvResult.error ? `not found: ${dotenvResult.error.message}` : "loaded"})
8229
8328
  `
8230
- );
8329
+ );
8330
+ } else {
8331
+ process.stderr.write(
8332
+ "Env: DOTENV_CONFIG_PATH not set; reading process.env only\n"
8333
+ );
8334
+ }
8231
8335
  var COMMANDS = {
8232
8336
  "guardian:judgeable": main5,
8233
8337
  "guardian:open": main3,
@@ -8342,7 +8446,7 @@ var COMMAND_HELP = {
8342
8446
  "roster:add-guardian": {
8343
8447
  usage: "elgora-cli roster:add-guardian [--roster-admin-address <address>]\n <name> <guardian_account> <encryption_public_key>",
8344
8448
  title: "Append one Guardian to the live roster.",
8345
- description: "Call ElgoraHub's guardianRosterAdmin-only addGuardian directly. Only\n future bounties see the new roster; open bounties keep judging\n against the hash pinned when they were created. With\n --roster-admin-address, the transaction is simulated and printed\n unsigned (to, data, value) for an external signer such as a Safe,\n instead of being sent. Run roster:prepare-guardian-proof,\n guardian:prove-roster-key, and roster:verify-guardian-proof against\n the proposed Guardian before ever calling this.",
8449
+ description: "Call ElgoraHub's guardianRosterAdmin-only addGuardian directly. Only\n future bounties see the new roster; open bounties keep judging\n against the hash pinned when they were created. With\n --roster-admin-address, the transaction is simulated and printed\n unsigned (to, data, value) for an external signer such as a Safe,\n instead of being sent. Run roster:prepare-guardian-proof,\n guardian:prove-roster-key, and roster:verify-guardian-proof against\n the proposed Guardian before ever calling this. name becomes this\n Guardian's key_id for every private submission a Solver seals\n afterward \u2014 it must be unique on the roster and is what this\n Guardian's own ELGORA_GUARDIAN_PRIVATE_KEYS_JSON entries must use as\n their key_id, not the account address.",
8346
8450
  required: [],
8347
8451
  defaulted: ["ELGORA_CHAIN_ID", "ELGORA_HUB_ADDRESS", "ELGORA_RPC_URL"]
8348
8452
  },
@@ -8434,6 +8538,32 @@ async function run(rawArgv) {
8434
8538
  printCommandHelpOrUsage(subcommand);
8435
8539
  return;
8436
8540
  }
8541
+ const nodeProblem = unsupportedNodeMessage();
8542
+ if (nodeProblem) {
8543
+ process.stderr.write(`${nodeProblem}
8544
+ `);
8545
+ process.exitCode = 1;
8546
+ return;
8547
+ }
8548
+ const missing = missingRequiredEnv(
8549
+ COMMAND_HELP[subcommand]?.required ?? [],
8550
+ env
8551
+ );
8552
+ if (missing.length > 0) {
8553
+ process.stderr.write(
8554
+ `${[
8555
+ `${subcommand} is missing required environment variable${missing.length > 1 ? "s" : ""}:`,
8556
+ ...missing.map((envName) => ` ${envName}`),
8557
+ "",
8558
+ "This CLI reads only process.env (plus DOTENV_CONFIG_PATH if you set it)",
8559
+ "\u2014 it never assumes a secrets location. Provide these before invoking",
8560
+ `elgora-cli, or run \`elgora-cli help ${subcommand}\` for the full list.`
8561
+ ].join("\n")}
8562
+ `
8563
+ );
8564
+ process.exitCode = 1;
8565
+ return;
8566
+ }
8437
8567
  await command(env, rest);
8438
8568
  }
8439
8569
  function printUsage() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elgora/cli",
3
- "version": "7.4.0",
3
+ "version": "7.5.0",
4
4
  "description": "Command-line client for Elgora, a decentralized bounty marketplace for science. Lets Posters, Solvers, and Guardians interact with Elgora's public API and on-chain contracts.",
5
5
  "type": "module",
6
6
  "license": "MIT",