@inversealtruism/cairn-sdk 0.4.5 → 0.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.
package/README.md CHANGED
@@ -6,9 +6,8 @@ The **Compute Substrate dApp kit:** one cohesive toolkit for building apps on CS
6
6
  |---|---|
7
7
  | `cairn.wallet` | Connect the **Cairn Wallet**, sign things (clear-signed approvals). The key never leaves the extension. |
8
8
  | `cairn.chain` | Node RPC + transaction builders + a verifying **light client** (re-exports the `csd-*` primitives). |
9
- | `cairn.board` | The signal **board** + work graph. Reads are public; writes are wallet-signed. |
10
9
  | `cairn.index` | The L2 **explorer/indexer**: REST + merkle proofs + live SSE/WS feeds. |
11
- | `cairn.content` | **Self-certifying content:** publish via the board, fetch with client-side verification. |
10
+ | `cairn.content` | **Self-certifying content:** hash for an on-chain Propose, fetch with client-side verification. |
12
11
  | `cairn.registry` | L3 **peer / gateway / identity** resolution. |
13
12
  | `cairn.names` | **.csd names + CairnX reads:** name resolution, reverse/primary lookup, tokens, offers, bids, canonical state. |
14
13
 
@@ -24,21 +23,17 @@ import { Cairn } from "@inversealtruism/cairn-sdk";
24
23
  const cairn = new Cairn({ network: "mainnet" }); // defaults to cairn-substrate.com
25
24
 
26
25
  // reads need no wallet
27
- const top = await cairn.board.top({ domain: "csd:apps" });
28
26
  const tip = await cairn.chain.tip();
27
+ const domains = await cairn.index.domains();
29
28
 
30
29
  // connect the user's wallet (browser): prompts once, then "connected"
31
30
  await cairn.connect();
32
31
 
33
- // publish a board item, signed by the user's wallet (clear-signed approval)
34
- const { txid } = await cairn.board.propose({
35
- domain: "csd:apps",
36
- title: "My dApp",
37
- body: "Built with the Cairn SDK",
38
- });
32
+ // a payment, signed by the user's wallet (clear-signed approval every time)
33
+ const { txid } = await cairn.wallet!.send({ to: "0x…", amount: 100_000_000 }); // 1 CSD
39
34
 
