@inversealtruism/cairn-cli 0.3.1 → 0.3.2

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/README.md CHANGED
@@ -91,9 +91,16 @@ Fees and amounts are in **CSD** (e.g. `--amount 1.5`, `--fee 0.05`). Minimums: 0
91
91
  - `verify` fetches an item, recomputes `sha256(canonical content)` locally, and if `CAIRN_RPC` is set,
92
92
  confirms that hash is the one committed on-chain. You trust the math, not the server.
93
93
  - `send` / `propose` / `support` / `wall place`: cairn-cli fetches a spendable input from the Cairn
94
- proxy, hands it to **your** `csd` (which signs with your wallet key — the key never leaves your
95
- machine and never touches cairn-cli), then submits the signed transaction through the proxy and (for
96
- proposals) registers the off-chain content. Sealed claims and Sign-in-with-CSD live in the Cairn Wallet.
94
+ proxy, hands it to **your** `csd` (which signs with your wallet key — for these commands the key
95
+ stays inside `csd` and never enters the cairn-cli process), then submits the signed transaction
96
+ through the proxy and (for proposals) registers the off-chain content. Sealed claims and
97
+ Sign-in-with-CSD live in the Cairn Wallet.
98
+ - **L3 registry commands** (`gateway register`, `peer announce`, `identity claim`) are the one
99
+ exception: they sign a registry *binding* with `@inversealtruism/csd-registry`, so cairn-cli reads
100
+ your private key from `csd wallet config` and signs **in-process** (the key is never networked — only
101
+ the signed canonical content is published). Because these load key material into the Node process,
102
+ the `csd-registry` / `csd-codec` dependencies are **pinned to exact versions** (no caret ranges) to
103
+ shrink the supply-chain surface. If you only ever `send`/`propose`/`support`, your key never leaves `csd`.
97
104
 
98
105
  ## License
99
106
 
package/dist/cli.js CHANGED
@@ -5,6 +5,9 @@ import { CAIRN_API, CAIRN_ADDR, CAIRN_TOKEN, CAIRN_RPC, MIN_FEE_PROPOSE, MIN_FEE
5
5
  import * as api from "./lib/api.js";
6
6
  import * as csd from "./lib/csd.js";
7
7
  import { buildCommitment } from "./lib/item.js";
8
+ import { buildGatewayRecord, buildPeerRecord, buildIdentityCommit, buildIdentityReveal } from "@inversealtruism/csd-registry";
9
+ import { canonicalJson } from "@inversealtruism/csd-codec";
10
+ import { randomBytes } from "node:crypto";
8
11
  import { c, banner, bannerAnimated, rule, badge, bar, csd as csdFmt, ok, warn, err, key as kdim, pad, spinner, sleep, isTty, anim, clearScreen, cursorHome, san } from "./lib/ui.js";
9
12
  const CSD = (n) => Number.isFinite(n) ? Math.round(n * CSD_PER_COIN) : NaN; // CSD → base units
10
13
  // Resolve the user's PUBLIC address (to fetch inputs from the proxy). Never reads the key
@@ -664,7 +667,143 @@ async function main() {
664
667
  case "propose":
665
668
  case "post": return cmdPropose(a);
666
669
  case "support": return cmdSupport(a);
670
+ case "gateway": return cmdGateway(a);
671
+ case "peer": return cmdPeer(a);
672
+ case "identity": return cmdIdentity(a);
667
673
  default: return help();
668
674
  }
669
675
  }
