@inversealtruism/cairn-sdk 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/board.cjs +1 -1
- package/dist/board.d.cts +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +2 -2
- package/dist/chain.cjs +1 -1
- package/dist/chain.d.cts +1 -1
- package/dist/chain.d.ts +1 -1
- package/dist/chain.js +1 -1
- package/dist/{chunk-G7FABIF4.js → chunk-BGJZF7S2.js} +11 -2
- package/dist/{chunk-5KAUQH72.js → chunk-C452UODV.js} +1 -1
- package/dist/{chunk-645CYBVN.js → chunk-FSQZVNEM.js} +1 -1
- package/dist/{chunk-G4OZKZSJ.js → chunk-K7SJOW3O.js} +1 -1
- package/dist/{chunk-ZAE2BIH5.js → chunk-MBSPHWZG.js} +21 -3
- package/dist/{chunk-OSXS26RA.js → chunk-MVX7VQT4.js} +4 -3
- package/dist/{chunk-FNBE2FPE.js → chunk-OG4KA3LS.js} +49 -17
- package/dist/{chunk-UMGWVOOT.js → chunk-RELP23CH.js} +1 -1
- package/dist/connect.cjs +1 -1
- package/dist/connect.d.cts +6 -0
- package/dist/connect.d.ts +6 -0
- package/dist/connect.js +2 -2
- package/dist/content.cjs +4 -3
- package/dist/content.d.cts +1 -1
- package/dist/content.d.ts +1 -1
- package/dist/content.js +2 -2
- package/dist/controller.cjs +21 -3
- package/dist/controller.d.cts +1 -0
- package/dist/controller.d.ts +1 -0
- package/dist/controller.js +3 -3
- package/dist/{http-D2CzfOsm.d.ts → http-CQsNL8w9.d.cts} +5 -0
- package/dist/{http-D2CzfOsm.d.cts → http-CQsNL8w9.d.ts} +5 -0
- package/dist/index.cjs +134 -30
- package/dist/index.d.cts +9 -5
- package/dist/index.d.ts +9 -5
- package/dist/index.js +56 -13
- package/dist/indexer.cjs +49 -17
- package/dist/indexer.d.cts +15 -4
- package/dist/indexer.d.ts +15 -4
- package/dist/indexer.js +1 -1
- package/dist/react.cjs +21 -3
- package/dist/react.js +3 -3
- package/dist/registry.cjs +11 -2
- package/dist/registry.d.cts +12 -3
- package/dist/registry.d.ts +12 -3
- package/dist/registry.js +1 -1
- package/package.json +29 -22
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ __export(index_exports, {
|
|
|
27
27
|
Chain: () => Chain,
|
|
28
28
|
ContentClient: () => ContentClient,
|
|
29
29
|
ContentVerificationError: () => ContentVerificationError,
|
|
30
|
+
DEFAULT_SPV_CHECKPOINT: () => DEFAULT_SPV_CHECKPOINT,
|
|
30
31
|
Http: () => Http,
|
|
31
32
|
HttpError: () => HttpError,
|
|
32
33
|
IndexerClient: () => IndexerClient,
|
|
@@ -48,7 +49,7 @@ __export(index_exports, {
|
|
|
48
49
|
module.exports = __toCommonJS(index_exports);
|
|
49
50
|
|
|
50
51
|
// src/errors.ts
|
|
51
|
-
var SDK_VERSION = "0.1.
|
|
52
|
+
var SDK_VERSION = "0.1.4";
|
|
52
53
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
53
54
|
var CairnError = class extends Error {
|
|
54
55
|
/** Stable machine code for programmatic branching. */
|
|
@@ -135,10 +136,48 @@ function errorCode(err) {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
// src/http.ts
|
|
139
|
+
var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
140
|
+
var ERR_TEXT_CAP = 8 * 1024;
|
|
141
|
+
async function readCapped(res, maxBytes) {
|
|
142
|
+
const cl = Number(res.headers.get("content-length"));
|
|
143
|
+
if (Number.isFinite(cl) && cl > maxBytes) throw new HttpError(res.status, res.url, `response too large (content-length ${cl} > ${maxBytes})`);
|
|
144
|
+
const body = res.body;
|
|
145
|
+
if (!body || typeof body.getReader !== "function") {
|
|
146
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
147
|
+
if (buf.byteLength > maxBytes) throw new HttpError(res.status, res.url, `response too large (${buf.byteLength} > ${maxBytes})`);
|
|
148
|
+
return buf;
|
|
149
|
+
}
|
|
150
|
+
const reader = body.getReader();
|
|
151
|
+
const chunks = [];
|
|
152
|
+
let total = 0;
|
|
153
|
+
for (; ; ) {
|
|
154
|
+
const { done, value } = await reader.read();
|
|
155
|
+
if (done) break;
|
|
156
|
+
if (value) {
|
|
157
|
+
total += value.byteLength;
|
|
158
|
+
if (total > maxBytes) {
|
|
159
|
+
try {
|
|
160
|
+
await reader.cancel();
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
throw new HttpError(res.status, res.url, `response exceeded ${maxBytes} bytes`);
|
|
164
|
+
}
|
|
165
|
+
chunks.push(value);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const out = new Uint8Array(total);
|
|
169
|
+
let off = 0;
|
|
170
|
+
for (const c of chunks) {
|
|
171
|
+
out.set(c, off);
|
|
172
|
+
off += c.byteLength;
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
138
176
|
var Http = class {
|
|
139
177
|
baseUrl;
|
|
140
178
|
_fetch;
|
|
141
179
|
timeoutMs;
|
|
180
|
+
maxBytes;
|
|
142
181
|
headers;
|
|
143
182
|
constructor(opts) {
|
|
144
183
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
@@ -146,12 +185,17 @@ var Http = class {
|
|
|
146
185
|
if (!f) throw new Error("No fetch available \u2014 pass `fetch` in the SDK options.");
|
|
147
186
|
this._fetch = opts.fetch ? opts.fetch : f.bind(globalThis);
|
|
148
187
|
this.timeoutMs = opts.timeoutMs ?? 15e3;
|
|
188
|
+
this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
149
189
|
this.headers = opts.headers ?? {};
|
|
150
190
|
}
|
|
151
191
|
/** Build an absolute URL from a path + optional query params. */
|
|
152
192
|
url(path, query) {
|
|
153
193
|
const p = path.startsWith("/") ? path : `/${path}`;
|
|
154
194
|
const u = new URL(this.baseUrl + p);
|
|
195
|
+
const basePath = new URL(this.baseUrl + "/").pathname;
|
|
196
|
+
if (basePath !== "/" && !(u.pathname + "/").startsWith(basePath)) {
|
|
197
|
+
throw new Error(`illegal path '${path}' \u2014 escapes base ${basePath}`);
|
|
198
|
+
}
|
|
155
199
|
if (query) {
|
|
156
200
|
for (const [k, v] of Object.entries(query)) {
|
|
157
201
|
if (v !== void 0 && v !== null) u.searchParams.set(k, String(v));
|
|
@@ -182,25 +226,24 @@ var Http = class {
|
|
|
182
226
|
async getJson(path, query) {
|
|
183
227
|
const res = await this.raw("GET", path, { query });
|
|
184
228
|
if (!res.ok) throw new HttpError(res.status, this.url(path, query), await safeText(res));
|
|
185
|
-
return await res.
|
|
229
|
+
return JSON.parse(new TextDecoder().decode(await readCapped(res, this.maxBytes)));
|
|
186
230
|
}
|
|
187
231
|
async postJson(path, body, headers) {
|
|
188
232
|
const res = await this.raw("POST", path, { body, headers });
|
|
189
233
|
if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
|
|
190
|
-
return await res.
|
|
234
|
+
return JSON.parse(new TextDecoder().decode(await readCapped(res, this.maxBytes)));
|
|
191
235
|
}
|
|
192
236
|
/** GET raw bytes (for content retrieval). Returns null on 404. */
|
|
193
237
|
async getBytes(path) {
|
|
194
238
|
const res = await this.raw("GET", path);
|
|
195
239
|
if (res.status === 404) return null;
|
|
196
240
|
if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
|
|
197
|
-
|
|
198
|
-
return { bytes: buf, headers: res.headers };
|
|
241
|
+
return { bytes: await readCapped(res, this.maxBytes), headers: res.headers };
|
|
199
242
|
}
|
|
200
243
|
};
|
|
201
244
|
async function safeText(res) {
|
|
202
245
|
try {
|
|
203
|
-
return await res
|
|
246
|
+
return new TextDecoder().decode(await readCapped(res, ERR_TEXT_CAP));
|
|
204
247
|
} catch {
|
|
205
248
|
return "";
|
|
206
249
|
}
|
|
@@ -257,7 +300,7 @@ var Chain = class {
|
|
|
257
300
|
});
|
|
258
301
|
if (!built.ok || p.broadcast === false) return built;
|
|
259
302
|
const submitResult = await this.client.submit(built.nodeJson);
|
|
260
|
-
return { ...built, submitResult };
|
|
303
|
+
return { ...built, ok: built.ok && submitResult.ok !== false, submitResult };
|
|
261
304
|
}
|
|
262
305
|
/** A verifying light client over the same RPC (built once, reused). */
|
|
263
306
|
light() {
|
|
@@ -426,6 +469,13 @@ function sleep(ms) {
|
|
|
426
469
|
|
|
427
470
|
// src/indexer.ts
|
|
428
471
|
var import_csd_codec4 = require("@inversealtruism/csd-codec");
|
|
472
|
+
var HEX64 = /^0x[0-9a-fA-F]{64}$/;
|
|
473
|
+
var MAX_SSE_BUF = 1024 * 1024;
|
|
474
|
+
function isValidMerkleProof(p) {
|
|
475
|
+
if (!p || typeof p !== "object") return false;
|
|
476
|
+
const m = p;
|
|
477
|
+
return Number.isInteger(m.block_height) && m.block_height >= 0 && Number.isInteger(m.pos) && m.pos >= 0 && typeof m.merkle_root === "string" && HEX64.test(m.merkle_root) && Array.isArray(m.merkle) && m.merkle.every((x) => typeof x === "string" && HEX64.test(x));
|
|
478
|
+
}
|
|
429
479
|
var IndexerClient = class {
|
|
430
480
|
http;
|
|
431
481
|
opts;
|
|
@@ -446,32 +496,35 @@ var IndexerClient = class {
|
|
|
446
496
|
blockHashAtHeight(h) {
|
|
447
497
|
return this.http.getJson(`/block-height/${h}`);
|
|
448
498
|
}
|
|
499
|
+
// CAIRNSDK-CONTENT-HASH-PATH-1: encode every caller-supplied id segment. Raw interpolation let an id like
|
|
500
|
+
// `../../api/rpc/tip` walk `new URL` to a DIFFERENT same-origin endpoint whose body was then returned
|
|
501
|
+
// verbatim as the requested object (worse than the content case, which discards the walked-to response).
|
|
449
502
|
block(hash) {
|
|
450
|
-
return this.http.getJson(`/block/${hash}`);
|
|
503
|
+
return this.http.getJson(`/block/${encodeURIComponent(hash)}`);
|
|
451
504
|
}
|
|
452
505
|
blockTxids(hash) {
|
|
453
|
-
return this.http.getJson(`/block/${hash}/txids`);
|
|
506
|
+
return this.http.getJson(`/block/${encodeURIComponent(hash)}/txids`);
|
|
454
507
|
}
|
|
455
508
|
tx(id) {
|
|
456
|
-
return this.http.getJson(`/tx/${id}`);
|
|
509
|
+
return this.http.getJson(`/tx/${encodeURIComponent(id)}`);
|
|
457
510
|
}
|
|
458
511
|
txStatus(id) {
|
|
459
|
-
return this.http.getJson(`/tx/${id}/status`);
|
|
512
|
+
return this.http.getJson(`/tx/${encodeURIComponent(id)}/status`);
|
|
460
513
|
}
|
|
461
514
|
txMerkleProof(id) {
|
|
462
|
-
return this.http.getJson(`/tx/${id}/merkle-proof`);
|
|
515
|
+
return this.http.getJson(`/tx/${encodeURIComponent(id)}/merkle-proof`);
|
|
463
516
|
}
|
|
464
517
|
address(a) {
|
|
465
|
-
return this.http.getJson(`/address/${a}`);
|
|
518
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}`);
|
|
466
519
|
}
|
|
467
520
|
addressTxs(a) {
|
|
468
|
-
return this.http.getJson(`/address/${a}/txs`);
|
|
521
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}/txs`);
|
|
469
522
|
}
|
|
470
523
|
addressUtxo(a) {
|
|
471
|
-
return this.http.getJson(`/address/${a}/utxo`);
|
|
524
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}/utxo`);
|
|
472
525
|
}
|
|
473
526
|
reputation(a) {
|
|
474
|
-
return this.http.getJson(`/address/${a}/reputation`);
|
|
527
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}/reputation`);
|
|
475
528
|
}
|
|
476
529
|
domains() {
|
|
477
530
|
return this.http.getJson("/domains");
|
|
@@ -480,10 +533,10 @@ var IndexerClient = class {
|
|
|
480
533
|
return this.http.getJson(`/domain/${encodeURIComponent(d)}/proposals`);
|
|
481
534
|
}
|
|
482
535
|
proposal(id) {
|
|
483
|
-
return this.http.getJson(`/proposal/${id}`);
|
|
536
|
+
return this.http.getJson(`/proposal/${encodeURIComponent(id)}`);
|
|
484
537
|
}
|
|
485
538
|
attestations(id) {
|
|
486
|
-
return this.http.getJson(`/proposal/${id}/attestations`);
|
|
539
|
+
return this.http.getJson(`/proposal/${encodeURIComponent(id)}/attestations`);
|
|
487
540
|
}
|
|
488
541
|
registryPeers() {
|
|
489
542
|
return this.http.getJson("/registry/peers");
|
|
@@ -495,7 +548,7 @@ var IndexerClient = class {
|
|
|
495
548
|
return this.http.getJson(`/identity/${encodeURIComponent(handle)}`);
|
|
496
549
|
}
|
|
497
550
|
reverseIdentity(addr) {
|
|
498
|
-
return this.http.getJson(`/address/${addr}/identity`);
|
|
551
|
+
return this.http.getJson(`/address/${encodeURIComponent(addr)}/identity`);
|
|
499
552
|
}
|
|
500
553
|
// ---- trust-minimized inclusion ----------------------------------------
|
|
501
554
|
/**
|
|
@@ -504,9 +557,11 @@ var IndexerClient = class {
|
|
|
504
557
|
* proof's root is cross-checked against the header merkle at that height →
|
|
505
558
|
* trustLevel "verified-inclusion" (only as trustworthy as that header source —
|
|
506
559
|
* see headerMerkleAt). Otherwise the proof is only internally consistent →
|
|
507
|
-
* "proof-consistent" (still useful, but the indexer is trusted).
|
|
508
|
-
*
|
|
509
|
-
*
|
|
560
|
+
* "proof-consistent" (still useful, but the indexer is trusted). INDEXER-DOC-STALE-1: the Cairn facade now
|
|
561
|
+
* wires headerMerkleAt UNCONDITIONALLY via a PoW-verifying checkpoint light client (index.ts), so the
|
|
562
|
+
* default reaches "verified-inclusion" when the proof root matches the PoW-verified header — what makes a
|
|
563
|
+
* same-origin RPC safe is the forward PoW re-verification, not an origin gate (which no longer exists).
|
|
564
|
+
* Fails closed: a malformed proof or a chain-disagreement returns "not-found" (the latter with equivocation:true).
|
|
510
565
|
*/
|
|
511
566
|
async verifyInclusion(txid2) {
|
|
512
567
|
let proof;
|
|
@@ -515,7 +570,15 @@ var IndexerClient = class {
|
|
|
515
570
|
} catch {
|
|
516
571
|
return { included: false, trustLevel: "not-found", reason: "no merkle proof (tx not indexed)" };
|
|
517
572
|
}
|
|
518
|
-
|
|
573
|
+
if (!isValidMerkleProof(proof)) {
|
|
574
|
+
return { included: false, trustLevel: "not-found", reason: "malformed merkle proof" };
|
|
575
|
+
}
|
|
576
|
+
let folds = false;
|
|
577
|
+
try {
|
|
578
|
+
folds = (0, import_csd_codec4.verifyMerkleProof)(txid2, proof.pos, proof.merkle, proof.merkle_root);
|
|
579
|
+
} catch {
|
|
580
|
+
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "malformed merkle proof" };
|
|
581
|
+
}
|
|
519
582
|
if (!folds) {
|
|
520
583
|
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "merkle branch does not fold to the claimed root" };
|
|
521
584
|
}
|
|
@@ -523,7 +586,7 @@ var IndexerClient = class {
|
|
|
523
586
|
try {
|
|
524
587
|
const onchain = await this.opts.headerMerkleAt(proof.block_height);
|
|
525
588
|
if (norm(onchain) !== norm(proof.merkle_root)) {
|
|
526
|
-
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
|
|
589
|
+
return { included: false, trustLevel: "not-found", equivocation: true, blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
|
|
527
590
|
}
|
|
528
591
|
return { included: true, trustLevel: "verified-inclusion", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root };
|
|
529
592
|
} catch {
|
|
@@ -547,6 +610,11 @@ var IndexerClient = class {
|
|
|
547
610
|
/**
|
|
548
611
|
* Subscribe over WebSocket with selective tracking. Returns a handle with
|
|
549
612
|
* `.close()`. `onEvent` receives every matching IndexEvent (incl. reorgs).
|
|
613
|
+
*
|
|
614
|
+
* WS-NORECONNECT-1: unlike the SSE feeds (streamAll/streamBlocks/streamDomain, which auto-reconnect with
|
|
615
|
+
* backoff), this WebSocket subscription does NOT auto-reconnect — it surfaces a terminal error via
|
|
616
|
+
* `onError` (when provided) and stops. A consumer tracking reorgs over WS MUST re-`subscribe()` on error
|
|
617
|
+
* to avoid a silently-stale view; for built-in reconnect prefer the SSE `stream*` methods.
|
|
550
618
|
*/
|
|
551
619
|
subscribe(sub, onEvent, onError) {
|
|
552
620
|
const WS = this.opts.WebSocketImpl ?? globalThis.WebSocket;
|
|
@@ -609,6 +677,13 @@ var IndexerClient = class {
|
|
|
609
677
|
buf = buf.slice(idx + 2);
|
|
610
678
|
dispatchFrame(frame, handlers);
|
|
611
679
|
}
|
|
680
|
+
if (buf.length > MAX_SSE_BUF) {
|
|
681
|
+
try {
|
|
682
|
+
await reader.cancel();
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
throw new Error(`SSE buffer exceeded ${MAX_SSE_BUF} bytes with no frame delimiter \u2014 aborting`);
|
|
686
|
+
}
|
|
612
687
|
}
|
|
613
688
|
} catch (err) {
|
|
614
689
|
if (closed) return;
|
|
@@ -721,8 +796,9 @@ var ContentClient = class {
|
|
|
721
796
|
}
|
|
722
797
|
};
|
|
723
798
|
function normalizeHash(hash) {
|
|
724
|
-
const h = hash.startsWith("0x") || hash.startsWith("0X") ? hash : `0x${hash}
|
|
725
|
-
|
|
799
|
+
const h = (hash.startsWith("0x") || hash.startsWith("0X") ? hash : `0x${hash}`).toLowerCase();
|
|
800
|
+
if (!/^0x[0-9a-f]{64}$/.test(h)) throw new Error(`invalid content hash: ${hash}`);
|
|
801
|
+
return h;
|
|
726
802
|
}
|
|
727
803
|
|
|
728
804
|
// src/registry.ts
|
|
@@ -741,11 +817,20 @@ var RegistryClient = class {
|
|
|
741
817
|
peers() {
|
|
742
818
|
return (0, import_csd_registry.discoverPeers)(this.src);
|
|
743
819
|
}
|
|
744
|
-
/**
|
|
820
|
+
/**
|
|
821
|
+
* Resolve a handle → identity (GET /identity/:handle). Returns null if unresolved.
|
|
822
|
+
*
|
|
823
|
+
* ⚠ TRUST (CAIRN-SDK-RESOLVE-VERIFIED-FOOTGUN): the returned `verified` flag is the INDEXER's assertion,
|
|
824
|
+
* NOT a client-side proof. A hostile/compromised indexer (or a MITM of the read proxy) can return
|
|
825
|
+
* `verified:true` for a wrong address. Do NOT use this directly as a payee/login target without either (a)
|
|
826
|
+
* recomputing from raw records via `fromRecords.name(...)`, or (b) cross-checking a second independent
|
|
827
|
+
* source (the wallet's namespv union cure). Treat `verified` here as a hint, not a guarantee.
|
|
828
|
+
*/
|
|
745
829
|
resolveName(handle) {
|
|
746
830
|
return (0, import_csd_registry.resolveName)(this.src, handle);
|
|
747
831
|
}
|
|
748
|
-
/** Reverse-resolve an address → identity (GET /address/:addr/identity).
|
|
832
|
+
/** Reverse-resolve an address → identity (GET /address/:addr/identity).
|
|
833
|
+
* ⚠ Same indexer-trust caveat as resolveName — `verified` is the indexer's claim, not a client proof. */
|
|
749
834
|
reverseName(address) {
|
|
750
835
|
return (0, import_csd_registry.reverseName)(this.src, address);
|
|
751
836
|
}
|
|
@@ -987,12 +1072,28 @@ var CairnController = class {
|
|
|
987
1072
|
for (const l of [...this.listeners]) l();
|
|
988
1073
|
}
|
|
989
1074
|
onAccounts = (accounts) => {
|
|
990
|
-
if (!Array.isArray(accounts) || accounts.length === 0)
|
|
991
|
-
|
|
1075
|
+
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
1076
|
+
this.setState({ status: "disconnected", account: null });
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const next = String(accounts[0]);
|
|
1080
|
+
if (this.state.status === "connected" && this.state.account && next.toLowerCase() === this.state.account.toLowerCase()) {
|
|
1081
|
+
this.setState({ status: "connected", account: next, error: null });
|
|
1082
|
+
} else {
|
|
1083
|
+
this.setState({ status: "disconnected", account: null });
|
|
1084
|
+
}
|
|
992
1085
|
};
|
|
993
1086
|
onDisconnect = () => {
|
|
994
1087
|
this.setState({ status: "disconnected", account: null });
|
|
995
1088
|
};
|
|
1089
|
+
// Detach the provider listeners (ghost-reconnect fix). Idempotent + best-effort.
|
|
1090
|
+
detach() {
|
|
1091
|
+
try {
|
|
1092
|
+
this.conn?.off?.("accountsChanged", this.onAccounts);
|
|
1093
|
+
this.conn?.off?.("disconnect", this.onDisconnect);
|
|
1094
|
+
} catch {
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
996
1097
|
/** Detect + connect (prompts the user the first time per origin). Resolves the connected address. */
|
|
997
1098
|
connect = async () => {
|
|
998
1099
|
this.setState({ status: "connecting", error: null });
|
|
@@ -1016,6 +1117,8 @@ var CairnController = class {
|
|
|
1016
1117
|
await this.conn?.revokePermissions();
|
|
1017
1118
|
} catch {
|
|
1018
1119
|
}
|
|
1120
|
+
this.detach();
|
|
1121
|
+
this.conn = null;
|
|
1019
1122
|
this.setState({ status: "disconnected", account: null, error: null });
|
|
1020
1123
|
};
|
|
1021
1124
|
/** Audience-bound sign-in (must connect() first). Returns the signed artifact to verify server-side. */
|
|
@@ -1114,6 +1217,7 @@ var Cairn = class {
|
|
|
1114
1217
|
Chain,
|
|
1115
1218
|
ContentClient,
|
|
1116
1219
|
ContentVerificationError,
|
|
1220
|
+
DEFAULT_SPV_CHECKPOINT,
|
|
1117
1221
|
Http,
|
|
1118
1222
|
HttpError,
|
|
1119
1223
|
IndexerClient,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { F as FetchLike } from './http-
|
|
2
|
-
export { H as Http, a as HttpOptions } from './http-
|
|
1
|
+
import { F as FetchLike } from './http-CQsNL8w9.cjs';
|
|
2
|
+
export { H as Http, a as HttpOptions } from './http-CQsNL8w9.cjs';
|
|
3
3
|
import { Chain } from './chain.cjs';
|
|
4
4
|
import { BoardClient } from './board.cjs';
|
|
5
5
|
export { BoardItemContent, BoardWindow, ProposeInput, ProposeResult, RankedItem, SupportInput } from './board.cjs';
|
|
@@ -20,7 +20,7 @@ import '@inversealtruism/csd-registry';
|
|
|
20
20
|
|
|
21
21
|
/** Stable machine-readable error codes (branch on these, not the message text). */
|
|
22
22
|
type CairnErrorCode = "NOT_INSTALLED" | "USER_REJECTED" | "WALLET_LOCKED" | "UNSUPPORTED_METHOD" | "HTTP_ERROR" | "CONTENT_VERIFICATION" | "UNKNOWN";
|
|
23
|
-
declare const SDK_VERSION = "0.1.
|
|
23
|
+
declare const SDK_VERSION = "0.1.4";
|
|
24
24
|
interface CairnErrorOptions {
|
|
25
25
|
code?: CairnErrorCode;
|
|
26
26
|
docsPath?: string;
|
|
@@ -34,7 +34,7 @@ declare class CairnError extends Error {
|
|
|
34
34
|
/** Relative docs path (under the SDK docs base), if any. */
|
|
35
35
|
readonly docsPath?: string;
|
|
36
36
|
/** The cairn-sdk version that produced this error. */
|
|
37
|
-
readonly version = "0.1.
|
|
37
|
+
readonly version = "0.1.4";
|
|
38
38
|
constructor(shortMessage: string, opts?: CairnErrorOptions);
|
|
39
39
|
/**
|
|
40
40
|
* Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
|
|
@@ -122,6 +122,10 @@ interface CairnConfig {
|
|
|
122
122
|
hash: string;
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
|
+
declare const DEFAULT_SPV_CHECKPOINT: {
|
|
126
|
+
height: number;
|
|
127
|
+
hash: string;
|
|
128
|
+
};
|
|
125
129
|
declare class Cairn {
|
|
126
130
|
readonly config: CairnConfig;
|
|
127
131
|
readonly chain: Chain;
|
|
@@ -155,4 +159,4 @@ declare class Cairn {
|
|
|
155
159
|
get connected(): boolean;
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, type CairnErrorCode, type CairnErrorOptions, Chain, ContentClient, ContentVerificationError, DetectOptions, FetchLike, HttpError, IndexerClient, NotInstalledError, RegistryClient, SDK_VERSION, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError, errorCode, mapProviderError };
|
|
162
|
+
export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, type CairnErrorCode, type CairnErrorOptions, Chain, ContentClient, ContentVerificationError, DEFAULT_SPV_CHECKPOINT, DetectOptions, FetchLike, HttpError, IndexerClient, NotInstalledError, RegistryClient, SDK_VERSION, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError, errorCode, mapProviderError };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { F as FetchLike } from './http-
|
|
2
|
-
export { H as Http, a as HttpOptions } from './http-
|
|
1
|
+
import { F as FetchLike } from './http-CQsNL8w9.js';
|
|
2
|
+
export { H as Http, a as HttpOptions } from './http-CQsNL8w9.js';
|
|
3
3
|
import { Chain } from './chain.js';
|
|
4
4
|
import { BoardClient } from './board.js';
|
|
5
5
|
export { BoardItemContent, BoardWindow, ProposeInput, ProposeResult, RankedItem, SupportInput } from './board.js';
|
|
@@ -20,7 +20,7 @@ import '@inversealtruism/csd-registry';
|
|
|
20
20
|
|
|
21
21
|
/** Stable machine-readable error codes (branch on these, not the message text). */
|
|
22
22
|
type CairnErrorCode = "NOT_INSTALLED" | "USER_REJECTED" | "WALLET_LOCKED" | "UNSUPPORTED_METHOD" | "HTTP_ERROR" | "CONTENT_VERIFICATION" | "UNKNOWN";
|
|
23
|
-
declare const SDK_VERSION = "0.1.
|
|
23
|
+
declare const SDK_VERSION = "0.1.4";
|
|
24
24
|
interface CairnErrorOptions {
|
|
25
25
|
code?: CairnErrorCode;
|
|
26
26
|
docsPath?: string;
|
|
@@ -34,7 +34,7 @@ declare class CairnError extends Error {
|
|
|
34
34
|
/** Relative docs path (under the SDK docs base), if any. */
|
|
35
35
|
readonly docsPath?: string;
|
|
36
36
|
/** The cairn-sdk version that produced this error. */
|
|
37
|
-
readonly version = "0.1.
|
|
37
|
+
readonly version = "0.1.4";
|
|
38
38
|
constructor(shortMessage: string, opts?: CairnErrorOptions);
|
|
39
39
|
/**
|
|
40
40
|
* Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
|
|
@@ -122,6 +122,10 @@ interface CairnConfig {
|
|
|
122
122
|
hash: string;
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
|
+
declare const DEFAULT_SPV_CHECKPOINT: {
|
|
126
|
+
height: number;
|
|
127
|
+
hash: string;
|
|
128
|
+
};
|
|
125
129
|
declare class Cairn {
|
|
126
130
|
readonly config: CairnConfig;
|
|
127
131
|
readonly chain: Chain;
|
|
@@ -155,4 +159,4 @@ declare class Cairn {
|
|
|
155
159
|
get connected(): boolean;
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, type CairnErrorCode, type CairnErrorOptions, Chain, ContentClient, ContentVerificationError, DetectOptions, FetchLike, HttpError, IndexerClient, NotInstalledError, RegistryClient, SDK_VERSION, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError, errorCode, mapProviderError };
|
|
162
|
+
export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, type CairnErrorCode, type CairnErrorOptions, Chain, ContentClient, ContentVerificationError, DEFAULT_SPV_CHECKPOINT, DetectOptions, FetchLike, HttpError, IndexerClient, NotInstalledError, RegistryClient, SDK_VERSION, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError, errorCode, mapProviderError };
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RegistryClient
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BGJZF7S2.js";
|
|
4
4
|
import {
|
|
5
5
|
BoardClient
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-FSQZVNEM.js";
|
|
7
7
|
import {
|
|
8
8
|
Chain,
|
|
9
9
|
LightClient
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-C452UODV.js";
|
|
11
11
|
import {
|
|
12
12
|
ContentClient
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-MVX7VQT4.js";
|
|
14
14
|
import {
|
|
15
15
|
IndexerClient
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-OG4KA3LS.js";
|
|
17
17
|
import {
|
|
18
18
|
CairnController
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-MBSPHWZG.js";
|
|
20
20
|
import {
|
|
21
21
|
WalletConnection,
|
|
22
22
|
connect,
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
discoverProviders,
|
|
25
25
|
getWallet,
|
|
26
26
|
isInstalled
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-K7SJOW3O.js";
|
|
28
28
|
import {
|
|
29
29
|
CairnError,
|
|
30
30
|
ContentVerificationError,
|
|
@@ -36,13 +36,51 @@ import {
|
|
|
36
36
|
WalletLockedError,
|
|
37
37
|
errorCode,
|
|
38
38
|
mapProviderError
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-RELP23CH.js";
|
|
40
40
|
|
|
41
41
|
// src/http.ts
|
|
42
|
+
var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
43
|
+
var ERR_TEXT_CAP = 8 * 1024;
|
|
44
|
+
async function readCapped(res, maxBytes) {
|
|
45
|
+
const cl = Number(res.headers.get("content-length"));
|
|
46
|
+
if (Number.isFinite(cl) && cl > maxBytes) throw new HttpError(res.status, res.url, `response too large (content-length ${cl} > ${maxBytes})`);
|
|
47
|
+
const body = res.body;
|
|
48
|
+
if (!body || typeof body.getReader !== "function") {
|
|
49
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
50
|
+
if (buf.byteLength > maxBytes) throw new HttpError(res.status, res.url, `response too large (${buf.byteLength} > ${maxBytes})`);
|
|
51
|
+
return buf;
|
|
52
|
+
}
|
|
53
|
+
const reader = body.getReader();
|
|
54
|
+
const chunks = [];
|
|
55
|
+
let total = 0;
|
|
56
|
+
for (; ; ) {
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
if (done) break;
|
|
59
|
+
if (value) {
|
|
60
|
+
total += value.byteLength;
|
|
61
|
+
if (total > maxBytes) {
|
|
62
|
+
try {
|
|
63
|
+
await reader.cancel();
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
throw new HttpError(res.status, res.url, `response exceeded ${maxBytes} bytes`);
|
|
67
|
+
}
|
|
68
|
+
chunks.push(value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const out = new Uint8Array(total);
|
|
72
|
+
let off = 0;
|
|
73
|
+
for (const c of chunks) {
|
|
74
|
+
out.set(c, off);
|
|
75
|
+
off += c.byteLength;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
42
79
|
var Http = class {
|
|
43
80
|
baseUrl;
|
|
44
81
|
_fetch;
|
|
45
82
|
timeoutMs;
|
|
83
|
+
maxBytes;
|
|
46
84
|
headers;
|
|
47
85
|
constructor(opts) {
|
|
48
86
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
@@ -50,12 +88,17 @@ var Http = class {
|
|
|
50
88
|
if (!f) throw new Error("No fetch available \u2014 pass `fetch` in the SDK options.");
|
|
51
89
|
this._fetch = opts.fetch ? opts.fetch : f.bind(globalThis);
|
|
52
90
|
this.timeoutMs = opts.timeoutMs ?? 15e3;
|
|
91
|
+
this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
53
92
|
this.headers = opts.headers ?? {};
|
|
54
93
|
}
|
|
55
94
|
/** Build an absolute URL from a path + optional query params. */
|
|
56
95
|
url(path, query) {
|
|
57
96
|
const p = path.startsWith("/") ? path : `/${path}`;
|
|
58
97
|
const u = new URL(this.baseUrl + p);
|
|
98
|
+
const basePath = new URL(this.baseUrl + "/").pathname;
|
|
99
|
+
if (basePath !== "/" && !(u.pathname + "/").startsWith(basePath)) {
|
|
100
|
+
throw new Error(`illegal path '${path}' \u2014 escapes base ${basePath}`);
|
|
101
|
+
}
|
|
59
102
|
if (query) {
|
|
60
103
|
for (const [k, v] of Object.entries(query)) {
|
|
61
104
|
if (v !== void 0 && v !== null) u.searchParams.set(k, String(v));
|
|
@@ -86,25 +129,24 @@ var Http = class {
|
|
|
86
129
|
async getJson(path, query) {
|
|
87
130
|
const res = await this.raw("GET", path, { query });
|
|
88
131
|
if (!res.ok) throw new HttpError(res.status, this.url(path, query), await safeText(res));
|
|
89
|
-
return await res.
|
|
132
|
+
return JSON.parse(new TextDecoder().decode(await readCapped(res, this.maxBytes)));
|
|
90
133
|
}
|
|
91
134
|
async postJson(path, body, headers) {
|
|
92
135
|
const res = await this.raw("POST", path, { body, headers });
|
|
93
136
|
if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
|
|
94
|
-
return await res.
|
|
137
|
+
return JSON.parse(new TextDecoder().decode(await readCapped(res, this.maxBytes)));
|
|
95
138
|
}
|
|
96
139
|
/** GET raw bytes (for content retrieval). Returns null on 404. */
|
|
97
140
|
async getBytes(path) {
|
|
98
141
|
const res = await this.raw("GET", path);
|
|
99
142
|
if (res.status === 404) return null;
|
|
100
143
|
if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
|
|
101
|
-
|
|
102
|
-
return { bytes: buf, headers: res.headers };
|
|
144
|
+
return { bytes: await readCapped(res, this.maxBytes), headers: res.headers };
|
|
103
145
|
}
|
|
104
146
|
};
|
|
105
147
|
async function safeText(res) {
|
|
106
148
|
try {
|
|
107
|
-
return await res
|
|
149
|
+
return new TextDecoder().decode(await readCapped(res, ERR_TEXT_CAP));
|
|
108
150
|
} catch {
|
|
109
151
|
return "";
|
|
110
152
|
}
|
|
@@ -198,6 +240,7 @@ export {
|
|
|
198
240
|
Chain,
|
|
199
241
|
ContentClient,
|
|
200
242
|
ContentVerificationError,
|
|
243
|
+
DEFAULT_SPV_CHECKPOINT,
|
|
201
244
|
Http,
|
|
202
245
|
HttpError,
|
|
203
246
|
IndexerClient,
|