@inversealtruism/cairn-sdk 0.3.2 → 0.4.1
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.js +2 -2
- package/dist/chunk-7UOTIAYK.js +224 -0
- package/dist/{chunk-CZWDA3A5.js → chunk-DUTVUIQF.js} +1 -1
- package/dist/{chunk-FMXNJQWL.js → chunk-ERBT4DFD.js} +2 -2
- package/dist/{chunk-JKC5K253.js → chunk-OCEHXK6A.js} +1 -1
- package/dist/{chunk-BN3PFJDY.js → chunk-PIPN6FSA.js} +53 -16
- package/dist/{chunk-AGU6K5EZ.js → chunk-QATE6AIP.js} +25 -13
- package/dist/connect.cjs +53 -17
- package/dist/connect.d.cts +11 -2
- package/dist/connect.d.ts +11 -2
- package/dist/connect.js +2 -2
- package/dist/content.cjs +1 -1
- package/dist/content.js +2 -2
- package/dist/controller.cjs +77 -29
- package/dist/controller.d.cts +4 -1
- package/dist/controller.d.ts +4 -1
- package/dist/controller.js +3 -3
- package/dist/index.cjs +317 -71
- package/dist/index.d.cts +65 -18
- package/dist/index.d.ts +65 -18
- package/dist/index.js +235 -181
- package/dist/react.cjs +77 -29
- package/dist/react.js +3 -3
- package/dist/registry.cjs +219 -7
- package/dist/registry.d.cts +16 -11
- package/dist/registry.d.ts +16 -11
- package/dist/registry.js +2 -1
- package/package.json +21 -28
- package/dist/chunk-BGJZF7S2.js +0 -69
package/dist/react.cjs
CHANGED
|
@@ -26,7 +26,7 @@ __export(react_exports, {
|
|
|
26
26
|
module.exports = __toCommonJS(react_exports);
|
|
27
27
|
|
|
28
28
|
// src/errors.ts
|
|
29
|
-
var SDK_VERSION = "0.
|
|
29
|
+
var SDK_VERSION = "0.4.1";
|
|
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. */
|
|
@@ -100,7 +100,7 @@ var SubmitInFlightError = class extends CairnError {
|
|
|
100
100
|
this.txid = txid;
|
|
101
101
|
}
|
|
102
102
|
};
|
|
103
|
-
var NATIVE_CODES = /* @__PURE__ */ new Set(["
|
|
103
|
+
var NATIVE_CODES = /* @__PURE__ */ new Set(["FIRST_PARTY_ONLY", "RATE_LIMITED", "FORBIDDEN", "INTERNAL"]);
|
|
104
104
|
var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
|
|
105
105
|
var NESTED_TERMINAL = /* @__PURE__ */ new Set([
|
|
106
106
|
"FILL_UNSAFE",
|
|
@@ -144,6 +144,7 @@ function mapSubmitResultError(result) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// src/connect.ts
|
|
147
|
+
var DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 6e4;
|
|
147
148
|
function detectProvider(opts = {}) {
|
|
148
149
|
const timeoutMs = opts.timeoutMs ?? 3e3;
|
|
149
150
|
return new Promise((resolve, reject) => {
|
|
@@ -200,8 +201,27 @@ function unwrapWrite(reply) {
|
|
|
200
201
|
var WalletConnection = class {
|
|
201
202
|
provider;
|
|
202
203
|
_addr = null;
|
|
203
|
-
|
|
204
|
+
/** B7c: per-call provider timeout (ms). GENEROUS by default so it never rejects an in-flight human
|
|
205
|
+
* approval; 0 disables it. Every provider call (writes especially) races against it. */
|
|
206
|
+
_timeoutMs;
|
|
207
|
+
constructor(provider, opts = {}) {
|
|
204
208
|
this.provider = provider;
|
|
209
|
+
this._timeoutMs = opts.timeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
210
|
+
}
|
|
211
|
+
// B7c: race a provider call against the per-call timeout. On timeout, throw a fail-closed CairnError
|
|
212
|
+
// (retryable:false: a write may already have reached the wallet, so never invite a blind auto-retry). The
|
|
213
|
+
// genuine wallet resolves quickly once the user acts; this only fires on a truly hung/absent response.
|
|
214
|
+
call(op, label) {
|
|
215
|
+
const ms = this._timeoutMs;
|
|
216
|
+
const invoke = () => new Promise((resolve) => {
|
|
217
|
+
resolve(op());
|
|
218
|
+
});
|
|
219
|
+
if (!Number.isFinite(ms) || ms <= 0) return invoke();
|
|
220
|
+
let timer;
|
|
221
|
+
const timeout = new Promise((_, reject) => {
|
|
222
|
+
timer = setTimeout(() => reject(new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
|
|
223
|
+
});
|
|
224
|
+
return Promise.race([invoke(), timeout]).finally(() => clearTimeout(timer));
|
|
205
225
|
}
|
|
206
226
|
/** The wallet's version string (e.g. "0.2.x"). */
|
|
207
227
|
get version() {
|
|
@@ -213,13 +233,13 @@ var WalletConnection = class {
|
|
|
213
233
|
}
|
|
214
234
|
/** Request connection. First time per origin prompts the user; afterwards may be silent. */
|
|
215
235
|
async connect() {
|
|
216
|
-
const { addr } = unwrap(await this.provider.connect());
|
|
236
|
+
const { addr } = unwrap(await this.call(() => this.provider.connect(), "connect"));
|
|
217
237
|
this._addr = addr;
|
|
218
238
|
return addr;
|
|
219
239
|
}
|
|
220
240
|
/** Get the connected address (does not force a new connection prompt if already consented). */
|
|
221
241
|
async getAddress() {
|
|
222
|
-
const { addr } = unwrap(await this.provider.getAddress());
|
|
242
|
+
const { addr } = unwrap(await this.call(() => this.provider.getAddress(), "getAddress"));
|
|
223
243
|
this._addr = addr;
|
|
224
244
|
return addr;
|
|
225
245
|
}
|
|
@@ -232,7 +252,7 @@ var WalletConnection = class {
|
|
|
232
252
|
if (typeof this.provider.signIn !== "function") {
|
|
233
253
|
return Promise.reject(new UnsupportedMethodError("this wallet does not expose signIn()"));
|
|
234
254
|
}
|
|
235
|
-
return
|
|
255
|
+
return this.call(() => this.provider.signIn(), "signIn").then(unwrap);
|
|
236
256
|
}
|
|
237
257
|
/**
|
|
238
258
|
* Audience-bound "Sign in with CSD" (SIWC) — the secure, replay-resistant third-party login.
|
|
@@ -248,7 +268,7 @@ var WalletConnection = class {
|
|
|
248
268
|
if (typeof this.provider.signInWithCsd !== "function") {
|
|
249
269
|
return Promise.reject(new UnsupportedMethodError("this wallet predates Sign-in-with-CSD \u2014 ask the user to update the Cairn Wallet"));
|
|
250
270
|
}
|
|
251
|
-
return
|
|
271
|
+
return this.call(() => this.provider.signInWithCsd(params), "signInWithCsd").then(unwrap);
|
|
252
272
|
}
|
|
253
273
|
/** True if this wallet supports audience-bound SIWC (safe to call `signInWithCsd`). */
|
|
254
274
|
get supportsSiwc() {
|
|
@@ -258,7 +278,7 @@ var WalletConnection = class {
|
|
|
258
278
|
async getCapabilities() {
|
|
259
279
|
if (typeof this.provider.getCapabilities !== "function") return null;
|
|
260
280
|
try {
|
|
261
|
-
return await this.provider.getCapabilities();
|
|
281
|
+
return await this.call(() => this.provider.getCapabilities(), "getCapabilities");
|
|
262
282
|
} catch {
|
|
263
283
|
return null;
|
|
264
284
|
}
|
|
@@ -274,24 +294,40 @@ var WalletConnection = class {
|
|
|
274
294
|
if (typeof this.provider.fillOffer !== "function") {
|
|
275
295
|
return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
|
|
276
296
|
}
|
|
277
|
-
|
|
297
|
+
this.adviseFillSpvCapability();
|
|
298
|
+
return this.call(() => this.provider.fillOffer(params), "fillOffer").then(unwrapWrite);
|
|
299
|
+
}
|
|
300
|
+
// P75-5 MF-10: ONE capability advisory per connection. The wallet's on-device fill-SPV (0.2.60+) is the
|
|
301
|
+
// payment-grade fill boundary; when the provider SELF-REPORTS an older version, warn the integrator once
|
|
302
|
+
// and STILL RELAY. WARN, NEVER BLOCK: a hard version gate would decline honest fills (version strings are
|
|
303
|
+
// spoofable and unparseable forks exist), and the wallet itself is the enforcement point. Reads only the
|
|
304
|
+
// sync `provider.version`; never an awaited getCapabilities() round-trip on the fill hot path. An
|
|
305
|
+
// unparseable version never warns (no false alarms).
|
|
306
|
+
_fillSpvAdvised = false;
|
|
307
|
+
adviseFillSpvCapability() {
|
|
308
|
+
if (this._fillSpvAdvised) return;
|
|
309
|
+
this._fillSpvAdvised = true;
|
|
310
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(String(this.provider?.version ?? ""));
|
|
311
|
+
if (!m) return;
|
|
312
|
+
const below = Number(m[1]) === 0 && (Number(m[2]) < 2 || Number(m[2]) === 2 && Number(m[3]) < 60);
|
|
313
|
+
if (below) console.warn(`[cairn-sdk] this Cairn Wallet (v${m[0]}) predates the on-device fill-SPV boundary (cairn-wallet 0.2.60+). The fill proceeds; suggest a wallet update for payment-grade offer verification.`);
|
|
278
314
|
}
|
|
279
315
|
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
|
280
316
|
async getPermissions() {
|
|
281
317
|
if (typeof this.provider.getPermissions !== "function") return [];
|
|
282
|
-
return unwrap(await this.provider.getPermissions());
|
|
318
|
+
return unwrap(await this.call(() => this.provider.getPermissions(), "getPermissions"));
|
|
283
319
|
}
|
|
284
320
|
/** Explicitly request permission to see the address (prompts, like connect). */
|
|
285
321
|
requestPermissions() {
|
|
286
322
|
if (typeof this.provider.requestPermissions !== "function") {
|
|
287
323
|
return Promise.reject(new UnsupportedMethodError("this wallet predates requestPermissions \u2014 use connect()"));
|
|
288
324
|
}
|
|
289
|
-
return
|
|
325
|
+
return this.call(() => this.provider.requestPermissions(), "requestPermissions").then(unwrap);
|
|
290
326
|
}
|
|
291
327
|
/** Revoke THIS origin's own access (silent). Returns { revoked }. Safe no-op on older wallets. */
|
|
292
328
|
async revokePermissions() {
|
|
293
329
|
if (typeof this.provider.revokePermissions !== "function") return { revoked: false };
|
|
294
|
-
return unwrap(await this.provider.revokePermissions());
|
|
330
|
+
return unwrap(await this.call(() => this.provider.revokePermissions(), "revokePermissions"));
|
|
295
331
|
}
|
|
296
332
|
/**
|
|
297
333
|
* Subscribe to provider events. `accountsChanged` fires with `[]` when this site loses access
|
|
@@ -307,26 +343,26 @@ var WalletConnection = class {
|
|
|
307
343
|
}
|
|
308
344
|
/** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
|
|
309
345
|
send(params) {
|
|
310
|
-
return
|
|
346
|
+
return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
|
|
311
347
|
}
|
|
312
348
|
/** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
|
|
313
349
|
propose(params) {
|
|
314
|
-
return
|
|
350
|
+
return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
|
|
315
351
|
}
|
|
316
352
|
/** Attest/support a proposal (always prompts). */
|
|
317
353
|
attest(params) {
|
|
318
|
-
return
|
|
354
|
+
return this.call(() => this.provider.attest(params), "attest").then(unwrapWrite);
|
|
319
355
|
}
|
|
320
356
|
/** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
|
|
321
357
|
sealClaim(params) {
|
|
322
|
-
return
|
|
358
|
+
return this.call(() => this.provider.sealClaim(params), "sealClaim").then(unwrapWrite);
|
|
323
359
|
}
|
|
324
360
|
/** Reveal a previously sealed claim by its sealing txid (always prompts). */
|
|
325
361
|
revealClaim(txid) {
|
|
326
362
|
if (typeof txid !== "string" || !txid) {
|
|
327
363
|
return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
|
|
328
364
|
}
|
|
329
|
-
return
|
|
365
|
+
return this.call(() => this.provider.revealClaim(txid), "revealClaim").then(unwrapWrite);
|
|
330
366
|
}
|
|
331
367
|
};
|
|
332
368
|
async function getWallet(opts = {}) {
|
|
@@ -338,6 +374,9 @@ var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error:
|
|
|
338
374
|
var CairnController = class {
|
|
339
375
|
state = DISCONNECTED;
|
|
340
376
|
conn = null;
|
|
377
|
+
// MF-15: connect/disconnect generation token. disconnect() bumps it; a connect() whose await straddled
|
|
378
|
+
// the bump must never commit "connected" over the torn-down session (a zombie null-connection session).
|
|
379
|
+
epoch = 0;
|
|
341
380
|
listeners = /* @__PURE__ */ new Set();
|
|
342
381
|
getWalletFn;
|
|
343
382
|
detectOpts;
|
|
@@ -390,34 +429,43 @@ var CairnController = class {
|
|
|
390
429
|
}
|
|
391
430
|
/** Detect + connect (prompts the user the first time per origin). Resolves the connected address. */
|
|
392
431
|
connect = async () => {
|
|
432
|
+
const epoch = this.epoch;
|
|
393
433
|
this.setState({ status: "connecting", error: null });
|
|
394
434
|
try {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
this.
|
|
398
|
-
this.
|
|
435
|
+
let conn = this.conn;
|
|
436
|
+
if (!conn) {
|
|
437
|
+
conn = await this.getWalletFn(this.detectOpts);
|
|
438
|
+
if (epoch !== this.epoch) throw new Error("connection was torn down while connecting - call connect() again");
|
|
439
|
+
conn.on("accountsChanged", this.onAccounts);
|
|
440
|
+
conn.on("disconnect", this.onDisconnect);
|
|
441
|
+
this.conn = conn;
|
|
399
442
|
}
|
|
400
|
-
const addr = await
|
|
443
|
+
const addr = await conn.connect();
|
|
444
|
+
if (epoch !== this.epoch) throw new Error("connection was torn down while awaiting approval - call connect() again");
|
|
401
445
|
this.setState({ status: "connected", account: addr, error: null });
|
|
402
446
|
return addr;
|
|
403
447
|
} catch (e) {
|
|
404
|
-
this.setState({ status: "disconnected", account: null, error: e?.message ?? String(e) });
|
|
448
|
+
if (epoch === this.epoch) this.setState({ status: "disconnected", account: null, error: e?.message ?? String(e) });
|
|
405
449
|
throw e;
|
|
406
450
|
}
|
|
407
451
|
};
|
|
408
|
-
/** Forget the connection locally AND revoke this origin's wallet-side permission (best-effort).
|
|
452
|
+
/** Forget the connection locally AND revoke this origin's wallet-side permission (best-effort).
|
|
453
|
+
* Local teardown is IMMEDIATE and unconditional; the wallet-side revoke happens after, so a hung
|
|
454
|
+
* provider can never hold a torn-down session in "connected". */
|
|
409
455
|
disconnect = async () => {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
} catch {
|
|
413
|
-
}
|
|
456
|
+
this.epoch++;
|
|
457
|
+
const conn = this.conn;
|
|
414
458
|
this.detach();
|
|
415
459
|
this.conn = null;
|
|
416
460
|
this.setState({ status: "disconnected", account: null, error: null });
|
|
461
|
+
try {
|
|
462
|
+
await conn?.revokePermissions();
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
417
465
|
};
|
|
418
466
|
/** Audience-bound sign-in (must connect() first). Returns the signed artifact to verify server-side. */
|
|
419
467
|
signInWithCsd = (params) => {
|
|
420
|
-
if (!this.conn) return Promise.reject(new Error("call connect() before signInWithCsd()"));
|
|
468
|
+
if (!this.conn || this.state.status !== "connected") return Promise.reject(new Error("call connect() before signInWithCsd()"));
|
|
421
469
|
return this.conn.signInWithCsd(params);
|
|
422
470
|
};
|
|
423
471
|
};
|
package/dist/react.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CairnController
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-QATE6AIP.js";
|
|
4
|
+
import "./chunk-PIPN6FSA.js";
|
|
5
|
+
import "./chunk-ERBT4DFD.js";
|
|
6
6
|
|
|
7
7
|
// src/react.ts
|
|
8
8
|
function createCairnHooks(React, controllerOrOpts) {
|
package/dist/registry.cjs
CHANGED
|
@@ -34,19 +34,221 @@ __export(registry_exports, {
|
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(registry_exports);
|
|
36
36
|
var import_csd_registry = require("@inversealtruism/csd-registry");
|
|
37
|
+
|
|
38
|
+
// src/errors.ts
|
|
39
|
+
var SDK_VERSION = "0.4.1";
|
|
40
|
+
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
41
|
+
var CairnError = class extends Error {
|
|
42
|
+
/** Stable machine code for programmatic branching. */
|
|
43
|
+
code;
|
|
44
|
+
/** The human message without the appended docs/version footer. */
|
|
45
|
+
shortMessage;
|
|
46
|
+
/** Relative docs path (under the SDK docs base), if any. */
|
|
47
|
+
docsPath;
|
|
48
|
+
/** The cairn-sdk version that produced this error. */
|
|
49
|
+
version = SDK_VERSION;
|
|
50
|
+
/**
|
|
51
|
+
* Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
|
|
52
|
+
* declines (the default); `true` only for provably transient infra faults where nothing was signed.
|
|
53
|
+
* A `SubmitInFlightError` is `false` too: reconcile its `txid`, never blind-retry.
|
|
54
|
+
*/
|
|
55
|
+
retryable;
|
|
56
|
+
constructor(shortMessage, opts = {}) {
|
|
57
|
+
const docs = opts.docsPath;
|
|
58
|
+
super(
|
|
59
|
+
[shortMessage, docs ? `Docs: ${DOCS_BASE}${docs}` : "", `Version: cairn-sdk@${SDK_VERSION}`].filter(Boolean).join("\n"),
|
|
60
|
+
{ cause: opts.cause }
|
|
61
|
+
);
|
|
62
|
+
this.name = new.target.name;
|
|
63
|
+
this.code = opts.code ?? "UNKNOWN";
|
|
64
|
+
this.shortMessage = shortMessage;
|
|
65
|
+
this.docsPath = docs;
|
|
66
|
+
this.retryable = opts.retryable ?? false;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
|
|
70
|
+
* returns the DEEPEST cause (the root). Mirrors viem's `BaseError.walk()`.
|
|
71
|
+
*/
|
|
72
|
+
walk(fn) {
|
|
73
|
+
let current = this;
|
|
74
|
+
let last = this;
|
|
75
|
+
while (current != null) {
|
|
76
|
+
if (fn && fn(current)) return current;
|
|
77
|
+
last = current;
|
|
78
|
+
current = current?.cause;
|
|
79
|
+
}
|
|
80
|
+
return fn ? null : last;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var HttpError = class extends CairnError {
|
|
84
|
+
status;
|
|
85
|
+
url;
|
|
86
|
+
body;
|
|
87
|
+
constructor(status, url, body) {
|
|
88
|
+
super(`HTTP ${status} for ${url}${body ? `: ${String(body).slice(0, 200)}` : ""}`, { code: "HTTP_ERROR" });
|
|
89
|
+
this.status = status;
|
|
90
|
+
this.url = url;
|
|
91
|
+
this.body = body;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// src/http.ts
|
|
96
|
+
var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
97
|
+
var ERR_TEXT_CAP = 8 * 1024;
|
|
98
|
+
async function readCapped(res, maxBytes) {
|
|
99
|
+
const cl = Number(res.headers.get("content-length"));
|
|
100
|
+
if (Number.isFinite(cl) && cl > maxBytes) throw new HttpError(res.status, res.url, `response too large (content-length ${cl} > ${maxBytes})`);
|
|
101
|
+
const body = res.body;
|
|
102
|
+
if (!body || typeof body.getReader !== "function") {
|
|
103
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
104
|
+
if (buf.byteLength > maxBytes) throw new HttpError(res.status, res.url, `response too large (${buf.byteLength} > ${maxBytes})`);
|
|
105
|
+
return buf;
|
|
106
|
+
}
|
|
107
|
+
const reader = body.getReader();
|
|
108
|
+
const chunks = [];
|
|
109
|
+
let total = 0;
|
|
110
|
+
for (; ; ) {
|
|
111
|
+
const { done, value } = await reader.read();
|
|
112
|
+
if (done) break;
|
|
113
|
+
if (value) {
|
|
114
|
+
total += value.byteLength;
|
|
115
|
+
if (total > maxBytes) {
|
|
116
|
+
try {
|
|
117
|
+
await reader.cancel();
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
throw new HttpError(res.status, res.url, `response exceeded ${maxBytes} bytes`);
|
|
121
|
+
}
|
|
122
|
+
chunks.push(value);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const out = new Uint8Array(total);
|
|
126
|
+
let off = 0;
|
|
127
|
+
for (const c of chunks) {
|
|
128
|
+
out.set(c, off);
|
|
129
|
+
off += c.byteLength;
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
var Http = class {
|
|
134
|
+
baseUrl;
|
|
135
|
+
_fetch;
|
|
136
|
+
timeoutMs;
|
|
137
|
+
maxBytes;
|
|
138
|
+
headers;
|
|
139
|
+
retries;
|
|
140
|
+
constructor(opts) {
|
|
141
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
142
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
143
|
+
if (!f) throw new Error("No fetch available \u2014 pass `fetch` in the SDK options.");
|
|
144
|
+
this._fetch = opts.fetch ? opts.fetch : f.bind(globalThis);
|
|
145
|
+
this.timeoutMs = opts.timeoutMs ?? 15e3;
|
|
146
|
+
this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
147
|
+
this.headers = opts.headers ?? {};
|
|
148
|
+
this.retries = Math.min(10, Math.max(0, Math.floor(opts.retries ?? 0)));
|
|
149
|
+
}
|
|
150
|
+
/** Build an absolute URL from a path + optional query params. */
|
|
151
|
+
url(path, query) {
|
|
152
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
153
|
+
const u = new URL(this.baseUrl + p);
|
|
154
|
+
const basePath = new URL(this.baseUrl + "/").pathname;
|
|
155
|
+
if (basePath !== "/" && !(u.pathname + "/").startsWith(basePath)) {
|
|
156
|
+
throw new Error(`illegal path '${path}' \u2014 escapes base ${basePath}`);
|
|
157
|
+
}
|
|
158
|
+
if (query) {
|
|
159
|
+
for (const [k, v] of Object.entries(query)) {
|
|
160
|
+
if (v !== void 0 && v !== null) u.searchParams.set(k, String(v));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return u.toString();
|
|
164
|
+
}
|
|
165
|
+
async raw(method, path, opts) {
|
|
166
|
+
const url = this.url(path, opts?.query);
|
|
167
|
+
const ctrl = new AbortController();
|
|
168
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
169
|
+
try {
|
|
170
|
+
const res = await this._fetch(url, {
|
|
171
|
+
method,
|
|
172
|
+
signal: ctrl.signal,
|
|
173
|
+
headers: {
|
|
174
|
+
...opts?.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
175
|
+
...this.headers,
|
|
176
|
+
...opts?.headers ?? {}
|
|
177
|
+
},
|
|
178
|
+
body: opts?.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
179
|
+
});
|
|
180
|
+
return res;
|
|
181
|
+
} finally {
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// A malformed 2xx body must surface as a TYPED HttpError, not a leaked SyntaxError (B10: dApp
|
|
186
|
+
// code branches on `instanceof CairnError`; a raw parse error escaped that contract).
|
|
187
|
+
parseJson(res, text, url) {
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(text);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
throw new HttpError(res.status, url, `invalid JSON body: ${text.slice(0, 120)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async getJson(path, query) {
|
|
195
|
+
const url = this.url(path, query);
|
|
196
|
+
for (let attempt = 0; ; attempt++) {
|
|
197
|
+
try {
|
|
198
|
+
const res = await this.raw("GET", path, { query });
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
if (!(res.status >= 500 && attempt < this.retries)) throw new HttpError(res.status, url, await safeText(res));
|
|
201
|
+
res.body?.cancel().catch(() => {
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
return this.parseJson(res, new TextDecoder().decode(await readCapped(res, this.maxBytes)), url);
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
if (e instanceof HttpError && e.status < 500) throw e;
|
|
208
|
+
if (attempt >= this.retries) throw e;
|
|
209
|
+
}
|
|
210
|
+
await new Promise((r) => setTimeout(r, Math.floor(Math.random() * Math.min(4e3, 250 * 2 ** attempt))));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async postJson(path, body, headers) {
|
|
214
|
+
const url = this.url(path);
|
|
215
|
+
const res = await this.raw("POST", path, { body, headers });
|
|
216
|
+
if (!res.ok) throw new HttpError(res.status, url, await safeText(res));
|
|
217
|
+
return this.parseJson(res, new TextDecoder().decode(await readCapped(res, this.maxBytes)), url);
|
|
218
|
+
}
|
|
219
|
+
/** GET raw bytes (for content retrieval). Returns null on 404. */
|
|
220
|
+
async getBytes(path) {
|
|
221
|
+
const res = await this.raw("GET", path);
|
|
222
|
+
if (res.status === 404) return null;
|
|
223
|
+
if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
|
|
224
|
+
return { bytes: await readCapped(res, this.maxBytes), headers: res.headers };
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
async function safeText(res) {
|
|
228
|
+
try {
|
|
229
|
+
return new TextDecoder().decode(await readCapped(res, ERR_TEXT_CAP));
|
|
230
|
+
} catch {
|
|
231
|
+
return "";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/registry.ts
|
|
37
236
|
var import_csd_registry2 = require("@inversealtruism/csd-registry");
|
|
38
237
|
var RegistryClient = class {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
238
|
+
http;
|
|
239
|
+
/** Accepts the SDK's hardened Http directly (the Cairn facade path) or the legacy
|
|
240
|
+
* { baseUrl, fetch } options (kept for direct constructors; they now get the same
|
|
241
|
+
* hardened transport, built internally). */
|
|
242
|
+
constructor(src) {
|
|
243
|
+
this.http = src instanceof Http ? src : new Http({ baseUrl: src.baseUrl, fetch: src.fetch });
|
|
42
244
|
}
|
|
43
245
|
/** Discover ranked content/pin gateways (GET /registry/gateways). */
|
|
44
246
|
gateways() {
|
|
45
|
-
return
|
|
247
|
+
return this.http.getJson("/registry/gateways");
|
|
46
248
|
}
|
|
47
249
|
/** Discover ranked libp2p peers (GET /registry/peers). */
|
|
48
250
|
peers() {
|
|
49
|
-
return
|
|
251
|
+
return this.http.getJson("/registry/peers");
|
|
50
252
|
}
|
|
51
253
|
/**
|
|
52
254
|
* Resolve a handle → identity (GET /identity/:handle). Returns null if unresolved.
|
|
@@ -58,12 +260,22 @@ var RegistryClient = class {
|
|
|
58
260
|
* source (the wallet's namespv union cure). Treat `verified` here as a hint, not a guarantee.
|
|
59
261
|
*/
|
|
60
262
|
resolveName(handle) {
|
|
61
|
-
return
|
|
263
|
+
return this.getOr404Null(`/identity/${encodeURIComponent(handle)}`);
|
|
62
264
|
}
|
|
63
265
|
/** Reverse-resolve an address → identity (GET /address/:addr/identity).
|
|
64
266
|
* ⚠ Same indexer-trust caveat as resolveName — `verified` is the indexer's claim, not a client proof. */
|
|
65
267
|
reverseName(address) {
|
|
66
|
-
return
|
|
268
|
+
return this.getOr404Null(`/address/${encodeURIComponent(address)}/identity`);
|
|
269
|
+
}
|
|
270
|
+
// MF-12: 404 → null (the preserved csd-registry contract for both name resolvers); every other
|
|
271
|
+
// failure rethrows the typed HttpError/CairnError the hardened Http raises.
|
|
272
|
+
async getOr404Null(path) {
|
|
273
|
+
try {
|
|
274
|
+
return await this.http.getJson(path);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
if (e instanceof HttpError && e.status === 404) return null;
|
|
277
|
+
return Promise.reject(e);
|
|
278
|
+
}
|
|
67
279
|
}
|
|
68
280
|
/** The deterministic, client-side resolvers (recompute from raw ChainRecord[]). */
|
|
69
281
|
get fromRecords() {
|
package/dist/registry.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as _inversealtruism_csd_registry from '@inversealtruism/csd-registry';
|
|
2
|
+
import { RankedGateway, RankedPeer, ResolvedIdentity } from '@inversealtruism/csd-registry';
|
|
2
3
|
export { BuiltRecord, ChainRecord, DOMAINS, RankedGateway, RankedPeer, ResolveOpts, ResolvedIdentity, buildGatewayRecord, buildIdentityCommit, buildIdentityReveal, buildPeerRecord, epochOf, fromRecords, verifyGateway, verifyIdentitySig, verifyPeer } from '@inversealtruism/csd-registry';
|
|
3
|
-
import { F as FetchLike } from './http-C9C4L4hz.cjs';
|
|
4
|
+
import { H as Http, F as FetchLike } from './http-C9C4L4hz.cjs';
|
|
4
5
|
|
|
5
6
|
interface RegistryClientOptions {
|
|
6
7
|
/** Indexer base URL exposing /registry/* + /identity/* (e.g. https://cairn-substrate.com/indexer). */
|
|
@@ -8,12 +9,15 @@ interface RegistryClientOptions {
|
|
|
8
9
|
fetch?: FetchLike;
|
|
9
10
|
}
|
|
10
11
|
declare class RegistryClient {
|
|
11
|
-
private readonly
|
|
12
|
-
|
|
12
|
+
private readonly http;
|
|
13
|
+
/** Accepts the SDK's hardened Http directly (the Cairn facade path) or the legacy
|
|
14
|
+
* { baseUrl, fetch } options (kept for direct constructors; they now get the same
|
|
15
|
+
* hardened transport, built internally). */
|
|
16
|
+
constructor(src: Http | RegistryClientOptions);
|
|
13
17
|
/** Discover ranked content/pin gateways (GET /registry/gateways). */
|
|
14
|
-
gateways(): Promise<
|
|
18
|
+
gateways(): Promise<RankedGateway[]>;
|
|
15
19
|
/** Discover ranked libp2p peers (GET /registry/peers). */
|
|
16
|
-
peers(): Promise<
|
|
20
|
+
peers(): Promise<RankedPeer[]>;
|
|
17
21
|
/**
|
|
18
22
|
* Resolve a handle → identity (GET /identity/:handle). Returns null if unresolved.
|
|
19
23
|
*
|
|
@@ -23,16 +27,17 @@ declare class RegistryClient {
|
|
|
23
27
|
* recomputing from raw records via `fromRecords.name(...)`, or (b) cross-checking a second independent
|
|
24
28
|
* source (the wallet's namespv union cure). Treat `verified` here as a hint, not a guarantee.
|
|
25
29
|
*/
|
|
26
|
-
resolveName(handle: string): Promise<
|
|
30
|
+
resolveName(handle: string): Promise<ResolvedIdentity | null>;
|
|
27
31
|
/** Reverse-resolve an address → identity (GET /address/:addr/identity).
|
|
28
32
|
* ⚠ Same indexer-trust caveat as resolveName — `verified` is the indexer's claim, not a client proof. */
|
|
29
|
-
reverseName(address: string): Promise<
|
|
33
|
+
reverseName(address: string): Promise<ResolvedIdentity | null>;
|
|
34
|
+
private getOr404Null;
|
|
30
35
|
/** The deterministic, client-side resolvers (recompute from raw ChainRecord[]). */
|
|
31
36
|
get fromRecords(): {
|
|
32
|
-
peers: (records: _inversealtruism_csd_registry.ChainRecord[], opts: _inversealtruism_csd_registry.ResolveOpts) =>
|
|
33
|
-
gateways: (records: _inversealtruism_csd_registry.ChainRecord[], opts: _inversealtruism_csd_registry.ResolveOpts) =>
|
|
34
|
-
name: (records: _inversealtruism_csd_registry.ChainRecord[], handle: string, opts: _inversealtruism_csd_registry.ResolveOpts) =>
|
|
35
|
-
reverse: (records: _inversealtruism_csd_registry.ChainRecord[], address: string, opts: _inversealtruism_csd_registry.ResolveOpts) =>
|
|
37
|
+
peers: (records: _inversealtruism_csd_registry.ChainRecord[], opts: _inversealtruism_csd_registry.ResolveOpts) => RankedPeer[];
|
|
38
|
+
gateways: (records: _inversealtruism_csd_registry.ChainRecord[], opts: _inversealtruism_csd_registry.ResolveOpts) => RankedGateway[];
|
|
39
|
+
name: (records: _inversealtruism_csd_registry.ChainRecord[], handle: string, opts: _inversealtruism_csd_registry.ResolveOpts) => ResolvedIdentity | null;
|
|
40
|
+
reverse: (records: _inversealtruism_csd_registry.ChainRecord[], address: string, opts: _inversealtruism_csd_registry.ResolveOpts) => ResolvedIdentity | null;
|
|
36
41
|
};
|
|
37
42
|
}
|
|
38
43
|
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as _inversealtruism_csd_registry from '@inversealtruism/csd-registry';
|
|
2
|
+
import { RankedGateway, RankedPeer, ResolvedIdentity } from '@inversealtruism/csd-registry';
|
|
2
3
|
export { BuiltRecord, ChainRecord, DOMAINS, RankedGateway, RankedPeer, ResolveOpts, ResolvedIdentity, buildGatewayRecord, buildIdentityCommit, buildIdentityReveal, buildPeerRecord, epochOf, fromRecords, verifyGateway, verifyIdentitySig, verifyPeer } from '@inversealtruism/csd-registry';
|
|
3
|
-
import { F as FetchLike } from './http-C9C4L4hz.js';
|
|
4
|
+
import { H as Http, F as FetchLike } from './http-C9C4L4hz.js';
|
|
4
5
|
|
|
5
6
|
interface RegistryClientOptions {
|
|
6
7
|
/** Indexer base URL exposing /registry/* + /identity/* (e.g. https://cairn-substrate.com/indexer). */
|
|
@@ -8,12 +9,15 @@ interface RegistryClientOptions {
|
|
|
8
9
|
fetch?: FetchLike;
|
|
9
10
|
}
|
|
10
11
|
declare class RegistryClient {
|
|
11
|
-
private readonly
|
|
12
|
-
|
|
12
|
+
private readonly http;
|
|
13
|
+
/** Accepts the SDK's hardened Http directly (the Cairn facade path) or the legacy
|
|
14
|
+
* { baseUrl, fetch } options (kept for direct constructors; they now get the same
|
|
15
|
+
* hardened transport, built internally). */
|
|
16
|
+
constructor(src: Http | RegistryClientOptions);
|
|
13
17
|
/** Discover ranked content/pin gateways (GET /registry/gateways). */
|
|
14
|
-
gateways(): Promise<
|
|
18
|
+
gateways(): Promise<RankedGateway[]>;
|
|
15
19
|
/** Discover ranked libp2p peers (GET /registry/peers). */
|
|
16
|
-
peers(): Promise<
|
|
20
|
+
peers(): Promise<RankedPeer[]>;
|
|
17
21
|
/**
|
|
18
22
|
* Resolve a handle → identity (GET /identity/:handle). Returns null if unresolved.
|
|
19
23
|
*
|
|
@@ -23,16 +27,17 @@ declare class RegistryClient {
|
|
|
23
27
|
* recomputing from raw records via `fromRecords.name(...)`, or (b) cross-checking a second independent
|
|
24
28
|
* source (the wallet's namespv union cure). Treat `verified` here as a hint, not a guarantee.
|
|
25
29
|
*/
|
|
26
|
-
resolveName(handle: string): Promise<
|
|
30
|
+
resolveName(handle: string): Promise<ResolvedIdentity | null>;
|
|
27
31
|
/** Reverse-resolve an address → identity (GET /address/:addr/identity).
|
|
28
32
|
* ⚠ Same indexer-trust caveat as resolveName — `verified` is the indexer's claim, not a client proof. */
|
|
29
|
-
reverseName(address: string): Promise<
|
|
33
|
+
reverseName(address: string): Promise<ResolvedIdentity | null>;
|
|
34
|
+
private getOr404Null;
|
|
30
35
|
/** The deterministic, client-side resolvers (recompute from raw ChainRecord[]). */
|
|
31
36
|
get fromRecords(): {
|
|
32
|
-
peers: (records: _inversealtruism_csd_registry.ChainRecord[], opts: _inversealtruism_csd_registry.ResolveOpts) =>
|
|
33
|
-
gateways: (records: _inversealtruism_csd_registry.ChainRecord[], opts: _inversealtruism_csd_registry.ResolveOpts) =>
|
|
34
|
-
name: (records: _inversealtruism_csd_registry.ChainRecord[], handle: string, opts: _inversealtruism_csd_registry.ResolveOpts) =>
|
|
35
|
-
reverse: (records: _inversealtruism_csd_registry.ChainRecord[], address: string, opts: _inversealtruism_csd_registry.ResolveOpts) =>
|
|
37
|
+
peers: (records: _inversealtruism_csd_registry.ChainRecord[], opts: _inversealtruism_csd_registry.ResolveOpts) => RankedPeer[];
|
|
38
|
+
gateways: (records: _inversealtruism_csd_registry.ChainRecord[], opts: _inversealtruism_csd_registry.ResolveOpts) => RankedGateway[];
|
|
39
|
+
name: (records: _inversealtruism_csd_registry.ChainRecord[], handle: string, opts: _inversealtruism_csd_registry.ResolveOpts) => ResolvedIdentity | null;
|
|
40
|
+
reverse: (records: _inversealtruism_csd_registry.ChainRecord[], address: string, opts: _inversealtruism_csd_registry.ResolveOpts) => ResolvedIdentity | null;
|
|
36
41
|
};
|
|
37
42
|
}
|
|
38
43
|
|