@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/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.
|
|
30
|
+
var SDK_VERSION = "0.4.1";
|
|
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
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HttpError
|
|
3
|
+
} from "./chunk-ERBT4DFD.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
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/errors.ts
|
|
2
|
-
var SDK_VERSION = "0.
|
|
2
|
+
var SDK_VERSION = "0.4.1";
|
|
3
3
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
4
4
|
var CairnError = class extends Error {
|
|
5
5
|
/** Stable machine code for programmatic branching. */
|
|
@@ -91,7 +91,7 @@ var SubmitInFlightError = class extends CairnError {
|
|
|
91
91
|
this.txid = txid;
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
|
-
var NATIVE_CODES = /* @__PURE__ */ new Set(["
|
|
94
|
+
var NATIVE_CODES = /* @__PURE__ */ new Set(["FIRST_PARTY_ONLY", "RATE_LIMITED", "FORBIDDEN", "INTERNAL"]);
|
|
95
95
|
var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
|
|
96
96
|
var NESTED_TERMINAL = /* @__PURE__ */ new Set([
|
|
97
97
|
"FILL_UNSAFE",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CairnError,
|
|
2
3
|
NotInstalledError,
|
|
3
4
|
UnsupportedMethodError,
|
|
4
5
|
mapProviderError,
|
|
5
6
|
mapSubmitResultError
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ERBT4DFD.js";
|
|
7
8
|
|
|
8
9
|
// src/connect.ts
|
|
10
|
+
var DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 6e4;
|
|
9
11
|
function discoverProviders(opts = {}) {
|
|
10
12
|
const timeoutMs = opts.timeoutMs ?? 1e3;
|
|
11
13
|
return new Promise((resolve) => {
|
|
@@ -85,8 +87,27 @@ function unwrapWrite(reply) {
|
|
|
85
87
|
var WalletConnection = class {
|
|
86
88
|
provider;
|
|
87
89
|
_addr = null;
|
|
88
|
-
|
|
90
|
+
/** B7c: per-call provider timeout (ms). GENEROUS by default so it never rejects an in-flight human
|
|
91
|
+
* approval; 0 disables it. Every provider call (writes especially) races against it. */
|
|
92
|
+
_timeoutMs;
|
|
93
|
+
constructor(provider, opts = {}) {
|
|
89
94
|
this.provider = provider;
|
|
95
|
+
this._timeoutMs = opts.timeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
96
|
+
}
|
|
97
|
+
// B7c: race a provider call against the per-call timeout. On timeout, throw a fail-closed CairnError
|
|
98
|
+
// (retryable:false: a write may already have reached the wallet, so never invite a blind auto-retry). The
|
|
99
|
+
// genuine wallet resolves quickly once the user acts; this only fires on a truly hung/absent response.
|
|
100
|
+
call(op, label) {
|
|
101
|
+
const ms = this._timeoutMs;
|
|
102
|
+
const invoke = () => new Promise((resolve) => {
|
|
103
|
+
resolve(op());
|
|
104
|
+
});
|
|
105
|
+
if (!Number.isFinite(ms) || ms <= 0) return invoke();
|
|
106
|
+
let timer;
|
|
107
|
+
const timeout = new Promise((_, reject) => {
|
|
108
|
+
timer = setTimeout(() => reject(new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
|
|
109
|
+
});
|
|
110
|
+
return Promise.race([invoke(), timeout]).finally(() => clearTimeout(timer));
|
|
90
111
|
}
|
|
91
112
|
/** The wallet's version string (e.g. "0.2.x"). */
|
|
92
113
|
get version() {
|
|
@@ -98,13 +119,13 @@ var WalletConnection = class {
|
|
|
98
119
|
}
|
|
99
120
|
/** Request connection. First time per origin prompts the user; afterwards may be silent. */
|
|
100
121
|
async connect() {
|
|
101
|
-
const { addr } = unwrap(await this.provider.connect());
|
|
122
|
+
const { addr } = unwrap(await this.call(() => this.provider.connect(), "connect"));
|
|
102
123
|
this._addr = addr;
|
|
103
124
|
return addr;
|
|
104
125
|
}
|
|
105
126
|
/** Get the connected address (does not force a new connection prompt if already consented). */
|
|
106
127
|
async getAddress() {
|
|
107
|
-
const { addr } = unwrap(await this.provider.getAddress());
|
|
128
|
+
const { addr } = unwrap(await this.call(() => this.provider.getAddress(), "getAddress"));
|
|
108
129
|
this._addr = addr;
|
|
109
130
|
return addr;
|
|
110
131
|
}
|
|
@@ -117,7 +138,7 @@ var WalletConnection = class {
|
|
|
117
138
|
if (typeof this.provider.signIn !== "function") {
|
|
118
139
|
return Promise.reject(new UnsupportedMethodError("this wallet does not expose signIn()"));
|
|
119
140
|
}
|
|
120
|
-
return
|
|
141
|
+
return this.call(() => this.provider.signIn(), "signIn").then(unwrap);
|
|
121
142
|
}
|
|
122
143
|
/**
|
|
123
144
|
* Audience-bound "Sign in with CSD" (SIWC) — the secure, replay-resistant third-party login.
|
|
@@ -133,7 +154,7 @@ var WalletConnection = class {
|
|
|
133
154
|
if (typeof this.provider.signInWithCsd !== "function") {
|
|
134
155
|
return Promise.reject(new UnsupportedMethodError("this wallet predates Sign-in-with-CSD \u2014 ask the user to update the Cairn Wallet"));
|
|
135
156
|
}
|
|
136
|
-
return
|
|
157
|
+
return this.call(() => this.provider.signInWithCsd(params), "signInWithCsd").then(unwrap);
|
|
137
158
|
}
|
|
138
159
|
/** True if this wallet supports audience-bound SIWC (safe to call `signInWithCsd`). */
|
|
139
160
|
get supportsSiwc() {
|
|
@@ -143,7 +164,7 @@ var WalletConnection = class {
|
|
|
143
164
|
async getCapabilities() {
|
|
144
165
|
if (typeof this.provider.getCapabilities !== "function") return null;
|
|
145
166
|
try {
|
|
146
|
-
return await this.provider.getCapabilities();
|
|
167
|
+
return await this.call(() => this.provider.getCapabilities(), "getCapabilities");
|
|
147
168
|
} catch {
|
|
148
169
|
return null;
|
|
149
170
|
}
|
|
@@ -159,24 +180,40 @@ var WalletConnection = class {
|
|
|
159
180
|
if (typeof this.provider.fillOffer !== "function") {
|
|
160
181
|
return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
|
|
161
182
|
}
|
|
162
|
-
|
|
183
|
+
this.adviseFillSpvCapability();
|
|
184
|
+
return this.call(() => this.provider.fillOffer(params), "fillOffer").then(unwrapWrite);
|
|
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.`);
|
|
163
200
|
}
|
|
164
201
|
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
|
165
202
|
async getPermissions() {
|
|
166
203
|
if (typeof this.provider.getPermissions !== "function") return [];
|
|
167
|
-
return unwrap(await this.provider.getPermissions());
|
|
204
|
+
return unwrap(await this.call(() => this.provider.getPermissions(), "getPermissions"));
|
|
168
205
|
}
|
|
169
206
|
/** Explicitly request permission to see the address (prompts, like connect). */
|
|
170
207
|
requestPermissions() {
|
|
171
208
|
if (typeof this.provider.requestPermissions !== "function") {
|
|
172
209
|
return Promise.reject(new UnsupportedMethodError("this wallet predates requestPermissions \u2014 use connect()"));
|
|
173
210
|
}
|
|
174
|
-
return
|
|
211
|
+
return this.call(() => this.provider.requestPermissions(), "requestPermissions").then(unwrap);
|
|
175
212
|
}
|
|
176
213
|
/** Revoke THIS origin's own access (silent). Returns { revoked }. Safe no-op on older wallets. */
|
|
177
214
|
async revokePermissions() {
|
|
178
215
|
if (typeof this.provider.revokePermissions !== "function") return { revoked: false };
|
|
179
|
-
return unwrap(await this.provider.revokePermissions());
|
|
216
|
+
return unwrap(await this.call(() => this.provider.revokePermissions(), "revokePermissions"));
|
|
180
217
|
}
|
|
181
218
|
/**
|
|
182
219
|
* Subscribe to provider events. `accountsChanged` fires with `[]` when this site loses access
|
|
@@ -192,26 +229,26 @@ var WalletConnection = class {
|
|
|
192
229
|
}
|
|
193
230
|
/** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
|
|
194
231
|
send(params) {
|
|
195
|
-
return
|
|
232
|
+
return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
|
|
196
233
|
}
|
|
197
234
|
/** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
|
|
198
235
|
propose(params) {
|
|
199
|
-
return
|
|
236
|
+
return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
|
|
200
237
|
}
|
|
201
238
|
/** Attest/support a proposal (always prompts). */
|
|
202
239
|
attest(params) {
|
|
203
|
-
return
|
|
240
|
+
return this.call(() => this.provider.attest(params), "attest").then(unwrapWrite);
|
|
204
241
|
}
|
|
205
242
|
/** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
|
|
206
243
|
sealClaim(params) {
|
|
207
|
-
return
|
|
244
|
+
return this.call(() => this.provider.sealClaim(params), "sealClaim").then(unwrapWrite);
|
|
208
245
|
}
|
|
209
246
|
/** Reveal a previously sealed claim by its sealing txid (always prompts). */
|
|
210
247
|
revealClaim(txid) {
|
|
211
248
|
if (typeof txid !== "string" || !txid) {
|
|
212
249
|
return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
|
|
213
250
|
}
|
|
214
|
-
return
|
|
251
|
+
return this.call(() => this.provider.revealClaim(txid), "revealClaim").then(unwrapWrite);
|
|
215
252
|
}
|
|
216
253
|
};
|
|
217
254
|
async function getWallet(opts = {}) {
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getWallet
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PIPN6FSA.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
|
};
|