@capxul/sdk 2.1.1 → 2.3.0

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/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { A as OBSERVATION_CONTEXT_HEADER, B as captureException, C as bootstrapErrorFromCapxul, D as fingerprintPaymentIntent, E as version, F as EngineeringTelemetryBootstrapPolicy, H as CAPXUL_PAYMENTS_V2_ADDRESS, I as isSettingUpLifecycle, K as destination, L as formatTraceparent, M as sanitizeObservationContext, N as CAPXUL_FUNCTIONS, O as toWei, P as BootstrapEnvelope, R as copyInvocationObservation, S as BootstrapPortTag, T as AuthClientPortTag, U as normalizeBindingEmail, V as captureExceptionSync, W as BASE_SEPOLIA_CHAIN_ID, _ as identityErrorFromCapxul, a as observeFailedResult, b as ClockError, c as PostHogTelemetryLayer, d as SmartAccountPortTag, f as smartAccountErrorFromCapxul, g as IdentityPortTag, h as wireChainId, i as observationContextProps, j as encodeObservationContextHeader, k as fromWei, l as TelemetryPortTag, m as accountReadErrorFromCapxul, n as postHogProductTelemetry, o as postHogFailureObservation, p as AccountReadPortTag, r as CAPXUL_SDK_EXPECTED_OUTCOME_EVENT, s as detectAuthCacheAdapter, t as assembleCapxulClient, v as ConvexCallPortTag, w as authClientPortFromPromiseAdapter, x as ClockPortTag, y as convexCallErrorFromCapxul, z as readInvocationObservation } from "./create-capxul-client-CPhTPz_9.mjs";
2
- import { A as toSessionToken, C as toEpochMs, D as toOrgId, E as toKycTier, F as isCapxulError, M as CapxulError, O as toPublishableKey, P as Errors, S as toEmail, T as toJwtToken, _ as toAuthUserId, b as toCurrencyCode, h as toAllowedOrigin, j as decodeConvexError, k as toRoleKey, m as toAddress, o as AuthCachePortTag, p as toAccountId, v as toChainId, w as toEpochSeconds, x as toDurationMs, y as toCountryCode } from "./InMemoryAuthCacheAdapter-Rc8tCtml.mjs";
1
+ import { A as fingerprintPaymentIntent, B as formatTraceparent, C as ClockPortTag, D as AuthClientError, E as authClientPortFromPromiseAdapter, F as sanitizeObservationContext, G as signerFailure, H as readInvocationObservation, I as CAPXUL_FUNCTIONS, J as BASE_SEPOLIA_CHAIN_ID, K as CAPXUL_PAYMENTS_V2_ADDRESS, L as BootstrapEnvelope, M as fromWei, N as OBSERVATION_CONTEXT_HEADER, O as AuthClientPortTag, P as encodeObservationContextHeader, Q as isRestoring, R as EngineeringTelemetryBootstrapPolicy, S as ClockError, T as bootstrapErrorFromCapxul, U as causeChain, V as copyInvocationObservation, W as injectedWalletSigner, X as destination, Z as isClaimed, _ as wireChainId, a as observeFailedResult, b as ConvexCallPortTag, c as captureExceptionSync, d as TelemetryPortTag, g as accountReadErrorFromCapxul, h as AccountReadPortTag, i as observationContextProps, j as toWei, k as version, l as detectAuthCacheAdapter, m as smartAccountErrorFromCapxul, n as postHogProductTelemetry, o as postHogFailureObservation, p as SmartAccountPortTag, q as normalizeBindingEmail, r as CAPXUL_SDK_EXPECTED_OUTCOME_EVENT, s as captureException, t as assembleCapxulClient, u as PostHogTelemetryLayer, v as IdentityPortTag, w as BootstrapPortTag, x as convexCallErrorFromCapxul, y as identityErrorFromCapxul, z as isSettingUpLifecycle } from "./create-capxul-client-mArFr_Os.mjs";
2
+ import { C as toDurationMs, D as toJwtToken, E as toEpochSeconds, F as CAPXUL_ERROR_CODES, I as CapxulError, M as toRoleKey, N as toSessionToken, O as toKycTier, P as decodeConvexError, R as Errors, S as toCurrencyCode, T as toEpochMs, b as toChainId, g as toAllowedOrigin, h as toAddress, j as toPublishableKey, k as toOrgId, m as toAccountId, o as AuthCachePortTag, p as currencySymbolFor, v as toAuthUserId, w as toEmail, x as toCountryCode, z as isCapxulError } from "./InMemoryAuthCacheAdapter-qMpBOGb3.mjs";
3
3
  import { keccak256, recoverAddress, stringToHex } from "viem";
4
4
  import { Cause, Context, Data, Effect, Exit, Layer, Result, SchemaIssue, SchemaParser, Scope, Tracer } from "effect";
