@iicp/client 0.7.36 → 0.7.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.applySavedNode = applySavedNode;
5
+ exports.verifyCreditAwards = verifyCreditAwards;
5
6
  exports.main = main;
6
7
  // SPDX-License-Identifier: Apache-2.0
7
8
  /**
@@ -35,6 +36,7 @@ const cip_policy_js_1 = require("./cip_policy.js");
35
36
  const instance_lock_js_1 = require("./instance_lock.js");
36
37
  const index_js_1 = require("./backends/index.js");
37
38
  const identity_js_1 = require("./identity.js");
39
+ const delegation_js_1 = require("./delegation.js");
38
40
  function envOr(name, fallback) {
39
41
  const v = process.env[name];
40
42
  return v ?? fallback;
@@ -58,7 +60,11 @@ function printHelp() {
58
60
  ` init Interactive wizard — set up operator + first node config\n` +
59
61
  ` list List node configs saved under ~/.iicp/nodes/\n` +
60
62
  ` serve Register and serve a node\n` +
61
- ` query <prompt> Discover mesh nodes and submit a chat task\n\n` +
63
+ ` query <prompt> Discover mesh nodes and submit a chat task\n` +
64
+ ` credits Show this node's earned / spent / balance credits\n` +
65
+ ` operator rename <name> Change your public display_name (signed by your operator key)\n` +
66
+ ` operator encrypt Password-encrypt the operator secret at rest ($IICP_OPERATOR_PASSPHRASE)\n` +
67
+ ` operator decrypt Remove at-rest encryption of the operator secret\n\n` +
62
68
  `Run an IICP provider node backed by an OpenAI-compatible server.\n\n` +
63
69
  `serve required (flag or env):\n` +
64
70
  ` --model NAME IICP_BACKEND_MODEL — model name (e.g. qwen2.5:0.5b)\n` +
@@ -540,6 +546,20 @@ async function runServe(opts) {
540
546
  }
541
547
  const backendFlavor = await detectBackendFlavor(opts.backendUrl, opts.backendApiKey, opts.backendType);
542
548
  process.stderr.write(`backend detected: ${backendFlavor}\n`);
549
+ // #463/#464 — bind the operator identity: issue a delegation FROM the (key-backed) operator
550
+ // identity for this node and advertise the public display_name. The directory verifies the
551
+ // delegation (operator_pub == operator_id) and records the operator. Never sends the secret/contact.
552
+ const _op = (0, identity_js_1.loadOperator)();
553
+ let _opDelegation;
554
+ let _opDisplayName;
555
+ let _opCreatedAt;
556
+ let _opIntegrityHash;
557
+ if (_op && (0, identity_js_1.operatorIsKeyBacked)(_op)) {
558
+ _opDelegation = (0, delegation_js_1.issueDelegation)((0, identity_js_1.operatorSigningKey)(_op), nodeId);
559
+ _opDisplayName = _op.display_name || undefined;
560
+ _opCreatedAt = _op.created_at;
561
+ _opIntegrityHash = _op.operator_integrity_hash || undefined;
562
+ }
543
563
  const node = new node_js_1.IicpNode({
544
564
  nodeId,
545
565
  endpoint: publicEndpoint,
@@ -550,6 +570,10 @@ async function runServe(opts) {
550
570
  directoryUrl: opts.directoryUrl,
551
571
  maxConcurrent: opts.maxConcurrent,
552
572
  relayWorkerEndpoint: opts.relayWorkerEndpoint || undefined,
573
+ operatorDelegation: _opDelegation,
574
+ operatorDisplayName: _opDisplayName,
575
+ operatorCreatedAt: _opCreatedAt,
576
+ operatorIntegrityHash: _opIntegrityHash,
553
577
  });
554
578
  // Apply collected NAT profile (covers both auto-detect and tier-0 IPv6 cases).
555
579
  if (natProfile) {
@@ -654,6 +678,18 @@ async function runServe(opts) {
654
678
  "Set IICP_PUBLIC_ENDPOINT=<url> or IICP_RELAY_WORKER_ENDPOINT=<host>:<port> to register.");
655
679
  opts.skipRegistration = true;
656
680
  }
681
+ // #457 / ADR-040 — advertise the native IICP binary transport. serve() multiplexes it
682
+ // onto the SAME socket as HTTP (first-byte detection), so transport_endpoint shares the
683
+ // endpoint's host:port with the iicp:// scheme. Set from the FINAL endpoint (after NAT
684
+ // profile application); register() only sends it when registering (skipRegistration gates
685
+ // the non-routable case) → advertise-when-reachable. Opt out with IICP_DISABLE_NATIVE_TRANSPORT=1.
686
+ if (!opts.skipRegistration && process.env["IICP_DISABLE_NATIVE_TRANSPORT"] !== "1") {
687
+ const finalEndpoint = node["_cfg"].endpoint;
688
+ const nativeEndpoint = finalEndpoint ? (0, node_js_1.deriveNativeEndpoint)(finalEndpoint) : null;
689
+ if (nativeEndpoint) {
690
+ node["_cfg"].transportEndpoint = nativeEndpoint;
691
+ }
692
+ }
657
693
  // #404 — register with bounded backoff retry. On persistent failure, pass an
658
694
  // empty token (NOT undefined) so the heartbeat loop still starts and re-registers
659
695
  // on the first 401 (#399 path) once the directory is reachable — the self-healing
@@ -666,6 +702,20 @@ async function runServe(opts) {
666
702
  // eslint-disable-next-line no-console
667
703
  console.log(`[iicp-node] registered as ${nodeId} (token=${(token ?? "").slice(0, 8)}…)`);
668
704
  (0, node_log_js_1.writeNodeEvent)(nodeId, "register_ok", `endpoint=${opts.publicEndpoint || `http://localhost:${opts.port}`}`, logDir);
705
+ // #456 — cache the token in the saved config so `iicp-node credits` can
706
+ // authenticate later without re-registering (best-effort).
707
+ if (opts.node && token) {
708
+ const saved = (0, identity_js_1.loadNode)(opts.node);
709
+ if (saved) {
710
+ saved.node_token = token;
711
+ try {
712
+ (0, identity_js_1.saveNode)(saved);
713
+ }
714
+ catch {
715
+ /* best-effort cache */
716
+ }
717
+ }
718
+ }
669
719
  break;