40
- // watch it land, live
41
- cairn.index.streamAll({ onProposal: (e) => console.log("new proposal", e) });
35
+ // watch the chain, live
36
+ cairn.index.streamAll({ onBlock: (e) => console.log("new block", e) });
42
37
  ```
43
38
 
44
39
  ## How wallet connection works (and why it's safe)
@@ -62,9 +57,7 @@ try {
62
57
 
63
58
  ## API by namespace
64
59
 
65
- **`cairn.board`:** `top()/board({domain,window})`, `item(id)`, `domains()`, `quests()/quest(id)`, `profile(addr)`, `leaderboard()`, `network()/networkSeries()/miner(addr)`, `activity()`, `wall()`; writes `propose({domain,title,body,links?,fee?})`, `support(id,{score?,confidence?,fee?})`.
66
-
67
- **`cairn.index`:** `tipHeight()`, `block()/blockTxids()`, `tx()/txStatus()/txMerkleProof()`, `address()/addressTxs()/addressUtxo()`, `proposal()/attestations()`, `reputation()`, `registryPeers()/registryGateways()/identity()`, **`verifyInclusion(txid)`** (trust-minimized), `streamAll()/streamBlocks()/streamDomain()` (SSE), `subscribe()` (WS).
60
+ **`cairn.index`:** `tipHeight()`, `block()/blockTxids()`, `tx()/txStatus()/txMerkleProof()`, `address()/addressTxs()/addressUtxo()`, `domains()/domainProposals(domain)`, `proposal()/attestations()`, `reputation()`, `registryPeers()/registryGateways()/identity()`, **`verifyInclusion(txid)`** (trust-minimized), `streamAll()/streamBlocks()/streamDomain()` (SSE), `subscribe()` (WS).
68
61
 
69
62
  **`cairn.names`:** `resolve(name)` (display-path name to address), `name(name)`/`nameHistory(name)`, `primary(addr)` (reverse lookup)/`namesOf(addr)`, `names()`, `tokens()/token(ticker)`, `offers()`, `bids()`, `state()` (the canonical CairnX state). Reads go through the public `/trade/api/cairnx/*` proxy; for payment-grade name resolution verify through the wallet or a replaying resolver, not a bare read (see `src/names.ts` header).
70
63
 
@@ -74,17 +67,17 @@ try {
74
67
 
75
68
  **`cairn.registry`:** `gateways()`, `peers()`, `resolveName(handle)`, `reverseName(addr)`, `.fromRecords` (deterministic local resolvers).
76
69
 
77
- Subpath imports are available for tree-shaking: `@inversealtruism/cairn-sdk/{connect,board,indexer,content,registry,chain}`.
70
+ Subpath imports are available for tree-shaking: `@inversealtruism/cairn-sdk/{connect,indexer,content,registry,chain,controller,react}`.
78
71
 
79
72
  ## Examples
80
73
 
81
- - **`examples/read-only.mjs`:** runs today against live mainnet, no wallet, no build: `npm run test:live`. Demonstrates chain/board/indexer/registry reads + verified merkle inclusion + verified content fetch.
82
- - **`examples/hello-csd/`:** a no-framework browser dApp (connect → read board → publish → live feed). Build the bundle with `npm run build:example`, then serve the folder from **localhost** (the wallet injects there) with the Cairn Wallet installed.
74
+ - **`examples/read-only.mjs`:** runs today against live mainnet, no wallet, no build: `npm run test:live`. Demonstrates chain/indexer/registry reads + verified merkle inclusion + verified content fetch.
75
+ - **`examples/hello-csd/`:** a no-framework browser dApp (connect → read recent proposals → live feed). Build the bundle with `npm run build:example`, then serve the folder from **localhost** (the wallet injects there) with the Cairn Wallet installed.
83
76
 
84
77
  ## Trust model & honest limits
85
78
 
86
79
  - **Content is always verified client-side.** `cairn.content.get()` rejects bytes that don't hash to the requested `payloadHash`, an untrusted gateway can't serve tampered content.
87
- - **Merkle inclusion is trust-minimized, to the strength of your header source.** `verifyInclusion()` folds the proof and, when a header-merkle source is wired, cross-checks the root against it → `trustLevel: "verified-inclusion"`; otherwise `"proof-consistent"` (indexer trusted). That cross-check only means something against an *independent* header, so the default `Cairn` wiring enables it ONLY when your node RPC is a different origin than the indexer (the default same-origin proxy stays at `"proof-consistent"`, a single compromised server can't be cross-checked against itself). For full trust-minimization against a lying node too, supply a PoW-verifying `headerMerkleAt` (e.g. backed by `cairn.chain.light()`).
80
+ - **Merkle inclusion is trust-minimized, to the strength of your header source.** `verifyInclusion()` folds the proof and cross-checks its root against a header merkle from `headerMerkleAt`. The default `Cairn` wiring backs that with a light client that PoW-verifies headers forward from a pinned checkpoint, so a match reads `trustLevel: "verified-inclusion"` with `included: true`. A header within 12 blocks of the light client's cached tip is first compared with the header the base serves at that height, so a block orphaned by a reorg stops counting; the hosted base edge-caches rows at or below its tip minus 6, so there this catches reorgs of up to 6 blocks. When no header can be verified (below the checkpoint, header source unreachable, no source configured) the result is `"proof-consistent"` with `included: false` and a `reason`: the indexer alone vouches for it. Gate on `included` or on `trustLevel === "verified-inclusion"`; they agree.
88
81
  - **Balances are RPC-trusted.** Headers don't commit to the UTXO set; balance reads trust the node/proxy (same as every light client).
89
82
  - **Indexer/swarm run behind the cairn proxy.** By default `cairn.index` and content resolution use the cairn server's hardened, read-only, rate-limited `/explorer/api` reverse proxy (no separate public indexer needed). **SSE works through it; `subscribe()` (WebSocket) needs a direct indexer endpoint:** set `baseUrls.indexer` to a private/localhost indexer for WS.
90
83
  - **The wallet injects on every site** (connect-anywhere shipped in 0.2.4x) behind a per-origin consent gate: a site sees nothing until the user approves the connection, and some request kinds stay first-party-only (the `FIRST_PARTY_ONLY` error code).
@@ -95,7 +88,7 @@ Subpath imports are available for tree-shaking: `@inversealtruism/cairn-sdk/{con
95
88
  new Cairn({
96
89
  network: "mainnet",
97
90
  baseUrls: {
98
- cairn: "https://cairn-substrate.com", // board + /api/rpc proxy + /content origin
91
+ cairn: "https://cairn-substrate.com", // /api/rpc proxy + /api/headers + /content origin + names
99
92
  rpc: "https://cairn-substrate.com/api/rpc", // node RPC (proxy)
100
93
  indexer: "https://cairn-substrate.com/explorer/api",
101
94
  swarm: undefined, // optional direct swarm gateway
@@ -112,10 +105,9 @@ new Cairn({
112
105
  pnpm build && pnpm test # unit (offline): error mapping, client paths, content+merkle verify
113
106
  pnpm test:e2e:read # live reads vs mainnet + adversarial (free)
114
107
  pnpm test:e2e:wallet # real extension + real connector under Xvfb: connect/consent/sign (free)
115
- pnpm test:e2e:write # full pipeline incl. a real on-chain Propose+Attest (spends ~0.3 CSD)
116
108
  ```
117
109
 
118
- See [`test/e2e/README.md`](test/e2e/README.md) for coverage, requirements, and cost of each layer. The unit + live-read + wallet-connector suites are CI-friendly and free; the write suite needs a funded key and is opt-in.
110
+ See [`test/e2e/README.md`](test/e2e/README.md) for coverage, requirements, and cost of each layer. All three suites are free; none spends CSD.
119
111
 
120
112
  ## License
121
113
 
@@ -4,7 +4,7 @@ import {
4
4
  UnsupportedMethodError,
5
5
  mapProviderError,
6
6
  mapSubmitResultError
7
- } from "./chunk-LOAOBV36.js";
7
+ } from "./chunk-OSPAN7GX.js";
8
8
 
9
9
  // src/connect.ts
10
10
  var DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 6e4;
@@ -231,7 +231,7 @@ var WalletConnection = class {
231
231
  send(params) {
232
232
  return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
233
233
  }
234
- /** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
234
+ /** Low-level Propose (always prompts). The caller computes the payload hash, uri and expiry epoch. */
235
235
  propose(params) {
236
236
  return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
237
237
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HttpError
3
- } from "./chunk-LOAOBV36.js";
3
+ } from "./chunk-OSPAN7GX.js";
4
4
 