676
+ // ── L3 registry publish commands (build a signed record → anchor Propose → serve bytes) ──
677
+ // Anchor a built registry record: Propose{domain, payloadHash} signed by the csd wallet,
678
+ // then publish the EXACT canonical bytes to the content origin (self-certified on arrival).
679
+ async function anchorRecord(rec, addr, fee, days, label) {
680
+ const uri = "csd:" + rec.domain.replace(/[^a-z]/gi, "").slice(0, 6) + ":v1:" + rec.payloadHash.slice(2, 14);
681
+ const sp = spinner("fetching input → csd signs → submit");
682
+ const picked = await api.pickInput(addr, fee).catch(() => null);
683
+ if (!picked) {
684
+ sp.stop();
685
+ console.log(err("no confirmed UTXO above the fee") + c.gray(" — fund " + addr));
686
+ return false;
687
+ }
688
+ const tip = await api.tipHeight().catch(() => 0);
689
+ const r = await signAndSubmit(["propose", "--domain", rec.domain, "--payload-hash", rec.payloadHash, "--uri", uri, "--expires-epoch", String(Math.floor(tip / 30) + days * 24), "--fee", String(fee), "--change", addr, "--input", picked.input]);
690
+ sp.stop();
691
+ if (!r.ok) {
692
+ console.log(err(r.error || "failed"));
693
+ return false;
694
+ }
695
+ console.log(ok(`${label} anchored ${c.cyan(r.txid)}`) + c.gray(" (signed by your csd wallet)"));
696
+ const sp2 = spinner("publishing content (waits for the tx to mine)");
697
+ const done = await api.registerRawContent(canonicalJson(rec.content), r.txid);
698
+ sp2.stop();
699
+ console.log(done ? ok("content published — record is now resolvable") : warn("content not published yet — re-run once mined"));
700
+ return done;
701
+ }
702
+ // Shared setup for the registry commands: require csd, the privkey (to sign the binding
703
+ // locally — never networked), and the address.
704
+ async function registryPrep(a) {
705
+ if (!(await requireCsd()))
706
+ return null;
707
+ const cfg = await csd.walletConfig();
708
+ const priv = cfg?.default_privkey;
709
+ if (!priv) {
710
+ console.log(err("no csd wallet key configured.") + c.gray(" Run ") + c.cyan("csd wallet new"));
711
+ return null;
712
+ }
713
+ const addr = await resolveAddr(a);
714
+ if (!addr) {
715
+ console.log(err("could not resolve your address — run ") + c.cyan("cairn setup"));
716
+ return null;
717
+ }
718
+ return { priv, addr };
719
+ }
720
+ async function cmdGateway(a) {
721
+ if (a._[1] !== "register") {
722
+ console.log(warn("usage: ") + c.cyan("cairn gateway register --url https://gw/content/0x{hash} [--pin] [--fee 0.25]"));
723
+ return;
724
+ }
725
+ const url = String(a.flags.url ?? "");
726
+ if (!url.includes("{hash}")) {
727
+ console.log(err("--url must contain the {hash} template, e.g. https://gw/content/0x{hash}"));
728
+ return;
729
+ }
730
+ const p = await registryPrep(a);
731
+ if (!p)
732
+ return;
733
+ const rec = buildGatewayRecord({ priv: p.priv, url, kind: a.flags.pin ? "pin" : "gateway", address: p.addr });
734
+ const fee = Math.max(MIN_FEE_PROPOSE, a.flags.fee !== undefined ? CSD(Number(a.flags.fee)) : MIN_FEE_PROPOSE);
735
+ if (a.flags["dry-run"]) {
736
+ console.log(`${kdim("domain")} ${c.cyan(rec.domain)}\n${kdim("url")} ${c.white(url)}\n${kdim("hash")} ${c.magenta(rec.payloadHash)}`);
737
+ console.log(c.gray("\n[dry-run] not signed or submitted"));
738
+ return;
739
+ }
740
+ await anchorRecord(rec, p.addr, fee, 10, "gateway");
741
+ }
742
+ async function cmdPeer(a) {
743
+ if (a._[1] !== "announce") {
744
+ console.log(warn("usage: ") + c.cyan("cairn peer announce --peer-id <id> --addr /ip4/…/tcp/… [--addr …] [--cap full] [--fee 0.25]"));
745
+ return;
746
+ }
747
+ const peerId = String(a.flags["peer-id"] ?? "");
748
+ const multiaddrs = (a.multi.addr ?? (a.flags.addr ? [String(a.flags.addr)] : [])).filter(Boolean);
749
+ if (!peerId || multiaddrs.length === 0) {
750
+ console.log(err("--peer-id and at least one --addr required"));
751
+ return;
752
+ }
753
+ const p = await registryPrep(a);
754
+ if (!p)
755
+ return;
756
+ const caps = (a.multi.cap ?? (a.flags.cap ? [String(a.flags.cap)] : [])).filter(Boolean);
757
+ const rec = buildPeerRecord({ priv: p.priv, peer_id: peerId, multiaddrs, caps: caps.length ? caps : undefined, address: p.addr });
758
+ const fee = Math.max(MIN_FEE_PROPOSE, a.flags.fee !== undefined ? CSD(Number(a.flags.fee)) : MIN_FEE_PROPOSE);
759
+ if (a.flags["dry-run"]) {
760
+ console.log(`${kdim("domain")} ${c.cyan(rec.domain)}\n${kdim("peer")} ${c.white(peerId)}\n${kdim("hash")} ${c.magenta(rec.payloadHash)}`);
761
+ console.log(c.gray("\n[dry-run] not signed or submitted"));
762
+ return;
763
+ }
764
+ await anchorRecord(rec, p.addr, fee, 10, "peer");
765
+ }
766
+ async function cmdIdentity(a) {
767
+ const sub = a._[1];
768
+ const handle = String(a.flags.handle ?? a._[2] ?? "");
769
+ if (sub !== "claim" || !handle) {
770
+ console.log(warn("usage: ") + c.cyan("cairn identity claim <handle> [--salt <hex>] [--commit-only|--reveal] [--fee 0.25]"));
771
+ console.log(c.gray(" step 1: --commit-only (saves a salt) · step 2 (next epoch): --reveal --salt <hex>"));
772
+ return;
773
+ }
774
+ if (!/^[a-z0-9_.-]{3,32}$/i.test(handle)) {
775
+ console.log(err("handle must be 3–32 chars [a-z0-9_.-]"));
776
+ return;
777
+ }
778
+ const p = await registryPrep(a);
779
+ if (!p)
780
+ return;
781
+ const fee = Math.max(MIN_FEE_PROPOSE, a.flags.fee !== undefined ? CSD(Number(a.flags.fee)) : MIN_FEE_PROPOSE);
782
+ if (a.flags.reveal) {
783
+ const salt = String(a.flags.salt ?? "");
784
+ if (!/^[0-9a-f]{16,}$/i.test(salt)) {
785
+ console.log(err("--salt <hex> from your earlier --commit-only step is required to reveal"));
786
+ return;
787
+ }
788
+ const rec = buildIdentityReveal({ priv: p.priv, handle, salt, address: p.addr });
789
+ if (a.flags["dry-run"]) {
790
+ console.log(`${kdim("reveal")} ${c.white(handle)} → ${c.cyan(p.addr)}\n${kdim("hash")} ${c.magenta(rec.payloadHash)}`);
791
+ console.log(c.gray("\n[dry-run] not signed"));
792
+ return;
793
+ }
794
+ await anchorRecord(rec, p.addr, fee, 90, "identity reveal");
795
+ return;
796
+ }
797
+ // default / --commit-only: step 1
798
+ const salt = String(a.flags.salt ?? randomBytes(16).toString("hex"));
799
+ const rec = buildIdentityCommit({ handle, salt, address: p.addr });
800
+ if (a.flags["dry-run"]) {
801
+ console.log(`${kdim("commit")} ${c.white(handle)}\n${kdim("salt")} ${c.magenta(salt)}\n${kdim("hash")} ${c.magenta(rec.payloadHash)}`);
802
+ console.log(c.gray("\n[dry-run] not signed"));
803
+ return;
804
+ }
805
+ const okc = await anchorRecord(rec, p.addr, fee, 90, "identity commit");
806
+ if (okc)
807
+ console.log(c.gray("\n save this salt — reveal NEXT epoch (~1h): ") + c.cyan(`cairn identity claim ${handle} --reveal --salt ${salt}`));
808
+ }
670
809
  main().catch((e) => { console.error(err(String(e?.message ?? e))); process.exit(1); });