670
720
  }
671
721
  catch (exc) {
@@ -765,7 +815,8 @@ async function runQuery(argv) {
765
815
  intent,
766
816
  payload,
767
817
  });
768
- if (resp.status === "completed" && resp.result) {
818
+ // Spec terminal success status is "success" (was "completed"); accept both.
819
+ if ((resp.status === "success" || resp.status === "completed") && resp.result) {
769
820
  const res = resp.result;
770
821
  const content = typeof res["content"] === "string"
771
822
  ? res["content"]
@@ -787,6 +838,467 @@ async function runQuery(argv) {
787
838
  return 1;
788
839
  }
789
840
  }
841
+ /** SPKI DER prefix for an Ed25519 public key (12 bytes); + 32 raw key bytes = a parseable SPKI key. */
842
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
843
+ /** #458 hash-chain genesis root: SHA256_hex("iicp:dir:event-log:genesis:v1"). The prev_hash
844
+ * of a genesis/legacy event; bound into the signing input the directory verifies against. */
845
+ const EVENT_LOG_GENESIS_ROOT = "c44802bedf3e63b5a3f1634c5d19263634f92f26dd15401b09b06dd53a80cf9d";
846
+ function isLNum(v) {
847
+ return typeof v === "object" && v !== null && !Array.isArray(v) && "__num" in v;
848
+ }
849
+ /** Minimal recursive-descent JSON parse that preserves number literals verbatim. */
850
+ function losslessParse(text) {
851
+ let i = 0;
852
+ const ws = () => {
853
+ while (i < text.length && (text[i] === " " || text[i] === "\t" || text[i] === "\n" || text[i] === "\r"))
854
+ i++;
855
+ };
856
+ const parseStr = () => {
857
+ i++; // opening quote
858
+ let s = "";
859
+ while (text[i] !== '"') {
860
+ const ch = text[i];
861
+ if (ch === "\\") {
862
+ const e = text[++i];
863
+ if (e === "u") {
864
+ s += String.fromCharCode(parseInt(text.slice(i + 1, i + 5), 16));
865
+ i += 5;
866
+ }
867
+ else {
868
+ const map = {
869
+ '"': '"', "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: "\t",
870
+ };
871
+ s += map[e] ?? e;
872
+ i++;
873
+ }
874
+ }
875
+ else {
876
+ s += ch;
877
+ i++;
878
+ }
879
+ }
880
+ i++; // closing quote
881
+ return s;
882
+ };
883
+ const parseVal = () => {
884
+ ws();
885
+ const c = text[i];
886
+ if (c === "{") {
887
+ const o = {};
888
+ i++;
889
+ ws();
890
+ if (text[i] === "}") {
891
+ i++;
892
+ return o;
893
+ }
894
+ for (;;) {
895
+ ws();
896
+ const key = parseStr();
897
+ ws();
898
+ i++; // ':'
899
+ o[key] = parseVal();
900
+ ws();
901
+ if (text[i] === ",") {
902
+ i++;
903
+ continue;
904
+ }
905
+ i++; // '}'
906
+ break;
907
+ }
908
+ return o;
909
+ }
910
+ if (c === "[") {
911
+ const a = [];
912
+ i++;
913
+ ws();
914
+ if (text[i] === "]") {
915
+ i++;
916
+ return a;
917
+ }
918
+ for (;;) {
919
+ a.push(parseVal());
920
+ ws();
921
+ if (text[i] === ",") {
922
+ i++;
923
+ continue;
924
+ }
925
+ i++; // ']'
926
+ break;
927
+ }
928
+ return a;
929
+ }
930
+ if (c === '"')
931
+ return parseStr();
932
+ if (c === "t") {
933
+ i += 4;
934
+ return true;
935
+ }
936
+ if (c === "f") {
937
+ i += 5;
938
+ return false;
939
+ }
940
+ if (c === "n") {
941
+ i += 4;
942
+ return null;
943
+ }
944
+ const start = i;
945
+ while (i < text.length && "+-0123456789.eE".includes(text[i]))
946
+ i++;
947
+ return { __num: text.slice(start, i) };
948
+ };
949
+ return parseVal();
950
+ }
951
+ /**
952
+ * Canonical JSON byte-for-byte matching the directory (`federation.rs` / PHP `json_encode`):
953
+ * recursive lexicographic key-sort, no whitespace, `/` and non-ASCII unescaped (JS
954
+ * `JSON.stringify` already matches for strings), numbers emitted as their source literal.
955
+ */
956
+ function canonicalJson(node) {
957
+ if (node === null || typeof node === "boolean" || typeof node === "string")
958
+ return JSON.stringify(node);
959
+ if (isLNum(node))
960
+ return node.__num;
961
+ if (Array.isArray(node))
962
+ return "[" + node.map(canonicalJson).join(",") + "]";
963
+ const keys = Object.keys(node).sort();
964
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalJson(node[k])).join(",") + "}";
965
+ }
966
+ /**
967
+ * #456 --verify: cryptographically confirm this node's CREDIT_AWARD income against the
968
+ * directory's signed event log (defends against a lying directory). Resolves the directory's
969
+ * Ed25519 key from /.well-known/did.json and re-derives + verifies each award signature.
970
+ * Free-tier CREDIT_ALLOCATION is unsigned by design, so it is not counted here (no cry-wolf).
971
+ */
972
+ async function verifyCreditAwards(directoryUrl, nodeId) {
973
+ const origin = new URL(directoryUrl).origin; // did.json lives at the host root, not under /api
974
+ const didResp = await fetch(`${origin}/.well-known/did.json`, { signal: AbortSignal.timeout(20000) });
975
+ const did = (await didResp.json());
976
+ const x = did.verificationMethod?.[0]?.publicKeyJwk?.x;
977
+ if (!x)
978
+ throw new Error("directory did.json has no Ed25519 verification key");
979
+ const raw = Buffer.from(x, "base64url");
980
+ if (raw.length !== 32)
981
+ throw new Error(`bad Ed25519 key length ${raw.length}`);
982
+ const pubKey = (0, node_crypto_1.createPublicKey)({
983
+ key: Buffer.concat([ED25519_SPKI_PREFIX, raw]),
984
+ format: "der",
985
+ type: "spki",
986
+ });
987
+ const base = directoryUrl.replace(/\/+$/, "");
988
+ let sum = 0;
989
+ let verified = 0;
990
+ let failed = 0;
991
+ let since = 0;
992
+ for (;;) {
993
+ const url = `${base}/v1/events?event_types=CREDIT_AWARD&since_seq=${since}&limit=500`;
994
+ const resp = await fetch(url, { signal: AbortSignal.timeout(20000) });
995
+ const tree = losslessParse(await resp.text());
996
+ const events = tree["events"] ?? [];
997
+ if (events.length === 0)
998
+ break;
999
+ let maxSeq = since;
1000
+ for (const evRaw of events) {
1001
+ const ev = evRaw;
1002
+ const seqNode = ev["seq"];
1003
+ const seq = isLNum(seqNode) ? Number(seqNode.__num) : 0;
1004
+ maxSeq = Math.max(maxSeq, seq);
1005
+ if (ev["event_type"] !== "CREDIT_AWARD" || ev["node_id"] !== nodeId)
1006
+ continue;
1007
+ const sig = ev["sig"];
1008
+ if (typeof sig !== "string")
1009
+ continue;
1010
+ const payload = ev["payload"];
1011
+ const payloadHash = (0, node_crypto_1.createHash)("sha256").update(canonicalJson(payload)).digest("hex");
1012
+ const eventId = typeof ev["event_id"] === "string" ? ev["event_id"] : "";
1013
+ const tsNode = ev["ts_ms"];
1014
+ const tsMs = isLNum(tsNode) ? Number(tsNode.__num) : 0;
1015
+ // #458: prev_hash (tamper-evident chain) is bound into the signing input; the directory
1016
+ // serves it per event, defaulting to GENESIS_ROOT for a genesis/legacy event.
1017
+ const prevHash = typeof ev["prev_hash"] === "string" ? ev["prev_hash"] : EVENT_LOG_GENESIS_ROOT;
1018
+ const msg = (0, node_crypto_1.createHash)("sha256")
1019
+ .update(`${eventId}:CREDIT_AWARD:${seq}:${tsMs}:${payloadHash}:${prevHash}`)
1020
+ .digest();
1021
+ const sigBuf = Buffer.from(sig, "hex");
1022
+ if (sigBuf.length === 64 && (0, node_crypto_1.verify)(null, msg, pubKey, sigBuf)) {
1023
+ verified++;
1024
+ const amt = typeof payload === "object" && payload !== null && !Array.isArray(payload)
1025
+ ? payload["amount"]
1026
+ : undefined;
1027
+ if (amt !== undefined && isLNum(amt))
1028
+ sum += Number(amt.__num);
1029
+ }
1030
+ else {
1031
+ failed++;
1032
+ }
1033
+ }
1034
+ if (events.length < 500 || maxSeq <= since)
1035
+ break;
1036
+ since = maxSeq;
1037
+ }
1038
+ return { sum, verified, failed };
1039
+ }
1040
+ /**
1041
+ * `iicp-node credits` (#456) — earned / spent / balance from the directory's
1042
+ * reconcile-checked GET /v1/credits/summary. Figures come authenticated from the
1043
+ * directory (not the local config), so editing the saved file cannot inflate them;
1044
+ * `reconciles` flags a ledger that does not add up.
1045
+ */
1046
+ async function runCredits(argv) {
1047
+ const { values } = (0, node_util_1.parseArgs)({
1048
+ args: argv,
1049
+ options: {
1050
+ node: { type: "string" },
1051
+ "node-id": { type: "string" },
1052
+ token: { type: "string" },
1053
+ "directory-url": { type: "string" },
1054
+ json: { type: "boolean" },
1055
+ verify: { type: "boolean" },
1056
+ },
1057
+ allowPositionals: false,
1058
+ });
1059
+ const nodeName = values["node"];
1060
+ let directoryUrl = values["directory-url"];
1061
+ let nodeId = values["node-id"];
1062
+ let token = values["token"] ?? process.env["IICP_NODE_TOKEN"];
1063
+ if (nodeName) {
1064
+ const saved = (0, identity_js_1.loadNode)(nodeName);
1065
+ if (!saved) {
1066
+ process.stderr.write(`ERROR: no saved config at ~/.iicp/nodes/${nodeName}.json — run \`iicp-node init\` / \`serve\` first.\n`);
1067
+ return 1;
1068
+ }
1069
+ directoryUrl = directoryUrl ?? saved.directory_url;
1070
+ nodeId = nodeId ?? saved.node_id;
1071
+ token = token ?? saved.node_token;
1072
+ }
1073
+ directoryUrl = directoryUrl ?? process.env["IICP_DIRECTORY_URL"] ?? "https://iicp.network/api";
1074
+ if (!nodeId) {
1075
+ process.stderr.write("ERROR: node_id required (use --node NAME or --node-id ID)\n");
1076
+ return 1;
1077
+ }
1078
+ if (!token) {
1079
+ process.stderr.write("ERROR: no node_token — run `iicp-node serve` once (it caches the token), or pass --token / $IICP_NODE_TOKEN\n");
1080
+ return 1;
1081
+ }
1082
+ const url = `${directoryUrl.replace(/\/+$/, "")}/v1/credits/summary`;
1083
+ let resp;
1084
+ try {
1085
+ resp = await fetch(url, {
1086
+ headers: { Authorization: `Bearer ${token}`, "X-Node-Id": nodeId },
1087
+ signal: AbortSignal.timeout(15000),
1088
+ });
1089
+ }
1090
+ catch (e) {
1091
+ process.stderr.write(`ERROR: request failed: ${e instanceof Error ? e.message : String(e)}\n`);
1092
+ return 1;
1093
+ }
1094
+ let body;
1095
+ try {
1096
+ body = (await resp.json());
1097
+ }
1098
+ catch {
1099
+ process.stderr.write(`ERROR: bad response (HTTP ${resp.status})\n`);
1100
+ return 1;
1101
+ }
1102
+ if (!resp.ok) {
1103
+ const err = body["error"];
1104
+ process.stderr.write(`ERROR: HTTP ${resp.status}: ${err?.message ?? "request rejected"}\n`);
1105
+ return 1;
1106
+ }
1107
+ if (values["json"]) {
1108
+ process.stdout.write(JSON.stringify(body, null, 2) + "\n");
1109
+ return 0;
1110
+ }
1111
+ const earned = Number(body["total_earned"] ?? 0);
1112
+ const spent = Number(body["total_spent"] ?? 0);
1113
+ const balance = Number(body["balance"] ?? 0);
1114
+ const tx = Number(body["tx_count"] ?? 0);
1115
+ const reconciles = Boolean(body["reconciles"]);
1116
+ const tpc = Number(body["tokens_per_credit"] ?? 1000);
1117
+ const pad = (n) => n.toFixed(3).padStart(12);
1118
+ const check = reconciles ? "✓ reconciles" : "✗ DOES NOT RECONCILE";
1119
+ process.stdout.write(`IICP credits — ${nodeName ?? nodeId}\n`);
1120
+ process.stdout.write(` Earned (income) ${pad(earned)}\n`);
1121
+ process.stdout.write(` Spent ${pad(spent)}\n`);
1122
+ process.stdout.write(" ─────────────────────────────\n");
1123
+ process.stdout.write(` Balance ${pad(balance)} ${check} (≈ ${Math.trunc(balance * tpc)} tokens)\n`);
1124
+ process.stdout.write(` ${tx} transactions · \`iicp-node credits --json\` for raw\n`);
1125
+ if (!reconciles) {
1126
+ process.stderr.write("[iicp-node] WARNING: balance != earned − spent — the ledger does not reconcile; do not trust these figures.\n");
1127
+ }
1128
+ if (values["verify"]) {
1129
+ process.stdout.write(" ── cryptographic verification (signed CREDIT_AWARD log) ──\n");
1130
+ let v;
1131
+ try {
1132
+ v = await verifyCreditAwards(directoryUrl, nodeId);
1133
+ }
1134
+ catch (e) {
1135
+ process.stderr.write(`[iicp-node] --verify failed: ${e instanceof Error ? e.message : String(e)}\n`);
1136
+ return 1;
1137
+ }
1138
+ if (v.failed > 0) {
1139
+ process.stderr.write(`[iicp-node] ✗ ${v.failed} award event(s) FAILED Ed25519 verification — ` +
1140
+ "tampered or inconsistent event log. Do NOT trust these figures.\n");
1141
+ return 1;
1142
+ }
1143
+ process.stdout.write(` ✓ ${v.verified} award(s) cryptographically verified · ${v.sum.toFixed(3)} credits ` +
1144
+ "(Ed25519, signed by the directory)\n");
1145
+ const freeTier = earned - v.sum;
1146
+ if (freeTier > 0.0001) {
1147
+ process.stdout.write(` · ${freeTier.toFixed(3)} credits are free-tier allocation ` +
1148
+ "(directory-granted, not signed task awards)\n");
1149
+ }
1150
+ }
1151
+ return 0;
1152
+ }
1153
+ /**
1154
+ * Resolve a passphrase: $IICP_OPERATOR_PASSPHRASE if set (headless/CI), else an interactive
1155
+ * readline prompt (this command is operator-run, so a prompt is fine here — only `serve` must
1156
+ * stay non-interactive). For `confirm`, the prompt is asked twice and must match.
1157
+ */
1158
+ async function operatorPassphrase(prompt, confirm) {
1159
+ const env = process.env["IICP_OPERATOR_PASSPHRASE"];
1160
+ if (env)
1161
+ return env;
1162
+ const { createInterface } = await import("node:readline/promises");
1163
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1164
+ try {
1165
+ const pw = (await rl.question(prompt)).trim();
1166
+ if (confirm && pw !== (await rl.question("Confirm passphrase: ")).trim()) {
1167
+ process.stderr.write("ERROR: passphrases do not match.\n");
1168
+ return null;
1169
+ }
1170
+ return pw || null;
1171
+ }
1172
+ finally {
1173
+ rl.close();
1174
+ }
1175
+ }
1176
+ /** `iicp-node operator encrypt` (#460) — seal the operator secret at rest under a passphrase. */
1177
+ async function runOperatorEncrypt() {
1178
+ const op = (0, identity_js_1.loadOperator)();
1179
+ if (!op) {
1180
+ process.stderr.write("ERROR: no operator identity — run `iicp-node init` first.\n");
1181
+ return 1;
1182
+ }
1183
+ if ((0, identity_js_1.operatorIsEncrypted)(op)) {
1184
+ process.stdout.write("Operator secret is already encrypted at rest.\n");
1185
+ return 0;
1186
+ }
1187
+ if (!(0, identity_js_1.operatorIsKeyBacked)(op)) {
1188
+ process.stderr.write("ERROR: legacy keyless operator identity has nothing to encrypt (#464).\n");
1189
+ return 1;
1190
+ }
1191
+ const pw = await operatorPassphrase("New operator passphrase: ", true);
1192
+ if (!pw) {
1193
+ process.stderr.write("ERROR: a non-empty passphrase is required.\n");
1194
+ return 1;
1195
+ }
1196
+ (0, identity_js_1.saveOperator)((0, identity_js_1.operatorEncryptAtRest)(op, pw));
1197
+ process.stdout.write("Operator secret encrypted at rest (AES-256-GCM / PBKDF2). Set $IICP_OPERATOR_PASSPHRASE " +
1198
+ "to unlock it headlessly during `serve`.\n");
1199
+ return 0;
1200
+ }
1201
+ /** `iicp-node operator decrypt` (#460) — restore the plaintext secret at rest. */
1202
+ async function runOperatorDecrypt() {
1203
+ const op = (0, identity_js_1.loadOperator)();
1204
+ if (!op) {
1205
+ process.stderr.write("ERROR: no operator identity — run `iicp-node init` first.\n");
1206
+ return 1;
1207
+ }
1208
+ if (!(0, identity_js_1.operatorIsEncrypted)(op)) {
1209
+ process.stdout.write("Operator secret is already stored in plaintext.\n");
1210
+ return 0;
1211
+ }
1212
+ const pw = await operatorPassphrase("Operator passphrase: ", false);
1213
+ if (!pw) {
1214
+ process.stderr.write("ERROR: a passphrase is required to decrypt.\n");
1215
+ return 1;
1216
+ }
1217
+ try {
1218
+ (0, identity_js_1.saveOperator)((0, identity_js_1.operatorDecryptAtRest)(op, pw));
1219
+ }
1220
+ catch (e) {
1221
+ process.stderr.write(`ERROR: ${e instanceof Error ? e.message : String(e)}\n`);
1222
+ return 1;
1223
+ }
1224
+ process.stdout.write("Operator secret decrypted (now stored in plaintext at rest).\n");
1225
+ return 0;
1226
+ }
1227
+ /**
1228
+ * `iicp-node operator rename <name>` (#460) — change the public, mutable display_name over
1229
+ * the immutable operator_id. The operator signs the canonical rename bytes with their own
1230
+ * key, so the directory authenticates the change by signature alone (no node token); one
1231
+ * signed call updates the single operator record, reflected on every node + the leaderboard.
1232
+ * Updates the local operator.json on success. Never sends the secret/contact.
1233
+ */
1234
+ async function runOperator(argv) {
1235
+ const sub = argv[0];
1236
+ if (sub === "encrypt")
1237
+ return runOperatorEncrypt();
1238
+ if (sub === "decrypt")
1239
+ return runOperatorDecrypt();
1240
+ if (sub !== "rename") {
1241
+ process.stderr.write(`unknown operator subcommand: ${sub ?? "(none)"}\n`);
1242
+ return 2;
1243
+ }
1244
+ const { values, positionals } = (0, node_util_1.parseArgs)({
1245
+ args: argv.slice(1),
1246
+ options: { "directory-url": { type: "string" } },
1247
+ allowPositionals: true,
1248
+ });
1249
+ const name = positionals[0];
1250
+ // eslint-disable-next-line no-control-regex
1251
+ if (!name || name.length > 64 || /[\u0000-\u001f\u007f]/.test(name)) {
1252
+ process.stderr.write("ERROR: display name must be 1-64 chars with no control characters.\n");
1253
+ return 1;
1254
+ }
1255
+ const op = (0, identity_js_1.loadOperator)();
1256
+ if (!op) {
1257
+ process.stderr.write("ERROR: no operator identity — run `iicp-node init` first.\n");
1258
+ return 1;
1259
+ }
1260
+ if (!(0, identity_js_1.operatorIsKeyBacked)(op)) {
1261
+ process.stderr.write("ERROR: legacy keyless operator identity (operator_id is a UUID, not a key) — " +
1262
+ "cannot sign a rename. Regenerate with a key-backed identity (#464).\n");
1263
+ return 1;
1264
+ }
1265
+ const directoryUrl = values["directory-url"] ??
1266
+ process.env["IICP_DIRECTORY_URL"] ??
1267
+ "https://iicp.network/api";
1268
+ const ts = Math.floor(Date.now() / 1000);
1269
+ const sig = (0, delegation_js_1.signRename)((0, identity_js_1.operatorSigningKey)(op), name, op.operator_id, ts);
1270
+ const url = `${directoryUrl.replace(/\/+$/, "")}/v1/operator/rename`;
1271
+ let resp;
1272
+ try {
1273
+ resp = await fetch(url, {
1274
+ method: "POST",
1275
+ headers: { "Content-Type": "application/json" },
1276
+ body: JSON.stringify({ operator_pub: op.operator_id, display_name: name, ts, sig }),
1277
+ signal: AbortSignal.timeout(15000),
1278
+ });
1279
+ }
1280
+ catch (e) {
1281
+ process.stderr.write(`ERROR: request failed: ${e instanceof Error ? e.message : String(e)}\n`);
1282
+ return 1;
1283
+ }
1284
+ let body;
1285
+ try {
1286
+ body = (await resp.json());
1287
+ }
1288
+ catch {
1289
+ body = {};
1290
+ }
1291
+ if (!resp.ok) {
1292
+ const err = body["error"];
1293
+ process.stderr.write(`ERROR: HTTP ${resp.status}: ${err?.message ?? "request rejected"}\n`);
1294
+ return 1;
1295
+ }
1296
+ // Persist the new name locally so the next `serve` re-asserts it at register.
1297
+ op.display_name = body["display_name"] ?? name;
1298
+ (0, identity_js_1.saveOperator)(op);
1299
+ process.stdout.write(`Renamed operator display_name to ${JSON.stringify(op.display_name)}.\n`);
1300
+ return 0;
1301
+ }
790
1302
  async function main(argv = process.argv.slice(2)) {
791
1303
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
792
1304
  printHelp();
@@ -803,6 +1315,10 @@ async function main(argv = process.argv.slice(2)) {
803
1315
  return runList();
804
1316
  if (cmd === "query")
805
1317
  return runQuery(argv.slice(1));
1318
+ if (cmd === "credits")
1319
+ return runCredits(argv.slice(1));
1320
+ if (cmd === "operator")
1321
+ return runOperator(argv.slice(1));
806
1322
  if (cmd !== "serve") {
807
1323
  process.stderr.write(`unknown command: ${cmd}\n`);
808
1324
  printHelp();