@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.
Files changed (45) hide show
  1. package/dist/board.cjs +1 -1
  2. package/dist/board.d.cts +1 -1
  3. package/dist/board.d.ts +1 -1
  4. package/dist/board.js +2 -2
  5. package/dist/chain.cjs +1 -1
  6. package/dist/chain.d.cts +1 -1
  7. package/dist/chain.d.ts +1 -1
  8. package/dist/chain.js +1 -1
  9. package/dist/{chunk-G7FABIF4.js → chunk-BGJZF7S2.js} +11 -2
  10. package/dist/{chunk-5KAUQH72.js → chunk-C452UODV.js} +1 -1
  11. package/dist/{chunk-645CYBVN.js → chunk-FSQZVNEM.js} +1 -1
  12. package/dist/{chunk-G4OZKZSJ.js → chunk-K7SJOW3O.js} +1 -1
  13. package/dist/{chunk-ZAE2BIH5.js → chunk-MBSPHWZG.js} +21 -3
  14. package/dist/{chunk-OSXS26RA.js → chunk-MVX7VQT4.js} +4 -3
  15. package/dist/{chunk-FNBE2FPE.js → chunk-OG4KA3LS.js} +49 -17
  16. package/dist/{chunk-UMGWVOOT.js → chunk-RELP23CH.js} +1 -1
  17. package/dist/connect.cjs +1 -1
  18. package/dist/connect.d.cts +6 -0
  19. package/dist/connect.d.ts +6 -0
  20. package/dist/connect.js +2 -2
  21. package/dist/content.cjs +4 -3
  22. package/dist/content.d.cts +1 -1
  23. package/dist/content.d.ts +1 -1
  24. package/dist/content.js +2 -2
  25. package/dist/controller.cjs +21 -3
  26. package/dist/controller.d.cts +1 -0
  27. package/dist/controller.d.ts +1 -0
  28. package/dist/controller.js +3 -3
  29. package/dist/{http-D2CzfOsm.d.ts → http-CQsNL8w9.d.cts} +5 -0
  30. package/dist/{http-D2CzfOsm.d.cts → http-CQsNL8w9.d.ts} +5 -0
  31. package/dist/index.cjs +134 -30
  32. package/dist/index.d.cts +9 -5
  33. package/dist/index.d.ts +9 -5
  34. package/dist/index.js +56 -13
  35. package/dist/indexer.cjs +49 -17
  36. package/dist/indexer.d.cts +15 -4
  37. package/dist/indexer.d.ts +15 -4
  38. package/dist/indexer.js +1 -1
  39. package/dist/react.cjs +21 -3
  40. package/dist/react.js +3 -3
  41. package/dist/registry.cjs +11 -2
  42. package/dist/registry.d.cts +12 -3
  43. package/dist/registry.d.ts +12 -3
  44. package/dist/registry.js +1 -1
  45. 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.2";
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.json();
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.json();
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
- const buf = new Uint8Array(await res.arrayBuffer());
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.text();
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). The Cairn
508
- * facade wires headerMerkleAt ONLY when the node RPC is an independent origin
509
- * from the indexer, so the default same-origin setup stays at "proof-consistent".
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
- const folds = (0, import_csd_codec4.verifyMerkleProof)(txid2, proof.pos, proof.merkle, proof.merkle_root);
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
- return h.toLowerCase();
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
- /** Resolve a handle → identity (GET /identity/:handle). Returns null if unresolved. */
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) this.setState({ status: "disconnected", account: null });
991
- else this.setState({ status: "connected", account: String(accounts[0]), error: null });
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-D2CzfOsm.cjs';
2
- export { H as Http, a as HttpOptions } from './http-D2CzfOsm.cjs';
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.2";
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.2";
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-D2CzfOsm.js';
2
- export { H as Http, a as HttpOptions } from './http-D2CzfOsm.js';
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.2";
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.2";
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-G7FABIF4.js";
3
+ } from "./chunk-BGJZF7S2.js";
4
4
  import {
5
5
  BoardClient
6
- } from "./chunk-645CYBVN.js";
6
+ } from "./chunk-FSQZVNEM.js";
7
7
  import {
8
8
  Chain,
9
9
  LightClient
10
- } from "./chunk-5KAUQH72.js";
10
+ } from "./chunk-C452UODV.js";
11
11
  import {
12
12
  ContentClient
13
- } from "./chunk-OSXS26RA.js";
13
+ } from "./chunk-MVX7VQT4.js";
14
14
  import {
15
15
  IndexerClient
16
- } from "./chunk-FNBE2FPE.js";
16
+ } from "./chunk-OG4KA3LS.js";
17
17
  import {
18
18
  CairnController
19
- } from "./chunk-ZAE2BIH5.js";
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-G4OZKZSJ.js";
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-UMGWVOOT.js";
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.json();
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.json();
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
- const buf = new Uint8Array(await res.arrayBuffer());
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.text();
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,