package/dist/lib/api.js CHANGED
@@ -85,6 +85,20 @@ export async function registerContent(content, txid, attempts = 20) {
85
85
  }
86
86
  return false;
87
87
  }
88
+ // Register an L3 registry record's EXACT canonical bytes (origin serves them verbatim;
89
+ // accepted only if sha256(bytes) == the on-chain payload_hash). Retries while it mines.
90
+ export async function registerRawContent(bytes, txid, attempts = 20) {
91
+ for (let i = 0; i < attempts; i++) {
92
+ try {
93
+ const r = await req("/api/content", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ bytes, txid }) });
94
+ if (r.ok)
95
+ return true;
96
+ }
97
+ catch { /* keep trying while it mines */ }
98
+ await new Promise((res) => setTimeout(res, 8000));
99
+ }
100
+ return false;
101
+ }
88
102
  // optional: query a raw csd node RPC (for trustless verify)
89
103
  export async function chainProposal(id) {
90
104
  if (!CAIRN_RPC)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inversealtruism/cairn-cli",
3
- "version": "0.3.1",
4
- "description": "CLI for Compute Substrate / Cairn \u2014 browse the board/wall/network, and send CSD, propose, attest, and place stones non-custodially by driving your own installed `csd` wallet (cairn never holds your key; works node-less via the Cairn proxy).",
3
+ "version": "0.3.2",
4
+ "description": "CLI for Compute Substrate / Cairn browse the board/wall/network, and send CSD, propose, attest, and place stones non-custodially by driving your own installed `csd` wallet (cairn never holds your key; works node-less via the Cairn proxy).",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "cairn": "dist/cli.js"
@@ -22,10 +22,12 @@
22
22
  "access": "public"
23
23
  },
24
24
  "scripts": {
25
- "build": "tsc",
25
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
26
+ "build": "npm run clean && tsc",
26
27
  "dev": "tsx src/cli.ts",
27
- "test": "tsc && node test/security.mjs && node test/e2e.mjs",
28
- "prepare": "tsc"
28
+ "test": "npm run build && node test/security.mjs && node test/e2e.mjs",
29
+ "prepare": "npm run clean && tsc",
30
+ "prepublishOnly": "npm run clean && tsc"
29
31
  },
30
32
  "keywords": [
31
33
  "cairn",
@@ -37,5 +39,9 @@
37
39
  "@types/node": "^22.10.0",
38
40
  "tsx": "^4.19.2",
39
41
  "typescript": "^5.7.2"
42
+ },
43
+ "dependencies": {
44
+ "@inversealtruism/csd-codec": "0.1.3",
45
+ "@inversealtruism/csd-registry": "0.1.3"
40
46
  }
41
47
  }