@inversealtruism/cairn-sdk 0.4.0 → 0.4.2
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-GTTLMWGY.js → chunk-FS2BQWA5.js} +25 -13
- package/dist/{chunk-2L5QNDAE.js → chunk-PZEQ4YHR.js} +1 -1
- package/dist/{chunk-MREZG2HN.js → chunk-REHDMKBF.js} +1 -1
- package/dist/{chunk-RKR3JMIA.js → chunk-VGKXFRHY.js} +22 -3
- package/dist/chunk-WBU7KDI3.js +224 -0
- package/dist/{chunk-2ZML7IKV.js → chunk-XPTP5W75.js} +1 -1
- package/dist/connect.cjs +22 -3
- package/dist/connect.d.cts +4 -1
- package/dist/connect.d.ts +4 -1
- package/dist/connect.js +2 -2
- package/dist/content.cjs +1 -1
- package/dist/content.js +2 -2
- package/dist/controller.cjs +46 -15
- package/dist/controller.d.cts +4 -1
- package/dist/controller.d.ts +4 -1
- package/dist/controller.js +3 -3
- package/dist/index.cjs +183 -63
- package/dist/index.d.cts +30 -9
- package/dist/index.d.ts +30 -9
- package/dist/index.js +126 -187
- package/dist/react.cjs +46 -15
- 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 +6 -6
- package/dist/chunk-BGJZF7S2.js +0 -69
package/dist/board.cjs
CHANGED
|
@@ -27,7 +27,7 @@ var import_csd_codec = require("@inversealtruism/csd-codec");
|
|
|
27
27
|
var import_csd_codec2 = require("@inversealtruism/csd-codec");
|
|
28
28
|
|
|
29
29
|
// src/errors.ts
|
|
30
|
-
var SDK_VERSION = "0.4.
|
|
30
|
+
var SDK_VERSION = "0.4.2";
|
|
31
31
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
32
32
|
var CairnError = class extends Error {
|
|
33
33
|
/** Stable machine code for programmatic branching. */
|
package/dist/board.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getWallet
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-VGKXFRHY.js";
|
|
4
4
|
|
|
5
5
|
// src/controller.ts
|
|
6
6
|
var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error: null });
|
|
7
7
|
var CairnController = class {
|
|
8
8
|
state = DISCONNECTED;
|
|
9
9
|
conn = null;
|
|
10
|
+
// MF-15: connect/disconnect generation token. disconnect() bumps it; a connect() whose await straddled
|
|
11
|
+
// the bump must never commit "connected" over the torn-down session (a zombie null-connection session).
|
|
12
|
+
epoch = 0;
|
|
10
13
|
listeners = /* @__PURE__ */ new Set();
|
|
11
14
|
getWalletFn;
|
|
12
15
|
detectOpts;
|
|
@@ -59,34 +62,43 @@ var CairnController = class {
|
|
|
59
62
|
}
|
|
60
63
|
/** Detect + connect (prompts the user the first time per origin). Resolves the connected address. */
|
|
61
64
|
connect = async () => {
|
|
65
|
+
const epoch = this.epoch;
|
|
62
66
|
this.setState({ status: "connecting", error: null });
|
|
63
67
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
+
let conn = this.conn;
|
|
69
|
+
if (!conn) {
|
|
70
|
+
conn = await this.getWalletFn(this.detectOpts);
|
|
71
|
+
if (epoch !== this.epoch) throw new Error("connection was torn down while connecting - call connect() again");
|
|
72
|
+
conn.on("accountsChanged", this.onAccounts);
|
|
73
|
+
conn.on("disconnect", this.onDisconnect);
|
|
74
|
+
this.conn = conn;
|
|
68
75
|
}
|
|
69
|
-
const addr = await
|
|
76
|
+
const addr = await conn.connect();
|
|
77
|
+
if (epoch !== this.epoch) throw new Error("connection was torn down while awaiting approval - call connect() again");
|
|
70
78
|
this.setState({ status: "connected", account: addr, error: null });
|
|
71
79
|
return addr;
|
|
72
80
|
} catch (e) {
|
|
73
|
-
this.setState({ status: "disconnected", account: null, error: e?.message ?? String(e) });
|
|
81
|
+
if (epoch === this.epoch) this.setState({ status: "disconnected", account: null, error: e?.message ?? String(e) });
|
|
74
82
|
throw e;
|
|
75
83
|
}
|
|
76
84
|
};
|
|
77
|
-
/** Forget the connection locally AND revoke this origin's wallet-side permission (best-effort).
|
|
85
|
+
/** Forget the connection locally AND revoke this origin's wallet-side permission (best-effort).
|
|
86
|
+
* Local teardown is IMMEDIATE and unconditional; the wallet-side revoke happens after, so a hung
|
|
87
|
+
* provider can never hold a torn-down session in "connected". */
|
|
78
88
|
disconnect = async () => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
} catch {
|
|
82
|
-
}
|
|
89
|
+
this.epoch++;
|
|
90
|
+
const conn = this.conn;
|
|
83
91
|
this.detach();
|
|
84
92
|
this.conn = null;
|
|
85
93
|
this.setState({ status: "disconnected", account: null, error: null });
|
|
94
|
+
try {
|
|
95
|
+
await conn?.revokePermissions();
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
86
98
|
};
|
|
87
99
|
/** Audience-bound sign-in (must connect() first). Returns the signed artifact to verify server-side. */
|
|
88
100
|
signInWithCsd = (params) => {
|
|
89
|
-
if (!this.conn) return Promise.reject(new Error("call connect() before signInWithCsd()"));
|
|
101
|
+
if (!this.conn || this.state.status !== "connected") return Promise.reject(new Error("call connect() before signInWithCsd()"));
|
|
90
102
|
return this.conn.signInWithCsd(params);
|
|
91
103
|
};
|
|
92
104
|
};
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
UnsupportedMethodError,
|
|
5
5
|
mapProviderError,
|
|
6
6
|
mapSubmitResultError
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-REHDMKBF.js";
|
|
8
8
|
|
|
9
9
|
// src/connect.ts
|
|
10
10
|
var DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 6e4;
|
|
@@ -99,12 +99,15 @@ var WalletConnection = class {
|
|
|
99
99
|
// genuine wallet resolves quickly once the user acts; this only fires on a truly hung/absent response.
|
|
100
100
|
call(op, label) {
|
|
101
101
|
const ms = this._timeoutMs;
|
|
102
|
-
|
|
102
|
+
const invoke = () => new Promise((resolve) => {
|
|
103
|
+
resolve(op());
|
|
104
|
+
});
|
|
105
|
+
if (!Number.isFinite(ms) || ms <= 0) return invoke();
|
|
103
106
|
let timer;
|
|
104
107
|
const timeout = new Promise((_, reject) => {
|
|
105
108
|
timer = setTimeout(() => reject(new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
|
|
106
109
|
});
|
|
107
|
-
return Promise.race([
|
|
110
|
+
return Promise.race([invoke(), timeout]).finally(() => clearTimeout(timer));
|
|
108
111
|
}
|
|
109
112
|
/** The wallet's version string (e.g. "0.2.x"). */
|
|
110
113
|
get version() {
|
|
@@ -177,8 +180,24 @@ var WalletConnection = class {
|
|
|
177
180
|
if (typeof this.provider.fillOffer !== "function") {
|
|
178
181
|
return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
|
|
179
182
|
}
|
|
183
|
+
this.adviseFillSpvCapability();
|
|
180
184
|
return this.call(() => this.provider.fillOffer(params), "fillOffer").then(unwrapWrite);
|
|
181
185
|
}
|
|
186
|
+
// P75-5 MF-10: ONE capability advisory per connection. The wallet's on-device fill-SPV (0.2.60+) is the
|
|
187
|
+
// payment-grade fill boundary; when the provider SELF-REPORTS an older version, warn the integrator once
|
|
188
|
+
// and STILL RELAY. WARN, NEVER BLOCK: a hard version gate would decline honest fills (version strings are
|
|
189
|
+
// spoofable and unparseable forks exist), and the wallet itself is the enforcement point. Reads only the
|
|
190
|
+
// sync `provider.version`; never an awaited getCapabilities() round-trip on the fill hot path. An
|
|
191
|
+
// unparseable version never warns (no false alarms).
|
|
192
|
+
_fillSpvAdvised = false;
|
|
193
|
+
adviseFillSpvCapability() {
|
|
194
|
+
if (this._fillSpvAdvised) return;
|
|
195
|
+
this._fillSpvAdvised = true;
|
|
196
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(String(this.provider?.version ?? ""));
|
|
197
|
+
if (!m) return;
|
|
198
|
+
const below = Number(m[1]) === 0 && (Number(m[2]) < 2 || Number(m[2]) === 2 && Number(m[3]) < 60);
|
|
199
|
+
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.`);
|
|
200
|
+
}
|
|
182
201
|
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
|
183
202
|
async getPermissions() {
|
|
184
203
|
if (typeof this.provider.getPermissions !== "function") return [];
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HttpError
|
|
3
|
+
} from "./chunk-REHDMKBF.js";
|
|
4
|
+
|
|
5
|
+
// src/registry.ts
|
|
6
|
+
import { fromRecords } from "@inversealtruism/csd-registry";
|
|
7
|
+
|
|
8
|
+
// src/http.ts
|
|
9
|
+
var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
var ERR_TEXT_CAP = 8 * 1024;
|
|
11
|
+
async function readCapped(res, maxBytes) {
|
|
12
|
+
const cl = Number(res.headers.get("content-length"));
|
|
13
|
+
if (Number.isFinite(cl) && cl > maxBytes) throw new HttpError(res.status, res.url, `response too large (content-length ${cl} > ${maxBytes})`);
|
|
14
|
+
const body = res.body;
|
|
15
|
+
if (!body || typeof body.getReader !== "function") {
|
|
16
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
17
|
+
if (buf.byteLength > maxBytes) throw new HttpError(res.status, res.url, `response too large (${buf.byteLength} > ${maxBytes})`);
|
|
18
|
+
return buf;
|
|
19
|
+
}
|
|
20
|
+
const reader = body.getReader();
|
|
21
|
+
const chunks = [];
|
|
22
|
+
let total = 0;
|
|
23
|
+
for (; ; ) {
|
|
24
|
+
const { done, value } = await reader.read();
|
|
25
|
+
if (done) break;
|
|
26
|
+
if (value) {
|
|
27
|
+
total += value.byteLength;
|
|
28
|
+
if (total > maxBytes) {
|
|
29
|
+
try {
|
|
30
|
+
await reader.cancel();
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
throw new HttpError(res.status, res.url, `response exceeded ${maxBytes} bytes`);
|
|
34
|
+
}
|
|
35
|
+
chunks.push(value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const out = new Uint8Array(total);
|
|
39
|
+
let off = 0;
|
|
40
|
+
for (const c of chunks) {
|
|
41
|
+
out.set(c, off);
|
|
42
|
+
off += c.byteLength;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
var Http = class {
|
|
47
|
+
baseUrl;
|
|
48
|
+
_fetch;
|
|
49
|
+
timeoutMs;
|
|
50
|
+
maxBytes;
|
|
51
|
+
headers;
|
|
52
|
+
retries;
|
|
53
|
+
constructor(opts) {
|
|
54
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
55
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
56
|
+
if (!f) throw new Error("No fetch available \u2014 pass `fetch` in the SDK options.");
|
|
57
|
+
this._fetch = opts.fetch ? opts.fetch : f.bind(globalThis);
|
|
58
|
+
this.timeoutMs = opts.timeoutMs ?? 15e3;
|
|
59
|
+
this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
60
|
+
this.headers = opts.headers ?? {};
|
|
61
|
+
this.retries = Math.min(10, Math.max(0, Math.floor(opts.retries ?? 0)));
|
|
62
|
+
}
|
|
63
|
+
/** Build an absolute URL from a path + optional query params. */
|
|
64
|
+
url(path, query) {
|
|
65
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
66
|
+
const u = new URL(this.baseUrl + p);
|
|
67
|
+
const basePath = new URL(this.baseUrl + "/").pathname;
|
|
68
|
+
if (basePath !== "/" && !(u.pathname + "/").startsWith(basePath)) {
|
|
69
|
+
throw new Error(`illegal path '${path}' \u2014 escapes base ${basePath}`);
|
|
70
|
+
}
|
|
71
|
+
if (query) {
|
|
72
|
+
for (const [k, v] of Object.entries(query)) {
|
|
73
|
+
if (v !== void 0 && v !== null) u.searchParams.set(k, String(v));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return u.toString();
|
|
77
|
+
}
|
|
78
|
+
async raw(method, path, opts) {
|
|
79
|
+
const url = this.url(path, opts?.query);
|
|
80
|
+
const ctrl = new AbortController();
|
|
81
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
82
|
+
try {
|
|
83
|
+
const res = await this._fetch(url, {
|
|
84
|
+
method,
|
|
85
|
+
signal: ctrl.signal,
|
|
86
|
+
headers: {
|
|
87
|
+
...opts?.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
88
|
+
...this.headers,
|
|
89
|
+
...opts?.headers ?? {}
|
|
90
|
+
},
|
|
91
|
+
body: opts?.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
92
|
+
});
|
|
93
|
+
return res;
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// A malformed 2xx body must surface as a TYPED HttpError, not a leaked SyntaxError (B10: dApp
|
|
99
|
+
// code branches on `instanceof CairnError`; a raw parse error escaped that contract).
|
|
100
|
+
parseJson(res, text, url) {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(text);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
throw new HttpError(res.status, url, `invalid JSON body: ${text.slice(0, 120)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async getJson(path, query) {
|
|
108
|
+
const url = this.url(path, query);
|
|
109
|
+
for (let attempt = 0; ; attempt++) {
|
|
110
|
+
try {
|
|
111
|
+
const res = await this.raw("GET", path, { query });
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
if (!(res.status >= 500 && attempt < this.retries)) throw new HttpError(res.status, url, await safeText(res));
|
|
114
|
+
res.body?.cancel().catch(() => {
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
return this.parseJson(res, new TextDecoder().decode(await readCapped(res, this.maxBytes)), url);
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
if (e instanceof HttpError && e.status < 500) throw e;
|
|
121
|
+
if (attempt >= this.retries) throw e;
|
|
122
|
+
}
|
|
123
|
+
await new Promise((r) => setTimeout(r, Math.floor(Math.random() * Math.min(4e3, 250 * 2 ** attempt))));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async postJson(path, body, headers) {
|
|
127
|
+
const url = this.url(path);
|
|
128
|
+
const res = await this.raw("POST", path, { body, headers });
|
|
129
|
+
if (!res.ok) throw new HttpError(res.status, url, await safeText(res));
|
|
130
|
+
return this.parseJson(res, new TextDecoder().decode(await readCapped(res, this.maxBytes)), url);
|
|
131
|
+
}
|
|
132
|
+
/** GET raw bytes (for content retrieval). Returns null on 404. */
|
|
133
|
+
async getBytes(path) {
|
|
134
|
+
const res = await this.raw("GET", path);
|
|
135
|
+
if (res.status === 404) return null;
|
|
136
|
+
if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
|
|
137
|
+
return { bytes: await readCapped(res, this.maxBytes), headers: res.headers };
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
async function safeText(res) {
|
|
141
|
+
try {
|
|
142
|
+
return new TextDecoder().decode(await readCapped(res, ERR_TEXT_CAP));
|
|
143
|
+
} catch {
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/registry.ts
|
|
149
|
+
import {
|
|
150
|
+
fromRecords as fromRecords2,
|
|
151
|
+
verifyPeer,
|
|
152
|
+
verifyGateway,
|
|
153
|
+
verifyIdentitySig,
|
|
154
|
+
DOMAINS,
|
|
155
|
+
buildPeerRecord,
|
|
156
|
+
buildGatewayRecord,
|
|
157
|
+
buildIdentityCommit,
|
|
158
|
+
buildIdentityReveal,
|
|
159
|
+
epochOf
|
|
160
|
+
} from "@inversealtruism/csd-registry";
|
|
161
|
+
var RegistryClient = class {
|
|
162
|
+
http;
|
|
163
|
+
/** Accepts the SDK's hardened Http directly (the Cairn facade path) or the legacy
|
|
164
|
+
* { baseUrl, fetch } options (kept for direct constructors; they now get the same
|
|
165
|
+
* hardened transport, built internally). */
|
|
166
|
+
constructor(src) {
|
|
167
|
+
this.http = src instanceof Http ? src : new Http({ baseUrl: src.baseUrl, fetch: src.fetch });
|
|
168
|
+
}
|
|
169
|
+
/** Discover ranked content/pin gateways (GET /registry/gateways). */
|
|
170
|
+
gateways() {
|
|
171
|
+
return this.http.getJson("/registry/gateways");
|
|
172
|
+
}
|
|
173
|
+
/** Discover ranked libp2p peers (GET /registry/peers). */
|
|
174
|
+
peers() {
|
|
175
|
+
return this.http.getJson("/registry/peers");
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Resolve a handle → identity (GET /identity/:handle). Returns null if unresolved.
|
|
179
|
+
*
|
|
180
|
+
* ⚠ TRUST (CAIRN-SDK-RESOLVE-VERIFIED-FOOTGUN): the returned `verified` flag is the INDEXER's assertion,
|
|
181
|
+
* NOT a client-side proof. A hostile/compromised indexer (or a MITM of the read proxy) can return
|
|
182
|
+
* `verified:true` for a wrong address. Do NOT use this directly as a payee/login target without either (a)
|
|
183
|
+
* recomputing from raw records via `fromRecords.name(...)`, or (b) cross-checking a second independent
|
|
184
|
+
* source (the wallet's namespv union cure). Treat `verified` here as a hint, not a guarantee.
|
|
185
|
+
*/
|
|
186
|
+
resolveName(handle) {
|
|
187
|
+
return this.getOr404Null(`/identity/${encodeURIComponent(handle)}`);
|
|
188
|
+
}
|
|
189
|
+
/** Reverse-resolve an address → identity (GET /address/:addr/identity).
|
|
190
|
+
* ⚠ Same indexer-trust caveat as resolveName — `verified` is the indexer's claim, not a client proof. */
|
|
191
|
+
reverseName(address) {
|
|
192
|
+
return this.getOr404Null(`/address/${encodeURIComponent(address)}/identity`);
|
|
193
|
+
}
|
|
194
|
+
// MF-12: 404 → null (the preserved csd-registry contract for both name resolvers); every other
|
|
195
|
+
// failure rethrows the typed HttpError/CairnError the hardened Http raises.
|
|
196
|
+
async getOr404Null(path) {
|
|
197
|
+
try {
|
|
198
|
+
return await this.http.getJson(path);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
if (e instanceof HttpError && e.status === 404) return null;
|
|
201
|
+
return Promise.reject(e);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** The deterministic, client-side resolvers (recompute from raw ChainRecord[]). */
|
|
205
|
+
get fromRecords() {
|
|
206
|
+
return fromRecords;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
readCapped,
|
|
212
|
+
Http,
|
|
213
|
+
RegistryClient,
|
|
214
|
+
fromRecords2 as fromRecords,
|
|
215
|
+
verifyPeer,
|
|
216
|
+
verifyGateway,
|
|
217
|
+
verifyIdentitySig,
|
|
218
|
+
DOMAINS,
|
|
219
|
+
buildPeerRecord,
|
|
220
|
+
buildGatewayRecord,
|
|
221
|
+
buildIdentityCommit,
|
|
222
|
+
buildIdentityReveal,
|
|
223
|
+
epochOf
|
|
224
|
+
};
|
package/dist/connect.cjs
CHANGED
|
@@ -30,7 +30,7 @@ __export(connect_exports, {
|
|
|
30
30
|
module.exports = __toCommonJS(connect_exports);
|
|
31
31
|
|
|
32
32
|
// src/errors.ts
|
|
33
|
-
var SDK_VERSION = "0.4.
|
|
33
|
+
var SDK_VERSION = "0.4.2";
|
|
34
34
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
35
35
|
var CairnError = class extends Error {
|
|
36
36
|
/** Stable machine code for programmatic branching. */
|
|
@@ -240,12 +240,15 @@ var WalletConnection = class {
|
|
|
240
240
|
// genuine wallet resolves quickly once the user acts; this only fires on a truly hung/absent response.
|
|
241
241
|
call(op, label) {
|
|
242
242
|
const ms = this._timeoutMs;
|
|
243
|
-
|
|
243
|
+
const invoke = () => new Promise((resolve) => {
|
|
244
|
+
resolve(op());
|
|
245
|
+
});
|
|
246
|
+
if (!Number.isFinite(ms) || ms <= 0) return invoke();
|
|
244
247
|
let timer;
|
|
245
248
|
const timeout = new Promise((_, reject) => {
|
|
246
249
|
timer = setTimeout(() => reject(new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
|
|
247
250
|
});
|
|
248
|
-
return Promise.race([
|
|
251
|
+
return Promise.race([invoke(), timeout]).finally(() => clearTimeout(timer));
|
|
249
252
|
}
|
|
250
253
|
/** The wallet's version string (e.g. "0.2.x"). */
|
|
251
254
|
get version() {
|
|
@@ -318,8 +321,24 @@ var WalletConnection = class {
|
|
|
318
321
|
if (typeof this.provider.fillOffer !== "function") {
|
|
319
322
|
return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
|
|
320
323
|
}
|
|
324
|
+
this.adviseFillSpvCapability();
|
|
321
325
|
return this.call(() => this.provider.fillOffer(params), "fillOffer").then(unwrapWrite);
|
|
322
326
|
}
|
|
327
|
+
// P75-5 MF-10: ONE capability advisory per connection. The wallet's on-device fill-SPV (0.2.60+) is the
|
|
328
|
+
// payment-grade fill boundary; when the provider SELF-REPORTS an older version, warn the integrator once
|
|
329
|
+
// and STILL RELAY. WARN, NEVER BLOCK: a hard version gate would decline honest fills (version strings are
|
|
330
|
+
// spoofable and unparseable forks exist), and the wallet itself is the enforcement point. Reads only the
|
|
331
|
+
// sync `provider.version`; never an awaited getCapabilities() round-trip on the fill hot path. An
|
|
332
|
+
// unparseable version never warns (no false alarms).
|
|
333
|
+
_fillSpvAdvised = false;
|
|
334
|
+
adviseFillSpvCapability() {
|
|
335
|
+
if (this._fillSpvAdvised) return;
|
|
336
|
+
this._fillSpvAdvised = true;
|
|
337
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(String(this.provider?.version ?? ""));
|
|
338
|
+
if (!m) return;
|
|
339
|
+
const below = Number(m[1]) === 0 && (Number(m[2]) < 2 || Number(m[2]) === 2 && Number(m[3]) < 60);
|
|
340
|
+
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.`);
|
|
341
|
+
}
|
|
323
342
|
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
|
324
343
|
async getPermissions() {
|
|
325
344
|
if (typeof this.provider.getPermissions !== "function") return [];
|
package/dist/connect.d.cts
CHANGED
|
@@ -57,7 +57,8 @@ interface SealClaimParams {
|
|
|
57
57
|
* so a dApp settling real value MUST merkle-prove the offer and derive the recipient from its on-chain author
|
|
58
58
|
* before building `outputs`. Payment-grade offer verification is the wallet's on-device fill-SPV path (F13;
|
|
59
59
|
* added in cairn-wallet 0.2.60, which binds the fill payment/rebate/fee legs to the merkle-proven offer; see
|
|
60
|
-
* docs/SDK-GUIDE.md verifies-vs-trusts).
|
|
60
|
+
* docs/SDK-GUIDE.md verifies-vs-trusts). The SDK's SPV pre-verify SHIPPED: call `preverifyOffer` or
|
|
61
|
+
* `cairn.verifyOfferForFill(offerId, servedOffer, { pay })` before building `outputs`.
|
|
61
62
|
*/
|
|
62
63
|
interface FillParams {
|
|
63
64
|
proposalId: string;
|
|
@@ -249,6 +250,8 @@ declare class WalletConnection {
|
|
|
249
250
|
* 0.2.60+, which binds payment/rebate/fee to the merkle-proven offer) is the payment-grade check; see FillParams.
|
|
250
251
|
*/
|
|
251
252
|
fillOffer(params: FillParams): Promise<TxResult>;
|
|
253
|
+
private _fillSpvAdvised;
|
|
254
|
+
private adviseFillSpvCapability;
|
|
252
255
|
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
|
253
256
|
getPermissions(): Promise<WalletPermission[]>;
|
|
254
257
|
/** Explicitly request permission to see the address (prompts, like connect). */
|
package/dist/connect.d.ts
CHANGED
|
@@ -57,7 +57,8 @@ interface SealClaimParams {
|
|
|
57
57
|
* so a dApp settling real value MUST merkle-prove the offer and derive the recipient from its on-chain author
|
|
58
58
|
* before building `outputs`. Payment-grade offer verification is the wallet's on-device fill-SPV path (F13;
|
|
59
59
|
* added in cairn-wallet 0.2.60, which binds the fill payment/rebate/fee legs to the merkle-proven offer; see
|
|
60
|
-
* docs/SDK-GUIDE.md verifies-vs-trusts).
|
|
60
|
+
* docs/SDK-GUIDE.md verifies-vs-trusts). The SDK's SPV pre-verify SHIPPED: call `preverifyOffer` or
|
|
61
|
+
* `cairn.verifyOfferForFill(offerId, servedOffer, { pay })` before building `outputs`.
|
|
61
62
|
*/
|
|
62
63
|
interface FillParams {
|
|
63
64
|
proposalId: string;
|
|
@@ -249,6 +250,8 @@ declare class WalletConnection {
|
|
|
249
250
|
* 0.2.60+, which binds payment/rebate/fee to the merkle-proven offer) is the payment-grade check; see FillParams.
|
|
250
251
|
*/
|
|
251
252
|
fillOffer(params: FillParams): Promise<TxResult>;
|
|
253
|
+
private _fillSpvAdvised;
|
|
254
|
+
private adviseFillSpvCapability;
|
|
252
255
|
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
|
253
256
|
getPermissions(): Promise<WalletPermission[]>;
|
|
254
257
|
/** Explicitly request permission to see the address (prompts, like connect). */
|
package/dist/connect.js
CHANGED
package/dist/content.cjs
CHANGED
|
@@ -26,7 +26,7 @@ module.exports = __toCommonJS(content_exports);
|
|
|
26
26
|
var import_csd_codec = require("@inversealtruism/csd-codec");
|
|
27
27
|
|
|
28
28
|
// src/errors.ts
|
|
29
|
-
var SDK_VERSION = "0.4.
|
|
29
|
+
var SDK_VERSION = "0.4.2";
|
|
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. */
|
package/dist/content.js
CHANGED
package/dist/controller.cjs
CHANGED
|
@@ -25,7 +25,7 @@ __export(controller_exports, {
|
|
|
25
25
|
module.exports = __toCommonJS(controller_exports);
|
|
26
26
|
|
|
27
27
|
// src/errors.ts
|
|
28
|
-
var SDK_VERSION = "0.4.
|
|
28
|
+
var SDK_VERSION = "0.4.2";
|
|
29
29
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
30
30
|
var CairnError = class extends Error {
|
|
31
31
|
/** Stable machine code for programmatic branching. */
|
|
@@ -212,12 +212,15 @@ var WalletConnection = class {
|
|
|
212
212
|
// genuine wallet resolves quickly once the user acts; this only fires on a truly hung/absent response.
|
|
213
213
|
call(op, label) {
|
|
214
214
|
const ms = this._timeoutMs;
|
|
215
|
-
|
|
215
|
+
const invoke = () => new Promise((resolve) => {
|
|
216
|
+
resolve(op());
|
|
217
|
+
});
|
|
218
|
+
if (!Number.isFinite(ms) || ms <= 0) return invoke();
|
|
216
219
|
let timer;
|
|
217
220
|
const timeout = new Promise((_, reject) => {
|
|
218
221
|
timer = setTimeout(() => reject(new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
|
|
219
222
|
});
|
|
220
|
-
return Promise.race([
|
|
223
|
+
return Promise.race([invoke(), timeout]).finally(() => clearTimeout(timer));
|
|
221
224
|
}
|
|
222
225
|
/** The wallet's version string (e.g. "0.2.x"). */
|
|
223
226
|
get version() {
|
|
@@ -290,8 +293,24 @@ var WalletConnection = class {
|
|
|
290
293
|
if (typeof this.provider.fillOffer !== "function") {
|
|
291
294
|
return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
|
|
292
295
|
}
|
|
296
|
+
this.adviseFillSpvCapability();
|
|
293
297
|
return this.call(() => this.provider.fillOffer(params), "fillOffer").then(unwrapWrite);
|
|
294
298
|
}
|
|
299
|
+
// P75-5 MF-10: ONE capability advisory per connection. The wallet's on-device fill-SPV (0.2.60+) is the
|
|
300
|
+
// payment-grade fill boundary; when the provider SELF-REPORTS an older version, warn the integrator once
|
|
301
|
+
// and STILL RELAY. WARN, NEVER BLOCK: a hard version gate would decline honest fills (version strings are
|
|
302
|
+
// spoofable and unparseable forks exist), and the wallet itself is the enforcement point. Reads only the
|
|
303
|
+
// sync `provider.version`; never an awaited getCapabilities() round-trip on the fill hot path. An
|
|
304
|
+
// unparseable version never warns (no false alarms).
|
|
305
|
+
_fillSpvAdvised = false;
|
|
306
|
+
adviseFillSpvCapability() {
|
|
307
|
+
if (this._fillSpvAdvised) return;
|
|
308
|
+
this._fillSpvAdvised = true;
|
|
309
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(String(this.provider?.version ?? ""));
|
|
310
|
+
if (!m) return;
|
|
311
|
+
const below = Number(m[1]) === 0 && (Number(m[2]) < 2 || Number(m[2]) === 2 && Number(m[3]) < 60);
|
|
312
|
+
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.`);
|
|
313
|
+
}
|
|
295
314
|
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
|
296
315
|
async getPermissions() {
|
|
297
316
|
if (typeof this.provider.getPermissions !== "function") return [];
|
|
@@ -354,6 +373,9 @@ var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error:
|
|
|
354
373
|
var CairnController = class {
|
|
355
374
|
state = DISCONNECTED;
|
|
356
375
|
conn = null;
|
|
376
|
+
// MF-15: connect/disconnect generation token. disconnect() bumps it; a connect() whose await straddled
|
|
377
|
+
// the bump must never commit "connected" over the torn-down session (a zombie null-connection session).
|
|
378
|
+
epoch = 0;
|
|
357
379
|
listeners = /* @__PURE__ */ new Set();
|
|
358
380
|
getWalletFn;
|
|
359
381
|
detectOpts;
|
|
@@ -406,34 +428,43 @@ var CairnController = class {
|
|
|
406
428
|
}
|
|
407
429
|
/** Detect + connect (prompts the user the first time per origin). Resolves the connected address. */
|
|
408
430
|
connect = async () => {
|
|
431
|
+
const epoch = this.epoch;
|
|
409
432
|
this.setState({ status: "connecting", error: null });
|
|
410
433
|
try {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
this.
|
|
414
|
-
this.
|
|
434
|
+
let conn = this.conn;
|
|
435
|
+
if (!conn) {
|
|
436
|
+
conn = await this.getWalletFn(this.detectOpts);
|
|
437
|
+
if (epoch !== this.epoch) throw new Error("connection was torn down while connecting - call connect() again");
|
|
438
|
+
conn.on("accountsChanged", this.onAccounts);
|
|
439
|
+
conn.on("disconnect", this.onDisconnect);
|
|
440
|
+
this.conn = conn;
|
|
415
441
|
}
|
|
416
|
-
const addr = await
|
|
442
|
+
const addr = await conn.connect();
|
|
443
|
+
if (epoch !== this.epoch) throw new Error("connection was torn down while awaiting approval - call connect() again");
|
|
417
444
|
this.setState({ status: "connected", account: addr, error: null });
|
|
418
445
|
return addr;
|
|
419
446
|
} catch (e) {
|
|
420
|
-
this.setState({ status: "disconnected", account: null, error: e?.message ?? String(e) });
|
|
447
|
+
if (epoch === this.epoch) this.setState({ status: "disconnected", account: null, error: e?.message ?? String(e) });
|
|
421
448
|
throw e;
|
|
422
449
|
}
|
|
423
450
|
};
|
|
424
|
-
/** Forget the connection locally AND revoke this origin's wallet-side permission (best-effort).
|
|
451
|
+
/** Forget the connection locally AND revoke this origin's wallet-side permission (best-effort).
|
|
452
|
+
* Local teardown is IMMEDIATE and unconditional; the wallet-side revoke happens after, so a hung
|
|
453
|
+
* provider can never hold a torn-down session in "connected". */
|
|
425
454
|
disconnect = async () => {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
} catch {
|
|
429
|
-
}
|
|
455
|
+
this.epoch++;
|
|
456
|
+
const conn = this.conn;
|
|
430
457
|
this.detach();
|
|
431
458
|
this.conn = null;
|
|
432
459
|
this.setState({ status: "disconnected", account: null, error: null });
|
|
460
|
+
try {
|
|
461
|
+
await conn?.revokePermissions();
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
433
464
|
};
|
|
434
465
|
/** Audience-bound sign-in (must connect() first). Returns the signed artifact to verify server-side. */
|
|
435
466
|
signInWithCsd = (params) => {
|
|
436
|
-
if (!this.conn) return Promise.reject(new Error("call connect() before signInWithCsd()"));
|
|
467
|
+
if (!this.conn || this.state.status !== "connected") return Promise.reject(new Error("call connect() before signInWithCsd()"));
|
|
437
468
|
return this.conn.signInWithCsd(params);
|
|
438
469
|
};
|
|
439
470
|
};
|
package/dist/controller.d.cts
CHANGED
|
@@ -12,6 +12,7 @@ interface CairnControllerOptions extends DetectOptions {
|
|
|
12
12
|
declare class CairnController {
|
|
13
13
|
private state;
|
|
14
14
|
private conn;
|
|
15
|
+
private epoch;
|
|
15
16
|
private readonly listeners;
|
|
16
17
|
private readonly getWalletFn;
|
|
17
18
|
private readonly detectOpts;
|
|
@@ -28,7 +29,9 @@ declare class CairnController {
|
|
|
28
29
|
private detach;
|
|
29
30
|
/** Detect + connect (prompts the user the first time per origin). Resolves the connected address. */
|
|
30
31
|
connect: () => Promise<string>;
|
|
31
|
-
/** Forget the connection locally AND revoke this origin's wallet-side permission (best-effort).
|
|
32
|
+
/** Forget the connection locally AND revoke this origin's wallet-side permission (best-effort).
|
|
33
|
+
* Local teardown is IMMEDIATE and unconditional; the wallet-side revoke happens after, so a hung
|
|
34
|
+
* provider can never hold a torn-down session in "connected". */
|
|
32
35
|
disconnect: () => Promise<void>;
|
|
33
36
|
/** Audience-bound sign-in (must connect() first). Returns the signed artifact to verify server-side. */
|
|
34
37
|
signInWithCsd: (params: SiwcParams) => Promise<SiwcResult>;
|