5
5
  import { getFunctionName, makeFunctionReference } from "convex/server";
@@ -8,6 +8,89 @@ import { FetchHttpClient, Headers, HttpClient } from "effect/unstable/http";
8
8
  import { OtlpExporter, OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
9
9
  import { ConvexClient } from "convex/browser";
10
10
  import { AccountTypeEnum, ChainTypeEnum, EmbeddedState, Openfort, RecoveryMethod, ThirdPartyOAuthProvider } from "@openfort/openfort-js";
11
+ //#region src/domain/money/format-money.ts
12
+ function formatMoney(money, options = {}) {
13
+ if (!Number.isInteger(money.decimals) || money.decimals < 0) throw Errors.invalidInput("money.decimals", "must be a non-negative integer");
14
+ const symbol = currencySymbolFor(money.currency);
15
+ const maximumFractionDigits = options.grammar === "code" ? 4 : 2;
16
+ const rounded = roundDecimal(money.value, money.decimals, maximumFractionDigits);
17
+ const grouped = rounded.integer.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
18
+ const signed = `${rounded.negative ? "-" : ""}${grouped}`;
19
+ if (options.grammar === "code") {
20
+ if (!rounded.hadFraction) return `${signed} ${String(money.currency)}`;
21
+ let displayedFraction = rounded.fraction;
22
+ while (displayedFraction.length > 2 && displayedFraction.endsWith("0")) displayedFraction = displayedFraction.slice(0, -1);
23
+ return `${signed}.${displayedFraction} ${String(money.currency)}`;
24
+ }
25
+ return `${symbol}${signed}.${rounded.fraction}`;
26
+ }
27
+ function roundDecimal(value, decimals, maximumFractionDigits) {
28
+ const match = /^(-?)(\d+)(?:\.(\d+))?$/.exec(value);
29
+ if (match === null) throw Errors.invalidInput("money.value", "must be a base-10 decimal string");
30
+ const integer = match[2] ?? "0";
31
+ const sourceFraction = match[3] ?? "";
32
+ if (sourceFraction.length > decimals) throw Errors.invalidInput("money.value", "fraction exceeds money.decimals");
33
+ const scale = 10n ** BigInt(maximumFractionDigits);
34
+ const keptFraction = sourceFraction.slice(0, maximumFractionDigits).padEnd(maximumFractionDigits, "0");
35
+ let scaled = BigInt(integer) * scale + BigInt(keptFraction || "0");
36
+ if ((sourceFraction[maximumFractionDigits] ?? "0") >= "5") scaled += 1n;
37
+ return {
38
+ negative: match[1] === "-",
39
+ integer: (scaled / scale).toString(),
40
+ fraction: (scaled % scale).toString().padStart(maximumFractionDigits, "0"),
41
+ hadFraction: /[1-9]/.test(sourceFraction)
42
+ };
43
+ }
44
+ //#endregion
45
+ //#region src/domain/money/parse-money.ts
46
+ /**
47
+ * ADR-0023 R1: the reason is a closed code and the ONLY failure vocabulary —
48
+ * the consuming app maps each code to its own sentence. The SDK ships no
49
+ * English for user-text failure.
50
+ */
51
+ function isMoneyParseError(value) {
52
+ return "kind" in value;
53
+ }
54
+ const AMOUNT_PATTERN = /^(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?$/;
55
+ const ZERO_PATTERN = /^0+(?:\.0+)?$/;
56
+ function parseMoney(input, asset) {
57
+ if (!Number.isInteger(asset.decimals) || asset.decimals < 0) throw Errors.invalidInput("asset.decimals", "must be a non-negative integer");
58
+ const value = input.trim();
59
+ if (value.length === 0) return {
60
+ kind: "error",
61
+ reason: "required"
62
+ };
63
+ if (value.length > 24) return {
64
+ kind: "error",
65
+ reason: "too-long"
66
+ };
67
+ const negative = value.startsWith("-");
68
+ const unsigned = negative ? value.slice(1) : value;
69
+ if (!AMOUNT_PATTERN.test(unsigned)) return {
70
+ kind: "error",
71
+ reason: "invalid-format"
72
+ };
73
+ if (negative) return {
74
+ kind: "error",
75
+ reason: "non-positive"
76
+ };
77
+ const normalized = unsigned.replaceAll(",", "");
78
+ const fraction = normalized.split(".")[1];
79
+ if (fraction !== void 0 && fraction.length > asset.decimals) return {
80
+ kind: "error",
81
+ reason: "too-many-decimals"
82
+ };
83
+ if (ZERO_PATTERN.test(normalized)) return {
84
+ kind: "error",
85
+ reason: "non-positive"
86
+ };
87
+ return {
88
+ currency: asset.currency,
89
+ value: normalized,
90
+ decimals: asset.decimals
91
+ };
92
+ }
93
+ //#endregion
11
94
  //#region src/surface/account-providers.ts
12
95
  function localPrivateKeyAccountProvider(input) {
13
96
  const account = privateKeyToAccount(input.privateKey);
@@ -53,7 +136,7 @@ function eip1193AccountProvider(input) {
53
136
  } catch (err) {
54
137
  return {
55
138
  ok: false,
56
- error: Errors.providerError("eip1193", "eth_accounts", err)
139
+ error: signerFailure("injected-eip1193", "eth_accounts", err)
57
140
  };
58
141
  } finally {
59
142
  inFlight = null;
@@ -78,60 +161,6 @@ function firstAccount(value) {
78
161
  return typeof first === "string" ? first : null;
79
162
  }
80
163
  //#endregion
81
- //#region src/signer.ts
82
- const EVM_ADDRESS_HEX$1 = /^0x[0-9a-fA-F]{40}$/;
83
- const ECDSA_SIGNATURE_HEX$1 = /^0x[0-9a-fA-F]{130}$/;
84
- const SAFE_OP_DIGEST_HEX$1 = /^0x[0-9a-fA-F]{64}$/;
85
- /**
86
- * Browser `CapxulSigner` backed by an injected EIP-1193 wallet (MetaMask, etc.).
87
- * Signs the SafeOp digest via `eth_sign`, then verifies the returned signature
88
- * recovers the selected account against that raw digest. Wallets that prefix
89
- * `eth_sign` payloads are rejected before the backend submits an invalid SafeOp.
90
- * The node key signer lives in `@capxul/sdk/node` (`localPrivateKeySigner`).
91
- */
92
- function injectedWalletSigner(provider) {
93
- const resolveAddress = async () => {
94
- const accounts = await provider.request({ method: "eth_requestAccounts" });
95
- const first = Array.isArray(accounts) ? accounts[0] : void 0;
96
- if (typeof first !== "string") throw new Error("injectedWalletSigner: wallet returned no accounts");
97
- if (!EVM_ADDRESS_HEX$1.test(first)) throw new Error("injectedWalletSigner: wallet returned invalid address format");
98
- return toAddress(first);
99
- };
100
- return {
101
- source: "injected-eip1193",
102
- getAddress: resolveAddress,
103
- async signUserOpHash(hash) {
104
- if (!SAFE_OP_DIGEST_HEX$1.test(hash)) throw new Error("injectedWalletSigner: SafeOp digest must be a 0x-prefixed 32-byte hex");
105
- const address = await resolveAddress();
106
- let signature;
107
- try {
108
- signature = await provider.request({
109
- method: "eth_sign",
110
- params: [address, hash]
111
- });
112
- } catch (cause) {
113
- const detail = cause instanceof Error ? cause.message : String(cause);
114
- throw new Error(`injectedWalletSigner: eth_sign failed; enable raw-hash signing for deployment (${detail})`, { cause });
115
- }
116
- if (typeof signature !== "string") throw new Error("injectedWalletSigner: wallet returned a non-string signature");
117
- if (!ECDSA_SIGNATURE_HEX$1.test(signature)) throw new Error("injectedWalletSigner: wallet returned invalid signature format");
118
- if ((await recoverRawDigestSigner$1({
119
- hash,
120
- signature
121
- })).toLowerCase() !== address.toLowerCase()) throw new Error("injectedWalletSigner: wallet signature did not recover the selected account for the raw SafeOp digest; use a raw-hash-capable wallet or @capxul/sdk/node localPrivateKeySigner for deployed flows");
122
- return signature;
123
- }
124
- };
125
- }
126
- async function recoverRawDigestSigner$1(input) {
127
- try {
128
- return toAddress(await recoverAddress(input));
129
- } catch (cause) {
130
- const detail = cause instanceof Error ? cause.message : String(cause);
131
- throw new Error(`injectedWalletSigner: could not verify raw SafeOp digest signature (${detail})`, { cause });
132
- }
133
- }
134
- //#endregion
135
164
  //#region src/dev-signer.ts
136
165
  const SESSION_KEY = "capxul.session";
137
166
  /**
@@ -1399,9 +1428,27 @@ function mapToCapxulError(operation, err) {
1399
1428
  function mapToConvexCallError(operation, err) {
1400
1429
  return convexCallErrorFromCapxul(operation, mapToCapxulError(operation, err));
1401
1430
  }
1431
+ const TRANSPORT_ERROR_CODES = new Set([
1432
+ "EAI_AGAIN",
1433
+ "ECONNREFUSED",
1434
+ "ECONNRESET",
1435
+ "ENOTFOUND",
1436
+ "EPIPE",
1437
+ "ETIMEDOUT",
1438
+ "UND_ERR_CONNECT_TIMEOUT",
1439
+ "UND_ERR_HEADERS_TIMEOUT",
1440
+ "UND_ERR_SOCKET"
1441
+ ]);
1402
1442
  function isTransportError(err) {
1403
- const message = err.message.toLowerCase();
1404
- return message.includes("failed to fetch") || message.includes("network") || message.includes("econnrefused") || message.includes("econnreset") || message.includes("enotfound") || message.includes("etimedout") || message.includes("socket");
1443
+ const seen = /* @__PURE__ */ new Set();
1444
+ let current = err;
1445
+ while (current !== void 0 && !seen.has(current)) {
1446
+ seen.add(current);
1447
+ const code = current.code;
1448
+ if (current.name === "FetchError" || current.name === "NetworkError" || typeof code === "string" && TRANSPORT_ERROR_CODES.has(code)) return true;
1449
+ current = current.cause instanceof Error ? current.cause : void 0;
1450
+ }
1451
+ return false;
1405
1452
  }
1406
1453
  //#endregion
1407
1454
  //#region src/adapters/identity/ConvexIdentityAdapter.ts
@@ -1678,6 +1725,7 @@ function parseRoleDefinition(json) {
1678
1725
  ...raw.spend.perDay === void 0 ? {} : { perDay: parseRoleMoney(raw.spend.perDay, "perDay") },
1679
1726
  ...raw.spend.toRecipients === void 0 ? {} : { toRecipients: parseRoleRecipients(raw.spend.toRecipients) }
1680
1727
  } },
1728
+ ...typeof raw.canSpend === "boolean" ? { canSpend: raw.canSpend } : {},
1681
1729
  ...typeof raw.canManageMembers === "boolean" ? { canManageMembers: raw.canManageMembers } : {},
1682
1730
  ...typeof raw.canManageRoles === "boolean" ? { canManageRoles: raw.canManageRoles } : {}
1683
1731
  };
@@ -1848,7 +1896,7 @@ var ConvexOrganizationSetupAdapter = class {
1848
1896
  async authorizeAndSubmitBootstrap(input) {
1849
1897
  const cancelled = cancellation(input.signal);
1850
1898
  if (cancelled !== void 0) return cancelled;
1851
- const signerAddress = await signerResult("getAddress", () => this.#signer.getAddress());
1899
+ const signerAddress = await signerResult(this.#signer.source, "getAddress", () => this.#signer.getAddress());
1852
1900
  if (!signerAddress.ok) return signerAddress;
1853
1901
  const prepared = await runCall("prepareBootstrap", this.#convex.action(this.#fns.prepareBootstrap, copyInvocationObservation(input, {
1854
1902
  orgId: input.orgId,
@@ -1860,7 +1908,7 @@ var ConvexOrganizationSetupAdapter = class {
1860
1908
  if (!authority.ok) return authority;
1861
1909
  const cancelledAfterPrepare = cancellation(input.signal);
1862
1910
  if (cancelledAfterPrepare !== void 0) return cancelledAfterPrepare;
1863
- const signature = await signerResult("signUserOpHash", () => this.#signer.signUserOpHash(prepared.value.digest));
1911
+ const signature = await signerResult(this.#signer.source, "signUserOpHash", () => this.#signer.signUserOpHash(prepared.value.digest));
1864
1912
  if (!signature.ok) return signature;
1865
1913
  const cancelledAfterSign = cancellation(input.signal);
1866
1914
  if (cancelledAfterSign !== void 0) return cancelledAfterSign;
@@ -1949,7 +1997,7 @@ function resetSignerSession(signer) {
1949
1997
  value: void 0
1950
1998
  };
1951
1999
  } catch (cause) {
1952
- return fail(cause instanceof CapxulError ? cause : Errors.providerError("openfort", "resetSession", cause, { failure_mode: "unknown" }));
2000
+ return fail(signerFailure(signer.source, "resetSession", cause));
1953
2001
  }
1954
2002
  }
1955
2003
  async function runCall(operation, effect) {
@@ -1971,14 +2019,14 @@ function publicError(operation, cause) {
1971
2019
  }
1972
2020
  return Errors.providerError("convex-organization", operation, cause);
1973
2021
  }
1974
- async function signerResult(operation, run) {
2022
+ async function signerResult(source, operation, run) {
1975
2023
  try {
1976
2024
  return {
1977
2025
  ok: true,
1978
2026
  value: await run()
1979
2027
  };
1980
2028
  } catch (cause) {
1981
- return fail(cause instanceof CapxulError ? cause : Errors.providerError("organization-signer", operation, cause));
2029
+ return fail(signerFailure(source, operation, cause));
1982
2030
  }
1983
2031
  }
1984
2032
  function validatePreparedAuthorities(prepared, signerAddress) {
@@ -2076,6 +2124,10 @@ var ConsoleDiagnosticAdapter = class {
2076
2124
  function openfortProviderError(operation, cause) {
2077
2125
  return cause instanceof CapxulError ? cause : Errors.providerError("openfort", operation, cause, { failure_mode: "unknown" });
2078
2126
  }
2127
+ /** Build a named PROVIDER_ERROR. The assembled Core SDK boundary reports it. */
2128
+ function failOpenfort(operation, failure_mode, cause) {
2129
+ return Errors.providerError("openfort", operation, cause, { failure_mode });
2130
+ }
2079
2131
  /**
2080
2132
  * True when the browser cannot perform Web Crypto — sandboxed iframes, headless
2081
2133
  * agent browsers, or non-HTTPS origins. OpenFort's embedded-wallet `configure`
@@ -2112,30 +2164,77 @@ function clearStaleOpenfortBrowserStorage(publishableKey) {
2112
2164
  if (scope === void 0) return;
2113
2165
  for (const key of OPENFORT_BROWSER_STORAGE_KEYS) localStorage.removeItem(`${scope}.${key}`);
2114
2166
  }
2167
+ /**
2168
+ * The Openfort error code that names the stale-user class (#1435).
2169
+ * `getThirdPartyAuthToken` skips `authenticateThirdParty` while a `userId` sits
2170
+ * in scoped storage. A purged `userId` therefore pins every later call to a 401
2171
+ * that `extractApiError` reports as `USER_NOT_FOUND`.
2172
+ *
2173
+ * The set holds one member on purpose. Session-expiry codes (`SESSION_EXPIRED`,
2174
+ * `NOT_LOGGED_IN`, `INVALID_TOKEN`, `REFRESH_TOKEN_ERROR`) are a different
2175
+ * cause, and a bare 401 is a different cause again: `app-env-allowlist` is a
2176
+ * 401 by definition (`packages/errors/src/errors.ts:80-81`). Healing those and
2177
+ * tagging them `stale-openfort-cache` would delete the triage signal the tag
2178
+ * exists to carry.
2179
+ */
2180
+ const STALE_USER_ERROR_CODES = new Set(["USER_NOT_FOUND"]);
2181
+ /**
2182
+ * The one predicate that opens the heal. It reads the Openfort error code and
2183
+ * never the message text (ADR-0023 R4). It walks the cause chain the way
2184
+ * `isTransportError` walks it in the Convex transport adapter.
2185
+ *
2186
+ * Known ceiling (#1435): a 401 that carries no recognized code is NOT healed.
2187
+ * `extractApiError` keeps the status only on `AuthenticationError`, so such a
2188
+ * payload is reachable. Add the code a live payload shows; do not add a message
2189
+ * match, because that trades one bug class for an ADR-0023 R4 violation.
2190
+ */
2191
+ function isStaleUserSignal(cause) {
2192
+ for (const link of causeChain(cause)) {
2193
+ const error = link;
2194
+ if (typeof error.error === "string" && STALE_USER_ERROR_CODES.has(error.error) || typeof error.code === "string" && STALE_USER_ERROR_CODES.has(error.code)) return true;
2195
+ }
2196
+ return false;
2197
+ }
2115
2198
  function createOpenfortBrowserSignerFromBootstrap(bootstrap, options = {}) {
2116
2199
  const diagnostic = options.diagnostic;
2117
- const telemetry = options.telemetry;
2118
2200
  const authBaseUrl = normalizeBetterAuthBaseUrl(bootstrap.authBaseUrl);
2201
+ let currentStatus = "unknown";
2202
+ const statusListeners = /* @__PURE__ */ new Set();
2203
+ function setStatus(next) {
2204
+ if (currentStatus === next) return;
2205
+ currentStatus = next;
2206
+ for (const listener of statusListeners) try {
2207
+ listener(next);
2208
+ } catch {
2209
+ statusListeners.delete(listener);
2210
+ diagnostic?.trace("openfort.signerStatusListener", {
2211
+ ok: false,
2212
+ failure_mode: "unknown"
2213
+ });
2214
+ }
2215
+ }
2216
+ const statusStore = {
2217
+ status: () => currentStatus,
2218
+ subscribe: (listener) => {
2219
+ statusListeners.add(listener);
2220
+ return () => {
2221
+ statusListeners.delete(listener);
2222
+ };
2223
+ }
2224
+ };
2119
2225
  /**
2120
2226
  * Closes the black hole: when the browser has no Web Crypto, OpenFort's
2121
2227
  * `configure` would resolve to no address and the failure would be reported
2122
- * as `unknown`. Detect it before any network/wallet work, tag it
2123
- * `no-secure-context` on a PROVIDER_ERROR scoped to `configure`, breadcrumb
2124
- * it via DiagnosticPort, and self-report via TelemetryPort.
2228
+ * as `unknown`. Detect it before any network or wallet work. Tag it
2229
+ * `no-secure-context` on a PROVIDER_ERROR scoped to `configure`. Add a
2230
+ * DiagnosticPort breadcrumb. The assembled Core SDK boundary reports it.
2125
2231
  */
2126
2232
  function failNoSecureContext() {
2127
2233
  diagnostic?.trace("openfort.configure", {
2128
2234
  ok: false,
2129
2235
  failure_mode: "no-secure-context"
2130
2236
  });
2131
- const error = Errors.providerError("openfort", "configure", /* @__PURE__ */ new Error("Web Crypto unavailable: browser is not a secure context"), { failure_mode: "no-secure-context" });
2132
- if (telemetry) captureExceptionSync(telemetry, error, {
2133
- layer: "openfort",
2134
- operation: "configure",
2135
- provider: "openfort",
2136
- failure_mode: "no-secure-context"
2137
- });
2138
- throw error;
2237
+ throw failOpenfort("configure", "no-secure-context", /* @__PURE__ */ new Error("Web Crypto unavailable: browser is not a secure context"));
2139
2238
  }
2140
2239
  function betterAuthSessionUrl() {
2141
2240
  return `${authBaseUrl}/get-session`;
@@ -2186,6 +2285,39 @@ function createOpenfortBrowserSignerFromBootstrap(bootstrap, options = {}) {
2186
2285
  getAccessToken: fetchBetterAuthAccessToken
2187
2286
  }
2188
2287
  });
2288
+ async function configureEmbeddedWallet(encryptionSession) {
2289
+ await openfort.embeddedWallet.configure({
2290
+ accountType: AccountTypeEnum.EOA,
2291
+ chainType: ChainTypeEnum.EVM,
2292
+ recoveryParams: {
2293
+ recoveryMethod: RecoveryMethod.AUTOMATIC,
2294
+ encryptionSession
2295
+ }
2296
+ });
2297
+ }
2298
+ /**
2299
+ * ONE heal cycle on the stale-user rejection (#1435). Clear the scoped
2300
+ * storage that pins the dead `userId`, run the existing configure path — it
2301
+ * re-runs third-party auth against the live Better Auth session now that no
2302
+ * `userId` is cached — and retry the read once. Exactly one cycle: a second
2303
+ * rejection is terminal and carries `failure_mode: "stale-openfort-cache"`.
2304
+ * The caller stays in `recovering` throughout; only the outcome moves it.
2305
+ */
2306
+ async function healStaleOpenfortCache(encryptionSession) {
2307
+ try {
2308
+ clearStaleOpenfortBrowserStorage(bootstrap.openfortPublishableKey);
2309
+ diagnostic?.trace("openfort.storageCleared", { beforeConfigure: false });
2310
+ await configureEmbeddedWallet(encryptionSession);
2311
+ await openfort.embeddedWallet.get();
2312
+ } catch (cause) {
2313
+ diagnostic?.trace("openfort.staleCacheHeal", {
2314
+ ok: false,
2315
+ failure_mode: "stale-openfort-cache"
2316
+ });
2317
+ throw failOpenfort("get", "stale-openfort-cache", cause);
2318
+ }
2319
+ diagnostic?.trace("openfort.staleCacheHeal", { ok: true });
2320
+ }
2189
2321
  let walletReadyPromise = null;
2190
2322
  function startWalletReady() {
2191
2323
  return (async () => {
@@ -2250,14 +2382,7 @@ function createOpenfortBrowserSignerFromBootstrap(bootstrap, options = {}) {
2250
2382
  clearStaleOpenfortBrowserStorage(bootstrap.openfortPublishableKey);
2251
2383
  diagnostic?.trace("openfort.storageCleared", { beforeConfigure: true });
2252
2384
  try {
2253
- await openfort.embeddedWallet.configure({
2254
- accountType: AccountTypeEnum.EOA,
2255
- chainType: ChainTypeEnum.EVM,
2256
- recoveryParams: {
2257
- recoveryMethod: RecoveryMethod.AUTOMATIC,
2258
- encryptionSession: encryptionBody.sessionId
2259
- }
2260
- });
2385
+ await configureEmbeddedWallet(encryptionBody.sessionId);
2261
2386
  diagnostic?.trace("openfort.configure", { ok: true });
2262
2387
  } catch (cause) {
2263
2388
  diagnostic?.trace("openfort.configure", {
@@ -2271,20 +2396,36 @@ function createOpenfortBrowserSignerFromBootstrap(bootstrap, options = {}) {
2271
2396
  await openfort.embeddedWallet.get();
2272
2397
  diagnostic?.trace("openfort.get", { ok: true });
2273
2398
  } catch (cause) {
2399
+ if (!isStaleUserSignal(cause)) {
2400
+ diagnostic?.trace("openfort.get", {
2401
+ ok: false,
2402
+ failure_mode: "unknown"
2403
+ });
2404
+ throw openfortProviderError("get", cause);
2405
+ }
2274
2406
  diagnostic?.trace("openfort.get", {
2275
2407
  ok: false,
2276
- failure_mode: "unknown"
2408
+ failure_mode: "stale-openfort-cache"
2277
2409
  });
2278
- throw openfortProviderError("get", cause);
2410
+ await healStaleOpenfortCache(encryptionBody.sessionId);
2279
2411
  }
2280
2412
  })();
2281
2413
  }
2282
2414
  async function ensureOpenfortWalletReady() {
2283
- walletReadyPromise ??= startWalletReady();
2415
+ let joined = walletReadyPromise;
2416
+ if (joined === null) {
2417
+ joined = startWalletReady();
2418
+ walletReadyPromise = joined;
2419
+ setStatus("recovering");
2420
+ }
2284
2421
  try {
2285
- await walletReadyPromise;
2422
+ await joined;
2423
+ if (walletReadyPromise === joined) setStatus("ready");
2286
2424
  } catch (cause) {
2287
- walletReadyPromise = null;
2425
+ if (walletReadyPromise === joined) {
2426
+ walletReadyPromise = null;
2427
+ setStatus("unavailable");
2428
+ }
2288
2429
  throw cause;
2289
2430
  }
2290
2431
  }
@@ -2294,6 +2435,7 @@ function createOpenfortBrowserSignerFromBootstrap(bootstrap, options = {}) {
2294
2435
  });
2295
2436
  return {
2296
2437
  ...signer,
2438
+ statusStore,
2297
2439
  getAddress: async () => {
2298
2440
  try {
2299
2441
  const address = await signer.getAddress();
@@ -2311,6 +2453,7 @@ function createOpenfortBrowserSignerFromBootstrap(bootstrap, options = {}) {
2311
2453
  walletReadyPromise = null;
2312
2454
  clearStaleOpenfortBrowserStorage(bootstrap.openfortPublishableKey);
2313
2455
  signer.resetAddressCache();
2456
+ setStatus("unknown");
2314
2457
  }
2315
2458
  };
2316
2459
  }
@@ -2674,16 +2817,24 @@ function refreshConvexAuthOnSession(authClient, refresh) {
2674
2817
  * particular object rather than on the auth seam itself. Here it wraps
2675
2818
  * `AuthClientPort.signOut`, so it holds for every route to a sign-out.
2676
2819
  *
2677
- * `Effect.ensuring` (not `Effect.tap`) is deliberate: the old wrapper used
2678
- * `try/finally`, so the session was reset even when sign-out failed. Dropping
2679
- * to `tap` would silently strand an Openfort session on the error path.
2820
+ * Capture the sign-out exit before reset. This preserves the old `try/finally`
2821
+ * order and keeps a reset failure in the typed error channel.
2680
2822
  */
2681
2823
  function resetSignerSessionOnSignOut(authClient, resetRef) {
2682
2824
  return {
2683
2825
  ...authClient,
2684
- signOut: (options) => authClient.signOut(options).pipe(Effect.ensuring(Effect.sync(() => {
2685
- resetRef.current?.();
2686
- })))
2826
+ signOut: (options) => Effect.gen(function* () {
2827
+ const exit = yield* Effect.exit(authClient.signOut(options));
2828
+ yield* Effect.try({
2829
+ try: () => resetRef.current?.(),
2830
+ catch: (cause) => new AuthClientError({
2831
+ operation: "signOut",
2832
+ kind: "signer",
2833
+ cause
2834
+ })
2835
+ });
2836
+ return yield* exit;
2837
+ })
2687
2838
  };
2688
2839
  }
2689
2840
  async function createCapxulClient$1(input) {
@@ -2709,14 +2860,15 @@ async function createCapxulClient$1(input) {
2709
2860
  if (signer === void 0 && runtime === "browser") signer = createOpenfortBrowserSignerFromBootstrap({
2710
2861
  ...adapters.value.bootstrap,
2711
2862
  authBaseUrl: resolvedAuthBaseUrl
2712
- }, {
2713
- diagnostic: new ConsoleDiagnosticAdapter(),
2714
- telemetry: adapters.value.ports.telemetry
2715
- });
2863
+ }, { diagnostic: new ConsoleDiagnosticAdapter() });
2716
2864
  if (signer !== void 0 && "resetSession" in signer) {
2717
- const { resetSession } = signer;
2865
+ const { resetSession, source } = signer;
2718
2866
  resetSignerSession.current = () => {
2719
- resetSession();
2867
+ try {
2868
+ resetSession();
2869
+ } catch (cause) {
2870
+ throw signerFailure(source, "resetSession", cause);
2871
+ }
2720
2872
  };
2721
2873
  }
2722
2874
  const client = assembleCapxulClient({
@@ -2734,6 +2886,7 @@ async function createCapxulClient$1(input) {
2734
2886
  ...input.signal === void 0 ? {} : { signal: input.signal },
2735
2887
  ...input.otpTtlMs === void 0 ? {} : { otpTtlMs: input.otpTtlMs },
2736
2888
  invokeTimeoutMs: input.invokeTimeoutMs ?? 3e4,
2889
+ ...input.observability?.failures === void 0 && input.observation === void 0 ? {} : { failureObservation: input.observability?.failures ?? input.observation },
2737
2890
  ...input.observability === void 0 ? {} : { hostObservationSnapshot: () => snapshotHostObservability(input.observability) },
2738
2891
  effectRunner: {
2739
2892
  runSync: Effect.runSyncWith(adapters.value.context),
@@ -2743,7 +2896,11 @@ async function createCapxulClient$1(input) {
2743
2896
  const upstreamClose = adapters.value.close;
2744
2897
  const close = idempotentClose(async () => {
2745
2898
  try {
2746
- if ("resetSession" in (signer ?? {})) signer.resetSession();
2899
+ if (signer !== void 0 && "resetSession" in signer) try {
2900
+ signer.resetSession();
2901
+ } catch (cause) {
2902
+ throw signerFailure(signer.source, "resetSession", cause);
2903
+ }
2747
2904
  } finally {
2748
2905
  await Promise.all([client._internal.close?.(), upstreamClose()]);
2749
2906
  }
@@ -2912,4 +3069,4 @@ async function createCapxulClient(input) {
2912
3069
  return createCapxulClient$1(input);
2913
3070
  }
2914
3071
  //#endregion
2915
- export { CAPXUL_PAYMENTS_V2_ADDRESS, CAPXUL_SDK_EXPECTED_OUTCOME_EVENT, CapxulError, captureException, captureExceptionSync, createCapxulClient, deriveDevPrivateKey, devPrivateKeySigner, eip1193AccountProvider, embeddedSigner, fingerprintPaymentIntent, injectedWalletSigner, isCapxulError, isSettingUpLifecycle, localPrivateKeyAccountProvider, openfortEmbeddedSigner, openfortEmbeddedSignerFromWallet, openfortEmbeddedWalletPort, postHogObservability, destination as resolveIdentityDestination, toCountryCode, toAddress as toEvmAddress };
3072
+ export { CAPXUL_ERROR_CODES, CAPXUL_PAYMENTS_V2_ADDRESS, CAPXUL_SDK_EXPECTED_OUTCOME_EVENT, CapxulError, Errors, captureException, captureExceptionSync, createCapxulClient, deriveDevPrivateKey, devPrivateKeySigner, eip1193AccountProvider, embeddedSigner, fingerprintPaymentIntent, formatMoney, injectedWalletSigner, isCapxulError, isClaimed, isMoneyParseError, isRestoring, isSettingUpLifecycle, localPrivateKeyAccountProvider, openfortEmbeddedSigner, openfortEmbeddedSignerFromWallet, openfortEmbeddedWalletPort, parseMoney, postHogObservability, destination as resolveIdentityDestination, toCountryCode, toAddress as toEvmAddress };
@@ -1,4 +1,4 @@
1
- import { E as AuthSession, _ as AuthCachePortTag, g as AuthCachePort, h as AuthCacheError, n as CapxulSigner, v as CachedJwt } from "../signer-Bj4F-RwT.mjs";
1
+ import { O as AuthSession, _ as AuthCacheError, b as CachedJwt, n as CapxulSigner, v as AuthCachePort, y as AuthCachePortTag } from "../signer-BejoR3bA.mjs";
2
2
  import { Hex } from "viem";
3
3
  import { Effect, FileSystem, Layer, Path } from "effect";
4
4
 
@@ -1,4 +1,4 @@
1
- import { a as AuthCacheError, i as parseCachedJwt, m as toAddress, n as BrowserAuthCacheAdapter, o as AuthCachePortTag, r as parseAuthSession, t as InMemoryAuthCacheAdapter } from "../InMemoryAuthCacheAdapter-Rc8tCtml.mjs";
1
+ import { a as AuthCacheError, h as toAddress, i as parseCachedJwt, n as BrowserAuthCacheAdapter, o as AuthCachePortTag, r as parseAuthSession, t as InMemoryAuthCacheAdapter } from "../InMemoryAuthCacheAdapter-qMpBOGb3.mjs";
2
2
  import { Effect, FileSystem, Layer, Path } from "effect";
3
3
  import { privateKeyToAccount } from "viem/accounts";
4
4
  import * as os from "node:os";