@inversealtruism/cairn-sdk 0.4.6 → 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 +13 -21
- package/dist/{chunk-FWTPXLGV.js → chunk-5RGFC2JR.js} +2 -2
- package/dist/{chunk-2HP3ZXBO.js → chunk-BL6VFALH.js} +36 -5
- package/dist/{chunk-E4YQRWZZ.js → chunk-CZAQNZZG.js} +19 -8
- package/dist/{chunk-J5J4JEJ6.js → chunk-K5MGEYEU.js} +13 -3
- package/dist/{chunk-OB33SSGA.js → chunk-OE5BEEQY.js} +6 -3
- package/dist/{chunk-ZFVE6MU7.js → chunk-OSPAN7GX.js} +1 -1
- package/dist/connect.cjs +2 -2
- package/dist/connect.d.cts +2 -2
- package/dist/connect.d.ts +2 -2
- package/dist/connect.js +2 -2
- package/dist/content.cjs +18 -8
- package/dist/content.d.cts +8 -5
- package/dist/content.d.ts +8 -5
- package/dist/content.js +2 -2
- package/dist/controller.cjs +7 -4
- package/dist/controller.js +3 -3
- package/dist/index.cjs +214 -227
- package/dist/index.d.cts +52 -22
- package/dist/index.d.ts +52 -22
- package/dist/index.js +144 -53
- package/dist/indexer.cjs +13 -3
- package/dist/indexer.d.cts +3 -0
- package/dist/indexer.d.ts +3 -0
- package/dist/indexer.js +1 -1
- package/dist/react.cjs +12 -5
- package/dist/react.js +8 -4
- package/dist/registry.cjs +34 -5
- package/dist/registry.js +2 -2
- package/package.json +3 -9
- package/dist/board.cjs +0 -231
- package/dist/board.d.cts +0 -151
- package/dist/board.d.ts +0 -151
- package/dist/board.js +0 -7
- package/dist/chunk-E2MA75S3.js +0 -163
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:**
|
|
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
|
-
//
|
|
34
|
-
const { txid } = await cairn.
|
|
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
|
|
41
|
-
cairn.index.streamAll({
|
|
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.
|
|
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,
|
|
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/
|
|
82
|
-
- **`examples/hello-csd/`:** a no-framework browser dApp (connect → read
|
|
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
|
|
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", //
|
|
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.
|
|
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-
|
|
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).
|
|
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-
|
|
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
|
|
79
|
+
this.maxBytes = Number.isFinite(opts.maxBytes) && opts.maxBytes >= 1 ? Math.floor(opts.maxBytes) : DEFAULT_MAX_BYTES;
|
|
60
80
|
this.headers = opts.headers ?? {};
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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()
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
//
|
|
11
|
-
//
|
|
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.
|
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.
|
|
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. */
|
|
@@ -376,7 +376,7 @@ var WalletConnection = class {
|
|
|
376
376
|
send(params) {
|
|
377
377
|
return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
|
|
378
378
|
}
|
|
379
|
-
/** Low-level Propose (always prompts).
|
|
379
|
+
/** Low-level Propose (always prompts). The caller computes the payload hash, uri and expiry epoch. */
|
|
380
380
|
propose(params) {
|
|
381
381
|
return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
|
|
382
382
|
}
|
package/dist/connect.d.cts
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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
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.
|
|
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()
|
|
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
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/content.d.cts
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()
|
|
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
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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()
|
|
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
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
package/dist/controller.cjs
CHANGED
|
@@ -25,7 +25,7 @@ __export(controller_exports, {
|
|
|
25
25
|
module.exports = __toCommonJS(controller_exports);
|
|
26
26
|
|
|
27
27
|
// src/errors.ts
|
|
28
|
-
var SDK_VERSION = "0.
|
|
28
|
+
var SDK_VERSION = "0.5.0";
|
|
29
29
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
30
30
|
var CairnError = class extends Error {
|
|
31
31
|
/** Stable machine code for programmatic branching. */
|
|
@@ -348,7 +348,7 @@ var WalletConnection = class {
|
|
|
348
348
|
send(params) {
|
|
349
349
|
return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
|
|
350
350
|
}
|
|
351
|
-
/** Low-level Propose (always prompts).
|
|
351
|
+
/** Low-level Propose (always prompts). The caller computes the payload hash, uri and expiry epoch. */
|
|
352
352
|
propose(params) {
|
|
353
353
|
return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
|
|
354
354
|
}
|
|
@@ -377,8 +377,9 @@ var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error:
|
|
|
377
377
|
var CairnController = class {
|
|
378
378
|
state = DISCONNECTED;
|
|
379
379
|
conn = null;
|
|
380
|
-
//
|
|
381
|
-
//
|
|
380
|
+
// Connect/disconnect generation token. disconnect(), the wallet's `disconnect` event and an empty
|
|
381
|
+
// accountsChanged (lock, account switch, revoke) bump it; a connect() whose await straddled the bump must
|
|
382
|
+
// never commit "connected" over the torn-down session.
|
|
382
383
|
epoch = 0;
|
|
383
384
|
listeners = /* @__PURE__ */ new Set();
|
|
384
385
|
getWalletFn;
|
|
@@ -409,6 +410,7 @@ var CairnController = class {
|
|
|
409
410
|
}
|
|
410
411
|
onAccounts = (accounts) => {
|
|
411
412
|
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
413
|
+
this.epoch++;
|
|
412
414
|
this.setState({ status: "disconnected", account: null });
|
|
413
415
|
return;
|
|
414
416
|
}
|
|
@@ -420,6 +422,7 @@ var CairnController = class {
|
|
|
420
422
|
}
|
|
421
423
|
};
|
|
422
424
|
onDisconnect = () => {
|
|
425
|
+
this.epoch++;
|
|
423
426
|
this.setState({ status: "disconnected", account: null });
|
|
424
427
|
};
|
|
425
428
|
// Detach the provider listeners (ghost-reconnect fix). Idempotent + best-effort.
|
package/dist/controller.js
CHANGED