@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/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.3.2";
33
+ var SDK_VERSION = "0.4.1";
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. */
@@ -104,7 +104,7 @@ var SubmitInFlightError = class extends CairnError {
104
104
  this.txid = txid;
105
105
  }
106
106
  };
107
- var NATIVE_CODES = /* @__PURE__ */ new Set(["ACCOUNT_CHANGED", "FIRST_PARTY_ONLY", "RATE_LIMITED", "APPROVAL_CLOSED", "FORBIDDEN", "INTERNAL"]);
107
+ var NATIVE_CODES = /* @__PURE__ */ new Set(["FIRST_PARTY_ONLY", "RATE_LIMITED", "FORBIDDEN", "INTERNAL"]);
108
108
  var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
109
109
  var NESTED_TERMINAL = /* @__PURE__ */ new Set([
110
110
  "FILL_UNSAFE",
@@ -148,6 +148,7 @@ function mapSubmitResultError(result) {
148
148
  }
149
149
 
150
150
  // src/connect.ts
151
+ var DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 6e4;
151
152
  function discoverProviders(opts = {}) {
152
153
  const timeoutMs = opts.timeoutMs ?? 1e3;
153
154
  return new Promise((resolve) => {
@@ -227,8 +228,27 @@ function unwrapWrite(reply) {
227
228
  var WalletConnection = class {
228
229
  provider;
229
230
  _addr = null;
230
- constructor(provider) {
231
+ /** B7c: per-call provider timeout (ms). GENEROUS by default so it never rejects an in-flight human
232
+ * approval; 0 disables it. Every provider call (writes especially) races against it. */
233
+ _timeoutMs;
234
+ constructor(provider, opts = {}) {
231
235
  this.provider = provider;
236
+ this._timeoutMs = opts.timeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
237
+ }
238
+ // B7c: race a provider call against the per-call timeout. On timeout, throw a fail-closed CairnError
239
+ // (retryable:false: a write may already have reached the wallet, so never invite a blind auto-retry). The
240
+ // genuine wallet resolves quickly once the user acts; this only fires on a truly hung/absent response.
241
+ call(op, label) {
242
+ const ms = this._timeoutMs;
243
+ const invoke = () => new Promise((resolve) => {
244
+ resolve(op());
245
+ });
246
+ if (!Number.isFinite(ms) || ms <= 0) return invoke();
247
+ let timer;
248
+ const timeout = new Promise((_, reject) => {
249
+ timer = setTimeout(() => reject(new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
250
+ });
251
+ return Promise.race([invoke(), timeout]).finally(() => clearTimeout(timer));
232
252
  }
233
253
  /** The wallet's version string (e.g. "0.2.x"). */
234
254
  get version() {
@@ -240,13 +260,13 @@ var WalletConnection = class {
240
260
  }
241
261
  /** Request connection. First time per origin prompts the user; afterwards may be silent. */
242
262
  async connect() {
243
- const { addr } = unwrap(await this.provider.connect());
263
+ const { addr } = unwrap(await this.call(() => this.provider.connect(), "connect"));
244
264
  this._addr = addr;
245
265
  return addr;
246
266
  }
247
267
  /** Get the connected address (does not force a new connection prompt if already consented). */
248
268
  async getAddress() {
249
- const { addr } = unwrap(await this.provider.getAddress());
269
+ const { addr } = unwrap(await this.call(() => this.provider.getAddress(), "getAddress"));
250
270
  this._addr = addr;
251
271
  return addr;
252
272
  }
@@ -259,7 +279,7 @@ var WalletConnection = class {
259
279
  if (typeof this.provider.signIn !== "function") {
260
280
  return Promise.reject(new UnsupportedMethodError("this wallet does not expose signIn()"));
261
281
  }
262
- return Promise.resolve(this.provider.signIn()).then(unwrap);
282
+ return this.call(() => this.provider.signIn(), "signIn").then(unwrap);
263
283
  }
264
284
  /**
265
285
  * Audience-bound "Sign in with CSD" (SIWC) — the secure, replay-resistant third-party login.
@@ -275,7 +295,7 @@ var WalletConnection = class {
275
295
  if (typeof this.provider.signInWithCsd !== "function") {
276
296
  return Promise.reject(new UnsupportedMethodError("this wallet predates Sign-in-with-CSD \u2014 ask the user to update the Cairn Wallet"));
277
297
  }
278
- return Promise.resolve(this.provider.signInWithCsd(params)).then(unwrap);
298
+ return this.call(() => this.provider.signInWithCsd(params), "signInWithCsd").then(unwrap);
279
299
  }
280
300
  /** True if this wallet supports audience-bound SIWC (safe to call `signInWithCsd`). */
281
301
  get supportsSiwc() {
@@ -285,7 +305,7 @@ var WalletConnection = class {
285
305
  async getCapabilities() {
286
306
  if (typeof this.provider.getCapabilities !== "function") return null;
287
307
  try {
288
- return await this.provider.getCapabilities();
308
+ return await this.call(() => this.provider.getCapabilities(), "getCapabilities");
289
309
  } catch {
290
310
  return null;
291
311
  }
@@ -301,24 +321,40 @@ var WalletConnection = class {
301
321
  if (typeof this.provider.fillOffer !== "function") {
302
322
  return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
303
323
  }
304
- return Promise.resolve(this.provider.fillOffer(params)).then(unwrapWrite);
324
+ this.adviseFillSpvCapability();
325
+ return this.call(() => this.provider.fillOffer(params), "fillOffer").then(unwrapWrite);
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.`);
305
341
  }
306
342
  /** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
307
343
  async getPermissions() {
308
344
  if (typeof this.provider.getPermissions !== "function") return [];
309
- return unwrap(await this.provider.getPermissions());
345
+ return unwrap(await this.call(() => this.provider.getPermissions(), "getPermissions"));
310
346
  }
311
347
  /** Explicitly request permission to see the address (prompts, like connect). */
312
348
  requestPermissions() {
313
349
  if (typeof this.provider.requestPermissions !== "function") {
314
350
  return Promise.reject(new UnsupportedMethodError("this wallet predates requestPermissions \u2014 use connect()"));
315
351
  }
316
- return Promise.resolve(this.provider.requestPermissions()).then(unwrap);
352
+ return this.call(() => this.provider.requestPermissions(), "requestPermissions").then(unwrap);
317
353
  }
318
354
  /** Revoke THIS origin's own access (silent). Returns { revoked }. Safe no-op on older wallets. */
319
355
  async revokePermissions() {
320
356
  if (typeof this.provider.revokePermissions !== "function") return { revoked: false };
321
- return unwrap(await this.provider.revokePermissions());
357
+ return unwrap(await this.call(() => this.provider.revokePermissions(), "revokePermissions"));
322
358
  }
323
359
  /**
324
360
  * Subscribe to provider events. `accountsChanged` fires with `[]` when this site loses access
@@ -334,26 +370,26 @@ var WalletConnection = class {
334
370
  }
335
371
  /** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
336
372
  send(params) {
337
- return Promise.resolve(this.provider.send(params)).then(unwrapWrite);
373
+ return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
338
374
  }
339
375
  /** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
340
376
  propose(params) {
341
- return Promise.resolve(this.provider.propose(params)).then(unwrapWrite);
377
+ return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
342
378
  }
343
379
  /** Attest/support a proposal (always prompts). */
344
380
  attest(params) {
345
- return Promise.resolve(this.provider.attest(params)).then(unwrapWrite);
381
+ return this.call(() => this.provider.attest(params), "attest").then(unwrapWrite);
346
382
  }
347
383
  /** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
348
384
  sealClaim(params) {
349
- return Promise.resolve(this.provider.sealClaim(params)).then(unwrapWrite);
385
+ return this.call(() => this.provider.sealClaim(params), "sealClaim").then(unwrapWrite);
350
386
  }
351
387
  /** Reveal a previously sealed claim by its sealing txid (always prompts). */
352
388
  revealClaim(txid) {
353
389
  if (typeof txid !== "string" || !txid) {
354
390
  return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
355
391
  }
356
- return Promise.resolve(this.provider.revealClaim(txid)).then(unwrapWrite);
392
+ return this.call(() => this.provider.revealClaim(txid), "revealClaim").then(unwrapWrite);
357
393
  }
358
394
  };
359
395
  async function getWallet(opts = {}) {
@@ -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). An SPV pre-verify helper in this SDK is roadmapped.
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;
@@ -208,7 +209,13 @@ declare function isInstalled(): boolean;
208
209
  declare class WalletConnection {
209
210
  readonly provider: CairnProvider;
210
211
  private _addr;
211
- constructor(provider: CairnProvider);
212
+ /** B7c: per-call provider timeout (ms). GENEROUS by default so it never rejects an in-flight human
213
+ * approval; 0 disables it. Every provider call (writes especially) races against it. */
214
+ private readonly _timeoutMs;
215
+ constructor(provider: CairnProvider, opts?: {
216
+ timeoutMs?: number;
217
+ });
218
+ private call;
212
219
  /** The wallet's version string (e.g. "0.2.x"). */
213
220
  get version(): string;
214
221
  /** The connected address, if `connect()`/`getAddress()` has resolved. */
@@ -243,6 +250,8 @@ declare class WalletConnection {
243
250
  * 0.2.60+, which binds payment/rebate/fee to the merkle-proven offer) is the payment-grade check; see FillParams.
244
251
  */
245
252
  fillOffer(params: FillParams): Promise<TxResult>;
253
+ private _fillSpvAdvised;
254
+ private adviseFillSpvCapability;
246
255
  /** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
247
256
  getPermissions(): Promise<WalletPermission[]>;
248
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). An SPV pre-verify helper in this SDK is roadmapped.
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;
@@ -208,7 +209,13 @@ declare function isInstalled(): boolean;
208
209
  declare class WalletConnection {
209
210
  readonly provider: CairnProvider;
210
211
  private _addr;
211
- constructor(provider: CairnProvider);
212
+ /** B7c: per-call provider timeout (ms). GENEROUS by default so it never rejects an in-flight human
213
+ * approval; 0 disables it. Every provider call (writes especially) races against it. */
214
+ private readonly _timeoutMs;
215
+ constructor(provider: CairnProvider, opts?: {
216
+ timeoutMs?: number;
217
+ });
218
+ private call;
212
219
  /** The wallet's version string (e.g. "0.2.x"). */
213
220
  get version(): string;
214
221
  /** The connected address, if `connect()`/`getAddress()` has resolved. */
@@ -243,6 +250,8 @@ declare class WalletConnection {
243
250
  * 0.2.60+, which binds payment/rebate/fee to the merkle-proven offer) is the payment-grade check; see FillParams.
244
251
  */
245
252
  fillOffer(params: FillParams): Promise<TxResult>;
253
+ private _fillSpvAdvised;
254
+ private adviseFillSpvCapability;
246
255
  /** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
247
256
  getPermissions(): Promise<WalletPermission[]>;
248
257
  /** Explicitly request permission to see the address (prompts, like connect). */
package/dist/connect.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  discoverProviders,
6
6
  getWallet,
7
7
  isInstalled
8
- } from "./chunk-BN3PFJDY.js";
9
- import "./chunk-FMXNJQWL.js";
8
+ } from "./chunk-PIPN6FSA.js";
9
+ import "./chunk-ERBT4DFD.js";
10
10
  export {
11
11
  WalletConnection,
12
12
  connect,
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.3.2";
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. */
package/dist/content.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ContentClient
3
- } from "./chunk-JKC5K253.js";
4
- import "./chunk-FMXNJQWL.js";
3
+ } from "./chunk-OCEHXK6A.js";
4
+ import "./chunk-ERBT4DFD.js";
5
5
  export {
6
6
  ContentClient
7
7
  };
@@ -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.3.2";
28
+ var SDK_VERSION = "0.4.1";
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. */
@@ -99,7 +99,7 @@ var SubmitInFlightError = class extends CairnError {
99
99
  this.txid = txid;
100
100
  }
101
101
  };
102
- var NATIVE_CODES = /* @__PURE__ */ new Set(["ACCOUNT_CHANGED", "FIRST_PARTY_ONLY", "RATE_LIMITED", "APPROVAL_CLOSED", "FORBIDDEN", "INTERNAL"]);
102
+ var NATIVE_CODES = /* @__PURE__ */ new Set(["FIRST_PARTY_ONLY", "RATE_LIMITED", "FORBIDDEN", "INTERNAL"]);
103
103
  var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
104
104
  var NESTED_TERMINAL = /* @__PURE__ */ new Set([
105
105
  "FILL_UNSAFE",
@@ -143,6 +143,7 @@ function mapSubmitResultError(result) {
143
143
  }
144
144
 
145
145
  // src/connect.ts
146
+ var DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 6e4;
146
147
  function detectProvider(opts = {}) {
147
148
  const timeoutMs = opts.timeoutMs ?? 3e3;
148
149
  return new Promise((resolve, reject) => {
@@ -199,8 +200,27 @@ function unwrapWrite(reply) {
199
200
  var WalletConnection = class {
200
201
  provider;
201
202
  _addr = null;
202
- constructor(provider) {
203
+ /** B7c: per-call provider timeout (ms). GENEROUS by default so it never rejects an in-flight human
204
+ * approval; 0 disables it. Every provider call (writes especially) races against it. */
205
+ _timeoutMs;
206
+ constructor(provider, opts = {}) {
203
207
  this.provider = provider;
208
+ this._timeoutMs = opts.timeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
209
+ }
210
+ // B7c: race a provider call against the per-call timeout. On timeout, throw a fail-closed CairnError
211
+ // (retryable:false: a write may already have reached the wallet, so never invite a blind auto-retry). The
212
+ // genuine wallet resolves quickly once the user acts; this only fires on a truly hung/absent response.
213
+ call(op, label) {
214
+ const ms = this._timeoutMs;
215
+ const invoke = () => new Promise((resolve) => {
216
+ resolve(op());
217
+ });
218
+ if (!Number.isFinite(ms) || ms <= 0) return invoke();
219
+ let timer;
220
+ const timeout = new Promise((_, reject) => {
221
+ timer = setTimeout(() => reject(new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
222
+ });
223
+ return Promise.race([invoke(), timeout]).finally(() => clearTimeout(timer));
204
224
  }
205
225
  /** The wallet's version string (e.g. "0.2.x"). */
206
226
  get version() {
@@ -212,13 +232,13 @@ var WalletConnection = class {
212
232
  }
213
233
  /** Request connection. First time per origin prompts the user; afterwards may be silent. */
214
234
  async connect() {
215
- const { addr } = unwrap(await this.provider.connect());
235
+ const { addr } = unwrap(await this.call(() => this.provider.connect(), "connect"));
216
236
  this._addr = addr;
217
237
  return addr;
218
238
  }
219
239
  /** Get the connected address (does not force a new connection prompt if already consented). */
220
240
  async getAddress() {
221
- const { addr } = unwrap(await this.provider.getAddress());
241
+ const { addr } = unwrap(await this.call(() => this.provider.getAddress(), "getAddress"));
222
242
  this._addr = addr;
223
243
  return addr;
224
244
  }
@@ -231,7 +251,7 @@ var WalletConnection = class {
231
251
  if (typeof this.provider.signIn !== "function") {
232
252
  return Promise.reject(new UnsupportedMethodError("this wallet does not expose signIn()"));
233
253
  }
234
- return Promise.resolve(this.provider.signIn()).then(unwrap);
254
+ return this.call(() => this.provider.signIn(), "signIn").then(unwrap);
235
255
  }
236
256
  /**
237
257
  * Audience-bound "Sign in with CSD" (SIWC) — the secure, replay-resistant third-party login.
@@ -247,7 +267,7 @@ var WalletConnection = class {
247
267
  if (typeof this.provider.signInWithCsd !== "function") {
248
268
  return Promise.reject(new UnsupportedMethodError("this wallet predates Sign-in-with-CSD \u2014 ask the user to update the Cairn Wallet"));
249
269
  }
250
- return Promise.resolve(this.provider.signInWithCsd(params)).then(unwrap);
270
+ return this.call(() => this.provider.signInWithCsd(params), "signInWithCsd").then(unwrap);
251
271
  }
252
272
  /** True if this wallet supports audience-bound SIWC (safe to call `signInWithCsd`). */
253
273
  get supportsSiwc() {
@@ -257,7 +277,7 @@ var WalletConnection = class {
257
277
  async getCapabilities() {
258
278
  if (typeof this.provider.getCapabilities !== "function") return null;
259
279
  try {
260
- return await this.provider.getCapabilities();
280
+ return await this.call(() => this.provider.getCapabilities(), "getCapabilities");
261
281
  } catch {
262
282
  return null;
263
283
  }
@@ -273,24 +293,40 @@ var WalletConnection = class {
273
293
  if (typeof this.provider.fillOffer !== "function") {
274
294
  return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
275
295
  }
276
- return Promise.resolve(this.provider.fillOffer(params)).then(unwrapWrite);
296
+ this.adviseFillSpvCapability();
297
+ return this.call(() => this.provider.fillOffer(params), "fillOffer").then(unwrapWrite);
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.`);
277
313
  }
278
314
  /** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
279
315
  async getPermissions() {
280
316
  if (typeof this.provider.getPermissions !== "function") return [];
281
- return unwrap(await this.provider.getPermissions());
317
+ return unwrap(await this.call(() => this.provider.getPermissions(), "getPermissions"));
282
318
  }
283
319
  /** Explicitly request permission to see the address (prompts, like connect). */
284
320
  requestPermissions() {
285
321
  if (typeof this.provider.requestPermissions !== "function") {
286
322
  return Promise.reject(new UnsupportedMethodError("this wallet predates requestPermissions \u2014 use connect()"));
287
323
  }
288
- return Promise.resolve(this.provider.requestPermissions()).then(unwrap);
324
+ return this.call(() => this.provider.requestPermissions(), "requestPermissions").then(unwrap);
289
325
  }
290
326
  /** Revoke THIS origin's own access (silent). Returns { revoked }. Safe no-op on older wallets. */
291
327
  async revokePermissions() {
292
328
  if (typeof this.provider.revokePermissions !== "function") return { revoked: false };
293
- return unwrap(await this.provider.revokePermissions());
329
+ return unwrap(await this.call(() => this.provider.revokePermissions(), "revokePermissions"));
294
330
  }
295
331
  /**
296
332
  * Subscribe to provider events. `accountsChanged` fires with `[]` when this site loses access
@@ -306,26 +342,26 @@ var WalletConnection = class {
306
342
  }
307
343
  /** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
308
344
  send(params) {
309
- return Promise.resolve(this.provider.send(params)).then(unwrapWrite);
345
+ return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
310
346
  }
311
347
  /** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
312
348
  propose(params) {
313
- return Promise.resolve(this.provider.propose(params)).then(unwrapWrite);
349
+ return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
314
350
  }
315
351
  /** Attest/support a proposal (always prompts). */
316
352
  attest(params) {
317
- return Promise.resolve(this.provider.attest(params)).then(unwrapWrite);
353
+ return this.call(() => this.provider.attest(params), "attest").then(unwrapWrite);
318
354
  }
319
355
  /** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
320
356
  sealClaim(params) {
321
- return Promise.resolve(this.provider.sealClaim(params)).then(unwrapWrite);
357
+ return this.call(() => this.provider.sealClaim(params), "sealClaim").then(unwrapWrite);
322
358
  }
323
359
  /** Reveal a previously sealed claim by its sealing txid (always prompts). */
324
360
  revealClaim(txid) {
325
361
  if (typeof txid !== "string" || !txid) {
326
362
  return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
327
363
  }
328
- return Promise.resolve(this.provider.revealClaim(txid)).then(unwrapWrite);
364
+ return this.call(() => this.provider.revealClaim(txid), "revealClaim").then(unwrapWrite);
329
365
  }
330
366
  };
331
367
  async function getWallet(opts = {}) {
@@ -337,6 +373,9 @@ var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error:
337
373
  var CairnController = class {
338
374
  state = DISCONNECTED;
339
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;
340
379
  listeners = /* @__PURE__ */ new Set();
341
380
  getWalletFn;
342
381
  detectOpts;
@@ -389,34 +428,43 @@ var CairnController = class {
389
428
  }
390
429
  /** Detect + connect (prompts the user the first time per origin). Resolves the connected address. */
391
430
  connect = async () => {
431
+ const epoch = this.epoch;
392
432
  this.setState({ status: "connecting", error: null });
393
433
  try {
394
- if (!this.conn) {
395
- this.conn = await this.getWalletFn(this.detectOpts);
396
- this.conn.on("accountsChanged", this.onAccounts);
397
- this.conn.on("disconnect", this.onDisconnect);
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;
398
441
  }
399
- const addr = await this.conn.connect();
442
+ const addr = await conn.connect();
443
+ if (epoch !== this.epoch) throw new Error("connection was torn down while awaiting approval - call connect() again");
400
444
  this.setState({ status: "connected", account: addr, error: null });
401
445
  return addr;
402
446
  } catch (e) {
403
- 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) });
404
448
  throw e;
405
449
  }
406
450
  };
407
- /** 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". */
408
454
  disconnect = async () => {
409
- try {
410
- await this.conn?.revokePermissions();
411
- } catch {
412
- }
455
+ this.epoch++;
456
+ const conn = this.conn;
413
457
  this.detach();
414
458
  this.conn = null;
415
459
  this.setState({ status: "disconnected", account: null, error: null });
460
+ try {
461
+ await conn?.revokePermissions();
462
+ } catch {
463
+ }
416
464
  };
417
465
  /** Audience-bound sign-in (must connect() first). Returns the signed artifact to verify server-side. */
418
466
  signInWithCsd = (params) => {
419
- 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()"));
420
468
  return this.conn.signInWithCsd(params);
421
469
  };
422
470
  };
@@ -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>;
@@ -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>;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  CairnController
3
- } from "./chunk-AGU6K5EZ.js";
4
- import "./chunk-BN3PFJDY.js";
5
- import "./chunk-FMXNJQWL.js";
3
+ } from "./chunk-QATE6AIP.js";
4
+ import "./chunk-PIPN6FSA.js";
5
+ import "./chunk-ERBT4DFD.js";
6
6
  export {
7
7
  CairnController
8
8
  };