@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 +10 -3
- package/dist/cli.js +139 -0
- package/dist/lib/api.js +14 -0
- package/package.json +11 -5
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 —
|
|
95
|
-
|
|
96
|
-
proposals) registers the off-chain content. Sealed claims and
|
|
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.
|
|
4
|
-
"description": "CLI for Compute Substrate / Cairn
|
|
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
|
-
"
|
|
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": "
|
|
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
|
}
|