5
5
  // src/registry.ts
6
6
  import { fromRecords } from "@inversealtruism/csd-registry";
@@ -8,6 +8,26 @@ import { fromRecords } from "@inversealtruism/csd-registry";
8
8
  // src/http.ts
9
9
  var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
10
10
  var ERR_TEXT_CAP = 8 * 1024;
11
+ var MAX_RETRY_AFTER_MS = 6e4;
12
+ function retryAfterMs(res) {
13
+ const secs = Number(res.headers.get("retry-after"));
14
+ return Number.isFinite(secs) && secs > 0 ? Math.min(secs * 1e3, MAX_RETRY_AFTER_MS) : 0;
15
+ }
16
+ var RetryWaitBudget = class {
17
+ left;
18
+ constructor(totalMs) {
19
+ this.left = totalMs;
20
+ }
21
+ /** The ms to sleep before the next attempt (`ownMs` is the caller's backoff, clipped to what is left), or
22
+ * null when the server's hint exceeds the remaining budget. */
23
+ take(ownMs, serverMs) {
24
+ if (serverMs > this.left) return null;
25
+ const ms = Math.min(Math.max(ownMs, serverMs), this.left);
26
+ this.left -= ms;
27
+ return ms;
28
+ }
29
+ };
30
+ var HTTP_RETRY_WAIT_BUDGET_MS = 3e4;
11
31
  async function readCapped(res, maxBytes) {
12
32
  const cl = Number(res.headers.get("content-length"));
13
33
  if (Number.isFinite(cl) && cl > maxBytes) throw new HttpError(res.status, res.url, `response too large (content-length ${cl} > ${maxBytes})`);
@@ -56,9 +76,10 @@ var Http = class {
56
76
  if (!f) throw new Error("No fetch available \u2014 pass `fetch` in the SDK options.");
57
77
  this._fetch = opts.fetch ? opts.fetch : f.bind(globalThis);
58
78
  this.timeoutMs = opts.timeoutMs ?? 15e3;
59
- this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
79
+ this.maxBytes = Number.isFinite(opts.maxBytes) && opts.maxBytes >= 1 ? Math.floor(opts.maxBytes) : DEFAULT_MAX_BYTES;
60
80
  this.headers = opts.headers ?? {};
61
- this.retries = Math.min(10, Math.max(0, Math.floor(opts.retries ?? 0)));
81
+ const retries = Number(opts.retries ?? 0);
82
+ this.retries = Number.isNaN(retries) ? 0 : Math.min(10, Math.max(0, Math.floor(retries)));
62
83
  }
63
84
  /** Build an absolute URL from a path + optional query params. */
64
85
  url(path, query) {
@@ -107,11 +128,16 @@ var Http = class {
107
128
  }
108
129
  async getJson(path, query) {
109
130
  const url = this.url(path, query);
131
+ const budget = new RetryWaitBudget(HTTP_RETRY_WAIT_BUDGET_MS);
110
132
  for (let attempt = 0; ; attempt++) {
133
+ let serverWaitMs = 0;
134
+ let last;
111
135
  try {
112
136
  const { res, body } = await this.raw("GET", path, { query });
113
137
  if (!res.ok) {
114
- if (!(res.status >= 500 && attempt < this.retries)) throw new HttpError(res.status, url, errText(body));
138
+ last = new HttpError(res.status, url, errText(body));
139
+ if (!(res.status >= 500 && attempt < this.retries)) throw last;
140
+ serverWaitMs = retryAfterMs(res);
115
141
  } else {
116
142
  return this.parseJson(res, new TextDecoder().decode(body), url);
117
143
  }
@@ -119,7 +145,10 @@ var Http = class {
119
145
  if (e instanceof HttpError && e.status < 500) throw e;
120
146
  if (attempt >= this.retries) throw e;
121
147
  }
122
- await new Promise((r) => setTimeout(r, Math.floor(Math.random() * Math.min(4e3, 250 * 2 ** attempt))));
148
+ const jitterMs = Math.floor(Math.random() * Math.min(4e3, 250 * 2 ** attempt));
149
+ const waitMs = budget.take(jitterMs, serverWaitMs);
150
+ if (waitMs === null) throw last ?? new HttpError(0, url, "retry wait budget exhausted");
151
+ await new Promise((r) => setTimeout(r, waitMs));
123
152
  }
124
153
  }
125
154
  async postJson(path, body, headers) {
@@ -207,6 +236,8 @@ var RegistryClient = class {
207
236
  };
208
237
 
209
238
  export {
239
+ retryAfterMs,
240
+ RetryWaitBudget,
210
241
  readCapped,
211
242
  Http,
212
243
  RegistryClient,
@@ -1,6 +1,7 @@
1
1
  import {
2
+ CairnError,
2
3
  ContentVerificationError
3
- } from "./chunk-LOAOBV36.js";
4
+ } from "./chunk-OSPAN7GX.js";
4
5
 
5
6
  // src/content.ts
6
7
  import { payloadHash, canonicalJson, verifyContentBytes } from "@inversealtruism/csd-codec";
@@ -24,7 +25,7 @@ var ContentClient = class {
24
25
  bytes: new TextEncoder().encode(canonical)
25
26
  };
26
27
  }
27
- /** Alias for `prepare()` — emphasises this does not itself publish (anchor via board.propose). */
28
+ /** Alias for `prepare()`. It does not publish anything; anchor the hash with a Propose. */
28
29
  put(obj) {
29
30
  return this.prepare(obj);
30
31
  }
@@ -34,12 +35,15 @@ var ContentClient = class {
34
35
  }
35
36
  /**
36
37
  * Fetch content by hash and VERIFY it self-certifies (sha256(bytes) == hash).
37
- * Tries each configured source in order. Returns raw bytes, or null if no
38
- * source holds it. Throws ContentVerificationError if a source returns bytes
39
- * that don't match the hash (tampering / wrong object).
38
+ * Tries each configured source in order; a source that is unreachable, lacks
39
+ * the content or serves bytes that don't match the hash is skipped. Returns the
40
+ * first verified bytes, or null if no source holds it. Throws
41
+ * ContentVerificationError only when no source served verified bytes and at
42
+ * least one served mismatching bytes (tampering / wrong object).
40
43
  */
41
44
  async getBytes(hash) {
42
45
  const h = normalizeHash(hash);
46
+ let mismatched = false;
43
47
  for (const src of this.sources) {
44
48
  let got = null;
45
49
  try {
@@ -49,17 +53,24 @@ var ContentClient = class {
49
53
  }
50
54
  if (!got) continue;
51
55
  if (!verifyContentBytes(got.bytes, h)) {
52
- throw new ContentVerificationError(h);
56
+ mismatched = true;
57
+ continue;
53
58
  }
54
59
  return got.bytes;
55
60
  }
61
+ if (mismatched) throw new ContentVerificationError(h);
56
62
  return null;
57
63
  }
58
- /** Fetch + verify + parse as UTF-8 JSON. Returns null if not found. */
64
+ /** Fetch + verify + parse as UTF-8 JSON. Returns null if not found. Throws a CairnError with code
65
+ * CONTENT_NOT_JSON when the verified bytes are not JSON. */
59
66
  async get(hash) {
60
67
  const bytes = await this.getBytes(hash);
61
68
  if (!bytes) return null;
62
- return JSON.parse(new TextDecoder().decode(bytes));
69
+ try {
70
+ return JSON.parse(new TextDecoder().decode(bytes));
71
+ } catch (e) {
72
+ throw new CairnError(`Content ${normalizeHash(hash)} verified against its hash but is not JSON; use getBytes() for raw content.`, { code: "CONTENT_NOT_JSON", cause: e });
73
+ }
63
74
  }
64
75
  /** Verify already-fetched bytes against a hash (no network). */
65
76
  verify(bytes, hash) {
@@ -113,17 +113,27 @@ var IndexerClient = class {
113
113
  if (!folds) {
114
114
  return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "merkle branch does not fold to the claimed root" };
115
115
  }
116
+ let unverified;
116
117
  if (this.opts.headerMerkleAt) {
118
+ let onchain;
117
119
  try {
118
- const onchain = await this.opts.headerMerkleAt(proof.block_height);
120
+ onchain = await this.opts.headerMerkleAt(proof.block_height);
121
+ } catch (e) {
122
+ onchain = "";
123
+ unverified = `header at ${proof.block_height} not verified (${e?.message ?? String(e)})`;
124
+ }
125
+ if (onchain) {
119
126
  if (norm(onchain) !== norm(proof.merkle_root)) {
120
127
  return { included: false, trustLevel: "not-found", equivocation: true, blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
121
128
  }
122
129
  return { included: true, trustLevel: "verified-inclusion", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root };
123
- } catch {
124
130
  }
131
+ unverified ??= `header source returned no merkle root for ${proof.block_height}`;
132
+ } else {
133
+ unverified = "no header source configured (headerMerkleAt)";
125
134
  }
126
- return { included: true, trustLevel: "proof-consistent", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root };
135
+ const trivial = proof.merkle.length === 0 && norm(proof.merkle_root) === norm(txid) ? "; empty branch (root equals the txid)" : "";
136
+ return { included: false, trustLevel: "proof-consistent", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root, reason: `indexer proof only, ${unverified}${trivial}` };
127
137
  }
128
138
  // ---- live streams ------------------------------------------------------
129
139
  /** Stream all events (blocks, proposals, attestations, reorgs) via SSE. */
@@ -1,14 +1,15 @@
1
1
  import {
2
2
  getWallet
3
- } from "./chunk-SWCUE26T.js";
3
+ } from "./chunk-5RGFC2JR.js";
4
4
 
5
5
  // src/controller.ts
6
6
  var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error: null });
7
7
  var CairnController = class {
8
8
  state = DISCONNECTED;
9
9
  conn = null;
10
- // MF-15: connect/disconnect generation token. disconnect() bumps it; a connect() whose await straddled
11
- // the bump must never commit "connected" over the torn-down session (a zombie null-connection session).
10
+ // Connect/disconnect generation token. disconnect(), the wallet's `disconnect` event and an empty
11
+ // accountsChanged (lock, account switch, revoke) bump it; a connect() whose await straddled the bump must
12
+ // never commit "connected" over the torn-down session.
12
13
  epoch = 0;
13
14
  listeners = /* @__PURE__ */ new Set();
14
15
  getWalletFn;
@@ -39,6 +40,7 @@ var CairnController = class {
39
40
  }
40
41
  onAccounts = (accounts) => {
41
42
  if (!Array.isArray(accounts) || accounts.length === 0) {
43
+ this.epoch++;
42
44
  this.setState({ status: "disconnected", account: null });
43
45
  return;
44
46
  }
@@ -50,6 +52,7 @@ var CairnController = class {
50
52
  }
51
53
  };
52
54
  onDisconnect = () => {
55
+ this.epoch++;
53
56
  this.setState({ status: "disconnected", account: null });
54
57
  };
55
58
  // Detach the provider listeners (ghost-reconnect fix). Idempotent + best-effort.
@@ -1,5 +1,5 @@
1
1
  // src/errors.ts
2
- var SDK_VERSION = "0.4.5";
2
+ var SDK_VERSION = "0.5.0";
3
3
  var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
4
4
  var CairnError = class extends Error {
5
5
  /** Stable machine code for programmatic branching. */
@@ -92,7 +92,7 @@ var SubmitInFlightError = class extends CairnError {
92
92
  }
93
93
  };
94
94
  var NATIVE_CODES = /* @__PURE__ */ new Set(["FIRST_PARTY_ONLY", "RATE_LIMITED", "FORBIDDEN", "INTERNAL"]);
95
- var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
95
+ var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN", "WALLET_UNAVAILABLE", "REVIEW_REQUIRED"]);
96
96
  var NESTED_TERMINAL = /* @__PURE__ */ new Set([
97
97
  "FILL_UNSAFE",
98
98
  "FILL_WRONG_TARGET",
@@ -110,7 +110,11 @@ var NESTED_TERMINAL = /* @__PURE__ */ new Set([
110
110
  "FEE_TOO_LOW",
111
111
  "ACCOUNT_CHANGED",
112
112
  "GHOST_INPUTS_SKIPPED",
113
- "NOTHING_TO_CONSOLIDATE"
113
+ "NOTHING_TO_CONSOLIDATE",
114
+ "OFFER_SOURCE_CONFLICT",
115
+ "FILL_LEGACY_SUNSET",
116
+ "VERIFY_HORIZON",
117
+ "VERIFY_UNREPRESENTABLE"
114
118
  ]);
115
119
  function mapProviderError(error, code) {
116
120
  if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
package/dist/connect.cjs CHANGED
@@ -30,7 +30,7 @@ __export(connect_exports, {
30
30
  module.exports = __toCommonJS(connect_exports);
31
31
 
32
32
  // src/errors.ts
33
- var SDK_VERSION = "0.4.5";
33
+ var SDK_VERSION = "0.5.0";
34
34
  var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
35
35
  var CairnError = class extends Error {
36
36
  /** Stable machine code for programmatic branching. */
@@ -105,7 +105,7 @@ var SubmitInFlightError = class extends CairnError {
105
105
  }
106
106
  };
107
107
  var NATIVE_CODES = /* @__PURE__ */ new Set(["FIRST_PARTY_ONLY", "RATE_LIMITED", "FORBIDDEN", "INTERNAL"]);
108
- var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
108
+ var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN", "WALLET_UNAVAILABLE", "REVIEW_REQUIRED"]);
109
109
  var NESTED_TERMINAL = /* @__PURE__ */ new Set([
110
110
  "FILL_UNSAFE",
111
111
  "FILL_WRONG_TARGET",
@@ -123,7 +123,11 @@ var NESTED_TERMINAL = /* @__PURE__ */ new Set([
123
123
  "FEE_TOO_LOW",
124
124
  "ACCOUNT_CHANGED",
125
125
  "GHOST_INPUTS_SKIPPED",
126
- "NOTHING_TO_CONSOLIDATE"
126
+ "NOTHING_TO_CONSOLIDATE",
127
+ "OFFER_SOURCE_CONFLICT",
128
+ "FILL_LEGACY_SUNSET",
129
+ "VERIFY_HORIZON",
130
+ "VERIFY_UNREPRESENTABLE"
127
131
  ]);
128
132
  function mapProviderError(error, code) {
129
133
  if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
@@ -372,7 +376,7 @@ var WalletConnection = class {
372
376
  send(params) {
373
377
  return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
374
378
  }
375
- /** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
379
+ /** Low-level Propose (always prompts). The caller computes the payload hash, uri and expiry epoch. */
376
380
  propose(params) {
377
381
  return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
378
382
  }
@@ -16,7 +16,7 @@ interface SignInResult {
16
16
  nonce?: string;
17
17
  [k: string]: unknown;
18
18
  }
19
- /** Low-level Propose params (matches the wallet exactly). Prefer `cairn.board.propose()` for ergonomics. */
19
+ /** Low-level Propose params (matches the wallet exactly). `payloadHash` is csd-codec `payloadHash` of your content. */
20
20
  interface ProposeParams {
21
21
  domain: string;
22
22
  payloadHash: string;
@@ -270,7 +270,7 @@ declare class WalletConnection {
270
270
  off(event: string, handler: (data: any) => void): void;
271
271
  /** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
272
272
  send(params: SendParams): Promise<TxResult>;
273
- /** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
273
+ /** Low-level Propose (always prompts). The caller computes the payload hash, uri and expiry epoch. */
274
274
  propose(params: ProposeParams): Promise<TxResult>;
275
275
  /** Attest/support a proposal (always prompts). */
276
276
  attest(params: AttestParams): Promise<TxResult>;
package/dist/connect.d.ts CHANGED
@@ -16,7 +16,7 @@ interface SignInResult {
16
16
  nonce?: string;
17
17
  [k: string]: unknown;
18
18
  }
19
- /** Low-level Propose params (matches the wallet exactly). Prefer `cairn.board.propose()` for ergonomics. */
19
+ /** Low-level Propose params (matches the wallet exactly). `payloadHash` is csd-codec `payloadHash` of your content. */
20
20
  interface ProposeParams {
21
21
  domain: string;
22
22
  payloadHash: string;
@@ -270,7 +270,7 @@ declare class WalletConnection {
270
270
  off(event: string, handler: (data: any) => void): void;
271
271
  /** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
272
272
  send(params: SendParams): Promise<TxResult>;
273
- /** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
273
+ /** Low-level Propose (always prompts). The caller computes the payload hash, uri and expiry epoch. */
274
274
  propose(params: ProposeParams): Promise<TxResult>;
275
275
  /** Attest/support a proposal (always prompts). */
276
276
  attest(params: AttestParams): Promise<TxResult>;
package/dist/connect.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  discoverProviders,
6
6
  getWallet,
7
7
  isInstalled
8
- } from "./chunk-SWCUE26T.js";
9
- import "./chunk-LOAOBV36.js";
8
+ } from "./chunk-5RGFC2JR.js";
9
+ import "./chunk-OSPAN7GX.js";
10
10
  export {
11
11
  WalletConnection,
12
12
  connect,
package/dist/content.cjs CHANGED
@@ -26,7 +26,7 @@ module.exports = __toCommonJS(content_exports);
26
26
  var import_csd_codec = require("@inversealtruism/csd-codec");
27
27
 
28
28
  // src/errors.ts
29
- var SDK_VERSION = "0.4.5";
29
+ var SDK_VERSION = "0.5.0";
30
30
  var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
31
31
  var CairnError = class extends Error {
32
32
  /** Stable machine code for programmatic branching. */
@@ -99,7 +99,7 @@ var ContentClient = class {
99
99
  bytes: new TextEncoder().encode(canonical)
100
100
  };
101
101
  }
102
- /** Alias for `prepare()` — emphasises this does not itself publish (anchor via board.propose). */
102
+ /** Alias for `prepare()`. It does not publish anything; anchor the hash with a Propose. */
103
103
  put(obj) {
104
104
  return this.prepare(obj);
105
105
  }
@@ -109,12 +109,15 @@ var ContentClient = class {
109
109
  }
110
110
  /**
111
111
  * Fetch content by hash and VERIFY it self-certifies (sha256(bytes) == hash).
112
- * Tries each configured source in order. Returns raw bytes, or null if no
113
- * source holds it. Throws ContentVerificationError if a source returns bytes
114
- * that don't match the hash (tampering / wrong object).
112
+ * Tries each configured source in order; a source that is unreachable, lacks
113
+ * the content or serves bytes that don't match the hash is skipped. Returns the
114
+ * first verified bytes, or null if no source holds it. Throws
115
+ * ContentVerificationError only when no source served verified bytes and at
116
+ * least one served mismatching bytes (tampering / wrong object).
115
117
  */
116
118
  async getBytes(hash) {
117
119
  const h = normalizeHash(hash);
120
+ let mismatched = false;
118
121
  for (const src of this.sources) {
119
122
  let got = null;
120
123
  try {
@@ -124,17 +127,24 @@ var ContentClient = class {
124
127
  }
125
128
  if (!got) continue;
126
129
  if (!(0, import_csd_codec.verifyContentBytes)(got.bytes, h)) {
127
- throw new ContentVerificationError(h);
130
+ mismatched = true;
131
+ continue;
128
132
  }
129
133
  return got.bytes;
130
134
  }
135
+ if (mismatched) throw new ContentVerificationError(h);
131
136
  return null;
132
137
  }
133
- /** Fetch + verify + parse as UTF-8 JSON. Returns null if not found. */
138
+ /** Fetch + verify + parse as UTF-8 JSON. Returns null if not found. Throws a CairnError with code
139
+ * CONTENT_NOT_JSON when the verified bytes are not JSON. */
134
140
  async get(hash) {
135
141
  const bytes = await this.getBytes(hash);
136
142
  if (!bytes) return null;
137
- return JSON.parse(new TextDecoder().decode(bytes));
143
+ try {
144
+ return JSON.parse(new TextDecoder().decode(bytes));
145
+ } catch (e) {
146
+ throw new CairnError(`Content ${normalizeHash(hash)} verified against its hash but is not JSON; use getBytes() for raw content.`, { code: "CONTENT_NOT_JSON", cause: e });
147
+ }
138
148
  }
139
149
  /** Verify already-fetched bytes against a hash (no network). */
140
150
  verify(bytes, hash) {
@@ -24,18 +24,21 @@ declare class ContentClient {
24
24
  * This is byte-identical to what the cairn server and wallet compute.
25
25
  */
26
26
  prepare(obj: unknown): PreparedContent;
27
- /** Alias for `prepare()` — emphasises this does not itself publish (anchor via board.propose). */
27
+ /** Alias for `prepare()`. It does not publish anything; anchor the hash with a Propose. */
28
28
  put(obj: unknown): PreparedContent;
29
29
  /** Just the payload hash for an object. */
30
30
  hash(obj: unknown): string;
31
31
  /**
32
32
  * Fetch content by hash and VERIFY it self-certifies (sha256(bytes) == hash).
33
- * Tries each configured source in order. Returns raw bytes, or null if no
34
- * source holds it. Throws ContentVerificationError if a source returns bytes
35
- * that don't match the hash (tampering / wrong object).
33
+ * Tries each configured source in order; a source that is unreachable, lacks
34
+ * the content or serves bytes that don't match the hash is skipped. Returns the
35
+ * first verified bytes, or null if no source holds it. Throws
36
+ * ContentVerificationError only when no source served verified bytes and at
37
+ * least one served mismatching bytes (tampering / wrong object).
36
38
  */
37
39
  getBytes(hash: string): Promise<Uint8Array | null>;
38
- /** Fetch + verify + parse as UTF-8 JSON. Returns null if not found. */
40
+ /** Fetch + verify + parse as UTF-8 JSON. Returns null if not found. Throws a CairnError with code
41
+ * CONTENT_NOT_JSON when the verified bytes are not JSON. */
39
42
  get<T = unknown>(hash: string): Promise<T | null>;
40
43
  /** Verify already-fetched bytes against a hash (no network). */
41
44
  verify(bytes: Uint8Array, hash: string): boolean;
package/dist/content.d.ts CHANGED
@@ -24,18 +24,21 @@ declare class ContentClient {
24
24
  * This is byte-identical to what the cairn server and wallet compute.
25
25
  */
26
26
  prepare(obj: unknown): PreparedContent;
27
- /** Alias for `prepare()` — emphasises this does not itself publish (anchor via board.propose). */
27
+ /** Alias for `prepare()`. It does not publish anything; anchor the hash with a Propose. */
28
28
  put(obj: unknown): PreparedContent;
29
29
  /** Just the payload hash for an object. */
30
30
  hash(obj: unknown): string;
31
31
  /**
32
32
  * Fetch content by hash and VERIFY it self-certifies (sha256(bytes) == hash).
33
- * Tries each configured source in order. Returns raw bytes, or null if no
34
- * source holds it. Throws ContentVerificationError if a source returns bytes
35
- * that don't match the hash (tampering / wrong object).
33
+ * Tries each configured source in order; a source that is unreachable, lacks
34
+ * the content or serves bytes that don't match the hash is skipped. Returns the
35
+ * first verified bytes, or null if no source holds it. Throws
36
+ * ContentVerificationError only when no source served verified bytes and at
37
+ * least one served mismatching bytes (tampering / wrong object).
36
38
  */
37
39
  getBytes(hash: string): Promise<Uint8Array | null>;
38
- /** Fetch + verify + parse as UTF-8 JSON. Returns null if not found. */
40
+ /** Fetch + verify + parse as UTF-8 JSON. Returns null if not found. Throws a CairnError with code
41
+ * CONTENT_NOT_JSON when the verified bytes are not JSON. */
39
42
  get<T = unknown>(hash: string): Promise<T | null>;
40
43
  /** Verify already-fetched bytes against a hash (no network). */
41
44
  verify(bytes: Uint8Array, hash: string): boolean;
package/dist/content.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ContentClient
3
- } from "./chunk-FX5LLMNP.js";
4
- import "./chunk-LOAOBV36.js";
3
+ } from "./chunk-CZAQNZZG.js";
4
+ import "./chunk-OSPAN7GX.js";
5
5
  export {
6
6
  ContentClient
7
7
  };